gazetta 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (738) hide show
  1. package/admin-dist/assets/index-CBeq0rRb.js +693 -0
  2. package/admin-dist/assets/index-Dtg1dTZQ.css +1 -0
  3. package/admin-dist/assets/rolldown-runtime-BYbx6iT9.js +1 -0
  4. package/admin-dist/assets/{vendor-primevue-C0Q_YTCb.js → vendor-primevue-CBGHkaXv.js} +183 -39
  5. package/admin-dist/assets/{vendor-react-BipDVGow.js → vendor-react-BdW_kNCG.js} +2 -2
  6. package/admin-dist/assets/vendor-rjsf-lN2SztQt.js +33 -0
  7. package/admin-dist/assets/vendor-tiptap-C36yDquB.js +141 -0
  8. package/admin-dist/assets/vendor-vue-Bt5uR1VW.js +1 -0
  9. package/admin-dist/assets/workbox-window.prod.es5-DGMtIXHc.js +2 -0
  10. package/admin-dist/index.html +8 -8
  11. package/admin-dist/sw.js +1 -0
  12. package/dist/admin-api/archived-name-conflict.d.ts +31 -0
  13. package/dist/admin-api/archived-name-conflict.d.ts.map +1 -0
  14. package/dist/admin-api/archived-name-conflict.js +226 -0
  15. package/dist/admin-api/archived-name-conflict.js.map +1 -0
  16. package/dist/admin-api/cache-stats-logger.d.ts +83 -0
  17. package/dist/admin-api/cache-stats-logger.d.ts.map +1 -0
  18. package/dist/admin-api/cache-stats-logger.js +59 -0
  19. package/dist/admin-api/cache-stats-logger.js.map +1 -0
  20. package/dist/admin-api/hook-audit-emitter.d.ts +38 -0
  21. package/dist/admin-api/hook-audit-emitter.d.ts.map +1 -0
  22. package/dist/admin-api/hook-audit-emitter.js +21 -0
  23. package/dist/admin-api/hook-audit-emitter.js.map +1 -0
  24. package/dist/admin-api/index.d.ts +84 -0
  25. package/dist/admin-api/index.d.ts.map +1 -1
  26. package/dist/admin-api/index.js +254 -9
  27. package/dist/admin-api/index.js.map +1 -1
  28. package/dist/admin-api/middleware/audit.d.ts +25 -0
  29. package/dist/admin-api/middleware/audit.d.ts.map +1 -0
  30. package/dist/admin-api/middleware/audit.js +65 -0
  31. package/dist/admin-api/middleware/audit.js.map +1 -0
  32. package/dist/admin-api/middleware/capability.d.ts +8 -0
  33. package/dist/admin-api/middleware/capability.d.ts.map +1 -0
  34. package/dist/admin-api/middleware/capability.js +65 -0
  35. package/dist/admin-api/middleware/capability.js.map +1 -0
  36. package/dist/admin-api/middleware/principal.d.ts +18 -0
  37. package/dist/admin-api/middleware/principal.d.ts.map +1 -0
  38. package/dist/admin-api/middleware/principal.js +128 -0
  39. package/dist/admin-api/middleware/principal.js.map +1 -0
  40. package/dist/admin-api/routes/archive-review.d.ts +80 -0
  41. package/dist/admin-api/routes/archive-review.d.ts.map +1 -0
  42. package/dist/admin-api/routes/archive-review.js +70 -0
  43. package/dist/admin-api/routes/archive-review.js.map +1 -0
  44. package/dist/admin-api/routes/archive.d.ts +145 -0
  45. package/dist/admin-api/routes/archive.d.ts.map +1 -0
  46. package/dist/admin-api/routes/archive.js +540 -0
  47. package/dist/admin-api/routes/archive.js.map +1 -0
  48. package/dist/admin-api/routes/assets.d.ts +6 -1
  49. package/dist/admin-api/routes/assets.d.ts.map +1 -1
  50. package/dist/admin-api/routes/assets.js +167 -14
  51. package/dist/admin-api/routes/assets.js.map +1 -1
  52. package/dist/admin-api/routes/audit.d.ts +71 -0
  53. package/dist/admin-api/routes/audit.d.ts.map +1 -0
  54. package/dist/admin-api/routes/audit.js +178 -0
  55. package/dist/admin-api/routes/audit.js.map +1 -0
  56. package/dist/admin-api/routes/compare.d.ts.map +1 -1
  57. package/dist/admin-api/routes/compare.js +3 -2
  58. package/dist/admin-api/routes/compare.js.map +1 -1
  59. package/dist/admin-api/routes/fields.d.ts.map +1 -1
  60. package/dist/admin-api/routes/fields.js +2 -1
  61. package/dist/admin-api/routes/fields.js.map +1 -1
  62. package/dist/admin-api/routes/fragments.d.ts +13 -1
  63. package/dist/admin-api/routes/fragments.d.ts.map +1 -1
  64. package/dist/admin-api/routes/fragments.js +127 -92
  65. package/dist/admin-api/routes/fragments.js.map +1 -1
  66. package/dist/admin-api/routes/health.d.ts +60 -0
  67. package/dist/admin-api/routes/health.d.ts.map +1 -0
  68. package/dist/admin-api/routes/health.js +65 -0
  69. package/dist/admin-api/routes/health.js.map +1 -0
  70. package/dist/admin-api/routes/history.d.ts +2 -1
  71. package/dist/admin-api/routes/history.d.ts.map +1 -1
  72. package/dist/admin-api/routes/history.js +26 -4
  73. package/dist/admin-api/routes/history.js.map +1 -1
  74. package/dist/admin-api/routes/pages.d.ts +20 -1
  75. package/dist/admin-api/routes/pages.d.ts.map +1 -1
  76. package/dist/admin-api/routes/pages.js +157 -117
  77. package/dist/admin-api/routes/pages.js.map +1 -1
  78. package/dist/admin-api/routes/preview.d.ts.map +1 -1
  79. package/dist/admin-api/routes/preview.js +56 -17
  80. package/dist/admin-api/routes/preview.js.map +1 -1
  81. package/dist/admin-api/routes/publish.d.ts +19 -1
  82. package/dist/admin-api/routes/publish.d.ts.map +1 -1
  83. package/dist/admin-api/routes/publish.js +508 -92
  84. package/dist/admin-api/routes/publish.js.map +1 -1
  85. package/dist/admin-api/routes/rename.d.ts +62 -0
  86. package/dist/admin-api/routes/rename.d.ts.map +1 -0
  87. package/dist/admin-api/routes/rename.js +366 -0
  88. package/dist/admin-api/routes/rename.js.map +1 -0
  89. package/dist/admin-api/routes/site.d.ts.map +1 -1
  90. package/dist/admin-api/routes/site.js +6 -18
  91. package/dist/admin-api/routes/site.js.map +1 -1
  92. package/dist/admin-api/routes/system.d.ts +23 -0
  93. package/dist/admin-api/routes/system.d.ts.map +1 -0
  94. package/dist/admin-api/routes/system.js +115 -0
  95. package/dist/admin-api/routes/system.js.map +1 -0
  96. package/dist/admin-api/routes/templates.d.ts +11 -1
  97. package/dist/admin-api/routes/templates.d.ts.map +1 -1
  98. package/dist/admin-api/routes/templates.js +36 -3
  99. package/dist/admin-api/routes/templates.js.map +1 -1
  100. package/dist/admin-api/routes/validation.d.ts +47 -0
  101. package/dist/admin-api/routes/validation.d.ts.map +1 -0
  102. package/dist/admin-api/routes/validation.js +120 -0
  103. package/dist/admin-api/routes/validation.js.map +1 -0
  104. package/dist/admin-api/schemas/archive.d.ts +124 -0
  105. package/dist/admin-api/schemas/archive.d.ts.map +1 -0
  106. package/dist/admin-api/schemas/archive.js +93 -0
  107. package/dist/admin-api/schemas/archive.js.map +1 -0
  108. package/dist/admin-api/schemas/assets.d.ts +16 -0
  109. package/dist/admin-api/schemas/assets.d.ts.map +1 -1
  110. package/dist/admin-api/schemas/assets.js +15 -0
  111. package/dist/admin-api/schemas/assets.js.map +1 -1
  112. package/dist/admin-api/schemas/audit.d.ts +175 -0
  113. package/dist/admin-api/schemas/audit.d.ts.map +1 -0
  114. package/dist/admin-api/schemas/audit.js +91 -0
  115. package/dist/admin-api/schemas/audit.js.map +1 -0
  116. package/dist/admin-api/schemas/error.d.ts +94 -0
  117. package/dist/admin-api/schemas/error.d.ts.map +1 -0
  118. package/dist/admin-api/schemas/error.js +79 -0
  119. package/dist/admin-api/schemas/error.js.map +1 -0
  120. package/dist/admin-api/schemas/fragments.d.ts +2 -0
  121. package/dist/admin-api/schemas/fragments.d.ts.map +1 -1
  122. package/dist/admin-api/schemas/fragments.js +4 -0
  123. package/dist/admin-api/schemas/fragments.js.map +1 -1
  124. package/dist/admin-api/schemas/index.d.ts +8 -0
  125. package/dist/admin-api/schemas/index.d.ts.map +1 -1
  126. package/dist/admin-api/schemas/index.js +8 -0
  127. package/dist/admin-api/schemas/index.js.map +1 -1
  128. package/dist/admin-api/schemas/pages.d.ts +2 -0
  129. package/dist/admin-api/schemas/pages.d.ts.map +1 -1
  130. package/dist/admin-api/schemas/pages.js +11 -0
  131. package/dist/admin-api/schemas/pages.js.map +1 -1
  132. package/dist/admin-api/schemas/rename.d.ts +77 -0
  133. package/dist/admin-api/schemas/rename.d.ts.map +1 -0
  134. package/dist/admin-api/schemas/rename.js +75 -0
  135. package/dist/admin-api/schemas/rename.js.map +1 -0
  136. package/dist/admin-api/schemas/site.d.ts +3 -2
  137. package/dist/admin-api/schemas/site.d.ts.map +1 -1
  138. package/dist/admin-api/schemas/site.js +3 -2
  139. package/dist/admin-api/schemas/site.js.map +1 -1
  140. package/dist/admin-api/schemas/system.d.ts +28 -0
  141. package/dist/admin-api/schemas/system.d.ts.map +1 -0
  142. package/dist/admin-api/schemas/system.js +35 -0
  143. package/dist/admin-api/schemas/system.js.map +1 -0
  144. package/dist/admin-api/schemas/targets.d.ts +55 -0
  145. package/dist/admin-api/schemas/targets.d.ts.map +1 -1
  146. package/dist/admin-api/schemas/targets.js +46 -0
  147. package/dist/admin-api/schemas/targets.js.map +1 -1
  148. package/dist/admin-api/schemas/templates.d.ts +54 -0
  149. package/dist/admin-api/schemas/templates.d.ts.map +1 -1
  150. package/dist/admin-api/schemas/templates.js +21 -0
  151. package/dist/admin-api/schemas/templates.js.map +1 -1
  152. package/dist/admin-api/schemas/validation.d.ts +101 -0
  153. package/dist/admin-api/schemas/validation.d.ts.map +1 -0
  154. package/dist/admin-api/schemas/validation.js +57 -0
  155. package/dist/admin-api/schemas/validation.js.map +1 -0
  156. package/dist/admin-api/source-context.d.ts +66 -10
  157. package/dist/admin-api/source-context.d.ts.map +1 -1
  158. package/dist/admin-api/source-context.js +43 -5
  159. package/dist/admin-api/source-context.js.map +1 -1
  160. package/dist/ai/adapter-scaffold.d.ts +63 -0
  161. package/dist/ai/adapter-scaffold.d.ts.map +1 -0
  162. package/dist/ai/adapter-scaffold.js +89 -0
  163. package/dist/ai/adapter-scaffold.js.map +1 -0
  164. package/dist/ai/compose-prompt.d.ts +50 -0
  165. package/dist/ai/compose-prompt.d.ts.map +1 -0
  166. package/dist/ai/compose-prompt.js +49 -0
  167. package/dist/ai/compose-prompt.js.map +1 -0
  168. package/dist/ai/errors.d.ts +65 -0
  169. package/dist/ai/errors.d.ts.map +1 -0
  170. package/dist/ai/errors.js +59 -0
  171. package/dist/ai/errors.js.map +1 -0
  172. package/dist/ai/index.d.ts +17 -0
  173. package/dist/ai/index.d.ts.map +1 -0
  174. package/dist/ai/index.js +16 -0
  175. package/dist/ai/index.js.map +1 -0
  176. package/dist/ai/provider.d.ts +76 -0
  177. package/dist/ai/provider.d.ts.map +1 -0
  178. package/dist/ai/provider.js +13 -0
  179. package/dist/ai/provider.js.map +1 -0
  180. package/dist/ai/refusal.d.ts +50 -0
  181. package/dist/ai/refusal.d.ts.map +1 -0
  182. package/dist/ai/refusal.js +100 -0
  183. package/dist/ai/refusal.js.map +1 -0
  184. package/dist/ai/vision-prep.d.ts +32 -0
  185. package/dist/ai/vision-prep.d.ts.map +1 -0
  186. package/dist/ai/vision-prep.js +113 -0
  187. package/dist/ai/vision-prep.js.map +1 -0
  188. package/dist/alt/adapter.d.ts +140 -0
  189. package/dist/alt/adapter.d.ts.map +1 -0
  190. package/dist/alt/adapter.js +7 -0
  191. package/dist/alt/adapter.js.map +1 -0
  192. package/dist/alt/anthropic.d.ts +63 -0
  193. package/dist/alt/anthropic.d.ts.map +1 -0
  194. package/dist/alt/anthropic.js +147 -0
  195. package/dist/alt/anthropic.js.map +1 -0
  196. package/dist/alt/config.d.ts +67 -0
  197. package/dist/alt/config.d.ts.map +1 -0
  198. package/dist/alt/config.js +41 -0
  199. package/dist/alt/config.js.map +1 -0
  200. package/dist/alt/factory.d.ts +19 -0
  201. package/dist/alt/factory.d.ts.map +1 -0
  202. package/dist/alt/factory.js +69 -0
  203. package/dist/alt/factory.js.map +1 -0
  204. package/dist/alt/null-adapter.d.ts +3 -0
  205. package/dist/alt/null-adapter.d.ts.map +1 -0
  206. package/dist/alt/null-adapter.js +43 -0
  207. package/dist/alt/null-adapter.js.map +1 -0
  208. package/dist/alt/ollama.d.ts +40 -0
  209. package/dist/alt/ollama.d.ts.map +1 -0
  210. package/dist/alt/ollama.js +139 -0
  211. package/dist/alt/ollama.js.map +1 -0
  212. package/dist/alt/openai.d.ts +46 -0
  213. package/dist/alt/openai.d.ts.map +1 -0
  214. package/dist/alt/openai.js +118 -0
  215. package/dist/alt/openai.js.map +1 -0
  216. package/dist/alt/prompt-policies.d.ts +79 -0
  217. package/dist/alt/prompt-policies.d.ts.map +1 -0
  218. package/dist/alt/prompt-policies.js +67 -0
  219. package/dist/alt/prompt-policies.js.map +1 -0
  220. package/dist/alt/route-handler.d.ts +56 -0
  221. package/dist/alt/route-handler.d.ts.map +1 -0
  222. package/dist/alt/route-handler.js +122 -0
  223. package/dist/alt/route-handler.js.map +1 -0
  224. package/dist/alt/suggester.d.ts +57 -0
  225. package/dist/alt/suggester.d.ts.map +1 -0
  226. package/dist/alt/suggester.js +133 -0
  227. package/dist/alt/suggester.js.map +1 -0
  228. package/dist/app.js +1 -1
  229. package/dist/app.js.map +1 -1
  230. package/dist/archive-aliases.d.ts +79 -0
  231. package/dist/archive-aliases.d.ts.map +1 -0
  232. package/dist/archive-aliases.js +60 -0
  233. package/dist/archive-aliases.js.map +1 -0
  234. package/dist/archive-helpers.d.ts +73 -0
  235. package/dist/archive-helpers.d.ts.map +1 -0
  236. package/dist/archive-helpers.js +94 -0
  237. package/dist/archive-helpers.js.map +1 -0
  238. package/dist/assets/find-refs.d.ts +1 -1
  239. package/dist/assets/find-refs.js +1 -1
  240. package/dist/assets/find-refs.js.map +1 -1
  241. package/dist/assets/rename.js +1 -1
  242. package/dist/assets/rename.js.map +1 -1
  243. package/dist/assets/replace.js +1 -1
  244. package/dist/assets/replace.js.map +1 -1
  245. package/dist/assets/resolve.js +4 -4
  246. package/dist/assets/resolve.js.map +1 -1
  247. package/dist/assets/serve-route.js +2 -2
  248. package/dist/assets/serve-route.js.map +1 -1
  249. package/dist/assets/validate.d.ts +1 -1
  250. package/dist/assets/validate.js +1 -1
  251. package/dist/audit/config.d.ts +75 -0
  252. package/dist/audit/config.d.ts.map +1 -0
  253. package/dist/audit/config.js +91 -0
  254. package/dist/audit/config.js.map +1 -0
  255. package/dist/audit/context.d.ts +98 -0
  256. package/dist/audit/context.d.ts.map +1 -0
  257. package/dist/audit/context.js +51 -0
  258. package/dist/audit/context.js.map +1 -0
  259. package/dist/audit/errors.d.ts +73 -0
  260. package/dist/audit/errors.d.ts.map +1 -0
  261. package/dist/audit/errors.js +78 -0
  262. package/dist/audit/errors.js.map +1 -0
  263. package/dist/audit/index.d.ts +16 -0
  264. package/dist/audit/index.d.ts.map +1 -0
  265. package/dist/audit/index.js +10 -0
  266. package/dist/audit/index.js.map +1 -0
  267. package/dist/audit/provider.d.ts +73 -0
  268. package/dist/audit/provider.d.ts.map +1 -0
  269. package/dist/audit/provider.js +2 -0
  270. package/dist/audit/provider.js.map +1 -0
  271. package/dist/audit/providers/history.d.ts +66 -0
  272. package/dist/audit/providers/history.d.ts.map +1 -0
  273. package/dist/audit/providers/history.js +102 -0
  274. package/dist/audit/providers/history.js.map +1 -0
  275. package/dist/audit/pseudonymize.d.ts +26 -0
  276. package/dist/audit/pseudonymize.d.ts.map +1 -0
  277. package/dist/audit/pseudonymize.js +86 -0
  278. package/dist/audit/pseudonymize.js.map +1 -0
  279. package/dist/audit/recorder.d.ts +102 -0
  280. package/dist/audit/recorder.d.ts.map +1 -0
  281. package/dist/audit/recorder.js +55 -0
  282. package/dist/audit/recorder.js.map +1 -0
  283. package/dist/audit/retention.d.ts +83 -0
  284. package/dist/audit/retention.d.ts.map +1 -0
  285. package/dist/audit/retention.js +142 -0
  286. package/dist/audit/retention.js.map +1 -0
  287. package/dist/audit/source-ip.d.ts +32 -0
  288. package/dist/audit/source-ip.d.ts.map +1 -0
  289. package/dist/audit/source-ip.js +164 -0
  290. package/dist/audit/source-ip.js.map +1 -0
  291. package/dist/audit/types.d.ts +143 -0
  292. package/dist/audit/types.d.ts.map +1 -0
  293. package/dist/audit/types.js +33 -0
  294. package/dist/audit/types.js.map +1 -0
  295. package/dist/audit/user-agent.d.ts +28 -0
  296. package/dist/audit/user-agent.d.ts.map +1 -0
  297. package/dist/audit/user-agent.js +63 -0
  298. package/dist/audit/user-agent.js.map +1 -0
  299. package/dist/auth/capabilities.d.ts +28 -0
  300. package/dist/auth/capabilities.d.ts.map +1 -0
  301. package/dist/auth/capabilities.js +101 -0
  302. package/dist/auth/capabilities.js.map +1 -0
  303. package/dist/auth/config.d.ts +109 -0
  304. package/dist/auth/config.d.ts.map +1 -0
  305. package/dist/auth/config.js +221 -0
  306. package/dist/auth/config.js.map +1 -0
  307. package/dist/auth/errors.d.ts +72 -0
  308. package/dist/auth/errors.d.ts.map +1 -0
  309. package/dist/auth/errors.js +78 -0
  310. package/dist/auth/errors.js.map +1 -0
  311. package/dist/auth/factory.d.ts +43 -0
  312. package/dist/auth/factory.d.ts.map +1 -0
  313. package/dist/auth/factory.js +48 -0
  314. package/dist/auth/factory.js.map +1 -0
  315. package/dist/auth/index.d.ts +21 -0
  316. package/dist/auth/index.d.ts.map +1 -0
  317. package/dist/auth/index.js +14 -0
  318. package/dist/auth/index.js.map +1 -0
  319. package/dist/auth/ip-match.d.ts +29 -0
  320. package/dist/auth/ip-match.d.ts.map +1 -0
  321. package/dist/auth/ip-match.js +162 -0
  322. package/dist/auth/ip-match.js.map +1 -0
  323. package/dist/auth/provider.d.ts +76 -0
  324. package/dist/auth/provider.d.ts.map +1 -0
  325. package/dist/auth/provider.js +2 -0
  326. package/dist/auth/provider.js.map +1 -0
  327. package/dist/auth/providers/aws-cognito.d.ts +55 -0
  328. package/dist/auth/providers/aws-cognito.d.ts.map +1 -0
  329. package/dist/auth/providers/aws-cognito.js +114 -0
  330. package/dist/auth/providers/aws-cognito.js.map +1 -0
  331. package/dist/auth/providers/azure-easy-auth.d.ts +7 -0
  332. package/dist/auth/providers/azure-easy-auth.d.ts.map +1 -0
  333. package/dist/auth/providers/azure-easy-auth.js +48 -0
  334. package/dist/auth/providers/azure-easy-auth.js.map +1 -0
  335. package/dist/auth/providers/cloudflare-access.d.ts +71 -0
  336. package/dist/auth/providers/cloudflare-access.d.ts.map +1 -0
  337. package/dist/auth/providers/cloudflare-access.js +120 -0
  338. package/dist/auth/providers/cloudflare-access.js.map +1 -0
  339. package/dist/auth/providers/forwarded-user.d.ts +31 -0
  340. package/dist/auth/providers/forwarded-user.d.ts.map +1 -0
  341. package/dist/auth/providers/forwarded-user.js +72 -0
  342. package/dist/auth/providers/forwarded-user.js.map +1 -0
  343. package/dist/auth/providers/none.d.ts +6 -0
  344. package/dist/auth/providers/none.d.ts.map +1 -0
  345. package/dist/auth/providers/none.js +19 -0
  346. package/dist/auth/providers/none.js.map +1 -0
  347. package/dist/auth/providers/tailscale.d.ts +7 -0
  348. package/dist/auth/providers/tailscale.d.ts.map +1 -0
  349. package/dist/auth/providers/tailscale.js +30 -0
  350. package/dist/auth/providers/tailscale.js.map +1 -0
  351. package/dist/auth/role-resolver.d.ts +38 -0
  352. package/dist/auth/role-resolver.d.ts.map +1 -0
  353. package/dist/auth/role-resolver.js +92 -0
  354. package/dist/auth/role-resolver.js.map +1 -0
  355. package/dist/auth/types.d.ts +150 -0
  356. package/dist/auth/types.d.ts.map +1 -0
  357. package/dist/auth/types.js +60 -0
  358. package/dist/auth/types.js.map +1 -0
  359. package/dist/cache/errors.d.ts +41 -0
  360. package/dist/cache/errors.d.ts.map +1 -0
  361. package/dist/cache/errors.js +44 -0
  362. package/dist/cache/errors.js.map +1 -0
  363. package/dist/cache/factories.d.ts +17 -0
  364. package/dist/cache/factories.d.ts.map +1 -0
  365. package/dist/cache/factories.js +17 -0
  366. package/dist/cache/factories.js.map +1 -0
  367. package/dist/cache/keys.d.ts +63 -0
  368. package/dist/cache/keys.d.ts.map +1 -0
  369. package/dist/cache/keys.js +145 -0
  370. package/dist/cache/keys.js.map +1 -0
  371. package/dist/cache/memory.d.ts +51 -0
  372. package/dist/cache/memory.d.ts.map +1 -0
  373. package/dist/cache/memory.js +204 -0
  374. package/dist/cache/memory.js.map +1 -0
  375. package/dist/cache/per-site.d.ts +22 -0
  376. package/dist/cache/per-site.d.ts.map +1 -0
  377. package/dist/cache/per-site.js +114 -0
  378. package/dist/cache/per-site.js.map +1 -0
  379. package/dist/cache/types.d.ts +142 -0
  380. package/dist/cache/types.d.ts.map +1 -0
  381. package/dist/cache/types.js +33 -0
  382. package/dist/cache/types.js.map +1 -0
  383. package/dist/cli/archive.d.ts +44 -0
  384. package/dist/cli/archive.d.ts.map +1 -0
  385. package/dist/cli/archive.js +310 -0
  386. package/dist/cli/archive.js.map +1 -0
  387. package/dist/cli/bootstrap.d.ts +15 -8
  388. package/dist/cli/bootstrap.d.ts.map +1 -1
  389. package/dist/cli/bootstrap.js +59 -23
  390. package/dist/cli/bootstrap.js.map +1 -1
  391. package/dist/cli/dev-template-watcher.d.ts +29 -0
  392. package/dist/cli/dev-template-watcher.d.ts.map +1 -0
  393. package/dist/cli/dev-template-watcher.js +38 -0
  394. package/dist/cli/dev-template-watcher.js.map +1 -0
  395. package/dist/cli/history.d.ts.map +1 -1
  396. package/dist/cli/history.js +5 -3
  397. package/dist/cli/history.js.map +1 -1
  398. package/dist/cli/index.js +712 -395
  399. package/dist/cli/index.js.map +1 -1
  400. package/dist/cli/validate-flags.d.ts +29 -0
  401. package/dist/cli/validate-flags.d.ts.map +1 -0
  402. package/dist/cli/validate-flags.js +49 -0
  403. package/dist/cli/validate-flags.js.map +1 -0
  404. package/dist/compare.d.ts +1 -1
  405. package/dist/compare.d.ts.map +1 -1
  406. package/dist/compare.js +25 -23
  407. package/dist/compare.js.map +1 -1
  408. package/dist/component-ids.d.ts +25 -0
  409. package/dist/component-ids.d.ts.map +1 -0
  410. package/dist/component-ids.js +83 -0
  411. package/dist/component-ids.js.map +1 -0
  412. package/dist/config/define.d.ts +61 -0
  413. package/dist/config/define.d.ts.map +1 -0
  414. package/dist/config/define.js +64 -0
  415. package/dist/config/define.js.map +1 -0
  416. package/dist/config/errors.d.ts +32 -0
  417. package/dist/config/errors.d.ts.map +1 -0
  418. package/dist/config/errors.js +40 -0
  419. package/dist/config/errors.js.map +1 -0
  420. package/dist/config/index.d.ts +13 -0
  421. package/dist/config/index.d.ts.map +1 -0
  422. package/dist/config/index.js +20 -0
  423. package/dist/config/index.js.map +1 -0
  424. package/dist/config/loader.d.ts +105 -0
  425. package/dist/config/loader.d.ts.map +1 -0
  426. package/dist/config/loader.js +265 -0
  427. package/dist/config/loader.js.map +1 -0
  428. package/dist/config/schemas.d.ts +89 -0
  429. package/dist/config/schemas.d.ts.map +1 -0
  430. package/dist/config/schemas.js +172 -0
  431. package/dist/config/schemas.js.map +1 -0
  432. package/dist/config/types.d.ts +32 -0
  433. package/dist/config/types.d.ts.map +1 -0
  434. package/dist/config/types.js +15 -0
  435. package/dist/config/types.js.map +1 -0
  436. package/dist/deploy/cloudflare-workers.d.ts +46 -0
  437. package/dist/deploy/cloudflare-workers.d.ts.map +1 -0
  438. package/dist/deploy/cloudflare-workers.js +213 -0
  439. package/dist/deploy/cloudflare-workers.js.map +1 -0
  440. package/dist/deploy/errors.d.ts +66 -0
  441. package/dist/deploy/errors.d.ts.map +1 -0
  442. package/dist/deploy/errors.js +82 -0
  443. package/dist/deploy/errors.js.map +1 -0
  444. package/dist/deploy/index.d.ts +9 -0
  445. package/dist/deploy/index.d.ts.map +1 -0
  446. package/dist/deploy/index.js +3 -0
  447. package/dist/deploy/index.js.map +1 -0
  448. package/dist/deploy/types.d.ts +162 -0
  449. package/dist/deploy/types.d.ts.map +1 -0
  450. package/dist/deploy/types.js +2 -0
  451. package/dist/deploy/types.js.map +1 -0
  452. package/dist/fragments/create.d.ts +70 -0
  453. package/dist/fragments/create.d.ts.map +1 -0
  454. package/dist/fragments/create.js +93 -0
  455. package/dist/fragments/create.js.map +1 -0
  456. package/dist/fragments/publish.d.ts +37 -0
  457. package/dist/fragments/publish.d.ts.map +1 -0
  458. package/dist/fragments/publish.js +52 -0
  459. package/dist/fragments/publish.js.map +1 -0
  460. package/dist/fragments/save.d.ts +81 -0
  461. package/dist/fragments/save.d.ts.map +1 -0
  462. package/dist/fragments/save.js +105 -0
  463. package/dist/fragments/save.js.map +1 -0
  464. package/dist/history-recorder.d.ts +5 -5
  465. package/dist/history-recorder.d.ts.map +1 -1
  466. package/dist/history-recorder.js +4 -4
  467. package/dist/history-recorder.js.map +1 -1
  468. package/dist/history-restorer.js +2 -2
  469. package/dist/history-restorer.js.map +1 -1
  470. package/dist/history.d.ts +1 -1
  471. package/dist/hooks/audit-emitter.d.ts +73 -0
  472. package/dist/hooks/audit-emitter.d.ts.map +1 -0
  473. package/dist/hooks/audit-emitter.js +13 -0
  474. package/dist/hooks/audit-emitter.js.map +1 -0
  475. package/dist/hooks/context.d.ts +78 -0
  476. package/dist/hooks/context.d.ts.map +1 -0
  477. package/dist/hooks/context.js +56 -0
  478. package/dist/hooks/context.js.map +1 -0
  479. package/dist/hooks/contribution.d.ts +90 -0
  480. package/dist/hooks/contribution.d.ts.map +1 -0
  481. package/dist/hooks/contribution.js +2 -0
  482. package/dist/hooks/contribution.js.map +1 -0
  483. package/dist/hooks/dispatch.d.ts +30 -0
  484. package/dist/hooks/dispatch.d.ts.map +1 -0
  485. package/dist/hooks/dispatch.js +252 -0
  486. package/dist/hooks/dispatch.js.map +1 -0
  487. package/dist/hooks/errors.d.ts +100 -0
  488. package/dist/hooks/errors.d.ts.map +1 -0
  489. package/dist/hooks/errors.js +103 -0
  490. package/dist/hooks/errors.js.map +1 -0
  491. package/dist/hooks/index.d.ts +15 -0
  492. package/dist/hooks/index.d.ts.map +1 -0
  493. package/dist/hooks/index.js +6 -0
  494. package/dist/hooks/index.js.map +1 -0
  495. package/dist/hooks/registry.d.ts +53 -0
  496. package/dist/hooks/registry.d.ts.map +1 -0
  497. package/dist/hooks/registry.js +139 -0
  498. package/dist/hooks/registry.js.map +1 -0
  499. package/dist/hooks/storage.d.ts +43 -0
  500. package/dist/hooks/storage.d.ts.map +1 -0
  501. package/dist/hooks/storage.js +2 -0
  502. package/dist/hooks/storage.js.map +1 -0
  503. package/dist/hooks/types.d.ts +324 -0
  504. package/dist/hooks/types.d.ts.map +1 -0
  505. package/dist/hooks/types.js +2 -0
  506. package/dist/hooks/types.js.map +1 -0
  507. package/dist/index.d.ts +26 -6
  508. package/dist/index.d.ts.map +1 -1
  509. package/dist/index.js +49 -5
  510. package/dist/index.js.map +1 -1
  511. package/dist/locale.d.ts +5 -1
  512. package/dist/locale.d.ts.map +1 -1
  513. package/dist/locale.js +6 -2
  514. package/dist/locale.js.map +1 -1
  515. package/dist/manifest-save.d.ts +255 -0
  516. package/dist/manifest-save.d.ts.map +1 -0
  517. package/dist/manifest-save.js +260 -0
  518. package/dist/manifest-save.js.map +1 -0
  519. package/dist/manifest.d.ts +1 -2
  520. package/dist/manifest.d.ts.map +1 -1
  521. package/dist/manifest.js +43 -44
  522. package/dist/manifest.js.map +1 -1
  523. package/dist/node-floor.d.ts +3 -0
  524. package/dist/node-floor.d.ts.map +1 -0
  525. package/dist/node-floor.js +3 -0
  526. package/dist/node-floor.js.map +1 -0
  527. package/dist/pages/create.d.ts +103 -0
  528. package/dist/pages/create.d.ts.map +1 -0
  529. package/dist/pages/create.js +117 -0
  530. package/dist/pages/create.js.map +1 -0
  531. package/dist/pages/publish.d.ts +59 -0
  532. package/dist/pages/publish.d.ts.map +1 -0
  533. package/dist/pages/publish.js +78 -0
  534. package/dist/pages/publish.js.map +1 -0
  535. package/dist/pages/save.d.ts +97 -0
  536. package/dist/pages/save.d.ts.map +1 -0
  537. package/dist/pages/save.js +138 -0
  538. package/dist/pages/save.js.map +1 -0
  539. package/dist/providers/factories.d.ts +65 -0
  540. package/dist/providers/factories.d.ts.map +1 -0
  541. package/dist/providers/factories.js +189 -0
  542. package/dist/providers/factories.js.map +1 -0
  543. package/dist/publish-item.d.ts +225 -0
  544. package/dist/publish-item.d.ts.map +1 -0
  545. package/dist/publish-item.js +210 -0
  546. package/dist/publish-item.js.map +1 -0
  547. package/dist/publish-rendered.d.ts.map +1 -1
  548. package/dist/publish-rendered.js +75 -6
  549. package/dist/publish-rendered.js.map +1 -1
  550. package/dist/publish-renderers.d.ts +132 -0
  551. package/dist/publish-renderers.d.ts.map +1 -0
  552. package/dist/publish-renderers.js +240 -0
  553. package/dist/publish-renderers.js.map +1 -0
  554. package/dist/publish-run.d.ts +223 -0
  555. package/dist/publish-run.d.ts.map +1 -0
  556. package/dist/publish-run.js +307 -0
  557. package/dist/publish-run.js.map +1 -0
  558. package/dist/publish.d.ts.map +1 -1
  559. package/dist/publish.js +1 -10
  560. package/dist/publish.js.map +1 -1
  561. package/dist/render-for-analysis.d.ts +24 -0
  562. package/dist/render-for-analysis.d.ts.map +1 -0
  563. package/dist/render-for-analysis.js +146 -0
  564. package/dist/render-for-analysis.js.map +1 -0
  565. package/dist/resolver.d.ts.map +1 -1
  566. package/dist/resolver.js +47 -23
  567. package/dist/resolver.js.map +1 -1
  568. package/dist/runtime/archive-marker.d.ts +62 -0
  569. package/dist/runtime/archive-marker.d.ts.map +1 -0
  570. package/dist/runtime/archive-marker.js +88 -0
  571. package/dist/runtime/archive-marker.js.map +1 -0
  572. package/dist/runtime/capability-gap-warnings.d.ts +42 -0
  573. package/dist/runtime/capability-gap-warnings.d.ts.map +1 -0
  574. package/dist/runtime/capability-gap-warnings.js +28 -0
  575. package/dist/runtime/capability-gap-warnings.js.map +1 -0
  576. package/dist/runtime/redirects-emit.d.ts +93 -0
  577. package/dist/runtime/redirects-emit.d.ts.map +1 -0
  578. package/dist/runtime/redirects-emit.js +89 -0
  579. package/dist/runtime/redirects-emit.js.map +1 -0
  580. package/dist/runtime/runtime-capabilities.d.ts +79 -0
  581. package/dist/runtime/runtime-capabilities.d.ts.map +1 -0
  582. package/dist/runtime/runtime-capabilities.js +60 -0
  583. package/dist/runtime/runtime-capabilities.js.map +1 -0
  584. package/dist/save-etag.d.ts +69 -0
  585. package/dist/save-etag.d.ts.map +1 -0
  586. package/dist/save-etag.js +118 -0
  587. package/dist/save-etag.js.map +1 -0
  588. package/dist/site-loader.d.ts +42 -4
  589. package/dist/site-loader.d.ts.map +1 -1
  590. package/dist/site-loader.js +27 -8
  591. package/dist/site-loader.js.map +1 -1
  592. package/dist/targets.d.ts +21 -12
  593. package/dist/targets.d.ts.map +1 -1
  594. package/dist/targets.js +27 -95
  595. package/dist/targets.js.map +1 -1
  596. package/dist/testing/admin-cache-contract.d.ts +52 -0
  597. package/dist/testing/admin-cache-contract.d.ts.map +1 -0
  598. package/dist/testing/admin-cache-contract.js +203 -0
  599. package/dist/testing/admin-cache-contract.js.map +1 -0
  600. package/dist/testing/index.d.ts +11 -0
  601. package/dist/testing/index.d.ts.map +1 -0
  602. package/dist/testing/index.js +11 -0
  603. package/dist/testing/index.js.map +1 -0
  604. package/dist/transforms/factories.d.ts +16 -0
  605. package/dist/transforms/factories.d.ts.map +1 -0
  606. package/dist/transforms/factories.js +18 -0
  607. package/dist/transforms/factories.js.map +1 -0
  608. package/dist/transforms/index.d.ts +10 -17
  609. package/dist/transforms/index.d.ts.map +1 -1
  610. package/dist/transforms/index.js +4 -28
  611. package/dist/transforms/index.js.map +1 -1
  612. package/dist/transforms/sharp.d.ts +15 -1
  613. package/dist/transforms/sharp.d.ts.map +1 -1
  614. package/dist/transforms/sharp.js +34 -20
  615. package/dist/transforms/sharp.js.map +1 -1
  616. package/dist/types.d.ts +379 -52
  617. package/dist/types.d.ts.map +1 -1
  618. package/dist/types.js +20 -1
  619. package/dist/types.js.map +1 -1
  620. package/dist/validation/alt-required-walker.d.ts +27 -0
  621. package/dist/validation/alt-required-walker.d.ts.map +1 -0
  622. package/dist/validation/alt-required-walker.js +108 -0
  623. package/dist/validation/alt-required-walker.js.map +1 -0
  624. package/dist/validation/default-registry.d.ts +12 -0
  625. package/dist/validation/default-registry.d.ts.map +1 -0
  626. package/dist/validation/default-registry.js +55 -0
  627. package/dist/validation/default-registry.js.map +1 -0
  628. package/dist/validation/publish-audit.d.ts +44 -0
  629. package/dist/validation/publish-audit.d.ts.map +1 -0
  630. package/dist/validation/publish-audit.js +64 -0
  631. package/dist/validation/publish-audit.js.map +1 -0
  632. package/dist/validation/registry.d.ts +23 -0
  633. package/dist/validation/registry.d.ts.map +1 -0
  634. package/dist/validation/registry.js +15 -0
  635. package/dist/validation/registry.js.map +1 -0
  636. package/dist/validation/save-delta.d.ts +46 -0
  637. package/dist/validation/save-delta.d.ts.map +1 -0
  638. package/dist/validation/save-delta.js +57 -0
  639. package/dist/validation/save-delta.js.map +1 -0
  640. package/dist/validation/scanner.d.ts +91 -0
  641. package/dist/validation/scanner.d.ts.map +1 -0
  642. package/dist/validation/scanner.js +327 -0
  643. package/dist/validation/scanner.js.map +1 -0
  644. package/dist/validation/template-impact.d.ts +52 -0
  645. package/dist/validation/template-impact.d.ts.map +1 -0
  646. package/dist/validation/template-impact.js +53 -0
  647. package/dist/validation/template-impact.js.map +1 -0
  648. package/dist/validation/types.d.ts +123 -0
  649. package/dist/validation/types.d.ts.map +1 -0
  650. package/dist/validation/types.js +7 -0
  651. package/dist/validation/types.js.map +1 -0
  652. package/dist/validation/validators/accessibility.d.ts +3 -0
  653. package/dist/validation/validators/accessibility.d.ts.map +1 -0
  654. package/dist/validation/validators/accessibility.js +106 -0
  655. package/dist/validation/validators/accessibility.js.map +1 -0
  656. package/dist/validation/validators/aliasof-points-to-archived.d.ts +40 -0
  657. package/dist/validation/validators/aliasof-points-to-archived.d.ts.map +1 -0
  658. package/dist/validation/validators/aliasof-points-to-archived.js +34 -0
  659. package/dist/validation/validators/aliasof-points-to-archived.js.map +1 -0
  660. package/dist/validation/validators/alt-required.d.ts +3 -0
  661. package/dist/validation/validators/alt-required.d.ts.map +1 -0
  662. package/dist/validation/validators/alt-required.js +118 -0
  663. package/dist/validation/validators/alt-required.js.map +1 -0
  664. package/dist/validation/validators/archive-not-supported-on-target.d.ts +3 -0
  665. package/dist/validation/validators/archive-not-supported-on-target.d.ts.map +1 -0
  666. package/dist/validation/validators/archive-not-supported-on-target.js +38 -0
  667. package/dist/validation/validators/archive-not-supported-on-target.js.map +1 -0
  668. package/dist/validation/validators/broken-links.d.ts +3 -0
  669. package/dist/validation/validators/broken-links.d.ts.map +1 -0
  670. package/dist/validation/validators/broken-links.js +190 -0
  671. package/dist/validation/validators/broken-links.js.map +1 -0
  672. package/dist/validation/validators/circular-alias.d.ts +36 -0
  673. package/dist/validation/validators/circular-alias.d.ts.map +1 -0
  674. package/dist/validation/validators/circular-alias.js +63 -0
  675. package/dist/validation/validators/circular-alias.js.map +1 -0
  676. package/dist/validation/validators/circular-fragment.d.ts +15 -0
  677. package/dist/validation/validators/circular-fragment.d.ts.map +1 -0
  678. package/dist/validation/validators/circular-fragment.js +97 -0
  679. package/dist/validation/validators/circular-fragment.js.map +1 -0
  680. package/dist/validation/validators/dangling-alias.d.ts +38 -0
  681. package/dist/validation/validators/dangling-alias.d.ts.map +1 -0
  682. package/dist/validation/validators/dangling-alias.js +31 -0
  683. package/dist/validation/validators/dangling-alias.js.map +1 -0
  684. package/dist/validation/validators/deploy-target-type-supported.d.ts +3 -0
  685. package/dist/validation/validators/deploy-target-type-supported.d.ts.map +1 -0
  686. package/dist/validation/validators/deploy-target-type-supported.js +32 -0
  687. package/dist/validation/validators/deploy-target-type-supported.js.map +1 -0
  688. package/dist/validation/validators/dynamic-route-conflict.d.ts +18 -0
  689. package/dist/validation/validators/dynamic-route-conflict.d.ts.map +1 -0
  690. package/dist/validation/validators/dynamic-route-conflict.js +80 -0
  691. package/dist/validation/validators/dynamic-route-conflict.js.map +1 -0
  692. package/dist/validation/validators/html-validity.d.ts +3 -0
  693. package/dist/validation/validators/html-validity.d.ts.map +1 -0
  694. package/dist/validation/validators/html-validity.js +89 -0
  695. package/dist/validation/validators/html-validity.js.map +1 -0
  696. package/dist/validation/validators/orphaned-locale-file.d.ts +21 -0
  697. package/dist/validation/validators/orphaned-locale-file.d.ts.map +1 -0
  698. package/dist/validation/validators/orphaned-locale-file.js +84 -0
  699. package/dist/validation/validators/orphaned-locale-file.js.map +1 -0
  700. package/dist/validation/validators/referenced-archived-without-alias.d.ts +3 -0
  701. package/dist/validation/validators/referenced-archived-without-alias.d.ts.map +1 -0
  702. package/dist/validation/validators/referenced-archived-without-alias.js +65 -0
  703. package/dist/validation/validators/referenced-archived-without-alias.js.map +1 -0
  704. package/dist/validation/validators/referenced-asset-exists.d.ts +13 -0
  705. package/dist/validation/validators/referenced-asset-exists.d.ts.map +1 -0
  706. package/dist/validation/validators/referenced-asset-exists.js +80 -0
  707. package/dist/validation/validators/referenced-asset-exists.js.map +1 -0
  708. package/dist/validation/validators/referenced-fragment-exists.d.ts +9 -0
  709. package/dist/validation/validators/referenced-fragment-exists.d.ts.map +1 -0
  710. package/dist/validation/validators/referenced-fragment-exists.js +52 -0
  711. package/dist/validation/validators/referenced-fragment-exists.js.map +1 -0
  712. package/dist/validation/validators/referenced-template-exists.d.ts +10 -0
  713. package/dist/validation/validators/referenced-template-exists.d.ts.map +1 -0
  714. package/dist/validation/validators/referenced-template-exists.js +74 -0
  715. package/dist/validation/validators/referenced-template-exists.js.map +1 -0
  716. package/dist/validation/validators/schema-conformance.d.ts +17 -0
  717. package/dist/validation/validators/schema-conformance.d.ts.map +1 -0
  718. package/dist/validation/validators/schema-conformance.js +94 -0
  719. package/dist/validation/validators/schema-conformance.js.map +1 -0
  720. package/dist/validation/validators/target-deploy-coverage.d.ts +3 -0
  721. package/dist/validation/validators/target-deploy-coverage.d.ts.map +1 -0
  722. package/dist/validation/validators/target-deploy-coverage.js +37 -0
  723. package/dist/validation/validators/target-deploy-coverage.js.map +1 -0
  724. package/dist/validation/validators/unused-fragment.d.ts +16 -0
  725. package/dist/validation/validators/unused-fragment.d.ts.map +1 -0
  726. package/dist/validation/validators/unused-fragment.js +86 -0
  727. package/dist/validation/validators/unused-fragment.js.map +1 -0
  728. package/package.json +54 -31
  729. package/admin-dist/assets/index-BO9-CXmW.css +0 -1
  730. package/admin-dist/assets/index-Ufu8zZH_.js +0 -668
  731. package/admin-dist/assets/rolldown-runtime-COnpUsM8.js +0 -1
  732. package/admin-dist/assets/vendor-rjsf-HKBAjOmQ.js +0 -32
  733. package/admin-dist/assets/vendor-tiptap-IyO99U4R.js +0 -142
  734. package/admin-dist/assets/vendor-vue-D3wBSmDf.js +0 -1
  735. package/dist/publish-locale.d.ts +0 -44
  736. package/dist/publish-locale.d.ts.map +0 -1
  737. package/dist/publish-locale.js +0 -103
  738. package/dist/publish-locale.js.map +0 -1
package/dist/cli/index.js CHANGED
@@ -5,14 +5,16 @@ import { serve } from '@hono/node-server';
5
5
  import { serveStatic } from '@hono/node-server/serve-static';
6
6
  import { Hono } from 'hono';
7
7
  import { streamSSE } from 'hono/streaming';
8
- import yaml from 'js-yaml';
9
8
  import { loadSite } from '../site-loader.js';
10
9
  import { resolvePage } from '../resolver.js';
11
10
  import { renderPage } from '../renderer.js';
12
11
  import { createFilesystemProvider } from '../providers/filesystem.js';
13
12
  import { invalidateTemplate, invalidateAllTemplates } from '../template-loader.js';
13
+ import { REQUIRED_NODE_FLOOR } from '../node-floor.js';
14
+ import { isTemplateEventRecent, TEMPLATE_RECENT_CHANGE_WINDOW_MS } from './dev-template-watcher.js';
14
15
  import { getEnvironment, getType, isEditable } from '../types.js';
15
- import { createAdminApp } from '../admin-api/index.js';
16
+ import { buildHooksRegistry, createAdminApp } from '../admin-api/index.js';
17
+ import { parseValidateFlags } from './validate-flags.js';
16
18
  // ANSI color helpers — no dependency, suppressed when NO_COLOR or CI
17
19
  const noColor = !!process.env.NO_COLOR || !process.stdout.isTTY;
18
20
  const c = {
@@ -27,6 +29,39 @@ const c = {
27
29
  };
28
30
  const args = process.argv.slice(2);
29
31
  const command = args[0];
32
+ /**
33
+ * Load a site manifest from `siteDir` via the TS config loader. Recognizes
34
+ * `site.config.ts`, `site.config.js`, and `site.config.mjs`.
35
+ *
36
+ * Returns null when no config file is found.
37
+ */
38
+ async function loadSiteManifestForCli(siteDir) {
39
+ const tsConfigCandidates = ['site.config.ts', 'site.config.js', 'site.config.mjs'];
40
+ if (!tsConfigCandidates.some(f => existsSync(join(siteDir, f))))
41
+ return null;
42
+ const { loadSiteConfig, siteConfigToManifest } = await import('../config/loader.js');
43
+ const loaded = await loadSiteConfig(siteDir);
44
+ if (!loaded)
45
+ return null;
46
+ return siteConfigToManifest(loaded.config);
47
+ }
48
+ /**
49
+ * Read `admin.hooks` factory contributions from a site manifest.
50
+ *
51
+ * The manifest types `admin?` as a loose record (`Record<string, unknown>`)
52
+ * to keep `SiteManifest` stable across foundation additions; each foundation
53
+ * narrow-types its own block at the consumption site. For hooks the runtime
54
+ * shape is `ReadonlyArray<HookContribution>` (per design-hooks.md
55
+ * "Registration"); we accept it as `unknown` here and cast at the boundary.
56
+ *
57
+ * Returns undefined when the field is absent or empty.
58
+ */
59
+ function readHookContributions(manifest) {
60
+ const hooks = manifest?.admin?.hooks;
61
+ if (!Array.isArray(hooks) || hooks.length === 0)
62
+ return undefined;
63
+ return hooks;
64
+ }
30
65
  // Served to /admin/* requests during dev-server startup before Vite middleware
31
66
  // is attached. Polls /admin/ping every 500ms and reloads when the admin becomes
32
67
  // reachable. See #132 and cli/index.ts for why this is needed.
@@ -122,6 +157,84 @@ function detectProjectRoot(siteDir) {
122
157
  // Fallback — use siteDir (templates/ may not exist yet)
123
158
  return siteDir;
124
159
  }
160
+ /**
161
+ * Resolve the siteDir for any CLI command by extracting the site
162
+ * positional from the parsed args per the command's documented
163
+ * positional layout. Pure fs walk; no config eval — safe to call
164
+ * before env-load.
165
+ *
166
+ * Single dispatch table so env-load happens exactly once at the top
167
+ * of `main()`, before any config-eval can read process.env.
168
+ */
169
+ async function resolveSiteDirForCommand(cmd, parsed) {
170
+ // Commands whose site positional sits at a fixed index.
171
+ if (cmd === 'build')
172
+ return resolveSiteDir(parsed.positional[0]);
173
+ if (cmd === 'dev' || cmd === 'validate' || cmd === 'admin')
174
+ return resolveSiteDir(parsed.positional[0]);
175
+ // `rollback <rev> [target|site] [site]`: site is positional[1] when
176
+ // site-shaped, else positional[2].
177
+ if (cmd === 'rollback') {
178
+ const second = parsed.positional[1];
179
+ const third = parsed.positional[2];
180
+ const secondIsSite = second && (second.includes('/') || hasSiteConfig(resolve(second)));
181
+ return resolveSiteDir(secondIsSite ? second : third);
182
+ }
183
+ // `publish|serve|deploy|history|undo [target|site] [site]`: site is
184
+ // positional[0] when site-shaped, else positional[1].
185
+ if (cmd === 'publish' || cmd === 'serve' || cmd === 'deploy' || cmd === 'history' || cmd === 'undo') {
186
+ const first = parsed.positional[0];
187
+ const firstIsSite = first && (first.includes('/') || hasSiteConfig(resolve(first)));
188
+ return resolveSiteDir(firstIsSite ? first : parsed.positional[1]);
189
+ }
190
+ // `translate <item> --to <locale> [target]` — no explicit site arg.
191
+ if (cmd === 'translate')
192
+ return resolveSiteDir(undefined);
193
+ // `assets <subcmd> [args...] [target] [site]`: layout differs by subcmd.
194
+ if (cmd === 'assets') {
195
+ const subcmd = parsed.positional[0];
196
+ const sitePositional = subcmd === 'info' ? parsed.positional[3] : parsed.positional[2];
197
+ return resolveSiteDir(sitePositional);
198
+ }
199
+ // `archive <subcmd> [args...] [target] [site]`: site at trailingStart+1.
200
+ if (cmd === 'archive') {
201
+ const subcmd = parsed.positional[0];
202
+ const requiredArgs = subcmd === 'rename' ? 2 : subcmd === 'purge' || subcmd === 'restore' ? 1 : 0;
203
+ return resolveSiteDir(parsed.positional[1 + requiredArgs + 1]);
204
+ }
205
+ // Unknown command → auto-detect (main's switch will reject with a
206
+ // clear "Unknown command" error after env-load runs).
207
+ return resolveSiteDir(undefined);
208
+ }
209
+ /**
210
+ * Load `.env` + `.env.local` from project root and site dir, in that
211
+ * order, so per-site secrets override project-wide defaults. Must run
212
+ * BEFORE the first config-eval (the site's storage / deploy / etc.
213
+ * factories read `process.env` synchronously at construction). Skipped
214
+ * in CI — CI runners inject env directly.
215
+ *
216
+ * Per Q1 of CLI env-loading discipline: site-local `.env` is honored
217
+ * (gazetta.studio has its own `.env` with R2/Cloudflare creds; this
218
+ * function ensures factories see them at config-eval time).
219
+ */
220
+ function loadEnvFiles(siteDir) {
221
+ if (process.env.CI)
222
+ return;
223
+ const projectRoot = detectProjectRoot(siteDir);
224
+ const envDirs = projectRoot !== siteDir ? [projectRoot, siteDir] : [siteDir];
225
+ for (const dir of envDirs) {
226
+ for (const name of ['.env', '.env.local']) {
227
+ const envPath = join(dir, name);
228
+ if (!existsSync(envPath))
229
+ continue;
230
+ for (const line of readFileSync(envPath, 'utf-8').split('\n')) {
231
+ const m = line.match(/^\s*([A-Z_][A-Z0-9_]*)\s*=\s*(.*)$/);
232
+ if (m && !(m[1] in process.env))
233
+ process.env[m[1]] = m[2].replace(/^["']|["']$/g, '');
234
+ }
235
+ }
236
+ }
237
+ }
125
238
  function printHelp() {
126
239
  console.log(`
127
240
  gazetta - Stateless CMS for composable websites
@@ -147,6 +260,14 @@ function printHelp() {
147
260
  Show full detail (variants, overrides, refs) for one asset
148
261
  gazetta assets reindex [target] [site]
149
262
  Rebuild the asset-refs sidecar index from manifests
263
+ gazetta archive list [--kind=page|fragment] [target] [site]
264
+ List archived items
265
+ gazetta archive purge <name> [--kind=...] [--force] [target] [site]
266
+ Permanently delete an archive
267
+ gazetta archive restore <name> [--kind=...] [target] [site]
268
+ Unarchive an item (back to live)
269
+ gazetta archive rename <oldname> <newname> [--kind=...] [--no-keep-alias] [target] [site]
270
+ Rename a live item; keeps the redirect by default
150
271
  gazetta help Show this help message
151
272
 
152
273
  Options:
@@ -160,7 +281,7 @@ function printHelp() {
160
281
  Site is auto-detected from sites/ directory. If multiple sites exist,
161
282
  you'll be prompted to choose (or pass it as an argument).
162
283
 
163
- Target is auto-detected as the first target in site.yaml. If multiple
284
+ Target is auto-detected as the first target in site.config.ts. If multiple
164
285
  targets exist, you'll be prompted to choose (or pass it as an argument).
165
286
 
166
287
  Examples:
@@ -213,24 +334,30 @@ function parseArgs(input) {
213
334
  * For commands like `publish` and `serve`, the first positional is the target
214
335
  * and the second is the site.
215
336
  */
337
+ /** Returns true if the directory contains a Gazetta site config (`site.config.ts`/.js/.mjs). */
338
+ function hasSiteConfig(dir) {
339
+ return (existsSync(join(dir, 'site.config.ts')) ||
340
+ existsSync(join(dir, 'site.config.js')) ||
341
+ existsSync(join(dir, 'site.config.mjs')));
342
+ }
216
343
  async function resolveSiteDir(positionalSite) {
217
344
  // Explicit site dir provided
218
345
  if (positionalSite) {
219
346
  const dir = resolve(positionalSite);
220
- if (existsSync(join(dir, 'site.yaml')))
347
+ if (hasSiteConfig(dir))
221
348
  return dir;
222
349
  // Maybe it's a site name under sites/
223
350
  const sitesSubdir = resolve('sites', positionalSite);
224
- if (existsSync(join(sitesSubdir, 'site.yaml')))
351
+ if (hasSiteConfig(sitesSubdir))
225
352
  return sitesSubdir;
226
353
  // Maybe it's a project root with sites/
227
354
  const mainSite = resolve(dir, 'sites/main');
228
- if (existsSync(join(mainSite, 'site.yaml')))
355
+ if (hasSiteConfig(mainSite))
229
356
  return mainSite;
230
357
  return dir; // let loadSite produce a clear error
231
358
  }
232
359
  // Auto-detect: check current dir first
233
- if (existsSync(join(resolve('.'), 'site.yaml')))
360
+ if (hasSiteConfig(resolve('.')))
234
361
  return resolve('.');
235
362
  // Check sites/ directory
236
363
  const sitesDir = resolve('sites');
@@ -238,7 +365,7 @@ async function resolveSiteDir(positionalSite) {
238
365
  const { readdirSync, statSync } = await import('node:fs');
239
366
  const sites = readdirSync(sitesDir).filter(name => {
240
367
  const dir = join(sitesDir, name);
241
- return statSync(dir).isDirectory() && existsSync(join(dir, 'site.yaml'));
368
+ return statSync(dir).isDirectory() && hasSiteConfig(dir);
242
369
  });
243
370
  if (sites.length === 1)
244
371
  return join(sitesDir, sites[0]);
@@ -270,11 +397,10 @@ async function resolveSiteDir(positionalSite) {
270
397
  async function resolveTarget(positionalTarget, siteDir) {
271
398
  if (positionalTarget)
272
399
  return positionalTarget;
273
- const siteYamlPath = join(siteDir, 'site.yaml');
274
- if (!existsSync(siteYamlPath))
400
+ const manifest = await loadSiteManifestForCli(siteDir);
401
+ if (!manifest)
275
402
  return undefined;
276
- const siteYaml = yaml.load(readFileSync(siteYamlPath, 'utf-8'));
277
- const targets = Object.keys(siteYaml.targets ?? {});
403
+ const targets = Object.keys(manifest.targets ?? {});
278
404
  if (targets.length <= 1)
279
405
  return targets[0]; // auto-select if 0 or 1
280
406
  if (process.env.CI) {
@@ -293,13 +419,27 @@ async function resolveTarget(positionalTarget, siteDir) {
293
419
  async function runInit(dir) {
294
420
  const { writeFile, mkdir } = await import('node:fs/promises');
295
421
  const target = resolve(dir);
296
- if (existsSync(join(target, 'sites')) || existsSync(join(target, 'site.yaml'))) {
422
+ if (existsSync(join(target, 'sites')) || existsSync(join(target, 'site.config.ts'))) {
297
423
  console.error(`\n Error: project already exists in ${target}\n`);
298
424
  process.exit(1);
299
425
  }
300
426
  const name = target.split('/').pop() ?? 'my-site';
301
427
  const files = {
302
- 'sites/main/site.yaml': `name: ${name}\nversion: 1.0.0\nsystemPages:\n - "404"\ntargets:\n local:\n storage:\n type: filesystem\n # environment=local, editable=true (defaults); path=./targets/local (default)\n`,
428
+ 'sites/main/site.config.ts': `import { defineSite, filesystemStorage } from 'gazetta'
429
+
430
+ export default defineSite({
431
+ name: '${name}',
432
+ version: '1.0.0',
433
+ systemPages: ['404'],
434
+ targets: {
435
+ local: {
436
+ storage: filesystemStorage(),
437
+ // environment: 'local' (default); editable: true (default for local);
438
+ // filesystemStorage() defaults path to ./targets/local
439
+ },
440
+ },
441
+ })
442
+ `,
303
443
  'templates/page-layout/index.ts': `import { z } from 'zod'
304
444
  import type { TemplateFunction } from 'gazetta'
305
445
 
@@ -419,7 +559,7 @@ export default template
419
559
  name,
420
560
  private: true,
421
561
  type: 'module',
422
- engines: { node: '>=22' },
562
+ engines: { node: REQUIRED_NODE_FLOOR },
423
563
  scripts: { dev: 'gazetta dev' },
424
564
  dependencies: { gazetta: '*', react: '^19.0.0', 'react-dom': '^19.0.0', zod: '^4.0.0' },
425
565
  }, null, 2) + '\n',
@@ -437,7 +577,7 @@ export default template
437
577
  ` ${c.dim('pages/home/')} ${c.dim('home page with hero + intro')}\n` +
438
578
  ` ${c.dim('pages/404/')} ${c.dim('error page')}\n` +
439
579
  ` ${c.dim('fragments/header/')} ${c.dim('shared header nav')}\n` +
440
- ` ${c.dim('site.yaml')} ${c.dim('site config + local target')}\n` +
580
+ ` ${c.dim('site.config.ts')} ${c.dim('site config + local target')}\n` +
441
581
  `${c.bold('package.json')}`, `Created ${c.green(name)}/`);
442
582
  // Run npm install
443
583
  const s = spinner();
@@ -456,7 +596,7 @@ export default template
456
596
  async function runPublish(siteDir, targetName, opts = {}) {
457
597
  const projectRoot = detectProjectRoot(siteDir);
458
598
  const templatesDir = join(projectRoot, 'templates');
459
- // Source comes from the default editable target in site.yaml.
599
+ // Source comes from the default editable target in site.config.ts.
460
600
  const { buildSourceContext } = await import('./bootstrap.js');
461
601
  let source, manifest, targetConfigs;
462
602
  try {
@@ -471,7 +611,7 @@ async function runPublish(siteDir, targetName, opts = {}) {
471
611
  const site = await loadSite({ contentRoot: source.contentRoot, templatesDir, manifest });
472
612
  const siteYaml = manifest;
473
613
  if (!siteYaml.targets || Object.keys(siteYaml.targets).length === 0) {
474
- console.error(`\n Error: no targets configured in site.yaml`);
614
+ console.error(`\n Error: no targets configured in site.config.ts`);
475
615
  process.exit(1);
476
616
  }
477
617
  // Determine which targets to publish to
@@ -484,19 +624,23 @@ async function runPublish(siteDir, targetName, opts = {}) {
484
624
  }
485
625
  // Initialize targets
486
626
  const { createTargetRegistry } = await import('../targets.js');
487
- const targets = await createTargetRegistry(Object.fromEntries(targetNames.map(n => [n, siteYaml.targets[n]])), siteDir);
488
- const { publishPageRendered, publishPageStatic, publishFragmentRendered, publishSiteManifest, publishDepIndices } = await import('../publish-rendered.js');
489
- const { publishPageAllLocales, publishFragmentAllLocales } = await import('../publish-locale.js');
490
- const { scanTemplates, templateHashesFrom, reportTemplateErrors } = await import('../templates-scan.js');
491
- const { hashManifest } = await import('../hash.js');
492
- // Validate + hash templates once for this publish run
627
+ const targets = await createTargetRegistry(Object.fromEntries(targetNames.map(n => [n, siteYaml.targets[n]])));
628
+ // Per Cut 6 cutover: per-target render loop now goes through publishRun
629
+ // (imported below at use site). publishRun owns: per-item dispatch +
630
+ // asset publish + dep indices + site manifest. CLI keeps: incremental
631
+ // skip via compareTargets, locale fan-out into ItemRefs, console
632
+ // output via onProgress, sitemap+robots+redirects+purge post-loop.
633
+ const { scanTemplates, reportTemplateErrors } = await import('../templates-scan.js');
634
+ // Validate templates once for this publish run. publishRun consumes
635
+ // templateInfos directly and computes hashes internally (per Cut 5
636
+ // expansion); CLI only needs to scan + bail on invalid templates
637
+ // before launching the run.
493
638
  const templateInfos = await scanTemplates(templatesDir, projectRoot);
494
639
  const invalid = reportTemplateErrors(templateInfos);
495
640
  if (invalid > 0) {
496
641
  console.error(`\n ${c.red('✗')} Refusing to publish with invalid templates.`);
497
642
  process.exit(1);
498
643
  }
499
- const templateHashes = templateHashesFrom(templateInfos);
500
644
  console.log();
501
645
  console.log(` ${c.bgGreen(c.bold(' gazetta '))} ${c.green('publish')} ${c.dim(site.manifest.name)}`);
502
646
  console.log();
@@ -504,6 +648,24 @@ async function runPublish(siteDir, targetName, opts = {}) {
504
648
  console.log(` ${c.dim('┃')} Fragments ${c.dim([...site.fragments.keys()].join(', '))}`);
505
649
  console.log(` ${c.dim('┃')} Targets ${targetNames.join(', ')}`);
506
650
  console.log();
651
+ // Cutover (Cut 6 of publish-pipeline-extraction): per-target render
652
+ // loop now delegates to `publishRun` for the per-target × per-item
653
+ // dispatch + asset publish + dep indices + site manifest. CLI keeps
654
+ // ownership of:
655
+ // - target registry init + per-target loop (so target-init failures
656
+ // can short-circuit just that target)
657
+ // - incremental skip via compareTargets (publishRun is filter-blind;
658
+ // CLI pre-filters ItemRefs[] based on per-locale unchanged sets)
659
+ // - locale fan-out (CLI expands page name → ItemRef[] including all
660
+ // locale variants supported by the target)
661
+ // - console output (onProgress callback formats human-readable lines)
662
+ // - sitemap / robots.txt / _redirects / cache purge (post-publishRun
663
+ // side effects on each target — kept inline)
664
+ //
665
+ // publishRun owns: per-item mode dispatch, archive short-circuit,
666
+ // sidecar emit, asset publish, dep indices, site manifest emit.
667
+ const { publishRun } = await import('../publish-run.js');
668
+ const { createContentRoot: _createContentRoot } = await import('../content-root.js');
507
669
  for (const name of targetNames) {
508
670
  const targetStorage = targets.get(name);
509
671
  if (!targetStorage) {
@@ -513,7 +675,6 @@ async function runPublish(siteDir, targetName, opts = {}) {
513
675
  const targetConfig = siteYaml.targets[name];
514
676
  const { getType } = await import('../types.js');
515
677
  const targetType = targetConfig ? getType(targetConfig) : 'static';
516
- const isStatic = targetType === 'static';
517
678
  console.log(` ${c.bold(name)} ${c.dim(`(${targetType})`)}`);
518
679
  let totalFiles = 0;
519
680
  let totalRemoved = 0;
@@ -535,117 +696,102 @@ async function runPublish(siteDir, targetName, opts = {}) {
535
696
  unchanged.add(item);
536
697
  }
537
698
  let skipped = 0;
538
- const sourceRoot = source.contentRoot;
539
- // Asset publish before any page render, so static-mode page HTML
540
- // doesn't bake in URLs to bytes that aren't on the target yet. Skips
541
- // assets that are already on target (content-addressed dedupe).
542
- {
543
- const { publishAssets } = await import('../assets/publish.js');
544
- const { createContentRoot } = await import('../content-root.js');
545
- const targetRoot = createContentRoot(targetStorage);
546
- const itemNames = [
547
- ...[...site.pages.keys()].map(n => `pages/${n}`),
548
- ...[...site.fragments.keys()].map(n => `fragments/${n}`),
549
- ];
550
- const assetResult = await publishAssets({ sourceRoot, targetRoot, itemNames });
551
- if (!assetResult.ok) {
552
- console.error(` ${c.red('✗')} Asset publish failed: source is missing — ${assetResult.missing.join(', ')}`);
553
- process.exit(1);
554
- }
555
- if (assetResult.copiedAssets > 0) {
556
- console.log(` ${c.green('✓')} ${assetResult.copiedAssets} asset(s), ${assetResult.copiedFiles} file(s)`);
557
- }
558
- totalFiles += assetResult.copiedFiles;
559
- }
560
- // SEO context for this target — built once, shared across all page renders.
561
- const seo = {
562
- siteName: site.manifest.name,
563
- siteUrl: targetConfig?.siteUrl,
564
- locale: site.manifest.locale,
565
- defaultOgImage: site.manifest.defaultOgImage,
566
- };
567
- if (isStatic) {
568
- // Static mode — fully assembled HTML, no fragments needed separately.
569
- // Page hash must include fragment hashes so a fragment change
570
- // invalidates every page that bakes it in (compareTargets uses the
571
- // same combination on the local side).
572
- const fragmentHashes = new Map();
573
- for (const [fragName, frag] of site.fragments) {
574
- fragmentHashes.set(fragName, hashManifest(frag, { templateHashes }));
699
+ // Build the per-target ItemRef list. CLI does the locale fan-out
700
+ // explicitly here (vs publishRun, which treats the input list
701
+ // verbatim). Each (page|fragment, locale) cell becomes one ItemRef
702
+ // unless the unchanged set covers it.
703
+ const targetLocales = targetConfig?.locales;
704
+ const itemRefs = [];
705
+ const skippedNames = new Set(); // for console output (item entirely skipped)
706
+ // Pages default + per-locale variants, filtered by target.locales when set.
707
+ for (const [pageName] of site.pages) {
708
+ const pageLocales = site.pageLocales.get(pageName);
709
+ const localesForPage = [undefined];
710
+ if (pageLocales) {
711
+ for (const loc of pageLocales.locales.keys()) {
712
+ if (targetLocales && !targetLocales.includes(loc))
713
+ continue;
714
+ localesForPage.push(loc);
715
+ }
575
716
  }
576
- for (const [pageName, page] of site.pages) {
577
- if (unchanged.has(`pages/${pageName}`)) {
578
- skipped++;
717
+ let added = 0;
718
+ for (const loc of localesForPage) {
719
+ const key = loc ? `pages/${pageName}:${loc}` : `pages/${pageName}`;
720
+ if (unchanged.has(key))
579
721
  continue;
580
- }
581
- const manifestHash = hashManifest(page, { templateHashes, fragmentHashes });
582
- const { files } = await publishPageStatic(pageName, sourceRoot, targetStorage, templatesDir, manifestHash, site, seo);
583
- totalFiles += files;
584
- console.log(` ${c.green('✓')} ${pageName}`);
722
+ itemRefs.push({ kind: 'page', name: pageName, locale: loc });
723
+ added++;
724
+ }
725
+ if (added === 0 && localesForPage.length > 0) {
726
+ skippedNames.add(`pages/${pageName}`);
727
+ skipped++;
585
728
  }
586
729
  }
587
- else {
588
- // ESI mode fragments separate, pages with placeholders
730
+ // Fragments — only emitted as separate items for non-static targets
731
+ // (static bakes them into pages). For static, the publishRun spine
732
+ // still needs them in the site for page rendering (already loaded).
733
+ if (targetType !== 'static') {
589
734
  for (const [fragName] of site.fragments) {
590
- // Build per-locale unchanged set: null = default, 'fr' = French
591
- const fragUnchanged = new Set();
592
- if (unchanged.has(`fragments/${fragName}`))
593
- fragUnchanged.add(null);
594
735
  const fragLocales = site.fragmentLocales.get(fragName);
736
+ const localesForFrag = [undefined];
595
737
  if (fragLocales) {
596
738
  for (const loc of fragLocales.locales.keys()) {
597
- if (unchanged.has(`fragments/${fragName}:${loc}`))
598
- fragUnchanged.add(loc);
739
+ if (targetLocales && !targetLocales.includes(loc))
740
+ continue;
741
+ localesForFrag.push(loc);
599
742
  }
600
743
  }
601
- // Skip entirely if all locales unchanged
602
- const totalFragLocales = 1 + (fragLocales?.locales.size ?? 0);
603
- if (fragUnchanged.size >= totalFragLocales) {
744
+ let added = 0;
745
+ for (const loc of localesForFrag) {
746
+ const key = loc ? `fragments/${fragName}:${loc}` : `fragments/${fragName}`;
747
+ if (unchanged.has(key))
748
+ continue;
749
+ itemRefs.push({ kind: 'fragment', name: fragName, locale: loc });
750
+ added++;
751
+ }
752
+ if (added === 0 && localesForFrag.length > 0) {
753
+ skippedNames.add(`fragments/${fragName}`);
604
754
  skipped++;
605
- continue;
606
755
  }
607
- const { files, removed } = await publishFragmentAllLocales(fragName, sourceRoot, targetStorage, site, { templateHashes }, { templatesDir, targetLocales: targetConfig?.locales, unchangedLocales: fragUnchanged });
608
- totalFiles += files;
609
- totalRemoved += removed;
610
- const skippedCount = fragUnchanged.size > 0 ? ` (${fragUnchanged.size} locale${fragUnchanged.size > 1 ? 's' : ''} skipped)` : '';
611
- console.log(` ${c.green('✓')} @${fragName}${skippedCount}`);
612
756
  }
613
- for (const [pageName] of site.pages) {
614
- // Build per-locale unchanged set
615
- const pageUnchanged = new Set();
616
- if (unchanged.has(`pages/${pageName}`))
617
- pageUnchanged.add(null);
618
- const pageLocales = site.pageLocales.get(pageName);
619
- if (pageLocales) {
620
- for (const loc of pageLocales.locales.keys()) {
621
- if (unchanged.has(`pages/${pageName}:${loc}`))
622
- pageUnchanged.add(loc);
757
+ }
758
+ // Delegate the per-item × per-target render loop + asset publish +
759
+ // dep indices + site manifest to publishRun. Single-target call;
760
+ // CLI loops targets itself so per-target init failures (storage
761
+ // missing) short-circuit just that target.
762
+ const runResult = await publishRun({
763
+ items: itemRefs,
764
+ targets: [name],
765
+ site,
766
+ sourceRoot: source.contentRoot,
767
+ siteManifest: manifest,
768
+ targetStorages: new Map([[name, targetStorage]]),
769
+ templateInfos,
770
+ onProgress: ev => {
771
+ if (ev.kind === 'item-done') {
772
+ if (ev.result.ok) {
773
+ const label = ev.result.kind === 'fragment' ? `@${ev.result.name}` : ev.result.name;
774
+ const localeMark = ev.result.locale ? c.dim(` (${ev.result.locale})`) : '';
775
+ console.log(` ${c.green('✓')} ${label}${localeMark}`);
776
+ }
777
+ else {
778
+ const label = ev.result.kind === 'fragment' ? `@${ev.result.name}` : ev.result.name;
779
+ console.error(` ${c.red('✗')} ${label}: ${ev.result.code}`);
623
780
  }
624
781
  }
625
- const totalPageLocales = 1 + (pageLocales?.locales.size ?? 0);
626
- if (pageUnchanged.size >= totalPageLocales) {
627
- skipped++;
628
- continue;
629
- }
630
- const { files, removed } = await publishPageAllLocales(pageName, sourceRoot, targetStorage, site, { templateHashes }, {
631
- cache: targetConfig?.cache,
632
- templatesDir,
633
- seo,
634
- targetLocales: targetConfig?.locales,
635
- unchangedLocales: pageUnchanged,
636
- });
637
- totalFiles += files;
638
- totalRemoved += removed;
639
- const skippedCount = pageUnchanged.size > 0 ? ` (${pageUnchanged.size} locale${pageUnchanged.size > 1 ? 's' : ''} skipped)` : '';
640
- console.log(` ${c.green('✓')} ${pageName}${skippedCount}`);
782
+ },
783
+ });
784
+ // Aggregate counts from publishRun's per-target result.
785
+ const tr = runResult.targets.find(t => t.name === name);
786
+ if (tr) {
787
+ totalFiles += tr.filesWritten;
788
+ totalRemoved += tr.filesRemoved;
789
+ if (tr.failed) {
790
+ console.error(` ${c.red('✗')} target failed: ${tr.failureReason ?? 'all items failed'}`);
641
791
  }
642
792
  }
643
793
  if (skipped > 0)
644
794
  console.log(` ${c.dim(`· ${skipped} unchanged (skipped)`)}`);
645
- // Site manifest + dep-sidecar indices
646
- await publishSiteManifest(sourceRoot, targetStorage, site);
647
- await publishDepIndices(sourceRoot, targetStorage, site);
648
- totalFiles += 1;
649
795
  // Sitemap + robots.txt — generated from target sidecars
650
796
  const siteUrl = targetConfig?.siteUrl;
651
797
  if (siteUrl) {
@@ -755,6 +901,36 @@ async function runPublish(siteDir, targetName, opts = {}) {
755
901
  console.log(` ${c.dim('· robots.txt')}`);
756
902
  }
757
903
  }
904
+ // Host-format redirects file (e.g. _redirects for Cloudflare/Netlify).
905
+ // Per design-soft-delete.md Q10: plain-static targets without a worker
906
+ // can't read the per-page HTML marker; this file is the host-glue
907
+ // that makes archived-page redirects work for them. Independent of
908
+ // siteUrl — the format determines whether to emit at all.
909
+ const redirectsFormat = targetConfig?.redirects?.format;
910
+ if (redirectsFormat && redirectsFormat !== 'none') {
911
+ const { emitRedirects } = await import('../runtime/redirects-emit.js');
912
+ const { allPageEntries, deriveRoute } = await import('../site-loader.js');
913
+ const archives = [];
914
+ for (const entry of allPageEntries(site)) {
915
+ if (entry.page.archived !== true)
916
+ continue;
917
+ // Locale variants share the parent's archive state in v1; the
918
+ // default-locale row is the canonical entry. Per-locale archive
919
+ // is reserved for a future cut (per `design-soft-delete.md`
920
+ // future directions).
921
+ if (entry.locale)
922
+ continue;
923
+ const from = deriveRoute(entry.name);
924
+ const to = entry.page.aliasOf ? deriveRoute(entry.page.aliasOf) : undefined;
925
+ archives.push(to !== undefined ? { from, to } : { from });
926
+ }
927
+ const result = emitRedirects(redirectsFormat, archives);
928
+ if (result && result.body !== '') {
929
+ await targetStorage.writeFile(result.filename, result.body);
930
+ totalFiles++;
931
+ console.log(` ${c.dim(`· ${result.filename}`)}`);
932
+ }
933
+ }
758
934
  const removedMsg = totalRemoved > 0 ? c.dim(` (${totalRemoved} old files cleaned)`) : '';
759
935
  console.log(`\n ${c.green('✓')} ${c.bold(name)}: ${totalFiles} files published${removedMsg}\n`);
760
936
  }
@@ -960,7 +1136,9 @@ async function runAdmin(siteDir, port) {
960
1136
  app.get('/__reload', ctx => ctx.body(null, 204));
961
1137
  const { buildSourceContext } = await import('./bootstrap.js');
962
1138
  const { source, targetConfigs } = await buildSourceContext({ projectSiteDir: siteDir });
963
- await setupProductionMode(app, source, siteDir, builtAdminDir, templatesDir, adminDir, targetConfigs);
1139
+ const manifest = await loadSiteManifestForCli(siteDir);
1140
+ const hookContributions = readHookContributions(manifest);
1141
+ await setupProductionMode(app, source, siteDir, builtAdminDir, templatesDir, adminDir, targetConfigs, hookContributions, manifest);
964
1142
  // SPA fallback for non-API admin routes
965
1143
  app.get('*', ctx => {
966
1144
  const indexPath = join(builtAdminDir, 'index.html');
@@ -968,10 +1146,10 @@ async function runAdmin(siteDir, port) {
968
1146
  return ctx.html(readFileSync(indexPath, 'utf-8'));
969
1147
  return ctx.notFound();
970
1148
  });
971
- const siteYaml = yaml.load(readFileSync(join(siteDir, 'site.yaml'), 'utf-8'));
1149
+ const siteManifest = (await loadSiteManifestForCli(siteDir)) ?? { name: 'gazetta' };
972
1150
  const server = serve({ fetch: app.fetch, port }, () => {
973
1151
  console.log();
974
- console.log(` ${c.bgGreen(c.bold(' gazetta '))} ${c.green('admin')} ${c.dim(siteYaml.name)}`);
1152
+ console.log(` ${c.bgGreen(c.bold(' gazetta '))} ${c.green('admin')} ${c.dim(siteManifest.name)}`);
975
1153
  console.log();
976
1154
  console.log(` ${c.dim('┃')} Admin ${c.cyan(`http://localhost:${port}/admin`)}`);
977
1155
  console.log();
@@ -984,25 +1162,24 @@ async function runAdmin(siteDir, port) {
984
1162
  }
985
1163
  }
986
1164
  async function runServe(siteDir, port, targetName) {
987
- const siteYamlPath = join(siteDir, 'site.yaml');
988
- if (!existsSync(siteYamlPath)) {
989
- console.error(`\n Error: ${siteYamlPath} not found\n`);
1165
+ const siteYaml = await loadSiteManifestForCli(siteDir);
1166
+ if (!siteYaml) {
1167
+ console.error(`\n Error: no site config found in ${siteDir} (looked for site.config.ts)\n`);
990
1168
  process.exit(1);
991
1169
  }
992
- const siteYaml = yaml.load(readFileSync(siteYamlPath, 'utf-8'));
993
1170
  if (!siteYaml.targets || Object.keys(siteYaml.targets).length === 0) {
994
- console.error('\n Error: no targets configured in site.yaml\n');
1171
+ console.error('\n Error: no targets configured in site config\n');
995
1172
  process.exit(1);
996
1173
  }
997
1174
  const name = targetName ?? Object.keys(siteYaml.targets)[0];
998
1175
  const config = siteYaml.targets[name];
999
1176
  if (!config) {
1000
- console.error(`\n Error: target "${name}" not found in site.yaml\n`);
1177
+ console.error(`\n Error: target "${name}" not found in site config\n`);
1001
1178
  process.exit(1);
1002
1179
  }
1003
- const { createStorageProvider } = await import('../targets.js');
1004
- // Pass targetName so filesystem path defaults to ./targets/<name> when unset.
1005
- const storage = await createStorageProvider(config.storage, siteDir, name);
1180
+ // Storage is already a constructed provider (Path X — operator-facing
1181
+ // factory ran at config-eval).
1182
+ const storage = config.storage;
1006
1183
  const { getType } = await import('../types.js');
1007
1184
  const { createServer } = await import('../serve.js');
1008
1185
  const app = createServer({ storage, type: getType(config) });
@@ -1021,16 +1198,13 @@ async function runServe(siteDir, port, targetName) {
1021
1198
  }
1022
1199
  }
1023
1200
  async function runDeploy(siteDir, targetName) {
1024
- const { execSync } = await import('node:child_process');
1025
- const { writeFile, mkdir, rm } = await import('node:fs/promises');
1026
- const siteYamlPath = join(siteDir, 'site.yaml');
1027
- if (!existsSync(siteYamlPath)) {
1028
- console.error(`\n Error: No site.yaml found at ${siteDir}\n`);
1201
+ const siteYaml = await loadSiteManifestForCli(siteDir);
1202
+ if (!siteYaml) {
1203
+ console.error(`\n Error: no site config found at ${siteDir} (looked for site.config.ts)\n`);
1029
1204
  process.exit(1);
1030
1205
  }
1031
- const siteYaml = yaml.load(readFileSync(siteYamlPath, 'utf-8'));
1032
1206
  if (!siteYaml.targets) {
1033
- console.error(`\n Error: No targets configured in site.yaml\n`);
1207
+ console.error(`\n Error: No targets configured in site config\n`);
1034
1208
  process.exit(1);
1035
1209
  }
1036
1210
  if (!targetName) {
@@ -1042,130 +1216,193 @@ async function runDeploy(siteDir, targetName) {
1042
1216
  console.error(`\n Error: Unknown target "${targetName}". Available: ${Object.keys(siteYaml.targets).join(', ')}\n`);
1043
1217
  process.exit(1);
1044
1218
  }
1045
- if (!target.worker) {
1046
- console.error(`\n Error: Target "${targetName}" has no worker config. Add to site.yaml:\n\n worker:\n type: cloudflare\n name: my-site\n`);
1219
+ if (!target.deploy) {
1220
+ console.error(`\n Error: Target "${targetName}" has no \`deploy:\` adapter configured. Add to site.config.ts:\n\n ` +
1221
+ `import { cloudflareWorkersDeploy } from 'gazetta'\n ` +
1222
+ `deploy: cloudflareWorkersDeploy({\n ` +
1223
+ ` apiToken: process.env.CLOUDFLARE_API_TOKEN!,\n ` +
1224
+ ` accountId: process.env.CLOUDFLARE_ACCOUNT_ID!,\n ` +
1225
+ ` name: '${targetName}',\n ` +
1226
+ ` bucket: '${targetName}',\n })\n\n` +
1227
+ `For container hosts (Fly.io, Cloud Run, Railway, Render), use platform-native\n` +
1228
+ `deploy tooling; see docs/container-deployment.md.\n`);
1047
1229
  process.exit(1);
1048
1230
  }
1049
- if (target.worker.type !== 'cloudflare') {
1050
- console.error(`\n Error: Unsupported worker type "${target.worker.type}". Currently only "cloudflare" is supported.\n`);
1051
- process.exit(1);
1052
- }
1053
- // Generate worker in temp dir
1054
- const workerName = target.worker.name ?? targetName;
1055
- const bucketName = target.storage.bucket ?? workerName;
1056
- const tmpDir = join(siteDir, '.gazetta-deploy');
1057
- await rm(tmpDir, { recursive: true, force: true });
1058
- await mkdir(tmpDir, { recursive: true });
1059
- // Generate wrangler.toml
1060
- let wranglerToml = `name = "${workerName}"\nmain = "index.ts"\ncompatibility_date = "2024-12-01"\nworkers_dev = true\n\n[[r2_buckets]]\nbinding = "SITE_BUCKET"\nbucket_name = "${bucketName}"\n`;
1061
- // Add custom domain route if siteUrl is configured
1062
- if (target.siteUrl) {
1063
- const url = new URL(target.siteUrl);
1064
- const hostname = url.hostname;
1065
- wranglerToml += `\n[[routes]]\npattern = "${hostname}/*"\nzone_name = "${hostname}"\n`;
1066
- }
1067
- await writeFile(join(tmpDir, 'wrangler.toml'), wranglerToml);
1068
- // Generate worker entry point
1069
- const workerCode = `import { createWorker } from 'gazetta/workers/cloudflare-r2'\nexport default createWorker()\n`;
1070
- await writeFile(join(tmpDir, 'index.ts'), workerCode);
1071
- // Generate package.json for wrangler
1072
- const pkgJson = JSON.stringify({
1073
- type: 'module',
1074
- dependencies: { gazetta: '*', hono: '*' },
1075
- });
1076
- await writeFile(join(tmpDir, 'package.json'), pkgJson);
1077
- // Install deps and deploy
1078
- console.log(` Deploying worker "${workerName}" to Cloudflare...`);
1231
+ const adapter = target.deploy;
1232
+ console.log(` ${c.cyan(`Deploying via ${adapter.name}...`)}`);
1233
+ // Build a no-op logger; v1 deploy doesn't require structured logging yet.
1234
+ const logger = {
1235
+ debug: (_o, _m) => { },
1236
+ info: (_o, _m) => { },
1237
+ warn: (o, m) => console.warn(` ${typeof o === 'string' ? o : (m ?? '')}`),
1238
+ error: (o, m) => console.error(` ${typeof o === 'string' ? o : (m ?? '')}`),
1239
+ };
1079
1240
  try {
1080
- execSync('npm install --install-links ' + resolve(import.meta.dirname, '../..'), { cwd: tmpDir, stdio: 'pipe' });
1081
- const output = execSync('npx wrangler deploy', { cwd: tmpDir, stdio: 'pipe' }).toString();
1082
- const urlMatch = output.match(/https:\/\/[^\s]+/);
1083
- console.log(` Worker deployed: ${urlMatch?.[0] ?? workerName}`);
1241
+ const result = await adapter.execute({
1242
+ target,
1243
+ targetName,
1244
+ // outputDir not used by worker-deploy adapters (worker reads R2 at request).
1245
+ // Adapters that need bytes (static-host) read storage themselves.
1246
+ outputDir: join(siteDir, 'dist', targetName),
1247
+ storage: target.storage,
1248
+ env: process.env,
1249
+ logger,
1250
+ signal: new AbortController().signal,
1251
+ });
1252
+ if (result.url)
1253
+ console.log(` ${c.green('✓')} Deployed: ${result.url}`);
1254
+ else
1255
+ console.log(` ${c.green('✓')} Deployed.`);
1084
1256
  if (target.siteUrl)
1085
1257
  console.log(` Site: ${target.siteUrl}`);
1086
1258
  }
1087
1259
  catch (err) {
1088
- const stderr = err.stderr?.toString() ?? err.message;
1089
- console.error(`\n Deploy failed: ${stderr}\n`);
1260
+ const message = err instanceof Error ? err.message : String(err);
1261
+ const adapterTag = err.adapter ?? adapter.name;
1262
+ console.error(`\n ${c.red(`Deploy failed (${adapterTag}):`)} ${message}\n`);
1090
1263
  process.exit(1);
1091
1264
  }
1092
- finally {
1093
- await rm(tmpDir, { recursive: true, force: true });
1094
- }
1095
- console.log(`\n ${c.green('✓')} Worker deployed. Now publish content:\n ${c.cyan(`gazetta publish ${targetName}`)}\n`);
1265
+ console.log(`\n ${c.green('✓')} Deploy complete. Publish content with:\n ${c.cyan(`gazetta publish ${targetName}`)}\n`);
1096
1266
  }
1097
- async function runValidate(siteDir) {
1267
+ const QUALITY_VALIDATORS = new Set(['accessibility', 'html-validity', 'broken-links']);
1268
+ async function runValidate(siteDir, rawArgs = []) {
1098
1269
  const projectRoot = detectProjectRoot(siteDir);
1099
1270
  const templatesDir = join(projectRoot, 'templates');
1271
+ const opts = parseValidateFlags(rawArgs);
1100
1272
  console.log();
1101
1273
  console.log(` ${c.bgGreen(c.bold(' gazetta '))} ${c.green('validate')} ${c.dim(siteDir)}`);
1102
1274
  console.log();
1103
- // 1. Check site.yaml + load default editable target's content
1275
+ // 1. Load site
1276
+ const configLabel = existsSync(join(siteDir, 'site.config.ts'))
1277
+ ? 'site.config.ts'
1278
+ : existsSync(join(siteDir, 'site.config.js'))
1279
+ ? 'site.config.js'
1280
+ : 'site.config.mjs';
1281
+ const { buildSourceContext } = await import('./bootstrap.js');
1104
1282
  let site;
1283
+ let source;
1105
1284
  try {
1106
- const { buildSourceContext } = await import('./bootstrap.js');
1107
- const { source, manifest } = await buildSourceContext({ projectSiteDir: siteDir });
1108
- site = await loadSite({ contentRoot: source.contentRoot, templatesDir, manifest });
1109
- console.log(` ${c.green('✓')} site.yaml ${c.dim(`— ${site.manifest.name}`)}`);
1285
+ const built = await buildSourceContext({ projectSiteDir: siteDir });
1286
+ source = built.source;
1287
+ site = await loadSite({ contentRoot: source.contentRoot, templatesDir, manifest: built.manifest });
1288
+ console.log(` ${c.green('✓')} ${configLabel} ${c.dim(`— ${site.manifest.name}`)}`);
1110
1289
  }
1111
1290
  catch (err) {
1112
- console.error(` ${c.red('✗')} site.yaml ${c.dim(`— ${err.message}`)}`);
1291
+ console.error(` ${c.red('✗')} ${configLabel} ${c.dim(`— ${err.message}`)}`);
1113
1292
  process.exit(1);
1114
1293
  }
1115
- let errors = 0;
1116
- // 2. Validate all fragments
1117
- for (const [fragName, frag] of site.fragments) {
1118
- try {
1119
- const { resolveComponent } = await import('../resolver.js');
1120
- const ctx = { site, templatesDir: site.templatesDir, visited: new Set(), path: [`@${fragName}`] };
1121
- await resolveComponent(`@${fragName}`, ctx);
1122
- const childCount = frag.components?.length ?? 0;
1123
- console.log(` ${c.green('✓')} @${fragName} ${c.dim(`(${childCount} components)`)}`);
1124
- }
1125
- catch (err) {
1126
- console.error(` ${c.red('✗')} @${fragName} ${c.dim(`— ${err.message}`)}`);
1127
- errors++;
1128
- }
1294
+ // 2. Build the registry. Skip quality validators (a11y, html-validity)
1295
+ // unless `--include-quality` they trigger rendering and add seconds
1296
+ // per page; default keeps the CLI snappy for CI ref-existence gating.
1297
+ const { defaultValidatorRegistry } = await import('../validation/default-registry.js');
1298
+ const { createValidatorRegistry } = await import('../validation/registry.js');
1299
+ const fullRegistry = defaultValidatorRegistry();
1300
+ const filtered = opts.includeQuality
1301
+ ? fullRegistry
1302
+ : createValidatorRegistry(fullRegistry.all().filter(v => !QUALITY_VALIDATORS.has(v.name)));
1303
+ // 3. Run via the scanner — same orchestrator as the admin background scan,
1304
+ // so the CLI exercises identical code paths.
1305
+ const { createValidationScanner } = await import('../validation/scanner.js');
1306
+ const { createMemoryCache } = await import('../cache/memory.js');
1307
+ const scanner = createValidationScanner({
1308
+ storage: source.storage,
1309
+ contentRoot: source.contentRoot,
1310
+ registry: filtered,
1311
+ cache: createMemoryCache(),
1312
+ siteOptions: { templatesDir, manifest: site.manifest },
1313
+ loadSiteImpl: async () => site,
1314
+ });
1315
+ await scanner.scanAll();
1316
+ const allIssues = scanner.allIssues();
1317
+ // 4. Per-item summary. Pages first, then fragments — matches the existing
1318
+ // output ordering for unsurprising diff vs. the prior implementation.
1319
+ let errorCount = 0;
1320
+ let warnCount = 0;
1321
+ let infoCount = 0;
1322
+ const issuesByPath = new Map();
1323
+ for (const issue of allIssues) {
1324
+ const list = issuesByPath.get(issue.itemPath) ?? [];
1325
+ list.push(issue);
1326
+ issuesByPath.set(issue.itemPath, list);
1327
+ if (issue.severity === 'error')
1328
+ errorCount++;
1329
+ else if (issue.severity === 'warn')
1330
+ warnCount++;
1331
+ else
1332
+ infoCount++;
1333
+ }
1334
+ function summaryGlyph(issues) {
1335
+ if (issues.some(i => i.severity === 'error'))
1336
+ return { glyph: '✗', color: c.red };
1337
+ if (issues.some(i => i.severity === 'warn'))
1338
+ return { glyph: '⚠', color: c.yellow };
1339
+ if (issues.length > 0)
1340
+ return { glyph: 'ⓘ', color: c.cyan };
1341
+ return { glyph: '✓', color: c.green };
1342
+ }
1343
+ function shouldShow(severity) {
1344
+ if (opts.severity === 'all')
1345
+ return true;
1346
+ if (opts.severity === 'warn')
1347
+ return severity !== 'info';
1348
+ return severity === 'error';
1129
1349
  }
1130
- // 3. Validate all pages
1131
1350
  for (const [pageName, page] of site.pages) {
1132
- try {
1133
- await resolvePage(pageName, site);
1134
- const componentCount = page.components?.length ?? 0;
1135
- const fragmentCount = page.components?.filter(cc => typeof cc === 'string' && cc.startsWith('@')).length ?? 0;
1136
- console.log(` ${c.green('✓')} ${pageName} ${c.dim(`(${componentCount} components, ${fragmentCount} fragments)`)}`);
1351
+ const path = `${page.dir}/page.json`;
1352
+ const issues = issuesByPath.get(path) ?? [];
1353
+ const visible = issues.filter(i => shouldShow(i.severity));
1354
+ const { glyph, color } = summaryGlyph(visible);
1355
+ const componentCount = page.components?.length ?? 0;
1356
+ console.log(` ${color(glyph)} ${pageName} ${c.dim(`(${componentCount} components)`)}`);
1357
+ if (opts.verbose) {
1358
+ for (const issue of visible) {
1359
+ console.log(` ${severityIcon(issue.severity)} ${issue.message}`);
1360
+ }
1137
1361
  }
1138
- catch (err) {
1139
- console.error(` ${c.red('✗')} ${pageName} ${c.dim(`— ${err.message}`)}`);
1140
- errors++;
1362
+ }
1363
+ for (const [fragName, frag] of site.fragments) {
1364
+ const path = `${frag.dir}/fragment.json`;
1365
+ const issues = issuesByPath.get(path) ?? [];
1366
+ const visible = issues.filter(i => shouldShow(i.severity));
1367
+ const { glyph, color } = summaryGlyph(visible);
1368
+ const childCount = frag.components?.length ?? 0;
1369
+ console.log(` ${color(glyph)} @${fragName} ${c.dim(`(${childCount} components)`)}`);
1370
+ if (opts.verbose) {
1371
+ for (const issue of visible) {
1372
+ console.log(` ${severityIcon(issue.severity)} ${issue.message}`);
1373
+ }
1141
1374
  }
1142
1375
  }
1143
- // 4. List templates (project-level filesystem, not target content)
1376
+ // 5. Project-structure checks (orphaned editors, missing custom fields).
1377
+ // These are project-layout concerns rather than per-item content rules,
1378
+ // so they don't fit the Validator interface — kept inline here.
1379
+ const adminDir = join(projectRoot, 'admin');
1144
1380
  const projectStorage = createFilesystemProvider();
1145
1381
  let templateNames = [];
1146
1382
  try {
1147
1383
  const entries = await projectStorage.readDir(templatesDir);
1148
- templateNames = entries.filter(e => e.isDirectory).map((e) => e.name);
1149
- console.log(` ${c.green('✓')} ${c.dim(`${templateNames.length} templates`)}`);
1384
+ templateNames = entries.filter(e => e.isDirectory).map(e => e.name);
1150
1385
  }
1151
1386
  catch {
1152
- console.log(` ${c.yellow('⚠')} ${c.dim('templates/ directory not found')}`);
1387
+ /* templates dir missing — site already errored above */
1153
1388
  }
1154
- // 5. Check for orphaned editors (editor exists but template doesn't)
1155
- const adminDir = join(projectRoot, 'admin');
1389
+ // 5a. Orphaned editors: editor file exists but no matching template.
1390
+ // Always shown regardless of --severity since these are structural
1391
+ // issues operators need to know about even at the strictest filter.
1156
1392
  const editorsDir = join(adminDir, 'editors');
1157
1393
  if (existsSync(editorsDir)) {
1158
- const editorFiles = (await import('node:fs'))
1159
- .readdirSync(editorsDir)
1160
- .filter(f => f.endsWith('.ts') || f.endsWith('.tsx'));
1394
+ const fs = await import('node:fs');
1395
+ const editorFiles = fs.readdirSync(editorsDir).filter(f => f.endsWith('.ts') || f.endsWith('.tsx'));
1161
1396
  for (const file of editorFiles) {
1162
1397
  const editorName = file.replace(/\.(ts|tsx)$/, '');
1163
1398
  if (!templateNames.includes(editorName)) {
1164
1399
  console.log(` ${c.yellow('⚠')} orphaned editor: ${c.dim(`admin/editors/${file}`)} ${c.dim('— no matching template')}`);
1400
+ warnCount++;
1165
1401
  }
1166
1402
  }
1167
1403
  }
1168
- // 6. Check for missing custom fields (schema references field but file doesn't exist)
1404
+ // 5b. Missing custom fields: schema references field: 'name' but no
1405
+ // admin/fields/name.{ts,tsx}. Hard error — render fails without it.
1169
1406
  const fieldsDir = join(adminDir, 'fields');
1170
1407
  const fieldFiles = existsSync(fieldsDir)
1171
1408
  ? (await import('node:fs'))
@@ -1173,96 +1410,60 @@ async function runValidate(siteDir) {
1173
1410
  .filter(f => f.endsWith('.ts') || f.endsWith('.tsx'))
1174
1411
  .map(f => f.replace(/\.(ts|tsx)$/, ''))
1175
1412
  : [];
1176
- const { loadTemplate } = await import('../template-loader.js');
1177
- const zod = await import('zod');
1178
- for (const tplName of templateNames) {
1179
- try {
1180
- const loaded = await loadTemplate(projectStorage, templatesDir, tplName);
1181
- const jsonSchema = zod.z.toJSONSchema(loaded.schema);
1182
- const props = jsonSchema.properties;
1183
- if (!props)
1184
- continue;
1185
- for (const [propName, prop] of Object.entries(props)) {
1186
- const fieldRef = prop.field;
1187
- if (fieldRef && !fieldFiles.includes(fieldRef)) {
1188
- console.error(` ${c.red('✗')} template ${tplName}.${propName} references field "${fieldRef}" ${c.dim('— not found in admin/fields/')}`);
1189
- errors++;
1190
- }
1191
- }
1192
- }
1193
- catch {
1194
- /* template load errors already caught above */
1195
- }
1196
- }
1197
- // 7. Locale validation
1198
- const { defaultLocaleFor } = await import('../locale.js');
1199
- const defLoc = defaultLocaleFor(site.manifest);
1200
- const hasI18n = !!site.manifest.locales?.supported?.length;
1201
- // 7a. Warn about orphaned locale files when i18n is disabled
1202
- if (!hasI18n && (site.pageLocales.size > 0 || site.fragmentLocales.size > 0)) {
1203
- const orphanCount = site.pageLocales.size + site.fragmentLocales.size;
1204
- console.log(` ${c.yellow('⚠')} ${orphanCount} locale file${orphanCount > 1 ? 's' : ''} found but i18n is disabled ${c.dim('— add locales.supported to site.yaml or remove *.locale.json files')}`);
1205
- }
1206
- // 7b. Warn about ambiguous page.en.json when en is default
1207
- if (hasI18n) {
1208
- for (const [name, entry] of site.pageLocales) {
1209
- if (entry.locales.has(defLoc)) {
1210
- console.log(` ${c.yellow('⚠')} page.${defLoc}.json in ${name} is ambiguous ${c.dim(`— "${defLoc}" is the default locale, use page.json instead`)}`);
1211
- }
1212
- }
1213
- for (const [name, entry] of site.fragmentLocales) {
1214
- if (entry.locales.has(defLoc)) {
1215
- console.log(` ${c.yellow('⚠')} fragment.${defLoc}.json in ${name} is ambiguous ${c.dim(`— "${defLoc}" is the default locale, use fragment.json instead`)}`);
1216
- }
1217
- }
1218
- }
1219
- // 7c. Validate locale variant template/fragment refs
1220
- if (hasI18n) {
1221
- for (const [pageName, entry] of site.pageLocales) {
1222
- for (const [locale] of entry.locales) {
1223
- try {
1224
- await resolvePage(pageName, site, locale);
1225
- }
1226
- catch (err) {
1227
- console.error(` ${c.red('✗')} ${pageName} (${locale}) ${c.dim(`— ${err.message}`)}`);
1228
- errors++;
1229
- }
1230
- }
1231
- }
1232
- }
1233
- // 8. Cross-domain hreflang bidirectional check
1234
- // For per-domain targets (each with siteUrl + single locale), verify that
1235
- // all targets serving the same page cross-link to each other.
1236
- if (hasI18n && site.manifest.targets) {
1237
- const targetsWithSiteUrl = Object.entries(site.manifest.targets).filter(([, cfg]) => cfg.siteUrl && cfg.locales?.length === 1);
1238
- if (targetsWithSiteUrl.length > 1) {
1239
- const localeToUrl = new Map();
1240
- for (const [, cfg] of targetsWithSiteUrl) {
1241
- localeToUrl.set(cfg.locales[0], cfg.siteUrl);
1242
- }
1243
- const missingPairs = [];
1244
- for (const [locA, urlA] of localeToUrl) {
1245
- for (const [locB, urlB] of localeToUrl) {
1246
- if (locA === locB)
1247
- continue;
1248
- // Each target's sitemap should cross-link to the other
1249
- // We can't check the actual sitemaps here (would need network),
1250
- // but we can verify the config is consistent
1413
+ if (templateNames.length > 0) {
1414
+ const { loadTemplate } = await import('../template-loader.js');
1415
+ const zod = await import('zod');
1416
+ for (const tplName of templateNames) {
1417
+ try {
1418
+ const loaded = await loadTemplate(projectStorage, templatesDir, tplName);
1419
+ const jsonSchema = zod.z.toJSONSchema(loaded.schema);
1420
+ const props = jsonSchema.properties;
1421
+ if (!props)
1422
+ continue;
1423
+ for (const [propName, prop] of Object.entries(props)) {
1424
+ const fieldRef = prop.field;
1425
+ if (fieldRef && !fieldFiles.includes(fieldRef)) {
1426
+ console.error(` ${c.red('✗')} template ${tplName}.${propName} references field "${fieldRef}" ${c.dim('— not found in admin/fields/')}`);
1427
+ errorCount++;
1428
+ }
1251
1429
  }
1252
1430
  }
1253
- if (localeToUrl.size > 1) {
1254
- console.log(` ${c.green('✓')} cross-domain hreflang: ${[...localeToUrl.entries()].map(([l, u]) => `${l} → ${u}`).join(', ')}`);
1431
+ catch {
1432
+ /* template load errors surface via referenced-template-exists */
1255
1433
  }
1256
1434
  }
1257
1435
  }
1436
+ // 6. Footer + exit code.
1258
1437
  console.log();
1259
- if (errors > 0) {
1260
- console.error(` ${errors} error${errors > 1 ? 's' : ''} found.\n`);
1261
- process.exit(1);
1262
- }
1263
- else {
1264
- console.log(` All good.\n`);
1438
+ const totalShown = opts.severity === 'all'
1439
+ ? errorCount + warnCount + infoCount
1440
+ : opts.severity === 'warn'
1441
+ ? errorCount + warnCount
1442
+ : errorCount;
1443
+ if (totalShown === 0) {
1444
+ console.log(` ${c.green('All good.')}\n`);
1445
+ return;
1265
1446
  }
1447
+ const parts = [];
1448
+ if (errorCount > 0)
1449
+ parts.push(`${errorCount} error${errorCount > 1 ? 's' : ''}`);
1450
+ if (warnCount > 0 && opts.severity !== 'error')
1451
+ parts.push(`${warnCount} warning${warnCount > 1 ? 's' : ''}`);
1452
+ if (infoCount > 0 && opts.severity === 'all')
1453
+ parts.push(`${infoCount} info`);
1454
+ const summary = parts.join(', ');
1455
+ console.log(` ${summary}.\n`);
1456
+ // Exit non-zero if errors OR (warns AND warn-as-error is on).
1457
+ const fail = errorCount > 0 || (opts.warnAsError && warnCount > 0);
1458
+ if (fail)
1459
+ process.exit(1);
1460
+ }
1461
+ function severityIcon(severity) {
1462
+ if (severity === 'error')
1463
+ return c.red('✗');
1464
+ if (severity === 'warn')
1465
+ return c.yellow('⚠');
1466
+ return c.cyan('ⓘ');
1266
1467
  }
1267
1468
  function renderErrorOverlay(err) {
1268
1469
  const message = err.message.replace(/</g, '&lt;').replace(/>/g, '&gt;');
@@ -1313,7 +1514,7 @@ async function runDev(siteDir, port) {
1313
1514
  const projectRoot = detectProjectRoot(siteDir);
1314
1515
  const templatesDir = join(projectRoot, 'templates');
1315
1516
  const adminDir = join(projectRoot, 'admin');
1316
- // Build the source context from the default editable target in site.yaml.
1517
+ // Build the source context from the default editable target in site.config.ts.
1317
1518
  // Cloud targets aren't init'd — admin API handles them lazily.
1318
1519
  const { buildSourceContext } = await import('./bootstrap.js');
1319
1520
  const { source, manifest, targetConfigs } = await buildSourceContext({ projectSiteDir: siteDir });
@@ -1368,6 +1569,7 @@ async function runDev(siteDir, port) {
1368
1569
  app.route('/', assetServeRoutes(async () => source.storage));
1369
1570
  // ---- Site page routes (default + locale variants) ----
1370
1571
  const { allPageEntries } = await import('../site-loader.js');
1572
+ const { defaultLocaleFor: _devDefaultLocaleFor } = await import('../locale.js');
1371
1573
  for (const { name: pageName, page, locale: pageLocale } of allPageEntries(site)) {
1372
1574
  app.get(page.route, async (c) => {
1373
1575
  try {
@@ -1382,7 +1584,7 @@ async function runDev(siteDir, port) {
1382
1584
  route: freshPage?.route ?? page.route,
1383
1585
  seo: {
1384
1586
  siteName: freshSite.manifest.name,
1385
- locale: pageLocale ?? freshSite.manifest.locale,
1587
+ locale: pageLocale ?? _devDefaultLocaleFor(freshSite.manifest),
1386
1588
  defaultOgImage: freshSite.manifest.defaultOgImage,
1387
1589
  },
1388
1590
  });
@@ -1437,14 +1639,17 @@ async function runDev(siteDir, port) {
1437
1639
  const isDevMode = cmsWebDir !== null;
1438
1640
  // Admin Hono instance — captured so the template file watcher can
1439
1641
  // invalidate its memoized template-scan cache on .ts/.tsx changes.
1642
+ // `rescanForTemplate` is optional: only the dev-mode setup decorates
1643
+ // it (production has no file watcher). Watcher uses optional-chaining.
1440
1644
  let cmsApp = null;
1645
+ const hookContributions = readHookContributions(manifest);
1441
1646
  if (isDevMode) {
1442
1647
  // Dev mode: mount CMS API inline (same process = shared template cache)
1443
- cmsApp = await setupCmsApi(app, source, siteDir, templatesDir, adminDir, targetConfigs);
1648
+ cmsApp = await setupCmsApi(app, source, siteDir, templatesDir, adminDir, targetConfigs, hookContributions, manifest);
1444
1649
  }
1445
1650
  else if (cmsStaticDir) {
1446
1651
  // Production mode: inline CMS API + static files
1447
- cmsApp = await setupProductionMode(app, source, siteDir, cmsStaticDir, templatesDir, adminDir, targetConfigs);
1652
+ cmsApp = await setupProductionMode(app, source, siteDir, cmsStaticDir, templatesDir, adminDir, targetConfigs, hookContributions, manifest);
1448
1653
  }
1449
1654
  // ---- 404 ----
1450
1655
  app.notFound(c => {
@@ -1493,8 +1698,10 @@ async function runDev(siteDir, port) {
1493
1698
  const env = getEnvironment(cfg);
1494
1699
  const type = getType(cfg);
1495
1700
  const ed = isEditable(cfg) ? 'editable ' : 'read-only';
1496
- const storagePath = cfg.storage?.type === 'filesystem' ? (cfg.storage.path ?? `targets/${name}`) : `${cfg.storage?.type ?? '?'}`;
1497
- console.log(` ${c.dim('┃')} ${c.dim('•')} ${name.padEnd(14)} ${c.dim(env.padEnd(11))} ${c.dim(ed)} ${c.dim(type.padEnd(8))} ${c.dim('→ ' + storagePath)}`);
1701
+ // Path X — the storage provider is opaque (operator constructed it via
1702
+ // factory at config-eval). Display target name + axes; the provider
1703
+ // identity is not introspectable from the StorageProvider interface.
1704
+ console.log(` ${c.dim('┃')} ${c.dim('•')} ${name.padEnd(14)} ${c.dim(env.padEnd(11))} ${c.dim(ed)} ${c.dim(type.padEnd(8))}`);
1498
1705
  }
1499
1706
  }
1500
1707
  if (isDevMode && cmsWebDir) {
@@ -1580,7 +1787,15 @@ async function runDev(siteDir, port) {
1580
1787
  // written into the source, so the scanner misses it. Include it
1581
1788
  // explicitly to avoid a mid-session page reload when a TSX editor
1582
1789
  // is first loaded (#122).
1583
- include: ['react/jsx-dev-runtime', 'react/jsx-runtime'],
1790
+ //
1791
+ // @formkit/drag-and-drop/vue is the ComponentTree's reorder lib
1792
+ // (#105). The package's main entry is CJS; the Vue subpath is
1793
+ // ESM. Vite's auto-scanner doesn't always pre-bundle subpath
1794
+ // ESM imports with mixed CJS-main packages on first load,
1795
+ // which triggers a runtime "Re-optimizing dependencies"
1796
+ // page reload mid-test on CI cold start. Explicit include
1797
+ // pins the subpath in the boot-time pre-bundle.
1798
+ include: ['react/jsx-dev-runtime', 'react/jsx-runtime', '@formkit/drag-and-drop/vue'],
1584
1799
  },
1585
1800
  server: {
1586
1801
  middlewareMode: true,
@@ -1646,7 +1861,7 @@ async function runDev(siteDir, port) {
1646
1861
  console.log();
1647
1862
  });
1648
1863
  // ---- File watching ----
1649
- // Watch site dir for content changes (JSON manifests + site.yaml config).
1864
+ // Watch site dir for content changes (JSON manifests + site.config.ts).
1650
1865
  // Swallow FSWatcher 'error' events — Node's recursive watcher throws ENOENT
1651
1866
  // when a watched subdir disappears (rm -rf during publish, git checkout).
1652
1867
  // Letting it crash would take the whole dev server down; logging a warning
@@ -1665,6 +1880,12 @@ async function runDev(siteDir, port) {
1665
1880
  if (filename.endsWith('.json') || filename.endsWith('.yaml')) {
1666
1881
  console.log(` Manifest changed: ${filename}`);
1667
1882
  invalidateAllTemplates();
1883
+ // Out-of-band manifest changes (git pull, manual edit, e2e
1884
+ // test wipes) bypass the admin-api save handler that would
1885
+ // otherwise invalidate the AdminCache. Drop content-summary
1886
+ // entries so the next /api/pages or /api/fragments rebuilds
1887
+ // from disk. Best-effort; the void promise is fire-and-forget.
1888
+ void cmsApp?.invalidateContentCache();
1668
1889
  notifyReload();
1669
1890
  }
1670
1891
  });
@@ -1677,12 +1898,34 @@ async function runDev(siteDir, port) {
1677
1898
  if (filename.endsWith('.ts') || filename.endsWith('.tsx')) {
1678
1899
  const parts = filename.split('/');
1679
1900
  if (parts.length >= 1) {
1680
- console.log(` Template changed: ${parts[0]}`);
1681
- invalidateTemplate(parts[0]);
1682
- // Drop the admin-api's cached scan so next compare/publish
1683
- // rehashes. Cheap (the scan is what's slow, not invalidation).
1684
- cmsApp?.invalidateTemplatesCache();
1685
- notifyReload();
1901
+ const templateName = parts[0];
1902
+ const fullPath = join(templatesDir, filename);
1903
+ // Suppress spurious events for files whose mtime predates the
1904
+ // watcher's recent window these are kernel-delayed metadata
1905
+ // flushes for files written before the watcher began observing,
1906
+ // typical of the worker-scoped `cp -r` setup in the e2e fixture
1907
+ // (#286). Without the gate, the resulting notifyReload() closes
1908
+ // any open admin UI mid-interaction.
1909
+ void (async () => {
1910
+ if (!(await isTemplateEventRecent(fullPath, TEMPLATE_RECENT_CHANGE_WINDOW_MS))) {
1911
+ return;
1912
+ }
1913
+ console.log(` Template changed: ${templateName}`);
1914
+ invalidateTemplate(templateName);
1915
+ // Drop the admin-api's cached scan so next compare/publish
1916
+ // rehashes. Cheap (the scan is what's slow, not invalidation).
1917
+ cmsApp?.invalidateTemplatesCache();
1918
+ // Cut 6 — fire a validation rescan with the template-edit
1919
+ // cause so the scanner re-runs schema-conformance against
1920
+ // every page+fragment using this template. The ScanEvent
1921
+ // emits on the /__validation SSE channel; the admin's
1922
+ // TemplateChangedBanner consumes it to show the
1923
+ // template-developer's "did I break anything?" surface.
1924
+ // Fire-and-forget — the scan runs in the background and
1925
+ // the SSE event is the signal that it finished.
1926
+ void cmsApp?.rescanForTemplate?.(templateName);
1927
+ notifyReload();
1928
+ })();
1686
1929
  }
1687
1930
  }
1688
1931
  });
@@ -1711,16 +1954,98 @@ function mountUserThemeRoute(cmsApp, adminDir) {
1711
1954
  return c.body(readFileSync(themePath, 'utf-8'));
1712
1955
  });
1713
1956
  }
1714
- async function setupCmsApi(app, source, siteDir, templatesDir, adminDir, targetConfigs) {
1715
- const cmsApp = createAdminApp({ source, siteDir, templatesDir, adminDir, targetConfigs });
1957
+ /**
1958
+ * Build the background validation scanner (validation Cut 2). The scanner
1959
+ * is constructed against the resolved source's storage + cache; the boot
1960
+ * path kicks off an initial full-site scan in the background so admin
1961
+ * responses don't block on it.
1962
+ *
1963
+ * Returns null when the manifest is unavailable (site lacks site.config.ts)
1964
+ * — the admin app degrades gracefully (route returns empty issues; SSE
1965
+ * channel idle).
1966
+ */
1967
+ async function buildValidationScanner(opts) {
1968
+ if (!opts.manifest)
1969
+ return null;
1970
+ const { createValidationScanner } = await import('../validation/scanner.js');
1971
+ const { defaultValidatorRegistry } = await import('../validation/default-registry.js');
1972
+ const scanner = createValidationScanner({
1973
+ storage: opts.source.storage,
1974
+ contentRoot: opts.source.contentRoot,
1975
+ registry: defaultValidatorRegistry(),
1976
+ cache: opts.source.cache,
1977
+ siteOptions: { templatesDir: opts.templatesDir, manifest: opts.manifest },
1978
+ });
1979
+ // Boot warm: kick off the initial scan in the background. Errors are
1980
+ // logged but don't block boot — broken validators surface as info-issues
1981
+ // rather than failing the admin process.
1982
+ void scanner.scanAll().catch(err => {
1983
+ console.error('[validation] initial scan failed:', err);
1984
+ });
1985
+ return scanner;
1986
+ }
1987
+ async function setupCmsApi(app, source, siteDir, templatesDir, adminDir, targetConfigs, contributions, manifest) {
1988
+ // Build + seal the hook registry from `admin.hooks` factory
1989
+ // contributions before wiring the admin app. Hooks are an opt-in
1990
+ // extension surface; sites without `admin.hooks` get an empty
1991
+ // registry (no overhead).
1992
+ const hooks = await buildHooksRegistry({ contributions });
1993
+ const validationScanner = await buildValidationScanner({ source, templatesDir, manifest });
1994
+ // Mount SSE on the OUTER Hono app — matches `/__reload`'s placement so
1995
+ // the browser EventSource (which connects without an `/admin/` prefix)
1996
+ // bypasses Vite's middleware and reaches the route. Same in prod for
1997
+ // consistency: a save from one tab updates the badge in every other
1998
+ // tab without polling.
1999
+ const { mountValidationSse } = await import('../admin-api/routes/validation.js');
2000
+ mountValidationSse(app, validationScanner);
2001
+ const cmsApp = createAdminApp({
2002
+ source,
2003
+ siteDir,
2004
+ templatesDir,
2005
+ adminDir,
2006
+ targetConfigs,
2007
+ hooks,
2008
+ validationScanner,
2009
+ });
2010
+ // Decorate cmsApp with the template-rescan hook before returning. The
2011
+ // file watcher in startServer() invokes this on `.ts/.tsx` changes
2012
+ // under `templates/{name}/`. Failure is fail-open (logged + dropped)
2013
+ // — a scan failure shouldn't break dev-mode hot reload.
2014
+ const decoratedApp = cmsApp;
2015
+ decoratedApp.rescanForTemplate = async (name) => {
2016
+ if (!validationScanner)
2017
+ return;
2018
+ try {
2019
+ await validationScanner.rescan({ kind: 'template', name });
2020
+ }
2021
+ catch (err) {
2022
+ console.warn(` Validation scanner: template rescan failed for "${name}": ${err.message}`);
2023
+ }
2024
+ };
1716
2025
  mountUserThemeRoute(cmsApp, adminDir);
1717
2026
  app.route('/admin', cmsApp);
1718
- return cmsApp;
2027
+ return decoratedApp;
1719
2028
  }
1720
2029
  // ---- Production mode: inline CMS API + static files from admin-dist/ ----
1721
- async function setupProductionMode(app, source, siteDir, cmsStaticDir, templatesDir, adminDir, targetConfigs) {
2030
+ async function setupProductionMode(app, source, siteDir, cmsStaticDir, templatesDir, adminDir, targetConfigs, contributions, manifest) {
2031
+ // Same shape as dev mode — `gazetta serve` reads `admin.hooks`
2032
+ // factory contributions from the same site config.
2033
+ const hooks = await buildHooksRegistry({ contributions });
2034
+ const validationScanner = await buildValidationScanner({ source, templatesDir, manifest });
2035
+ // SSE channel at the outer app's root; see setupCmsApi for rationale.
2036
+ const { mountValidationSse } = await import('../admin-api/routes/validation.js');
2037
+ mountValidationSse(app, validationScanner);
1722
2038
  // Mount CMS API inline at /admin (production mode — bundled editors/fields)
1723
- const cmsApp = createAdminApp({ source, siteDir, templatesDir, adminDir, production: true, targetConfigs });
2039
+ const cmsApp = createAdminApp({
2040
+ source,
2041
+ siteDir,
2042
+ templatesDir,
2043
+ adminDir,
2044
+ production: true,
2045
+ targetConfigs,
2046
+ hooks,
2047
+ validationScanner,
2048
+ });
1724
2049
  mountUserThemeRoute(cmsApp, adminDir);
1725
2050
  app.route('/admin', cmsApp);
1726
2051
  // Serve pre-built CMS static files (includes bundled editors/fields)
@@ -1785,77 +2110,76 @@ async function main() {
1785
2110
  await runInit(parsed.positional[0] ?? '.');
1786
2111
  return;
1787
2112
  }
1788
- else if (command === 'build') {
1789
- const siteDir = await resolveSiteDir(parsed.positional[0]);
2113
+ // ── Resolve siteDir once (fs walk; no config eval) + load .env
2114
+ // ── BEFORE any config-eval. Storage / deploy / AI factories read
2115
+ // ── process.env synchronously at construction; resolveTarget()
2116
+ // ── triggers config-eval, so env must be in place first. Per-
2117
+ // ── command positional layouts determine which slot carries the
2118
+ // ── site path. Site-local `.env` is honored (e.g.,
2119
+ // ── sites/gazetta.studio/.env with R2 creds) per design-config.md.
2120
+ siteDir = await resolveSiteDirForCommand(command, parsed);
2121
+ loadEnvFiles(siteDir);
2122
+ if (command === 'build') {
1790
2123
  await runBuild(siteDir);
1791
2124
  return;
1792
2125
  }
1793
2126
  else if (command === 'rollback') {
1794
2127
  // gazetta rollback <rev> [target] [site]
1795
- const [rev, second, third] = parsed.positional;
2128
+ const [rev, second] = parsed.positional;
1796
2129
  if (!rev || !rev.startsWith('rev-')) {
1797
2130
  console.error(`\n Error: rollback requires a revision id as the first argument (e.g. gazetta rollback rev-1776337441608 [target])\n`);
1798
2131
  process.exit(1);
1799
2132
  return;
1800
2133
  }
1801
2134
  rollbackRevisionId = rev;
1802
- const secondIsSite = second && (second.includes('/') || existsSync(join(resolve(second), 'site.yaml')));
1803
- if (secondIsSite) {
1804
- siteDir = await resolveSiteDir(second);
1805
- targetName = await resolveTarget(undefined, siteDir);
1806
- }
1807
- else {
1808
- siteDir = await resolveSiteDir(third);
1809
- targetName = await resolveTarget(second, siteDir);
1810
- }
2135
+ const secondIsSite = second && (second.includes('/') || hasSiteConfig(resolve(second)));
2136
+ targetName = secondIsSite ? await resolveTarget(undefined, siteDir) : await resolveTarget(second, siteDir);
1811
2137
  }
1812
2138
  else if (targetFirstCommands.has(command)) {
1813
2139
  // gazetta publish [target] [site]
1814
- const [first, second] = parsed.positional;
1815
- // If first arg looks like a site path (contains / or has site.yaml), it's the site
1816
- const firstIsSite = first && (first.includes('/') || existsSync(join(resolve(first), 'site.yaml')));
1817
- if (firstIsSite) {
1818
- siteDir = await resolveSiteDir(first);
1819
- targetName = await resolveTarget(undefined, siteDir);
1820
- }
1821
- else {
1822
- siteDir = await resolveSiteDir(second);
1823
- targetName = await resolveTarget(first, siteDir);
1824
- }
2140
+ const [first] = parsed.positional;
2141
+ const firstIsSite = first && (first.includes('/') || hasSiteConfig(resolve(first)));
2142
+ targetName = firstIsSite ? await resolveTarget(undefined, siteDir) : await resolveTarget(first, siteDir);
1825
2143
  }
1826
2144
  else if (siteOnlyCommands.has(command)) {
1827
- siteDir = await resolveSiteDir(parsed.positional[0]);
2145
+ // siteDir set up front; nothing more needed.
1828
2146
  }
1829
2147
  else if (command === 'translate') {
1830
2148
  // gazetta translate <item> --to <locale> [target]
1831
- // positional args after the item are the optional target name
1832
- siteDir = await resolveSiteDir(undefined);
1833
- // Find the target arg — skip the item (pages/... or fragments/...) and --to/locale flags
1834
2149
  const translatePositionals = parsed.positional.filter(p => !p.startsWith('pages/') && !p.startsWith('fragments/'));
1835
2150
  if (translatePositionals.length > 0)
1836
2151
  targetName = translatePositionals[0];
1837
2152
  }
1838
2153
  else if (command === 'assets') {
1839
2154
  // gazetta assets <subcommand> [args...] [target] [site]
1840
- //
1841
- // Subcommand layouts:
1842
2155
  // assets list [target] [site] → subcmd, target, site
1843
2156
  // assets info <name> [target] [site] → subcmd, name, target, site
1844
2157
  // assets reindex [target] [site] → subcmd, target, site
2158
+ const subcmd = parsed.positional[0];
2159
+ const targetArg = subcmd === 'info' ? parsed.positional[2] : parsed.positional[1];
2160
+ targetName = targetArg ? await resolveTarget(targetArg, siteDir) : undefined;
2161
+ }
2162
+ else if (command === 'archive') {
2163
+ // gazetta archive <subcommand> [args...]
1845
2164
  //
1846
- // The dispatcher in the assets-cli module reads the asset name
1847
- // from its `args` slice; here we resolve target/site by checking
1848
- // the positional layout.
2165
+ // Site/target resolution: positional after the subcommand's
2166
+ // required args are interpreted as [target] [site]. The archive
2167
+ // module honors `--kind=` / `--force` / `--no-keep-alias` flags
2168
+ // separately (those don't reach the parsed.positional list per
2169
+ // global parseArgs).
2170
+ //
2171
+ // Subcommand positional shapes:
2172
+ // archive list → []
2173
+ // archive purge <name> → [name]
2174
+ // archive restore <name> → [name]
2175
+ // archive rename <oldname> <newname> → [oldname, newname]
2176
+ //
2177
+ // Remaining slots are [target] [site] in that order.
1849
2178
  const subcmd = parsed.positional[0];
1850
- if (subcmd === 'info') {
1851
- // info has an extra positional (the asset name) before target/site.
1852
- siteDir = await resolveSiteDir(parsed.positional[3]);
1853
- targetName = parsed.positional[2] ? await resolveTarget(parsed.positional[2], siteDir) : undefined;
1854
- }
1855
- else {
1856
- siteDir = await resolveSiteDir(parsed.positional[2]);
1857
- targetName = parsed.positional[1] ? await resolveTarget(parsed.positional[1], siteDir) : undefined;
1858
- }
2179
+ const requiredArgs = subcmd === 'rename' ? 2 : subcmd === 'purge' || subcmd === 'restore' ? 1 : 0;
2180
+ const trailingStart = 1 + requiredArgs;
2181
+ const targetArg = parsed.positional[trailingStart];
2182
+ targetName = targetArg ? await resolveTarget(targetArg, siteDir) : undefined;
1859
2183
  }
1860
2184
  else {
1861
2185
  console.error(` Unknown command: ${command}\n`);
@@ -1863,23 +2187,6 @@ async function main() {
1863
2187
  process.exit(1);
1864
2188
  return;
1865
2189
  }
1866
- // Load .env from project root and site dir (skipped in CI)
1867
- if (!process.env.CI) {
1868
- const projectRoot = detectProjectRoot(siteDir);
1869
- const envDirs = projectRoot !== siteDir ? [projectRoot, siteDir] : [siteDir];
1870
- for (const dir of envDirs) {
1871
- for (const name of ['.env', '.env.local']) {
1872
- const envPath = join(dir, name);
1873
- if (existsSync(envPath)) {
1874
- for (const line of readFileSync(envPath, 'utf-8').split('\n')) {
1875
- const m = line.match(/^\s*([A-Z_][A-Z0-9_]*)\s*=\s*(.*)$/);
1876
- if (m && !(m[1] in process.env))
1877
- process.env[m[1]] = m[2].replace(/^["']|["']$/g, '');
1878
- }
1879
- }
1880
- }
1881
- }
1882
- }
1883
2190
  switch (command) {
1884
2191
  case 'publish':
1885
2192
  await runPublish(siteDir, targetName, { force: parsed.force });
@@ -1891,7 +2198,7 @@ async function main() {
1891
2198
  await runDeploy(siteDir, targetName);
1892
2199
  break;
1893
2200
  case 'validate':
1894
- await runValidate(siteDir);
2201
+ await runValidate(siteDir, args.slice(1));
1895
2202
  break;
1896
2203
  case 'dev':
1897
2204
  await runDev(siteDir, parsed.port ?? 3000);
@@ -1921,7 +2228,11 @@ async function main() {
1921
2228
  }
1922
2229
  // Resolve the content directory — translate operates on a target's filesystem.
1923
2230
  // Uses the specified target or falls back to the first editable target.
1924
- const siteYaml = yaml.load(readFileSync(join(siteDir, 'site.yaml'), 'utf-8'));
2231
+ const siteYaml = await loadSiteManifestForCli(siteDir);
2232
+ if (!siteYaml) {
2233
+ console.error(` Error: no site config found at ${siteDir}`);
2234
+ process.exit(1);
2235
+ }
1925
2236
  const { isEditable } = await import('../types.js');
1926
2237
  const resolvedTarget = targetName ?? Object.entries(siteYaml.targets ?? {}).find(([, cfg]) => isEditable(cfg))?.[0];
1927
2238
  if (!resolvedTarget) {
@@ -1930,26 +2241,27 @@ async function main() {
1930
2241
  }
1931
2242
  const targetConfig = siteYaml.targets[resolvedTarget];
1932
2243
  if (!targetConfig) {
1933
- console.error(` Error: target "${resolvedTarget}" not found in site.yaml`);
2244
+ console.error(` Error: target "${resolvedTarget}" not found in site config`);
1934
2245
  process.exit(1);
1935
2246
  }
1936
- const storagePath = targetConfig.storage.path ?? join('targets', resolvedTarget);
1937
- const contentDir = resolve(siteDir, storagePath);
2247
+ // Translate goes through the storage provider so it works on any
2248
+ // storage backend (filesystem / R2 / S3 / Azure). Path X — the storage
2249
+ // provider was constructed by the operator's factory at config-eval.
2250
+ const storage = targetConfig.storage;
1938
2251
  const baseName = isPage ? 'page' : 'fragment';
1939
- const dir = join(contentDir, itemArg);
1940
- const sourceFile = join(dir, `${baseName}.json`);
1941
- const destFile = join(dir, localeFilename(baseName, locale));
1942
- const fs = await import('node:fs/promises');
1943
- if (!existsSync(sourceFile)) {
1944
- console.error(` Error: ${sourceFile} not found`);
2252
+ const sourcePath = `${itemArg}/${baseName}.json`;
2253
+ const destPath = `${itemArg}/${localeFilename(baseName, locale)}`;
2254
+ if (!(await storage.exists(sourcePath))) {
2255
+ console.error(` Error: ${sourcePath} not found on target "${resolvedTarget}"`);
1945
2256
  process.exit(1);
1946
2257
  }
1947
- if (existsSync(destFile)) {
1948
- console.error(` Error: ${destFile} already exists`);
2258
+ if (await storage.exists(destPath)) {
2259
+ console.error(` Error: ${destPath} already exists on target "${resolvedTarget}"`);
1949
2260
  process.exit(1);
1950
2261
  }
1951
- await fs.copyFile(sourceFile, destFile);
1952
- console.log(` ${c.green('✓')} Created ${relative(process.cwd(), destFile)}`);
2262
+ const sourceContent = await storage.readFile(sourcePath);
2263
+ await storage.writeFile(destPath, sourceContent);
2264
+ console.log(` ${c.green('✓')} Created ${destPath}`);
1953
2265
  console.log(` Edit the file to translate the content.`);
1954
2266
  break;
1955
2267
  }
@@ -1971,12 +2283,17 @@ async function main() {
1971
2283
  await runAssetsSubcommand({ args: args.slice(1), siteDir, targetName });
1972
2284
  break;
1973
2285
  }
2286
+ case 'archive': {
2287
+ const { runArchiveSubcommand } = await import('./archive.js');
2288
+ await runArchiveSubcommand({ args: args.slice(1), siteDir, targetName });
2289
+ break;
2290
+ }
1974
2291
  }
1975
2292
  }
1976
2293
  /**
1977
2294
  * Resolve site + target + config into the shape HistoryCommandContext
1978
2295
  * expects. Lives here rather than in cli/history.ts so the target-
1979
- * resolution logic (site.yaml parsing, CI env handling) stays with
2296
+ * resolution logic (site config parsing, CI env handling) stays with
1980
2297
  * the other CLI commands that already do it the same way.
1981
2298
  */
1982
2299
  async function resolveHistoryContext(siteDir, targetName) {