@cicore/cli 1.0.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 (337) hide show
  1. package/bin/ci.js +13 -0
  2. package/dist/commands/addon/api-actions.d.ts +45 -0
  3. package/dist/commands/addon/api-actions.d.ts.map +1 -0
  4. package/dist/commands/addon/api-actions.js +281 -0
  5. package/dist/commands/addon/api-actions.js.map +1 -0
  6. package/dist/commands/addon/build.d.ts +11 -0
  7. package/dist/commands/addon/build.d.ts.map +1 -0
  8. package/dist/commands/addon/build.js +182 -0
  9. package/dist/commands/addon/build.js.map +1 -0
  10. package/dist/commands/addon/create.d.ts +11 -0
  11. package/dist/commands/addon/create.d.ts.map +1 -0
  12. package/dist/commands/addon/create.js +1186 -0
  13. package/dist/commands/addon/create.js.map +1 -0
  14. package/dist/commands/addon/delete.d.ts +13 -0
  15. package/dist/commands/addon/delete.d.ts.map +1 -0
  16. package/dist/commands/addon/delete.js +83 -0
  17. package/dist/commands/addon/delete.js.map +1 -0
  18. package/dist/commands/addon/deploy.d.ts +27 -0
  19. package/dist/commands/addon/deploy.d.ts.map +1 -0
  20. package/dist/commands/addon/deploy.js +459 -0
  21. package/dist/commands/addon/deploy.js.map +1 -0
  22. package/dist/commands/addon/dev-deploy.d.ts +31 -0
  23. package/dist/commands/addon/dev-deploy.d.ts.map +1 -0
  24. package/dist/commands/addon/dev-deploy.js +128 -0
  25. package/dist/commands/addon/dev-deploy.js.map +1 -0
  26. package/dist/commands/addon/dev.d.ts +36 -0
  27. package/dist/commands/addon/dev.d.ts.map +1 -0
  28. package/dist/commands/addon/dev.js +323 -0
  29. package/dist/commands/addon/dev.js.map +1 -0
  30. package/dist/commands/addon/extract-classes.d.ts +23 -0
  31. package/dist/commands/addon/extract-classes.d.ts.map +1 -0
  32. package/dist/commands/addon/extract-classes.js +281 -0
  33. package/dist/commands/addon/extract-classes.js.map +1 -0
  34. package/dist/commands/addon/generate-safelist.d.ts +24 -0
  35. package/dist/commands/addon/generate-safelist.d.ts.map +1 -0
  36. package/dist/commands/addon/generate-safelist.js +276 -0
  37. package/dist/commands/addon/generate-safelist.js.map +1 -0
  38. package/dist/commands/addon/index.d.ts +19 -0
  39. package/dist/commands/addon/index.d.ts.map +1 -0
  40. package/dist/commands/addon/index.js +296 -0
  41. package/dist/commands/addon/index.js.map +1 -0
  42. package/dist/commands/addon/init-repo.d.ts +25 -0
  43. package/dist/commands/addon/init-repo.d.ts.map +1 -0
  44. package/dist/commands/addon/init-repo.js +171 -0
  45. package/dist/commands/addon/init-repo.js.map +1 -0
  46. package/dist/commands/addon/install.d.ts +23 -0
  47. package/dist/commands/addon/install.d.ts.map +1 -0
  48. package/dist/commands/addon/install.js +84 -0
  49. package/dist/commands/addon/install.js.map +1 -0
  50. package/dist/commands/addon/list.d.ts +10 -0
  51. package/dist/commands/addon/list.d.ts.map +1 -0
  52. package/dist/commands/addon/list.js +102 -0
  53. package/dist/commands/addon/list.js.map +1 -0
  54. package/dist/commands/addon/manifest-refresh.d.ts +17 -0
  55. package/dist/commands/addon/manifest-refresh.d.ts.map +1 -0
  56. package/dist/commands/addon/manifest-refresh.js +48 -0
  57. package/dist/commands/addon/manifest-refresh.js.map +1 -0
  58. package/dist/commands/addon/migrate.d.ts +40 -0
  59. package/dist/commands/addon/migrate.d.ts.map +1 -0
  60. package/dist/commands/addon/migrate.js +236 -0
  61. package/dist/commands/addon/migrate.js.map +1 -0
  62. package/dist/commands/addon/publish.d.ts +33 -0
  63. package/dist/commands/addon/publish.d.ts.map +1 -0
  64. package/dist/commands/addon/publish.js +236 -0
  65. package/dist/commands/addon/publish.js.map +1 -0
  66. package/dist/commands/addon/scaffold-quality.d.ts +21 -0
  67. package/dist/commands/addon/scaffold-quality.d.ts.map +1 -0
  68. package/dist/commands/addon/scaffold-quality.js +90 -0
  69. package/dist/commands/addon/scaffold-quality.js.map +1 -0
  70. package/dist/commands/addon/sign.d.ts +9 -0
  71. package/dist/commands/addon/sign.d.ts.map +1 -0
  72. package/dist/commands/addon/sign.js +83 -0
  73. package/dist/commands/addon/sign.js.map +1 -0
  74. package/dist/commands/addon/toggle.d.ts +6 -0
  75. package/dist/commands/addon/toggle.d.ts.map +1 -0
  76. package/dist/commands/addon/toggle.js +46 -0
  77. package/dist/commands/addon/toggle.js.map +1 -0
  78. package/dist/commands/agent/index.d.ts +34 -0
  79. package/dist/commands/agent/index.d.ts.map +1 -0
  80. package/dist/commands/agent/index.js +564 -0
  81. package/dist/commands/agent/index.js.map +1 -0
  82. package/dist/commands/brand/index.d.ts +54 -0
  83. package/dist/commands/brand/index.d.ts.map +1 -0
  84. package/dist/commands/brand/index.js +367 -0
  85. package/dist/commands/brand/index.js.map +1 -0
  86. package/dist/commands/build/index.d.ts +53 -0
  87. package/dist/commands/build/index.d.ts.map +1 -0
  88. package/dist/commands/build/index.js +726 -0
  89. package/dist/commands/build/index.js.map +1 -0
  90. package/dist/commands/cache/flush-local.d.ts +31 -0
  91. package/dist/commands/cache/flush-local.d.ts.map +1 -0
  92. package/dist/commands/cache/flush-local.js +161 -0
  93. package/dist/commands/cache/flush-local.js.map +1 -0
  94. package/dist/commands/cache/index.d.ts +14 -0
  95. package/dist/commands/cache/index.d.ts.map +1 -0
  96. package/dist/commands/cache/index.js +453 -0
  97. package/dist/commands/cache/index.js.map +1 -0
  98. package/dist/commands/check/index.d.ts +8 -0
  99. package/dist/commands/check/index.d.ts.map +1 -0
  100. package/dist/commands/check/index.js +1316 -0
  101. package/dist/commands/check/index.js.map +1 -0
  102. package/dist/commands/cloudflare/index.d.ts +8 -0
  103. package/dist/commands/cloudflare/index.d.ts.map +1 -0
  104. package/dist/commands/cloudflare/index.js +453 -0
  105. package/dist/commands/cloudflare/index.js.map +1 -0
  106. package/dist/commands/core/create.d.ts +12 -0
  107. package/dist/commands/core/create.d.ts.map +1 -0
  108. package/dist/commands/core/create.js +206 -0
  109. package/dist/commands/core/create.js.map +1 -0
  110. package/dist/commands/core/delete.d.ts +11 -0
  111. package/dist/commands/core/delete.d.ts.map +1 -0
  112. package/dist/commands/core/delete.js +64 -0
  113. package/dist/commands/core/delete.js.map +1 -0
  114. package/dist/commands/core/env.d.ts +12 -0
  115. package/dist/commands/core/env.d.ts.map +1 -0
  116. package/dist/commands/core/env.js +95 -0
  117. package/dist/commands/core/env.js.map +1 -0
  118. package/dist/commands/core/health.d.ts +6 -0
  119. package/dist/commands/core/health.d.ts.map +1 -0
  120. package/dist/commands/core/health.js +215 -0
  121. package/dist/commands/core/health.js.map +1 -0
  122. package/dist/commands/core/index.d.ts +15 -0
  123. package/dist/commands/core/index.d.ts.map +1 -0
  124. package/dist/commands/core/index.js +86 -0
  125. package/dist/commands/core/index.js.map +1 -0
  126. package/dist/commands/core/list.d.ts +11 -0
  127. package/dist/commands/core/list.d.ts.map +1 -0
  128. package/dist/commands/core/list.js +58 -0
  129. package/dist/commands/core/list.js.map +1 -0
  130. package/dist/commands/core/rebuild.d.ts +13 -0
  131. package/dist/commands/core/rebuild.d.ts.map +1 -0
  132. package/dist/commands/core/rebuild.js +119 -0
  133. package/dist/commands/core/rebuild.js.map +1 -0
  134. package/dist/commands/db/index.d.ts +23 -0
  135. package/dist/commands/db/index.d.ts.map +1 -0
  136. package/dist/commands/db/index.js +355 -0
  137. package/dist/commands/db/index.js.map +1 -0
  138. package/dist/commands/db/promote-silo.d.ts +320 -0
  139. package/dist/commands/db/promote-silo.d.ts.map +1 -0
  140. package/dist/commands/db/promote-silo.js +930 -0
  141. package/dist/commands/db/promote-silo.js.map +1 -0
  142. package/dist/commands/db/relocate.d.ts +41 -0
  143. package/dist/commands/db/relocate.d.ts.map +1 -0
  144. package/dist/commands/db/relocate.js +482 -0
  145. package/dist/commands/db/relocate.js.map +1 -0
  146. package/dist/commands/db/rollback-silo.d.ts +44 -0
  147. package/dist/commands/db/rollback-silo.d.ts.map +1 -0
  148. package/dist/commands/db/rollback-silo.js +402 -0
  149. package/dist/commands/db/rollback-silo.js.map +1 -0
  150. package/dist/commands/deploy/index.d.ts +26 -0
  151. package/dist/commands/deploy/index.d.ts.map +1 -0
  152. package/dist/commands/deploy/index.js +107 -0
  153. package/dist/commands/deploy/index.js.map +1 -0
  154. package/dist/commands/devops/index.d.ts +6 -0
  155. package/dist/commands/devops/index.d.ts.map +1 -0
  156. package/dist/commands/devops/index.js +220 -0
  157. package/dist/commands/devops/index.js.map +1 -0
  158. package/dist/commands/domain/index.d.ts +8 -0
  159. package/dist/commands/domain/index.d.ts.map +1 -0
  160. package/dist/commands/domain/index.js +386 -0
  161. package/dist/commands/domain/index.js.map +1 -0
  162. package/dist/commands/image/index.d.ts +8 -0
  163. package/dist/commands/image/index.d.ts.map +1 -0
  164. package/dist/commands/image/index.js +308 -0
  165. package/dist/commands/image/index.js.map +1 -0
  166. package/dist/commands/install/factory-reset.d.ts +21 -0
  167. package/dist/commands/install/factory-reset.d.ts.map +1 -0
  168. package/dist/commands/install/factory-reset.js +83 -0
  169. package/dist/commands/install/factory-reset.js.map +1 -0
  170. package/dist/commands/install/index.d.ts +17 -0
  171. package/dist/commands/install/index.d.ts.map +1 -0
  172. package/dist/commands/install/index.js +44 -0
  173. package/dist/commands/install/index.js.map +1 -0
  174. package/dist/commands/install/install.d.ts +35 -0
  175. package/dist/commands/install/install.d.ts.map +1 -0
  176. package/dist/commands/install/install.js +171 -0
  177. package/dist/commands/install/install.js.map +1 -0
  178. package/dist/commands/login/index.d.ts +15 -0
  179. package/dist/commands/login/index.d.ts.map +1 -0
  180. package/dist/commands/login/index.js +58 -0
  181. package/dist/commands/login/index.js.map +1 -0
  182. package/dist/commands/nginx/index.d.ts +11 -0
  183. package/dist/commands/nginx/index.d.ts.map +1 -0
  184. package/dist/commands/nginx/index.js +580 -0
  185. package/dist/commands/nginx/index.js.map +1 -0
  186. package/dist/commands/server/bootstrap.d.ts +25 -0
  187. package/dist/commands/server/bootstrap.d.ts.map +1 -0
  188. package/dist/commands/server/bootstrap.js +260 -0
  189. package/dist/commands/server/bootstrap.js.map +1 -0
  190. package/dist/commands/server/index.d.ts +8 -0
  191. package/dist/commands/server/index.d.ts.map +1 -0
  192. package/dist/commands/server/index.js +2524 -0
  193. package/dist/commands/server/index.js.map +1 -0
  194. package/dist/commands/setup/index.d.ts +34 -0
  195. package/dist/commands/setup/index.d.ts.map +1 -0
  196. package/dist/commands/setup/index.js +423 -0
  197. package/dist/commands/setup/index.js.map +1 -0
  198. package/dist/commands/ssl/index.d.ts +8 -0
  199. package/dist/commands/ssl/index.d.ts.map +1 -0
  200. package/dist/commands/ssl/index.js +275 -0
  201. package/dist/commands/ssl/index.js.map +1 -0
  202. package/dist/commands/superadmin/index.d.ts +16 -0
  203. package/dist/commands/superadmin/index.d.ts.map +1 -0
  204. package/dist/commands/superadmin/index.js +81 -0
  205. package/dist/commands/superadmin/index.js.map +1 -0
  206. package/dist/commands/tenant/index.d.ts +6 -0
  207. package/dist/commands/tenant/index.d.ts.map +1 -0
  208. package/dist/commands/tenant/index.js +192 -0
  209. package/dist/commands/tenant/index.js.map +1 -0
  210. package/dist/index.d.ts +11 -0
  211. package/dist/index.d.ts.map +1 -0
  212. package/dist/index.js +107 -0
  213. package/dist/index.js.map +1 -0
  214. package/dist/lib/addon-sign.d.ts +23 -0
  215. package/dist/lib/addon-sign.d.ts.map +1 -0
  216. package/dist/lib/addon-sign.js +39 -0
  217. package/dist/lib/addon-sign.js.map +1 -0
  218. package/dist/lib/addon-sign.test.d.ts +2 -0
  219. package/dist/lib/addon-sign.test.d.ts.map +1 -0
  220. package/dist/lib/addon-sign.test.js +27 -0
  221. package/dist/lib/addon-sign.test.js.map +1 -0
  222. package/dist/lib/cdn.d.ts +25 -0
  223. package/dist/lib/cdn.d.ts.map +1 -0
  224. package/dist/lib/cdn.js +131 -0
  225. package/dist/lib/cdn.js.map +1 -0
  226. package/dist/lib/cloudflare.d.ts +133 -0
  227. package/dist/lib/cloudflare.d.ts.map +1 -0
  228. package/dist/lib/cloudflare.js +435 -0
  229. package/dist/lib/cloudflare.js.map +1 -0
  230. package/dist/lib/config.d.ts +96 -0
  231. package/dist/lib/config.d.ts.map +1 -0
  232. package/dist/lib/config.js +132 -0
  233. package/dist/lib/config.js.map +1 -0
  234. package/dist/lib/env.d.ts +8 -0
  235. package/dist/lib/env.d.ts.map +1 -0
  236. package/dist/lib/env.js +64 -0
  237. package/dist/lib/env.js.map +1 -0
  238. package/dist/lib/hosts.d.ts +194 -0
  239. package/dist/lib/hosts.d.ts.map +1 -0
  240. package/dist/lib/hosts.js +183 -0
  241. package/dist/lib/hosts.js.map +1 -0
  242. package/dist/lib/logger.d.ts +68 -0
  243. package/dist/lib/logger.d.ts.map +1 -0
  244. package/dist/lib/logger.js +130 -0
  245. package/dist/lib/logger.js.map +1 -0
  246. package/dist/lib/nginx-config.d.ts +78 -0
  247. package/dist/lib/nginx-config.d.ts.map +1 -0
  248. package/dist/lib/nginx-config.js +736 -0
  249. package/dist/lib/nginx-config.js.map +1 -0
  250. package/dist/lib/ops/addon-dev.d.ts +93 -0
  251. package/dist/lib/ops/addon-dev.d.ts.map +1 -0
  252. package/dist/lib/ops/addon-dev.js +237 -0
  253. package/dist/lib/ops/addon-dev.js.map +1 -0
  254. package/dist/lib/ops/addon-quality.d.ts +38 -0
  255. package/dist/lib/ops/addon-quality.d.ts.map +1 -0
  256. package/dist/lib/ops/addon-quality.js +338 -0
  257. package/dist/lib/ops/addon-quality.js.map +1 -0
  258. package/dist/lib/ops/addon-routes.d.ts +49 -0
  259. package/dist/lib/ops/addon-routes.d.ts.map +1 -0
  260. package/dist/lib/ops/addon-routes.js +189 -0
  261. package/dist/lib/ops/addon-routes.js.map +1 -0
  262. package/dist/lib/ops/addon.d.ts +120 -0
  263. package/dist/lib/ops/addon.d.ts.map +1 -0
  264. package/dist/lib/ops/addon.js +260 -0
  265. package/dist/lib/ops/addon.js.map +1 -0
  266. package/dist/lib/ops/cdn.d.ts +87 -0
  267. package/dist/lib/ops/cdn.d.ts.map +1 -0
  268. package/dist/lib/ops/cdn.js +170 -0
  269. package/dist/lib/ops/cdn.js.map +1 -0
  270. package/dist/lib/ops/cf.d.ts +36 -0
  271. package/dist/lib/ops/cf.d.ts.map +1 -0
  272. package/dist/lib/ops/cf.js +114 -0
  273. package/dist/lib/ops/cf.js.map +1 -0
  274. package/dist/lib/ops/compose.d.ts +95 -0
  275. package/dist/lib/ops/compose.d.ts.map +1 -0
  276. package/dist/lib/ops/compose.js +165 -0
  277. package/dist/lib/ops/compose.js.map +1 -0
  278. package/dist/lib/ops/core.d.ts +117 -0
  279. package/dist/lib/ops/core.d.ts.map +1 -0
  280. package/dist/lib/ops/core.js +322 -0
  281. package/dist/lib/ops/core.js.map +1 -0
  282. package/dist/lib/ops/db.d.ts +116 -0
  283. package/dist/lib/ops/db.d.ts.map +1 -0
  284. package/dist/lib/ops/db.js +351 -0
  285. package/dist/lib/ops/db.js.map +1 -0
  286. package/dist/lib/ops/dns.d.ts +111 -0
  287. package/dist/lib/ops/dns.d.ts.map +1 -0
  288. package/dist/lib/ops/dns.js +306 -0
  289. package/dist/lib/ops/dns.js.map +1 -0
  290. package/dist/lib/ops/image.d.ts +94 -0
  291. package/dist/lib/ops/image.d.ts.map +1 -0
  292. package/dist/lib/ops/image.js +159 -0
  293. package/dist/lib/ops/image.js.map +1 -0
  294. package/dist/lib/ops/nginx.d.ts +114 -0
  295. package/dist/lib/ops/nginx.d.ts.map +1 -0
  296. package/dist/lib/ops/nginx.js +388 -0
  297. package/dist/lib/ops/nginx.js.map +1 -0
  298. package/dist/lib/ops/redis.d.ts +7 -0
  299. package/dist/lib/ops/redis.d.ts.map +1 -0
  300. package/dist/lib/ops/redis.js +35 -0
  301. package/dist/lib/ops/redis.js.map +1 -0
  302. package/dist/lib/ops/ssh.d.ts +127 -0
  303. package/dist/lib/ops/ssh.d.ts.map +1 -0
  304. package/dist/lib/ops/ssh.js +269 -0
  305. package/dist/lib/ops/ssh.js.map +1 -0
  306. package/dist/lib/prompts.d.ts +46 -0
  307. package/dist/lib/prompts.d.ts.map +1 -0
  308. package/dist/lib/prompts.js +113 -0
  309. package/dist/lib/prompts.js.map +1 -0
  310. package/dist/lib/sast.d.ts +43 -0
  311. package/dist/lib/sast.d.ts.map +1 -0
  312. package/dist/lib/sast.js +79 -0
  313. package/dist/lib/sast.js.map +1 -0
  314. package/dist/lib/sast.test.d.ts +2 -0
  315. package/dist/lib/sast.test.d.ts.map +1 -0
  316. package/dist/lib/sast.test.js +33 -0
  317. package/dist/lib/sast.test.js.map +1 -0
  318. package/dist/lib/shell.d.ts +61 -0
  319. package/dist/lib/shell.d.ts.map +1 -0
  320. package/dist/lib/shell.js +183 -0
  321. package/dist/lib/shell.js.map +1 -0
  322. package/dist/lib/ssh-config.d.ts +37 -0
  323. package/dist/lib/ssh-config.d.ts.map +1 -0
  324. package/dist/lib/ssh-config.js +122 -0
  325. package/dist/lib/ssh-config.js.map +1 -0
  326. package/dist/lib/tenant-scope.d.ts +38 -0
  327. package/dist/lib/tenant-scope.d.ts.map +1 -0
  328. package/dist/lib/tenant-scope.js +129 -0
  329. package/dist/lib/tenant-scope.js.map +1 -0
  330. package/dist/lib/tenant-scope.test.d.ts +2 -0
  331. package/dist/lib/tenant-scope.test.d.ts.map +1 -0
  332. package/dist/lib/tenant-scope.test.js +223 -0
  333. package/dist/lib/tenant-scope.test.js.map +1 -0
  334. package/package.json +58 -0
  335. package/templates/bootstrap/.env.template +54 -0
  336. package/templates/bootstrap/docker-compose.yml +145 -0
  337. package/templates/vhost.conf.tmpl +446 -0
@@ -0,0 +1,726 @@
1
+ /**
2
+ * CiCore CLI — Build commands (host-aware)
3
+ *
4
+ * Three subcommands, one for each core image plus addons:
5
+ *
6
+ * - `vu build nuxt --host <h> [--deploy]` build Nuxt → push → upload UI → deploy
7
+ * - `vu build php --host <h> [--deploy]` build PHP → push → deploy
8
+ * - `vu build addon <name> --host <h>` vite build → upload → install backend → deploy
9
+ *
10
+ * Everything host-specific (registry namespace, CDN endpoint, server paths,
11
+ * container names, ssh target, CF zone) comes from {@link getHostConfig};
12
+ * there is no global CONFIG sabit any more, no hardcoded URL, no hardcoded
13
+ * `/home/cores/...`. Add a host to STATIC_TOPOLOGY and the same commands
14
+ * deploy to it.
15
+ *
16
+ * Heavy lifting (image build/push/pull/retag, compose ops, CDN clean/upload,
17
+ * nginx reload, CF purge) is delegated to the `ops/` primitives written in
18
+ * step 2. This file is mostly orchestration + CLI plumbing + the things
19
+ * still local to the Mac build host (Tailwind safelist, Vite build, manifest
20
+ * generation, Asset hashing).
21
+ */
22
+ import { createWriteStream } from 'node:fs';
23
+ import { join as joinPath } from 'node:path';
24
+ import archiver from 'archiver';
25
+ import fs from 'fs-extra';
26
+ import { log, spinner } from '../../lib/logger.js';
27
+ import { paths } from '../../lib/config.js';
28
+ import { confirm } from '../../lib/prompts.js';
29
+ import { getHostConfig, } from '../../lib/hosts.js';
30
+ import { buildImage, composeImageRef, pushImage, pullImage, retagForCompose, } from '../../lib/ops/image.js';
31
+ import { composeForceRecreate } from '../../lib/ops/compose.js';
32
+ import { cdnCleanCoreUi, cdnCleanAddon, cdnUploadZip } from '../../lib/ops/cdn.js';
33
+ import { cfPurgeZone } from '../../lib/ops/cf.js';
34
+ import { addonClearRouteCache } from '../../lib/ops/addon.js';
35
+ import { runLocal, runRemote, uploadFile } from '../../lib/ops/ssh.js';
36
+ export function registerBuildCommands(program) {
37
+ const build = program.command('build').description('Build and deploy Docker images');
38
+ build
39
+ .command('nuxt')
40
+ .description('Build Nuxt image, push to registry, upload CoreUI to CDN, optionally deploy')
41
+ .option('--skip-cdn', 'Skip CDN upload')
42
+ .option('--skip-push', 'Skip Docker Hub push')
43
+ .option('--skip-build', 'Skip Docker build and push (use existing image)')
44
+ .option('--skip-server', 'Skip server pull/restart')
45
+ .option('--skip-safelist', 'Skip addon CSS safelist generation')
46
+ .option('-h, --host <host>', 'Host alias (vucore | cicore)')
47
+ .option('-c, --core <core>', 'Core name for cache clearing (e.g. cicore, vucore)')
48
+ .option('--deploy', 'Full deployment: pull, cache clear, recreate container')
49
+ .option('--dry-run', 'Show what would be done without executing')
50
+ .action(async (options) => {
51
+ await buildNuxt(options);
52
+ });
53
+ build
54
+ .command('php')
55
+ .description('Build PHP image and push to registry, optionally deploy')
56
+ .option('--skip-push', 'Skip Docker Hub push')
57
+ .option('--skip-server', 'Skip server pull/restart')
58
+ .option('-h, --host <host>', 'Host alias (vucore | cicore)')
59
+ .option('-c, --core <core>', 'Core name for cache clearing (e.g. cicore, vucore)')
60
+ .option('--deploy', 'Full deployment: pull, cache clear, recreate container')
61
+ .option('--dry-run', 'Show what would be done without executing')
62
+ .action(async (options) => {
63
+ await buildPHP(options);
64
+ });
65
+ build
66
+ .command('addon <name>')
67
+ .description('Build addon UI to CDN and deploy backend to server')
68
+ .option('-c, --core <name>', 'Core name', 'core1')
69
+ .option('--skip-cdn', 'Skip CDN upload')
70
+ .option('--skip-server', 'Skip server deploy')
71
+ .option('-h, --host <host>', 'Host alias (vucore | cicore)')
72
+ .option('--dry-run', 'Show what would be done without executing')
73
+ .action(async (addonName, options) => {
74
+ await buildAddon(addonName, options);
75
+ });
76
+ }
77
+ // ─────────────────────────────────────────────────────────────────────────
78
+ // vu build nuxt
79
+ // ─────────────────────────────────────────────────────────────────────────
80
+ export async function buildNuxt(options) {
81
+ const startTime = Date.now();
82
+ log.title('🔨 Build Nuxt Image + CDN Upload');
83
+ // Resolve host eagerly so a missing/typo'd --host fails before any work.
84
+ const cfg = options.host !== undefined ? getHostConfig(options.host) : undefined;
85
+ if (cfg !== undefined)
86
+ log.info(`Target: ${cfg.brandName}`);
87
+ log.info(`Skip build: ${options.skipBuild === true}`);
88
+ log.info(`Skip CDN: ${options.skipCdn === true}`);
89
+ log.info(`Skip push: ${options.skipPush === true}`);
90
+ log.info(`Dry run: ${options.dryRun === true}`);
91
+ log.blank();
92
+ if (!(await confirm('Start Nuxt build?', true))) {
93
+ log.warn('Cancelled');
94
+ return;
95
+ }
96
+ if (options.skipSafelist !== true) {
97
+ await runSafelistPipeline(options.dryRun === true);
98
+ }
99
+ if (options.skipBuild !== true) {
100
+ if (cfg === undefined) {
101
+ log.error('--host is required for build (image namespace comes from host config)');
102
+ process.exit(1);
103
+ }
104
+ await runImageBuild(cfg, 'nuxt');
105
+ if (options.skipPush !== true) {
106
+ await runImagePush(cfg, 'nuxt');
107
+ }
108
+ else {
109
+ log.warn('[skip] Docker Hub push');
110
+ }
111
+ }
112
+ else {
113
+ log.warn('[skip] Docker build (using existing image)');
114
+ }
115
+ if (options.skipCdn !== true) {
116
+ if (cfg === undefined) {
117
+ log.error('--host is required for CDN upload (cdn endpoint comes from host config)');
118
+ process.exit(1);
119
+ }
120
+ await uploadCoreUiBundle(cfg, options.dryRun === true);
121
+ }
122
+ else {
123
+ log.warn('[skip] CDN upload');
124
+ }
125
+ if (options.skipServer !== true && cfg !== undefined && options.deploy === true) {
126
+ await deployNuxtToHost(cfg, options.core ?? 'core1', options.dryRun === true);
127
+ }
128
+ log.blank();
129
+ log.success(`✨ Nuxt build complete in ${elapsed(startTime)}`);
130
+ }
131
+ async function runImageBuild(cfg, service) {
132
+ const sp = spinner(`Building ${service} image (linux/amd64) ...`).start();
133
+ const result = await buildImage(cfg, service, 'latest', { silent: true });
134
+ if (!result.success) {
135
+ sp.fail('Docker build failed');
136
+ log.error(result.stderr || result.stdout);
137
+ process.exit(1);
138
+ }
139
+ sp.succeed('Docker image built');
140
+ }
141
+ async function runImagePush(cfg, service) {
142
+ const sp = spinner('Pushing to Docker Hub ...').start();
143
+ const result = await pushImage(cfg, service, 'latest', { silent: true });
144
+ if (!result.success) {
145
+ sp.fail('Docker push failed');
146
+ log.error(result.stderr || result.stdout);
147
+ process.exit(1);
148
+ }
149
+ sp.succeed('Image pushed');
150
+ }
151
+ /**
152
+ * Extract `/app/.output/public/` from the freshly-built Nuxt image, zip it,
153
+ * and hand off to {@link cdnUploadZip}. The Worker unpacks the zip into
154
+ * `CoreUI/latest/**` in R2 server-side.
155
+ *
156
+ * No-op on hosts with cdnBypassForNuxt — `ops/cdn` honors that flag.
157
+ */
158
+ async function uploadCoreUiBundle(cfg, dryRun) {
159
+ if (cfg.cdnBypassForNuxt) {
160
+ log.info(`[cdn] ${cfg.brandName} bypasses CoreUI CDN — nothing to upload`);
161
+ return;
162
+ }
163
+ const imageRef = composeImageRef(cfg, 'nuxt');
164
+ const tempDir = joinPath(paths.dev.root, '.tmp-core-ui-latest');
165
+ const zipPath = joinPath(paths.dev.root, '.tmp-core-ui-latest.zip');
166
+ if (dryRun) {
167
+ log.warn(`[dry-run] extract ${imageRef}:/app/.output/public → ${tempDir}`);
168
+ log.warn(`[dry-run] zip ${tempDir} → ${zipPath}`);
169
+ log.warn(`[dry-run] cdnUploadZip ${zipPath}`);
170
+ return;
171
+ }
172
+ try {
173
+ await fs.remove(tempDir);
174
+ await fs.ensureDir(tempDir);
175
+ const createResult = await runLocal('docker', ['create', imageRef], { silent: true });
176
+ if (!createResult.success) {
177
+ log.error('Failed to create temp container for UI extract');
178
+ process.exit(1);
179
+ }
180
+ const containerId = createResult.stdout.trim();
181
+ try {
182
+ log.info(`Extracting CoreUI from ${containerId.substring(0, 12)} ...`);
183
+ const copyResult = await runLocal('docker', ['cp', `${containerId}:/app/.output/public/.`, tempDir], { silent: true });
184
+ if (!copyResult.success) {
185
+ log.error('Failed to extract UI files');
186
+ process.exit(1);
187
+ }
188
+ }
189
+ finally {
190
+ await runLocal('docker', ['rm', containerId], { silent: true });
191
+ }
192
+ await pruneNonCdnFiles(tempDir);
193
+ await zipDirectory(tempDir, zipPath);
194
+ await cdnCleanCoreUi(cfg);
195
+ const upload = await cdnUploadZip(cfg, zipPath, { version: 'latest' });
196
+ if (!upload.success) {
197
+ log.error(`CDN upload failed: ${upload.errorMessage}`);
198
+ process.exit(1);
199
+ }
200
+ log.success('CoreUI uploaded to CDN');
201
+ }
202
+ finally {
203
+ await fs.remove(tempDir).catch(() => undefined);
204
+ await fs.remove(zipPath).catch(() => undefined);
205
+ }
206
+ }
207
+ /**
208
+ * Full deployment sequence for Nuxt: server-side pull → retag for compose →
209
+ * cache clear → force-recreate container → CF zone purge.
210
+ */
211
+ async function deployNuxtToHost(cfg, core, dryRun) {
212
+ log.blank();
213
+ log.title('🚀 Deploy Nuxt');
214
+ log.info(`Target: ${cfg.brandName} (core: ${core})`);
215
+ if (dryRun) {
216
+ log.warn(`[dry-run] full Nuxt deploy to ${cfg.alias} for ${core}`);
217
+ return;
218
+ }
219
+ log.info('[1/4] pull image on server');
220
+ const pull = await pullImage(cfg, 'nuxt', 'latest', { silent: true });
221
+ if (!pull.success)
222
+ log.warn(`pull had issues (continuing): ${pull.stderr}`);
223
+ log.info('[2/4] retag for compose');
224
+ await retagForCompose(cfg, 'nuxt', 'latest', { silent: true });
225
+ log.info('[3/4] clear addon route cache + opcache');
226
+ await addonClearRouteCache(cfg, core);
227
+ log.info('[4/4] force-recreate Nuxt container');
228
+ const recreate = await composeForceRecreate(cfg, 'nuxt', { silent: true });
229
+ if (!recreate.success)
230
+ log.warn(`recreate had issues: ${recreate.stderr}`);
231
+ log.info('[+] purge Cloudflare zone cache');
232
+ await cfPurgeZone(cfg);
233
+ log.success('✅ Nuxt deploy complete');
234
+ }
235
+ // ─────────────────────────────────────────────────────────────────────────
236
+ // vu build php
237
+ // ─────────────────────────────────────────────────────────────────────────
238
+ export async function buildPHP(options) {
239
+ const startTime = Date.now();
240
+ log.title('🔨 Build PHP Image');
241
+ const cfg = options.host !== undefined ? getHostConfig(options.host) : undefined;
242
+ if (cfg !== undefined)
243
+ log.info(`Target: ${cfg.brandName}`);
244
+ log.info(`Skip push: ${options.skipPush === true}`);
245
+ log.info(`Dry run: ${options.dryRun === true}`);
246
+ log.blank();
247
+ if (!(await confirm('Start PHP build?', true))) {
248
+ log.warn('Cancelled');
249
+ return;
250
+ }
251
+ if (cfg === undefined) {
252
+ log.error('--host is required for build (image namespace comes from host config)');
253
+ process.exit(1);
254
+ }
255
+ await runImageBuild(cfg, 'php');
256
+ if (options.skipPush !== true) {
257
+ await runImagePush(cfg, 'php');
258
+ }
259
+ else {
260
+ log.warn('[skip] Docker Hub push');
261
+ }
262
+ if (options.skipServer !== true && options.deploy === true) {
263
+ await deployPhpToHost(cfg, options.core ?? 'core1', options.dryRun === true);
264
+ }
265
+ log.blank();
266
+ log.success(`✨ PHP build complete in ${elapsed(startTime)}`);
267
+ }
268
+ async function deployPhpToHost(cfg, core, dryRun) {
269
+ log.blank();
270
+ log.title('🚀 Deploy PHP');
271
+ log.info(`Target: ${cfg.brandName} (core: ${core})`);
272
+ if (dryRun) {
273
+ log.warn(`[dry-run] full PHP deploy to ${cfg.alias} for ${core}`);
274
+ return;
275
+ }
276
+ log.info('[1/5] pull image on server');
277
+ const pull = await pullImage(cfg, 'php', 'latest', { silent: true });
278
+ if (!pull.success)
279
+ log.warn(`pull had issues (continuing): ${pull.stderr}`);
280
+ log.info('[2/5] retag for compose');
281
+ await retagForCompose(cfg, 'php', 'latest', { silent: true });
282
+ log.info('[3/5] force-recreate PHP container');
283
+ const recreate = await composeForceRecreate(cfg, 'php', { silent: true });
284
+ if (!recreate.success)
285
+ log.warn(`recreate had issues: ${recreate.stderr}`);
286
+ log.info('[4/5] fix storage perms + clear caches');
287
+ await fixStoragePerms(cfg);
288
+ await addonClearRouteCache(cfg, core);
289
+ log.info('[5/5] purge Cloudflare zone cache');
290
+ await cfPurgeZone(cfg);
291
+ log.success('✅ PHP deploy complete');
292
+ }
293
+ /**
294
+ * Ensure `/var/www/storage` is owned by www-data (uid 82) and writable.
295
+ * Done after every PHP container recreate because the freshly-started
296
+ * container resets ownership of any volume-mounted dirs without owner pin.
297
+ */
298
+ async function fixStoragePerms(cfg) {
299
+ const phpContainer = cfg.containerNames.php;
300
+ const script = `docker exec -u root ${phpContainer} mkdir -p /var/www/storage/cache || true; ` +
301
+ `docker exec -u root ${phpContainer} chown -R www-data:www-data /var/www/storage || true; ` +
302
+ `docker exec -u root ${phpContainer} chmod -R 777 /var/www/storage || true`;
303
+ await runRemote(cfg, script, { silent: true });
304
+ }
305
+ // ─────────────────────────────────────────────────────────────────────────
306
+ // vu build addon
307
+ // ─────────────────────────────────────────────────────────────────────────
308
+ export async function buildAddon(addonName, options) {
309
+ const startTime = Date.now();
310
+ const core = options.core ?? 'core1';
311
+ log.title(`🔨 Build Addon: ${addonName}`);
312
+ const cfg = options.host !== undefined ? getHostConfig(options.host) : undefined;
313
+ if (cfg !== undefined)
314
+ log.info(`Target: ${cfg.brandName}`);
315
+ log.info(`Core: ${core}`);
316
+ log.info(`Skip CDN: ${options.skipCdn === true}`);
317
+ log.info(`Skip server: ${options.skipServer === true}`);
318
+ log.info(`Dry run: ${options.dryRun === true}`);
319
+ log.blank();
320
+ const addonPath = joinPath(paths.dev.root, 'addons', addonName);
321
+ const addonJsonPath = joinPath(addonPath, 'addon.json');
322
+ if (!(await fs.pathExists(addonJsonPath))) {
323
+ log.error(`addon.json not found: ${addonJsonPath}`);
324
+ process.exit(1);
325
+ }
326
+ const addonConfig = (await fs.readJson(addonJsonPath));
327
+ log.info(`Addon: ${addonConfig.name} v${addonConfig.version}`);
328
+ if (!(await confirm('Start addon build?', true))) {
329
+ log.warn('Cancelled');
330
+ return;
331
+ }
332
+ const tempCdnDir = joinPath(paths.dev.root, `.tmp-addon-${addonName}-latest`);
333
+ await runAddonViteBuild(addonName, addonPath, tempCdnDir, options.dryRun === true);
334
+ await copyAddonAssetsWithHash(addonPath, tempCdnDir);
335
+ if (cfg !== undefined) {
336
+ await writeAddonUiManifest(cfg, addonName, addonPath, tempCdnDir);
337
+ }
338
+ if (options.skipCdn !== true) {
339
+ if (cfg === undefined) {
340
+ log.error('--host is required for CDN upload (cdn endpoint comes from host config)');
341
+ process.exit(1);
342
+ }
343
+ await uploadAddonBundleToCdn(cfg, addonName, tempCdnDir, options.dryRun === true);
344
+ }
345
+ else {
346
+ log.warn('[skip] CDN upload');
347
+ }
348
+ const stagedBackendPath = joinPath(paths.dev.root, 'product', 'cores', core, 'addons', addonName);
349
+ if (options.dryRun === true) {
350
+ log.warn(`[dry-run] stage backend → ${stagedBackendPath}`);
351
+ }
352
+ else {
353
+ await stageAddonBackend(addonPath, addonJsonPath, tempCdnDir, stagedBackendPath);
354
+ }
355
+ if (options.skipServer !== true && cfg !== undefined) {
356
+ await deployAddonBackend(cfg, addonName, core, stagedBackendPath, options.dryRun === true);
357
+ }
358
+ else {
359
+ log.warn('[skip] Server deploy');
360
+ }
361
+ if (cfg !== undefined && options.dryRun !== true) {
362
+ await cfPurgeZone(cfg);
363
+ }
364
+ log.blank();
365
+ log.success(`✨ Addon build complete in ${elapsed(startTime)}`);
366
+ }
367
+ async function runAddonViteBuild(addonName, addonPath, tempCdnDir, dryRun) {
368
+ if (dryRun) {
369
+ log.warn(`[dry-run] vite build for ${addonName}`);
370
+ return;
371
+ }
372
+ await fs.remove(tempCdnDir);
373
+ await fs.ensureDir(tempCdnDir);
374
+ const ownConfig = joinPath(addonPath, 'vite.config.js');
375
+ const viteBin = joinPath(paths.dev.root, 'node_modules', '.bin', 'vite');
376
+ if (await fs.pathExists(ownConfig)) {
377
+ log.info(`Using addon's own vite.config.js`);
378
+ const build = await runLocal(viteBin, ['build'], { cwd: addonPath });
379
+ if (!build.success) {
380
+ log.error('Vite build failed');
381
+ log.error(build.stderr);
382
+ process.exit(1);
383
+ }
384
+ const distDir = joinPath(addonPath, 'dist');
385
+ if (await fs.pathExists(distDir)) {
386
+ await fs.copy(distDir, tempCdnDir);
387
+ }
388
+ const inputCss = joinPath(addonPath, 'ui', 'input.css');
389
+ if (await fs.pathExists(inputCss)) {
390
+ const tailwindConfig = joinPath(addonPath, 'ui', 'tailwind.config.js');
391
+ const args = ['tailwindcss'];
392
+ if (await fs.pathExists(tailwindConfig)) {
393
+ args.push('-c', tailwindConfig);
394
+ }
395
+ args.push('-i', inputCss, '-o', joinPath(tempCdnDir, 'style.css'), '--minify');
396
+ await runLocal('npx', args, { cwd: paths.dev.root });
397
+ }
398
+ }
399
+ else {
400
+ log.info('No vite.config.js — using legacy temp-build approach');
401
+ const tempBuildDir = joinPath(paths.dev.root, `.temp-addon-esm-${addonName}`);
402
+ try {
403
+ await fs.remove(tempBuildDir);
404
+ await fs.ensureDir(tempBuildDir);
405
+ for (const sub of ['ui', 'pages', 'locales', 'components']) {
406
+ const src = joinPath(addonPath, sub);
407
+ if (await fs.pathExists(src)) {
408
+ const dest = sub === 'ui' ? tempBuildDir : joinPath(tempBuildDir, sub);
409
+ await fs.copy(src, dest);
410
+ }
411
+ }
412
+ await fs.writeFile(joinPath(tempBuildDir, 'vite.config.js'), legacyViteConfig(addonName));
413
+ await fs.writeJson(joinPath(tempBuildDir, 'package.json'), {
414
+ name: `${addonName.toLowerCase()}-addon`,
415
+ version: '1.0.0',
416
+ type: 'module',
417
+ scripts: { build: 'vite build' },
418
+ });
419
+ const build = await runLocal(viteBin, ['build', '--config', 'vite.config.js'], {
420
+ cwd: tempBuildDir,
421
+ });
422
+ if (!build.success) {
423
+ log.error('Vite build failed');
424
+ log.error(build.stderr);
425
+ process.exit(1);
426
+ }
427
+ const distDir = joinPath(tempBuildDir, 'dist');
428
+ if (await fs.pathExists(distDir))
429
+ await fs.copy(distDir, tempCdnDir);
430
+ }
431
+ finally {
432
+ await fs.remove(tempBuildDir).catch(() => undefined);
433
+ }
434
+ }
435
+ log.success('Addon UI built');
436
+ }
437
+ async function copyAddonAssetsWithHash(addonPath, tempCdnDir) {
438
+ const assetsSrc = joinPath(addonPath, 'Assets');
439
+ if (!(await fs.pathExists(assetsSrc)))
440
+ return;
441
+ const assetsDst = joinPath(tempCdnDir, 'assets');
442
+ await fs.ensureDir(assetsDst);
443
+ await walkAndHashCopy(assetsSrc, assetsDst);
444
+ }
445
+ async function walkAndHashCopy(src, dst) {
446
+ const { createHash } = await import('node:crypto');
447
+ const items = await fs.readdir(src, { withFileTypes: true });
448
+ for (const item of items) {
449
+ const srcPath = joinPath(src, item.name);
450
+ if (item.isDirectory()) {
451
+ const dstSub = joinPath(dst, item.name);
452
+ await fs.ensureDir(dstSub);
453
+ await walkAndHashCopy(srcPath, dstSub);
454
+ }
455
+ else {
456
+ const content = await fs.readFile(srcPath);
457
+ const hash = createHash('md5').update(content).digest('hex').substring(0, 8);
458
+ const dotIdx = item.name.lastIndexOf('.');
459
+ const base = dotIdx === -1 ? item.name : item.name.slice(0, dotIdx);
460
+ const ext = dotIdx === -1 ? '' : item.name.slice(dotIdx);
461
+ const hashed = `${base}-${hash}${ext}`;
462
+ // Ship both: hashed for cache-busting (Vue components resolve through
463
+ // manifest), un-hashed for stable references (og:image, share cards).
464
+ await fs.copy(srcPath, joinPath(dst, hashed));
465
+ await fs.copy(srcPath, joinPath(dst, item.name));
466
+ }
467
+ }
468
+ }
469
+ async function writeAddonUiManifest(cfg, addonName, addonPath, tempCdnDir) {
470
+ const cdnBase = `${cfg.cdnBaseUrl}/addons/${addonName}/latest`;
471
+ const cssFiles = [];
472
+ const builtAssetsDir = joinPath(tempCdnDir, 'assets');
473
+ if (await fs.pathExists(builtAssetsDir)) {
474
+ for (const file of await fs.readdir(builtAssetsDir)) {
475
+ if (file.endsWith('.css'))
476
+ cssFiles.push(`${cdnBase}/assets/${file}`);
477
+ }
478
+ }
479
+ const chunkFiles = [];
480
+ const chunksDir = joinPath(tempCdnDir, 'chunks');
481
+ if (await fs.pathExists(chunksDir)) {
482
+ for (const file of await fs.readdir(chunksDir)) {
483
+ if (file.endsWith('.js'))
484
+ chunkFiles.push(`${cdnBase}/chunks/${file}`);
485
+ }
486
+ }
487
+ const imageAssets = {};
488
+ if (await fs.pathExists(builtAssetsDir)) {
489
+ await scanForImageAssets(builtAssetsDir, '', imageAssets, cdnBase);
490
+ }
491
+ let thumbnailUrl = '';
492
+ try {
493
+ const decl = (await fs.readJson(joinPath(addonPath, 'addon.json')));
494
+ const declared = typeof decl.thumbnail === 'string' ? decl.thumbnail : null;
495
+ if (declared !== null) {
496
+ const lookupKey = declared.replace(/^Assets\//i, '').replace(/^\//, '');
497
+ if (imageAssets[lookupKey] !== undefined)
498
+ thumbnailUrl = imageAssets[lookupKey];
499
+ }
500
+ }
501
+ catch {
502
+ // missing or unparseable addon.json — fall through to conventions
503
+ }
504
+ if (thumbnailUrl === '') {
505
+ for (const k of [
506
+ 'img/thumbnail.webp', 'thumbnail.webp',
507
+ 'img/thumbnail.png', 'thumbnail.png',
508
+ 'img/thumbnail.jpg', 'thumbnail.jpg',
509
+ 'img/thumbnail.jpeg', 'thumbnail.jpeg',
510
+ ]) {
511
+ if (imageAssets[k] !== undefined) {
512
+ thumbnailUrl = imageAssets[k];
513
+ break;
514
+ }
515
+ }
516
+ }
517
+ const manifest = {
518
+ addon: addonName,
519
+ version: 'latest',
520
+ cdn_base: cdnBase,
521
+ entry: `${cdnBase}/entry.js`,
522
+ css: cssFiles,
523
+ chunks: chunkFiles,
524
+ assets: imageAssets,
525
+ thumbnail: thumbnailUrl,
526
+ built_at: new Date().toISOString(),
527
+ };
528
+ await fs.writeJson(joinPath(tempCdnDir, 'ui-manifest.json'), manifest, { spaces: 2 });
529
+ log.info(`Manifest written with ${Object.keys(imageAssets).length} asset mappings`);
530
+ }
531
+ async function scanForImageAssets(dir, prefix, out, cdnBase) {
532
+ const items = await fs.readdir(dir, { withFileTypes: true });
533
+ for (const item of items) {
534
+ const rel = prefix === '' ? item.name : `${prefix}/${item.name}`;
535
+ if (item.isDirectory()) {
536
+ await scanForImageAssets(joinPath(dir, item.name), rel, out, cdnBase);
537
+ }
538
+ else if (/\.(png|jpe?g|jfif|gif|svg|webp|ico)$/i.test(item.name)) {
539
+ const match = item.name.match(/^(.+)-[a-f0-9]{8}(\.[^.]+)$/);
540
+ if (match !== null) {
541
+ const originalName = `${match[1]}${match[2]}`;
542
+ const originalPath = prefix === '' ? originalName : `${prefix}/${originalName}`;
543
+ out[originalPath] = `${cdnBase}/assets/${rel}`;
544
+ }
545
+ }
546
+ }
547
+ }
548
+ async function uploadAddonBundleToCdn(cfg, addonName, tempCdnDir, dryRun) {
549
+ const zipPath = joinPath(paths.dev.root, `.tmp-addon-${addonName}-latest.zip`);
550
+ if (dryRun) {
551
+ log.warn(`[dry-run] zip ${tempCdnDir} → ${zipPath}`);
552
+ log.warn(`[dry-run] cdnUploadZip addon=${addonName}`);
553
+ return;
554
+ }
555
+ try {
556
+ await zipDirectory(tempCdnDir, zipPath);
557
+ await cdnCleanAddon(cfg, addonName);
558
+ const upload = await cdnUploadZip(cfg, zipPath, { addon: addonName, version: 'latest' });
559
+ if (!upload.success) {
560
+ log.error(`Addon CDN upload failed: ${upload.errorMessage}`);
561
+ process.exit(1);
562
+ }
563
+ log.success('Addon UI uploaded to CDN');
564
+ }
565
+ finally {
566
+ await fs.remove(zipPath).catch(() => undefined);
567
+ }
568
+ }
569
+ async function stageAddonBackend(addonPath, addonJsonPath, tempCdnDir, destPath) {
570
+ await fs.ensureDir(destPath);
571
+ for (const entry of ['Backend', 'locales', 'Assets']) {
572
+ const src = joinPath(addonPath, entry);
573
+ if (await fs.pathExists(src))
574
+ await fs.copy(src, joinPath(destPath, entry));
575
+ }
576
+ await fs.copy(addonJsonPath, joinPath(destPath, 'addon.json'));
577
+ const manifestSrc = joinPath(tempCdnDir, 'ui-manifest.json');
578
+ if (await fs.pathExists(manifestSrc)) {
579
+ await fs.copy(manifestSrc, joinPath(destPath, 'ui-manifest.json'));
580
+ }
581
+ log.success(`Backend staged at ${destPath}`);
582
+ }
583
+ async function deployAddonBackend(cfg, addonName, core, stagedBackendPath, dryRun) {
584
+ const serverPath = `${cfg.coresHostPath}/${core}/addons/${addonName}`;
585
+ if (dryRun) {
586
+ log.warn(`[dry-run] upload ${stagedBackendPath}/. → ${cfg.sshUser}@${cfg.sshHostname}:${serverPath}/`);
587
+ return;
588
+ }
589
+ log.info('Cleaning existing addon directory on server ...');
590
+ await runRemote(cfg, `rm -rf ${shellQuote(serverPath)} && mkdir -p ${shellQuote(serverPath)}`, { silent: true });
591
+ log.info('Uploading addon files ...');
592
+ const upload = await uploadFile(cfg, `${stagedBackendPath}/.`, `${serverPath}/`, {
593
+ recursive: true,
594
+ });
595
+ if (!upload.success) {
596
+ log.error(`scp failed: ${upload.stderr}`);
597
+ process.exit(1);
598
+ }
599
+ log.info('Fixing permissions ...');
600
+ const containerAddonPath = `/var/www/cores/${core}/addons/${addonName}`;
601
+ const permScript = `chmod -R 777 ${shellQuote(serverPath)} || true; ` +
602
+ `docker exec -u root ${cfg.containerNames.php} chown -R www-data:www-data ${shellQuote(containerAddonPath)} || true; ` +
603
+ `docker exec -u root ${cfg.containerNames.php} chmod -R 777 ${shellQuote(containerAddonPath)} || true`;
604
+ await runRemote(cfg, permScript, { silent: true });
605
+ log.info('Restarting PHP + clearing caches ...');
606
+ await runRemote(cfg, `docker restart ${cfg.containerNames.php}`, { silent: true });
607
+ await addonClearRouteCache(cfg, core);
608
+ await runRemote(cfg, `docker exec ${cfg.containerNames.nginx} nginx -s reload`, { silent: true }).catch(() => undefined);
609
+ log.success(`Backend deployed: ${serverPath}`);
610
+ }
611
+ // ─────────────────────────────────────────────────────────────────────────
612
+ // Shared helpers
613
+ // ─────────────────────────────────────────────────────────────────────────
614
+ async function runSafelistPipeline(dryRun) {
615
+ if (dryRun) {
616
+ log.warn('[dry-run] vu addon extract-classes && vu addon generate-safelist');
617
+ return;
618
+ }
619
+ log.info('📦 Extracting addon Tailwind classes ...');
620
+ const extract = await runLocal('npx', ['vu', 'addon', 'extract-classes'], {
621
+ cwd: paths.dev.root,
622
+ });
623
+ if (!extract.success) {
624
+ log.warn('Class extraction failed (continuing with base safelist)');
625
+ return;
626
+ }
627
+ log.info('🧬 Generating Tailwind safelist regex patterns ...');
628
+ const gen = await runLocal('npx', ['vu', 'addon', 'generate-safelist'], {
629
+ cwd: paths.dev.root,
630
+ });
631
+ if (!gen.success) {
632
+ log.warn('Safelist generation failed (continuing with base safelist)');
633
+ }
634
+ }
635
+ async function pruneNonCdnFiles(dir) {
636
+ const entries = await fs.readdir(dir, { recursive: true });
637
+ for (const entry of entries) {
638
+ const p = joinPath(dir, entry);
639
+ const stat = await fs.stat(p);
640
+ if (!stat.isFile())
641
+ continue;
642
+ if (p.endsWith('.php') || p.endsWith('.br') || p.endsWith('.gz')) {
643
+ await fs.remove(p);
644
+ }
645
+ }
646
+ }
647
+ async function zipDirectory(srcDir, destZip) {
648
+ await new Promise((resolve, reject) => {
649
+ const output = createWriteStream(destZip);
650
+ const archive = archiver('zip', { zlib: { level: 9 } });
651
+ output.on('close', () => resolve());
652
+ output.on('error', reject);
653
+ archive.on('error', reject);
654
+ archive.pipe(output);
655
+ archive.directory(srcDir, false);
656
+ archive.finalize().catch(reject);
657
+ });
658
+ }
659
+ function shellQuote(s) {
660
+ return `'${s.replace(/'/g, `'\\''`)}'`;
661
+ }
662
+ function elapsed(startMs) {
663
+ const seconds = (Date.now() - startMs) / 1000;
664
+ if (seconds < 60)
665
+ return `${seconds.toFixed(1)}s`;
666
+ const minutes = Math.floor(seconds / 60);
667
+ const remSec = seconds - minutes * 60;
668
+ return `${minutes}m${remSec.toFixed(0)}s`;
669
+ }
670
+ function legacyViteConfig(addonName) {
671
+ return `import { defineConfig } from 'vite'
672
+ import vue from '@vitejs/plugin-vue'
673
+ import { resolve } from 'path'
674
+
675
+ // Nuxt bare-specifier shims (bundled inline — not externalized)
676
+ const SHIMS_DIR = resolve(__dirname, '../../addons/_shims')
677
+
678
+ export default defineConfig({
679
+ plugins: [vue()],
680
+ css: { postcss: {} },
681
+ // Nuxt compile-time macros → no-op (standalone Vite has no Nuxt macro plugin)
682
+ define: {
683
+ definePageMeta: '(() => {})',
684
+ },
685
+ resolve: {
686
+ alias: {
687
+ 'vue': '@cicore/shared',
688
+ '#imports': resolve(SHIMS_DIR, 'nuxt-imports.js'),
689
+ '#app': resolve(SHIMS_DIR, 'nuxt-app.js'),
690
+ '#build': resolve(SHIMS_DIR, 'nuxt-build.js'),
691
+ },
692
+ },
693
+ build: {
694
+ lib: {
695
+ entry: resolve(__dirname, 'entry.js'),
696
+ name: '${addonName}',
697
+ fileName: () => 'entry.js',
698
+ formats: ['es']
699
+ },
700
+ rollupOptions: {
701
+ external(id) {
702
+ if (id.startsWith('#')) return false
703
+ if (id.startsWith('@/')) return false // @/-alias = addon-local, bundle
704
+ if (id === 'vue' || id.startsWith('vue/') || id.startsWith('@vue/')) return true
705
+ if (id === '@cicore/shared' || id.startsWith('@cicore/')) return true
706
+ if (['vue-router', 'pinia', '@vueuse/core', '@nuxt/ui'].includes(id)) return true
707
+ if (/^@iconify\\//.test(id)) return true
708
+ return false
709
+ },
710
+ output: {
711
+ format: 'es',
712
+ entryFileNames: 'entry.js',
713
+ chunkFileNames: 'chunks/[name]-[hash].js',
714
+ assetFileNames: 'assets/[name]-[hash][extname]',
715
+ paths: { 'vue': '@cicore/shared' },
716
+ exports: 'named'
717
+ }
718
+ },
719
+ outDir: 'dist',
720
+ sourcemap: false,
721
+ minify: 'esbuild',
722
+ emptyOutDir: true
723
+ }
724
+ })`;
725
+ }
726
+ //# sourceMappingURL=index.js.map