@woodsportal/hubspot-kit 1.0.41 → 1.0.42-dev.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 (598) hide show
  1. package/CHANGELOG.md +14 -1
  2. package/README.md +191 -157
  3. package/dist/{chunk-S2YJDWZU.js → chunk-NWBK3XBZ.js} +1 -1
  4. package/dist/chunk-NWBK3XBZ.js.map +1 -0
  5. package/dist/chunk-WHYTQEZX.js +554 -0
  6. package/dist/chunk-WHYTQEZX.js.map +1 -0
  7. package/dist/cli.js +1778 -864
  8. package/dist/cli.js.map +1 -1
  9. package/dist/ensure-wphs-workspace-5S3GTO6K.js +234 -0
  10. package/dist/ensure-wphs-workspace-5S3GTO6K.js.map +1 -0
  11. package/dist/index.d.ts +12 -0
  12. package/dist/index.js +6 -2
  13. package/dist/index.js.map +1 -1
  14. package/dist/scaffold-page-lifecycle-EHMPWPPE.js +194 -0
  15. package/dist/scaffold-page-lifecycle-EHMPWPPE.js.map +1 -0
  16. package/dist/vite/index.js +1 -1
  17. package/examples/greenfield/module-hybrid-cdn/public/fields.json +73 -28
  18. package/examples/greenfield/module-hybrid-cdn/public/module.html +21 -5
  19. package/examples/greenfield/module-hybrid-cdn/src/assets/css/main.dev.css +15 -0
  20. package/examples/greenfield/module-hybrid-cdn/src/components/layout/starter-nav.tsx +12 -15
  21. package/examples/greenfield/module-hybrid-cdn/src/config/starter-nav.ts +9 -0
  22. package/examples/greenfield/module-hybrid-cdn/src/features/starter/components/starter-home.tsx +25 -29
  23. package/examples/greenfield/module-hybrid-cdn/src/features/starter/hooks/starter-preview-values.ts +60 -0
  24. package/examples/greenfield/module-hybrid-cdn/src/features/starter/hooks/use-starter-preview.dev.ts +18 -0
  25. package/examples/greenfield/module-hybrid-cdn/src/features/starter/hooks/use-starter-preview.ts +20 -37
  26. package/examples/greenfield/module-hybrid-cdn-auth-full/.env.dev.example +3 -0
  27. package/examples/greenfield/module-hybrid-cdn-auth-full/.env.local.example +6 -0
  28. package/examples/greenfield/module-hybrid-cdn-auth-full/.env.prod.example +5 -0
  29. package/examples/greenfield/module-hybrid-cdn-auth-full/.env.stg.example +3 -0
  30. package/examples/greenfield/module-hybrid-cdn-auth-full/.wphs/README.md +17 -0
  31. package/examples/greenfield/module-hybrid-cdn-auth-full/README.md +59 -0
  32. package/examples/greenfield/module-hybrid-cdn-auth-full/hubspot.config.yml.example +10 -0
  33. package/examples/greenfield/module-hybrid-cdn-auth-full/index.html +12 -0
  34. package/examples/greenfield/module-hybrid-cdn-auth-full/package.json +34 -0
  35. package/examples/greenfield/module-hybrid-cdn-auth-full/portal.cdn.json.example +8 -0
  36. package/examples/greenfield/module-hybrid-cdn-auth-full/public/fields.json +100 -0
  37. package/examples/greenfield/module-hybrid-cdn-auth-full/public/meta.json +9 -0
  38. package/examples/greenfield/module-hybrid-cdn-auth-full/public/module.html +38 -0
  39. package/examples/greenfield/module-hybrid-cdn-auth-full/src/app.tsx +172 -0
  40. package/examples/greenfield/module-hybrid-cdn-auth-full/src/assets/css/main.dev.css +23 -0
  41. package/examples/greenfield/module-hybrid-cdn-auth-full/src/assets/css/main.prod.css +12 -0
  42. package/examples/greenfield/module-hybrid-cdn-auth-full/src/assets/css/tailwind.css.config.css +7 -0
  43. package/examples/greenfield/module-hybrid-cdn-auth-full/src/cdn/app-entry.ts +48 -0
  44. package/examples/greenfield/module-hybrid-cdn-auth-full/src/cdn/vendor-entry.ts +28 -0
  45. package/examples/greenfield/module-hybrid-cdn-auth-full/src/components/Layouts/AppLayoutWrapper.tsx +38 -0
  46. package/examples/greenfield/module-hybrid-cdn-auth-full/src/components/Layouts/AuthLayout.tsx +7 -0
  47. package/examples/greenfield/module-hybrid-cdn-auth-full/src/components/Layouts/DefaultLayout.tsx +5 -0
  48. package/examples/greenfield/module-hybrid-cdn-auth-full/src/components/Layouts/MainLayout.tsx +42 -0
  49. package/examples/greenfield/module-hybrid-cdn-auth-full/src/components/Layouts/UnauthorizedLayout.tsx +5 -0
  50. package/examples/greenfield/module-hybrid-cdn-auth-full/src/components/Layouts/index.ts +13 -0
  51. package/examples/greenfield/module-hybrid-cdn-auth-full/src/components/layout/section-card.tsx +20 -0
  52. package/examples/greenfield/module-hybrid-cdn-auth-full/src/components/layout/starter-nav.tsx +28 -0
  53. package/examples/greenfield/module-hybrid-cdn-auth-full/src/components/layout/starter-shell.tsx +18 -0
  54. package/examples/greenfield/module-hybrid-cdn-auth-full/src/components/ui/Button.tsx +22 -0
  55. package/examples/greenfield/module-hybrid-cdn-auth-full/src/components/ui/Form.tsx +22 -0
  56. package/examples/greenfield/module-hybrid-cdn-auth-full/src/components/ui/Input.tsx +12 -0
  57. package/examples/greenfield/module-hybrid-cdn-auth-full/src/components/ui/Toaster.tsx +5 -0
  58. package/examples/greenfield/module-hybrid-cdn-auth-full/src/components/ui/auth/SsoLoginButton.tsx +34 -0
  59. package/examples/greenfield/module-hybrid-cdn-auth-full/src/components/ui/link.tsx +10 -0
  60. package/examples/greenfield/module-hybrid-cdn-auth-full/src/components/ui/loader/loader.tsx +18 -0
  61. package/examples/greenfield/module-hybrid-cdn-auth-full/src/config/auth-tier.ts +4 -0
  62. package/examples/greenfield/module-hybrid-cdn-auth-full/src/config/nav-scaffold.ts +4 -0
  63. package/examples/greenfield/module-hybrid-cdn-auth-full/src/config/portal-nav.ts +9 -0
  64. package/examples/greenfield/module-hybrid-cdn-auth-full/src/config/router.ts +1 -0
  65. package/examples/greenfield/module-hybrid-cdn-auth-full/src/config/routes.ts +10 -0
  66. package/examples/greenfield/module-hybrid-cdn-auth-full/src/data/hubSpotContext.ts +57 -0
  67. package/examples/greenfield/module-hybrid-cdn-auth-full/src/data/sync-sdk-hub-context.ts +44 -0
  68. package/examples/greenfield/module-hybrid-cdn-auth-full/src/defaultData.ts +2 -0
  69. package/examples/greenfield/module-hybrid-cdn-auth-full/src/env.ts +59 -0
  70. package/examples/greenfield/module-hybrid-cdn-auth-full/src/features/auth/app-bootstrap-guards.ts +14 -0
  71. package/examples/greenfield/module-hybrid-cdn-auth-full/src/features/auth/auth-navigation.ts +81 -0
  72. package/examples/greenfield/module-hybrid-cdn-auth-full/src/features/auth/auth-route-paths.ts +29 -0
  73. package/examples/greenfield/module-hybrid-cdn-auth-full/src/features/auth/auth-session-events.ts +2 -0
  74. package/examples/greenfield/module-hybrid-cdn-auth-full/src/features/auth/auth-session.ts +51 -0
  75. package/examples/greenfield/module-hybrid-cdn-auth-full/src/features/auth/clear-client-auth-session.ts +62 -0
  76. package/examples/greenfield/module-hybrid-cdn-auth-full/src/features/auth/components/starter-login.tsx +83 -0
  77. package/examples/greenfield/module-hybrid-cdn-auth-full/src/features/auth/login-email-storage.ts +64 -0
  78. package/examples/greenfield/module-hybrid-cdn-auth-full/src/features/auth/login-session-bridge.ts +74 -0
  79. package/examples/greenfield/module-hybrid-cdn-auth-full/src/features/auth/mfa-otp-utils.ts +24 -0
  80. package/examples/greenfield/module-hybrid-cdn-auth-full/src/features/auth/mfa-pending-storage.ts +108 -0
  81. package/examples/greenfield/module-hybrid-cdn-auth-full/src/features/auth/mfa-portal.ts +45 -0
  82. package/examples/greenfield/module-hybrid-cdn-auth-full/src/features/auth/post-login-path.ts +17 -0
  83. package/examples/greenfield/module-hybrid-cdn-auth-full/src/features/auth/resolve-auth-login-error-message.ts +52 -0
  84. package/examples/greenfield/module-hybrid-cdn-auth-full/src/features/auth/resolve-post-login-path.ts +21 -0
  85. package/examples/greenfield/module-hybrid-cdn-auth-full/src/features/auth/sso-callback.ts +120 -0
  86. package/examples/greenfield/module-hybrid-cdn-auth-full/src/features/auth/use-mfa-login-otp-flow.ts +78 -0
  87. package/examples/greenfield/module-hybrid-cdn-auth-full/src/features/auth/use-portal-session.ts +25 -0
  88. package/examples/greenfield/module-hybrid-cdn-auth-full/src/features/auth/use-signed-in-email.ts +55 -0
  89. package/examples/greenfield/module-hybrid-cdn-auth-full/src/features/auth/use-sso-callback-handler.ts +92 -0
  90. package/examples/greenfield/module-hybrid-cdn-auth-full/src/features/starter/components/starter-guide-page.tsx +210 -0
  91. package/examples/greenfield/module-hybrid-cdn-auth-full/src/features/starter/components/starter-home-page.tsx +21 -0
  92. package/examples/greenfield/module-hybrid-cdn-auth-full/src/features/starter/components/starter-home.tsx +48 -0
  93. package/examples/greenfield/module-hybrid-cdn-auth-full/src/features/starter/components/starter-page-frame.tsx +25 -0
  94. package/examples/greenfield/module-hybrid-cdn-auth-full/src/features/starter/components/starter-protected-page.tsx +54 -0
  95. package/examples/greenfield/module-hybrid-cdn-auth-full/src/features/starter/components/starter-single-page.tsx +17 -0
  96. package/examples/greenfield/module-hybrid-cdn-auth-full/src/features/starter/content/auth-guide-content.ts +85 -0
  97. package/examples/greenfield/module-hybrid-cdn-auth-full/src/features/starter/content/woodscli-guide-content.ts +86 -0
  98. package/examples/greenfield/module-hybrid-cdn-auth-full/src/features/starter/hooks/starter-preview-values.ts +60 -0
  99. package/examples/greenfield/module-hybrid-cdn-auth-full/src/features/starter/hooks/use-starter-preview.dev.ts +18 -0
  100. package/examples/greenfield/module-hybrid-cdn-auth-full/src/features/starter/hooks/use-starter-preview.ts +30 -0
  101. package/examples/greenfield/module-hybrid-cdn-auth-full/src/features/starter/index.ts +6 -0
  102. package/examples/greenfield/module-hybrid-cdn-auth-full/src/hubspot/bootstrap.ts +16 -0
  103. package/examples/greenfield/module-hybrid-cdn-auth-full/src/hubspot/default-data.ts +7 -0
  104. package/examples/greenfield/module-hybrid-cdn-auth-full/src/hubspot/is-cms-editor.ts +78 -0
  105. package/examples/greenfield/module-hybrid-cdn-auth-full/src/hubspot/module-entry.tsx +9 -0
  106. package/examples/greenfield/module-hybrid-cdn-auth-full/src/hubspot/types.ts +17 -0
  107. package/examples/greenfield/module-hybrid-cdn-auth-full/src/integrations/configure-woodsportal-sdk.ts +98 -0
  108. package/examples/greenfield/module-hybrid-cdn-auth-full/src/integrations/tanstack-query/root-provider.tsx +18 -0
  109. package/examples/greenfield/module-hybrid-cdn-auth-full/src/integrations/tanstack-router/router.tsx +39 -0
  110. package/examples/greenfield/module-hybrid-cdn-auth-full/src/integrations/woodsportal-sdk.ts +37 -0
  111. package/examples/greenfield/module-hybrid-cdn-auth-full/src/main.tsx +5 -0
  112. package/examples/greenfield/module-hybrid-cdn-auth-full/src/portal/api-endpoint.ts +11 -0
  113. package/examples/greenfield/module-hybrid-cdn-auth-full/src/portal/ensure-portal-cdn-stylesheet.ts +115 -0
  114. package/examples/greenfield/module-hybrid-cdn-auth-full/src/portal/mount-dev-preview.tsx +12 -0
  115. package/examples/greenfield/module-hybrid-cdn-auth-full/src/portal/mount-portal.tsx +57 -0
  116. package/examples/greenfield/module-hybrid-cdn-auth-full/src/routeTree.gen.ts +182 -0
  117. package/examples/greenfield/module-hybrid-cdn-auth-full/src/routes/__root.tsx +12 -0
  118. package/examples/greenfield/module-hybrid-cdn-auth-full/src/routes/_app/portal.tsx +10 -0
  119. package/examples/greenfield/module-hybrid-cdn-auth-full/src/routes/_auth/login/sso.tsx +27 -0
  120. package/examples/greenfield/module-hybrid-cdn-auth-full/src/routes/_auth/login.tsx +10 -0
  121. package/examples/greenfield/module-hybrid-cdn-auth-full/src/routes/_auth/two-fa.tsx +91 -0
  122. package/examples/greenfield/module-hybrid-cdn-auth-full/src/routes/guide.tsx +9 -0
  123. package/examples/greenfield/module-hybrid-cdn-auth-full/src/routes/index.tsx +8 -0
  124. package/examples/greenfield/module-hybrid-cdn-auth-full/src/routes/unauthorized.tsx +50 -0
  125. package/examples/greenfield/module-hybrid-cdn-auth-full/src/utils/ValidationSchema.ts +1 -0
  126. package/examples/greenfield/module-hybrid-cdn-auth-full/src/utils/cookie.ts +23 -0
  127. package/examples/greenfield/module-hybrid-cdn-auth-full/src/utils/normalize-router-path.ts +10 -0
  128. package/examples/greenfield/module-hybrid-cdn-auth-full/src/utils/private-route.tsx +100 -0
  129. package/examples/greenfield/module-hybrid-cdn-auth-full/src/utils/public-route.tsx +64 -0
  130. package/examples/greenfield/module-hybrid-cdn-auth-full/src/utils/resolve-api-base-url.ts +44 -0
  131. package/examples/greenfield/module-hybrid-cdn-auth-full/src/utils/safe-router-navigation.ts +82 -0
  132. package/examples/greenfield/module-hybrid-cdn-auth-full/tsconfig.json +20 -0
  133. package/examples/greenfield/module-hybrid-cdn-auth-full/vite.config.cdn-app.mjs +1 -0
  134. package/examples/greenfield/module-hybrid-cdn-auth-full/vite.config.cdn-esm.mjs +1 -0
  135. package/examples/greenfield/module-hybrid-cdn-auth-full/vite.config.cdn-vendor.mjs +1 -0
  136. package/examples/greenfield/module-hybrid-cdn-auth-full/vite.config.dev.mjs +1 -0
  137. package/examples/greenfield/module-hybrid-cdn-auth-full/vite.config.hubspot.mjs +1 -0
  138. package/examples/greenfield/module-hybrid-cdn-auth-full/vite.config.monolith.mjs +8 -0
  139. package/examples/greenfield/module-hybrid-cdn-auth-full/woodscli.json +38 -0
  140. package/examples/greenfield/module-hybrid-cdn-auth-full/wphs.adapter.ts +7 -0
  141. package/examples/greenfield/module-hybrid-cdn-auth-sso/.env.dev.example +3 -0
  142. package/examples/greenfield/module-hybrid-cdn-auth-sso/.env.local.example +6 -0
  143. package/examples/greenfield/module-hybrid-cdn-auth-sso/.env.prod.example +5 -0
  144. package/examples/greenfield/module-hybrid-cdn-auth-sso/.env.stg.example +3 -0
  145. package/examples/greenfield/module-hybrid-cdn-auth-sso/.wphs/README.md +17 -0
  146. package/examples/greenfield/module-hybrid-cdn-auth-sso/README.md +59 -0
  147. package/examples/greenfield/module-hybrid-cdn-auth-sso/hubspot.config.yml.example +10 -0
  148. package/examples/greenfield/module-hybrid-cdn-auth-sso/index.html +12 -0
  149. package/examples/greenfield/module-hybrid-cdn-auth-sso/package.json +34 -0
  150. package/examples/greenfield/module-hybrid-cdn-auth-sso/portal.cdn.json.example +8 -0
  151. package/examples/greenfield/module-hybrid-cdn-auth-sso/public/fields.json +100 -0
  152. package/examples/greenfield/module-hybrid-cdn-auth-sso/public/meta.json +9 -0
  153. package/examples/greenfield/module-hybrid-cdn-auth-sso/public/module.html +38 -0
  154. package/examples/greenfield/module-hybrid-cdn-auth-sso/src/app.tsx +180 -0
  155. package/examples/greenfield/module-hybrid-cdn-auth-sso/src/assets/css/main.dev.css +23 -0
  156. package/examples/greenfield/module-hybrid-cdn-auth-sso/src/assets/css/main.prod.css +12 -0
  157. package/examples/greenfield/module-hybrid-cdn-auth-sso/src/assets/css/tailwind.css.config.css +7 -0
  158. package/examples/greenfield/module-hybrid-cdn-auth-sso/src/cdn/app-entry.ts +48 -0
  159. package/examples/greenfield/module-hybrid-cdn-auth-sso/src/cdn/vendor-entry.ts +28 -0
  160. package/examples/greenfield/module-hybrid-cdn-auth-sso/src/components/Layouts/AppLayoutWrapper.tsx +38 -0
  161. package/examples/greenfield/module-hybrid-cdn-auth-sso/src/components/Layouts/AuthLayout.tsx +7 -0
  162. package/examples/greenfield/module-hybrid-cdn-auth-sso/src/components/Layouts/DefaultLayout.tsx +5 -0
  163. package/examples/greenfield/module-hybrid-cdn-auth-sso/src/components/Layouts/MainLayout.tsx +42 -0
  164. package/examples/greenfield/module-hybrid-cdn-auth-sso/src/components/Layouts/UnauthorizedLayout.tsx +5 -0
  165. package/examples/greenfield/module-hybrid-cdn-auth-sso/src/components/Layouts/index.ts +13 -0
  166. package/examples/greenfield/module-hybrid-cdn-auth-sso/src/components/layout/section-card.tsx +20 -0
  167. package/examples/greenfield/module-hybrid-cdn-auth-sso/src/components/layout/starter-nav.tsx +28 -0
  168. package/examples/greenfield/module-hybrid-cdn-auth-sso/src/components/layout/starter-shell.tsx +18 -0
  169. package/examples/greenfield/module-hybrid-cdn-auth-sso/src/components/ui/Button.tsx +22 -0
  170. package/examples/greenfield/module-hybrid-cdn-auth-sso/src/components/ui/Form.tsx +22 -0
  171. package/examples/greenfield/module-hybrid-cdn-auth-sso/src/components/ui/Input.tsx +12 -0
  172. package/examples/greenfield/module-hybrid-cdn-auth-sso/src/components/ui/Toaster.tsx +5 -0
  173. package/examples/greenfield/module-hybrid-cdn-auth-sso/src/components/ui/auth/SsoLoginButton.tsx +34 -0
  174. package/examples/greenfield/module-hybrid-cdn-auth-sso/src/components/ui/link.tsx +10 -0
  175. package/examples/greenfield/module-hybrid-cdn-auth-sso/src/components/ui/loader/loader.tsx +18 -0
  176. package/examples/greenfield/module-hybrid-cdn-auth-sso/src/config/auth-tier.ts +4 -0
  177. package/examples/greenfield/module-hybrid-cdn-auth-sso/src/config/nav-scaffold.ts +4 -0
  178. package/examples/greenfield/module-hybrid-cdn-auth-sso/src/config/portal-nav.ts +9 -0
  179. package/examples/greenfield/module-hybrid-cdn-auth-sso/src/config/router.ts +1 -0
  180. package/examples/greenfield/module-hybrid-cdn-auth-sso/src/config/routes.ts +10 -0
  181. package/examples/greenfield/module-hybrid-cdn-auth-sso/src/data/hubSpotContext.ts +57 -0
  182. package/examples/greenfield/module-hybrid-cdn-auth-sso/src/data/sync-sdk-hub-context.ts +44 -0
  183. package/examples/greenfield/module-hybrid-cdn-auth-sso/src/defaultData.ts +2 -0
  184. package/examples/greenfield/module-hybrid-cdn-auth-sso/src/env.ts +59 -0
  185. package/examples/greenfield/module-hybrid-cdn-auth-sso/src/features/auth/app-bootstrap-guards.ts +14 -0
  186. package/examples/greenfield/module-hybrid-cdn-auth-sso/src/features/auth/auth-navigation.ts +81 -0
  187. package/examples/greenfield/module-hybrid-cdn-auth-sso/src/features/auth/auth-route-paths.ts +29 -0
  188. package/examples/greenfield/module-hybrid-cdn-auth-sso/src/features/auth/auth-session-events.ts +2 -0
  189. package/examples/greenfield/module-hybrid-cdn-auth-sso/src/features/auth/auth-session.ts +26 -0
  190. package/examples/greenfield/module-hybrid-cdn-auth-sso/src/features/auth/clear-client-auth-session.ts +62 -0
  191. package/examples/greenfield/module-hybrid-cdn-auth-sso/src/features/auth/components/starter-login.tsx +83 -0
  192. package/examples/greenfield/module-hybrid-cdn-auth-sso/src/features/auth/login-email-storage.ts +64 -0
  193. package/examples/greenfield/module-hybrid-cdn-auth-sso/src/features/auth/login-session-bridge.ts +72 -0
  194. package/examples/greenfield/module-hybrid-cdn-auth-sso/src/features/auth/post-login-path.ts +17 -0
  195. package/examples/greenfield/module-hybrid-cdn-auth-sso/src/features/auth/resolve-auth-login-error-message.ts +52 -0
  196. package/examples/greenfield/module-hybrid-cdn-auth-sso/src/features/auth/resolve-post-login-path.ts +21 -0
  197. package/examples/greenfield/module-hybrid-cdn-auth-sso/src/features/auth/sso-callback.ts +120 -0
  198. package/examples/greenfield/module-hybrid-cdn-auth-sso/src/features/auth/use-portal-session.ts +25 -0
  199. package/examples/greenfield/module-hybrid-cdn-auth-sso/src/features/auth/use-signed-in-email.ts +55 -0
  200. package/examples/greenfield/module-hybrid-cdn-auth-sso/src/features/auth/use-sso-callback-handler.ts +92 -0
  201. package/examples/greenfield/module-hybrid-cdn-auth-sso/src/features/starter/components/starter-guide-page.tsx +210 -0
  202. package/examples/greenfield/module-hybrid-cdn-auth-sso/src/features/starter/components/starter-home-page.tsx +21 -0
  203. package/examples/greenfield/module-hybrid-cdn-auth-sso/src/features/starter/components/starter-home.tsx +48 -0
  204. package/examples/greenfield/module-hybrid-cdn-auth-sso/src/features/starter/components/starter-page-frame.tsx +25 -0
  205. package/examples/greenfield/module-hybrid-cdn-auth-sso/src/features/starter/components/starter-protected-page.tsx +54 -0
  206. package/examples/greenfield/module-hybrid-cdn-auth-sso/src/features/starter/components/starter-single-page.tsx +17 -0
  207. package/examples/greenfield/module-hybrid-cdn-auth-sso/src/features/starter/content/auth-guide-content.ts +85 -0
  208. package/examples/greenfield/module-hybrid-cdn-auth-sso/src/features/starter/content/woodscli-guide-content.ts +86 -0
  209. package/examples/greenfield/module-hybrid-cdn-auth-sso/src/features/starter/hooks/starter-preview-values.ts +60 -0
  210. package/examples/greenfield/module-hybrid-cdn-auth-sso/src/features/starter/hooks/use-starter-preview.dev.ts +18 -0
  211. package/examples/greenfield/module-hybrid-cdn-auth-sso/src/features/starter/hooks/use-starter-preview.ts +30 -0
  212. package/examples/greenfield/module-hybrid-cdn-auth-sso/src/features/starter/index.ts +6 -0
  213. package/examples/greenfield/module-hybrid-cdn-auth-sso/src/hubspot/bootstrap.ts +16 -0
  214. package/examples/greenfield/module-hybrid-cdn-auth-sso/src/hubspot/default-data.ts +7 -0
  215. package/examples/greenfield/module-hybrid-cdn-auth-sso/src/hubspot/is-cms-editor.ts +78 -0
  216. package/examples/greenfield/module-hybrid-cdn-auth-sso/src/hubspot/module-entry.tsx +9 -0
  217. package/examples/greenfield/module-hybrid-cdn-auth-sso/src/hubspot/types.ts +17 -0
  218. package/examples/greenfield/module-hybrid-cdn-auth-sso/src/integrations/configure-woodsportal-sdk.ts +94 -0
  219. package/examples/greenfield/module-hybrid-cdn-auth-sso/src/integrations/tanstack-query/root-provider.tsx +18 -0
  220. package/examples/greenfield/module-hybrid-cdn-auth-sso/src/integrations/tanstack-router/router.tsx +39 -0
  221. package/examples/greenfield/module-hybrid-cdn-auth-sso/src/integrations/woodsportal-sdk.ts +37 -0
  222. package/examples/greenfield/module-hybrid-cdn-auth-sso/src/main.tsx +5 -0
  223. package/examples/greenfield/module-hybrid-cdn-auth-sso/src/portal/api-endpoint.ts +11 -0
  224. package/examples/greenfield/module-hybrid-cdn-auth-sso/src/portal/ensure-portal-cdn-stylesheet.ts +115 -0
  225. package/examples/greenfield/module-hybrid-cdn-auth-sso/src/portal/mount-dev-preview.tsx +12 -0
  226. package/examples/greenfield/module-hybrid-cdn-auth-sso/src/portal/mount-portal.tsx +57 -0
  227. package/examples/greenfield/module-hybrid-cdn-auth-sso/src/routeTree.gen.ts +155 -0
  228. package/examples/greenfield/module-hybrid-cdn-auth-sso/src/routes/__root.tsx +12 -0
  229. package/examples/greenfield/module-hybrid-cdn-auth-sso/src/routes/_app/portal.tsx +10 -0
  230. package/examples/greenfield/module-hybrid-cdn-auth-sso/src/routes/_auth/login/sso.tsx +27 -0
  231. package/examples/greenfield/module-hybrid-cdn-auth-sso/src/routes/_auth/login.tsx +10 -0
  232. package/examples/greenfield/module-hybrid-cdn-auth-sso/src/routes/guide.tsx +9 -0
  233. package/examples/greenfield/module-hybrid-cdn-auth-sso/src/routes/index.tsx +8 -0
  234. package/examples/greenfield/module-hybrid-cdn-auth-sso/src/routes/unauthorized.tsx +50 -0
  235. package/examples/greenfield/module-hybrid-cdn-auth-sso/src/utils/ValidationSchema.ts +1 -0
  236. package/examples/greenfield/module-hybrid-cdn-auth-sso/src/utils/cookie.ts +23 -0
  237. package/examples/greenfield/module-hybrid-cdn-auth-sso/src/utils/normalize-router-path.ts +10 -0
  238. package/examples/greenfield/module-hybrid-cdn-auth-sso/src/utils/private-route.tsx +100 -0
  239. package/examples/greenfield/module-hybrid-cdn-auth-sso/src/utils/public-route.tsx +64 -0
  240. package/examples/greenfield/module-hybrid-cdn-auth-sso/src/utils/resolve-api-base-url.ts +44 -0
  241. package/examples/greenfield/module-hybrid-cdn-auth-sso/src/utils/safe-router-navigation.ts +82 -0
  242. package/examples/greenfield/module-hybrid-cdn-auth-sso/tsconfig.json +20 -0
  243. package/examples/greenfield/module-hybrid-cdn-auth-sso/vite.config.cdn-app.mjs +1 -0
  244. package/examples/greenfield/module-hybrid-cdn-auth-sso/vite.config.cdn-esm.mjs +1 -0
  245. package/examples/greenfield/module-hybrid-cdn-auth-sso/vite.config.cdn-vendor.mjs +1 -0
  246. package/examples/greenfield/module-hybrid-cdn-auth-sso/vite.config.dev.mjs +1 -0
  247. package/examples/greenfield/module-hybrid-cdn-auth-sso/vite.config.hubspot.mjs +1 -0
  248. package/examples/greenfield/module-hybrid-cdn-auth-sso/vite.config.monolith.mjs +8 -0
  249. package/examples/greenfield/module-hybrid-cdn-auth-sso/woodscli.json +38 -0
  250. package/examples/greenfield/module-hybrid-cdn-auth-sso/wphs.adapter.ts +7 -0
  251. package/examples/greenfield/module-hybrid-cdn-auth-starter/.env.dev.example +3 -0
  252. package/examples/greenfield/module-hybrid-cdn-auth-starter/.env.local.example +6 -0
  253. package/examples/greenfield/module-hybrid-cdn-auth-starter/.env.prod.example +5 -0
  254. package/examples/greenfield/module-hybrid-cdn-auth-starter/.env.stg.example +3 -0
  255. package/examples/greenfield/module-hybrid-cdn-auth-starter/.wphs/README.md +17 -0
  256. package/examples/greenfield/module-hybrid-cdn-auth-starter/README.md +59 -0
  257. package/examples/greenfield/module-hybrid-cdn-auth-starter/hubspot.config.yml.example +10 -0
  258. package/examples/greenfield/module-hybrid-cdn-auth-starter/index.html +12 -0
  259. package/examples/greenfield/module-hybrid-cdn-auth-starter/package.json +34 -0
  260. package/examples/greenfield/module-hybrid-cdn-auth-starter/portal.cdn.json.example +8 -0
  261. package/examples/greenfield/module-hybrid-cdn-auth-starter/public/fields.json +100 -0
  262. package/examples/greenfield/module-hybrid-cdn-auth-starter/public/meta.json +9 -0
  263. package/examples/greenfield/module-hybrid-cdn-auth-starter/public/module.html +38 -0
  264. package/examples/greenfield/module-hybrid-cdn-auth-starter/src/app.tsx +173 -0
  265. package/examples/greenfield/module-hybrid-cdn-auth-starter/src/assets/css/main.dev.css +23 -0
  266. package/examples/greenfield/module-hybrid-cdn-auth-starter/src/assets/css/main.prod.css +12 -0
  267. package/examples/greenfield/module-hybrid-cdn-auth-starter/src/assets/css/tailwind.css.config.css +7 -0
  268. package/examples/greenfield/module-hybrid-cdn-auth-starter/src/cdn/app-entry.ts +48 -0
  269. package/examples/greenfield/module-hybrid-cdn-auth-starter/src/cdn/vendor-entry.ts +28 -0
  270. package/examples/greenfield/module-hybrid-cdn-auth-starter/src/components/Layouts/AppLayoutWrapper.tsx +38 -0
  271. package/examples/greenfield/module-hybrid-cdn-auth-starter/src/components/Layouts/AuthLayout.tsx +7 -0
  272. package/examples/greenfield/module-hybrid-cdn-auth-starter/src/components/Layouts/DefaultLayout.tsx +5 -0
  273. package/examples/greenfield/module-hybrid-cdn-auth-starter/src/components/Layouts/MainLayout.tsx +42 -0
  274. package/examples/greenfield/module-hybrid-cdn-auth-starter/src/components/Layouts/UnauthorizedLayout.tsx +5 -0
  275. package/examples/greenfield/module-hybrid-cdn-auth-starter/src/components/Layouts/index.ts +13 -0
  276. package/examples/greenfield/module-hybrid-cdn-auth-starter/src/components/layout/section-card.tsx +20 -0
  277. package/examples/greenfield/module-hybrid-cdn-auth-starter/src/components/layout/starter-nav.tsx +28 -0
  278. package/examples/greenfield/module-hybrid-cdn-auth-starter/src/components/layout/starter-shell.tsx +18 -0
  279. package/examples/greenfield/module-hybrid-cdn-auth-starter/src/components/ui/Button.tsx +22 -0
  280. package/examples/greenfield/module-hybrid-cdn-auth-starter/src/components/ui/Form.tsx +22 -0
  281. package/examples/greenfield/module-hybrid-cdn-auth-starter/src/components/ui/Input.tsx +12 -0
  282. package/examples/greenfield/module-hybrid-cdn-auth-starter/src/components/ui/Toaster.tsx +5 -0
  283. package/examples/greenfield/module-hybrid-cdn-auth-starter/src/components/ui/link.tsx +10 -0
  284. package/examples/greenfield/module-hybrid-cdn-auth-starter/src/components/ui/loader/loader.tsx +18 -0
  285. package/examples/greenfield/module-hybrid-cdn-auth-starter/src/config/auth-tier.ts +4 -0
  286. package/examples/greenfield/module-hybrid-cdn-auth-starter/src/config/nav-scaffold.ts +4 -0
  287. package/examples/greenfield/module-hybrid-cdn-auth-starter/src/config/portal-nav.ts +9 -0
  288. package/examples/greenfield/module-hybrid-cdn-auth-starter/src/config/router.ts +1 -0
  289. package/examples/greenfield/module-hybrid-cdn-auth-starter/src/config/routes.ts +10 -0
  290. package/examples/greenfield/module-hybrid-cdn-auth-starter/src/data/hubSpotContext.ts +57 -0
  291. package/examples/greenfield/module-hybrid-cdn-auth-starter/src/data/sync-sdk-hub-context.ts +44 -0
  292. package/examples/greenfield/module-hybrid-cdn-auth-starter/src/defaultData.ts +2 -0
  293. package/examples/greenfield/module-hybrid-cdn-auth-starter/src/env.ts +59 -0
  294. package/examples/greenfield/module-hybrid-cdn-auth-starter/src/features/auth/app-bootstrap-guards.ts +14 -0
  295. package/examples/greenfield/module-hybrid-cdn-auth-starter/src/features/auth/auth-navigation.ts +81 -0
  296. package/examples/greenfield/module-hybrid-cdn-auth-starter/src/features/auth/auth-route-paths.ts +29 -0
  297. package/examples/greenfield/module-hybrid-cdn-auth-starter/src/features/auth/auth-session-events.ts +2 -0
  298. package/examples/greenfield/module-hybrid-cdn-auth-starter/src/features/auth/auth-session.ts +26 -0
  299. package/examples/greenfield/module-hybrid-cdn-auth-starter/src/features/auth/clear-client-auth-session.ts +62 -0
  300. package/examples/greenfield/module-hybrid-cdn-auth-starter/src/features/auth/components/starter-login.tsx +76 -0
  301. package/examples/greenfield/module-hybrid-cdn-auth-starter/src/features/auth/login-email-storage.ts +64 -0
  302. package/examples/greenfield/module-hybrid-cdn-auth-starter/src/features/auth/login-session-bridge.ts +72 -0
  303. package/examples/greenfield/module-hybrid-cdn-auth-starter/src/features/auth/post-login-path.ts +17 -0
  304. package/examples/greenfield/module-hybrid-cdn-auth-starter/src/features/auth/resolve-auth-login-error-message.ts +52 -0
  305. package/examples/greenfield/module-hybrid-cdn-auth-starter/src/features/auth/resolve-post-login-path.ts +21 -0
  306. package/examples/greenfield/module-hybrid-cdn-auth-starter/src/features/auth/use-portal-session.ts +25 -0
  307. package/examples/greenfield/module-hybrid-cdn-auth-starter/src/features/auth/use-signed-in-email.ts +55 -0
  308. package/examples/greenfield/module-hybrid-cdn-auth-starter/src/features/starter/components/starter-guide-page.tsx +218 -0
  309. package/examples/greenfield/module-hybrid-cdn-auth-starter/src/features/starter/components/starter-home-page.tsx +21 -0
  310. package/examples/greenfield/module-hybrid-cdn-auth-starter/src/features/starter/components/starter-home.tsx +48 -0
  311. package/examples/greenfield/module-hybrid-cdn-auth-starter/src/features/starter/components/starter-page-frame.tsx +25 -0
  312. package/examples/greenfield/module-hybrid-cdn-auth-starter/src/features/starter/components/starter-protected-page.tsx +54 -0
  313. package/examples/greenfield/module-hybrid-cdn-auth-starter/src/features/starter/components/starter-single-page.tsx +17 -0
  314. package/examples/greenfield/module-hybrid-cdn-auth-starter/src/features/starter/content/auth-guide-content.ts +91 -0
  315. package/examples/greenfield/module-hybrid-cdn-auth-starter/src/features/starter/content/woodscli-guide-content.ts +86 -0
  316. package/examples/greenfield/module-hybrid-cdn-auth-starter/src/features/starter/hooks/starter-preview-values.ts +60 -0
  317. package/examples/greenfield/module-hybrid-cdn-auth-starter/src/features/starter/hooks/use-starter-preview.dev.ts +18 -0
  318. package/examples/greenfield/module-hybrid-cdn-auth-starter/src/features/starter/hooks/use-starter-preview.ts +30 -0
  319. package/examples/greenfield/module-hybrid-cdn-auth-starter/src/features/starter/index.ts +6 -0
  320. package/examples/greenfield/module-hybrid-cdn-auth-starter/src/hubspot/bootstrap.ts +16 -0
  321. package/examples/greenfield/module-hybrid-cdn-auth-starter/src/hubspot/default-data.ts +7 -0
  322. package/examples/greenfield/module-hybrid-cdn-auth-starter/src/hubspot/is-cms-editor.ts +78 -0
  323. package/examples/greenfield/module-hybrid-cdn-auth-starter/src/hubspot/module-entry.tsx +9 -0
  324. package/examples/greenfield/module-hybrid-cdn-auth-starter/src/hubspot/types.ts +17 -0
  325. package/examples/greenfield/module-hybrid-cdn-auth-starter/src/integrations/configure-woodsportal-sdk.ts +94 -0
  326. package/examples/greenfield/module-hybrid-cdn-auth-starter/src/integrations/tanstack-query/root-provider.tsx +18 -0
  327. package/examples/greenfield/module-hybrid-cdn-auth-starter/src/integrations/tanstack-router/router.tsx +39 -0
  328. package/examples/greenfield/module-hybrid-cdn-auth-starter/src/integrations/woodsportal-sdk.ts +37 -0
  329. package/examples/greenfield/module-hybrid-cdn-auth-starter/src/main.tsx +5 -0
  330. package/examples/greenfield/module-hybrid-cdn-auth-starter/src/portal/api-endpoint.ts +11 -0
  331. package/examples/greenfield/module-hybrid-cdn-auth-starter/src/portal/ensure-portal-cdn-stylesheet.ts +115 -0
  332. package/examples/greenfield/module-hybrid-cdn-auth-starter/src/portal/mount-dev-preview.tsx +12 -0
  333. package/examples/greenfield/module-hybrid-cdn-auth-starter/src/portal/mount-portal.tsx +52 -0
  334. package/examples/greenfield/module-hybrid-cdn-auth-starter/src/routeTree.gen.ts +134 -0
  335. package/examples/greenfield/module-hybrid-cdn-auth-starter/src/routes/__root.tsx +12 -0
  336. package/examples/greenfield/module-hybrid-cdn-auth-starter/src/routes/_app/portal.tsx +10 -0
  337. package/examples/greenfield/module-hybrid-cdn-auth-starter/src/routes/_auth/login.tsx +10 -0
  338. package/examples/greenfield/module-hybrid-cdn-auth-starter/src/routes/guide.tsx +9 -0
  339. package/examples/greenfield/module-hybrid-cdn-auth-starter/src/routes/index.tsx +8 -0
  340. package/examples/greenfield/module-hybrid-cdn-auth-starter/src/routes/unauthorized.tsx +50 -0
  341. package/examples/greenfield/module-hybrid-cdn-auth-starter/src/utils/ValidationSchema.ts +1 -0
  342. package/examples/greenfield/module-hybrid-cdn-auth-starter/src/utils/cookie.ts +23 -0
  343. package/examples/greenfield/module-hybrid-cdn-auth-starter/src/utils/normalize-router-path.ts +10 -0
  344. package/examples/greenfield/module-hybrid-cdn-auth-starter/src/utils/private-route.tsx +100 -0
  345. package/examples/greenfield/module-hybrid-cdn-auth-starter/src/utils/public-route.tsx +64 -0
  346. package/examples/greenfield/module-hybrid-cdn-auth-starter/src/utils/resolve-api-base-url.ts +44 -0
  347. package/examples/greenfield/module-hybrid-cdn-auth-starter/src/utils/safe-router-navigation.ts +82 -0
  348. package/examples/greenfield/module-hybrid-cdn-auth-starter/tsconfig.json +20 -0
  349. package/examples/greenfield/module-hybrid-cdn-auth-starter/vite.config.cdn-app.mjs +1 -0
  350. package/examples/greenfield/module-hybrid-cdn-auth-starter/vite.config.cdn-esm.mjs +1 -0
  351. package/examples/greenfield/module-hybrid-cdn-auth-starter/vite.config.cdn-vendor.mjs +1 -0
  352. package/examples/greenfield/module-hybrid-cdn-auth-starter/vite.config.dev.mjs +1 -0
  353. package/examples/greenfield/module-hybrid-cdn-auth-starter/vite.config.hubspot.mjs +1 -0
  354. package/examples/greenfield/module-hybrid-cdn-auth-starter/vite.config.monolith.mjs +8 -0
  355. package/examples/greenfield/module-hybrid-cdn-auth-starter/woodscli.json +38 -0
  356. package/examples/greenfield/module-hybrid-cdn-auth-starter/wphs.adapter.ts +7 -0
  357. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/.env.dev.example +3 -0
  358. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/.env.local.example +6 -0
  359. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/.env.prod.example +5 -0
  360. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/.env.stg.example +3 -0
  361. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/.wphs/README.md +17 -0
  362. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/README.md +59 -0
  363. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/hubspot.config.yml.example +10 -0
  364. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/index.html +12 -0
  365. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/package.json +34 -0
  366. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/portal.cdn.json.example +8 -0
  367. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/public/fields.json +100 -0
  368. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/public/meta.json +9 -0
  369. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/public/module.html +38 -0
  370. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/src/app.tsx +173 -0
  371. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/src/assets/css/main.dev.css +23 -0
  372. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/src/assets/css/main.prod.css +12 -0
  373. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/src/assets/css/tailwind.css.config.css +7 -0
  374. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/src/cdn/app-entry.ts +48 -0
  375. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/src/cdn/vendor-entry.ts +28 -0
  376. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/src/components/Layouts/AppLayoutWrapper.tsx +38 -0
  377. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/src/components/Layouts/AuthLayout.tsx +7 -0
  378. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/src/components/Layouts/DefaultLayout.tsx +5 -0
  379. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/src/components/Layouts/MainLayout.tsx +7 -0
  380. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/src/components/Layouts/UnauthorizedLayout.tsx +5 -0
  381. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/src/components/Layouts/index.ts +13 -0
  382. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/src/components/layout/portal-sidebar-nav.tsx +33 -0
  383. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/src/components/layout/portal-sidebar.tsx +139 -0
  384. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/src/components/layout/section-card.tsx +20 -0
  385. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/src/components/layout/starter-nav.tsx +28 -0
  386. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/src/components/layout/starter-shell.tsx +18 -0
  387. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/src/components/ui/Button.tsx +22 -0
  388. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/src/components/ui/Form.tsx +22 -0
  389. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/src/components/ui/Input.tsx +12 -0
  390. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/src/components/ui/Toaster.tsx +5 -0
  391. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/src/components/ui/link.tsx +10 -0
  392. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/src/components/ui/loader/loader.tsx +18 -0
  393. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/src/config/auth-tier.ts +4 -0
  394. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/src/config/nav-scaffold.ts +4 -0
  395. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/src/config/portal-nav.ts +9 -0
  396. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/src/config/router.ts +1 -0
  397. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/src/config/routes.ts +10 -0
  398. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/src/data/hubSpotContext.ts +57 -0
  399. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/src/data/sync-sdk-hub-context.ts +44 -0
  400. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/src/defaultData.ts +2 -0
  401. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/src/env.ts +59 -0
  402. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/src/features/auth/app-bootstrap-guards.ts +14 -0
  403. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/src/features/auth/auth-navigation.ts +81 -0
  404. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/src/features/auth/auth-route-paths.ts +29 -0
  405. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/src/features/auth/auth-session-events.ts +2 -0
  406. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/src/features/auth/auth-session.ts +26 -0
  407. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/src/features/auth/clear-client-auth-session.ts +62 -0
  408. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/src/features/auth/components/starter-login.tsx +76 -0
  409. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/src/features/auth/login-email-storage.ts +64 -0
  410. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/src/features/auth/login-session-bridge.ts +72 -0
  411. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/src/features/auth/post-login-path.ts +17 -0
  412. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/src/features/auth/resolve-auth-login-error-message.ts +52 -0
  413. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/src/features/auth/resolve-post-login-path.ts +21 -0
  414. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/src/features/auth/use-portal-session.ts +25 -0
  415. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/src/features/auth/use-signed-in-email.ts +55 -0
  416. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/src/features/starter/components/starter-guide-page.tsx +210 -0
  417. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/src/features/starter/components/starter-home-page.tsx +21 -0
  418. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/src/features/starter/components/starter-home.tsx +48 -0
  419. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/src/features/starter/components/starter-page-frame.tsx +25 -0
  420. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/src/features/starter/components/starter-protected-page.tsx +54 -0
  421. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/src/features/starter/components/starter-single-page.tsx +17 -0
  422. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/src/features/starter/content/auth-guide-content.ts +85 -0
  423. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/src/features/starter/content/woodscli-guide-content.ts +86 -0
  424. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/src/features/starter/hooks/starter-preview-values.ts +60 -0
  425. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/src/features/starter/hooks/use-starter-preview.dev.ts +18 -0
  426. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/src/features/starter/hooks/use-starter-preview.ts +30 -0
  427. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/src/features/starter/index.ts +6 -0
  428. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/src/hubspot/bootstrap.ts +16 -0
  429. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/src/hubspot/default-data.ts +7 -0
  430. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/src/hubspot/is-cms-editor.ts +78 -0
  431. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/src/hubspot/module-entry.tsx +9 -0
  432. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/src/hubspot/types.ts +17 -0
  433. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/src/integrations/configure-woodsportal-sdk.ts +94 -0
  434. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/src/integrations/tanstack-query/root-provider.tsx +18 -0
  435. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/src/integrations/tanstack-router/router.tsx +39 -0
  436. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/src/integrations/woodsportal-sdk.ts +37 -0
  437. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/src/main.tsx +5 -0
  438. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/src/portal/api-endpoint.ts +11 -0
  439. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/src/portal/ensure-portal-cdn-stylesheet.ts +115 -0
  440. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/src/portal/mount-dev-preview.tsx +12 -0
  441. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/src/portal/mount-portal.tsx +52 -0
  442. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/src/routeTree.gen.ts +134 -0
  443. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/src/routes/__root.tsx +12 -0
  444. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/src/routes/_app/portal.tsx +10 -0
  445. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/src/routes/_auth/login.tsx +10 -0
  446. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/src/routes/guide.tsx +9 -0
  447. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/src/routes/index.tsx +8 -0
  448. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/src/routes/unauthorized.tsx +50 -0
  449. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/src/utils/ValidationSchema.ts +1 -0
  450. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/src/utils/cookie.ts +23 -0
  451. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/src/utils/normalize-router-path.ts +10 -0
  452. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/src/utils/private-route.tsx +100 -0
  453. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/src/utils/public-route.tsx +64 -0
  454. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/src/utils/resolve-api-base-url.ts +44 -0
  455. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/src/utils/safe-router-navigation.ts +82 -0
  456. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/tsconfig.json +20 -0
  457. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/vite.config.cdn-app.mjs +1 -0
  458. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/vite.config.cdn-esm.mjs +1 -0
  459. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/vite.config.cdn-vendor.mjs +1 -0
  460. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/vite.config.dev.mjs +1 -0
  461. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/vite.config.hubspot.mjs +1 -0
  462. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/vite.config.monolith.mjs +8 -0
  463. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/woodscli.json +38 -0
  464. package/examples/greenfield/module-hybrid-cdn-auth-starter-sidebar/wphs.adapter.ts +7 -0
  465. package/examples/greenfield/module-monolith/public/fields.json +66 -16
  466. package/examples/greenfield/module-monolith/src/assets/css/main.dev.css +7 -0
  467. package/examples/greenfield/module-monolith/src/components/layout/starter-nav.tsx +12 -15
  468. package/examples/greenfield/module-monolith/src/config/starter-nav.ts +9 -0
  469. package/examples/greenfield/module-monolith/src/features/starter/components/starter-home.tsx +25 -29
  470. package/examples/greenfield/module-monolith/src/features/starter/hooks/starter-preview-values.ts +60 -0
  471. package/examples/greenfield/module-monolith/src/features/starter/hooks/use-starter-preview.dev.ts +18 -0
  472. package/examples/greenfield/module-monolith/src/features/starter/hooks/use-starter-preview.ts +20 -37
  473. package/examples/manifest.json +5 -1
  474. package/examples/smoke/run-matrix.mjs +37 -0
  475. package/package.json +28 -12
  476. package/scripts/audit-tdz-split.mjs +11 -5
  477. package/scripts/check-cdn-size-regression.mjs +49 -0
  478. package/scripts/check-hubspot-bundle-size.mjs +18 -4
  479. package/scripts/cli-smoke-helpers.mjs +150 -0
  480. package/scripts/generate-cdn-bundle-breakdown.mjs +61 -0
  481. package/scripts/generate-cdn-manifest.mjs +23 -8
  482. package/scripts/portal-vite-env.mjs +76 -2
  483. package/scripts/publish-cdn-github.mjs +79 -14
  484. package/scripts/refresh-artifact-sizes.mjs +77 -0
  485. package/scripts/regenerate-route-tree.mjs +71 -0
  486. package/scripts/sync-auth-scaffold-from-client.mjs +41 -0
  487. package/scripts/verify-cdn-api-endpoint.mjs +17 -10
  488. package/scripts/vite/create-dev-server-config.mjs +7 -1
  489. package/scripts/vite/portal-build-shared.js +62 -2
  490. package/scripts/wp-cli-smoke.test.mjs +263 -0
  491. package/src/dev/bootstrap-greenfield.tsx +6 -0
  492. package/src/dev-module-config/ui/module-config-overlay-side.ts +13 -3
  493. package/src/dev-module-config/ui/module-config-toolbar-theme.ts +12 -3
  494. package/templates/module-hybrid-cdn/public/fields.json +47 -7
  495. package/templates/module-hybrid-cdn/public/module.html +7 -6
  496. package/templates/module-hybrid-cdn/src/assets/css/main.dev.css +7 -0
  497. package/templates/module-hybrid-cdn/src/components/layout/starter-nav.tsx +12 -15
  498. package/templates/module-hybrid-cdn/src/config/starter-nav.ts +9 -0
  499. package/templates/module-hybrid-cdn/src/features/starter/components/starter-home.tsx +25 -27
  500. package/templates/module-hybrid-cdn/src/features/starter/hooks/starter-preview-values.ts +5 -5
  501. package/templates/module-monolith/public/fields.json +49 -6
  502. package/templates/module-monolith/src/assets/css/main.dev.css +7 -0
  503. package/templates/module-monolith/src/components/layout/starter-nav.tsx +12 -15
  504. package/templates/module-monolith/src/config/starter-nav.ts +9 -0
  505. package/templates/module-monolith/src/features/starter/components/starter-home.tsx +25 -27
  506. package/templates/module-monolith/src/features/starter/hooks/starter-preview-values.ts +5 -5
  507. package/templates/scaffold/auth/_full/src/app.tsx +172 -0
  508. package/templates/scaffold/auth/_full/src/config/auth-tier.ts +4 -0
  509. package/templates/scaffold/auth/_full/src/features/auth/auth-session.ts +51 -0
  510. package/templates/scaffold/auth/_full/src/features/auth/login-session-bridge.ts +74 -0
  511. package/templates/scaffold/auth/_full/src/features/auth/mfa-otp-utils.ts +24 -0
  512. package/templates/scaffold/auth/_full/src/features/auth/mfa-pending-storage.ts +108 -0
  513. package/templates/scaffold/auth/_full/src/features/auth/mfa-portal.ts +45 -0
  514. package/templates/scaffold/auth/_full/src/features/auth/use-mfa-login-otp-flow.ts +78 -0
  515. package/templates/scaffold/auth/_full/src/routeTree.gen.ts +182 -0
  516. package/templates/scaffold/auth/_full/src/routes/_auth/two-fa.tsx +91 -0
  517. package/templates/scaffold/auth/_nav-sidebar/src/components/Layouts/MainLayout.tsx +7 -0
  518. package/templates/scaffold/auth/_nav-sidebar/src/components/layout/portal-sidebar-nav.tsx +33 -0
  519. package/templates/scaffold/auth/_nav-sidebar/src/components/layout/portal-sidebar.tsx +139 -0
  520. package/templates/scaffold/auth/_nav-top/src/components/Layouts/MainLayout.tsx +42 -0
  521. package/templates/scaffold/auth/_shared/src/app.tsx +178 -0
  522. package/templates/scaffold/auth/_shared/src/components/Layouts/AppLayoutWrapper.tsx +38 -0
  523. package/templates/scaffold/auth/_shared/src/components/Layouts/AuthLayout.tsx +7 -0
  524. package/templates/scaffold/auth/_shared/src/components/Layouts/DefaultLayout.tsx +5 -0
  525. package/templates/scaffold/auth/_shared/src/components/Layouts/UnauthorizedLayout.tsx +5 -0
  526. package/templates/scaffold/auth/_shared/src/components/Layouts/index.ts +13 -0
  527. package/templates/scaffold/auth/_shared/src/components/ui/Button.tsx +22 -0
  528. package/templates/scaffold/auth/_shared/src/components/ui/Form.tsx +22 -0
  529. package/templates/scaffold/auth/_shared/src/components/ui/Input.tsx +12 -0
  530. package/templates/scaffold/auth/_shared/src/components/ui/Toaster.tsx +5 -0
  531. package/templates/scaffold/auth/_shared/src/components/ui/link.tsx +10 -0
  532. package/templates/scaffold/auth/_shared/src/components/ui/loader/loader.tsx +18 -0
  533. package/templates/scaffold/auth/_shared/src/config/auth-tier.ts +4 -0
  534. package/templates/scaffold/auth/_shared/src/config/nav-scaffold.ts +4 -0
  535. package/templates/scaffold/auth/_shared/src/config/portal-nav.ts +9 -0
  536. package/templates/scaffold/auth/_shared/src/config/router.ts +1 -0
  537. package/templates/scaffold/auth/_shared/src/config/routes.ts +10 -0
  538. package/templates/scaffold/auth/_shared/src/data/hubSpotContext.ts +57 -0
  539. package/templates/scaffold/auth/_shared/src/data/sync-sdk-hub-context.ts +44 -0
  540. package/templates/scaffold/auth/_shared/src/env.ts +59 -0
  541. package/templates/scaffold/auth/_shared/src/features/auth/app-bootstrap-guards.ts +14 -0
  542. package/templates/scaffold/auth/_shared/src/features/auth/auth-navigation.ts +81 -0
  543. package/templates/scaffold/auth/_shared/src/features/auth/auth-route-paths.ts +29 -0
  544. package/templates/scaffold/auth/_shared/src/features/auth/auth-session-events.ts +2 -0
  545. package/templates/scaffold/auth/_shared/src/features/auth/auth-session.ts +26 -0
  546. package/templates/scaffold/auth/_shared/src/features/auth/clear-client-auth-session.ts +62 -0
  547. package/templates/scaffold/auth/_shared/src/features/auth/components/starter-login.tsx +76 -0
  548. package/templates/scaffold/auth/_shared/src/features/auth/login-email-storage.ts +64 -0
  549. package/templates/scaffold/auth/_shared/src/features/auth/login-session-bridge.ts +72 -0
  550. package/templates/scaffold/auth/_shared/src/features/auth/post-login-path.ts +17 -0
  551. package/templates/scaffold/auth/_shared/src/features/auth/resolve-auth-login-error-message.ts +52 -0
  552. package/templates/scaffold/auth/_shared/src/features/auth/resolve-post-login-path.ts +21 -0
  553. package/templates/scaffold/auth/_shared/src/features/auth/use-portal-session.ts +25 -0
  554. package/templates/scaffold/auth/_shared/src/features/auth/use-signed-in-email.ts +55 -0
  555. package/templates/scaffold/auth/_shared/src/features/starter/components/starter-guide-page.tsx +218 -0
  556. package/templates/scaffold/auth/_shared/src/features/starter/components/starter-page-frame.tsx +25 -0
  557. package/templates/scaffold/auth/_shared/src/features/starter/components/starter-protected-page.tsx +54 -0
  558. package/templates/scaffold/auth/_shared/src/features/starter/content/auth-guide-content.ts +91 -0
  559. package/templates/scaffold/auth/_shared/src/hubspot/is-cms-editor.ts +78 -0
  560. package/templates/scaffold/auth/_shared/src/integrations/configure-woodsportal-sdk.ts +100 -0
  561. package/templates/scaffold/auth/_shared/src/integrations/tanstack-query/root-provider.tsx +18 -0
  562. package/templates/scaffold/auth/_shared/src/integrations/tanstack-router/router.tsx +39 -0
  563. package/templates/scaffold/auth/_shared/src/integrations/woodsportal-sdk.ts +37 -0
  564. package/templates/scaffold/auth/_shared/src/portal/mount-portal.tsx +59 -0
  565. package/templates/scaffold/auth/_shared/src/routeTree.gen.ts +134 -0
  566. package/templates/scaffold/auth/_shared/src/routes/__root.tsx +12 -0
  567. package/templates/scaffold/auth/_shared/src/routes/_app/portal.tsx +10 -0
  568. package/templates/scaffold/auth/_shared/src/routes/_auth/login.tsx +10 -0
  569. package/templates/scaffold/auth/_shared/src/routes/guide.tsx +9 -0
  570. package/templates/scaffold/auth/_shared/src/routes/index.tsx +8 -0
  571. package/templates/scaffold/auth/_shared/src/routes/unauthorized.tsx +50 -0
  572. package/templates/scaffold/auth/_shared/src/utils/ValidationSchema.ts +1 -0
  573. package/templates/scaffold/auth/_shared/src/utils/cookie.ts +23 -0
  574. package/templates/scaffold/auth/_shared/src/utils/normalize-router-path.ts +10 -0
  575. package/templates/scaffold/auth/_shared/src/utils/private-route.tsx +100 -0
  576. package/templates/scaffold/auth/_shared/src/utils/public-route.tsx +64 -0
  577. package/templates/scaffold/auth/_shared/src/utils/resolve-api-base-url.ts +44 -0
  578. package/templates/scaffold/auth/_shared/src/utils/safe-router-navigation.ts +82 -0
  579. package/templates/scaffold/auth/_sso/src/app.tsx +180 -0
  580. package/templates/scaffold/auth/_sso/src/components/ui/auth/SsoLoginButton.tsx +34 -0
  581. package/templates/scaffold/auth/_sso/src/config/auth-tier.ts +4 -0
  582. package/templates/scaffold/auth/_sso/src/features/auth/components/starter-login.tsx +83 -0
  583. package/templates/scaffold/auth/_sso/src/features/auth/sso-callback.ts +120 -0
  584. package/templates/scaffold/auth/_sso/src/features/auth/use-sso-callback-handler.ts +92 -0
  585. package/templates/scaffold/auth/_sso/src/routeTree.gen.ts +155 -0
  586. package/templates/scaffold/auth/_sso/src/routes/_auth/login/sso.tsx +27 -0
  587. package/templates/scaffold/auth/nav-manifest.json +8 -0
  588. package/templates/scaffold/auth/sync-manifest.json +16 -0
  589. package/templates/scaffold/auth/tier-manifest.json +18 -0
  590. package/templates/scaffold/page/page-component-auth.tsx.tpl +10 -0
  591. package/templates/scaffold/page/page-component-plain.tsx.tpl +11 -0
  592. package/templates/scaffold/page/route-protected-auth.tsx.tpl +10 -0
  593. package/templates/scaffold/page/route-public-auth.tsx.tpl +9 -0
  594. package/templates/scaffold/page/route-public-plain.tsx.tpl +6 -0
  595. package/vite/vite.config.cdn-app.mjs +43 -30
  596. package/vite/vite.config.cdn-audit.mjs +34 -0
  597. package/vite/vite.config.cdn-esm.mjs +1 -1
  598. package/dist/chunk-S2YJDWZU.js.map +0 -1
package/dist/cli.js CHANGED
@@ -1,208 +1,42 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ DEFAULT_WOODSPORTAL_HS,
4
+ KIT_PACKAGE_NAME,
5
+ LEGACY_WOODSPORTAL_HS_FILE,
6
+ WOODSCLI_CONFIG_FILE,
7
+ WPHS_WORKSPACE_DIR,
8
+ WoodsPortalHsSchema,
9
+ findProjectRoot,
10
+ getKitRoot,
11
+ isModuleProject,
12
+ isThemeProject,
13
+ kitPath,
14
+ legacyConfigConflict,
15
+ navConfigFileExists,
16
+ normalizePagePath,
17
+ normalizePageSlug,
18
+ resolveProjectConfig,
19
+ scaffoldPage,
20
+ titleFromSlug,
21
+ usesCdnPipeline
22
+ } from "./chunk-WHYTQEZX.js";
2
23
  import {
3
24
  ExitCode,
4
25
  WpHsError,
5
26
  isWpHsError,
6
27
  runCommand,
7
28
  runCommandAsync,
8
- runNodeScript,
9
29
  runNodeScriptAsync,
10
30
  runNodeScriptLongRunning
11
31
  } from "./chunk-4GOJPOW3.js";
12
32
 
13
33
  // src/cli/program.ts
14
- import { readFileSync as readFileSync16 } from "fs";
15
- import { resolve as resolve23 } from "path";
16
34
  import { Command } from "commander";
17
35
 
18
36
  // src/lib/config.ts
19
- import { readFileSync as readFileSync2 } from "fs";
20
-
21
- // src/lib/config-files.ts
22
- import { existsSync } from "fs";
23
- import { resolve } from "path";
24
- var WOODSCLI_CONFIG_FILE = "woodscli.json";
25
- var LEGACY_WOODSPORTAL_HS_FILE = "woodsportal.hs.json";
26
- function envConfigPath(projectRoot) {
27
- const fromEnv = process.env.WOODSCLI_CONFIG?.trim() || process.env.WP_HS_CONFIG?.trim() || null;
28
- if (!fromEnv) return null;
29
- return resolve(projectRoot, fromEnv);
30
- }
31
- function legacyConfigPath(projectRoot) {
32
- return resolve(projectRoot, LEGACY_WOODSPORTAL_HS_FILE);
33
- }
34
- function woodscliConfigPath(projectRoot) {
35
- return resolve(projectRoot, WOODSCLI_CONFIG_FILE);
36
- }
37
- function legacyConfigConflict(projectRoot) {
38
- const hasWoodscli = existsSync(woodscliConfigPath(projectRoot));
39
- const hasLegacy = existsSync(legacyConfigPath(projectRoot));
40
- if (hasLegacy && hasWoodscli) return "duplicate";
41
- if (hasLegacy && !hasWoodscli) return "legacy-only";
42
- return null;
43
- }
44
- function resolveProjectConfig(projectRoot) {
45
- const envPath = envConfigPath(projectRoot);
46
- if (envPath) {
47
- if (!existsSync(envPath)) return null;
48
- return { path: envPath, fileName: WOODSCLI_CONFIG_FILE };
49
- }
50
- const path = woodscliConfigPath(projectRoot);
51
- if (existsSync(path)) {
52
- return { path, fileName: WOODSCLI_CONFIG_FILE };
53
- }
54
- return null;
55
- }
56
- function findProjectRoot(from = process.cwd()) {
57
- if (process.env.WP_HS_PROJECT_ROOT?.trim()) {
58
- return resolve(process.env.WP_HS_PROJECT_ROOT.trim());
59
- }
60
- let dir = resolve(from);
61
- while (true) {
62
- if (resolveProjectConfig(dir)) {
63
- return dir;
64
- }
65
- const parent = resolve(dir, "..");
66
- if (parent === dir) {
67
- return null;
68
- }
69
- dir = parent;
70
- }
71
- }
72
-
73
- // src/schema/woodsportal-hs.ts
74
- import { z } from "zod";
75
- var CONFIG_VERSION = 1;
76
- var WPHS_WORKSPACE_DIR = ".wphs";
77
- var WoodsPortalHsSchema = z.object({
78
- configVersion: z.number().int().default(CONFIG_VERSION),
79
- projectType: z.enum(["module-hybrid-cdn", "module-monolith", "theme-starter"]),
80
- module: z.object({
81
- name: z.string().min(1),
82
- uploadPath: z.string().min(1),
83
- outDir: z.string().default("dist/WoodsPortal.Module")
84
- }),
85
- theme: z.object({
86
- name: z.string().min(1),
87
- uploadPath: z.string().min(1),
88
- srcDir: z.string().default("src/theme"),
89
- themeJson: z.string().default("src/theme/theme.json")
90
- }).optional(),
91
- cdn: z.object({
92
- mode: z.enum(["jsdelivr", "custom", "local", "none"]).default("jsdelivr"),
93
- buildIdFile: z.string().default(".module-build-id"),
94
- mirror: z.string().default("portal.cdn.json")
95
- }).default({}),
96
- entries: z.object({
97
- app: z.string().default("src/cdn/app-entry.ts"),
98
- vendor: z.string().default("src/cdn/vendor-entry.ts"),
99
- hubspotBootstrap: z.string().default("src/hubspot/bootstrap.ts"),
100
- /** Monolith production IIFE entry (wp build --hubspot-only) */
101
- module: z.string().default("src/hubspot/module-entry.tsx"),
102
- localDev: z.string().default("src/main.tsx")
103
- }).default({}),
104
- env: z.object({
105
- tiers: z.array(z.enum(["dev", "stg", "prod"])).default(["dev", "stg", "prod"])
106
- }).default({}),
107
- dev: z.object({
108
- moduleConfigDir: z.literal(WPHS_WORKSPACE_DIR).default(WPHS_WORKSPACE_DIR),
109
- adapter: z.string().default("./wphs.adapter.ts"),
110
- fieldsJson: z.string().default("public/fields.json"),
111
- themeFieldsJson: z.string().optional()
112
- }).default({}),
113
- scaffold: z.object({
114
- /** hash = woodsportal-client-frontend (HubSpot CMS); browser = local clean URLs; none = single page */
115
- router: z.enum(["hash", "browser", "none"]).default("hash")
116
- }).optional()
117
- });
118
- var DEFAULT_WOODSPORTAL_HS = {
119
- configVersion: CONFIG_VERSION,
120
- projectType: "module-hybrid-cdn",
121
- module: {
122
- name: "woodsportal-module",
123
- uploadPath: "dw-modules/woodsportal-module.module",
124
- outDir: "dist/WoodsPortal.Module"
125
- },
126
- cdn: {
127
- mode: "jsdelivr",
128
- buildIdFile: ".module-build-id",
129
- mirror: "portal.cdn.json"
130
- },
131
- entries: {
132
- app: "src/cdn/app-entry.ts",
133
- vendor: "src/cdn/vendor-entry.ts",
134
- hubspotBootstrap: "src/hubspot/bootstrap.ts",
135
- module: "src/hubspot/module-entry.tsx",
136
- localDev: "src/main.tsx"
137
- },
138
- env: {
139
- tiers: ["dev", "stg", "prod"]
140
- },
141
- dev: {
142
- moduleConfigDir: WPHS_WORKSPACE_DIR,
143
- adapter: "./wphs.adapter.ts",
144
- fieldsJson: "public/fields.json"
145
- }
146
- };
147
- function isModuleProject(config) {
148
- return config.projectType === "module-hybrid-cdn" || config.projectType === "module-monolith";
149
- }
150
- function isThemeProject(config) {
151
- return config.projectType === "theme-starter";
152
- }
153
- function usesCdnPipeline(config) {
154
- return config.projectType === "module-hybrid-cdn" && config.cdn.mode !== "none";
155
- }
156
-
157
- // src/lib/paths.ts
158
- import { existsSync as existsSync2, readFileSync } from "fs";
159
- import { dirname, resolve as resolve2 } from "path";
160
- import { fileURLToPath } from "url";
161
- var KIT_PACKAGE_NAME = "@woodsportal/hubspot-kit";
162
- function resolveKitRoot() {
163
- let dir = dirname(fileURLToPath(import.meta.url));
164
- while (true) {
165
- const pkgPath = resolve2(dir, "package.json");
166
- if (existsSync2(pkgPath)) {
167
- try {
168
- const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
169
- if (pkg.name === KIT_PACKAGE_NAME) {
170
- return dir;
171
- }
172
- } catch {
173
- }
174
- }
175
- const parent = dirname(dir);
176
- if (parent === dir) {
177
- break;
178
- }
179
- dir = parent;
180
- }
181
- throw new WpHsError("Could not resolve @woodsportal/hubspot-kit package root.", {
182
- exitCode: ExitCode.INTERNAL
183
- });
184
- }
185
- var kitRoot = resolveKitRoot();
186
- function getKitRoot() {
187
- return kitRoot;
188
- }
189
- function findProjectRoot2(from = process.cwd()) {
190
- const resolved = findProjectRoot(from);
191
- if (!resolved) {
192
- throw new WpHsError("Could not find woodscli.json in this directory or any parent.", {
193
- exitCode: ExitCode.USER_ERROR,
194
- remediation: "Run wp init, or pass --project <path>. Legacy config: wp migrate --yes."
195
- });
196
- }
197
- return resolved;
198
- }
199
- function kitPath(...segments) {
200
- return resolve2(kitRoot, ...segments);
201
- }
202
-
203
- // src/lib/config.ts
37
+ import { readFileSync } from "fs";
204
38
  function loadConfig(projectRoot) {
205
- const root = projectRoot ?? findProjectRoot2();
39
+ const root = projectRoot ?? findProjectRoot();
206
40
  const resolved = resolveProjectConfig(root);
207
41
  if (!resolved) {
208
42
  throw new WpHsError(`Missing ${WOODSCLI_CONFIG_FILE}`, {
@@ -212,7 +46,7 @@ function loadConfig(projectRoot) {
212
46
  }
213
47
  let raw;
214
48
  try {
215
- raw = JSON.parse(readFileSync2(resolved.path, "utf8"));
49
+ raw = JSON.parse(readFileSync(resolved.path, "utf8"));
216
50
  } catch (cause) {
217
51
  throw new WpHsError(`Invalid JSON in ${resolved.fileName}`, {
218
52
  exitCode: ExitCode.USER_ERROR,
@@ -294,20 +128,171 @@ function createLogger(options = {}) {
294
128
  };
295
129
  }
296
130
 
131
+ // src/lib/kit-version-check.ts
132
+ import { existsSync, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
133
+ import { homedir } from "os";
134
+ import { dirname, join } from "path";
135
+ import pc2 from "picocolors";
136
+ import semver from "semver";
137
+ var NPM_REGISTRY_URL = "https://registry.npmjs.org/@woodsportal%2Fhubspot-kit";
138
+ var VERSION_CHECK_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
139
+ var VERSION_CHECK_FETCH_TIMEOUT_MS = 2500;
140
+ function getVersionCheckCachePath() {
141
+ if (process.env.WP_VERSION_CHECK_CACHE) {
142
+ return process.env.WP_VERSION_CHECK_CACHE;
143
+ }
144
+ const base = process.env.XDG_CACHE_HOME ? join(process.env.XDG_CACHE_HOME, "woodsportal") : join(homedir(), ".cache", "woodsportal");
145
+ return join(base, "wp-kit-version.json");
146
+ }
147
+ function readVersionCheckCache(now = Date.now()) {
148
+ const cachePath = getVersionCheckCachePath();
149
+ if (!existsSync(cachePath)) {
150
+ return null;
151
+ }
152
+ try {
153
+ const parsed = JSON.parse(readFileSync2(cachePath, "utf8"));
154
+ if (!parsed.latest || typeof parsed.fetchedAt !== "number") {
155
+ return null;
156
+ }
157
+ if (now - parsed.fetchedAt > VERSION_CHECK_CACHE_TTL_MS) {
158
+ return null;
159
+ }
160
+ return parsed;
161
+ } catch {
162
+ return null;
163
+ }
164
+ }
165
+ function writeVersionCheckCache(cache) {
166
+ const cachePath = getVersionCheckCachePath();
167
+ mkdirSync(dirname(cachePath), { recursive: true });
168
+ writeFileSync(cachePath, `${JSON.stringify(cache)}
169
+ `, "utf8");
170
+ }
171
+ async function fetchLatestKitVersion() {
172
+ try {
173
+ const response = await fetch(NPM_REGISTRY_URL, {
174
+ signal: AbortSignal.timeout(VERSION_CHECK_FETCH_TIMEOUT_MS),
175
+ headers: { accept: "application/json" }
176
+ });
177
+ if (!response.ok) {
178
+ return null;
179
+ }
180
+ const body = await response.json();
181
+ const latest = body["dist-tags"]?.latest;
182
+ return typeof latest === "string" && latest.length > 0 ? latest : null;
183
+ } catch {
184
+ return null;
185
+ }
186
+ }
187
+ async function resolveLatestKitVersion(now = Date.now()) {
188
+ const cached = readVersionCheckCache(now);
189
+ if (cached) {
190
+ return cached.latest;
191
+ }
192
+ const latest = await fetchLatestKitVersion();
193
+ if (!latest) {
194
+ return null;
195
+ }
196
+ writeVersionCheckCache({ latest, fetchedAt: now });
197
+ return latest;
198
+ }
199
+ function isKitUpdateAvailable(current, latest) {
200
+ const currentCoerced = semver.coerce(current);
201
+ const latestCoerced = semver.coerce(latest);
202
+ if (!currentCoerced || !latestCoerced) {
203
+ return false;
204
+ }
205
+ return semver.lt(currentCoerced, latestCoerced);
206
+ }
207
+ function formatKitUpgradeCommand(projectRoot) {
208
+ const root = projectRoot || process.cwd();
209
+ const spec = `${KIT_PACKAGE_NAME}@latest`;
210
+ if (existsSync(join(root, "yarn.lock"))) {
211
+ return `yarn add -D ${spec}`;
212
+ }
213
+ if (existsSync(join(root, "pnpm-lock.yaml"))) {
214
+ return `pnpm add -D ${spec}`;
215
+ }
216
+ return `npm install -D ${spec}`;
217
+ }
218
+ function shouldUseWarnColor() {
219
+ if (process.env.NO_COLOR !== void 0) return false;
220
+ if (process.env.CI === "true") return false;
221
+ return process.stderr.isTTY === true;
222
+ }
223
+ function shouldSkipVersionCheck(options) {
224
+ if (options.ci || process.env.CI === "true") return true;
225
+ if (options.json || options.quiet) return true;
226
+ if (process.env.WP_SKIP_VERSION_CHECK === "1") return true;
227
+ return false;
228
+ }
229
+ function formatKitUpdateWarning(current, latest, projectRoot) {
230
+ const upgrade = formatKitUpgradeCommand(projectRoot);
231
+ const useColor = shouldUseWarnColor();
232
+ const warn = useColor ? pc2.yellow("warn") : "warn";
233
+ const dim = useColor ? pc2.dim : (text5) => text5;
234
+ return [
235
+ `[${warn}] ${KIT_PACKAGE_NAME} v${current} is installed; latest is v${latest}.`,
236
+ `${dim(" ")}Upgrade: ${upgrade}`
237
+ ].join("\n");
238
+ }
239
+ async function maybeWarnKitUpdate(options) {
240
+ try {
241
+ if (shouldSkipVersionCheck(options)) {
242
+ return;
243
+ }
244
+ const latest = await resolveLatestKitVersion();
245
+ if (!latest || !isKitUpdateAvailable(options.current, latest)) {
246
+ return;
247
+ }
248
+ process.stderr.write(`${formatKitUpdateWarning(options.current, latest, options.projectRoot)}
249
+ `);
250
+ } catch {
251
+ }
252
+ }
253
+
254
+ // src/lib/kit-version.ts
255
+ import { readFileSync as readFileSync3 } from "fs";
256
+ import { resolve } from "path";
257
+ var cachedVersion = null;
258
+ function readKitVersion() {
259
+ if (cachedVersion) {
260
+ return cachedVersion;
261
+ }
262
+ try {
263
+ const pkg = JSON.parse(readFileSync3(resolve(getKitRoot(), "package.json"), "utf8"));
264
+ cachedVersion = pkg.version ?? "0.0.0";
265
+ } catch {
266
+ cachedVersion = "0.0.0";
267
+ }
268
+ return cachedVersion;
269
+ }
270
+
297
271
  // src/lib/wphs-baseline.ts
298
- import { resolve as resolve3 } from "path";
272
+ import { resolve as resolve2 } from "path";
299
273
  function defaultBaselineDir(projectRoot, config) {
300
274
  const workspace = config.dev.moduleConfigDir ?? WPHS_WORKSPACE_DIR;
301
- return resolve3(projectRoot, workspace, "audit", "baseline");
275
+ return resolve2(projectRoot, workspace, "audit", "baseline");
276
+ }
277
+
278
+ // src/lib/exit-cli.ts
279
+ async function exitCli(code) {
280
+ await new Promise((resolve23) => {
281
+ setImmediate(resolve23);
282
+ });
283
+ await new Promise((resolve23) => {
284
+ setTimeout(resolve23, 0);
285
+ });
286
+ process.exit(code);
302
287
  }
303
288
 
304
289
  // src/cli/commands/doctor.ts
305
- import { existsSync as existsSync7, readFileSync as readFileSync5 } from "fs";
306
- import { join, resolve as resolve8 } from "path";
290
+ import { existsSync as existsSync8, readFileSync as readFileSync8 } from "fs";
291
+ import { join as join2, resolve as resolve9 } from "path";
307
292
 
308
293
  // src/lib/cli-spinner.ts
309
294
  import * as p from "@clack/prompts";
310
- import pc2 from "picocolors";
295
+ import pc3 from "picocolors";
311
296
  function isInteractiveCli(ctx) {
312
297
  const { json, quiet, ci } = ctx.global;
313
298
  return !json && !quiet && !ci && process.stdout.isTTY === true;
@@ -321,11 +306,11 @@ function isVerboseStepLog(ctx) {
321
306
  return ctx.global.verbose === true && !ctx.global.quiet && !ctx.global.json;
322
307
  }
323
308
  async function yieldForSpinnerPaint() {
324
- await new Promise((resolve24) => {
325
- setImmediate(resolve24);
309
+ await new Promise((resolve23) => {
310
+ setImmediate(resolve23);
326
311
  });
327
- await new Promise((resolve24) => {
328
- setTimeout(resolve24, 48);
312
+ await new Promise((resolve23) => {
313
+ setTimeout(resolve23, 48);
329
314
  });
330
315
  }
331
316
  async function runCliTasks(ctx, tasks, options = {}) {
@@ -372,7 +357,7 @@ async function runCliTasks(ctx, tasks, options = {}) {
372
357
  await step.task();
373
358
  spinner2.stop(step.title);
374
359
  } catch (error) {
375
- spinner2.stop(pc2.red(step.title));
360
+ spinner2.stop(pc3.red(step.title));
376
361
  throw error;
377
362
  }
378
363
  }
@@ -384,7 +369,7 @@ function shouldQuietSubprocess(ctx, options = {}) {
384
369
  }
385
370
  function printCommandDone(ctx, message) {
386
371
  if (isInteractiveCli(ctx)) {
387
- process.stdout.write(`${pc2.green("\u25C6")} ${message}
372
+ process.stdout.write(`${pc3.green("\u25C6")} ${message}
388
373
  `);
389
374
  return;
390
375
  }
@@ -392,8 +377,8 @@ function printCommandDone(ctx, message) {
392
377
  }
393
378
 
394
379
  // src/lib/hubspot-cli-setup.ts
395
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
396
- import { resolve as resolve4 } from "path";
380
+ import { existsSync as existsSync2, readFileSync as readFileSync4 } from "fs";
381
+ import { resolve as resolve3 } from "path";
397
382
  import { spawnSync } from "child_process";
398
383
  var HUBSPOT_CONFIG_FILE = "hubspot.config.yml";
399
384
  var HUBSPOT_CONFIG_EXAMPLE = "hubspot.config.yml.example";
@@ -419,13 +404,13 @@ function hsTimedOut(result) {
419
404
  return err?.code === "ETIMEDOUT";
420
405
  }
421
406
  function assessHubSpotConfigFiles(projectRoot) {
422
- const configPath = resolve4(projectRoot, HUBSPOT_CONFIG_FILE);
423
- const examplePath = resolve4(projectRoot, HUBSPOT_CONFIG_EXAMPLE);
424
- const configPresent = existsSync3(configPath);
425
- const examplePresent = existsSync3(examplePath);
407
+ const configPath = resolve3(projectRoot, HUBSPOT_CONFIG_FILE);
408
+ const examplePath = resolve3(projectRoot, HUBSPOT_CONFIG_EXAMPLE);
409
+ const configPresent = existsSync2(configPath);
410
+ const examplePresent = existsSync2(examplePath);
426
411
  let looksLikePlaceholder = false;
427
412
  if (configPresent) {
428
- const raw = readFileSync3(configPath, "utf8");
413
+ const raw = readFileSync4(configPath, "utf8");
429
414
  looksLikePlaceholder = PLACEHOLDER_PATTERNS.some((pattern) => pattern.test(raw));
430
415
  } else {
431
416
  looksLikePlaceholder = true;
@@ -551,7 +536,7 @@ function doctorHubSpotChecks(projectRoot) {
551
536
  ok: assessment.hsCliInstalled,
552
537
  message: assessment.hsCliInstalled ? `HubSpot CLI ${assessment.hsCliVersion ?? ""}`.trim() : "HubSpot CLI not installed",
553
538
  remediation: assessment.hsCliInstalled ? void 0 : "npm i -g @hubspot/cli",
554
- severity: "error"
539
+ severity: "warn"
555
540
  },
556
541
  {
557
542
  name: "hubspot-config",
@@ -570,136 +555,345 @@ function doctorHubSpotChecks(projectRoot) {
570
555
  ];
571
556
  }
572
557
 
573
- // src/lib/template-dev-deps.ts
574
- var VITE_DEV_PEER_DEPS = {
575
- vite: "^6.1.0",
576
- "@vitejs/plugin-react": "^4.3.4",
577
- "babel-plugin-react-compiler": "^1.0.0",
578
- "@tailwindcss/vite": "^4.0.6",
579
- "@tanstack/router-plugin": "^1.121.2",
580
- "@tanstack/react-router": "^1.121.2",
581
- "@dnd-kit/core": "^6.3.1",
582
- "@dnd-kit/sortable": "^10.0.0",
583
- "@dnd-kit/utilities": "^3.2.2",
584
- react: "^19.0.0",
585
- "react-dom": "^19.0.0",
586
- tailwindcss: "^4.0.6"
587
- };
588
- var KIT_DEV_DEP = "@woodsportal/hubspot-kit";
589
- var KIT_DEV_DEP_VERSION = "^1.0.30";
590
- var MODULE_CONFIG_UI_DEPS = [
591
- "@dnd-kit/core",
592
- "@dnd-kit/sortable",
593
- "@dnd-kit/utilities"
594
- ];
595
- function templateDevDependencies() {
596
- return {
597
- [KIT_DEV_DEP]: KIT_DEV_DEP_VERSION,
598
- ...VITE_DEV_PEER_DEPS
599
- };
600
- }
558
+ // src/lib/cdn-github-setup.ts
559
+ import { existsSync as existsSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
560
+ import { resolve as resolve5 } from "path";
561
+ import * as p2 from "@clack/prompts";
601
562
 
602
- // src/lib/hubspot-fields-json.ts
603
- var HUBSPOT_MODULE_FIELD_TYPES = /* @__PURE__ */ new Set([
604
- "boolean",
605
- "choice",
606
- "color",
607
- "cta",
608
- "date",
609
- "datetime",
610
- "email",
611
- "embed",
612
- "file",
613
- "followup_email",
614
- "font",
615
- "form",
616
- "group",
617
- "html",
618
- "hubdb_row",
619
- "hubdb_table",
620
- "icon",
621
- "image",
622
- "link",
623
- "logo",
624
- "meeting",
625
- "menu",
626
- "number",
627
- "page",
628
- "payment",
629
- "richtext",
630
- "salesforce_campaign",
631
- "simple_menu",
632
- "spacing",
633
- "tag",
634
- "text",
635
- "url",
636
- "video",
637
- "workflow"
638
- ]);
639
- function isFieldRecord(value) {
640
- return typeof value === "object" && value !== null && !Array.isArray(value);
641
- }
642
- function validateFieldNode(node, path, issues) {
643
- if (!isFieldRecord(node)) {
644
- issues.push({ path, message: "expected a field object" });
645
- return;
646
- }
647
- const type = node.type;
648
- if (typeof type !== "string" || type.length === 0) {
649
- issues.push({ path: `${path}.type`, message: "missing field type" });
650
- } else if (!HUBSPOT_MODULE_FIELD_TYPES.has(type)) {
651
- issues.push({
652
- path: `${path}.type`,
653
- message: type === "textarea" ? `'textarea' is not a HubSpot field type \u2014 use "type": "text" with "display": "textarea"` : `'${type}' is not a valid HubSpot field type`
654
- });
655
- }
656
- const tab = node.tab;
657
- if (tab === "STYLE" && type !== "group") {
658
- issues.push({
659
- path: `${path}.tab`,
660
- message: 'STYLE tab fields must live inside a "type": "group" with "tab": "STYLE" (HubSpot page editor Styles tab)'
661
- });
662
- }
663
- if (type !== "group") {
664
- const name = node.name;
665
- if (typeof name !== "string" || name.trim().length === 0) {
666
- issues.push({ path: `${path}.name`, message: "missing field name" });
667
- }
668
- const label = node.label;
669
- if (typeof label !== "string" || label.trim().length === 0) {
670
- issues.push({ path: `${path}.label`, message: "missing field label" });
563
+ // src/lib/project-env-files.ts
564
+ import { existsSync as existsSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "fs";
565
+ import { resolve as resolve4 } from "path";
566
+ var ENV_LOCAL_FILE = ".env.local";
567
+ var ENV_GITIGNORE_LINES = [".env", ".env.*", "!.env*.example", ENV_LOCAL_FILE];
568
+ function parseEnvFile(content) {
569
+ const result = {};
570
+ for (const line of content.split("\n")) {
571
+ const trimmed = line.trim();
572
+ if (!trimmed || trimmed.startsWith("#")) continue;
573
+ const eq = trimmed.indexOf("=");
574
+ if (eq <= 0) continue;
575
+ const key = trimmed.slice(0, eq).trim();
576
+ let value = trimmed.slice(eq + 1).trim();
577
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
578
+ value = value.slice(1, -1);
671
579
  }
672
- } else {
673
- const children = node.children;
674
- if (!Array.isArray(children)) {
675
- issues.push({ path: `${path}.children`, message: "group fields require a children array" });
580
+ result[key] = value;
581
+ }
582
+ return result;
583
+ }
584
+ function readEnvLocal(projectRoot) {
585
+ const path = resolve4(projectRoot, ENV_LOCAL_FILE);
586
+ if (!existsSync3(path)) return {};
587
+ return parseEnvFile(readFileSync5(path, "utf8"));
588
+ }
589
+ function formatEnvLocal(entries) {
590
+ const lines = [
591
+ "# Local secrets \u2014 gitignored. Created by wp init / wp setup (hybrid CDN).",
592
+ "# PORTAL_CDN_PUBLISH_TOKEN needs repo push on your portal.cdn.json mirror.",
593
+ ""
594
+ ];
595
+ for (const [key, value] of Object.entries(entries)) {
596
+ if (value.includes(" ") || value.includes("#")) {
597
+ lines.push(`${key}="${value.replace(/"/g, '\\"')}"`);
676
598
  } else {
677
- children.forEach((child, index) => {
678
- validateFieldNode(child, `${path}.children[${index}]`, issues);
679
- });
599
+ lines.push(`${key}=${value}`);
680
600
  }
681
601
  }
602
+ return `${lines.join("\n").trimEnd()}
603
+ `;
682
604
  }
683
- function validateHubSpotFieldsJson(fields) {
684
- if (!Array.isArray(fields)) {
685
- return [{ path: "$", message: "fields.json must be a top-level array" }];
686
- }
687
- const issues = [];
688
- fields.forEach((field, index) => {
605
+ function writeEnvLocal(projectRoot, entries) {
606
+ const path = resolve4(projectRoot, ENV_LOCAL_FILE);
607
+ const existing = existsSync3(path) ? parseEnvFile(readFileSync5(path, "utf8")) : {};
608
+ writeFileSync2(path, formatEnvLocal({ ...existing, ...entries }));
609
+ }
610
+ function mergeProjectEnv(projectRoot, base = process.env) {
611
+ const local = readEnvLocal(projectRoot);
612
+ const merged = { ...base };
613
+ for (const [key, value] of Object.entries(local)) {
614
+ if (merged[key] == null || merged[key] === "") {
615
+ merged[key] = value;
616
+ }
617
+ }
618
+ return merged;
619
+ }
620
+ function ensureEnvFilesGitignore(projectRoot) {
621
+ const gitignorePath = resolve4(projectRoot, ".gitignore");
622
+ const existing = existsSync3(gitignorePath) ? readFileSync5(gitignorePath, "utf8") : "";
623
+ const toAppend = ENV_GITIGNORE_LINES.filter((line) => !existing.includes(line)).join("\n");
624
+ if (!toAppend) return;
625
+ writeFileSync2(gitignorePath, `${existing.trimEnd()}
626
+
627
+ # Env files (secrets)
628
+ ${toAppend}
629
+ `);
630
+ }
631
+
632
+ // src/lib/cdn-github-setup.ts
633
+ var PORTAL_CDN_PUBLISH_TOKEN_ENV = "PORTAL_CDN_PUBLISH_TOKEN";
634
+ var PORTAL_CDN_JSON = "portal.cdn.json";
635
+ var PLACEHOLDER_GITHUB = /your-org|your-portal-cdn-mirror/i;
636
+ function isPlaceholderGithubSlug(github) {
637
+ return PLACEHOLDER_GITHUB.test(github);
638
+ }
639
+ function defaultPortalCdnJson() {
640
+ const examplePath = kitPath("templates", "shared", "portal.cdn.json.example");
641
+ if (existsSync4(examplePath)) {
642
+ return JSON.parse(readFileSync6(examplePath, "utf8"));
643
+ }
644
+ return {
645
+ publishMode: "jsdelivr",
646
+ github: "your-org/your-portal-cdn-mirror",
647
+ gitRemote: "https://github.com/your-org/your-portal-cdn-mirror.git",
648
+ cdnPathInRepo: "dist/cdn",
649
+ defaultBranch: "main"
650
+ };
651
+ }
652
+ function readPortalCdnJson(projectRoot) {
653
+ const path = resolve5(projectRoot, PORTAL_CDN_JSON);
654
+ if (!existsSync4(path)) return null;
655
+ try {
656
+ return JSON.parse(readFileSync6(path, "utf8"));
657
+ } catch {
658
+ return null;
659
+ }
660
+ }
661
+ function normalizeGithubSlug(input) {
662
+ const trimmed = input.trim().replace(/^https:\/\/github\.com\//, "").replace(/\.git$/, "");
663
+ const parts = trimmed.split("/").filter(Boolean);
664
+ if (parts.length !== 2) return null;
665
+ return `${parts[0]}/${parts[1]}`;
666
+ }
667
+ function portalCdnJsonFromGithub(github, base) {
668
+ const slug = normalizeGithubSlug(github);
669
+ if (!slug) {
670
+ throw new Error(`Invalid GitHub repo \u2014 use org/repo (got "${github}")`);
671
+ }
672
+ return {
673
+ publishMode: base?.publishMode ?? "jsdelivr",
674
+ github: slug,
675
+ gitRemote: `https://github.com/${slug}.git`,
676
+ cdnPathInRepo: base?.cdnPathInRepo ?? "dist/cdn",
677
+ defaultBranch: base?.defaultBranch ?? "main"
678
+ };
679
+ }
680
+ function hasCdnPublishToken(projectRoot) {
681
+ if (process.env[PORTAL_CDN_PUBLISH_TOKEN_ENV]?.trim()) return true;
682
+ if (process.env.GITHUB_TOKEN?.trim()) return true;
683
+ return Boolean(readEnvLocal(projectRoot)[PORTAL_CDN_PUBLISH_TOKEN_ENV]?.trim());
684
+ }
685
+ function hasConfiguredCdnGithubMirror(projectRoot) {
686
+ const config = readPortalCdnJson(projectRoot);
687
+ const github = config?.github ?? "";
688
+ return Boolean(github) && !PLACEHOLDER_GITHUB.test(github);
689
+ }
690
+ function githubDefaultForPrompt(projectRoot) {
691
+ const existing = readPortalCdnJson(projectRoot);
692
+ if (existing?.github && !PLACEHOLDER_GITHUB.test(existing.github)) {
693
+ return existing.github;
694
+ }
695
+ return defaultPortalCdnJson().github;
696
+ }
697
+ function needsCdnGitHubSetup(projectRoot) {
698
+ return !hasConfiguredCdnGithubMirror(projectRoot) || !hasCdnPublishToken(projectRoot);
699
+ }
700
+ function applyCdnGitHubSetup(projectRoot, input) {
701
+ const existing = readPortalCdnJson(projectRoot) ?? defaultPortalCdnJson();
702
+ const updated = portalCdnJsonFromGithub(input.github, existing);
703
+ writeFileSync3(resolve5(projectRoot, PORTAL_CDN_JSON), `${JSON.stringify(updated, null, 2)}
704
+ `);
705
+ if (input.token?.trim()) {
706
+ writeEnvLocal(projectRoot, { [PORTAL_CDN_PUBLISH_TOKEN_ENV]: input.token.trim() });
707
+ }
708
+ ensureEnvFilesGitignore(projectRoot);
709
+ }
710
+ async function promptCdnGitHubSetup(options = {}) {
711
+ if (options.skip) return null;
712
+ const projectRoot = options.projectRoot ?? process.cwd();
713
+ if (!needsCdnGitHubSetup(projectRoot)) {
714
+ return null;
715
+ }
716
+ let github = options.github?.trim();
717
+ if (!github && hasConfiguredCdnGithubMirror(projectRoot)) {
718
+ github = readPortalCdnJson(projectRoot)?.github;
719
+ }
720
+ if (!github) {
721
+ const answer = await p2.text({
722
+ message: "Public GitHub CDN mirror (org/repo)",
723
+ placeholder: options.githubDefault ?? "your-org/your-portal-cdn-mirror",
724
+ initialValue: options.githubDefault,
725
+ validate: (value) => {
726
+ const slug = normalizeGithubSlug(value ?? "");
727
+ return slug ? void 0 : "Enter a public repo as org/repo (jsDelivr serves from it)";
728
+ }
729
+ });
730
+ if (p2.isCancel(answer)) return null;
731
+ github = normalizeGithubSlug(answer) ?? void 0;
732
+ }
733
+ if (!github) return null;
734
+ if (hasCdnPublishToken(projectRoot)) {
735
+ return { github };
736
+ }
737
+ const tokenAnswer = await p2.password({
738
+ message: "GitHub token (repo push on CDN mirror)",
739
+ validate: (value) => {
740
+ if (!value?.trim()) return "Required for wp publish \u2014 create a fine-grained PAT with Contents: Read and write";
741
+ return void 0;
742
+ }
743
+ });
744
+ if (p2.isCancel(tokenAnswer)) return null;
745
+ return { github, token: tokenAnswer.trim() };
746
+ }
747
+ function formatCdnSetupNote(_projectRoot) {
748
+ return `CDN config written \u2014 edit ${PORTAL_CDN_JSON} if needed; token saved to ${ENV_LOCAL_FILE} (gitignored)`;
749
+ }
750
+
751
+ // src/lib/wphs-workspace.ts
752
+ var ensureModulePromise;
753
+ function loadEnsureModule() {
754
+ if (!ensureModulePromise) {
755
+ ensureModulePromise = Promise.resolve().then(async () => {
756
+ const mod = await import("./ensure-wphs-workspace-5S3GTO6K.js");
757
+ return mod;
758
+ });
759
+ }
760
+ return ensureModulePromise;
761
+ }
762
+ async function ensureProjectWphsWorkspace(projectRoot) {
763
+ const { ensureWphsWorkspace } = await loadEnsureModule();
764
+ return ensureWphsWorkspace(projectRoot, getKitRoot());
765
+ }
766
+
767
+ // src/lib/template-dev-deps.ts
768
+ var VITE_DEV_PEER_DEPS = {
769
+ vite: "^6.1.0",
770
+ "@vitejs/plugin-react": "^4.3.4",
771
+ "babel-plugin-react-compiler": "^1.0.0",
772
+ "@tailwindcss/vite": "^4.0.6",
773
+ "@tanstack/router-plugin": "^1.121.2",
774
+ "@tanstack/react-router": "^1.121.2",
775
+ "@dnd-kit/core": "^6.3.1",
776
+ "@dnd-kit/sortable": "^10.0.0",
777
+ "@dnd-kit/utilities": "^3.2.2",
778
+ react: "^19.0.0",
779
+ "react-dom": "^19.0.0",
780
+ tailwindcss: "^4.0.6"
781
+ };
782
+ var KIT_DEV_DEP = "@woodsportal/hubspot-kit";
783
+ var KIT_DEV_DEP_VERSION = "^1.0.30";
784
+ var MODULE_CONFIG_UI_DEPS = [
785
+ "@dnd-kit/core",
786
+ "@dnd-kit/sortable",
787
+ "@dnd-kit/utilities"
788
+ ];
789
+ function templateDevDependencies() {
790
+ return {
791
+ [KIT_DEV_DEP]: KIT_DEV_DEP_VERSION,
792
+ ...VITE_DEV_PEER_DEPS
793
+ };
794
+ }
795
+
796
+ // src/lib/hubspot-fields-json.ts
797
+ var HUBSPOT_MODULE_FIELD_TYPES = /* @__PURE__ */ new Set([
798
+ "boolean",
799
+ "choice",
800
+ "color",
801
+ "cta",
802
+ "date",
803
+ "datetime",
804
+ "email",
805
+ "embed",
806
+ "file",
807
+ "followup_email",
808
+ "font",
809
+ "form",
810
+ "group",
811
+ "html",
812
+ "hubdb_row",
813
+ "hubdb_table",
814
+ "icon",
815
+ "image",
816
+ "link",
817
+ "logo",
818
+ "meeting",
819
+ "menu",
820
+ "number",
821
+ "page",
822
+ "payment",
823
+ "richtext",
824
+ "salesforce_campaign",
825
+ "simple_menu",
826
+ "spacing",
827
+ "tag",
828
+ "text",
829
+ "url",
830
+ "video",
831
+ "workflow"
832
+ ]);
833
+ function isFieldRecord(value) {
834
+ return typeof value === "object" && value !== null && !Array.isArray(value);
835
+ }
836
+ function validateFieldNode(node, path, issues) {
837
+ if (!isFieldRecord(node)) {
838
+ issues.push({ path, message: "expected a field object" });
839
+ return;
840
+ }
841
+ const type = node.type;
842
+ if (typeof type !== "string" || type.length === 0) {
843
+ issues.push({ path: `${path}.type`, message: "missing field type" });
844
+ } else if (!HUBSPOT_MODULE_FIELD_TYPES.has(type)) {
845
+ issues.push({
846
+ path: `${path}.type`,
847
+ message: type === "textarea" ? `'textarea' is not a HubSpot field type \u2014 use "type": "text" with "display": "textarea"` : `'${type}' is not a valid HubSpot field type`
848
+ });
849
+ }
850
+ const tab = node.tab;
851
+ if (tab === "STYLE" && type !== "group") {
852
+ issues.push({
853
+ path: `${path}.tab`,
854
+ message: 'STYLE tab fields must live inside a "type": "group" with "tab": "STYLE" (HubSpot page editor Styles tab)'
855
+ });
856
+ }
857
+ if (type !== "group") {
858
+ const name = node.name;
859
+ if (typeof name !== "string" || name.trim().length === 0) {
860
+ issues.push({ path: `${path}.name`, message: "missing field name" });
861
+ }
862
+ const label = node.label;
863
+ if (typeof label !== "string" || label.trim().length === 0) {
864
+ issues.push({ path: `${path}.label`, message: "missing field label" });
865
+ }
866
+ } else {
867
+ const children = node.children;
868
+ if (!Array.isArray(children)) {
869
+ issues.push({ path: `${path}.children`, message: "group fields require a children array" });
870
+ } else {
871
+ children.forEach((child, index) => {
872
+ validateFieldNode(child, `${path}.children[${index}]`, issues);
873
+ });
874
+ }
875
+ }
876
+ }
877
+ function validateHubSpotFieldsJson(fields) {
878
+ if (!Array.isArray(fields)) {
879
+ return [{ path: "$", message: "fields.json must be a top-level array" }];
880
+ }
881
+ const issues = [];
882
+ fields.forEach((field, index) => {
689
883
  validateFieldNode(field, `[${index}]`, issues);
690
884
  });
691
885
  return issues;
692
886
  }
693
887
 
694
888
  // src/lib/developer-slug.ts
695
- import { existsSync as existsSync4, mkdirSync, readFileSync as readFileSync4, writeFileSync } from "fs";
696
- import { resolve as resolve5 } from "path";
697
- import * as p2 from "@clack/prompts";
889
+ import { existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
890
+ import { resolve as resolve6 } from "path";
891
+ import * as p3 from "@clack/prompts";
698
892
  var SLUG_MAX_LEN = 12;
699
893
  var SLUG_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,10}[a-z0-9])?$/;
700
894
  var DEVELOPER_CONFIG_FILE = "developer.json";
701
895
  function developerConfigPath(projectRoot) {
702
- return resolve5(projectRoot, ".wphs", "config", DEVELOPER_CONFIG_FILE);
896
+ return resolve6(projectRoot, ".wphs", "config", DEVELOPER_CONFIG_FILE);
703
897
  }
704
898
  function sanitizeDevSlug(input) {
705
899
  if (!input?.trim()) return void 0;
@@ -711,9 +905,9 @@ function readDeveloperSlug(projectRoot) {
711
905
  const fromEnv = sanitizeDevSlug(process.env.PORTAL_CDN_DEV_SLUG) ?? sanitizeDevSlug(process.env.MODULE_CDN_DEV_SLUG);
712
906
  if (fromEnv) return fromEnv;
713
907
  const path = developerConfigPath(projectRoot);
714
- if (!existsSync4(path)) return void 0;
908
+ if (!existsSync5(path)) return void 0;
715
909
  try {
716
- const raw = JSON.parse(readFileSync4(path, "utf8"));
910
+ const raw = JSON.parse(readFileSync7(path, "utf8"));
717
911
  return sanitizeDevSlug(raw.slug);
718
912
  } catch {
719
913
  return void 0;
@@ -725,8 +919,8 @@ function writeDeveloperSlug(projectRoot, slug) {
725
919
  throw new Error("Invalid CDN dev slug \u2014 use lowercase letters, numbers, and hyphens (max 12 chars)");
726
920
  }
727
921
  const path = developerConfigPath(projectRoot);
728
- mkdirSync(resolve5(path, ".."), { recursive: true });
729
- writeFileSync(path, `${JSON.stringify({ slug: normalized }, null, 2)}
922
+ mkdirSync2(resolve6(path, ".."), { recursive: true });
923
+ writeFileSync4(path, `${JSON.stringify({ slug: normalized }, null, 2)}
730
924
  `);
731
925
  }
732
926
  function needsDeveloperSlugSetup(projectRoot) {
@@ -736,7 +930,7 @@ async function promptDeveloperSlug(projectRoot) {
736
930
  if (!needsDeveloperSlugSetup(projectRoot)) {
737
931
  return readDeveloperSlug(projectRoot);
738
932
  }
739
- const slug = await p2.text({
933
+ const slug = await p3.text({
740
934
  message: "CDN dev slug (unique per developer for shared CDN mirror \u2014 e.g. your first name)",
741
935
  placeholder: "manab",
742
936
  validate: (value) => {
@@ -746,7 +940,7 @@ async function promptDeveloperSlug(projectRoot) {
746
940
  return void 0;
747
941
  }
748
942
  });
749
- if (p2.isCancel(slug) || typeof slug !== "string") {
943
+ if (p3.isCancel(slug) || typeof slug !== "string") {
750
944
  return void 0;
751
945
  }
752
946
  writeDeveloperSlug(projectRoot, slug);
@@ -788,37 +982,37 @@ function resolveModuleBuildIdMetaForCli(projectRoot, env = process.env) {
788
982
  }
789
983
 
790
984
  // src/lib/upload-only-module.ts
791
- import { existsSync as existsSync5 } from "fs";
792
- import { resolve as resolve6 } from "path";
985
+ import { existsSync as existsSync6 } from "fs";
986
+ import { resolve as resolve7 } from "path";
793
987
  function isUploadOnlyPreexistingProject(projectRoot, moduleArtifactDirs) {
794
988
  if (moduleArtifactDirs.length === 0) return false;
795
- if (existsSync5(resolve6(projectRoot, "package.json"))) return false;
796
- if (existsSync5(resolve6(projectRoot, "src"))) return false;
797
- return moduleArtifactDirs.some((dir) => existsSync5(resolve6(dir, "fields.json")));
989
+ if (existsSync6(resolve7(projectRoot, "package.json"))) return false;
990
+ if (existsSync6(resolve7(projectRoot, "src"))) return false;
991
+ return moduleArtifactDirs.some((dir) => existsSync6(resolve7(dir, "fields.json")));
798
992
  }
799
993
 
800
994
  // src/vite/resolve-dev-config.ts
801
- import { existsSync as existsSync6 } from "fs";
995
+ import { existsSync as existsSync7 } from "fs";
802
996
  import { platform } from "os";
803
- import { resolve as resolve7 } from "path";
997
+ import { resolve as resolve8 } from "path";
804
998
  var VITE_JS_SEGMENTS = ["node_modules", "vite", "bin", "vite.js"];
805
999
  function isProjectViteInstalled(projectRoot) {
806
- return existsSync6(resolve7(projectRoot, ...VITE_JS_SEGMENTS));
1000
+ return existsSync7(resolve8(projectRoot, ...VITE_JS_SEGMENTS));
807
1001
  }
808
1002
  function resolveProjectViteSpawn(projectRoot) {
809
- const viteJs = resolve7(projectRoot, ...VITE_JS_SEGMENTS);
810
- if (existsSync6(viteJs)) {
1003
+ const viteJs = resolve8(projectRoot, ...VITE_JS_SEGMENTS);
1004
+ if (existsSync7(viteJs)) {
811
1005
  return { command: process.execPath, prefixArgs: [viteJs] };
812
1006
  }
813
- const binDir = resolve7(projectRoot, "node_modules", ".bin");
1007
+ const binDir = resolve8(projectRoot, "node_modules", ".bin");
814
1008
  if (platform() === "win32") {
815
- const winCmd = resolve7(binDir, "vite.cmd");
816
- if (existsSync6(winCmd)) {
1009
+ const winCmd = resolve8(binDir, "vite.cmd");
1010
+ if (existsSync7(winCmd)) {
817
1011
  return { command: winCmd, prefixArgs: [] };
818
1012
  }
819
1013
  }
820
- const posixShim = resolve7(binDir, "vite");
821
- if (existsSync6(posixShim)) {
1014
+ const posixShim = resolve8(binDir, "vite");
1015
+ if (existsSync7(posixShim)) {
822
1016
  return { command: posixShim, prefixArgs: [] };
823
1017
  }
824
1018
  return null;
@@ -905,10 +1099,10 @@ function envFileForTier(tier) {
905
1099
  return ".env.prod";
906
1100
  }
907
1101
  function readEnvFileValue(projectRoot, file, key) {
908
- const path = resolve8(projectRoot, file);
909
- if (!existsSync7(path)) return void 0;
1102
+ const path = resolve9(projectRoot, file);
1103
+ if (!existsSync8(path)) return void 0;
910
1104
  try {
911
- const content = readFileSync5(path, "utf8");
1105
+ const content = readFileSync8(path, "utf8");
912
1106
  const match = content.match(new RegExp(`^${key}=(.*)$`, "m"));
913
1107
  return match?.[1]?.trim();
914
1108
  } catch {
@@ -917,8 +1111,8 @@ function readEnvFileValue(projectRoot, file, key) {
917
1111
  }
918
1112
  function checkEnvFiles(ctx) {
919
1113
  const checks = [];
920
- const localPath = resolve8(ctx.projectRoot, ".env.local");
921
- const localOk = existsSync7(localPath);
1114
+ const localPath = resolve9(ctx.projectRoot, ".env.local");
1115
+ const localOk = existsSync8(localPath);
922
1116
  checks.push({
923
1117
  name: "env-local",
924
1118
  ok: localOk,
@@ -926,8 +1120,8 @@ function checkEnvFiles(ctx) {
926
1120
  remediation: localOk ? void 0 : "wp setup or copy .env.local.example \u2192 .env.local",
927
1121
  severity: "warn"
928
1122
  });
929
- const legacyLocalDev = resolve8(ctx.projectRoot, ".env.dev");
930
- if (!localOk && existsSync7(legacyLocalDev)) {
1123
+ const legacyLocalDev = resolve9(ctx.projectRoot, ".env.dev");
1124
+ if (!localOk && existsSync8(legacyLocalDev)) {
931
1125
  const nodeEnv = readEnvFileValue(ctx.projectRoot, ".env.dev", "VITE_NODE_ENV");
932
1126
  const hasDevMode = readEnvFileValue(ctx.projectRoot, ".env.dev", "VITE_DEV_MODE") != null;
933
1127
  const looksLikeLocal = hasDevMode || nodeEnv === "local" || nodeEnv == null || nodeEnv === "development";
@@ -943,7 +1137,7 @@ function checkEnvFiles(ctx) {
943
1137
  }
944
1138
  for (const tier of ctx.config.env.tiers) {
945
1139
  const file = envFileForTier(tier);
946
- const ok = existsSync7(resolve8(ctx.projectRoot, file));
1140
+ const ok = existsSync8(resolve9(ctx.projectRoot, file));
947
1141
  if (tier === "stg") {
948
1142
  checks.push({
949
1143
  name: "env-stg",
@@ -966,8 +1160,8 @@ function checkEnvFiles(ctx) {
966
1160
  }
967
1161
  function checkLegacyBuildIdFile(ctx) {
968
1162
  if (isThemeProject(ctx.config) || !usesCdnPipeline(ctx.config)) return null;
969
- const legacy = resolve8(ctx.projectRoot, ".portal-build-id");
970
- if (!existsSync7(legacy)) return null;
1163
+ const legacy = resolve9(ctx.projectRoot, ".portal-build-id");
1164
+ if (!existsSync8(legacy)) return null;
971
1165
  return {
972
1166
  name: "legacy-build-id",
973
1167
  ok: false,
@@ -978,8 +1172,8 @@ function checkLegacyBuildIdFile(ctx) {
978
1172
  }
979
1173
  function checkBuildId(ctx) {
980
1174
  if (isThemeProject(ctx.config) || !usesCdnPipeline(ctx.config)) return null;
981
- const file = resolve8(ctx.projectRoot, ctx.config.cdn.buildIdFile);
982
- const ok = existsSync7(file);
1175
+ const file = resolve9(ctx.projectRoot, ctx.config.cdn.buildIdFile);
1176
+ const ok = existsSync8(file);
983
1177
  return {
984
1178
  name: "build-id",
985
1179
  ok,
@@ -1033,7 +1227,7 @@ function checkDevSlug(ctx) {
1033
1227
  };
1034
1228
  }
1035
1229
  function isUploadOnlyProject(ctx) {
1036
- const moduleDirs = [resolve8(ctx.projectRoot, ctx.config.module.uploadPath)];
1230
+ const moduleDirs = [resolve9(ctx.projectRoot, ctx.config.module.uploadPath)];
1037
1231
  return isUploadOnlyPreexistingProject(ctx.projectRoot, moduleDirs);
1038
1232
  }
1039
1233
  function checkUploadOnlyNote(ctx) {
@@ -1047,8 +1241,8 @@ function checkUploadOnlyNote(ctx) {
1047
1241
  };
1048
1242
  }
1049
1243
  function checkDevViteConfig(ctx) {
1050
- const path = resolve8(ctx.projectRoot, "vite.config.dev.mjs");
1051
- const ok = existsSync7(path);
1244
+ const path = resolve9(ctx.projectRoot, "vite.config.dev.mjs");
1245
+ const ok = existsSync8(path);
1052
1246
  return {
1053
1247
  name: "vite-dev-config",
1054
1248
  ok,
@@ -1059,8 +1253,8 @@ function checkDevViteConfig(ctx) {
1059
1253
  }
1060
1254
  function checkMetaJson(ctx) {
1061
1255
  if (isThemeProject(ctx.config)) return null;
1062
- const path = resolve8(ctx.projectRoot, "public/meta.json");
1063
- if (!existsSync7(path)) {
1256
+ const path = resolve9(ctx.projectRoot, "public/meta.json");
1257
+ if (!existsSync8(path)) {
1064
1258
  return {
1065
1259
  name: "meta-json",
1066
1260
  ok: false,
@@ -1070,7 +1264,7 @@ function checkMetaJson(ctx) {
1070
1264
  };
1071
1265
  }
1072
1266
  try {
1073
- const meta = JSON.parse(readFileSync5(path, "utf8"));
1267
+ const meta = JSON.parse(readFileSync8(path, "utf8"));
1074
1268
  const hasContentTypes = Array.isArray(meta.content_types) && meta.content_types.length > 0;
1075
1269
  return {
1076
1270
  name: "meta-json",
@@ -1092,8 +1286,8 @@ function checkMetaJson(ctx) {
1092
1286
  }
1093
1287
  async function checkCdnMirrorPublic(ctx) {
1094
1288
  if (isThemeProject(ctx.config) || !usesCdnPipeline(ctx.config)) return null;
1095
- const mirrorPath = resolve8(ctx.projectRoot, ctx.config.cdn?.mirror ?? "portal.cdn.json");
1096
- if (!existsSync7(mirrorPath)) {
1289
+ const mirrorPath = resolve9(ctx.projectRoot, ctx.config.cdn?.mirror ?? "portal.cdn.json");
1290
+ if (!existsSync8(mirrorPath)) {
1097
1291
  return {
1098
1292
  name: "cdn-mirror-public",
1099
1293
  ok: false,
@@ -1104,7 +1298,7 @@ async function checkCdnMirrorPublic(ctx) {
1104
1298
  }
1105
1299
  let github;
1106
1300
  try {
1107
- const mirror = JSON.parse(readFileSync5(mirrorPath, "utf8"));
1301
+ const mirror = JSON.parse(readFileSync8(mirrorPath, "utf8"));
1108
1302
  github = mirror.github?.trim();
1109
1303
  } catch {
1110
1304
  return {
@@ -1124,6 +1318,15 @@ async function checkCdnMirrorPublic(ctx) {
1124
1318
  severity: "error"
1125
1319
  };
1126
1320
  }
1321
+ if (isPlaceholderGithubSlug(github)) {
1322
+ return {
1323
+ name: "cdn-mirror-public",
1324
+ ok: false,
1325
+ message: `portal.cdn.json still has placeholder github slug (${github})`,
1326
+ remediation: "Run wp setup and set a real public org/repo for jsDelivr",
1327
+ severity: "warn"
1328
+ };
1329
+ }
1127
1330
  try {
1128
1331
  const res = await fetch(`https://api.github.com/repos/${github}`, {
1129
1332
  headers: { Accept: "application/vnd.github+json" }
@@ -1156,8 +1359,8 @@ async function checkCdnMirrorPublic(ctx) {
1156
1359
  severity: "warn"
1157
1360
  };
1158
1361
  }
1159
- const manifestPath = resolve8(ctx.projectRoot, "dist/cdn/manifest.json");
1160
- if (!existsSync7(manifestPath)) {
1362
+ const manifestPath = resolve9(ctx.projectRoot, "dist/cdn/manifest.json");
1363
+ if (!existsSync8(manifestPath)) {
1161
1364
  return {
1162
1365
  name: "cdn-mirror-public",
1163
1366
  ok: true,
@@ -1166,7 +1369,7 @@ async function checkCdnMirrorPublic(ctx) {
1166
1369
  };
1167
1370
  }
1168
1371
  try {
1169
- const manifest = JSON.parse(readFileSync5(manifestPath, "utf8"));
1372
+ const manifest = JSON.parse(readFileSync8(manifestPath, "utf8"));
1170
1373
  const appUrl = manifest.scripts?.find((s) => s.name === "app")?.url;
1171
1374
  if (!appUrl) {
1172
1375
  return {
@@ -1176,20 +1379,36 @@ async function checkCdnMirrorPublic(ctx) {
1176
1379
  severity: "error"
1177
1380
  };
1178
1381
  }
1179
- const head = await fetch(appUrl, { method: "HEAD" });
1180
- if (!head.ok) {
1382
+ const appHead = await fetch(appUrl, { method: "HEAD" });
1383
+ if (!appHead.ok) {
1181
1384
  return {
1182
1385
  name: "cdn-mirror-public",
1183
1386
  ok: false,
1184
- message: `jsDelivr app chunk not reachable (HTTP ${head.status}) \u2014 HubSpot preview will be blank`,
1387
+ message: `jsDelivr app chunk not reachable (HTTP ${appHead.status}) \u2014 HubSpot preview will be blank`,
1185
1388
  remediation: "Run wp deploy (publish CDN + upload module) after making the mirror repo public",
1186
1389
  severity: "error"
1187
1390
  };
1188
1391
  }
1392
+ const lazyChunks = (manifest.scripts ?? []).filter(
1393
+ (entry) => typeof entry.url === "string" && (entry.file?.startsWith("chunks/") || entry.url.includes("/chunks/"))
1394
+ );
1395
+ for (const chunk of lazyChunks) {
1396
+ const chunkHead = await fetch(chunk.url, { method: "HEAD" });
1397
+ if (!chunkHead.ok) {
1398
+ return {
1399
+ name: "cdn-mirror-public",
1400
+ ok: false,
1401
+ message: `jsDelivr lazy chunk not reachable: ${chunk.file ?? chunk.url} (HTTP ${chunkHead.status})`,
1402
+ remediation: "Run wp deploy to republish dist/cdn (including chunks/) and wait 5\u201315 min for jsDelivr propagation",
1403
+ severity: "error"
1404
+ };
1405
+ }
1406
+ }
1407
+ const lazyChunkNote = lazyChunks.length > 0 ? ` and ${lazyChunks.length} lazy chunk(s) are reachable` : "";
1189
1408
  return {
1190
1409
  name: "cdn-mirror-public",
1191
1410
  ok: true,
1192
- message: `CDN mirror ${github} is public and jsDelivr app chunk is reachable`,
1411
+ message: `CDN mirror ${github} is public and jsDelivr app chunk${lazyChunkNote}`,
1193
1412
  severity: "error"
1194
1413
  };
1195
1414
  } catch {
@@ -1203,8 +1422,8 @@ async function checkCdnMirrorPublic(ctx) {
1203
1422
  }
1204
1423
  function checkFieldsJson(ctx) {
1205
1424
  if (isThemeProject(ctx.config)) return null;
1206
- const path = resolve8(ctx.projectRoot, "public/fields.json");
1207
- if (!existsSync7(path)) {
1425
+ const path = resolve9(ctx.projectRoot, "public/fields.json");
1426
+ if (!existsSync8(path)) {
1208
1427
  return {
1209
1428
  name: "fields-json",
1210
1429
  ok: false,
@@ -1214,7 +1433,7 @@ function checkFieldsJson(ctx) {
1214
1433
  };
1215
1434
  }
1216
1435
  try {
1217
- const fields = JSON.parse(readFileSync5(path, "utf8"));
1436
+ const fields = JSON.parse(readFileSync8(path, "utf8"));
1218
1437
  const issues = validateHubSpotFieldsJson(fields);
1219
1438
  if (issues.length === 0) {
1220
1439
  return {
@@ -1244,11 +1463,11 @@ function checkFieldsJson(ctx) {
1244
1463
  }
1245
1464
  }
1246
1465
  function readProjectFile(projectRoot, relativePath) {
1247
- const path = resolve8(projectRoot, relativePath);
1248
- if (!existsSync7(path)) {
1466
+ const path = resolve9(projectRoot, relativePath);
1467
+ if (!existsSync8(path)) {
1249
1468
  return null;
1250
1469
  }
1251
- return readFileSync5(path, "utf8");
1470
+ return readFileSync8(path, "utf8");
1252
1471
  }
1253
1472
  function checkCssPipeline(ctx) {
1254
1473
  if (isThemeProject(ctx.config) || isUploadOnlyProject(ctx)) {
@@ -1315,9 +1534,9 @@ function checkCssPipeline(ctx) {
1315
1534
  }
1316
1535
  const moduleOutDir = ctx.config.module?.outDir;
1317
1536
  if (moduleOutDir && usesCdnPipeline(ctx.config)) {
1318
- const moduleCssPath = resolve8(ctx.projectRoot, moduleOutDir, "module.css");
1319
- if (existsSync7(moduleCssPath)) {
1320
- const css = readFileSync5(moduleCssPath, "utf8");
1537
+ const moduleCssPath = resolve9(ctx.projectRoot, moduleOutDir, "module.css");
1538
+ if (existsSync8(moduleCssPath)) {
1539
+ const css = readFileSync8(moduleCssPath, "utf8");
1321
1540
  const hasDwUtilities = /dw\\:/.test(css) || /\.dw:/.test(css);
1322
1541
  checks.push({
1323
1542
  name: "css-module-dist-prefixed",
@@ -1332,8 +1551,8 @@ function checkCssPipeline(ctx) {
1332
1551
  }
1333
1552
  function checkHybridModuleHtml(ctx) {
1334
1553
  if (isThemeProject(ctx.config) || !usesCdnPipeline(ctx.config)) return null;
1335
- const path = resolve8(ctx.projectRoot, "public/module.html");
1336
- if (!existsSync7(path)) {
1554
+ const path = resolve9(ctx.projectRoot, "public/module.html");
1555
+ if (!existsSync8(path)) {
1337
1556
  return {
1338
1557
  name: "module-html",
1339
1558
  ok: false,
@@ -1342,7 +1561,7 @@ function checkHybridModuleHtml(ctx) {
1342
1561
  severity: "error"
1343
1562
  };
1344
1563
  }
1345
- const html = readFileSync5(path, "utf8");
1564
+ const html = readFileSync8(path, "utf8");
1346
1565
  const needs = ["__PORTAL_CDN_BASE__", "require_js", "hubSpotData", 'id="app"'];
1347
1566
  const missing = needs.filter((token) => !html.includes(token));
1348
1567
  const usesUndeclaredCdnFields = /module\.portal_(cdn_base_url|vendor_script|app_script|app_css)/.test(
@@ -1360,8 +1579,8 @@ function checkHybridModuleHtml(ctx) {
1360
1579
  function checkCdnViteConfigs(ctx) {
1361
1580
  if (isThemeProject(ctx.config) || !usesCdnPipeline(ctx.config)) return [];
1362
1581
  return ["vite.config.cdn-vendor.mjs", "vite.config.cdn-app.mjs"].map((file) => {
1363
- const path = resolve8(ctx.projectRoot, file);
1364
- const ok = existsSync7(path);
1582
+ const path = resolve9(ctx.projectRoot, file);
1583
+ const ok = existsSync8(path);
1365
1584
  return {
1366
1585
  name: file.replace(/\./g, "-"),
1367
1586
  ok,
@@ -1373,8 +1592,8 @@ function checkCdnViteConfigs(ctx) {
1373
1592
  }
1374
1593
  function checkDevEntry(ctx) {
1375
1594
  const checks = [];
1376
- const indexHtml = resolve8(ctx.projectRoot, "index.html");
1377
- const hasIndex = existsSync7(indexHtml);
1595
+ const indexHtml = resolve9(ctx.projectRoot, "index.html");
1596
+ const hasIndex = existsSync8(indexHtml);
1378
1597
  checks.push({
1379
1598
  name: "index-html",
1380
1599
  ok: hasIndex,
@@ -1382,8 +1601,8 @@ function checkDevEntry(ctx) {
1382
1601
  remediation: hasIndex ? void 0 : "Re-run wp init or copy index.html from kit templates",
1383
1602
  severity: "error"
1384
1603
  });
1385
- const localDev = resolve8(ctx.projectRoot, ctx.config.entries.localDev);
1386
- const hasEntry = existsSync7(localDev);
1604
+ const localDev = resolve9(ctx.projectRoot, ctx.config.entries.localDev);
1605
+ const hasEntry = existsSync8(localDev);
1387
1606
  checks.push({
1388
1607
  name: "local-dev-entry",
1389
1608
  ok: hasEntry,
@@ -1393,7 +1612,7 @@ function checkDevEntry(ctx) {
1393
1612
  });
1394
1613
  if (hasEntry) {
1395
1614
  try {
1396
- const source = readFileSync5(localDev, "utf8");
1615
+ const source = readFileSync8(localDev, "utf8");
1397
1616
  const usesKitNodeBootstrap = source.includes("@woodsportal/hubspot-kit/dev/bootstrap-greenfield") || source.includes("@woodsportal/hubspot-kit/dev/bootstrap");
1398
1617
  const usesWphsShim = source.includes(".wphs/.kit-bootstrap-greenfield") || source.includes(".wphs/.kit-bootstrap") || source.includes("@wphs/bootstrap-greenfield") || source.includes("@wphs/bootstrap");
1399
1618
  checks.push({
@@ -1410,7 +1629,7 @@ function checkDevEntry(ctx) {
1410
1629
  }
1411
1630
  function checkKitScripts() {
1412
1631
  const script = kitPath("scripts", "build-cdn.mjs");
1413
- const ok = existsSync7(script);
1632
+ const ok = existsSync8(script);
1414
1633
  return {
1415
1634
  name: "kit-scripts",
1416
1635
  ok,
@@ -1419,12 +1638,38 @@ function checkKitScripts() {
1419
1638
  severity: "error"
1420
1639
  };
1421
1640
  }
1641
+ function checkWphsScaffold(scaffoldError) {
1642
+ if (!scaffoldError) return null;
1643
+ return {
1644
+ name: "wphs-scaffold",
1645
+ ok: false,
1646
+ message: `.wphs workspace scaffold failed \u2014 ${scaffoldError}`,
1647
+ remediation: "Run wp doctor --verbose or wp setup; check write permissions on the project root (Windows: enable Developer Mode if junction creation fails)",
1648
+ severity: "error"
1649
+ };
1650
+ }
1651
+ async function runWphsScaffold(ctx) {
1652
+ if (ctx.global.dryRun) return void 0;
1653
+ try {
1654
+ const paths = await ensureProjectWphsWorkspace(ctx.projectRoot);
1655
+ if (ctx.global.verbose) {
1656
+ ctx.logger.info(`.wphs workspace ready at ${paths.workspaceRoot}`);
1657
+ }
1658
+ return void 0;
1659
+ } catch (error) {
1660
+ const message = error instanceof Error ? error.message : String(error);
1661
+ if (ctx.global.verbose) {
1662
+ ctx.logger.error(`Could not scaffold .wphs workspace: ${message}`);
1663
+ }
1664
+ return message;
1665
+ }
1666
+ }
1422
1667
  function checkWphsWorkspace(ctx) {
1423
1668
  const workspace = ctx.config.dev.moduleConfigDir;
1424
1669
  const checks = [];
1425
1670
  for (const legacy of [".dev", ".woodsportal"]) {
1426
- const legacyPath = resolve8(ctx.projectRoot, legacy);
1427
- if (existsSync7(legacyPath)) {
1671
+ const legacyPath = resolve9(ctx.projectRoot, legacy);
1672
+ if (existsSync8(legacyPath)) {
1428
1673
  checks.push({
1429
1674
  name: `legacy-${legacy}`,
1430
1675
  ok: false,
@@ -1434,7 +1679,7 @@ function checkWphsWorkspace(ctx) {
1434
1679
  });
1435
1680
  }
1436
1681
  }
1437
- if (existsSync7(resolve8(ctx.projectRoot, ".wp-hs-baseline"))) {
1682
+ if (existsSync8(resolve9(ctx.projectRoot, ".wp-hs-baseline"))) {
1438
1683
  checks.push({
1439
1684
  name: "legacy-baseline",
1440
1685
  ok: false,
@@ -1453,15 +1698,15 @@ function checkWphsWorkspace(ctx) {
1453
1698
  });
1454
1699
  return checks;
1455
1700
  }
1456
- const wphsRoot = resolve8(ctx.projectRoot, workspace);
1701
+ const wphsRoot = resolve9(ctx.projectRoot, workspace);
1457
1702
  const required = ["config", "assets/branding", "fixtures", "cache", "audit/baseline", "state"];
1458
1703
  for (const sub of required) {
1459
- const ok = existsSync7(join(wphsRoot, sub));
1704
+ const ok = existsSync8(join2(wphsRoot, sub));
1460
1705
  checks.push({
1461
1706
  name: `wphs-${sub.replace(/\//g, "-")}`,
1462
1707
  ok,
1463
1708
  message: ok ? `.wphs/${sub}/ present` : `.wphs/${sub}/ missing`,
1464
- remediation: ok ? void 0 : "Run wp doctor (auto-scaffolds) or wp init",
1709
+ remediation: ok ? void 0 : "Run wp setup or wp doctor --verbose; check write permissions on the project root",
1465
1710
  severity: "error"
1466
1711
  });
1467
1712
  }
@@ -1469,8 +1714,8 @@ function checkWphsWorkspace(ctx) {
1469
1714
  }
1470
1715
  function checkAdapter(ctx) {
1471
1716
  if (isThemeProject(ctx.config)) return null;
1472
- const adapterPath = resolve8(ctx.projectRoot, ctx.config.dev.adapter);
1473
- const ok = existsSync7(adapterPath);
1717
+ const adapterPath = resolve9(ctx.projectRoot, ctx.config.dev.adapter);
1718
+ const ok = existsSync8(adapterPath);
1474
1719
  return {
1475
1720
  name: "wphs-adapter",
1476
1721
  ok,
@@ -1481,11 +1726,11 @@ function checkAdapter(ctx) {
1481
1726
  }
1482
1727
  function checkModuleConfigUiDeps(ctx) {
1483
1728
  if (isThemeProject(ctx.config)) return [];
1484
- const pkgPath = resolve8(ctx.projectRoot, "package.json");
1485
- if (!existsSync7(pkgPath)) return [];
1729
+ const pkgPath = resolve9(ctx.projectRoot, "package.json");
1730
+ if (!existsSync8(pkgPath)) return [];
1486
1731
  let deps = {};
1487
1732
  try {
1488
- const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
1733
+ const pkg = JSON.parse(readFileSync8(pkgPath, "utf8"));
1489
1734
  deps = { ...pkg.dependencies, ...pkg.devDependencies };
1490
1735
  } catch {
1491
1736
  return [];
@@ -1512,7 +1757,7 @@ function checkNodeLoaderInterop() {
1512
1757
  };
1513
1758
  }
1514
1759
  function checkViteInstalled(ctx) {
1515
- if (isThemeProject(ctx.config) && !existsSync7(resolve8(ctx.projectRoot, "vite.config.dev.mjs"))) {
1760
+ if (isThemeProject(ctx.config) && !existsSync8(resolve9(ctx.projectRoot, "vite.config.dev.mjs"))) {
1516
1761
  return null;
1517
1762
  }
1518
1763
  const ok = isProjectViteInstalled(ctx.projectRoot);
@@ -1526,19 +1771,12 @@ function checkViteInstalled(ctx) {
1526
1771
  }
1527
1772
  async function gatherDoctorChecks(ctx) {
1528
1773
  const checks = [];
1774
+ let wphsScaffoldError;
1529
1775
  await runCliTasks(ctx, [
1530
1776
  {
1531
1777
  title: "Preparing .wphs workspace",
1532
1778
  task: async () => {
1533
- if (ctx.global.dryRun) return;
1534
- try {
1535
- runNodeScript(kitPath("scripts", "ensure-wphs-workspace.mjs"), [], {
1536
- cwd: ctx.projectRoot,
1537
- logger: ctx.logger,
1538
- dryRun: false
1539
- });
1540
- } catch {
1541
- }
1779
+ wphsScaffoldError = await runWphsScaffold(ctx);
1542
1780
  }
1543
1781
  },
1544
1782
  {
@@ -1562,7 +1800,11 @@ async function gatherDoctorChecks(ctx) {
1562
1800
  {
1563
1801
  title: "Checking woodscli.json and project layout",
1564
1802
  task: async () => {
1565
- checks.push(...checkConfig(ctx), ...checkWphsWorkspace(ctx));
1803
+ wphsScaffoldError = await runWphsScaffold(ctx) ?? wphsScaffoldError;
1804
+ checks.push(...checkConfig(ctx));
1805
+ const scaffoldCheck = checkWphsScaffold(wphsScaffoldError);
1806
+ if (scaffoldCheck) checks.push(scaffoldCheck);
1807
+ checks.push(...checkWphsWorkspace(ctx));
1566
1808
  const fieldsCheck = checkFieldsJson(ctx);
1567
1809
  if (fieldsCheck) checks.push(fieldsCheck);
1568
1810
  const metaCheck = checkMetaJson(ctx);
@@ -1672,26 +1914,7 @@ async function runConfigValidate(ctx) {
1672
1914
  import { resolve as resolve10 } from "path";
1673
1915
 
1674
1916
  // src/lib/cli-banner.ts
1675
- import pc3 from "picocolors";
1676
-
1677
- // src/lib/kit-version.ts
1678
- import { readFileSync as readFileSync6 } from "fs";
1679
- import { resolve as resolve9 } from "path";
1680
- var cachedVersion = null;
1681
- function readKitVersion() {
1682
- if (cachedVersion) {
1683
- return cachedVersion;
1684
- }
1685
- try {
1686
- const pkg = JSON.parse(readFileSync6(resolve9(getKitRoot(), "package.json"), "utf8"));
1687
- cachedVersion = pkg.version ?? "0.0.0";
1688
- } catch {
1689
- cachedVersion = "0.0.0";
1690
- }
1691
- return cachedVersion;
1692
- }
1693
-
1694
- // src/lib/cli-banner.ts
1917
+ import pc4 from "picocolors";
1695
1918
  var WOODSCLI_ASCII_BANNER = `
1696
1919
  __ __ _ _____ _ _____
1697
1920
  \\ \\ / / | | / ____| | |_ _|
@@ -1721,7 +1944,7 @@ function formatProjectLabel(config) {
1721
1944
  return `${config.module.name} \xB7 ${config.projectType}${routerNote}`;
1722
1945
  }
1723
1946
  function section(color, title, body) {
1724
- const bold = (text4) => color ? pc3.bold(text4) : text4;
1947
+ const bold = (text5) => color ? pc4.bold(text5) : text5;
1725
1948
  return ` ${bold(title)}
1726
1949
  ${body}`;
1727
1950
  }
@@ -1730,10 +1953,10 @@ function printWoodsCliDevBanner(options) {
1730
1953
  return;
1731
1954
  }
1732
1955
  const color = useCliColor();
1733
- const cyan = (text4) => color ? pc3.cyan(text4) : text4;
1734
- const dim = (text4) => color ? pc3.dim(text4) : text4;
1735
- const bold = (text4) => color ? pc3.bold(text4) : text4;
1736
- const yellow = (text4) => color ? pc3.yellow(text4) : text4;
1956
+ const cyan = (text5) => color ? pc4.cyan(text5) : text5;
1957
+ const dim = (text5) => color ? pc4.dim(text5) : text5;
1958
+ const bold = (text5) => color ? pc4.bold(text5) : text5;
1959
+ const yellow = (text5) => color ? pc4.yellow(text5) : text5;
1737
1960
  const version = options.version ?? readKitVersion();
1738
1961
  const project = formatProjectLabel(options.config);
1739
1962
  const isTheme = isThemeProject(options.config);
@@ -1994,86 +2217,17 @@ async function runDev(ctx, options) {
1994
2217
  }
1995
2218
 
1996
2219
  // src/cli/commands/deploy.ts
1997
- import { resolve as resolve13 } from "path";
1998
-
1999
- // src/lib/project-env-files.ts
2000
- import { existsSync as existsSync8, readFileSync as readFileSync7, writeFileSync as writeFileSync2 } from "fs";
2001
- import { resolve as resolve11 } from "path";
2002
- var ENV_LOCAL_FILE = ".env.local";
2003
- var ENV_GITIGNORE_LINES = [".env", ".env.*", "!.env*.example", ENV_LOCAL_FILE];
2004
- function parseEnvFile(content) {
2005
- const result = {};
2006
- for (const line of content.split("\n")) {
2007
- const trimmed = line.trim();
2008
- if (!trimmed || trimmed.startsWith("#")) continue;
2009
- const eq = trimmed.indexOf("=");
2010
- if (eq <= 0) continue;
2011
- const key = trimmed.slice(0, eq).trim();
2012
- let value = trimmed.slice(eq + 1).trim();
2013
- if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
2014
- value = value.slice(1, -1);
2015
- }
2016
- result[key] = value;
2017
- }
2018
- return result;
2019
- }
2020
- function readEnvLocal(projectRoot) {
2021
- const path = resolve11(projectRoot, ENV_LOCAL_FILE);
2022
- if (!existsSync8(path)) return {};
2023
- return parseEnvFile(readFileSync7(path, "utf8"));
2024
- }
2025
- function formatEnvLocal(entries) {
2026
- const lines = [
2027
- "# Local secrets \u2014 gitignored. Created by wp init / wp setup (hybrid CDN).",
2028
- "# PORTAL_CDN_PUBLISH_TOKEN needs repo push on your portal.cdn.json mirror.",
2029
- ""
2030
- ];
2031
- for (const [key, value] of Object.entries(entries)) {
2032
- if (value.includes(" ") || value.includes("#")) {
2033
- lines.push(`${key}="${value.replace(/"/g, '\\"')}"`);
2034
- } else {
2035
- lines.push(`${key}=${value}`);
2036
- }
2037
- }
2038
- return `${lines.join("\n").trimEnd()}
2039
- `;
2040
- }
2041
- function writeEnvLocal(projectRoot, entries) {
2042
- const path = resolve11(projectRoot, ENV_LOCAL_FILE);
2043
- const existing = existsSync8(path) ? parseEnvFile(readFileSync7(path, "utf8")) : {};
2044
- writeFileSync2(path, formatEnvLocal({ ...existing, ...entries }));
2045
- }
2046
- function mergeProjectEnv(projectRoot, base = process.env) {
2047
- const local = readEnvLocal(projectRoot);
2048
- const merged = { ...base };
2049
- for (const [key, value] of Object.entries(local)) {
2050
- if (merged[key] == null || merged[key] === "") {
2051
- merged[key] = value;
2052
- }
2053
- }
2054
- return merged;
2055
- }
2056
- function ensureEnvFilesGitignore(projectRoot) {
2057
- const gitignorePath = resolve11(projectRoot, ".gitignore");
2058
- const existing = existsSync8(gitignorePath) ? readFileSync7(gitignorePath, "utf8") : "";
2059
- const toAppend = ENV_GITIGNORE_LINES.filter((line) => !existing.includes(line)).join("\n");
2060
- if (!toAppend) return;
2061
- writeFileSync2(gitignorePath, `${existing.trimEnd()}
2062
-
2063
- # Env files (secrets)
2064
- ${toAppend}
2065
- `);
2066
- }
2220
+ import { resolve as resolve12 } from "path";
2067
2221
 
2068
2222
  // src/cli/commands/upload.ts
2069
- import { resolve as resolve12 } from "path";
2223
+ import { resolve as resolve11 } from "path";
2070
2224
  async function uploadModuleOrTheme(ctx) {
2071
2225
  if (isThemeProject(ctx.config)) {
2072
2226
  const theme = ctx.config.theme;
2073
2227
  if (!theme) {
2074
2228
  throw new WpHsError("theme block missing in woodscli.json", { exitCode: ExitCode.USER_ERROR });
2075
2229
  }
2076
- const srcDir = resolve12(ctx.projectRoot, theme.srcDir);
2230
+ const srcDir = resolve11(ctx.projectRoot, theme.srcDir);
2077
2231
  await runCommandAsync("hs", ["cms", "upload", srcDir, theme.uploadPath], {
2078
2232
  cwd: ctx.projectRoot,
2079
2233
  projectRoot: ctx.projectRoot,
@@ -2083,7 +2237,7 @@ async function uploadModuleOrTheme(ctx) {
2083
2237
  });
2084
2238
  return `Uploaded theme ${srcDir} \u2192 ${theme.uploadPath}`;
2085
2239
  }
2086
- const outDir = resolve12(ctx.projectRoot, ctx.config.module.outDir);
2240
+ const outDir = resolve11(ctx.projectRoot, ctx.config.module.outDir);
2087
2241
  const uploadPath = ctx.config.module.uploadPath;
2088
2242
  await runCommandAsync("hs", ["cms", "upload", outDir, uploadPath], {
2089
2243
  cwd: ctx.projectRoot,
@@ -2260,7 +2414,7 @@ async function runDeploy(ctx, options) {
2260
2414
  if (!ctx.global.json) {
2261
2415
  printCommandDone(
2262
2416
  ctx,
2263
- `Deploy complete (${resolve13(ctx.projectRoot, ctx.config.module.outDir)} \u2192 ${ctx.config.module.uploadPath})`
2417
+ `Deploy complete (${resolve12(ctx.projectRoot, ctx.config.module.outDir)} \u2192 ${ctx.config.module.uploadPath})`
2264
2418
  );
2265
2419
  }
2266
2420
  return ExitCode.SUCCESS;
@@ -2333,8 +2487,8 @@ async function runPublish(ctx, options) {
2333
2487
 
2334
2488
  // src/cli/commands/audit.ts
2335
2489
  import { createHash } from "crypto";
2336
- import { cpSync, existsSync as existsSync9, mkdirSync as mkdirSync2, readFileSync as readFileSync8, readdirSync, rmSync, statSync } from "fs";
2337
- import { resolve as resolve14, join as join2 } from "path";
2490
+ import { cpSync, existsSync as existsSync9, mkdirSync as mkdirSync3, readFileSync as readFileSync9, readdirSync, rmSync, statSync } from "fs";
2491
+ import { resolve as resolve13, join as join3 } from "path";
2338
2492
  var FORBIDDEN_SHIP_TOKENS = [
2339
2493
  "ModuleConfigOverlay",
2340
2494
  "module-config-store",
@@ -2343,15 +2497,16 @@ var FORBIDDEN_SHIP_TOKENS = [
2343
2497
  "dev.module-config"
2344
2498
  ];
2345
2499
  var FORBIDDEN_FIXTURE_MENU_TOKENS = ["User Home", "Primary Company Deals"];
2500
+ var FORBIDDEN_PREVIEW_FIXTURE_TOKENS = ["preview.editor@example.com", "CMS_EDITOR_PREVIEW"];
2346
2501
  function hashFile(path) {
2347
- return createHash("sha256").update(readFileSync8(path)).digest("hex");
2502
+ return createHash("sha256").update(readFileSync9(path)).digest("hex");
2348
2503
  }
2349
2504
  function walkFiles(dir) {
2350
2505
  if (!existsSync9(dir)) return [];
2351
2506
  const entries = readdirSync(dir);
2352
2507
  const files = [];
2353
2508
  for (const entry of entries) {
2354
- const full = join2(dir, entry);
2509
+ const full = join3(dir, entry);
2355
2510
  if (statSync(full).isDirectory()) {
2356
2511
  files.push(...walkFiles(full));
2357
2512
  } else {
@@ -2363,26 +2518,31 @@ function walkFiles(dir) {
2363
2518
  async function runAuditShipBundle(ctx) {
2364
2519
  const root = ctx.projectRoot;
2365
2520
  const targets = [
2366
- resolve14(root, "dist/cdn"),
2367
- resolve14(root, ctx.config.module.outDir)
2521
+ resolve13(root, "dist/cdn"),
2522
+ resolve13(root, ctx.config.module.outDir)
2368
2523
  ];
2369
2524
  const violations = [];
2370
2525
  for (const dir of targets) {
2371
2526
  for (const file of walkFiles(dir)) {
2372
2527
  if (!/\.(js|css|html)$/i.test(file)) continue;
2373
- const content = readFileSync8(file, "utf8");
2528
+ const content = readFileSync9(file, "utf8");
2374
2529
  for (const token of FORBIDDEN_SHIP_TOKENS) {
2375
2530
  if (content.includes(token)) {
2376
2531
  violations.push(`${file}: contains forbidden token "${token}"`);
2377
2532
  }
2378
2533
  }
2379
- const isCdnAppBundle = /[/\\]dist[/\\]cdn[/\\]app\..*\.js$/i.test(file);
2380
- if (isCdnAppBundle) {
2534
+ const isCdnAppEntry = /[/\\]dist[/\\]cdn[/\\]app\..*\.js$/i.test(file);
2535
+ if (isCdnAppEntry) {
2381
2536
  for (const token of FORBIDDEN_FIXTURE_MENU_TOKENS) {
2382
2537
  if (content.includes(token)) {
2383
2538
  violations.push(`${file}: contains fixture menu marker "${token}"`);
2384
2539
  }
2385
2540
  }
2541
+ for (const token of FORBIDDEN_PREVIEW_FIXTURE_TOKENS) {
2542
+ if (content.includes(token)) {
2543
+ violations.push(`${file}: contains preview fixture marker "${token}"`);
2544
+ }
2545
+ }
2386
2546
  }
2387
2547
  }
2388
2548
  }
@@ -2405,8 +2565,8 @@ async function runAuditBaselineCapture(ctx, options) {
2405
2565
  const root = ctx.projectRoot;
2406
2566
  const baseline = options.baselineDir ?? defaultBaselineDir(root, ctx.config);
2407
2567
  const sources = [
2408
- { label: "cdn", path: resolve14(root, "dist/cdn") },
2409
- { label: "hubspot", path: resolve14(root, ctx.config.module.outDir) }
2568
+ { label: "cdn", path: resolve13(root, "dist/cdn") },
2569
+ { label: "hubspot", path: resolve13(root, ctx.config.module.outDir) }
2410
2570
  ];
2411
2571
  if (ctx.global.dryRun) {
2412
2572
  ctx.logger.info(`[dry-run] Would capture baseline to ${baseline}`);
@@ -2419,9 +2579,9 @@ async function runAuditBaselineCapture(ctx, options) {
2419
2579
  remediation: "Run wp build before wp audit baseline --capture."
2420
2580
  });
2421
2581
  }
2422
- const target = resolve14(baseline, label);
2582
+ const target = resolve13(baseline, label);
2423
2583
  rmSync(target, { recursive: true, force: true });
2424
- mkdirSync2(baseline, { recursive: true });
2584
+ mkdirSync3(baseline, { recursive: true });
2425
2585
  cpSync(path, target, { recursive: true });
2426
2586
  }
2427
2587
  if (ctx.global.json) {
@@ -2435,12 +2595,12 @@ async function runAuditParity(ctx, options) {
2435
2595
  const root = ctx.projectRoot;
2436
2596
  const baseline = options.baselineDir ?? defaultBaselineDir(root, ctx.config);
2437
2597
  const currentDirs = [
2438
- { label: "cdn", path: resolve14(root, "dist/cdn") },
2439
- { label: "hubspot", path: resolve14(root, ctx.config.module.outDir) }
2598
+ { label: "cdn", path: resolve13(root, "dist/cdn") },
2599
+ { label: "hubspot", path: resolve13(root, ctx.config.module.outDir) }
2440
2600
  ];
2441
2601
  const mismatches = [];
2442
2602
  for (const { label, path } of currentDirs) {
2443
- const baselinePath = resolve14(baseline, label);
2603
+ const baselinePath = resolve13(baseline, label);
2444
2604
  if (!existsSync9(baselinePath)) {
2445
2605
  ctx.logger.warn(`No baseline at ${baselinePath} \u2014 skipping ${label} parity`);
2446
2606
  continue;
@@ -2456,7 +2616,7 @@ async function runAuditParity(ctx, options) {
2456
2616
  }
2457
2617
  for (const file of baselineFiles) {
2458
2618
  const rel = file.replace(baselinePath, "");
2459
- const current = resolve14(path, rel.slice(1));
2619
+ const current = resolve13(path, rel.slice(1));
2460
2620
  if (!existsSync9(current)) continue;
2461
2621
  const bh = hashFile(file);
2462
2622
  const ch = hashFile(current);
@@ -2484,13 +2644,13 @@ var SECRET_PATTERNS = [
2484
2644
  ];
2485
2645
  async function runAuditSecrets(ctx) {
2486
2646
  const root = ctx.projectRoot;
2487
- const scanRoots = ["src", "public", ".wphs/config"].map((d) => resolve14(root, d));
2647
+ const scanRoots = ["src", "public", ".wphs/config"].map((d) => resolve13(root, d));
2488
2648
  const findings = [];
2489
2649
  for (const dir of scanRoots) {
2490
2650
  for (const file of walkFiles(dir)) {
2491
2651
  if (!/\.(ts|tsx|js|jsx|json|env|yml|yaml|html)$/i.test(file)) continue;
2492
2652
  if (file.includes("node_modules")) continue;
2493
- const content = readFileSync8(file, "utf8");
2653
+ const content = readFileSync9(file, "utf8");
2494
2654
  for (const pattern of SECRET_PATTERNS) {
2495
2655
  pattern.lastIndex = 0;
2496
2656
  if (pattern.test(content)) {
@@ -2516,13 +2676,13 @@ async function runAuditSecrets(ctx) {
2516
2676
  }
2517
2677
 
2518
2678
  // src/cli/commands/init.ts
2519
- import { cpSync as cpSync2, existsSync as existsSync13, mkdirSync as mkdirSync4, readdirSync as readdirSync2, readFileSync as readFileSync11, writeFileSync as writeFileSync6 } from "fs";
2520
- import { basename as basename2, resolve as resolve18 } from "path";
2521
- import * as p4 from "@clack/prompts";
2679
+ import { cpSync as cpSync3, existsSync as existsSync13, mkdirSync as mkdirSync5, readdirSync as readdirSync2, readFileSync as readFileSync12, writeFileSync as writeFileSync8 } from "fs";
2680
+ import { basename as basename2, resolve as resolve17 } from "path";
2681
+ import * as p5 from "@clack/prompts";
2522
2682
 
2523
2683
  // src/lib/init-router-scaffold.ts
2524
- import { existsSync as existsSync10, mkdirSync as mkdirSync3, readFileSync as readFileSync9, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
2525
- import { resolve as resolve15 } from "path";
2684
+ import { existsSync as existsSync10, mkdirSync as mkdirSync4, readFileSync as readFileSync10, rmSync as rmSync2, writeFileSync as writeFileSync5 } from "fs";
2685
+ import { resolve as resolve14 } from "path";
2526
2686
  var ROUTER_FILES = [
2527
2687
  "src/routes",
2528
2688
  "src/routeTree.gen.ts",
@@ -2536,10 +2696,10 @@ function isModuleTemplate(template) {
2536
2696
  return template === "module-hybrid-cdn" || template === "module-monolith";
2537
2697
  }
2538
2698
  function writeRouterConfig(targetDir, mode) {
2539
- const configDir = resolve15(targetDir, "src/config");
2540
- mkdirSync3(configDir, { recursive: true });
2541
- writeFileSync3(
2542
- resolve15(configDir, "router.ts"),
2699
+ const configDir = resolve14(targetDir, "src/config");
2700
+ mkdirSync4(configDir, { recursive: true });
2701
+ writeFileSync5(
2702
+ resolve14(configDir, "router.ts"),
2543
2703
  `/** Router history \u2014 set by \`wp init\`. Hash matches woodsportal-client-frontend (HubSpot CMS embeds). */
2544
2704
  export type RouterHistoryMode = 'hash' | 'browser'
2545
2705
 
@@ -2548,8 +2708,8 @@ export const ROUTER_HISTORY: RouterHistoryMode = '${mode}'
2548
2708
  );
2549
2709
  }
2550
2710
  function writeMountDevPreviewRouter(targetDir) {
2551
- writeFileSync3(
2552
- resolve15(targetDir, "src/portal/mount-dev-preview.tsx"),
2711
+ writeFileSync5(
2712
+ resolve14(targetDir, "src/portal/mount-dev-preview.tsx"),
2553
2713
  `import { mountGreenfieldDev } from '../../.wphs/.kit-bootstrap-greenfield.tsx'
2554
2714
 
2555
2715
  import { App } from '@/app'
@@ -2562,8 +2722,8 @@ export async function mountDevPreview() {
2562
2722
  );
2563
2723
  }
2564
2724
  function writeMountDevPreviewSinglePage(targetDir) {
2565
- writeFileSync3(
2566
- resolve15(targetDir, "src/portal/mount-dev-preview.tsx"),
2725
+ writeFileSync5(
2726
+ resolve14(targetDir, "src/portal/mount-dev-preview.tsx"),
2567
2727
  `import { mountGreenfieldDev } from '../../.wphs/.kit-bootstrap-greenfield.tsx'
2568
2728
 
2569
2729
  import { StarterSinglePage } from '@/features/starter'
@@ -2576,8 +2736,8 @@ export async function mountDevPreview() {
2576
2736
  );
2577
2737
  }
2578
2738
  function writeMountPortalRouter(targetDir) {
2579
- writeFileSync3(
2580
- resolve15(targetDir, "src/portal/mount-portal.tsx"),
2739
+ writeFileSync5(
2740
+ resolve14(targetDir, "src/portal/mount-portal.tsx"),
2581
2741
  `import ReactDOM from 'react-dom/client'
2582
2742
 
2583
2743
  import { App } from '@/app'
@@ -2594,8 +2754,8 @@ export async function mountPortal() {
2594
2754
  );
2595
2755
  }
2596
2756
  function writeMountPortalSinglePage(targetDir) {
2597
- writeFileSync3(
2598
- resolve15(targetDir, "src/portal/mount-portal.tsx"),
2757
+ writeFileSync5(
2758
+ resolve14(targetDir, "src/portal/mount-portal.tsx"),
2599
2759
  `import ReactDOM from 'react-dom/client'
2600
2760
 
2601
2761
  import { StarterSinglePage } from '@/features/starter'
@@ -2612,11 +2772,11 @@ export async function mountPortal() {
2612
2772
  );
2613
2773
  }
2614
2774
  function writeStarterSinglePage(targetDir) {
2615
- const file = resolve15(targetDir, "src/features/starter/components/starter-single-page.tsx");
2775
+ const file = resolve14(targetDir, "src/features/starter/components/starter-single-page.tsx");
2616
2776
  if (existsSync10(file)) {
2617
2777
  return;
2618
2778
  }
2619
- writeFileSync3(
2779
+ writeFileSync5(
2620
2780
  file,
2621
2781
  `import { StarterShell } from '@/components/layout/starter-shell'
2622
2782
 
@@ -2639,25 +2799,25 @@ export function StarterSinglePage() {
2639
2799
  );
2640
2800
  }
2641
2801
  function removePath(targetDir, relativePath) {
2642
- const full = resolve15(targetDir, relativePath);
2802
+ const full = resolve14(targetDir, relativePath);
2643
2803
  if (!existsSync10(full)) {
2644
2804
  return;
2645
2805
  }
2646
2806
  rmSync2(full, { recursive: true, force: true });
2647
2807
  }
2648
2808
  function stripTanStackFromPackage(targetDir) {
2649
- const pkgPath = resolve15(targetDir, "package.json");
2809
+ const pkgPath = resolve14(targetDir, "package.json");
2650
2810
  if (!existsSync10(pkgPath)) {
2651
2811
  return;
2652
2812
  }
2653
- const pkg = JSON.parse(readFileSync9(pkgPath, "utf8"));
2813
+ const pkg = JSON.parse(readFileSync10(pkgPath, "utf8"));
2654
2814
  if (!pkg.devDependencies) {
2655
2815
  return;
2656
2816
  }
2657
2817
  for (const dep of TANSTACK_DEPS) {
2658
2818
  delete pkg.devDependencies[dep];
2659
2819
  }
2660
- writeFileSync3(pkgPath, `${JSON.stringify(pkg, null, 2)}
2820
+ writeFileSync5(pkgPath, `${JSON.stringify(pkg, null, 2)}
2661
2821
  `);
2662
2822
  }
2663
2823
  function applyRouterScaffold(targetDir, mode, template) {
@@ -2677,141 +2837,582 @@ function applyRouterScaffold(targetDir, mode, template) {
2677
2837
  writeRouterConfig(targetDir, mode);
2678
2838
  writeMountDevPreviewRouter(targetDir);
2679
2839
  writeMountPortalRouter(targetDir);
2680
- const featuresIndex = resolve15(targetDir, "src/features/starter/index.ts");
2840
+ const featuresIndex = resolve14(targetDir, "src/features/starter/index.ts");
2681
2841
  if (existsSync10(featuresIndex)) {
2682
- let content = readFileSync9(featuresIndex, "utf8");
2842
+ let content = readFileSync10(featuresIndex, "utf8");
2683
2843
  if (!content.includes("StarterSinglePage")) {
2684
2844
  content = content.replace(
2685
2845
  "export type { StarterPreviewValues } from './hooks/use-starter-preview'\n",
2686
2846
  "export type { StarterPreviewValues } from './hooks/use-starter-preview'\nexport { StarterSinglePage } from './components/starter-single-page'\n"
2687
2847
  );
2688
- writeFileSync3(featuresIndex, content);
2848
+ writeFileSync5(featuresIndex, content);
2689
2849
  }
2690
2850
  }
2691
2851
  }
2692
2852
 
2693
- // src/lib/cdn-github-setup.ts
2694
- import { existsSync as existsSync11, readFileSync as readFileSync10, writeFileSync as writeFileSync4 } from "fs";
2695
- import { resolve as resolve16 } from "path";
2696
- import * as p3 from "@clack/prompts";
2697
- var PORTAL_CDN_PUBLISH_TOKEN_ENV = "PORTAL_CDN_PUBLISH_TOKEN";
2698
- var PORTAL_CDN_JSON = "portal.cdn.json";
2699
- var PLACEHOLDER_GITHUB = /your-org|your-portal-cdn-mirror/i;
2700
- function defaultPortalCdnJson() {
2701
- const examplePath = kitPath("templates", "shared", "portal.cdn.json.example");
2702
- if (existsSync11(examplePath)) {
2703
- return JSON.parse(readFileSync10(examplePath, "utf8"));
2704
- }
2853
+ // src/lib/init-auth-scaffold.ts
2854
+ import { cpSync as cpSync2, existsSync as existsSync11, readFileSync as readFileSync11, rmSync as rmSync3, writeFileSync as writeFileSync6 } from "fs";
2855
+ import { resolve as resolve15 } from "path";
2856
+
2857
+ // src/lib/template-auth-deps.ts
2858
+ var AUTH_SCAFFOLD_DEPS = {
2859
+ "woodsportal-client-sdk": "^4.0.7-dev.11",
2860
+ "@tanstack/react-query": "^5.66.9",
2861
+ "@t3-oss/env-core": "^0.12.0",
2862
+ zod: "^3.24.2",
2863
+ sonner: "^2.0.1"
2864
+ };
2865
+ function mergeAuthDevDependencies(existing) {
2705
2866
  return {
2706
- publishMode: "jsdelivr",
2707
- github: "your-org/your-portal-cdn-mirror",
2708
- gitRemote: "https://github.com/your-org/your-portal-cdn-mirror.git",
2709
- cdnPathInRepo: "dist/cdn",
2710
- defaultBranch: "main"
2867
+ ...existing,
2868
+ ...AUTH_SCAFFOLD_DEPS
2711
2869
  };
2712
2870
  }
2713
- function readPortalCdnJson(projectRoot) {
2714
- const path = resolve16(projectRoot, PORTAL_CDN_JSON);
2715
- if (!existsSync11(path)) return null;
2716
- try {
2717
- return JSON.parse(readFileSync10(path, "utf8"));
2718
- } catch {
2719
- return null;
2871
+
2872
+ // src/lib/init-auth-scaffold.ts
2873
+ var AUTH_SCAFFOLD_ROOT = kitPath("templates", "scaffold", "auth");
2874
+ function readTierManifest() {
2875
+ const path = resolve15(AUTH_SCAFFOLD_ROOT, "tier-manifest.json");
2876
+ return JSON.parse(readFileSync11(path, "utf8"));
2877
+ }
2878
+ function readNavManifest() {
2879
+ const path = resolve15(AUTH_SCAFFOLD_ROOT, "nav-manifest.json");
2880
+ return JSON.parse(readFileSync11(path, "utf8"));
2881
+ }
2882
+ function layersForMode(mode) {
2883
+ const manifest = readTierManifest();
2884
+ const entry = manifest.tiers[mode];
2885
+ if (!entry?.layers?.length) {
2886
+ throw new Error(`Unknown auth scaffold tier in tier-manifest.json: ${mode}`);
2720
2887
  }
2888
+ return entry.layers;
2721
2889
  }
2722
- function normalizeGithubSlug(input) {
2723
- const trimmed = input.trim().replace(/^https:\/\/github\.com\//, "").replace(/\.git$/, "");
2724
- const parts = trimmed.split("/").filter(Boolean);
2725
- if (parts.length !== 2) return null;
2726
- return `${parts[0]}/${parts[1]}`;
2890
+ function copyLayer(targetDir, layerName) {
2891
+ const layerRoot = resolve15(AUTH_SCAFFOLD_ROOT, layerName);
2892
+ if (!existsSync11(layerRoot)) {
2893
+ throw new Error(`Auth scaffold layer not found: ${layerName}`);
2894
+ }
2895
+ cpSync2(layerRoot, targetDir, { recursive: true, force: true });
2727
2896
  }
2728
- function portalCdnJsonFromGithub(github, base) {
2729
- const slug = normalizeGithubSlug(github);
2730
- if (!slug) {
2731
- throw new Error(`Invalid GitHub repo \u2014 use org/repo (got "${github}")`);
2897
+ function layerForNavMode(nav) {
2898
+ const manifest = readNavManifest();
2899
+ const entry = manifest.modes[nav];
2900
+ if (!entry?.layer) {
2901
+ throw new Error(`Unknown nav scaffold mode in nav-manifest.json: ${nav}`);
2732
2902
  }
2733
- return {
2734
- publishMode: base?.publishMode ?? "jsdelivr",
2735
- github: slug,
2736
- gitRemote: `https://github.com/${slug}.git`,
2737
- cdnPathInRepo: base?.cdnPathInRepo ?? "dist/cdn",
2738
- defaultBranch: base?.defaultBranch ?? "main"
2739
- };
2903
+ return entry.layer;
2740
2904
  }
2741
- function hasCdnPublishToken(projectRoot) {
2742
- if (process.env[PORTAL_CDN_PUBLISH_TOKEN_ENV]?.trim()) return true;
2743
- if (process.env.GITHUB_TOKEN?.trim()) return true;
2744
- return Boolean(readEnvLocal(projectRoot)[PORTAL_CDN_PUBLISH_TOKEN_ENV]?.trim());
2905
+ function copyNavLayer(targetDir, nav) {
2906
+ copyLayer(targetDir, layerForNavMode(nav));
2745
2907
  }
2746
- function hasConfiguredCdnGithubMirror(projectRoot) {
2747
- const config = readPortalCdnJson(projectRoot);
2748
- const github = config?.github ?? "";
2749
- return Boolean(github) && !PLACEHOLDER_GITHUB.test(github);
2908
+ function patchNavScaffoldFile(targetDir, nav) {
2909
+ const navScaffoldPath = resolve15(targetDir, "src/config/nav-scaffold.ts");
2910
+ if (!existsSync11(navScaffoldPath)) {
2911
+ return;
2912
+ }
2913
+ const content = `/** Set by wp init --nav ${nav}. */
2914
+ export const NAV_SCAFFOLD_MODE = '${nav}' as const
2915
+
2916
+ export type NavScaffoldMode = typeof NAV_SCAFFOLD_MODE
2917
+ `;
2918
+ writeFileSync6(navScaffoldPath, content);
2750
2919
  }
2751
- function githubDefaultForPrompt(projectRoot) {
2752
- const existing = readPortalCdnJson(projectRoot);
2753
- if (existing?.github && !PLACEHOLDER_GITHUB.test(existing.github)) {
2754
- return existing.github;
2920
+ function patchAuthTierFile(targetDir, mode) {
2921
+ const authTierPath = resolve15(targetDir, "src/config/auth-tier.ts");
2922
+ if (!existsSync11(authTierPath)) {
2923
+ return;
2924
+ }
2925
+ const content = `/** Set by wp init --auth ${mode}. */
2926
+ export const AUTH_SCAFFOLD_TIER = '${mode}' as const
2927
+
2928
+ export type AuthScaffoldTier = typeof AUTH_SCAFFOLD_TIER
2929
+ `;
2930
+ writeFileSync6(authTierPath, content);
2931
+ }
2932
+ function patchStarterOnlyBlocks(targetDir, mode) {
2933
+ const hasSso = mode === "sso" || mode === "full";
2934
+ const hasFull = mode === "full";
2935
+ const filesToPatch = [
2936
+ "src/integrations/configure-woodsportal-sdk.ts",
2937
+ "src/features/auth/login-session-bridge.ts",
2938
+ "src/app.tsx",
2939
+ "src/features/auth/components/starter-login.tsx",
2940
+ "src/portal/mount-portal.tsx",
2941
+ "src/features/auth/auth-route-paths.ts"
2942
+ ];
2943
+ for (const relative of filesToPatch) {
2944
+ const filePath = resolve15(targetDir, relative);
2945
+ if (!existsSync11(filePath)) {
2946
+ continue;
2947
+ }
2948
+ let content = readFileSync11(filePath, "utf8");
2949
+ content = stripMarkedBlock(content, "WPHS_AUTH_SSO", hasSso);
2950
+ content = stripMarkedBlock(content, "WPHS_AUTH_FULL", hasFull);
2951
+ if (!hasFull && relative === "src/integrations/configure-woodsportal-sdk.ts") {
2952
+ content = content.replace(
2953
+ /^import \{ isMfaPendingSession \} from '@\/features\/auth\/auth-session'\n/m,
2954
+ ""
2955
+ );
2956
+ }
2957
+ writeFileSync6(filePath, content);
2755
2958
  }
2756
- return defaultPortalCdnJson().github;
2757
2959
  }
2758
- function needsCdnGitHubSetup(projectRoot) {
2759
- return !hasConfiguredCdnGithubMirror(projectRoot) || !hasCdnPublishToken(projectRoot);
2960
+ function stripMarkedBlock(content, marker, keep) {
2961
+ const start = `// ${marker}`;
2962
+ const end = `// /${marker}`;
2963
+ const pattern = new RegExp(
2964
+ `${escapeRegExp(start)}[\\s\\S]*?${escapeRegExp(end)}\\n?`,
2965
+ "g"
2966
+ );
2967
+ if (keep) {
2968
+ return content.replace(new RegExp(`${escapeRegExp(start)}\\n?`, "g"), "").replace(new RegExp(`${escapeRegExp(end)}\\n?`, "g"), "");
2969
+ }
2970
+ return content.replace(pattern, "");
2760
2971
  }
2761
- function applyCdnGitHubSetup(projectRoot, input) {
2762
- const existing = readPortalCdnJson(projectRoot) ?? defaultPortalCdnJson();
2763
- const updated = portalCdnJsonFromGithub(input.github, existing);
2764
- writeFileSync4(resolve16(projectRoot, PORTAL_CDN_JSON), `${JSON.stringify(updated, null, 2)}
2972
+ function escapeRegExp(value) {
2973
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2974
+ }
2975
+ var LOWER_TIER_AUTH_GUARD_FILES = [
2976
+ "src/features/auth/login-session-bridge.ts",
2977
+ "src/integrations/configure-woodsportal-sdk.ts",
2978
+ "src/app.tsx",
2979
+ "src/portal/mount-portal.tsx",
2980
+ "src/features/auth/components/starter-login.tsx"
2981
+ ];
2982
+ function assertLowerTierAuthScaffold(targetDir, mode) {
2983
+ const violations = [];
2984
+ for (const relative of LOWER_TIER_AUTH_GUARD_FILES) {
2985
+ const filePath = resolve15(targetDir, relative);
2986
+ if (!existsSync11(filePath)) {
2987
+ continue;
2988
+ }
2989
+ const content = readFileSync11(filePath, "utf8");
2990
+ if (content.includes("WPHS_AUTH_")) {
2991
+ violations.push(`${relative}: leftover WPHS_AUTH_* strip markers`);
2992
+ }
2993
+ if (relative === "src/features/auth/login-session-bridge.ts") {
2994
+ if (content.includes("mfa-pending-storage")) {
2995
+ violations.push(`${relative}: must not import mfa-pending-storage on --auth ${mode}`);
2996
+ }
2997
+ if (content.includes("navigateToMfaGate")) {
2998
+ violations.push(`${relative}: must not reference navigateToMfaGate on --auth ${mode}`);
2999
+ }
3000
+ }
3001
+ }
3002
+ if (existsSync11(resolve15(targetDir, "src/routes/_auth/two-fa.tsx"))) {
3003
+ violations.push("src/routes/_auth/two-fa.tsx: MFA route must not exist on --auth " + mode);
3004
+ }
3005
+ if (violations.length > 0) {
3006
+ throw new WpHsError(`Auth scaffold tier "${mode}" still contains full-tier artifacts:
3007
+ - ${violations.join("\n- ")}`, {
3008
+ exitCode: ExitCode.INTERNAL,
3009
+ remediation: "Fix strip markers in templates/scaffold/auth/_shared or report a hubspot-kit bug."
3010
+ });
3011
+ }
3012
+ }
3013
+ function assertFullTierAuthScaffold(targetDir) {
3014
+ const required = [
3015
+ "src/features/auth/mfa-pending-storage.ts",
3016
+ "src/routes/_auth/two-fa.tsx"
3017
+ ];
3018
+ const missing = required.filter((relative) => !existsSync11(resolve15(targetDir, relative)));
3019
+ if (missing.length > 0) {
3020
+ throw new WpHsError(`Auth scaffold tier "full" is missing required files:
3021
+ - ${missing.join("\n- ")}`, {
3022
+ exitCode: ExitCode.INTERNAL
3023
+ });
3024
+ }
3025
+ const bridge = readFileSync11(resolve15(targetDir, "src/features/auth/login-session-bridge.ts"), "utf8");
3026
+ if (bridge.includes("WPHS_AUTH_")) {
3027
+ throw new WpHsError('Auth scaffold tier "full" left WPHS_AUTH_* markers in login-session-bridge.ts', {
3028
+ exitCode: ExitCode.INTERNAL
3029
+ });
3030
+ }
3031
+ if (!bridge.includes("navigateToMfaGate") || !bridge.includes("mfa-pending-storage")) {
3032
+ throw new WpHsError('Auth scaffold tier "full" login-session-bridge.ts is missing MFA wiring', {
3033
+ exitCode: ExitCode.INTERNAL
3034
+ });
3035
+ }
3036
+ }
3037
+ function patchPackageJson(targetDir) {
3038
+ const pkgPath = resolve15(targetDir, "package.json");
3039
+ if (!existsSync11(pkgPath)) {
3040
+ return;
3041
+ }
3042
+ const pkg = JSON.parse(readFileSync11(pkgPath, "utf8"));
3043
+ pkg.devDependencies = mergeAuthDevDependencies(pkg.devDependencies);
3044
+ writeFileSync6(pkgPath, `${JSON.stringify(pkg, null, 2)}
2765
3045
  `);
2766
- if (input.token?.trim()) {
2767
- writeEnvLocal(projectRoot, { [PORTAL_CDN_PUBLISH_TOKEN_ENV]: input.token.trim() });
3046
+ }
3047
+ function writeAuthMountDevPreview(targetDir) {
3048
+ writeFileSync6(
3049
+ resolve15(targetDir, "src/portal/mount-dev-preview.tsx"),
3050
+ `import { mountGreenfieldDev } from '../../.wphs/.kit-bootstrap-greenfield.tsx'
3051
+
3052
+ import { App } from '@/app'
3053
+ import { getPortalRouter, mountPortal } from '@/portal/mount-portal'
3054
+
3055
+ /** Local dev: module-config overlay + auth-aware App shell. */
3056
+ export async function mountDevPreview() {
3057
+ await mountGreenfieldDev({
3058
+ beforePreview: () => mountPortal({ skipAppRender: true }),
3059
+ preview: <App router={getPortalRouter()} />,
3060
+ })
3061
+ }
3062
+ `
3063
+ );
3064
+ }
3065
+ function validateAuthRouterCombo(auth, router) {
3066
+ if (auth === "none") {
3067
+ return;
3068
+ }
3069
+ if (router === "none") {
3070
+ throw new Error(
3071
+ "Portal auth scaffold requires client-side routing. Use --router hash or --router browser."
3072
+ );
2768
3073
  }
2769
- ensureEnvFilesGitignore(projectRoot);
2770
3074
  }
2771
- async function promptCdnGitHubSetup(options = {}) {
2772
- if (options.skip) return null;
2773
- const projectRoot = options.projectRoot ?? process.cwd();
2774
- if (!needsCdnGitHubSetup(projectRoot)) {
2775
- return null;
3075
+ function validateNavAuthCombo(auth, nav) {
3076
+ if (!nav || nav === "top") {
3077
+ return;
2776
3078
  }
2777
- let github = options.github?.trim();
2778
- if (!github && hasConfiguredCdnGithubMirror(projectRoot)) {
2779
- github = readPortalCdnJson(projectRoot)?.github;
3079
+ if (auth === "none") {
3080
+ throw new Error(
3081
+ "Portal navigation shell requires auth scaffold. Use --auth starter|sso|full or omit --nav."
3082
+ );
2780
3083
  }
2781
- if (!github) {
2782
- const answer = await p3.text({
2783
- message: "Public GitHub CDN mirror (org/repo)",
2784
- placeholder: options.githubDefault ?? "your-org/your-portal-cdn-mirror",
2785
- initialValue: options.githubDefault,
2786
- validate: (value) => {
2787
- const slug = normalizeGithubSlug(value ?? "");
2788
- return slug ? void 0 : "Enter a public repo as org/repo (jsDelivr serves from it)";
3084
+ }
3085
+ function assertNavScaffold(targetDir, nav) {
3086
+ const mainLayoutPath = resolve15(targetDir, "src/components/Layouts/MainLayout.tsx");
3087
+ if (!existsSync11(mainLayoutPath)) {
3088
+ throw new WpHsError("Auth scaffold is missing MainLayout.tsx after nav layer copy", {
3089
+ exitCode: ExitCode.INTERNAL
3090
+ });
3091
+ }
3092
+ const sidebarPath = resolve15(targetDir, "src/components/layout/portal-sidebar.tsx");
3093
+ if (nav === "sidebar") {
3094
+ if (!existsSync11(sidebarPath)) {
3095
+ throw new WpHsError('Nav scaffold mode "sidebar" is missing portal-sidebar.tsx', {
3096
+ exitCode: ExitCode.INTERNAL
3097
+ });
3098
+ }
3099
+ } else if (existsSync11(sidebarPath)) {
3100
+ throw new WpHsError('Nav scaffold mode "top" must not include portal-sidebar.tsx', {
3101
+ exitCode: ExitCode.INTERNAL
3102
+ });
3103
+ }
3104
+ const navScaffold = readFileSync11(resolve15(targetDir, "src/config/nav-scaffold.ts"), "utf8");
3105
+ if (!navScaffold.includes(`'${nav}'`)) {
3106
+ throw new WpHsError(`nav-scaffold.ts was not patched for mode "${nav}"`, {
3107
+ exitCode: ExitCode.INTERNAL
3108
+ });
3109
+ }
3110
+ }
3111
+ function applyAuthScaffold(targetDir, mode, routerMode, template, nav = "top") {
3112
+ if (mode === "none" || !isModuleTemplate(template)) {
3113
+ return;
3114
+ }
3115
+ validateAuthRouterCombo(mode, routerMode);
3116
+ validateNavAuthCombo(mode, nav);
3117
+ for (const layer of layersForMode(mode)) {
3118
+ copyLayer(targetDir, layer);
3119
+ }
3120
+ patchAuthTierFile(targetDir, mode);
3121
+ patchStarterOnlyBlocks(targetDir, mode);
3122
+ patchNavScaffoldFile(targetDir, nav);
3123
+ copyNavLayer(targetDir, nav);
3124
+ assertNavScaffold(targetDir, nav);
3125
+ if (mode === "starter" || mode === "sso") {
3126
+ assertLowerTierAuthScaffold(targetDir, mode);
3127
+ } else if (mode === "full") {
3128
+ assertFullTierAuthScaffold(targetDir);
3129
+ }
3130
+ patchPackageJson(targetDir);
3131
+ if (routerMode && routerMode !== "none") {
3132
+ writeAuthMountDevPreview(targetDir);
3133
+ }
3134
+ }
3135
+ var AUTH_TIER_ORDER = ["none", "starter", "sso", "full"];
3136
+ function authTierRank(tier) {
3137
+ return AUTH_TIER_ORDER.indexOf(tier);
3138
+ }
3139
+ function readCurrentAuthTier(targetDir) {
3140
+ const authTierPath = resolve15(targetDir, "src/config/auth-tier.ts");
3141
+ if (!existsSync11(authTierPath)) {
3142
+ return "none";
3143
+ }
3144
+ const content = readFileSync11(authTierPath, "utf8");
3145
+ const match = content.match(/AUTH_SCAFFOLD_TIER = '(starter|sso|full)'/);
3146
+ return match?.[1] ?? "none";
3147
+ }
3148
+ function applyAuthTierUpgrade(targetDir, targetTier, routerMode, template, nav = "top") {
3149
+ if (!isModuleTemplate(template)) {
3150
+ throw new WpHsError("Auth upgrade is only supported for module projects.", {
3151
+ exitCode: ExitCode.USER_ERROR
3152
+ });
3153
+ }
3154
+ validateAuthRouterCombo(targetTier, routerMode);
3155
+ const currentTier = readCurrentAuthTier(targetDir);
3156
+ if (currentTier === "none") {
3157
+ applyAuthScaffold(targetDir, targetTier, routerMode, template, nav);
3158
+ return targetTier;
3159
+ }
3160
+ if (authTierRank(targetTier) <= authTierRank(currentTier)) {
3161
+ throw new WpHsError(
3162
+ `Auth upgrade requires a higher tier than "${currentTier}" (requested "${targetTier}").`,
3163
+ {
3164
+ exitCode: ExitCode.USER_ERROR,
3165
+ remediation: "Use starter \u2192 sso \u2192 full. Re-init for downgrades."
3166
+ }
3167
+ );
3168
+ }
3169
+ const targetLayers = layersForMode(targetTier);
3170
+ const currentLayers = layersForMode(currentTier);
3171
+ const layersToCopy = targetLayers.filter((layer) => !currentLayers.includes(layer));
3172
+ for (const layer of layersToCopy) {
3173
+ copyLayer(targetDir, layer);
3174
+ }
3175
+ patchAuthTierFile(targetDir, targetTier);
3176
+ patchStarterOnlyBlocks(targetDir, targetTier);
3177
+ if (targetTier === "starter" || targetTier === "sso") {
3178
+ assertLowerTierAuthScaffold(targetDir, targetTier);
3179
+ } else {
3180
+ assertFullTierAuthScaffold(targetDir);
3181
+ }
3182
+ patchPackageJson(targetDir);
3183
+ if (routerMode && routerMode !== "none") {
3184
+ writeAuthMountDevPreview(targetDir);
3185
+ }
3186
+ return targetTier;
3187
+ }
3188
+ var SIDEBAR_NAV_FILES = [
3189
+ "src/components/layout/portal-sidebar.tsx",
3190
+ "src/components/layout/portal-sidebar-nav.tsx"
3191
+ ];
3192
+ function applyNavScaffoldUpgrade(targetDir, nav) {
3193
+ const authTierPath = resolve15(targetDir, "src/config/auth-tier.ts");
3194
+ if (!existsSync11(authTierPath)) {
3195
+ throw new WpHsError("Nav scaffold requires portal auth (missing src/config/auth-tier.ts).", {
3196
+ exitCode: ExitCode.USER_ERROR,
3197
+ remediation: "Run wp scaffold auth upgrade --tier starter first."
3198
+ });
3199
+ }
3200
+ if (nav === "top") {
3201
+ for (const relative of SIDEBAR_NAV_FILES) {
3202
+ const filePath = resolve15(targetDir, relative);
3203
+ if (existsSync11(filePath)) {
3204
+ rmSync3(filePath);
3205
+ }
3206
+ }
3207
+ }
3208
+ patchNavScaffoldFile(targetDir, nav);
3209
+ copyNavLayer(targetDir, nav);
3210
+ assertNavScaffold(targetDir, nav);
3211
+ }
3212
+ function readProjectAuthTier(targetDir) {
3213
+ return readCurrentAuthTier(targetDir);
3214
+ }
3215
+
3216
+ // src/lib/init-wizard.ts
3217
+ import * as p4 from "@clack/prompts";
3218
+
3219
+ // src/lib/project-name.ts
3220
+ var PROJECT_NAME_PATTERN = /^[a-z][a-z0-9-]*$/;
3221
+ function normalizeProjectName(input) {
3222
+ return input.trim().toLowerCase().replace(/[\s_]+/g, "-").replace(/[^a-z0-9-]/g, "").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
3223
+ }
3224
+ function validateProjectName(name) {
3225
+ if (!name) return "Project name is required";
3226
+ if (!PROJECT_NAME_PATTERN.test(name)) {
3227
+ return "Use lowercase letters, numbers, and hyphens (e.g. my-portal-module)";
3228
+ }
3229
+ if (name.length > 64) return "Project name is too long (max 64 characters)";
3230
+ return void 0;
3231
+ }
3232
+ function moduleOutDirFromSlug(slug) {
3233
+ const pascal = slug.split("-").filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
3234
+ return `dist/${pascal || "HubSpot"}.Module`;
3235
+ }
3236
+
3237
+ // src/lib/init-wizard.ts
3238
+ function cancelIfNeeded(value) {
3239
+ if (p4.isCancel(value)) {
3240
+ throw new WpHsError("Init cancelled", { exitCode: ExitCode.USER_ERROR });
3241
+ }
3242
+ return value;
3243
+ }
3244
+ async function runInitWizard(options) {
3245
+ let template = options.template;
3246
+ if (!template) {
3247
+ const type = cancelIfNeeded(
3248
+ await p4.select({
3249
+ message: "Project type",
3250
+ options: [
3251
+ {
3252
+ value: "module-hybrid-cdn",
3253
+ label: "Module (hybrid CDN)",
3254
+ hint: "jsDelivr + thin module.js"
3255
+ },
3256
+ {
3257
+ value: "module-monolith",
3258
+ label: "Module (monolith)",
3259
+ hint: "Single module.js IIFE"
3260
+ },
3261
+ {
3262
+ value: "theme-starter",
3263
+ label: "Theme starter",
3264
+ hint: "CMS theme templates"
3265
+ }
3266
+ ]
3267
+ })
3268
+ );
3269
+ template = type;
3270
+ }
3271
+ let projectName = options.name ? normalizeProjectName(options.name) : void 0;
3272
+ if (!options.directory && !projectName) {
3273
+ const nameAnswer = cancelIfNeeded(
3274
+ await p4.text({
3275
+ message: "Project name (folder name)",
3276
+ placeholder: "my-portal-module",
3277
+ validate: (value) => validateProjectName(normalizeProjectName(value ?? ""))
3278
+ })
3279
+ );
3280
+ projectName = normalizeProjectName(nameAnswer);
3281
+ }
3282
+ if (!projectName && options.directory) {
3283
+ projectName = normalizeProjectName(options.directory.split("/").pop() ?? "my-portal");
3284
+ }
3285
+ if (!projectName) {
3286
+ throw new WpHsError("Project name is required", {
3287
+ exitCode: ExitCode.USER_ERROR,
3288
+ remediation: "Pass --name or --directory."
3289
+ });
3290
+ }
3291
+ let router = options.router;
3292
+ if (!router && isModuleTemplate(template)) {
3293
+ const routerAnswer = cancelIfNeeded(
3294
+ await p4.select({
3295
+ message: "Client-side routing",
3296
+ initialValue: "hash",
3297
+ options: [
3298
+ {
3299
+ value: "hash",
3300
+ label: "Hash history (recommended)",
3301
+ hint: "Works in HubSpot CMS embeds"
3302
+ },
3303
+ {
3304
+ value: "browser",
3305
+ label: "Browser history",
3306
+ hint: "Clean URLs for local dev only"
3307
+ },
3308
+ {
3309
+ value: "none",
3310
+ label: "No router",
3311
+ hint: "Single-page starter"
3312
+ }
3313
+ ]
3314
+ })
3315
+ );
3316
+ router = routerAnswer;
3317
+ }
3318
+ let auth = options.auth ?? "none";
3319
+ if (!options.auth && isModuleTemplate(template) && router && router !== "none") {
3320
+ const authAnswer = cancelIfNeeded(
3321
+ await p4.select({
3322
+ message: "Portal auth (woodsportal-api + SDK)",
3323
+ initialValue: "none",
3324
+ options: [
3325
+ { value: "none", label: "No portal login", hint: "Routes only" },
3326
+ { value: "starter", label: "Email + password", hint: "Login + sample protected page" },
3327
+ { value: "sso", label: "Email + password + SSO", hint: "OAuth callback route" },
3328
+ {
3329
+ value: "full",
3330
+ label: "Email + password + SSO + MFA",
3331
+ hint: "Recommended for production portals"
3332
+ }
3333
+ ]
3334
+ })
3335
+ );
3336
+ auth = authAnswer;
3337
+ } else if (router === "none" && isModuleTemplate(template)) {
3338
+ p4.log.info("Portal auth requires routing \u2014 auth tier set to none.");
3339
+ auth = "none";
3340
+ }
3341
+ let nav = options.nav ?? "top";
3342
+ if (!options.nav && auth !== "none" && isModuleTemplate(template)) {
3343
+ const navAnswer = cancelIfNeeded(
3344
+ await p4.select({
3345
+ message: "Portal navigation shell",
3346
+ initialValue: "top",
3347
+ options: [
3348
+ {
3349
+ value: "top",
3350
+ label: "Top navigation (recommended)",
3351
+ hint: "Simple header links"
3352
+ },
3353
+ {
3354
+ value: "sidebar",
3355
+ label: "Sidebar navigation",
3356
+ hint: "Collapsible left menu (production-shaped)"
3357
+ }
3358
+ ]
3359
+ })
3360
+ );
3361
+ nav = navAnswer;
3362
+ } else if (auth === "none") {
3363
+ nav = "top";
3364
+ }
3365
+ let cdnSetup = null;
3366
+ if (template === "module-hybrid-cdn") {
3367
+ if (options.cdnGithub) {
3368
+ cdnSetup = { github: options.cdnGithub };
3369
+ } else {
3370
+ p4.log.step("Hybrid CDN \u2014 GitHub mirror (public repo for jsDelivr)");
3371
+ const prompted = await promptCdnGitHubSetup({
3372
+ projectRoot: options.cwd,
3373
+ githubDefault: githubDefaultForPrompt(options.cwd),
3374
+ github: options.cdnGithub
3375
+ });
3376
+ if (prompted) {
3377
+ cdnSetup = prompted;
2789
3378
  }
2790
- });
2791
- if (p3.isCancel(answer)) return null;
2792
- github = normalizeGithubSlug(answer) ?? void 0;
3379
+ }
2793
3380
  }
2794
- if (!github) return null;
2795
- if (hasCdnPublishToken(projectRoot)) {
2796
- return { github };
3381
+ const folder = options.directory ?? `${options.cwd}/${projectName}`;
3382
+ const summary = [
3383
+ `Template: ${template}`,
3384
+ `Folder: ${folder}`,
3385
+ ...isModuleTemplate(template) ? [
3386
+ `Router: ${router ?? "hash"}`,
3387
+ `Auth: ${auth}`,
3388
+ ...auth !== "none" ? [`Nav: ${nav}`] : []
3389
+ ] : [],
3390
+ ...template === "module-hybrid-cdn" ? [`CDN repo: ${cdnSetup?.github ?? "(skipped \u2014 configure later)"}`] : []
3391
+ ].join("\n");
3392
+ p4.note(summary, "Scaffold summary");
3393
+ const confirmed = cancelIfNeeded(
3394
+ await p4.confirm({
3395
+ message: "Proceed with scaffold?",
3396
+ initialValue: true
3397
+ })
3398
+ );
3399
+ if (!confirmed) {
3400
+ throw new WpHsError("Init cancelled", { exitCode: ExitCode.USER_ERROR });
2797
3401
  }
2798
- const tokenAnswer = await p3.password({
2799
- message: "GitHub token (repo push on CDN mirror)",
2800
- validate: (value) => {
2801
- if (!value?.trim()) return "Required for wp publish \u2014 create a fine-grained PAT with Contents: Read and write";
2802
- return void 0;
2803
- }
2804
- });
2805
- if (p3.isCancel(tokenAnswer)) return null;
2806
- return { github, token: tokenAnswer.trim() };
2807
- }
2808
- function formatCdnSetupNote(_projectRoot) {
2809
- return `CDN config written \u2014 edit ${PORTAL_CDN_JSON} if needed; token saved to ${ENV_LOCAL_FILE} (gitignored)`;
3402
+ return {
3403
+ template,
3404
+ projectName,
3405
+ router,
3406
+ auth,
3407
+ nav,
3408
+ cdnSetup,
3409
+ confirmed: true
3410
+ };
2810
3411
  }
2811
3412
 
2812
3413
  // src/lib/project-setup.ts
2813
- import { copyFileSync, existsSync as existsSync12, writeFileSync as writeFileSync5 } from "fs";
2814
- import { basename, resolve as resolve17 } from "path";
3414
+ import { copyFileSync, existsSync as existsSync12, writeFileSync as writeFileSync7 } from "fs";
3415
+ import { basename, resolve as resolve16 } from "path";
2815
3416
  function envFileForTier2(tier) {
2816
3417
  return tier === "dev" ? ".env.dev" : tier === "stg" ? ".env.stg" : ".env.prod";
2817
3418
  }
@@ -2826,7 +3427,7 @@ function firstExisting(paths) {
2826
3427
  return null;
2827
3428
  }
2828
3429
  function copyMissingFile(projectRoot, targetName, sourceCandidates, dryRun) {
2829
- const target = resolve17(projectRoot, targetName);
3430
+ const target = resolve16(projectRoot, targetName);
2830
3431
  if (existsSync12(target)) {
2831
3432
  return { action: "skipped", path: targetName, detail: "already present" };
2832
3433
  }
@@ -2849,12 +3450,12 @@ function copyMissingFile(projectRoot, targetName, sourceCandidates, dryRun) {
2849
3450
  };
2850
3451
  }
2851
3452
  function writeMissingFile(projectRoot, targetName, content, dryRun) {
2852
- const target = resolve17(projectRoot, targetName);
3453
+ const target = resolve16(projectRoot, targetName);
2853
3454
  if (existsSync12(target)) {
2854
3455
  return { action: "skipped", path: targetName, detail: "already present" };
2855
3456
  }
2856
3457
  if (!dryRun) {
2857
- writeFileSync5(target, content);
3458
+ writeFileSync7(target, content);
2858
3459
  }
2859
3460
  return {
2860
3461
  action: "copied",
@@ -2880,15 +3481,15 @@ function applyProjectSetup(ctx, options = {}) {
2880
3481
  const templateDir = kitTemplateDir(config);
2881
3482
  results.push(
2882
3483
  copyMissingFile(projectRoot, HUBSPOT_CONFIG_FILE, [
2883
- resolve17(projectRoot, HUBSPOT_CONFIG_EXAMPLE),
2884
- resolve17(templateDir, HUBSPOT_CONFIG_EXAMPLE),
3484
+ resolve16(projectRoot, HUBSPOT_CONFIG_EXAMPLE),
3485
+ resolve16(templateDir, HUBSPOT_CONFIG_EXAMPLE),
2885
3486
  kitPath("templates", HUBSPOT_CONFIG_EXAMPLE)
2886
3487
  ], dryRun)
2887
3488
  );
2888
3489
  results.push(
2889
3490
  copyMissingFile(projectRoot, ".env.local", [
2890
- resolve17(projectRoot, ".env.local.example"),
2891
- resolve17(sharedDir, ".env.local.example")
3491
+ resolve16(projectRoot, ".env.local.example"),
3492
+ resolve16(sharedDir, ".env.local.example")
2892
3493
  ], dryRun)
2893
3494
  );
2894
3495
  for (const tier of tiersToSetup(config, options.tier)) {
@@ -2896,8 +3497,8 @@ function applyProjectSetup(ctx, options = {}) {
2896
3497
  const envExample = `${envFile}.example`;
2897
3498
  results.push(
2898
3499
  copyMissingFile(projectRoot, envFile, [
2899
- resolve17(projectRoot, envExample),
2900
- resolve17(sharedDir, envExample)
3500
+ resolve16(projectRoot, envExample),
3501
+ resolve16(sharedDir, envExample)
2901
3502
  ], dryRun)
2902
3503
  );
2903
3504
  }
@@ -2905,8 +3506,8 @@ function applyProjectSetup(ctx, options = {}) {
2905
3506
  const mirror = config.cdn.mirror;
2906
3507
  results.push(
2907
3508
  copyMissingFile(projectRoot, mirror, [
2908
- resolve17(projectRoot, `${mirror}.example`),
2909
- resolve17(sharedDir, "portal.cdn.json.example")
3509
+ resolve16(projectRoot, `${mirror}.example`),
3510
+ resolve16(sharedDir, "portal.cdn.json.example")
2910
3511
  ], dryRun)
2911
3512
  );
2912
3513
  const buildIdFile = config.cdn.buildIdFile;
@@ -2921,14 +3522,14 @@ function applyProjectSetup(ctx, options = {}) {
2921
3522
  }
2922
3523
  for (const file of templateViteFiles(config)) {
2923
3524
  results.push(
2924
- copyMissingFile(projectRoot, file, [resolve17(templateDir, file)], dryRun)
3525
+ copyMissingFile(projectRoot, file, [resolve16(templateDir, file)], dryRun)
2925
3526
  );
2926
3527
  }
2927
3528
  if (isModuleProject(config) && !isThemeProject(config)) {
2928
3529
  const adapter = config.dev.adapter;
2929
- const adapterPath = resolve17(projectRoot, adapter);
3530
+ const adapterPath = resolve16(projectRoot, adapter);
2930
3531
  if (!existsSync12(adapterPath)) {
2931
- const templateAdapter = resolve17(templateDir, "wphs.adapter.ts");
3532
+ const templateAdapter = resolve16(templateDir, "wphs.adapter.ts");
2932
3533
  if (existsSync12(templateAdapter)) {
2933
3534
  results.push(copyMissingFile(projectRoot, adapter, [templateAdapter], dryRun));
2934
3535
  } else {
@@ -2954,19 +3555,19 @@ function copySharedSetupExamples(targetDir) {
2954
3555
  ".env.prod.example",
2955
3556
  "portal.cdn.json.example"
2956
3557
  ]) {
2957
- const src = resolve17(sharedDir, name);
2958
- const dest = resolve17(targetDir, name);
3558
+ const src = resolve16(sharedDir, name);
3559
+ const dest = resolve16(targetDir, name);
2959
3560
  if (existsSync12(src) && !existsSync12(dest)) {
2960
3561
  copyFileSync(src, dest);
2961
3562
  }
2962
3563
  }
2963
3564
  const hubspotExample = firstExisting([
2964
- resolve17(sharedDir, HUBSPOT_CONFIG_EXAMPLE),
3565
+ resolve16(sharedDir, HUBSPOT_CONFIG_EXAMPLE),
2965
3566
  kitPath("templates", HUBSPOT_CONFIG_EXAMPLE)
2966
3567
  ]);
2967
3568
  if (!hubspotExample) return;
2968
- const destHubspot = resolve17(targetDir, HUBSPOT_CONFIG_EXAMPLE);
2969
- if (!existsSync12(destHubspot) && !existsSync12(resolve17(targetDir, HUBSPOT_CONFIG_FILE))) {
3569
+ const destHubspot = resolve16(targetDir, HUBSPOT_CONFIG_EXAMPLE);
3570
+ if (!existsSync12(destHubspot) && !existsSync12(resolve16(targetDir, HUBSPOT_CONFIG_FILE))) {
2970
3571
  copyFileSync(hubspotExample, destHubspot);
2971
3572
  }
2972
3573
  }
@@ -3014,26 +3615,8 @@ var TEMPLATE_SCRIPTS = {
3014
3615
  }
3015
3616
  };
3016
3617
 
3017
- // src/lib/project-name.ts
3018
- var PROJECT_NAME_PATTERN = /^[a-z][a-z0-9-]*$/;
3019
- function normalizeProjectName(input) {
3020
- return input.trim().toLowerCase().replace(/[\s_]+/g, "-").replace(/[^a-z0-9-]/g, "").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
3021
- }
3022
- function validateProjectName(name) {
3023
- if (!name) return "Project name is required";
3024
- if (!PROJECT_NAME_PATTERN.test(name)) {
3025
- return "Use lowercase letters, numbers, and hyphens (e.g. my-portal-module)";
3026
- }
3027
- if (name.length > 64) return "Project name is too long (max 64 characters)";
3028
- return void 0;
3029
- }
3030
- function moduleOutDirFromSlug(slug) {
3031
- const pascal = slug.split("-").filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
3032
- return `dist/${pascal || "HubSpot"}.Module`;
3033
- }
3034
-
3035
3618
  // src/cli/commands/init.ts
3036
- function buildConfigForProject(template, projectName, router) {
3619
+ function buildConfigForProject(template, projectName, router, auth = "none", nav = "top") {
3037
3620
  const base = { ...DEFAULT_WOODSPORTAL_HS, projectType: template };
3038
3621
  if (template === "theme-starter") {
3039
3622
  return {
@@ -3058,8 +3641,12 @@ function buildConfigForProject(template, projectName, router) {
3058
3641
  mode: template === "module-monolith" ? "none" : base.cdn.mode
3059
3642
  }
3060
3643
  };
3061
- if (isModuleTemplate(template) && router) {
3062
- config.scaffold = { router };
3644
+ if (isModuleTemplate(template)) {
3645
+ config.scaffold = {
3646
+ router: router ?? "hash",
3647
+ auth: auth ?? "none",
3648
+ ...auth !== "none" ? { nav: nav ?? "top" } : {}
3649
+ };
3063
3650
  }
3064
3651
  return config;
3065
3652
  }
@@ -3075,7 +3662,7 @@ function assertTargetDirIsScaffoldable(targetDir) {
3075
3662
  }
3076
3663
  function resolveInitTargetDir(options, interactiveName) {
3077
3664
  if (options.directory) {
3078
- return resolve18(options.directory);
3665
+ return resolve17(options.directory);
3079
3666
  }
3080
3667
  const parentDir = process.cwd();
3081
3668
  const slug = options.name ?? interactiveName;
@@ -3089,67 +3676,31 @@ function resolveInitTargetDir(options, interactiveName) {
3089
3676
  if (error) {
3090
3677
  throw new WpHsError(error, { exitCode: ExitCode.USER_ERROR });
3091
3678
  }
3092
- return resolve18(parentDir, slug);
3679
+ return resolve17(parentDir, slug);
3093
3680
  }
3094
3681
  async function runInit(ctx, options) {
3095
3682
  const nonInteractive = ctx.global.ci || ctx.global.yes || ctx.global.json;
3096
3683
  let template = options.template;
3097
3684
  let projectName = options.name ? normalizeProjectName(options.name) : void 0;
3098
3685
  let router = options.router;
3686
+ let auth = options.auth ?? "none";
3687
+ let nav = options.nav ?? "top";
3688
+ let cdnGithub = options.cdnGithub;
3689
+ let wizardCdnSetup = null;
3099
3690
  if (!nonInteractive) {
3100
- if (!template) {
3101
- const type = await p4.select({
3102
- message: "Project type",
3103
- options: [
3104
- {
3105
- value: "module-hybrid-cdn",
3106
- label: "Module (hybrid CDN)",
3107
- hint: "jsDelivr + thin module.js"
3108
- },
3109
- { value: "module-monolith", label: "Module (monolith)", hint: "Single module.js IIFE" },
3110
- { value: "theme-starter", label: "Theme starter", hint: "CMS theme templates" }
3111
- ]
3112
- });
3113
- if (p4.isCancel(type)) throw new WpHsError("Init cancelled", { exitCode: ExitCode.USER_ERROR });
3114
- template = type;
3115
- }
3116
- if (!options.directory && !projectName) {
3117
- const nameAnswer = await p4.text({
3118
- message: "Project name (folder name)",
3119
- placeholder: "my-portal-module",
3120
- validate: (value) => validateProjectName(normalizeProjectName(value ?? ""))
3121
- });
3122
- if (p4.isCancel(nameAnswer)) {
3123
- throw new WpHsError("Init cancelled", { exitCode: ExitCode.USER_ERROR });
3124
- }
3125
- projectName = normalizeProjectName(nameAnswer);
3126
- }
3127
- if (!router && template && isModuleTemplate(template)) {
3128
- const routerAnswer = await p4.select({
3129
- message: "Client-side routing",
3130
- initialValue: "hash",
3131
- options: [
3132
- {
3133
- value: "hash",
3134
- label: "Hash history (recommended)",
3135
- hint: "Same as woodsportal-client-frontend \u2014 works in HubSpot CMS embeds"
3136
- },
3137
- {
3138
- value: "browser",
3139
- label: "Browser history",
3140
- hint: "Clean /guide URLs for local dev (not for HubSpot iframe embeds)"
3141
- },
3142
- {
3143
- value: "none",
3144
- label: "No router",
3145
- hint: "Single-page starter \u2014 add TanStack Router later"
3146
- }
3147
- ]
3148
- });
3149
- if (p4.isCancel(routerAnswer)) {
3150
- throw new WpHsError("Init cancelled", { exitCode: ExitCode.USER_ERROR });
3151
- }
3152
- router = routerAnswer;
3691
+ const wizard = await runInitWizard({
3692
+ ...options,
3693
+ cwd: process.cwd(),
3694
+ auth: options.auth
3695
+ });
3696
+ template = wizard.template;
3697
+ projectName = wizard.projectName;
3698
+ router = wizard.router;
3699
+ auth = wizard.auth;
3700
+ nav = wizard.nav ?? "top";
3701
+ wizardCdnSetup = wizard.cdnSetup;
3702
+ if (wizard.cdnSetup?.github) {
3703
+ cdnGithub = wizard.cdnSetup.github;
3153
3704
  }
3154
3705
  }
3155
3706
  if (!template) {
@@ -3158,6 +3709,19 @@ async function runInit(ctx, options) {
3158
3709
  if (!router && isModuleTemplate(template)) {
3159
3710
  router = "hash";
3160
3711
  }
3712
+ if (!options.auth && nonInteractive && isModuleTemplate(template)) {
3713
+ auth = "none";
3714
+ }
3715
+ try {
3716
+ validateAuthRouterCombo(auth, router);
3717
+ validateNavAuthCombo(auth, nav);
3718
+ } catch (error) {
3719
+ const message = error instanceof Error ? error.message : String(error);
3720
+ throw new WpHsError(message, {
3721
+ exitCode: ExitCode.USER_ERROR,
3722
+ remediation: "Use --router hash or --router browser with --auth starter|sso|full. Nav shell (--nav sidebar) requires auth."
3723
+ });
3724
+ }
3161
3725
  const targetDir = resolveInitTargetDir(options, projectName);
3162
3726
  const slug = projectName ?? basename2(targetDir);
3163
3727
  assertTargetDirIsScaffoldable(targetDir);
@@ -3166,36 +3730,37 @@ async function runInit(ctx, options) {
3166
3730
  throw new WpHsError(`Template not found: ${template}`, { exitCode: ExitCode.USER_ERROR });
3167
3731
  }
3168
3732
  if (ctx.global.dryRun) {
3169
- ctx.logger.info(`[dry-run] Would scaffold ${template} in ${targetDir}`);
3733
+ ctx.logger.info(
3734
+ `[dry-run] Would scaffold ${template} in ${targetDir} (router=${router ?? "n/a"}, auth=${auth}, nav=${auth !== "none" ? nav : "n/a"})`
3735
+ );
3170
3736
  return ExitCode.SUCCESS;
3171
3737
  }
3172
- mkdirSync4(targetDir, { recursive: true });
3173
- cpSync2(templateDir, targetDir, { recursive: true });
3738
+ mkdirSync5(targetDir, { recursive: true });
3739
+ cpSync3(templateDir, targetDir, { recursive: true });
3174
3740
  copySharedSetupExamples(targetDir);
3175
- const config = buildConfigForProject(template, slug, router);
3176
- writeFileSync6(resolve18(targetDir, WOODSCLI_CONFIG_FILE), `${JSON.stringify(config, null, 2)}
3741
+ const config = buildConfigForProject(template, slug, router, auth, nav);
3742
+ writeFileSync8(resolve17(targetDir, WOODSCLI_CONFIG_FILE), `${JSON.stringify(config, null, 2)}
3177
3743
  `);
3178
- const pkgPath = resolve18(targetDir, "package.json");
3744
+ const pkgPath = resolve17(targetDir, "package.json");
3179
3745
  if (existsSync13(pkgPath)) {
3180
- const pkg = JSON.parse(readFileSync11(pkgPath, "utf8"));
3746
+ const pkg = JSON.parse(readFileSync12(pkgPath, "utf8"));
3181
3747
  pkg.name = slug;
3182
3748
  pkg.scripts = { ...pkg.scripts, ...TEMPLATE_SCRIPTS[template] };
3183
3749
  pkg.devDependencies = {
3184
3750
  ...pkg.devDependencies,
3185
3751
  ...templateDevDependencies()
3186
3752
  };
3187
- writeFileSync6(pkgPath, `${JSON.stringify(pkg, null, 2)}
3753
+ writeFileSync8(pkgPath, `${JSON.stringify(pkg, null, 2)}
3188
3754
  `);
3189
3755
  }
3190
3756
  if (router && isModuleTemplate(template)) {
3191
3757
  applyRouterScaffold(targetDir, router, template);
3192
3758
  }
3193
- runNodeScript(kitPath("scripts/ensure-wphs-workspace.mjs"), [], {
3194
- cwd: targetDir,
3195
- logger: ctx.logger,
3196
- dryRun: false
3197
- });
3198
- const gitignorePath = resolve18(targetDir, ".gitignore");
3759
+ if (isModuleTemplate(template)) {
3760
+ applyAuthScaffold(targetDir, auth, router, template, nav);
3761
+ }
3762
+ await ensureProjectWphsWorkspace(targetDir);
3763
+ const gitignorePath = resolve17(targetDir, ".gitignore");
3199
3764
  const gitignoreExtras = [
3200
3765
  "hubspot.config.yml",
3201
3766
  ".wphs/config/module-config.local.json",
@@ -3208,40 +3773,27 @@ async function runInit(ctx, options) {
3208
3773
  ".wphs/.kit-bootstrap.tsx",
3209
3774
  ".wphs/.kit-bootstrap-greenfield.tsx"
3210
3775
  ];
3211
- const existing = existsSync13(gitignorePath) ? readFileSync11(gitignorePath, "utf8") : "";
3776
+ const existing = existsSync13(gitignorePath) ? readFileSync12(gitignorePath, "utf8") : "";
3212
3777
  const toAppend = gitignoreExtras.filter((line) => !existing.includes(line)).join("\n");
3213
3778
  if (toAppend) {
3214
- writeFileSync6(gitignorePath, `${existing.trimEnd()}
3779
+ writeFileSync8(gitignorePath, `${existing.trimEnd()}
3215
3780
  ${toAppend}
3216
3781
  `);
3217
3782
  }
3218
- if (template === "module-hybrid-cdn" && !ctx.global.dryRun && needsCdnGitHubSetup(targetDir)) {
3219
- const githubDefault = githubDefaultForPrompt(targetDir);
3220
- let cdnSetup = null;
3221
- if (nonInteractive) {
3222
- if (options.cdnGithub) {
3223
- cdnSetup = { github: options.cdnGithub };
3224
- }
3225
- } else {
3226
- p4.log.step("Hybrid CDN \u2014 GitHub mirror (public repo for jsDelivr)");
3227
- cdnSetup = await promptCdnGitHubSetup({
3228
- projectRoot: targetDir,
3229
- githubDefault,
3230
- github: options.cdnGithub
3231
- });
3232
- }
3233
- if (cdnSetup) {
3234
- applyCdnGitHubSetup(targetDir, cdnSetup);
3783
+ if (template === "module-hybrid-cdn" && !ctx.global.dryRun) {
3784
+ const setup = wizardCdnSetup ?? (cdnGithub ? { github: cdnGithub } : null);
3785
+ if (setup) {
3786
+ applyCdnGitHubSetup(targetDir, setup);
3235
3787
  if (!nonInteractive) {
3236
- p4.log.success(formatCdnSetupNote(targetDir));
3788
+ p5.log.success(formatCdnSetupNote(targetDir));
3237
3789
  } else {
3238
3790
  ctx.logger.success(formatCdnSetupNote(targetDir));
3239
3791
  }
3240
3792
  }
3241
3793
  }
3242
3794
  if (!nonInteractive) {
3243
- p4.log.success(`Scaffolded ${template} in ${targetDir}`);
3244
- p4.note(`cd ${slug} && yarn install && wp setup && wp doctor && wp dev`, "Getting started");
3795
+ p5.log.success(`Scaffolded ${template} in ${targetDir}`);
3796
+ p5.note(`cd ${slug} && yarn install && wp setup && wp doctor && wp dev`, "Getting started");
3245
3797
  } else {
3246
3798
  ctx.logger.success(`Scaffolded ${template} in ${targetDir}`);
3247
3799
  }
@@ -3249,15 +3801,15 @@ ${toAppend}
3249
3801
  }
3250
3802
 
3251
3803
  // src/cli/commands/migrate.ts
3252
- import { cpSync as cpSync3, existsSync as existsSync14, mkdirSync as mkdirSync5, readFileSync as readFileSync12, readdirSync as readdirSync3, renameSync, rmSync as rmSync3, writeFileSync as writeFileSync7 } from "fs";
3253
- import { join as join3, resolve as resolve19 } from "path";
3804
+ import { cpSync as cpSync4, existsSync as existsSync14, mkdirSync as mkdirSync6, readFileSync as readFileSync13, readdirSync as readdirSync3, renameSync, rmSync as rmSync4, writeFileSync as writeFileSync9 } from "fs";
3805
+ import { join as join4, resolve as resolve18 } from "path";
3254
3806
  var LEGACY_DIRS = [".dev", ".woodsportal"];
3255
3807
  var LEGACY_BASELINE = ".wp-hs-baseline";
3256
3808
  var LEGACY_BUILD_ID_FILE = ".portal-build-id";
3257
3809
  var MODULE_BUILD_ID_FILE = ".module-build-id";
3258
3810
  function readFirstNonCommentLine(filePath) {
3259
3811
  if (!existsSync14(filePath)) return void 0;
3260
- const line = readFileSync12(filePath, "utf8").split("\n").map((l) => l.trim()).find((l) => l.length > 0 && !l.startsWith("#"));
3812
+ const line = readFileSync13(filePath, "utf8").split("\n").map((l) => l.trim()).find((l) => l.length > 0 && !l.startsWith("#"));
3261
3813
  return line || void 0;
3262
3814
  }
3263
3815
  function stripToBaseline(raw) {
@@ -3271,71 +3823,71 @@ function stripToBaseline(raw) {
3271
3823
  }
3272
3824
  async function runMigrate(ctx, _options = {}) {
3273
3825
  const root = ctx.projectRoot;
3274
- const wphsRoot = resolve19(root, WPHS_WORKSPACE_DIR);
3826
+ const wphsRoot = resolve18(root, WPHS_WORKSPACE_DIR);
3275
3827
  const actions = [];
3276
3828
  for (const legacy of LEGACY_DIRS) {
3277
- const legacyPath = resolve19(root, legacy);
3829
+ const legacyPath = resolve18(root, legacy);
3278
3830
  if (!existsSync14(legacyPath)) continue;
3279
3831
  actions.push(`Move ${legacy}/ \u2192 .wphs/`);
3280
3832
  if (ctx.global.dryRun) continue;
3281
- mkdirSync5(join3(wphsRoot, "config"), { recursive: true });
3282
- mkdirSync5(join3(wphsRoot, "assets", "branding"), { recursive: true });
3283
- const localConfig = join3(legacyPath, "module-config.local.json");
3284
- const exampleConfig = join3(legacyPath, "module-config.example.json");
3833
+ mkdirSync6(join4(wphsRoot, "config"), { recursive: true });
3834
+ mkdirSync6(join4(wphsRoot, "assets", "branding"), { recursive: true });
3835
+ const localConfig = join4(legacyPath, "module-config.local.json");
3836
+ const exampleConfig = join4(legacyPath, "module-config.example.json");
3285
3837
  if (existsSync14(localConfig)) {
3286
- cpSync3(localConfig, join3(wphsRoot, "config", "module-config.local.json"));
3838
+ cpSync4(localConfig, join4(wphsRoot, "config", "module-config.local.json"));
3287
3839
  }
3288
3840
  if (existsSync14(exampleConfig)) {
3289
- cpSync3(exampleConfig, join3(wphsRoot, "config", "module-config.example.json"));
3841
+ cpSync4(exampleConfig, join4(wphsRoot, "config", "module-config.example.json"));
3290
3842
  }
3291
- const branding = join3(legacyPath, "branding-assets");
3843
+ const branding = join4(legacyPath, "branding-assets");
3292
3844
  if (existsSync14(branding)) {
3293
3845
  for (const file of readdirSync3(branding)) {
3294
- cpSync3(join3(branding, file), join3(wphsRoot, "assets", "branding", file));
3846
+ cpSync4(join4(branding, file), join4(wphsRoot, "assets", "branding", file));
3295
3847
  }
3296
3848
  }
3297
- rmSync3(legacyPath, { recursive: true, force: true });
3849
+ rmSync4(legacyPath, { recursive: true, force: true });
3298
3850
  }
3299
- const legacyBaseline = resolve19(root, LEGACY_BASELINE);
3300
- const targetBaseline = join3(wphsRoot, "audit", "baseline");
3851
+ const legacyBaseline = resolve18(root, LEGACY_BASELINE);
3852
+ const targetBaseline = join4(wphsRoot, "audit", "baseline");
3301
3853
  if (existsSync14(legacyBaseline)) {
3302
3854
  actions.push(`Move ${LEGACY_BASELINE}/ \u2192 .wphs/audit/baseline/`);
3303
3855
  if (!ctx.global.dryRun) {
3304
- mkdirSync5(targetBaseline, { recursive: true });
3305
- cpSync3(legacyBaseline, targetBaseline, { recursive: true });
3306
- rmSync3(legacyBaseline, { recursive: true, force: true });
3856
+ mkdirSync6(targetBaseline, { recursive: true });
3857
+ cpSync4(legacyBaseline, targetBaseline, { recursive: true });
3858
+ rmSync4(legacyBaseline, { recursive: true, force: true });
3307
3859
  }
3308
3860
  }
3309
- const legacyConfigPath2 = resolve19(root, LEGACY_WOODSPORTAL_HS_FILE);
3310
- const woodscliPath = resolve19(root, WOODSCLI_CONFIG_FILE);
3311
- if (existsSync14(legacyConfigPath2) && !existsSync14(woodscliPath)) {
3861
+ const legacyConfigPath = resolve18(root, LEGACY_WOODSPORTAL_HS_FILE);
3862
+ const woodscliPath = resolve18(root, WOODSCLI_CONFIG_FILE);
3863
+ if (existsSync14(legacyConfigPath) && !existsSync14(woodscliPath)) {
3312
3864
  actions.push(`Rename ${LEGACY_WOODSPORTAL_HS_FILE} \u2192 ${WOODSCLI_CONFIG_FILE}`);
3313
3865
  if (!ctx.global.dryRun) {
3314
- renameSync(legacyConfigPath2, woodscliPath);
3866
+ renameSync(legacyConfigPath, woodscliPath);
3315
3867
  }
3316
- } else if (existsSync14(legacyConfigPath2) && existsSync14(woodscliPath)) {
3868
+ } else if (existsSync14(legacyConfigPath) && existsSync14(woodscliPath)) {
3317
3869
  actions.push(`Remove duplicate ${LEGACY_WOODSPORTAL_HS_FILE}`);
3318
3870
  if (!ctx.global.dryRun) {
3319
- rmSync3(legacyConfigPath2, { force: true });
3871
+ rmSync4(legacyConfigPath, { force: true });
3320
3872
  }
3321
3873
  }
3322
- const legacyBuildIdPath = resolve19(root, LEGACY_BUILD_ID_FILE);
3323
- const moduleBuildIdPath = resolve19(root, MODULE_BUILD_ID_FILE);
3874
+ const legacyBuildIdPath = resolve18(root, LEGACY_BUILD_ID_FILE);
3875
+ const moduleBuildIdPath = resolve18(root, MODULE_BUILD_ID_FILE);
3324
3876
  if (existsSync14(legacyBuildIdPath) && !existsSync14(moduleBuildIdPath)) {
3325
3877
  const baseline = stripToBaseline(readFirstNonCommentLine(legacyBuildIdPath)) ?? "v0.0.1-dev";
3326
3878
  actions.push(`Migrate ${LEGACY_BUILD_ID_FILE} \u2192 ${MODULE_BUILD_ID_FILE} (baseline ${baseline})`);
3327
3879
  if (!ctx.global.dryRun) {
3328
- writeFileSync7(
3880
+ writeFileSync9(
3329
3881
  moduleBuildIdPath,
3330
3882
  `# Channel baseline for local CDN deploys (wp deploy auto-bumps per developer).
3331
3883
  ${baseline}
3332
3884
  `
3333
3885
  );
3334
- rmSync3(legacyBuildIdPath, { force: true });
3886
+ rmSync4(legacyBuildIdPath, { force: true });
3335
3887
  }
3336
3888
  }
3337
3889
  if (existsSync14(woodscliPath)) {
3338
- const raw = JSON.parse(readFileSync12(woodscliPath, "utf8"));
3890
+ const raw = JSON.parse(readFileSync13(woodscliPath, "utf8"));
3339
3891
  let configChanged = false;
3340
3892
  if (raw?.dev?.moduleConfigDir && raw.dev.moduleConfigDir !== WPHS_WORKSPACE_DIR) {
3341
3893
  actions.push(`Update dev.moduleConfigDir \u2192 .wphs in project config`);
@@ -3352,7 +3904,7 @@ ${baseline}
3352
3904
  }
3353
3905
  }
3354
3906
  if (configChanged && !ctx.global.dryRun) {
3355
- writeFileSync7(woodscliPath, `${JSON.stringify(raw, null, 2)}
3907
+ writeFileSync9(woodscliPath, `${JSON.stringify(raw, null, 2)}
3356
3908
  `);
3357
3909
  }
3358
3910
  }
@@ -3370,20 +3922,16 @@ ${baseline}
3370
3922
  remediation: "Run: wp migrate --yes"
3371
3923
  });
3372
3924
  }
3373
- runNodeScript(kitPath("scripts/ensure-wphs-workspace.mjs"), [], {
3374
- cwd: root,
3375
- logger: ctx.logger,
3376
- dryRun: false
3377
- });
3925
+ await ensureProjectWphsWorkspace(root);
3378
3926
  for (const action of actions) ctx.logger.success(action);
3379
3927
  return ExitCode.SUCCESS;
3380
3928
  }
3381
3929
 
3382
3930
  // src/cli/commands/examples.ts
3383
- import { readFileSync as readFileSync13 } from "fs";
3931
+ import { readFileSync as readFileSync14 } from "fs";
3384
3932
  function loadManifest() {
3385
3933
  const manifestPath = kitPath("examples", "manifest.json");
3386
- const raw = JSON.parse(readFileSync13(manifestPath, "utf8"));
3934
+ const raw = JSON.parse(readFileSync14(manifestPath, "utf8"));
3387
3935
  return raw.scenarios;
3388
3936
  }
3389
3937
  async function runExamplesList(ctx, options = {}) {
@@ -3403,8 +3951,8 @@ async function runExamplesSmoke(ctx, options = {}) {
3403
3951
  const tier = options.tier ?? "static";
3404
3952
  const args = ["--tier", tier];
3405
3953
  if (options.scenario) args.push("--scenario", options.scenario);
3406
- const { runNodeScript: runNodeScript2 } = await import("./exec-VY6BGWF3.js");
3407
- runNodeScript2(kitPath("examples", "smoke", "run-matrix.mjs"), args, {
3954
+ const { runNodeScript } = await import("./exec-VY6BGWF3.js");
3955
+ runNodeScript(kitPath("examples", "smoke", "run-matrix.mjs"), args, {
3408
3956
  cwd: ctx.projectRoot,
3409
3957
  logger: ctx.logger,
3410
3958
  dryRun: ctx.global.dryRun
@@ -3416,13 +3964,13 @@ async function runExamplesSmoke(ctx, options = {}) {
3416
3964
  }
3417
3965
 
3418
3966
  // src/cli/commands/fields.ts
3419
- import { cpSync as cpSync4, existsSync as existsSync15, readFileSync as readFileSync14, writeFileSync as writeFileSync8 } from "fs";
3420
- import { resolve as resolve20 } from "path";
3967
+ import { cpSync as cpSync5, existsSync as existsSync15, readFileSync as readFileSync15, writeFileSync as writeFileSync10 } from "fs";
3968
+ import { resolve as resolve19 } from "path";
3421
3969
  async function runFieldsSync(ctx, options = {}) {
3422
3970
  const direction = options.direction ?? "pull";
3423
- const fieldsPath = resolve20(ctx.projectRoot, ctx.config.dev.fieldsJson);
3424
- const cacheDir = resolve20(ctx.projectRoot, ctx.config.dev.moduleConfigDir, "cache", "fields-sync");
3425
- const cachePath = resolve20(cacheDir, "fields.json");
3971
+ const fieldsPath = resolve19(ctx.projectRoot, ctx.config.dev.fieldsJson);
3972
+ const cacheDir = resolve19(ctx.projectRoot, ctx.config.dev.moduleConfigDir, "cache", "fields-sync");
3973
+ const cachePath = resolve19(cacheDir, "fields.json");
3426
3974
  if (direction === "pull") {
3427
3975
  if (!existsSync15(fieldsPath)) {
3428
3976
  throw new WpHsError(`Missing ${ctx.config.dev.fieldsJson}`, {
@@ -3434,9 +3982,9 @@ async function runFieldsSync(ctx, options = {}) {
3434
3982
  ctx.logger.info(`[dry-run] Would cache ${fieldsPath} \u2192 ${cachePath}`);
3435
3983
  return ExitCode.SUCCESS;
3436
3984
  }
3437
- const { mkdirSync: mkdirSync6 } = await import("fs");
3438
- mkdirSync6(cacheDir, { recursive: true });
3439
- cpSync4(fieldsPath, cachePath);
3985
+ const { mkdirSync: mkdirSync7 } = await import("fs");
3986
+ mkdirSync7(cacheDir, { recursive: true });
3987
+ cpSync5(fieldsPath, cachePath);
3440
3988
  ctx.logger.success(`Cached fields \u2192 ${cachePath}`);
3441
3989
  return ExitCode.SUCCESS;
3442
3990
  }
@@ -3449,14 +3997,14 @@ async function runFieldsSync(ctx, options = {}) {
3449
3997
  ctx.logger.info(`[dry-run] Would write ${cachePath} \u2192 ${fieldsPath}`);
3450
3998
  return ExitCode.SUCCESS;
3451
3999
  }
3452
- writeFileSync8(fieldsPath, readFileSync14(cachePath, "utf8"));
4000
+ writeFileSync10(fieldsPath, readFileSync15(cachePath, "utf8"));
3453
4001
  ctx.logger.success(`Restored fields from cache \u2192 ${fieldsPath}`);
3454
4002
  return ExitCode.SUCCESS;
3455
4003
  }
3456
4004
 
3457
4005
  // src/cli/commands/regression.ts
3458
4006
  import { existsSync as existsSync16 } from "fs";
3459
- import { resolve as resolve21 } from "path";
4007
+ import { resolve as resolve20 } from "path";
3460
4008
  var LEGACY_SHIM_SCRIPTS = [
3461
4009
  "build-cdn.mjs",
3462
4010
  "run-vite-build.mjs",
@@ -3479,7 +4027,7 @@ async function runRegressionSmoke(ctx, options = {}) {
3479
4027
  if (!validateOk) failures.push("wp config validate failed");
3480
4028
  if (options.checkLegacyShims !== false) {
3481
4029
  for (const script of LEGACY_SHIM_SCRIPTS) {
3482
- const shim = resolve21(ctx.projectRoot, "scripts", script);
4030
+ const shim = resolve20(ctx.projectRoot, "scripts", script);
3483
4031
  const kitScript = kitPath("scripts", script);
3484
4032
  const shimOk = existsSync16(shim) && existsSync16(kitScript);
3485
4033
  checks.push({ name: `shim:${script}`, ok: shimOk });
@@ -3487,12 +4035,12 @@ async function runRegressionSmoke(ctx, options = {}) {
3487
4035
  }
3488
4036
  }
3489
4037
  for (const rel of FORBIDDEN_CONSUMER_PATHS) {
3490
- const forbidden = resolve21(ctx.projectRoot, rel);
4038
+ const forbidden = resolve20(ctx.projectRoot, rel);
3491
4039
  const absent = !existsSync16(forbidden);
3492
4040
  checks.push({ name: `no-${rel}`, ok: absent });
3493
4041
  if (!absent) failures.push(`Forbidden dev path still present: ${rel}`);
3494
4042
  }
3495
- const woodscliConfig = resolve21(ctx.projectRoot, WOODSCLI_CONFIG_FILE);
4043
+ const woodscliConfig = resolve20(ctx.projectRoot, WOODSCLI_CONFIG_FILE);
3496
4044
  const configOk = existsSync16(woodscliConfig);
3497
4045
  checks.push({ name: WOODSCLI_CONFIG_FILE, ok: configOk });
3498
4046
  if (!configOk) failures.push(`${WOODSCLI_CONFIG_FILE} missing`);
@@ -3527,14 +4075,7 @@ async function runSetup(ctx, options = {}) {
3527
4075
  title: "Preparing .wphs workspace",
3528
4076
  task: async () => {
3529
4077
  if (ctx.global.dryRun) return;
3530
- try {
3531
- runNodeScript(kitPath("scripts", "ensure-wphs-workspace.mjs"), [], {
3532
- cwd: ctx.projectRoot,
3533
- logger: ctx.logger,
3534
- dryRun: false
3535
- });
3536
- } catch {
3537
- }
4078
+ await ensureProjectWphsWorkspace(ctx.projectRoot);
3538
4079
  }
3539
4080
  },
3540
4081
  {
@@ -3603,11 +4144,11 @@ async function runSetup(ctx, options = {}) {
3603
4144
  }
3604
4145
 
3605
4146
  // src/cli/commands/portal.ts
3606
- import * as p5 from "@clack/prompts";
4147
+ import * as p6 from "@clack/prompts";
3607
4148
 
3608
4149
  // src/lib/hubspot-portals.ts
3609
- import { existsSync as existsSync17, readFileSync as readFileSync15 } from "fs";
3610
- import { resolve as resolve22 } from "path";
4150
+ import { existsSync as existsSync17, readFileSync as readFileSync16 } from "fs";
4151
+ import { resolve as resolve21 } from "path";
3611
4152
  function stripYamlScalar(value) {
3612
4153
  const trimmed = value.trim();
3613
4154
  if (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'")) {
@@ -3645,20 +4186,20 @@ function parseHubSpotConfigPortals(raw) {
3645
4186
  return { defaultPortal, portals };
3646
4187
  }
3647
4188
  function readHubSpotPortals(projectRoot) {
3648
- const configPath = resolve22(projectRoot, HUBSPOT_CONFIG_FILE);
4189
+ const configPath = resolve21(projectRoot, HUBSPOT_CONFIG_FILE);
3649
4190
  if (!existsSync17(configPath)) {
3650
4191
  throw new WpHsError(`${HUBSPOT_CONFIG_FILE} not found in project root.`, {
3651
4192
  exitCode: ExitCode.USER_ERROR,
3652
4193
  remediation: "Run wp setup, then add your portal + PAT."
3653
4194
  });
3654
4195
  }
3655
- const raw = readFileSync15(configPath, "utf8");
4196
+ const raw = readFileSync16(configPath, "utf8");
3656
4197
  const { defaultPortal, portals } = parseHubSpotConfigPortals(raw);
3657
4198
  return { configPath, defaultPortal, portals };
3658
4199
  }
3659
4200
  function resolvePortalName(portals, nameOrId) {
3660
4201
  const needle = nameOrId.trim();
3661
- return portals.find((p6) => p6.name === needle) ?? portals.find((p6) => p6.portalId === needle) ?? null;
4202
+ return portals.find((p9) => p9.name === needle) ?? portals.find((p9) => p9.portalId === needle) ?? null;
3662
4203
  }
3663
4204
 
3664
4205
  // src/cli/commands/portal.ts
@@ -3713,7 +4254,7 @@ async function runPortalUse(ctx, options = {}) {
3713
4254
  remediation: "wp portal use <name> \xB7 wp portal list"
3714
4255
  });
3715
4256
  }
3716
- const answer = await p5.select({
4257
+ const answer = await p6.select({
3717
4258
  message: "Set default HubSpot portal",
3718
4259
  options: data.portals.map((portal2) => ({
3719
4260
  value: portal2.name,
@@ -3721,7 +4262,7 @@ async function runPortalUse(ctx, options = {}) {
3721
4262
  hint: portal2.name === data.defaultPortal ? `current default \xB7 ${portal2.portalId ?? "no id"}` : portal2.portalId ?? void 0
3722
4263
  }))
3723
4264
  });
3724
- if (p5.isCancel(answer)) {
4265
+ if (p6.isCancel(answer)) {
3725
4266
  throw new WpHsError("Portal selection cancelled", { exitCode: ExitCode.USER_ERROR });
3726
4267
  }
3727
4268
  targetName = answer;
@@ -3755,6 +4296,311 @@ async function runPortalUse(ctx, options = {}) {
3755
4296
  return ExitCode.SUCCESS;
3756
4297
  }
3757
4298
 
4299
+ // src/cli/commands/page.ts
4300
+ import * as p7 from "@clack/prompts";
4301
+ function cancelIfNeeded2(value) {
4302
+ if (p7.isCancel(value)) {
4303
+ throw new WpHsError("Page scaffold cancelled", { exitCode: ExitCode.USER_ERROR });
4304
+ }
4305
+ return value;
4306
+ }
4307
+ function resolveAccessFlags(options) {
4308
+ if (options.isPublic && options.isProtected) {
4309
+ throw new WpHsError("Use only one of --public or --protected.", {
4310
+ exitCode: ExitCode.USER_ERROR
4311
+ });
4312
+ }
4313
+ if (options.isPublic) {
4314
+ return "public";
4315
+ }
4316
+ if (options.isProtected) {
4317
+ return "protected";
4318
+ }
4319
+ return void 0;
4320
+ }
4321
+ async function promptPageAddOptions(ctx, options) {
4322
+ const nonInteractive = ctx.global.ci || ctx.global.yes || ctx.global.json;
4323
+ const hasNavConfig = navConfigFileExists(ctx.projectRoot);
4324
+ let slug = options.slug ? normalizePageSlug(options.slug) : void 0;
4325
+ if (!slug && nonInteractive) {
4326
+ throw new WpHsError("Page slug is required in non-interactive mode.", {
4327
+ exitCode: ExitCode.USER_ERROR,
4328
+ remediation: "Pass a slug: wp page add settings"
4329
+ });
4330
+ }
4331
+ if (!slug) {
4332
+ const slugAnswer = cancelIfNeeded2(
4333
+ await p7.text({
4334
+ message: "Page slug (URL segment)",
4335
+ placeholder: "settings",
4336
+ validate: (value) => {
4337
+ const normalized = normalizePageSlug(value ?? "");
4338
+ if (!normalized || !/^[a-z][a-z0-9-]*$/.test(normalized)) {
4339
+ return "Use lowercase letters, numbers, and hyphens";
4340
+ }
4341
+ return void 0;
4342
+ }
4343
+ })
4344
+ );
4345
+ slug = normalizePageSlug(slugAnswer);
4346
+ }
4347
+ let path = options.path ? normalizePagePath(options.path, slug) : void 0;
4348
+ if (!path && !nonInteractive) {
4349
+ const pathAnswer = cancelIfNeeded2(
4350
+ await p7.text({
4351
+ message: "Route path",
4352
+ initialValue: `/${slug}`,
4353
+ validate: (value) => {
4354
+ const normalized = normalizePagePath(value ?? "", slug);
4355
+ if (!normalized.startsWith("/")) {
4356
+ return "Path must start with /";
4357
+ }
4358
+ return void 0;
4359
+ }
4360
+ })
4361
+ );
4362
+ path = normalizePagePath(pathAnswer, slug);
4363
+ }
4364
+ path ??= normalizePagePath(`/${slug}`, slug);
4365
+ let title = options.title?.trim() || titleFromSlug(slug);
4366
+ if (!options.title && !nonInteractive) {
4367
+ const titleAnswer = cancelIfNeeded2(
4368
+ await p7.text({
4369
+ message: "Page title",
4370
+ initialValue: title,
4371
+ validate: (value) => value?.trim() ? void 0 : "Title is required"
4372
+ })
4373
+ );
4374
+ title = titleAnswer.trim();
4375
+ }
4376
+ let access = resolveAccessFlags(options);
4377
+ if (!access && nonInteractive) {
4378
+ throw new WpHsError("Access mode is required in non-interactive mode.", {
4379
+ exitCode: ExitCode.USER_ERROR,
4380
+ remediation: "Pass --public or --protected."
4381
+ });
4382
+ }
4383
+ if (!access) {
4384
+ const accessAnswer = cancelIfNeeded2(
4385
+ await p7.select({
4386
+ message: "Who can access this page?",
4387
+ initialValue: "public",
4388
+ options: [
4389
+ {
4390
+ value: "public",
4391
+ label: "Public",
4392
+ hint: "Visible without signing in"
4393
+ },
4394
+ {
4395
+ value: "protected",
4396
+ label: "Protected",
4397
+ hint: "Requires portal sign-in"
4398
+ }
4399
+ ]
4400
+ })
4401
+ );
4402
+ access = accessAnswer;
4403
+ }
4404
+ let addToNav;
4405
+ if (options.addToNav) {
4406
+ addToNav = true;
4407
+ } else if (options.skipNav) {
4408
+ addToNav = false;
4409
+ } else if (!hasNavConfig) {
4410
+ addToNav = false;
4411
+ } else if (nonInteractive) {
4412
+ throw new WpHsError("Navigation choice is required in non-interactive mode.", {
4413
+ exitCode: ExitCode.USER_ERROR,
4414
+ remediation: "Pass --nav or --no-nav when portal-nav.ts or starter-nav.ts exists."
4415
+ });
4416
+ } else {
4417
+ const navAnswer = cancelIfNeeded2(
4418
+ await p7.confirm({
4419
+ message: "Add to navigation menu?",
4420
+ initialValue: true
4421
+ })
4422
+ );
4423
+ addToNav = navAnswer;
4424
+ }
4425
+ return { slug, path, title, access, addToNav };
4426
+ }
4427
+ async function runPageAdd(ctx, options) {
4428
+ const resolved = await promptPageAddOptions(ctx, options);
4429
+ const result = await scaffoldPage({
4430
+ projectRoot: ctx.projectRoot,
4431
+ config: ctx.config,
4432
+ slug: resolved.slug,
4433
+ path: resolved.path,
4434
+ title: resolved.title,
4435
+ access: resolved.access,
4436
+ addToNav: resolved.addToNav,
4437
+ dryRun: ctx.global.dryRun,
4438
+ logger: ctx.logger
4439
+ });
4440
+ if (ctx.global.json) {
4441
+ ctx.logger.json({
4442
+ ok: true,
4443
+ slug: resolved.slug,
4444
+ path: resolved.path,
4445
+ access: resolved.access,
4446
+ addToNav: resolved.addToNav,
4447
+ ...result
4448
+ });
4449
+ return ExitCode.SUCCESS;
4450
+ }
4451
+ if (ctx.global.dryRun) {
4452
+ ctx.logger.info("[dry-run] Page scaffold planned.");
4453
+ return ExitCode.SUCCESS;
4454
+ }
4455
+ const lines = [
4456
+ `Created ${result.routeFile}`,
4457
+ `Created ${result.pageFile}`,
4458
+ result.routeTreeRegenerated ? "Regenerated src/routeTree.gen.ts" : result.routeTreeWarning ?? "Restart wp dev if navigation types look stale."
4459
+ ];
4460
+ if (!ctx.global.quiet) {
4461
+ p7.log.success(`Page "${resolved.title}" scaffolded at ${resolved.path}`);
4462
+ p7.note(lines.join("\n"), "Next steps");
4463
+ } else {
4464
+ ctx.logger.success(lines.join("\n"));
4465
+ }
4466
+ return ExitCode.SUCCESS;
4467
+ }
4468
+ async function runPageList(ctx) {
4469
+ const { listScaffoldedPages } = await import("./scaffold-page-lifecycle-EHMPWPPE.js");
4470
+ const pages = listScaffoldedPages(ctx.projectRoot, ctx.config);
4471
+ if (ctx.global.json) {
4472
+ ctx.logger.json({ pages });
4473
+ return ExitCode.SUCCESS;
4474
+ }
4475
+ if (pages.length === 0) {
4476
+ ctx.logger.info("No scaffolded pages in src/features/pages/ (use wp page add).");
4477
+ return ExitCode.SUCCESS;
4478
+ }
4479
+ if (!ctx.global.quiet) {
4480
+ p7.log.info(`Scaffolded pages (${pages.length})`);
4481
+ for (const page of pages) {
4482
+ const navFlag = page.inNav ? "nav" : "no-nav";
4483
+ p7.log.message(
4484
+ ` ${page.slug} ${page.path} ${page.access} ${navFlag} ${page.routeFile}`
4485
+ );
4486
+ }
4487
+ } else {
4488
+ for (const page of pages) {
4489
+ ctx.logger.info(`${page.slug} ${page.path} ${page.access} ${page.inNav}`);
4490
+ }
4491
+ }
4492
+ return ExitCode.SUCCESS;
4493
+ }
4494
+ async function runPageRemove(ctx, options) {
4495
+ const { removeScaffoldedPage } = await import("./scaffold-page-lifecycle-EHMPWPPE.js");
4496
+ const result = removeScaffoldedPage({
4497
+ projectRoot: ctx.projectRoot,
4498
+ config: ctx.config,
4499
+ slug: options.slug,
4500
+ removeFromNav: !options.skipNav,
4501
+ dryRun: ctx.global.dryRun,
4502
+ logger: ctx.logger
4503
+ });
4504
+ if (ctx.global.json) {
4505
+ ctx.logger.json({ ok: true, ...result });
4506
+ return ExitCode.SUCCESS;
4507
+ }
4508
+ if (ctx.global.dryRun) {
4509
+ ctx.logger.info("[dry-run] Page removal planned.");
4510
+ return ExitCode.SUCCESS;
4511
+ }
4512
+ p7.log.success(`Removed page "${result.slug}"`);
4513
+ return ExitCode.SUCCESS;
4514
+ }
4515
+
4516
+ // src/cli/commands/scaffold.ts
4517
+ import * as p8 from "@clack/prompts";
4518
+ import { writeFileSync as writeFileSync11 } from "fs";
4519
+ import { resolve as resolve22 } from "path";
4520
+ function persistScaffoldConfig(projectRoot, config, patch) {
4521
+ const auth = patch.auth ?? config.scaffold?.auth ?? "none";
4522
+ const router = patch.router ?? config.scaffold?.router ?? "hash";
4523
+ const scaffold = { router, auth };
4524
+ if (auth !== "none") {
4525
+ scaffold.nav = patch.nav ?? config.scaffold?.nav ?? "top";
4526
+ }
4527
+ const next = { ...config, scaffold };
4528
+ writeFileSync11(resolve22(projectRoot, WOODSCLI_CONFIG_FILE), `${JSON.stringify(next, null, 2)}
4529
+ `);
4530
+ return next;
4531
+ }
4532
+ function parseAuthTier(value) {
4533
+ if (value === "starter" || value === "sso" || value === "full") {
4534
+ return value;
4535
+ }
4536
+ return void 0;
4537
+ }
4538
+ function runScaffoldAuthUpgrade(ctx, options) {
4539
+ const routerMode = ctx.config.scaffold?.router ?? "hash";
4540
+ const nav = ctx.config.scaffold?.nav ?? "top";
4541
+ const current = readProjectAuthTier(ctx.projectRoot);
4542
+ if (ctx.global.dryRun) {
4543
+ ctx.logger.info(
4544
+ `[dry-run] Would upgrade auth from "${current}" to "${options.tier}" in ${ctx.projectRoot}`
4545
+ );
4546
+ return ExitCode.SUCCESS;
4547
+ }
4548
+ const applied = applyAuthTierUpgrade(
4549
+ ctx.projectRoot,
4550
+ options.tier,
4551
+ routerMode,
4552
+ ctx.config.projectType,
4553
+ nav
4554
+ );
4555
+ persistScaffoldConfig(ctx.projectRoot, ctx.config, {
4556
+ auth: applied,
4557
+ nav: applied !== "none" ? nav : void 0
4558
+ });
4559
+ if (ctx.global.json) {
4560
+ ctx.logger.json({ ok: true, previousTier: current, tier: applied });
4561
+ return ExitCode.SUCCESS;
4562
+ }
4563
+ p8.log.success(`Portal auth upgraded to "${applied}" (was "${current}")`);
4564
+ p8.note("Review new routes under src/routes/_auth/ and run wp dev.", "Next steps");
4565
+ return ExitCode.SUCCESS;
4566
+ }
4567
+ function runScaffoldNav(ctx, options) {
4568
+ const currentNav = ctx.config.scaffold?.nav ?? "top";
4569
+ if (ctx.config.scaffold?.auth === "none" || readProjectAuthTier(ctx.projectRoot) === "none") {
4570
+ throw new WpHsError("Nav scaffold requires portal auth.", {
4571
+ exitCode: ExitCode.USER_ERROR,
4572
+ remediation: "Run wp scaffold auth upgrade --tier starter first."
4573
+ });
4574
+ }
4575
+ if (ctx.global.dryRun) {
4576
+ ctx.logger.info(`[dry-run] Would apply nav shell "${options.mode}" (was "${currentNav}")`);
4577
+ return ExitCode.SUCCESS;
4578
+ }
4579
+ applyNavScaffoldUpgrade(ctx.projectRoot, options.mode);
4580
+ persistScaffoldConfig(ctx.projectRoot, ctx.config, { nav: options.mode });
4581
+ if (ctx.global.json) {
4582
+ ctx.logger.json({ ok: true, previousMode: currentNav, mode: options.mode });
4583
+ return ExitCode.SUCCESS;
4584
+ }
4585
+ p8.log.success(`Navigation shell set to "${options.mode}" (was "${currentNav}")`);
4586
+ return ExitCode.SUCCESS;
4587
+ }
4588
+ function parseScaffoldAuthTierArg(value) {
4589
+ const tier = value ? parseAuthTier(value) : void 0;
4590
+ if (!tier) {
4591
+ throw new WpHsError("Auth tier is required: starter | sso | full", {
4592
+ exitCode: ExitCode.USER_ERROR
4593
+ });
4594
+ }
4595
+ return tier;
4596
+ }
4597
+ function parseScaffoldNavModeArg(value) {
4598
+ if (value === "top" || value === "sidebar") {
4599
+ return value;
4600
+ }
4601
+ throw new WpHsError("Nav mode is required: top | sidebar", { exitCode: ExitCode.USER_ERROR });
4602
+ }
4603
+
3758
4604
  // src/cli/commands/watch.ts
3759
4605
  async function runWatch(ctx, options) {
3760
4606
  ensureHubSpotConfigForCms(ctx);
@@ -3780,14 +4626,19 @@ async function runWatch(ctx, options) {
3780
4626
  }
3781
4627
 
3782
4628
  // src/cli/program.ts
3783
- function readVersion() {
4629
+ function resolveProjectRootForVersionCheck(project) {
4630
+ if (project) {
4631
+ return project;
4632
+ }
3784
4633
  try {
3785
- const pkg = JSON.parse(readFileSync16(resolve23(getKitRoot(), "package.json"), "utf8"));
3786
- return pkg.version ?? "0.0.0";
4634
+ return findProjectRoot();
3787
4635
  } catch {
3788
- return "0.0.0";
4636
+ return process.cwd();
3789
4637
  }
3790
4638
  }
4639
+ function readVersion() {
4640
+ return readKitVersion();
4641
+ }
3791
4642
  function buildContext(global) {
3792
4643
  const logger = createLogger({
3793
4644
  verbose: global.verbose,
@@ -3795,7 +4646,7 @@ function buildContext(global) {
3795
4646
  json: global.json,
3796
4647
  ci: global.ci
3797
4648
  });
3798
- const projectRoot = global.project ?? findProjectRoot2();
4649
+ const projectRoot = global.project ?? findProjectRoot();
3799
4650
  const { config } = loadConfig(projectRoot);
3800
4651
  return { projectRoot, config, logger, global };
3801
4652
  }
@@ -3805,7 +4656,7 @@ function attachGlobals(command) {
3805
4656
  async function handle(fn) {
3806
4657
  try {
3807
4658
  const code = await fn();
3808
- process.exit(code);
4659
+ await exitCli(code);
3809
4660
  } catch (error) {
3810
4661
  if (isWpHsError(error)) {
3811
4662
  process.stderr.write(`Error: ${error.message}
@@ -3814,16 +4665,30 @@ async function handle(fn) {
3814
4665
  process.stderr.write(`\u2192 ${error.remediation}
3815
4666
  `);
3816
4667
  }
3817
- process.exit(error.exitCode);
4668
+ await exitCli(error.exitCode);
3818
4669
  }
3819
4670
  process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}
3820
4671
  `);
3821
- process.exit(ExitCode.INTERNAL);
4672
+ await exitCli(ExitCode.INTERNAL);
3822
4673
  }
3823
4674
  }
3824
4675
  function createProgram() {
3825
4676
  const program2 = new Command().name("wp").description("WoodsCLI \u2014 HubSpot CMS dev kit by WoodsPortal (modules + themes)").version(readVersion()).addHelpText("after", "\nWoodsCLI \xB7 WoodsPortal HubSpot CLI \xB7 npm @woodsportal/hubspot-kit");
3826
4677
  attachGlobals(program2);
4678
+ program2.hook("preAction", async (_thisCommand, actionCommand) => {
4679
+ const global = program2.opts();
4680
+ const name = actionCommand.name();
4681
+ if (name === "version" || name === "help") {
4682
+ return;
4683
+ }
4684
+ await maybeWarnKitUpdate({
4685
+ current: readKitVersion(),
4686
+ projectRoot: resolveProjectRootForVersionCheck(global.project),
4687
+ ci: global.ci,
4688
+ json: global.json,
4689
+ quiet: global.quiet
4690
+ });
4691
+ });
3827
4692
  program2.command("doctor").description("Check prerequisites (Node, HubSpot CLI, .wphs workspace, config)").action(async () => {
3828
4693
  const global = program2.opts();
3829
4694
  await handle(() => runDoctor(buildContext(global)));
@@ -3842,6 +4707,12 @@ function createProgram() {
3842
4707
  await handle(() => runConfigValidate(buildContext(global)));
3843
4708
  });
3844
4709
  program2.command("init").description("Scaffold a new module or theme project").option("-t, --template <type>", "module-hybrid-cdn | module-monolith | theme-starter").option("-n, --name <slug>", "Project name (creates ./<name>/ under cwd)").option("-d, --directory <path>", "Full target directory (skips name \u2192 folder prompt)").option("--router <mode>", "hash (client-frontend default) | browser | none \u2014 modules only").option(
4710
+ "--auth <tier>",
4711
+ "Portal auth: none | starter | sso | full \u2014 modules with router \u2260 none"
4712
+ ).option(
4713
+ "--nav <mode>",
4714
+ "Authenticated nav shell: top | sidebar (requires --auth starter|sso|full)"
4715
+ ).option(
3845
4716
  "--cdn-github <org/repo>",
3846
4717
  "Public GitHub CDN mirror repo (hybrid CDN; use with --ci / -y)"
3847
4718
  ).action(
@@ -3860,12 +4731,16 @@ function createProgram() {
3860
4731
  global
3861
4732
  };
3862
4733
  const routerMode = opts.router === "browser" || opts.router === "none" || opts.router === "hash" ? opts.router : void 0;
4734
+ const authMode = opts.auth === "none" || opts.auth === "starter" || opts.auth === "sso" || opts.auth === "full" ? opts.auth : void 0;
4735
+ const navMode = opts.nav === "top" || opts.nav === "sidebar" ? opts.nav : void 0;
3863
4736
  await handle(
3864
4737
  () => runInit(ctx, {
3865
4738
  template: opts.template,
3866
4739
  name: opts.name,
3867
4740
  directory: opts.directory,
3868
4741
  router: routerMode,
4742
+ auth: authMode,
4743
+ nav: navMode,
3869
4744
  cdnGithub: opts.cdnGithub
3870
4745
  })
3871
4746
  );
@@ -3998,6 +4873,45 @@ function createProgram() {
3998
4873
  const global = program2.opts();
3999
4874
  await handle(() => runPortalUse(buildContext(global), { name }));
4000
4875
  });
4876
+ const page = program2.command("page").description("Scaffold routed pages");
4877
+ page.command("add [slug]").description("Create a route file + page component").option("--path <path>", "URL path (default: /<slug>)").option("--title <title>", "Page heading").option("--public", "Public route (no sign-in)").option("--protected", "Protected route (requires auth)").option("--nav", "Add link to navigation menu (portal-nav.ts or starter-nav.ts)").option("--no-nav", "Skip navigation menu").action(
4878
+ async (slug, opts) => {
4879
+ const global = program2.opts();
4880
+ await handle(
4881
+ () => runPageAdd(buildContext(global), {
4882
+ slug,
4883
+ path: opts.path,
4884
+ title: opts.title,
4885
+ isPublic: opts.public,
4886
+ isProtected: opts.protected,
4887
+ addToNav: opts.nav,
4888
+ skipNav: opts.noNav
4889
+ })
4890
+ );
4891
+ }
4892
+ );
4893
+ page.command("list").alias("ls").description("List scaffolded pages under src/features/pages/").action(async () => {
4894
+ const global = program2.opts();
4895
+ await handle(() => runPageList(buildContext(global)));
4896
+ });
4897
+ page.command("remove <slug>").description("Remove a scaffolded page (route, component, routes.ts, nav)").option("--no-nav", "Leave navigation menu unchanged").action(async (slug, opts) => {
4898
+ const global = program2.opts();
4899
+ await handle(
4900
+ () => runPageRemove(buildContext(global), { slug, skipNav: opts.noNav })
4901
+ );
4902
+ });
4903
+ const scaffold = program2.command("scaffold").description("Re-apply portal scaffold layers");
4904
+ const scaffoldAuth = scaffold.command("auth").description("Portal auth tier upgrades");
4905
+ scaffoldAuth.command("upgrade").description("Upgrade portal auth tier on an existing module (starter \u2192 sso \u2192 full)").requiredOption("--tier <tier>", "starter | sso | full").action(async (opts) => {
4906
+ const global = program2.opts();
4907
+ const tier = parseScaffoldAuthTierArg(opts.tier);
4908
+ await handle(async () => runScaffoldAuthUpgrade(buildContext(global), { tier }));
4909
+ });
4910
+ scaffold.command("nav").description("Re-apply authenticated navigation shell (top | sidebar)").requiredOption("--mode <mode>", "top | sidebar").action(async (opts) => {
4911
+ const global = program2.opts();
4912
+ const mode = parseScaffoldNavModeArg(opts.mode);
4913
+ await handle(async () => runScaffoldNav(buildContext(global), { mode }));
4914
+ });
4001
4915
  return program2;
4002
4916
  }
4003
4917