@woodsportal/hubspot-kit 1.0.5 → 1.0.26

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 (165) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +4 -2
  3. package/dist/{chunk-JDGIAYHX.js → chunk-GTTIJVAR.js} +43 -3
  4. package/dist/chunk-GTTIJVAR.js.map +1 -0
  5. package/dist/cli.js +1378 -339
  6. package/dist/cli.js.map +1 -1
  7. package/dist/exec-U2OJATR3.js +12 -0
  8. package/dist/index.d.ts +20 -0
  9. package/dist/index.js +8 -1
  10. package/dist/index.js.map +1 -1
  11. package/examples/greenfield/module-hybrid-cdn/README.md +40 -3
  12. package/examples/greenfield/module-hybrid-cdn/public/fields.json +44 -1
  13. package/examples/greenfield/module-hybrid-cdn/src/app.tsx +8 -0
  14. package/examples/greenfield/module-hybrid-cdn/src/assets/css/main.dev.css +6 -1
  15. package/examples/greenfield/module-hybrid-cdn/src/cdn/app-entry.ts +32 -0
  16. package/examples/greenfield/module-hybrid-cdn/src/cdn/vendor-entry.ts +28 -0
  17. package/examples/greenfield/module-hybrid-cdn/src/components/layout/section-card.tsx +20 -0
  18. package/examples/greenfield/module-hybrid-cdn/src/components/layout/starter-nav.tsx +28 -0
  19. package/examples/greenfield/module-hybrid-cdn/src/components/layout/starter-shell.tsx +18 -0
  20. package/examples/greenfield/module-hybrid-cdn/src/config/router.ts +4 -0
  21. package/examples/greenfield/module-hybrid-cdn/src/config/routes.ts +5 -0
  22. package/examples/greenfield/module-hybrid-cdn/src/defaultData.ts +2 -7
  23. package/examples/greenfield/module-hybrid-cdn/src/features/starter/components/starter-guide-page.tsx +93 -0
  24. package/examples/greenfield/module-hybrid-cdn/src/features/starter/components/starter-home-page.tsx +21 -0
  25. package/examples/greenfield/module-hybrid-cdn/src/features/starter/components/starter-home.tsx +52 -0
  26. package/examples/greenfield/module-hybrid-cdn/src/features/starter/components/starter-single-page.tsx +17 -0
  27. package/examples/greenfield/module-hybrid-cdn/src/features/starter/content/woodscli-guide-content.ts +86 -0
  28. package/examples/greenfield/module-hybrid-cdn/src/features/starter/hooks/use-starter-preview.ts +47 -0
  29. package/examples/greenfield/module-hybrid-cdn/src/features/starter/index.ts +6 -0
  30. package/examples/greenfield/module-hybrid-cdn/src/hubspot/bootstrap.ts +10 -0
  31. package/examples/greenfield/module-hybrid-cdn/src/hubspot/default-data.ts +7 -0
  32. package/examples/greenfield/module-hybrid-cdn/src/integrations/tanstack-router/router.tsx +25 -0
  33. package/examples/greenfield/module-hybrid-cdn/src/main.tsx +4 -3
  34. package/examples/greenfield/module-hybrid-cdn/src/portal/mount-dev-preview.tsx +8 -0
  35. package/examples/greenfield/module-hybrid-cdn/src/portal/mount-portal.tsx +12 -0
  36. package/examples/greenfield/module-hybrid-cdn/src/routeTree.gen.ts +77 -0
  37. package/examples/greenfield/module-hybrid-cdn/src/routes/__root.tsx +15 -0
  38. package/examples/greenfield/module-hybrid-cdn/src/routes/guide.tsx +7 -0
  39. package/examples/greenfield/module-hybrid-cdn/src/routes/index.tsx +7 -0
  40. package/examples/greenfield/module-hybrid-cdn/tsconfig.json +20 -0
  41. package/examples/greenfield/module-monolith/public/fields.json +44 -1
  42. package/examples/greenfield/module-monolith/src/app.tsx +8 -0
  43. package/examples/greenfield/module-monolith/src/assets/css/main.dev.css +8 -0
  44. package/examples/greenfield/module-monolith/src/components/layout/section-card.tsx +20 -0
  45. package/examples/greenfield/module-monolith/src/components/layout/starter-nav.tsx +28 -0
  46. package/examples/greenfield/module-monolith/src/components/layout/starter-shell.tsx +18 -0
  47. package/examples/greenfield/module-monolith/src/config/router.ts +4 -0
  48. package/examples/greenfield/module-monolith/src/config/routes.ts +5 -0
  49. package/examples/greenfield/module-monolith/src/defaultData.ts +2 -7
  50. package/examples/greenfield/module-monolith/src/features/starter/components/starter-guide-page.tsx +93 -0
  51. package/examples/greenfield/module-monolith/src/features/starter/components/starter-home-page.tsx +21 -0
  52. package/examples/greenfield/module-monolith/src/features/starter/components/starter-home.tsx +52 -0
  53. package/examples/greenfield/module-monolith/src/features/starter/components/starter-single-page.tsx +17 -0
  54. package/examples/greenfield/module-monolith/src/features/starter/content/woodscli-guide-content.ts +86 -0
  55. package/examples/greenfield/module-monolith/src/features/starter/hooks/use-starter-preview.ts +47 -0
  56. package/examples/greenfield/module-monolith/src/features/starter/index.ts +6 -0
  57. package/examples/greenfield/module-monolith/src/hubspot/default-data.ts +7 -0
  58. package/examples/greenfield/module-monolith/src/hubspot/types.ts +17 -0
  59. package/examples/greenfield/module-monolith/src/integrations/tanstack-router/router.tsx +25 -0
  60. package/examples/greenfield/module-monolith/src/main.tsx +5 -0
  61. package/examples/greenfield/module-monolith/src/portal/mount-dev-preview.tsx +8 -0
  62. package/examples/greenfield/module-monolith/src/portal/mount-portal.tsx +12 -0
  63. package/examples/greenfield/module-monolith/src/routeTree.gen.ts +77 -0
  64. package/examples/greenfield/module-monolith/src/routes/__root.tsx +15 -0
  65. package/examples/greenfield/module-monolith/src/routes/guide.tsx +7 -0
  66. package/examples/greenfield/module-monolith/src/routes/index.tsx +7 -0
  67. package/examples/greenfield/module-monolith/tsconfig.json +20 -0
  68. package/package.json +1 -1
  69. package/scripts/check-hubspot-bundle-size.mjs +5 -2
  70. package/scripts/ensure-wphs-workspace.mjs +2 -1
  71. package/scripts/hubspot-module-postbuild.mjs +1 -1
  72. package/scripts/read-woodsportal-config.mjs +4 -1
  73. package/scripts/run-hubspot-module-postbuild.mjs +9 -4
  74. package/scripts/vite/bootstrap-shim-resolver.js +47 -0
  75. package/scripts/vite/ensure-dev-kit-shims.js +7 -1
  76. package/scripts/vite/react-dev-plugin-options.js +2 -1
  77. package/scripts/watch-hubspot.mjs +107 -17
  78. package/scripts/wphs-paths.mjs +1 -0
  79. package/scripts/write-audit-artifacts.mjs +2 -1
  80. package/src/dev/bootstrap-greenfield.tsx +17 -9
  81. package/src/dev-module-config/dev-module-display-name.ts +5 -0
  82. package/src/dev-module-config/ui/ModuleConfigPersistenceStatus.tsx +5 -5
  83. package/src/dev-module-config/ui/ModuleConfigShell.tsx +2 -1
  84. package/src/dev-module-config/ui/module-config-theme.css +6 -0
  85. package/src/dev-module-config/ui/navigation/ModuleConfigNavContext.tsx +11 -5
  86. package/templates/module-hybrid-cdn/README.md +53 -3
  87. package/templates/module-hybrid-cdn/package.json +6 -2
  88. package/templates/module-hybrid-cdn/public/fields.json +44 -1
  89. package/templates/module-hybrid-cdn/src/app.tsx +8 -0
  90. package/templates/module-hybrid-cdn/src/assets/css/main.dev.css +6 -1
  91. package/templates/module-hybrid-cdn/src/cdn/app-entry.ts +32 -0
  92. package/templates/module-hybrid-cdn/src/cdn/vendor-entry.ts +28 -0
  93. package/templates/module-hybrid-cdn/src/components/layout/section-card.tsx +20 -0
  94. package/templates/module-hybrid-cdn/src/components/layout/starter-nav.tsx +28 -0
  95. package/templates/module-hybrid-cdn/src/components/layout/starter-shell.tsx +18 -0
  96. package/templates/module-hybrid-cdn/src/config/router.ts +4 -0
  97. package/templates/module-hybrid-cdn/src/config/routes.ts +5 -0
  98. package/templates/module-hybrid-cdn/src/defaultData.ts +2 -7
  99. package/templates/module-hybrid-cdn/src/features/starter/components/starter-guide-page.tsx +93 -0
  100. package/templates/module-hybrid-cdn/src/features/starter/components/starter-home-page.tsx +21 -0
  101. package/templates/module-hybrid-cdn/src/features/starter/components/starter-home.tsx +52 -0
  102. package/templates/module-hybrid-cdn/src/features/starter/components/starter-single-page.tsx +17 -0
  103. package/templates/module-hybrid-cdn/src/features/starter/content/woodscli-guide-content.ts +86 -0
  104. package/templates/module-hybrid-cdn/src/features/starter/hooks/starter-preview-values.ts +42 -0
  105. package/templates/module-hybrid-cdn/src/features/starter/hooks/use-starter-preview.dev.ts +17 -0
  106. package/templates/module-hybrid-cdn/src/features/starter/hooks/use-starter-preview.ts +30 -0
  107. package/templates/module-hybrid-cdn/src/features/starter/index.ts +6 -0
  108. package/templates/module-hybrid-cdn/src/hubspot/bootstrap.ts +10 -0
  109. package/templates/module-hybrid-cdn/src/hubspot/default-data.ts +7 -0
  110. package/templates/module-hybrid-cdn/src/hubspot/module-entry.tsx +9 -0
  111. package/templates/module-hybrid-cdn/src/integrations/tanstack-router/router.tsx +25 -0
  112. package/templates/module-hybrid-cdn/src/main.tsx +4 -3
  113. package/templates/module-hybrid-cdn/src/portal/mount-dev-preview.tsx +8 -0
  114. package/templates/module-hybrid-cdn/src/portal/mount-portal.tsx +12 -0
  115. package/templates/module-hybrid-cdn/src/routeTree.gen.ts +77 -0
  116. package/templates/module-hybrid-cdn/src/routes/__root.tsx +15 -0
  117. package/templates/module-hybrid-cdn/src/routes/guide.tsx +7 -0
  118. package/templates/module-hybrid-cdn/src/routes/index.tsx +7 -0
  119. package/templates/module-hybrid-cdn/tsconfig.json +20 -0
  120. package/templates/module-hybrid-cdn/vite.config.monolith.mjs +8 -0
  121. package/templates/module-monolith/README.md +59 -0
  122. package/templates/module-monolith/index.html +1 -1
  123. package/templates/module-monolith/package.json +10 -3
  124. package/templates/module-monolith/public/fields.json +44 -1
  125. package/templates/module-monolith/public/module.html +1 -0
  126. package/templates/module-monolith/src/app.tsx +8 -0
  127. package/templates/module-monolith/src/assets/css/main.dev.css +6 -1
  128. package/templates/module-monolith/src/components/layout/section-card.tsx +20 -0
  129. package/templates/module-monolith/src/components/layout/starter-nav.tsx +28 -0
  130. package/templates/module-monolith/src/components/layout/starter-shell.tsx +18 -0
  131. package/templates/module-monolith/src/config/router.ts +4 -0
  132. package/templates/module-monolith/src/config/routes.ts +5 -0
  133. package/templates/module-monolith/src/defaultData.ts +2 -7
  134. package/templates/module-monolith/src/features/starter/components/starter-guide-page.tsx +93 -0
  135. package/templates/module-monolith/src/features/starter/components/starter-home-page.tsx +21 -0
  136. package/templates/module-monolith/src/features/starter/components/starter-home.tsx +52 -0
  137. package/templates/module-monolith/src/features/starter/components/starter-single-page.tsx +17 -0
  138. package/templates/module-monolith/src/features/starter/content/woodscli-guide-content.ts +86 -0
  139. package/templates/module-monolith/src/features/starter/hooks/starter-preview-values.ts +42 -0
  140. package/templates/module-monolith/src/features/starter/hooks/use-starter-preview.dev.ts +17 -0
  141. package/templates/module-monolith/src/features/starter/hooks/use-starter-preview.ts +30 -0
  142. package/templates/module-monolith/src/features/starter/index.ts +6 -0
  143. package/templates/module-monolith/src/hubspot/default-data.ts +7 -0
  144. package/templates/module-monolith/src/hubspot/module-entry.tsx +9 -0
  145. package/templates/module-monolith/src/integrations/tanstack-router/router.tsx +25 -0
  146. package/templates/module-monolith/src/main.tsx +4 -3
  147. package/templates/module-monolith/src/portal/mount-dev-preview.tsx +8 -0
  148. package/templates/module-monolith/src/portal/mount-portal.tsx +12 -0
  149. package/templates/module-monolith/src/routeTree.gen.ts +77 -0
  150. package/templates/module-monolith/src/routes/__root.tsx +15 -0
  151. package/templates/module-monolith/src/routes/guide.tsx +7 -0
  152. package/templates/module-monolith/src/routes/index.tsx +7 -0
  153. package/templates/module-monolith/tsconfig.json +20 -0
  154. package/templates/module-monolith/vite.config.monolith.mjs +8 -0
  155. package/templates/module-monolith/woodscli.json +1 -0
  156. package/templates/shared/.env.dev.example +5 -0
  157. package/templates/shared/.env.prod.example +5 -0
  158. package/templates/shared/.env.stg.example +3 -0
  159. package/templates/shared/portal.cdn.json.example +8 -0
  160. package/templates/theme-starter/package.json +5 -2
  161. package/vite/vite.config.dev.mjs +9 -1
  162. package/vite/vite.config.monolith.mjs +22 -17
  163. package/dist/chunk-JDGIAYHX.js.map +0 -1
  164. package/dist/exec-K5C6LZP4.js +0 -10
  165. /package/dist/{exec-K5C6LZP4.js.map → exec-U2OJATR3.js.map} +0 -0
package/dist/cli.js CHANGED
@@ -4,12 +4,13 @@ import {
4
4
  WpHsError,
5
5
  isWpHsError,
6
6
  runCommand,
7
- runNodeScript
8
- } from "./chunk-JDGIAYHX.js";
7
+ runNodeScript,
8
+ runNodeScriptLongRunning
9
+ } from "./chunk-GTTIJVAR.js";
9
10
 
10
11
  // src/cli/program.ts
11
- import { readFileSync as readFileSync10 } from "fs";
12
- import { resolve as resolve17 } from "path";
12
+ import { readFileSync as readFileSync14 } from "fs";
13
+ import { resolve as resolve21 } from "path";
13
14
  import { Command } from "commander";
14
15
 
15
16
  // src/lib/config.ts
@@ -94,6 +95,8 @@ var WoodsPortalHsSchema = z.object({
94
95
  app: z.string().default("src/cdn/app-entry.ts"),
95
96
  vendor: z.string().default("src/cdn/vendor-entry.ts"),
96
97
  hubspotBootstrap: z.string().default("src/hubspot/bootstrap.ts"),
98
+ /** Monolith production IIFE entry (wp build --hubspot-only) */
99
+ module: z.string().default("src/hubspot/module-entry.tsx"),
97
100
  localDev: z.string().default("src/main.tsx")
98
101
  }).default({}),
99
102
  env: z.object({
@@ -104,7 +107,11 @@ var WoodsPortalHsSchema = z.object({
104
107
  adapter: z.string().default("./wphs.adapter.ts"),
105
108
  fieldsJson: z.string().default("public/fields.json"),
106
109
  themeFieldsJson: z.string().optional()
107
- }).default({})
110
+ }).default({}),
111
+ scaffold: z.object({
112
+ /** hash = woodsportal-client-frontend (HubSpot CMS); browser = local clean URLs; none = single page */
113
+ router: z.enum(["hash", "browser", "none"]).default("hash")
114
+ }).optional()
108
115
  });
109
116
  var DEFAULT_WOODSPORTAL_HS = {
110
117
  configVersion: CONFIG_VERSION,
@@ -123,6 +130,7 @@ var DEFAULT_WOODSPORTAL_HS = {
123
130
  app: "src/cdn/app-entry.ts",
124
131
  vendor: "src/cdn/vendor-entry.ts",
125
132
  hubspotBootstrap: "src/hubspot/bootstrap.ts",
133
+ module: "src/hubspot/module-entry.tsx",
126
134
  localDev: "src/main.tsx"
127
135
  },
128
136
  env: {
@@ -285,22 +293,66 @@ function defaultBaselineDir(projectRoot, config) {
285
293
  }
286
294
 
287
295
  // src/cli/commands/doctor.ts
288
- import { existsSync as existsSync4 } from "fs";
296
+ import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
289
297
  import { join, resolve as resolve5 } from "path";
290
298
 
299
+ // src/lib/cli-spinner.ts
300
+ import * as p from "@clack/prompts";
301
+ import pc2 from "picocolors";
302
+ function isInteractiveCli(ctx) {
303
+ const { json, quiet, ci } = ctx.global;
304
+ return !json && !quiet && !ci && process.stdout.isTTY === true;
305
+ }
306
+ function shouldShowProgress(ctx, override) {
307
+ if (override === false) return false;
308
+ return isInteractiveCli(ctx);
309
+ }
310
+ async function runCliTasks(ctx, tasks2, options = {}) {
311
+ if (!shouldShowProgress(ctx, options.progress)) {
312
+ for (const step of tasks2) {
313
+ await step.task();
314
+ }
315
+ return;
316
+ }
317
+ await p.tasks(tasks2);
318
+ }
319
+ function printCommandDone(ctx, message) {
320
+ if (isInteractiveCli(ctx)) {
321
+ process.stdout.write(`${pc2.green("\u25C6")} ${message}
322
+ `);
323
+ return;
324
+ }
325
+ ctx.logger.success(message);
326
+ }
327
+
291
328
  // src/lib/hubspot-cli-setup.ts
292
329
  import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
293
330
  import { resolve as resolve4 } from "path";
294
331
  import { spawnSync } from "child_process";
295
332
  var HUBSPOT_CONFIG_FILE = "hubspot.config.yml";
296
333
  var HUBSPOT_CONFIG_EXAMPLE = "hubspot.config.yml.example";
334
+ var HS_VERSION_TIMEOUT_MS = 5e3;
335
+ var HS_ACCOUNTS_TIMEOUT_MS = 1e4;
297
336
  var PLACEHOLDER_PATTERNS = [
298
337
  /YOUR_PAT_HERE/i,
299
338
  /your-portal-id/i,
300
339
  /personalAccessKey:\s*$/m,
301
340
  /portalId:\s*12345678\b/
302
341
  ];
303
- function assessHubSpotCliSetup(projectRoot) {
342
+ function spawnHs(args, options = {}) {
343
+ return spawnSync("hs", args, {
344
+ cwd: options.cwd,
345
+ encoding: "utf8",
346
+ timeout: options.timeoutMs ?? HS_ACCOUNTS_TIMEOUT_MS,
347
+ // Keep HubSpot CLI off the interactive TTY (avoids hangs under @clack spinners)
348
+ stdio: ["ignore", "pipe", "pipe"]
349
+ });
350
+ }
351
+ function hsTimedOut(result) {
352
+ const err = result.error;
353
+ return err?.code === "ETIMEDOUT";
354
+ }
355
+ function assessHubSpotConfigFiles(projectRoot) {
304
356
  const configPath = resolve4(projectRoot, HUBSPOT_CONFIG_FILE);
305
357
  const examplePath = resolve4(projectRoot, HUBSPOT_CONFIG_EXAMPLE);
306
358
  const configPresent = existsSync3(configPath);
@@ -312,43 +364,47 @@ function assessHubSpotCliSetup(projectRoot) {
312
364
  } else {
313
365
  looksLikePlaceholder = true;
314
366
  }
315
- const hsVersion = spawnSync("hs", ["--version"], { encoding: "utf8" });
316
- const hsCliInstalled = hsVersion.status === 0;
367
+ return {
368
+ configPath,
369
+ examplePath,
370
+ configPresent,
371
+ examplePresent,
372
+ looksLikePlaceholder
373
+ };
374
+ }
375
+ function runHubSpotCliChecks(projectRoot, options = {}) {
376
+ const checkAccounts = options.checkAccounts !== false;
377
+ const hsVersion = spawnHs(["--version"], { timeoutMs: HS_VERSION_TIMEOUT_MS });
378
+ const hsCliInstalled = hsVersion.status === 0 && !hsTimedOut(hsVersion);
317
379
  const hsCliVersion = hsCliInstalled ? (hsVersion.stdout || hsVersion.stderr || "").trim() : null;
318
380
  let authenticated = false;
319
381
  let accountsHint = null;
320
- if (hsCliInstalled) {
321
- const accounts = spawnSync("hs", ["accounts", "list"], {
382
+ if (hsCliInstalled && checkAccounts) {
383
+ const accounts = spawnHs(["accounts", "list"], {
322
384
  cwd: projectRoot,
323
- encoding: "utf8",
324
- timeout: 15e3
385
+ timeoutMs: HS_ACCOUNTS_TIMEOUT_MS
325
386
  });
326
387
  const output = `${accounts.stdout || ""}
327
388
  ${accounts.stderr || ""}`.trim();
328
- accountsHint = output || null;
329
- authenticated = accounts.status === 0 && /Account:|portalId|portal/i.test(output) && !/No accounts/i.test(output);
389
+ accountsHint = hsTimedOut(accounts) ? "timed out" : output || null;
390
+ authenticated = !hsTimedOut(accounts) && accounts.status === 0 && /Account:|portalId|portal/i.test(output) && !/No accounts/i.test(output);
330
391
  }
392
+ return { hsCliInstalled, hsCliVersion, authenticated, accountsHint };
393
+ }
394
+ function assessHubSpotCliSetup(projectRoot, options = {}) {
331
395
  return {
332
- configPath,
333
- examplePath,
334
- configPresent,
335
- examplePresent,
336
- looksLikePlaceholder,
337
- hsCliInstalled,
338
- hsCliVersion,
339
- authenticated,
340
- accountsHint
396
+ ...assessHubSpotConfigFiles(projectRoot),
397
+ ...runHubSpotCliChecks(projectRoot, options)
341
398
  };
342
399
  }
343
- function hubSpotSetupRemediation(projectRoot) {
344
- const assessment = assessHubSpotCliSetup(projectRoot);
400
+ function hubSpotSetupRemediation(projectRoot, assessment = assessHubSpotCliSetup(projectRoot)) {
345
401
  const steps = [];
346
402
  if (!assessment.hsCliInstalled) {
347
403
  steps.push("Install HubSpot CLI: npm i -g @hubspot/cli");
348
404
  }
349
405
  if (!assessment.configPresent) {
350
406
  if (assessment.examplePresent) {
351
- steps.push(`Copy ${HUBSPOT_CONFIG_EXAMPLE} \u2192 ${HUBSPOT_CONFIG_FILE} and add your portal + PAT`);
407
+ steps.push(`Run wp setup (copies ${HUBSPOT_CONFIG_EXAMPLE} \u2192 ${HUBSPOT_CONFIG_FILE}) and add portal + PAT`);
352
408
  } else {
353
409
  steps.push(`Create ${HUBSPOT_CONFIG_FILE} (see docs/HUBSPOT-CLI-SETUP.md)`);
354
410
  }
@@ -361,29 +417,67 @@ function hubSpotSetupRemediation(projectRoot) {
361
417
  }
362
418
  return steps;
363
419
  }
420
+ function hubSpotLocalDevRemediation(files) {
421
+ const steps = [];
422
+ if (!files.configPresent) {
423
+ if (files.examplePresent) {
424
+ steps.push(`Run wp setup (copies ${HUBSPOT_CONFIG_EXAMPLE} \u2192 ${HUBSPOT_CONFIG_FILE}) and add portal + PAT`);
425
+ } else {
426
+ steps.push(`Create ${HUBSPOT_CONFIG_FILE} (see docs/HUBSPOT-CLI-SETUP.md)`);
427
+ }
428
+ steps.push("Generate a PAT: https://app.hubspot.com/l/personal-access-key");
429
+ } else if (files.looksLikePlaceholder) {
430
+ steps.push(`Edit ${HUBSPOT_CONFIG_FILE} \u2014 replace placeholder portalId and personalAccessKey`);
431
+ }
432
+ return steps;
433
+ }
434
+ function cmsEnsureAssessment(projectRoot) {
435
+ return assessHubSpotCliSetup(projectRoot, { checkAccounts: false });
436
+ }
437
+ function throwCmsConfigError(_ctx, files) {
438
+ const steps = hubSpotLocalDevRemediation(files);
439
+ throw new WpHsError("HubSpot config is not ready for cms watch/upload in this project.", {
440
+ exitCode: ExitCode.PREREQUISITE,
441
+ remediation: steps.join("; ")
442
+ });
443
+ }
444
+ function ensureHubSpotConfigForCms(ctx) {
445
+ if (ctx.global.dryRun || ctx.global.json) return;
446
+ const files = assessHubSpotConfigFiles(ctx.projectRoot);
447
+ if (files.configPresent && !files.looksLikePlaceholder) return;
448
+ throwCmsConfigError(ctx, files);
449
+ }
364
450
  function ensureHubSpotCliSetup(ctx, mode) {
365
451
  if (ctx.global.dryRun || ctx.global.json) return;
366
- const assessment = assessHubSpotCliSetup(ctx.projectRoot);
367
- const needsSetup = !assessment.hsCliInstalled || !assessment.configPresent || assessment.looksLikePlaceholder || !assessment.authenticated;
368
- if (!needsSetup) return;
369
- const steps = hubSpotSetupRemediation(ctx.projectRoot);
370
- const headline = mode === "local" ? "HubSpot upload/watch is not configured for this project (local preview still works)." : "HubSpot CLI is not ready for cms upload/watch in this project.";
371
452
  if (mode === "local") {
372
- ctx.logger.warn(headline);
373
- for (const step of steps) {
453
+ const files = assessHubSpotConfigFiles(ctx.projectRoot);
454
+ if (files.configPresent && !files.looksLikePlaceholder) {
455
+ return;
456
+ }
457
+ const steps2 = hubSpotLocalDevRemediation(files);
458
+ ctx.logger.warn(
459
+ "HubSpot upload/watch is not configured for this project (local preview still works)."
460
+ );
461
+ for (const step of steps2) {
374
462
  ctx.logger.info(` \u2192 ${step}`);
375
463
  }
464
+ ctx.logger.info(" \u2192 Full HubSpot CLI checks: wp doctor");
376
465
  ctx.logger.info(" \u2192 Docs: @woodsportal/hubspot-kit/docs/HUBSPOT-CLI-SETUP.md");
377
466
  return;
378
467
  }
468
+ const assessment = cmsEnsureAssessment(ctx.projectRoot);
469
+ const needsSetup = !assessment.hsCliInstalled || !assessment.configPresent || assessment.looksLikePlaceholder;
470
+ if (!needsSetup) return;
471
+ const steps = hubSpotSetupRemediation(ctx.projectRoot, assessment);
472
+ const headline = "HubSpot CLI is not ready for cms upload/watch in this project.";
379
473
  throw new WpHsError(headline, {
380
474
  exitCode: ExitCode.PREREQUISITE,
381
475
  remediation: steps.join("; ")
382
476
  });
383
477
  }
384
478
  function doctorHubSpotChecks(projectRoot) {
385
- const assessment = assessHubSpotCliSetup(projectRoot);
386
- const steps = hubSpotSetupRemediation(projectRoot);
479
+ const assessment = assessHubSpotCliSetup(projectRoot, { checkAccounts: true });
480
+ const steps = hubSpotSetupRemediation(projectRoot, assessment);
387
481
  const remediation = steps.length > 0 ? steps.join(" \xB7 ") : void 0;
388
482
  return [
389
483
  {
@@ -403,13 +497,42 @@ function doctorHubSpotChecks(projectRoot) {
403
497
  {
404
498
  name: "hubspot-auth",
405
499
  ok: assessment.authenticated,
406
- message: assessment.authenticated ? "HubSpot CLI authenticated for this project" : "HubSpot CLI not authenticated (or no portal in hubspot.config.yml)",
500
+ message: assessment.authenticated ? "HubSpot CLI authenticated for this project" : assessment.accountsHint === "timed out" ? "HubSpot CLI auth check timed out \u2014 run hs accounts list manually" : "HubSpot CLI not authenticated (or no portal in hubspot.config.yml)",
407
501
  remediation: assessment.authenticated ? void 0 : "wp auth \xB7 hs accounts list",
408
502
  severity: "warn"
409
503
  }
410
504
  ];
411
505
  }
412
506
 
507
+ // src/lib/template-dev-deps.ts
508
+ var VITE_DEV_PEER_DEPS = {
509
+ vite: "^6.1.0",
510
+ "@vitejs/plugin-react": "^4.3.4",
511
+ "babel-plugin-react-compiler": "^1.0.0",
512
+ "@tailwindcss/vite": "^4.0.6",
513
+ "@tanstack/router-plugin": "^1.121.2",
514
+ "@tanstack/react-router": "^1.121.2",
515
+ "@dnd-kit/core": "^6.3.1",
516
+ "@dnd-kit/sortable": "^10.0.0",
517
+ "@dnd-kit/utilities": "^3.2.2",
518
+ react: "^19.0.0",
519
+ "react-dom": "^19.0.0",
520
+ tailwindcss: "^4.0.6"
521
+ };
522
+ var KIT_DEV_DEP = "@woodsportal/hubspot-kit";
523
+ var KIT_DEV_DEP_VERSION = "^1.0.26";
524
+ var MODULE_CONFIG_UI_DEPS = [
525
+ "@dnd-kit/core",
526
+ "@dnd-kit/sortable",
527
+ "@dnd-kit/utilities"
528
+ ];
529
+ function templateDevDependencies() {
530
+ return {
531
+ [KIT_DEV_DEP]: KIT_DEV_DEP_VERSION,
532
+ ...VITE_DEV_PEER_DEPS
533
+ };
534
+ }
535
+
413
536
  // src/cli/commands/doctor.ts
414
537
  function checkNode() {
415
538
  const major = Number(process.versions.node.split(".")[0]);
@@ -476,13 +599,12 @@ function checkEnvFiles(ctx) {
476
599
  return ctx.config.env.tiers.map((tier) => {
477
600
  const file = tier === "dev" ? ".env.dev" : tier === "stg" ? ".env.stg" : ".env.prod";
478
601
  const path = resolve5(ctx.projectRoot, file);
479
- const example = resolve5(ctx.projectRoot, `${file}.example`);
480
602
  const ok = existsSync4(path);
481
603
  return {
482
604
  name: `env-${tier}`,
483
605
  ok,
484
606
  message: ok ? `${file} present` : `${file} missing`,
485
- remediation: ok ? void 0 : existsSync4(example) ? `cp ${file}.example ${file}` : `Create ${file}`,
607
+ remediation: ok ? void 0 : "wp setup (copies from .example templates)",
486
608
  severity: "warn"
487
609
  };
488
610
  });
@@ -495,7 +617,7 @@ function checkBuildId(ctx) {
495
617
  name: "build-id",
496
618
  ok,
497
619
  message: ok ? `${ctx.config.cdn.buildIdFile} present` : `${ctx.config.cdn.buildIdFile} missing`,
498
- remediation: ok ? void 0 : `Create ${ctx.config.cdn.buildIdFile} with your release tag (e.g. v0.0.1-dev.1)`,
620
+ remediation: ok ? void 0 : "wp setup (creates default) or set your release tag (e.g. v0.0.1-dev.1)",
499
621
  severity: "warn"
500
622
  };
501
623
  }
@@ -530,6 +652,21 @@ function checkDevEntry(ctx) {
530
652
  remediation: hasEntry ? void 0 : "Re-run wp init or restore src/main.tsx from kit templates",
531
653
  severity: "error"
532
654
  });
655
+ if (hasEntry) {
656
+ try {
657
+ const source = readFileSync4(localDev, "utf8");
658
+ const usesKitNodeBootstrap = source.includes("@woodsportal/hubspot-kit/dev/bootstrap-greenfield") || source.includes("@woodsportal/hubspot-kit/dev/bootstrap");
659
+ const usesWphsShim = source.includes(".wphs/.kit-bootstrap-greenfield") || source.includes(".wphs/.kit-bootstrap") || source.includes("@wphs/bootstrap-greenfield") || source.includes("@wphs/bootstrap");
660
+ checks.push({
661
+ name: "local-dev-bootstrap-import",
662
+ ok: !usesKitNodeBootstrap || usesWphsShim,
663
+ message: usesKitNodeBootstrap ? `${ctx.config.entries.localDev} imports kit bootstrap via node_modules (breaks React JSX in dev)` : "local dev bootstrap import uses .wphs shim",
664
+ remediation: usesKitNodeBootstrap ? `Use import from '../.wphs/.kit-bootstrap-greenfield.tsx' (greenfield) or '@wphs/bootstrap' (full portal)` : void 0,
665
+ severity: "error"
666
+ });
667
+ } catch {
668
+ }
669
+ }
533
670
  return checks;
534
671
  }
535
672
  function checkKitScripts() {
@@ -603,6 +740,28 @@ function checkAdapter(ctx) {
603
740
  severity: "warn"
604
741
  };
605
742
  }
743
+ function checkModuleConfigUiDeps(ctx) {
744
+ if (isThemeProject(ctx.config)) return [];
745
+ const pkgPath = resolve5(ctx.projectRoot, "package.json");
746
+ if (!existsSync4(pkgPath)) return [];
747
+ let deps = {};
748
+ try {
749
+ const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
750
+ deps = { ...pkg.dependencies, ...pkg.devDependencies };
751
+ } catch {
752
+ return [];
753
+ }
754
+ return MODULE_CONFIG_UI_DEPS.map((name) => {
755
+ const ok = Boolean(deps[name]);
756
+ return {
757
+ name: `module-config-${name.replace(/[@/]/g, "-")}`,
758
+ ok,
759
+ message: ok ? `${name} installed` : `${name} missing (module-config overlay)`,
760
+ remediation: ok ? void 0 : "Run yarn install after wp init, or add @dnd-kit/core @dnd-kit/sortable @dnd-kit/utilities to devDependencies",
761
+ severity: "error"
762
+ };
763
+ });
764
+ }
606
765
  function checkViteInstalled(ctx) {
607
766
  if (isThemeProject(ctx.config) && !existsSync4(resolve5(ctx.projectRoot, "vite.config.dev.mjs"))) {
608
767
  return null;
@@ -617,30 +776,80 @@ function checkViteInstalled(ctx) {
617
776
  severity: "error"
618
777
  };
619
778
  }
620
- async function runDoctor(ctx) {
621
- if (!ctx.global.dryRun) {
622
- try {
623
- runNodeScript(kitPath("scripts/ensure-wphs-workspace.mjs"), [], {
624
- cwd: ctx.projectRoot,
625
- logger: ctx.logger,
626
- dryRun: false
627
- });
628
- } catch {
779
+ async function gatherDoctorChecks(ctx) {
780
+ const checks = [];
781
+ await runCliTasks(ctx, [
782
+ {
783
+ title: "Preparing .wphs workspace",
784
+ task: async () => {
785
+ if (ctx.global.dryRun) return;
786
+ try {
787
+ runNodeScript(kitPath("scripts", "ensure-wphs-workspace.mjs"), [], {
788
+ cwd: ctx.projectRoot,
789
+ logger: ctx.logger,
790
+ dryRun: false
791
+ });
792
+ } catch {
793
+ }
794
+ }
795
+ },
796
+ {
797
+ title: "Checking Node, Vite, and kit scripts",
798
+ task: async () => {
799
+ checks.push(checkNode(), checkKitScripts(), checkDevViteConfig(ctx), ...checkDevEntry(ctx));
800
+ const viteCheck = checkViteInstalled(ctx);
801
+ if (viteCheck) checks.push(viteCheck);
802
+ checks.push(...checkModuleConfigUiDeps(ctx));
803
+ }
804
+ },
805
+ {
806
+ title: "Checking woodscli.json and project layout",
807
+ task: async () => {
808
+ checks.push(...checkConfig(ctx), ...checkWphsWorkspace(ctx));
809
+ const adapterCheck = checkAdapter(ctx);
810
+ if (adapterCheck) checks.push(adapterCheck);
811
+ }
812
+ },
813
+ {
814
+ title: "Checking HubSpot CLI (this can take a few seconds)",
815
+ task: async () => {
816
+ checks.push(...doctorHubSpotChecks(ctx.projectRoot));
817
+ }
818
+ },
819
+ {
820
+ title: "Checking environment and CDN files",
821
+ task: async () => {
822
+ const buildIdCheck = checkBuildId(ctx);
823
+ if (buildIdCheck) checks.push(buildIdCheck);
824
+ checks.push(...checkEnvFiles(ctx));
825
+ }
826
+ }
827
+ ]);
828
+ return checks.filter((c) => c !== null);
829
+ }
830
+ function printDoctorResults(ctx, checks) {
831
+ if (ctx.global.json) return;
832
+ const interactive = isInteractiveCli(ctx);
833
+ const verbose = ctx.global.verbose;
834
+ const showPassing = !interactive || verbose;
835
+ if (interactive) {
836
+ process.stdout.write("\n");
837
+ }
838
+ for (const check of checks) {
839
+ if (check.ok && !showPassing) continue;
840
+ if (check.ok) {
841
+ ctx.logger.success(check.message);
842
+ } else if (check.severity === "warn") {
843
+ ctx.logger.warn(check.message);
844
+ if (check.remediation) ctx.logger.info(` \u2192 ${check.remediation}`);
845
+ } else {
846
+ ctx.logger.error(check.message);
847
+ if (check.remediation) ctx.logger.info(` \u2192 ${check.remediation}`);
629
848
  }
630
849
  }
631
- const checks = [
632
- checkNode(),
633
- checkKitScripts(),
634
- checkDevViteConfig(ctx),
635
- ...checkDevEntry(ctx),
636
- checkViteInstalled(ctx),
637
- ...checkConfig(ctx),
638
- ...checkWphsWorkspace(ctx),
639
- checkAdapter(ctx),
640
- ...doctorHubSpotChecks(ctx.projectRoot),
641
- checkBuildId(ctx),
642
- ...checkEnvFiles(ctx)
643
- ].filter((c) => c !== null);
850
+ }
851
+ async function runDoctor(ctx) {
852
+ const checks = await gatherDoctorChecks(ctx);
644
853
  const failed = checks.filter((c) => !c.ok && (c.severity ?? "error") === "error");
645
854
  const warnings = checks.filter((c) => !c.ok && c.severity === "warn");
646
855
  if (ctx.global.json) {
@@ -651,16 +860,10 @@ async function runDoctor(ctx) {
651
860
  checks
652
861
  });
653
862
  } else {
654
- for (const check of checks) {
655
- if (check.ok) {
656
- ctx.logger.success(check.message);
657
- } else if (check.severity === "warn") {
658
- ctx.logger.warn(check.message);
659
- if (check.remediation) ctx.logger.info(` \u2192 ${check.remediation}`);
660
- } else {
661
- ctx.logger.error(check.message);
662
- if (check.remediation) ctx.logger.info(` \u2192 ${check.remediation}`);
663
- }
863
+ printDoctorResults(ctx, checks);
864
+ if (!ctx.global.quiet && failed.length === 0) {
865
+ const summary = warnings.length > 0 ? `${checks.length} checks passed \xB7 ${warnings.length} warning(s)` : `${checks.length} checks passed`;
866
+ printCommandDone(ctx, summary);
664
867
  }
665
868
  }
666
869
  if (failed.length > 0) {
@@ -698,43 +901,43 @@ async function runConfigValidate(ctx) {
698
901
 
699
902
  // src/cli/commands/build.ts
700
903
  import { resolve as resolve6 } from "path";
904
+
905
+ // src/lib/build-tier.ts
906
+ var DEFAULT_BUILD_TIER = "dev";
907
+ function parseBuildTier(value) {
908
+ if (value === "dev" || value === "stg" || value === "prod") return value;
909
+ return DEFAULT_BUILD_TIER;
910
+ }
911
+
912
+ // src/cli/commands/build.ts
701
913
  function tierToStgFlag(tier) {
702
914
  return tier === "dev" || tier === "stg" ? ["--stg"] : [];
703
915
  }
704
- async function runBuild(ctx, options) {
705
- const tier = options.tier ?? "prod";
706
- const env = { ...process.env, WP_HS_PROJECT_ROOT: ctx.projectRoot };
916
+ function buildTasks(ctx, options, run) {
917
+ const tier = options.tier ?? DEFAULT_BUILD_TIER;
707
918
  const isTheme = isThemeProject(ctx.config);
708
919
  const useCdn = usesCdnPipeline(ctx.config) && !isTheme;
709
- const execOpts = {
710
- cwd: ctx.projectRoot,
711
- env,
712
- logger: ctx.logger,
713
- dryRun: ctx.global.dryRun
714
- };
715
- const run = (script, args = []) => {
716
- runNodeScript(kitPath("scripts", script), args, execOpts);
717
- };
718
- try {
719
- if (isTheme && !options.cdnOnly) {
720
- ctx.logger.info("Theme project \u2014 skipping CDN/module.js build (use wp upload for theme assets)");
721
- if (ctx.global.json) {
722
- ctx.logger.json({ command: "build", tier, ok: true, projectType: ctx.config.projectType });
723
- } else {
724
- ctx.logger.success("Theme build complete (no-op)");
920
+ const tasks2 = [];
921
+ if (isTheme && !options.cdnOnly) {
922
+ return tasks2;
923
+ }
924
+ if (ctx.config.projectType === "module-monolith" && !options.cdnOnly) {
925
+ tasks2.push({
926
+ title: `Building monolith module (${tier})`,
927
+ task: async () => {
928
+ run("run-vite-build.mjs", [
929
+ resolve6(ctx.projectRoot, "vite.config.monolith.mjs"),
930
+ ...tierToStgFlag(tier)
931
+ ]);
932
+ run("run-hubspot-module-postbuild.mjs");
725
933
  }
726
- return ExitCode.SUCCESS;
727
- }
728
- if (ctx.config.projectType === "module-monolith" && !options.cdnOnly) {
729
- ctx.logger.info(`Building monolith module (${tier})\u2026`);
730
- run("run-vite-build.mjs", [
731
- resolve6(ctx.projectRoot, "vite.config.monolith.mjs"),
732
- ...tierToStgFlag(tier)
733
- ]);
734
- run("run-hubspot-module-postbuild.mjs");
735
- } else {
736
- if (!options.hubspotOnly && useCdn) {
737
- ctx.logger.info(`Building CDN (${tier})\u2026`);
934
+ });
935
+ return tasks2;
936
+ }
937
+ if (!options.hubspotOnly && useCdn) {
938
+ tasks2.push({
939
+ title: `Building CDN (${tier})`,
940
+ task: async () => {
738
941
  if (options.cdnEsm) {
739
942
  run("run-vite-build.mjs", [
740
943
  resolve6(ctx.projectRoot, "vite.config.cdn-esm.mjs"),
@@ -745,19 +948,64 @@ async function runBuild(ctx, options) {
745
948
  }
746
949
  run("generate-cdn-manifest.mjs");
747
950
  }
748
- if (!options.cdnOnly) {
749
- ctx.logger.info(`Building HubSpot module (${tier})\u2026`);
951
+ });
952
+ }
953
+ if (!options.cdnOnly) {
954
+ tasks2.push({
955
+ title: `Building HubSpot module (${tier})`,
956
+ task: async () => {
750
957
  run("run-vite-build.mjs", [
751
958
  resolve6(ctx.projectRoot, "vite.config.hubspot.mjs"),
752
959
  ...tierToStgFlag(tier)
753
960
  ]);
754
961
  run("run-hubspot-module-postbuild.mjs");
755
962
  }
963
+ });
964
+ }
965
+ if (!options.cdnOnly && !options.hubspotOnly) {
966
+ tasks2.push({
967
+ title: "Running bundle checks",
968
+ task: async () => {
969
+ run("check-hubspot-bundle-size.mjs");
970
+ run("write-audit-artifacts.mjs");
971
+ }
972
+ });
973
+ }
974
+ return tasks2;
975
+ }
976
+ async function runBuild(ctx, options) {
977
+ const tier = options.tier ?? DEFAULT_BUILD_TIER;
978
+ const env = { ...process.env, WP_HS_PROJECT_ROOT: ctx.projectRoot };
979
+ const isTheme = isThemeProject(ctx.config);
980
+ const execOpts = {
981
+ cwd: ctx.projectRoot,
982
+ env,
983
+ logger: ctx.logger,
984
+ dryRun: ctx.global.dryRun
985
+ };
986
+ const run = (script, args = []) => {
987
+ runNodeScript(kitPath("scripts", script), args, execOpts);
988
+ };
989
+ try {
990
+ if (isTheme && !options.cdnOnly) {
991
+ if (ctx.global.json) {
992
+ ctx.logger.json({ command: "build", tier, ok: true, projectType: ctx.config.projectType });
993
+ } else if (!options.progress) {
994
+ ctx.logger.info("Theme project \u2014 skipping CDN/module.js build (use wp upload for theme assets)");
995
+ printCommandDone(ctx, "Theme build complete (no-op)");
996
+ } else {
997
+ await runCliTasks(
998
+ ctx,
999
+ [{ title: "Theme project (no module build)", task: async () => {
1000
+ } }],
1001
+ options
1002
+ );
1003
+ printCommandDone(ctx, "Theme build complete (no-op)");
1004
+ }
1005
+ return ExitCode.SUCCESS;
756
1006
  }
757
- if (!options.cdnOnly && !options.hubspotOnly) {
758
- run("check-hubspot-bundle-size.mjs");
759
- run("write-audit-artifacts.mjs");
760
- }
1007
+ const tasks2 = buildTasks(ctx, options, run);
1008
+ await runCliTasks(ctx, tasks2, options);
761
1009
  if (ctx.global.json) {
762
1010
  ctx.logger.json({
763
1011
  command: "build",
@@ -766,7 +1014,7 @@ async function runBuild(ctx, options) {
766
1014
  outDir: resolve6(ctx.projectRoot, ctx.config.module.outDir)
767
1015
  });
768
1016
  } else {
769
- ctx.logger.success("Build complete");
1017
+ printCommandDone(ctx, "Build complete");
770
1018
  }
771
1019
  return ExitCode.SUCCESS;
772
1020
  } catch (error) {
@@ -778,22 +1026,142 @@ async function runBuild(ctx, options) {
778
1026
 
779
1027
  // src/cli/commands/dev.ts
780
1028
  import { existsSync as existsSync6 } from "fs";
781
- import { resolve as resolve8 } from "path";
1029
+ import { resolve as resolve9 } from "path";
782
1030
  import { spawn } from "child_process";
783
1031
 
1032
+ // src/lib/cli-banner.ts
1033
+ import pc3 from "picocolors";
1034
+
1035
+ // src/lib/kit-version.ts
1036
+ import { readFileSync as readFileSync5 } from "fs";
1037
+ import { resolve as resolve7 } from "path";
1038
+ var cachedVersion = null;
1039
+ function readKitVersion() {
1040
+ if (cachedVersion) {
1041
+ return cachedVersion;
1042
+ }
1043
+ try {
1044
+ const pkg = JSON.parse(readFileSync5(resolve7(getKitRoot(), "package.json"), "utf8"));
1045
+ cachedVersion = pkg.version ?? "0.0.0";
1046
+ } catch {
1047
+ cachedVersion = "0.0.0";
1048
+ }
1049
+ return cachedVersion;
1050
+ }
1051
+
1052
+ // src/lib/cli-banner.ts
1053
+ var WOODSCLI_ASCII_BANNER = `
1054
+ __ __ _ _____ _ _____
1055
+ \\ \\ / / | | / ____| | |_ _|
1056
+ \\ \\ /\\ / /__ ___ __| |___| | | | | |
1057
+ \\ \\/ \\/ / _ \\ / _ \\ / _\` / __| | | | | |
1058
+ \\ /\\ / (_) | (_) | (_| \\__ \\ |____| |____ _| |_
1059
+ \\/ \\/ \\___/ \\___/ \\__,_|___/\\_____|______|_____|
1060
+ `.trimEnd();
1061
+ var RULE = "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500";
1062
+ function shouldPrintCliBanner(global) {
1063
+ if (global.json || global.quiet || global.ci || global.dryRun) {
1064
+ return false;
1065
+ }
1066
+ return process.stdout.isTTY === true;
1067
+ }
1068
+ function useCliColor() {
1069
+ if (process.env.NO_COLOR !== void 0) return false;
1070
+ if (process.env.CI === "true") return false;
1071
+ return process.stdout.isTTY === true;
1072
+ }
1073
+ function formatProjectLabel(config) {
1074
+ if (isThemeProject(config)) {
1075
+ return `${config.theme?.name ?? "theme"} \xB7 theme-starter`;
1076
+ }
1077
+ const router = config.scaffold?.router;
1078
+ const routerNote = router && router !== "hash" ? ` \xB7 router ${router}` : " \xB7 hash router";
1079
+ return `${config.module.name} \xB7 ${config.projectType}${routerNote}`;
1080
+ }
1081
+ function section(color, title, body) {
1082
+ const bold = (text2) => color ? pc3.bold(text2) : text2;
1083
+ return ` ${bold(title)}
1084
+ ${body}`;
1085
+ }
1086
+ function printWoodsCliDevBanner(options) {
1087
+ if (!shouldPrintCliBanner(options.global)) {
1088
+ return;
1089
+ }
1090
+ const color = useCliColor();
1091
+ const cyan = (text2) => color ? pc3.cyan(text2) : text2;
1092
+ const dim = (text2) => color ? pc3.dim(text2) : text2;
1093
+ const bold = (text2) => color ? pc3.bold(text2) : text2;
1094
+ const yellow = (text2) => color ? pc3.yellow(text2) : text2;
1095
+ const version = options.version ?? readKitVersion();
1096
+ const project = formatProjectLabel(options.config);
1097
+ const isTheme = isThemeProject(options.config);
1098
+ const localUrl = `http://localhost:${options.port}/`;
1099
+ const docsRoot = "node_modules/@woodsportal/hubspot-kit/docs";
1100
+ const header = [
1101
+ color ? WOODSCLI_ASCII_BANNER.split("\n").map((line) => cyan(bold(line))).join("\n") : WOODSCLI_ASCII_BANNER,
1102
+ ` ${bold("HubSpot kit")}${dim(` v${version}`)} ${yellow("\xB7 dev")}`
1103
+ ].join("\n");
1104
+ const outputLine = isTheme ? " Vite HMR + theme field overlay (Ctrl+Shift+Z)" : " Vite HMR + module-config overlay (Ctrl+Shift+Z)";
1105
+ const quickChecks = isTheme ? ` wp doctor prerequisites + HubSpot CLI
1106
+ wp dev alias: wp local
1107
+ wp upload cms theme upload` : ` wp doctor prerequisites + HubSpot CLI
1108
+ wp setup copy missing config / .env templates
1109
+ wp build module.js / CDN (tier defaults dev)
1110
+ wp upload cms module upload`;
1111
+ const tips = [
1112
+ dim(RULE),
1113
+ section(color, "Output", ` ${outputLine}`),
1114
+ "",
1115
+ section(color, "Project", ` ${project}`),
1116
+ "",
1117
+ section(color, "Preview", ` ${color ? cyan(localUrl) : localUrl}`),
1118
+ "",
1119
+ section(
1120
+ color,
1121
+ "Overlay",
1122
+ ` Ctrl+Shift+Z ${isTheme ? "theme" : "module"} fields ${dim("(Mac \u2303\u21E7Z)")}`
1123
+ ),
1124
+ "",
1125
+ section(
1126
+ color,
1127
+ "Workspace",
1128
+ ` ${dim(".wphs/")} \xB7 ${options.config.dev.fieldsJson}`
1129
+ ),
1130
+ "",
1131
+ section(color, "Quick checks", `
1132
+ ${quickChecks}`),
1133
+ "",
1134
+ section(
1135
+ color,
1136
+ "Docs",
1137
+ ` ${docsRoot}/CLI-REFERENCE.md \xB7 ${docsRoot}/MODULES.md`
1138
+ ),
1139
+ dim(RULE)
1140
+ ].join("\n");
1141
+ process.stdout.write(`
1142
+ ${header}
1143
+
1144
+ ${tips}
1145
+
1146
+ `);
1147
+ process.stdout.write(`${dim("Starting Vite dev\u2026")}
1148
+
1149
+ `);
1150
+ }
1151
+
784
1152
  // src/vite/resolve-dev-config.ts
785
1153
  import { existsSync as existsSync5 } from "fs";
786
- import { resolve as resolve7 } from "path";
1154
+ import { resolve as resolve8 } from "path";
787
1155
  var CONSUMER_DEV_CONFIG = "vite.config.dev.mjs";
788
1156
  function resolveDevViteConfigPath(projectRoot) {
789
- const consumerConfig = resolve7(projectRoot, CONSUMER_DEV_CONFIG);
1157
+ const consumerConfig = resolve8(projectRoot, CONSUMER_DEV_CONFIG);
790
1158
  if (existsSync5(consumerConfig)) {
791
1159
  return consumerConfig;
792
1160
  }
793
1161
  return kitPath("vite", "vite.config.dev.mjs");
794
1162
  }
795
1163
  function resolveProjectViteBin(projectRoot) {
796
- const localBin = resolve7(projectRoot, "node_modules", ".bin", "vite");
1164
+ const localBin = resolve8(projectRoot, "node_modules", ".bin", "vite");
797
1165
  if (existsSync5(localBin)) {
798
1166
  return localBin;
799
1167
  }
@@ -802,7 +1170,6 @@ function resolveProjectViteBin(projectRoot) {
802
1170
 
803
1171
  // src/cli/commands/dev.ts
804
1172
  async function runDev(ctx, options) {
805
- ensureHubSpotCliSetup(ctx, "local");
806
1173
  const port = options.port ?? 3e3;
807
1174
  const env = {
808
1175
  ...process.env,
@@ -820,13 +1187,18 @@ async function runDev(ctx, options) {
820
1187
  remediation: "Run yarn install or npm install. If this is an older wp init project, add vite and peers to devDependencies (run wp init again or see @woodsportal/hubspot-kit templates)."
821
1188
  });
822
1189
  }
823
- const consumerConfig = resolve8(ctx.projectRoot, "vite.config.dev.mjs");
1190
+ const consumerConfig = resolve9(ctx.projectRoot, "vite.config.dev.mjs");
824
1191
  if (!existsSync6(consumerConfig)) {
825
1192
  throw new WpHsError("vite.config.dev.mjs is missing in the project root.", {
826
1193
  exitCode: ExitCode.USER_ERROR,
827
1194
  remediation: "Add vite.config.dev.mjs (see @woodsportal/hubspot-kit templates/module-hybrid-cdn)."
828
1195
  });
829
1196
  }
1197
+ printWoodsCliDevBanner({
1198
+ config: ctx.config,
1199
+ port,
1200
+ global: ctx.global
1201
+ });
830
1202
  return new Promise((resolvePromise) => {
831
1203
  const child = spawn(
832
1204
  viteBin,
@@ -837,6 +1209,7 @@ async function runDev(ctx, options) {
837
1209
  stdio: "inherit"
838
1210
  }
839
1211
  );
1212
+ setImmediate(() => ensureHubSpotCliSetup(ctx, "local"));
840
1213
  child.on("exit", (code) => {
841
1214
  resolvePromise(code ?? ExitCode.USER_ERROR);
842
1215
  });
@@ -844,11 +1217,17 @@ async function runDev(ctx, options) {
844
1217
  }
845
1218
 
846
1219
  // src/cli/commands/deploy.ts
847
- import { resolve as resolve10 } from "path";
1220
+ import { resolve as resolve11 } from "path";
848
1221
 
849
1222
  // src/cli/commands/publish.ts
850
1223
  async function runPublish(ctx, options) {
851
- const tier = options.tier ?? "prod";
1224
+ if (!usesCdnPipeline(ctx.config)) {
1225
+ throw new WpHsError("This project does not use the CDN pipeline.", {
1226
+ exitCode: ExitCode.USER_ERROR,
1227
+ remediation: "module-monolith / cdn.mode none: use wp build && wp upload (or wp deploy without CDN publish)."
1228
+ });
1229
+ }
1230
+ const tier = options.tier ?? DEFAULT_BUILD_TIER;
852
1231
  const env = { ...process.env, WP_HS_PROJECT_ROOT: ctx.projectRoot };
853
1232
  const execOpts = {
854
1233
  cwd: ctx.projectRoot,
@@ -859,16 +1238,32 @@ async function runPublish(ctx, options) {
859
1238
  const verifyFlag = tier === "prod" ? ["--prod"] : ["--stg"];
860
1239
  const publishArgs = options.dryRun ? ["--dry-run"] : [];
861
1240
  try {
862
- if (!options.dryRun) {
863
- ctx.logger.info(`Verifying CDN API endpoint (${tier})\u2026`);
864
- runNodeScript(kitPath("scripts", "verify-cdn-api-endpoint.mjs"), verifyFlag, execOpts);
865
- }
866
- ctx.logger.info(options.dryRun ? "CDN publish dry-run\u2026" : `Publishing CDN (${tier})\u2026`);
867
- runNodeScript(kitPath("scripts", "publish-cdn-github.mjs"), publishArgs, execOpts);
1241
+ await runCliTasks(
1242
+ ctx,
1243
+ [
1244
+ {
1245
+ title: `Verifying CDN API endpoint (${tier})`,
1246
+ task: async () => {
1247
+ if (options.dryRun) return;
1248
+ runNodeScript(kitPath("scripts", "verify-cdn-api-endpoint.mjs"), verifyFlag, execOpts);
1249
+ }
1250
+ },
1251
+ {
1252
+ title: options.dryRun ? `CDN publish dry-run (${tier})` : `Publishing CDN (${tier})`,
1253
+ task: async () => {
1254
+ runNodeScript(kitPath("scripts", "publish-cdn-github.mjs"), publishArgs, execOpts);
1255
+ }
1256
+ }
1257
+ ],
1258
+ options
1259
+ );
868
1260
  if (ctx.global.json) {
869
1261
  ctx.logger.json({ command: "publish", tier, dryRun: Boolean(options.dryRun), ok: true });
870
1262
  } else {
871
- ctx.logger.success(options.dryRun ? "CDN publish dry-run complete" : "CDN publish complete");
1263
+ printCommandDone(
1264
+ ctx,
1265
+ options.dryRun ? "CDN publish dry-run complete" : "CDN publish complete"
1266
+ );
872
1267
  }
873
1268
  return ExitCode.SUCCESS;
874
1269
  } catch (error) {
@@ -880,10 +1275,32 @@ async function runPublish(ctx, options) {
880
1275
  }
881
1276
 
882
1277
  // src/cli/commands/upload.ts
883
- import { resolve as resolve9 } from "path";
1278
+ import { resolve as resolve10 } from "path";
1279
+ async function uploadModuleOrTheme(ctx) {
1280
+ if (isThemeProject(ctx.config)) {
1281
+ const theme = ctx.config.theme;
1282
+ if (!theme) {
1283
+ throw new WpHsError("theme block missing in woodscli.json", { exitCode: ExitCode.USER_ERROR });
1284
+ }
1285
+ const srcDir = resolve10(ctx.projectRoot, theme.srcDir);
1286
+ runCommand("hs", ["cms", "upload", srcDir, theme.uploadPath], {
1287
+ cwd: ctx.projectRoot,
1288
+ logger: ctx.logger,
1289
+ dryRun: ctx.global.dryRun
1290
+ });
1291
+ return `Uploaded theme ${srcDir} \u2192 ${theme.uploadPath}`;
1292
+ }
1293
+ const outDir = resolve10(ctx.projectRoot, ctx.config.module.outDir);
1294
+ const uploadPath = ctx.config.module.uploadPath;
1295
+ runCommand("hs", ["cms", "upload", outDir, uploadPath], {
1296
+ cwd: ctx.projectRoot,
1297
+ logger: ctx.logger,
1298
+ dryRun: ctx.global.dryRun
1299
+ });
1300
+ return `Uploaded ${outDir} \u2192 ${uploadPath}`;
1301
+ }
884
1302
  async function runUpload(ctx, options = {}) {
885
- ensureHubSpotCliSetup(ctx, "cms");
886
- const tier = options.tier ?? "prod";
1303
+ const tier = options.tier ?? DEFAULT_BUILD_TIER;
887
1304
  if (tier === "prod" && !ctx.global.yes && !ctx.global.ci) {
888
1305
  const confirmed = process.env.WP_HS_CONFIRM_PROD === "1";
889
1306
  if (!confirmed) {
@@ -893,39 +1310,25 @@ async function runUpload(ctx, options = {}) {
893
1310
  });
894
1311
  }
895
1312
  }
896
- if (isThemeProject(ctx.config)) {
897
- const theme = ctx.config.theme;
898
- if (!theme) {
899
- throw new WpHsError("theme block missing in woodscli.json", { exitCode: ExitCode.USER_ERROR });
900
- }
901
- const srcDir = resolve9(ctx.projectRoot, theme.srcDir);
902
- try {
903
- runCommand("hs", ["cms", "upload", srcDir, theme.uploadPath], {
904
- cwd: ctx.projectRoot,
905
- logger: ctx.logger,
906
- dryRun: ctx.global.dryRun
907
- });
908
- if (!ctx.global.json && !ctx.global.quiet) {
909
- ctx.logger.success(`Uploaded theme ${srcDir} \u2192 ${theme.uploadPath}`);
910
- }
911
- return ExitCode.SUCCESS;
912
- } catch (error) {
913
- throw new WpHsError("HubSpot theme upload failed", {
914
- exitCode: ExitCode.PUBLISH_FAILED,
915
- cause: error
916
- });
917
- }
1313
+ if (!options.skipPrereqCheck) {
1314
+ ensureHubSpotCliSetup(ctx, "cms");
918
1315
  }
919
- const outDir = resolve9(ctx.projectRoot, ctx.config.module.outDir);
920
- const uploadPath = ctx.config.module.uploadPath;
921
1316
  try {
922
- runCommand("hs", ["cms", "upload", outDir, uploadPath], {
923
- cwd: ctx.projectRoot,
924
- logger: ctx.logger,
925
- dryRun: ctx.global.dryRun
926
- });
927
- if (!ctx.global.json && !ctx.global.quiet) {
928
- ctx.logger.success(`Uploaded ${outDir} \u2192 ${uploadPath}`);
1317
+ let successMessage = "";
1318
+ await runCliTasks(
1319
+ ctx,
1320
+ [
1321
+ {
1322
+ title: isThemeProject(ctx.config) ? "Uploading theme to HubSpot" : "Uploading module to HubSpot",
1323
+ task: async () => {
1324
+ successMessage = await uploadModuleOrTheme(ctx);
1325
+ }
1326
+ }
1327
+ ],
1328
+ options
1329
+ );
1330
+ if (!ctx.global.json && !options.quiet) {
1331
+ printCommandDone(ctx, successMessage);
929
1332
  }
930
1333
  return ExitCode.SUCCESS;
931
1334
  } catch (error) {
@@ -940,7 +1343,7 @@ async function runUpload(ctx, options = {}) {
940
1343
  // src/cli/commands/deploy.ts
941
1344
  async function runDeploy(ctx, options) {
942
1345
  ensureHubSpotCliSetup(ctx, "cms");
943
- const tier = options.tier ?? "prod";
1346
+ const tier = options.tier ?? DEFAULT_BUILD_TIER;
944
1347
  const env = { ...process.env, WP_HS_PROJECT_ROOT: ctx.projectRoot };
945
1348
  const execOpts = {
946
1349
  cwd: ctx.projectRoot,
@@ -948,24 +1351,67 @@ async function runDeploy(ctx, options) {
948
1351
  logger: ctx.logger,
949
1352
  dryRun: ctx.global.dryRun
950
1353
  };
951
- await runBuild(ctx, { tier, cdnOnly: true });
952
- if (!options.skipPublish) {
953
- await runPublish(ctx, { tier });
954
- }
955
- await runBuild(ctx, { tier, hubspotOnly: true });
956
- runNodeScript(kitPath("scripts", "check-hubspot-bundle-size.mjs"), [], execOpts);
957
- runNodeScript(kitPath("scripts", "write-audit-artifacts.mjs"), [], execOpts);
958
- await runUpload(ctx, { tier });
1354
+ const useCdn = usesCdnPipeline(ctx.config) && !isThemeProject(ctx.config);
1355
+ const nested = { progress: false };
1356
+ const tasks2 = [];
1357
+ if (useCdn) {
1358
+ tasks2.push({
1359
+ title: `Building CDN (${tier})`,
1360
+ task: async () => {
1361
+ await runBuild(ctx, { tier, cdnOnly: true, ...nested });
1362
+ }
1363
+ });
1364
+ if (!options.skipPublish) {
1365
+ tasks2.push({
1366
+ title: `Publishing CDN (${tier})`,
1367
+ task: async () => {
1368
+ await runPublish(ctx, { tier, ...nested });
1369
+ }
1370
+ });
1371
+ }
1372
+ tasks2.push({
1373
+ title: `Building HubSpot module (${tier})`,
1374
+ task: async () => {
1375
+ await runBuild(ctx, { tier, hubspotOnly: true, ...nested });
1376
+ }
1377
+ });
1378
+ } else {
1379
+ tasks2.push({
1380
+ title: `Building module (${tier})`,
1381
+ task: async () => {
1382
+ await runBuild(ctx, { tier, ...nested });
1383
+ }
1384
+ });
1385
+ }
1386
+ tasks2.push(
1387
+ {
1388
+ title: "Running bundle checks",
1389
+ task: async () => {
1390
+ runNodeScript(kitPath("scripts", "check-hubspot-bundle-size.mjs"), [], execOpts);
1391
+ runNodeScript(kitPath("scripts", "write-audit-artifacts.mjs"), [], execOpts);
1392
+ }
1393
+ },
1394
+ {
1395
+ title: "Uploading to HubSpot",
1396
+ task: async () => {
1397
+ await runUpload(ctx, { tier, ...nested, quiet: true, skipPrereqCheck: true });
1398
+ }
1399
+ }
1400
+ );
1401
+ await runCliTasks(ctx, tasks2, options);
959
1402
  if (!ctx.global.json) {
960
- ctx.logger.success(`Deploy complete (${resolve10(ctx.projectRoot, ctx.config.module.outDir)} \u2192 ${ctx.config.module.uploadPath})`);
1403
+ printCommandDone(
1404
+ ctx,
1405
+ `Deploy complete (${resolve11(ctx.projectRoot, ctx.config.module.outDir)} \u2192 ${ctx.config.module.uploadPath})`
1406
+ );
961
1407
  }
962
1408
  return ExitCode.SUCCESS;
963
1409
  }
964
1410
 
965
1411
  // src/cli/commands/audit.ts
966
1412
  import { createHash } from "crypto";
967
- import { cpSync, existsSync as existsSync7, mkdirSync, readFileSync as readFileSync4, readdirSync, rmSync, statSync } from "fs";
968
- import { resolve as resolve11, join as join2 } from "path";
1413
+ import { cpSync, existsSync as existsSync7, mkdirSync, readFileSync as readFileSync6, readdirSync, rmSync, statSync } from "fs";
1414
+ import { resolve as resolve12, join as join2 } from "path";
969
1415
  var FORBIDDEN_SHIP_TOKENS = [
970
1416
  "ModuleConfigOverlay",
971
1417
  "module-config-store",
@@ -974,7 +1420,7 @@ var FORBIDDEN_SHIP_TOKENS = [
974
1420
  "dev.module-config"
975
1421
  ];
976
1422
  function hashFile(path) {
977
- return createHash("sha256").update(readFileSync4(path)).digest("hex");
1423
+ return createHash("sha256").update(readFileSync6(path)).digest("hex");
978
1424
  }
979
1425
  function walkFiles(dir) {
980
1426
  if (!existsSync7(dir)) return [];
@@ -993,14 +1439,14 @@ function walkFiles(dir) {
993
1439
  async function runAuditShipBundle(ctx) {
994
1440
  const root = ctx.projectRoot;
995
1441
  const targets = [
996
- resolve11(root, "dist/cdn"),
997
- resolve11(root, ctx.config.module.outDir)
1442
+ resolve12(root, "dist/cdn"),
1443
+ resolve12(root, ctx.config.module.outDir)
998
1444
  ];
999
1445
  const violations = [];
1000
1446
  for (const dir of targets) {
1001
1447
  for (const file of walkFiles(dir)) {
1002
1448
  if (!/\.(js|css|html)$/i.test(file)) continue;
1003
- const content = readFileSync4(file, "utf8");
1449
+ const content = readFileSync6(file, "utf8");
1004
1450
  for (const token of FORBIDDEN_SHIP_TOKENS) {
1005
1451
  if (content.includes(token)) {
1006
1452
  violations.push(`${file}: contains forbidden token "${token}"`);
@@ -1027,8 +1473,8 @@ async function runAuditBaselineCapture(ctx, options) {
1027
1473
  const root = ctx.projectRoot;
1028
1474
  const baseline = options.baselineDir ?? defaultBaselineDir(root, ctx.config);
1029
1475
  const sources = [
1030
- { label: "cdn", path: resolve11(root, "dist/cdn") },
1031
- { label: "hubspot", path: resolve11(root, ctx.config.module.outDir) }
1476
+ { label: "cdn", path: resolve12(root, "dist/cdn") },
1477
+ { label: "hubspot", path: resolve12(root, ctx.config.module.outDir) }
1032
1478
  ];
1033
1479
  if (ctx.global.dryRun) {
1034
1480
  ctx.logger.info(`[dry-run] Would capture baseline to ${baseline}`);
@@ -1041,7 +1487,7 @@ async function runAuditBaselineCapture(ctx, options) {
1041
1487
  remediation: "Run wp build before wp audit baseline --capture."
1042
1488
  });
1043
1489
  }
1044
- const target = resolve11(baseline, label);
1490
+ const target = resolve12(baseline, label);
1045
1491
  rmSync(target, { recursive: true, force: true });
1046
1492
  mkdirSync(baseline, { recursive: true });
1047
1493
  cpSync(path, target, { recursive: true });
@@ -1057,12 +1503,12 @@ async function runAuditParity(ctx, options) {
1057
1503
  const root = ctx.projectRoot;
1058
1504
  const baseline = options.baselineDir ?? defaultBaselineDir(root, ctx.config);
1059
1505
  const currentDirs = [
1060
- { label: "cdn", path: resolve11(root, "dist/cdn") },
1061
- { label: "hubspot", path: resolve11(root, ctx.config.module.outDir) }
1506
+ { label: "cdn", path: resolve12(root, "dist/cdn") },
1507
+ { label: "hubspot", path: resolve12(root, ctx.config.module.outDir) }
1062
1508
  ];
1063
1509
  const mismatches = [];
1064
1510
  for (const { label, path } of currentDirs) {
1065
- const baselinePath = resolve11(baseline, label);
1511
+ const baselinePath = resolve12(baseline, label);
1066
1512
  if (!existsSync7(baselinePath)) {
1067
1513
  ctx.logger.warn(`No baseline at ${baselinePath} \u2014 skipping ${label} parity`);
1068
1514
  continue;
@@ -1078,7 +1524,7 @@ async function runAuditParity(ctx, options) {
1078
1524
  }
1079
1525
  for (const file of baselineFiles) {
1080
1526
  const rel = file.replace(baselinePath, "");
1081
- const current = resolve11(path, rel.slice(1));
1527
+ const current = resolve12(path, rel.slice(1));
1082
1528
  if (!existsSync7(current)) continue;
1083
1529
  const bh = hashFile(file);
1084
1530
  const ch = hashFile(current);
@@ -1106,13 +1552,13 @@ var SECRET_PATTERNS = [
1106
1552
  ];
1107
1553
  async function runAuditSecrets(ctx) {
1108
1554
  const root = ctx.projectRoot;
1109
- const scanRoots = ["src", "public", ".wphs/config"].map((d) => resolve11(root, d));
1555
+ const scanRoots = ["src", "public", ".wphs/config"].map((d) => resolve12(root, d));
1110
1556
  const findings = [];
1111
1557
  for (const dir of scanRoots) {
1112
1558
  for (const file of walkFiles(dir)) {
1113
1559
  if (!/\.(ts|tsx|js|jsx|json|env|yml|yaml|html)$/i.test(file)) continue;
1114
1560
  if (file.includes("node_modules")) continue;
1115
- const content = readFileSync4(file, "utf8");
1561
+ const content = readFileSync6(file, "utf8");
1116
1562
  for (const pattern of SECRET_PATTERNS) {
1117
1563
  pattern.lastIndex = 0;
1118
1564
  if (pattern.test(content)) {
@@ -1138,29 +1584,340 @@ async function runAuditSecrets(ctx) {
1138
1584
  }
1139
1585
 
1140
1586
  // src/cli/commands/init.ts
1141
- import { cpSync as cpSync2, existsSync as existsSync8, mkdirSync as mkdirSync2, readdirSync as readdirSync2, readFileSync as readFileSync5, writeFileSync } from "fs";
1142
- import { basename, resolve as resolve12 } from "path";
1143
- import * as p from "@clack/prompts";
1587
+ import { cpSync as cpSync2, existsSync as existsSync10, mkdirSync as mkdirSync3, readdirSync as readdirSync2, readFileSync as readFileSync8, writeFileSync as writeFileSync3 } from "fs";
1588
+ import { basename as basename2, resolve as resolve15 } from "path";
1589
+ import * as p2 from "@clack/prompts";
1144
1590
 
1145
- // src/lib/template-dev-deps.ts
1146
- var VITE_DEV_PEER_DEPS = {
1147
- vite: "^6.1.0",
1148
- "@vitejs/plugin-react": "^4.3.4",
1149
- "babel-plugin-react-compiler": "^1.0.0",
1150
- "@tailwindcss/vite": "^4.0.6",
1151
- "@tanstack/router-plugin": "^1.121.2",
1152
- react: "^19.0.0",
1153
- "react-dom": "^19.0.0",
1154
- tailwindcss: "^4.0.6"
1155
- };
1156
- var KIT_DEV_DEP = "@woodsportal/hubspot-kit";
1157
- var KIT_DEV_DEP_VERSION = "^1.0.5";
1158
- function templateDevDependencies() {
1591
+ // src/lib/init-router-scaffold.ts
1592
+ import { existsSync as existsSync8, mkdirSync as mkdirSync2, readFileSync as readFileSync7, rmSync as rmSync2, writeFileSync } from "fs";
1593
+ import { resolve as resolve13 } from "path";
1594
+ var ROUTER_FILES = [
1595
+ "src/routes",
1596
+ "src/routeTree.gen.ts",
1597
+ "src/integrations/tanstack-router",
1598
+ "src/config/routes.ts",
1599
+ "src/config/router.ts",
1600
+ "src/app.tsx"
1601
+ ];
1602
+ var TANSTACK_DEPS = ["@tanstack/react-router", "@tanstack/router-plugin"];
1603
+ function isModuleTemplate(template) {
1604
+ return template === "module-hybrid-cdn" || template === "module-monolith";
1605
+ }
1606
+ function writeRouterConfig(targetDir, mode) {
1607
+ const configDir = resolve13(targetDir, "src/config");
1608
+ mkdirSync2(configDir, { recursive: true });
1609
+ writeFileSync(
1610
+ resolve13(configDir, "router.ts"),
1611
+ `/** Router history \u2014 set by \`wp init\`. Hash matches woodsportal-client-frontend (HubSpot CMS embeds). */
1612
+ export type RouterHistoryMode = 'hash' | 'browser'
1613
+
1614
+ export const ROUTER_HISTORY: RouterHistoryMode = '${mode}'
1615
+ `
1616
+ );
1617
+ }
1618
+ function writeMountDevPreviewRouter(targetDir) {
1619
+ writeFileSync(
1620
+ resolve13(targetDir, "src/portal/mount-dev-preview.tsx"),
1621
+ `import { mountGreenfieldDev } from '../../.wphs/.kit-bootstrap-greenfield.tsx'
1622
+
1623
+ import { App } from '@/app'
1624
+
1625
+ /** Local dev: module-config overlay + routed starter preview. */
1626
+ export async function mountDevPreview() {
1627
+ await mountGreenfieldDev({ preview: <App /> })
1628
+ }
1629
+ `
1630
+ );
1631
+ }
1632
+ function writeMountDevPreviewSinglePage(targetDir) {
1633
+ writeFileSync(
1634
+ resolve13(targetDir, "src/portal/mount-dev-preview.tsx"),
1635
+ `import { mountGreenfieldDev } from '../../.wphs/.kit-bootstrap-greenfield.tsx'
1636
+
1637
+ import { StarterSinglePage } from '@/features/starter'
1638
+
1639
+ /** Local dev: module-config overlay + single-page starter (no TanStack Router). */
1640
+ export async function mountDevPreview() {
1641
+ await mountGreenfieldDev({ preview: <StarterSinglePage /> })
1642
+ }
1643
+ `
1644
+ );
1645
+ }
1646
+ function writeMountPortalRouter(targetDir) {
1647
+ writeFileSync(
1648
+ resolve13(targetDir, "src/portal/mount-portal.tsx"),
1649
+ `import ReactDOM from 'react-dom/client'
1650
+
1651
+ import { App } from '@/app'
1652
+
1653
+ /** Production mount \u2014 same App shell as woodsportal-client-frontend CDN app chunk. */
1654
+ export async function mountPortal() {
1655
+ const root = document.getElementById('app')
1656
+ if (!root) {
1657
+ return
1658
+ }
1659
+ ReactDOM.createRoot(root).render(<App />)
1660
+ }
1661
+ `
1662
+ );
1663
+ }
1664
+ function writeMountPortalSinglePage(targetDir) {
1665
+ writeFileSync(
1666
+ resolve13(targetDir, "src/portal/mount-portal.tsx"),
1667
+ `import ReactDOM from 'react-dom/client'
1668
+
1669
+ import { StarterSinglePage } from '@/features/starter'
1670
+
1671
+ /** Production mount \u2014 single-page starter when router scaffold is \`none\`. */
1672
+ export async function mountPortal() {
1673
+ const root = document.getElementById('app')
1674
+ if (!root) {
1675
+ return
1676
+ }
1677
+ ReactDOM.createRoot(root).render(<StarterSinglePage />)
1678
+ }
1679
+ `
1680
+ );
1681
+ }
1682
+ function writeStarterSinglePage(targetDir) {
1683
+ const file = resolve13(targetDir, "src/features/starter/components/starter-single-page.tsx");
1684
+ if (existsSync8(file)) {
1685
+ return;
1686
+ }
1687
+ writeFileSync(
1688
+ file,
1689
+ `import { StarterShell } from '@/components/layout/starter-shell'
1690
+
1691
+ import { StarterGuidePage } from './starter-guide-page'
1692
+ import { StarterHome } from './starter-home'
1693
+
1694
+ /** Single-page starter when \`wp init --router none\` (no TanStack Router). */
1695
+ export function StarterSinglePage() {
1696
+ return (
1697
+ <StarterShell showNav={false}>
1698
+ <StarterHome />
1699
+ <div className="mx-auto max-w-4xl px-6 sm:px-10">
1700
+ <hr className="my-10 border-[#cbd6e2]" />
1701
+ </div>
1702
+ <StarterGuidePage />
1703
+ </StarterShell>
1704
+ )
1705
+ }
1706
+ `
1707
+ );
1708
+ }
1709
+ function removePath(targetDir, relativePath) {
1710
+ const full = resolve13(targetDir, relativePath);
1711
+ if (!existsSync8(full)) {
1712
+ return;
1713
+ }
1714
+ rmSync2(full, { recursive: true, force: true });
1715
+ }
1716
+ function stripTanStackFromPackage(targetDir) {
1717
+ const pkgPath = resolve13(targetDir, "package.json");
1718
+ if (!existsSync8(pkgPath)) {
1719
+ return;
1720
+ }
1721
+ const pkg = JSON.parse(readFileSync7(pkgPath, "utf8"));
1722
+ if (!pkg.devDependencies) {
1723
+ return;
1724
+ }
1725
+ for (const dep of TANSTACK_DEPS) {
1726
+ delete pkg.devDependencies[dep];
1727
+ }
1728
+ writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}
1729
+ `);
1730
+ }
1731
+ function applyRouterScaffold(targetDir, mode, template) {
1732
+ if (!isModuleTemplate(template)) {
1733
+ return;
1734
+ }
1735
+ if (mode === "none") {
1736
+ for (const relativePath of ROUTER_FILES) {
1737
+ removePath(targetDir, relativePath);
1738
+ }
1739
+ writeStarterSinglePage(targetDir);
1740
+ writeMountDevPreviewSinglePage(targetDir);
1741
+ writeMountPortalSinglePage(targetDir);
1742
+ stripTanStackFromPackage(targetDir);
1743
+ return;
1744
+ }
1745
+ writeRouterConfig(targetDir, mode);
1746
+ writeMountDevPreviewRouter(targetDir);
1747
+ writeMountPortalRouter(targetDir);
1748
+ const featuresIndex = resolve13(targetDir, "src/features/starter/index.ts");
1749
+ if (existsSync8(featuresIndex)) {
1750
+ let content = readFileSync7(featuresIndex, "utf8");
1751
+ if (!content.includes("StarterSinglePage")) {
1752
+ content = content.replace(
1753
+ "export type { StarterPreviewValues } from './hooks/use-starter-preview'\n",
1754
+ "export type { StarterPreviewValues } from './hooks/use-starter-preview'\nexport { StarterSinglePage } from './components/starter-single-page'\n"
1755
+ );
1756
+ writeFileSync(featuresIndex, content);
1757
+ }
1758
+ }
1759
+ }
1760
+
1761
+ // src/lib/project-setup.ts
1762
+ import { copyFileSync, existsSync as existsSync9, writeFileSync as writeFileSync2 } from "fs";
1763
+ import { basename, resolve as resolve14 } from "path";
1764
+ function envFileForTier(tier) {
1765
+ return tier === "dev" ? ".env.dev" : tier === "stg" ? ".env.stg" : ".env.prod";
1766
+ }
1767
+ function tiersToSetup(config, tier) {
1768
+ if (tier) return [tier];
1769
+ return config.env.tiers;
1770
+ }
1771
+ function firstExisting(paths) {
1772
+ for (const path of paths) {
1773
+ if (existsSync9(path)) return path;
1774
+ }
1775
+ return null;
1776
+ }
1777
+ function copyMissingFile(projectRoot, targetName, sourceCandidates, dryRun) {
1778
+ const target = resolve14(projectRoot, targetName);
1779
+ if (existsSync9(target)) {
1780
+ return { action: "skipped", path: targetName, detail: "already present" };
1781
+ }
1782
+ const source = firstExisting(sourceCandidates);
1783
+ if (!source) {
1784
+ return {
1785
+ action: "manual",
1786
+ path: targetName,
1787
+ detail: `no template found \u2014 create ${targetName} manually`
1788
+ };
1789
+ }
1790
+ if (!dryRun) {
1791
+ copyFileSync(source, target);
1792
+ }
1793
+ const from = basename(source);
1159
1794
  return {
1160
- [KIT_DEV_DEP]: KIT_DEV_DEP_VERSION,
1161
- ...VITE_DEV_PEER_DEPS
1795
+ action: "copied",
1796
+ path: targetName,
1797
+ detail: dryRun ? `[dry-run] would copy ${from} \u2192 ${targetName}` : `copied ${from} \u2192 ${targetName}`
1798
+ };
1799
+ }
1800
+ function writeMissingFile(projectRoot, targetName, content, dryRun) {
1801
+ const target = resolve14(projectRoot, targetName);
1802
+ if (existsSync9(target)) {
1803
+ return { action: "skipped", path: targetName, detail: "already present" };
1804
+ }
1805
+ if (!dryRun) {
1806
+ writeFileSync2(target, content);
1807
+ }
1808
+ return {
1809
+ action: "copied",
1810
+ path: targetName,
1811
+ detail: dryRun ? `[dry-run] would create ${targetName}` : `created ${targetName}`
1162
1812
  };
1163
1813
  }
1814
+ function templateViteFiles(config) {
1815
+ if (isThemeProject(config)) return ["vite.config.dev.mjs"];
1816
+ if (config.projectType === "module-monolith") {
1817
+ return ["vite.config.dev.mjs", "vite.config.monolith.mjs"];
1818
+ }
1819
+ return ["vite.config.dev.mjs", "vite.config.hubspot.mjs", "vite.config.monolith.mjs"];
1820
+ }
1821
+ function kitTemplateDir(config) {
1822
+ return kitPath("templates", config.projectType);
1823
+ }
1824
+ function applyProjectSetup(ctx, options = {}) {
1825
+ const { projectRoot, config } = ctx;
1826
+ const dryRun = options.dryRun ?? false;
1827
+ const results = [];
1828
+ const sharedDir = kitPath("templates", "shared");
1829
+ const templateDir = kitTemplateDir(config);
1830
+ results.push(
1831
+ copyMissingFile(projectRoot, HUBSPOT_CONFIG_FILE, [
1832
+ resolve14(projectRoot, HUBSPOT_CONFIG_EXAMPLE),
1833
+ resolve14(templateDir, HUBSPOT_CONFIG_EXAMPLE),
1834
+ kitPath("templates", HUBSPOT_CONFIG_EXAMPLE)
1835
+ ], dryRun)
1836
+ );
1837
+ for (const tier of tiersToSetup(config, options.tier)) {
1838
+ const envFile = envFileForTier(tier);
1839
+ const envExample = `${envFile}.example`;
1840
+ results.push(
1841
+ copyMissingFile(projectRoot, envFile, [
1842
+ resolve14(projectRoot, envExample),
1843
+ resolve14(sharedDir, envExample)
1844
+ ], dryRun)
1845
+ );
1846
+ }
1847
+ if (usesCdnPipeline(config) && !isThemeProject(config)) {
1848
+ const mirror = config.cdn.mirror;
1849
+ results.push(
1850
+ copyMissingFile(projectRoot, mirror, [
1851
+ resolve14(projectRoot, `${mirror}.example`),
1852
+ resolve14(sharedDir, "portal.cdn.json.example")
1853
+ ], dryRun)
1854
+ );
1855
+ const buildIdFile = config.cdn.buildIdFile;
1856
+ results.push(
1857
+ writeMissingFile(projectRoot, buildIdFile, "v0.0.1-dev.1\n", dryRun)
1858
+ );
1859
+ }
1860
+ for (const file of templateViteFiles(config)) {
1861
+ results.push(
1862
+ copyMissingFile(projectRoot, file, [resolve14(templateDir, file)], dryRun)
1863
+ );
1864
+ }
1865
+ if (isModuleProject(config) && !isThemeProject(config)) {
1866
+ const adapter = config.dev.adapter;
1867
+ const adapterPath = resolve14(projectRoot, adapter);
1868
+ if (!existsSync9(adapterPath)) {
1869
+ const templateAdapter = resolve14(templateDir, "wphs.adapter.ts");
1870
+ if (existsSync9(templateAdapter)) {
1871
+ results.push(copyMissingFile(projectRoot, adapter, [templateAdapter], dryRun));
1872
+ } else {
1873
+ results.push({
1874
+ action: "manual",
1875
+ path: adapter,
1876
+ detail: "create wphs.adapter.ts (see docs/ADAPTER.md)"
1877
+ });
1878
+ }
1879
+ } else {
1880
+ results.push({ action: "skipped", path: adapter, detail: "already present" });
1881
+ }
1882
+ }
1883
+ return results;
1884
+ }
1885
+ function copySharedSetupExamples(targetDir) {
1886
+ const sharedDir = kitPath("templates", "shared");
1887
+ if (!existsSync9(sharedDir)) return;
1888
+ for (const name of [
1889
+ ".env.dev.example",
1890
+ ".env.stg.example",
1891
+ ".env.prod.example",
1892
+ "portal.cdn.json.example"
1893
+ ]) {
1894
+ const src = resolve14(sharedDir, name);
1895
+ const dest = resolve14(targetDir, name);
1896
+ if (existsSync9(src) && !existsSync9(dest)) {
1897
+ copyFileSync(src, dest);
1898
+ }
1899
+ }
1900
+ const hubspotExample = firstExisting([
1901
+ resolve14(sharedDir, HUBSPOT_CONFIG_EXAMPLE),
1902
+ kitPath("templates", HUBSPOT_CONFIG_EXAMPLE)
1903
+ ]);
1904
+ if (!hubspotExample) return;
1905
+ const destHubspot = resolve14(targetDir, HUBSPOT_CONFIG_EXAMPLE);
1906
+ if (!existsSync9(destHubspot) && !existsSync9(resolve14(targetDir, HUBSPOT_CONFIG_FILE))) {
1907
+ copyFileSync(hubspotExample, destHubspot);
1908
+ }
1909
+ }
1910
+ function formatSetupHint(results) {
1911
+ const hubspot = results.find((r) => r.path === HUBSPOT_CONFIG_FILE && r.action === "copied");
1912
+ if (hubspot) {
1913
+ return `Edit ${HUBSPOT_CONFIG_FILE} \u2014 set portalId and personalAccessKey (https://app.hubspot.com/l/personal-access-key)`;
1914
+ }
1915
+ const envCopied = results.some((r) => r.path.startsWith(".env.") && r.action === "copied");
1916
+ if (envCopied) {
1917
+ return "Edit .env.* files \u2014 set VITE_PUBLIC_REST_API_ENDPOINT and portal IDs for your environment";
1918
+ }
1919
+ return void 0;
1920
+ }
1164
1921
 
1165
1922
  // src/lib/project-name.ts
1166
1923
  var PROJECT_NAME_PATTERN = /^[a-z][a-z0-9-]*$/;
@@ -1194,7 +1951,11 @@ var TEMPLATE_SCRIPTS = {
1194
1951
  "module-monolith": {
1195
1952
  dev: "wp dev",
1196
1953
  local: "wp dev",
1197
- "build:prod": "wp build --tier prod --hubspot-only",
1954
+ "build:dev": "wp build --tier dev",
1955
+ "build:prod": "wp build --tier prod",
1956
+ "deploy:dev": "wp deploy --tier dev --yes",
1957
+ "deploy:prod": "wp deploy --tier prod --yes",
1958
+ watch: "wp watch --rebuild",
1198
1959
  doctor: "wp doctor"
1199
1960
  },
1200
1961
  "theme-starter": {
@@ -1203,7 +1964,7 @@ var TEMPLATE_SCRIPTS = {
1203
1964
  doctor: "wp doctor"
1204
1965
  }
1205
1966
  };
1206
- function buildConfigForProject(template, projectName) {
1967
+ function buildConfigForProject(template, projectName, router) {
1207
1968
  const base = { ...DEFAULT_WOODSPORTAL_HS, projectType: template };
1208
1969
  if (template === "theme-starter") {
1209
1970
  return {
@@ -1216,7 +1977,7 @@ function buildConfigForProject(template, projectName) {
1216
1977
  }
1217
1978
  };
1218
1979
  }
1219
- return {
1980
+ const config = {
1220
1981
  ...base,
1221
1982
  module: {
1222
1983
  name: projectName,
@@ -1228,9 +1989,13 @@ function buildConfigForProject(template, projectName) {
1228
1989
  mode: template === "module-monolith" ? "none" : base.cdn.mode
1229
1990
  }
1230
1991
  };
1992
+ if (isModuleTemplate(template) && router) {
1993
+ config.scaffold = { router };
1994
+ }
1995
+ return config;
1231
1996
  }
1232
1997
  function assertTargetDirIsScaffoldable(targetDir) {
1233
- if (!existsSync8(targetDir)) return;
1998
+ if (!existsSync10(targetDir)) return;
1234
1999
  const entries = readdirSync2(targetDir).filter((entry) => entry !== ".DS_Store");
1235
2000
  if (entries.length > 0) {
1236
2001
  throw new WpHsError(`Target directory is not empty: ${targetDir}`, {
@@ -1241,7 +2006,7 @@ function assertTargetDirIsScaffoldable(targetDir) {
1241
2006
  }
1242
2007
  function resolveInitTargetDir(options, interactiveName) {
1243
2008
  if (options.directory) {
1244
- return resolve12(options.directory);
2009
+ return resolve15(options.directory);
1245
2010
  }
1246
2011
  const parentDir = process.cwd();
1247
2012
  const slug = options.name ?? interactiveName;
@@ -1255,15 +2020,16 @@ function resolveInitTargetDir(options, interactiveName) {
1255
2020
  if (error) {
1256
2021
  throw new WpHsError(error, { exitCode: ExitCode.USER_ERROR });
1257
2022
  }
1258
- return resolve12(parentDir, slug);
2023
+ return resolve15(parentDir, slug);
1259
2024
  }
1260
2025
  async function runInit(ctx, options) {
1261
2026
  const nonInteractive = ctx.global.ci || ctx.global.yes || ctx.global.json;
1262
2027
  let template = options.template;
1263
2028
  let projectName = options.name ? normalizeProjectName(options.name) : void 0;
2029
+ let router = options.router;
1264
2030
  if (!nonInteractive) {
1265
2031
  if (!template) {
1266
- const type = await p.select({
2032
+ const type = await p2.select({
1267
2033
  message: "Project type",
1268
2034
  options: [
1269
2035
  {
@@ -1275,58 +2041,92 @@ async function runInit(ctx, options) {
1275
2041
  { value: "theme-starter", label: "Theme starter", hint: "CMS theme templates" }
1276
2042
  ]
1277
2043
  });
1278
- if (p.isCancel(type)) throw new WpHsError("Init cancelled", { exitCode: ExitCode.USER_ERROR });
2044
+ if (p2.isCancel(type)) throw new WpHsError("Init cancelled", { exitCode: ExitCode.USER_ERROR });
1279
2045
  template = type;
1280
2046
  }
1281
2047
  if (!options.directory && !projectName) {
1282
- const nameAnswer = await p.text({
2048
+ const nameAnswer = await p2.text({
1283
2049
  message: "Project name (folder name)",
1284
2050
  placeholder: "my-portal-module",
1285
2051
  validate: (value) => validateProjectName(normalizeProjectName(value ?? ""))
1286
2052
  });
1287
- if (p.isCancel(nameAnswer)) {
2053
+ if (p2.isCancel(nameAnswer)) {
1288
2054
  throw new WpHsError("Init cancelled", { exitCode: ExitCode.USER_ERROR });
1289
2055
  }
1290
2056
  projectName = normalizeProjectName(nameAnswer);
1291
2057
  }
2058
+ if (!router && template && isModuleTemplate(template)) {
2059
+ const routerAnswer = await p2.select({
2060
+ message: "Client-side routing",
2061
+ initialValue: "hash",
2062
+ options: [
2063
+ {
2064
+ value: "hash",
2065
+ label: "Hash history (recommended)",
2066
+ hint: "Same as woodsportal-client-frontend \u2014 works in HubSpot CMS embeds"
2067
+ },
2068
+ {
2069
+ value: "browser",
2070
+ label: "Browser history",
2071
+ hint: "Clean /guide URLs for local dev (not for HubSpot iframe embeds)"
2072
+ },
2073
+ {
2074
+ value: "none",
2075
+ label: "No router",
2076
+ hint: "Single-page starter \u2014 add TanStack Router later"
2077
+ }
2078
+ ]
2079
+ });
2080
+ if (p2.isCancel(routerAnswer)) {
2081
+ throw new WpHsError("Init cancelled", { exitCode: ExitCode.USER_ERROR });
2082
+ }
2083
+ router = routerAnswer;
2084
+ }
1292
2085
  }
1293
2086
  if (!template) {
1294
2087
  template = "module-hybrid-cdn";
1295
2088
  }
2089
+ if (!router && isModuleTemplate(template)) {
2090
+ router = "hash";
2091
+ }
1296
2092
  const targetDir = resolveInitTargetDir(options, projectName);
1297
- const slug = projectName ?? basename(targetDir);
2093
+ const slug = projectName ?? basename2(targetDir);
1298
2094
  assertTargetDirIsScaffoldable(targetDir);
1299
2095
  const templateDir = kitPath("templates", template);
1300
- if (!existsSync8(templateDir)) {
2096
+ if (!existsSync10(templateDir)) {
1301
2097
  throw new WpHsError(`Template not found: ${template}`, { exitCode: ExitCode.USER_ERROR });
1302
2098
  }
1303
2099
  if (ctx.global.dryRun) {
1304
2100
  ctx.logger.info(`[dry-run] Would scaffold ${template} in ${targetDir}`);
1305
2101
  return ExitCode.SUCCESS;
1306
2102
  }
1307
- mkdirSync2(targetDir, { recursive: true });
2103
+ mkdirSync3(targetDir, { recursive: true });
1308
2104
  cpSync2(templateDir, targetDir, { recursive: true });
1309
- const config = buildConfigForProject(template, slug);
1310
- writeFileSync(resolve12(targetDir, WOODSCLI_CONFIG_FILE), `${JSON.stringify(config, null, 2)}
2105
+ copySharedSetupExamples(targetDir);
2106
+ const config = buildConfigForProject(template, slug, router);
2107
+ writeFileSync3(resolve15(targetDir, WOODSCLI_CONFIG_FILE), `${JSON.stringify(config, null, 2)}
1311
2108
  `);
1312
- const pkgPath = resolve12(targetDir, "package.json");
1313
- if (existsSync8(pkgPath)) {
1314
- const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
2109
+ const pkgPath = resolve15(targetDir, "package.json");
2110
+ if (existsSync10(pkgPath)) {
2111
+ const pkg = JSON.parse(readFileSync8(pkgPath, "utf8"));
1315
2112
  pkg.name = slug;
1316
2113
  pkg.scripts = { ...pkg.scripts, ...TEMPLATE_SCRIPTS[template] };
1317
2114
  pkg.devDependencies = {
1318
2115
  ...pkg.devDependencies,
1319
2116
  ...templateDevDependencies()
1320
2117
  };
1321
- writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}
2118
+ writeFileSync3(pkgPath, `${JSON.stringify(pkg, null, 2)}
1322
2119
  `);
1323
2120
  }
2121
+ if (router && isModuleTemplate(template)) {
2122
+ applyRouterScaffold(targetDir, router, template);
2123
+ }
1324
2124
  runNodeScript(kitPath("scripts/ensure-wphs-workspace.mjs"), [], {
1325
2125
  cwd: targetDir,
1326
2126
  logger: ctx.logger,
1327
2127
  dryRun: false
1328
2128
  });
1329
- const gitignorePath = resolve12(targetDir, ".gitignore");
2129
+ const gitignorePath = resolve15(targetDir, ".gitignore");
1330
2130
  const gitignoreExtras = [
1331
2131
  "hubspot.config.yml",
1332
2132
  ".wphs/config/module-config.local.json",
@@ -1336,18 +2136,19 @@ async function runInit(ctx, options) {
1336
2136
  ".wphs/state/",
1337
2137
  ".wphs/fixtures/hubspot-data.json",
1338
2138
  ".wphs/.kit-module-config",
1339
- ".wphs/.kit-bootstrap.tsx"
2139
+ ".wphs/.kit-bootstrap.tsx",
2140
+ ".wphs/.kit-bootstrap-greenfield.tsx"
1340
2141
  ];
1341
- const existing = existsSync8(gitignorePath) ? readFileSync5(gitignorePath, "utf8") : "";
2142
+ const existing = existsSync10(gitignorePath) ? readFileSync8(gitignorePath, "utf8") : "";
1342
2143
  const toAppend = gitignoreExtras.filter((line) => !existing.includes(line)).join("\n");
1343
2144
  if (toAppend) {
1344
- writeFileSync(gitignorePath, `${existing.trimEnd()}
2145
+ writeFileSync3(gitignorePath, `${existing.trimEnd()}
1345
2146
  ${toAppend}
1346
2147
  `);
1347
2148
  }
1348
2149
  if (!nonInteractive) {
1349
- p.log.success(`Scaffolded ${template} in ${targetDir}`);
1350
- p.note(`cd ${slug} && yarn install && wp doctor && wp dev`, "Getting started");
2150
+ p2.log.success(`Scaffolded ${template} in ${targetDir}`);
2151
+ p2.note(`cd ${slug} && yarn install && wp setup && wp doctor && wp dev`, "Getting started");
1351
2152
  } else {
1352
2153
  ctx.logger.success(`Scaffolded ${template} in ${targetDir}`);
1353
2154
  }
@@ -1355,10 +2156,10 @@ ${toAppend}
1355
2156
  }
1356
2157
 
1357
2158
  // src/cli/commands/adopt.ts
1358
- import { existsSync as existsSync9, readFileSync as readFileSync6, writeFileSync as writeFileSync2 } from "fs";
1359
- import { resolve as resolve13 } from "path";
2159
+ import { existsSync as existsSync11, readFileSync as readFileSync9, writeFileSync as writeFileSync4 } from "fs";
2160
+ import { resolve as resolve16 } from "path";
1360
2161
  import { pathToFileURL } from "url";
1361
- import * as p2 from "@clack/prompts";
2162
+ import * as p3 from "@clack/prompts";
1362
2163
  async function loadDetectLayout() {
1363
2164
  const mod = await import(pathToFileURL(kitPath("scripts", "detect-layout.mjs")).href);
1364
2165
  return mod.detectProjectLayout;
@@ -1366,7 +2167,7 @@ async function loadDetectLayout() {
1366
2167
  async function runAdopt(ctx, _options = {}) {
1367
2168
  const detectProjectLayout = await loadDetectLayout();
1368
2169
  const layout = detectProjectLayout(ctx.projectRoot);
1369
- const configPath = resolve13(ctx.projectRoot, WOODSCLI_CONFIG_FILE);
2170
+ const configPath = resolve16(ctx.projectRoot, WOODSCLI_CONFIG_FILE);
1370
2171
  if (layout.track === "unknown") {
1371
2172
  throw new WpHsError("Could not detect module or theme layout.", {
1372
2173
  exitCode: ExitCode.USER_ERROR,
@@ -1378,7 +2179,7 @@ async function runAdopt(ctx, _options = {}) {
1378
2179
  projectType = layout.projectType;
1379
2180
  } else if (layout.track === "theme") {
1380
2181
  projectType = "theme-starter";
1381
- } else if (existsSync9(resolve13(ctx.projectRoot, "vite.config.monolith.mjs"))) {
2182
+ } else if (existsSync11(resolve16(ctx.projectRoot, "vite.config.monolith.mjs"))) {
1382
2183
  projectType = "module-monolith";
1383
2184
  } else {
1384
2185
  projectType = "module-hybrid-cdn";
@@ -1399,13 +2200,13 @@ async function runAdopt(ctx, _options = {}) {
1399
2200
  ctx.logger.info(`[dry-run] Would adopt as ${projectType} (${layout.track})`);
1400
2201
  return ExitCode.SUCCESS;
1401
2202
  }
1402
- if (!existsSync9(configPath)) {
1403
- writeFileSync2(configPath, `${JSON.stringify(config, null, 2)}
2203
+ if (!existsSync11(configPath)) {
2204
+ writeFileSync4(configPath, `${JSON.stringify(config, null, 2)}
1404
2205
  `);
1405
2206
  ctx.logger.success(`Created ${WOODSCLI_CONFIG_FILE}`);
1406
2207
  } else if (!layout.projectType) {
1407
- const existing = JSON.parse(readFileSync6(configPath, "utf8"));
1408
- writeFileSync2(configPath, `${JSON.stringify({ ...existing, projectType }, null, 2)}
2208
+ const existing = JSON.parse(readFileSync9(configPath, "utf8"));
2209
+ writeFileSync4(configPath, `${JSON.stringify({ ...existing, projectType }, null, 2)}
1409
2210
  `);
1410
2211
  ctx.logger.info(`Set projectType \u2192 ${projectType}`);
1411
2212
  }
@@ -1414,9 +2215,9 @@ async function runAdopt(ctx, _options = {}) {
1414
2215
  logger: ctx.logger,
1415
2216
  dryRun: false
1416
2217
  });
1417
- const adapterPath = resolve13(ctx.projectRoot, "wphs.adapter.ts");
1418
- if (!existsSync9(adapterPath) && isModuleProject(config)) {
1419
- writeFileSync2(
2218
+ const adapterPath = resolve16(ctx.projectRoot, "wphs.adapter.ts");
2219
+ if (!existsSync11(adapterPath) && isModuleProject(config)) {
2220
+ writeFileSync4(
1420
2221
  adapterPath,
1421
2222
  `import type { HubSpotKitAdapter } from '@woodsportal/hubspot-kit'
1422
2223
 
@@ -1432,11 +2233,11 @@ export default adapter
1432
2233
  ctx.logger.info("Created wphs.adapter.ts stub");
1433
2234
  }
1434
2235
  if (!ctx.global.ci && !ctx.global.quiet) {
1435
- p2.note(
2236
+ p3.note(
1436
2237
  [
1437
2238
  `Review ${WOODSCLI_CONFIG_FILE} upload paths`,
1438
2239
  "Implement wphs.adapter.ts for your app (if module)",
1439
- "Add vite.config.dev.mjs re-export if missing",
2240
+ "Run wp setup (copies hubspot.config.yml, .env.*, vite shims)",
1440
2241
  "Patch main.dev.css @source to .wphs/.kit-module-config/**"
1441
2242
  ].join("\n"),
1442
2243
  "Manual follow-ups"
@@ -1447,67 +2248,67 @@ export default adapter
1447
2248
  }
1448
2249
 
1449
2250
  // src/cli/commands/migrate.ts
1450
- import { cpSync as cpSync3, existsSync as existsSync10, mkdirSync as mkdirSync3, readFileSync as readFileSync7, readdirSync as readdirSync3, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
1451
- import { join as join3, resolve as resolve14 } from "path";
2251
+ import { cpSync as cpSync3, existsSync as existsSync12, mkdirSync as mkdirSync4, readFileSync as readFileSync10, readdirSync as readdirSync3, renameSync, rmSync as rmSync3, writeFileSync as writeFileSync5 } from "fs";
2252
+ import { join as join3, resolve as resolve17 } from "path";
1452
2253
  var LEGACY_DIRS = [".dev", ".woodsportal"];
1453
2254
  var LEGACY_BASELINE = ".wp-hs-baseline";
1454
2255
  async function runMigrate(ctx, _options = {}) {
1455
2256
  const root = ctx.projectRoot;
1456
- const wphsRoot = resolve14(root, WPHS_WORKSPACE_DIR);
2257
+ const wphsRoot = resolve17(root, WPHS_WORKSPACE_DIR);
1457
2258
  const actions = [];
1458
2259
  for (const legacy of LEGACY_DIRS) {
1459
- const legacyPath = resolve14(root, legacy);
1460
- if (!existsSync10(legacyPath)) continue;
2260
+ const legacyPath = resolve17(root, legacy);
2261
+ if (!existsSync12(legacyPath)) continue;
1461
2262
  actions.push(`Move ${legacy}/ \u2192 .wphs/`);
1462
2263
  if (ctx.global.dryRun) continue;
1463
- mkdirSync3(join3(wphsRoot, "config"), { recursive: true });
1464
- mkdirSync3(join3(wphsRoot, "assets", "branding"), { recursive: true });
2264
+ mkdirSync4(join3(wphsRoot, "config"), { recursive: true });
2265
+ mkdirSync4(join3(wphsRoot, "assets", "branding"), { recursive: true });
1465
2266
  const localConfig = join3(legacyPath, "module-config.local.json");
1466
2267
  const exampleConfig = join3(legacyPath, "module-config.example.json");
1467
- if (existsSync10(localConfig)) {
2268
+ if (existsSync12(localConfig)) {
1468
2269
  cpSync3(localConfig, join3(wphsRoot, "config", "module-config.local.json"));
1469
2270
  }
1470
- if (existsSync10(exampleConfig)) {
2271
+ if (existsSync12(exampleConfig)) {
1471
2272
  cpSync3(exampleConfig, join3(wphsRoot, "config", "module-config.example.json"));
1472
2273
  }
1473
2274
  const branding = join3(legacyPath, "branding-assets");
1474
- if (existsSync10(branding)) {
2275
+ if (existsSync12(branding)) {
1475
2276
  for (const file of readdirSync3(branding)) {
1476
2277
  cpSync3(join3(branding, file), join3(wphsRoot, "assets", "branding", file));
1477
2278
  }
1478
2279
  }
1479
- rmSync2(legacyPath, { recursive: true, force: true });
2280
+ rmSync3(legacyPath, { recursive: true, force: true });
1480
2281
  }
1481
- const legacyBaseline = resolve14(root, LEGACY_BASELINE);
2282
+ const legacyBaseline = resolve17(root, LEGACY_BASELINE);
1482
2283
  const targetBaseline = join3(wphsRoot, "audit", "baseline");
1483
- if (existsSync10(legacyBaseline)) {
2284
+ if (existsSync12(legacyBaseline)) {
1484
2285
  actions.push(`Move ${LEGACY_BASELINE}/ \u2192 .wphs/audit/baseline/`);
1485
2286
  if (!ctx.global.dryRun) {
1486
- mkdirSync3(targetBaseline, { recursive: true });
2287
+ mkdirSync4(targetBaseline, { recursive: true });
1487
2288
  cpSync3(legacyBaseline, targetBaseline, { recursive: true });
1488
- rmSync2(legacyBaseline, { recursive: true, force: true });
2289
+ rmSync3(legacyBaseline, { recursive: true, force: true });
1489
2290
  }
1490
2291
  }
1491
- const legacyConfigPath2 = resolve14(root, LEGACY_WOODSPORTAL_HS_FILE);
1492
- const woodscliPath = resolve14(root, WOODSCLI_CONFIG_FILE);
1493
- if (existsSync10(legacyConfigPath2) && !existsSync10(woodscliPath)) {
2292
+ const legacyConfigPath2 = resolve17(root, LEGACY_WOODSPORTAL_HS_FILE);
2293
+ const woodscliPath = resolve17(root, WOODSCLI_CONFIG_FILE);
2294
+ if (existsSync12(legacyConfigPath2) && !existsSync12(woodscliPath)) {
1494
2295
  actions.push(`Rename ${LEGACY_WOODSPORTAL_HS_FILE} \u2192 ${WOODSCLI_CONFIG_FILE}`);
1495
2296
  if (!ctx.global.dryRun) {
1496
2297
  renameSync(legacyConfigPath2, woodscliPath);
1497
2298
  }
1498
- } else if (existsSync10(legacyConfigPath2) && existsSync10(woodscliPath)) {
2299
+ } else if (existsSync12(legacyConfigPath2) && existsSync12(woodscliPath)) {
1499
2300
  actions.push(`Remove duplicate ${LEGACY_WOODSPORTAL_HS_FILE}`);
1500
2301
  if (!ctx.global.dryRun) {
1501
- rmSync2(legacyConfigPath2, { force: true });
2302
+ rmSync3(legacyConfigPath2, { force: true });
1502
2303
  }
1503
2304
  }
1504
- if (existsSync10(woodscliPath)) {
1505
- const raw = JSON.parse(readFileSync7(woodscliPath, "utf8"));
2305
+ if (existsSync12(woodscliPath)) {
2306
+ const raw = JSON.parse(readFileSync10(woodscliPath, "utf8"));
1506
2307
  if (raw?.dev?.moduleConfigDir && raw.dev.moduleConfigDir !== WPHS_WORKSPACE_DIR) {
1507
2308
  actions.push(`Update dev.moduleConfigDir \u2192 .wphs in project config`);
1508
2309
  if (!ctx.global.dryRun) {
1509
2310
  raw.dev.moduleConfigDir = WPHS_WORKSPACE_DIR;
1510
- writeFileSync3(woodscliPath, `${JSON.stringify(raw, null, 2)}
2311
+ writeFileSync5(woodscliPath, `${JSON.stringify(raw, null, 2)}
1511
2312
  `);
1512
2313
  }
1513
2314
  }
@@ -1536,10 +2337,10 @@ async function runMigrate(ctx, _options = {}) {
1536
2337
  }
1537
2338
 
1538
2339
  // src/cli/commands/examples.ts
1539
- import { readFileSync as readFileSync8 } from "fs";
2340
+ import { readFileSync as readFileSync11 } from "fs";
1540
2341
  function loadManifest() {
1541
2342
  const manifestPath = kitPath("examples", "manifest.json");
1542
- const raw = JSON.parse(readFileSync8(manifestPath, "utf8"));
2343
+ const raw = JSON.parse(readFileSync11(manifestPath, "utf8"));
1543
2344
  return raw.scenarios;
1544
2345
  }
1545
2346
  async function runExamplesList(ctx, options = {}) {
@@ -1559,7 +2360,7 @@ async function runExamplesSmoke(ctx, options = {}) {
1559
2360
  const tier = options.tier ?? "static";
1560
2361
  const args = ["--tier", tier];
1561
2362
  if (options.scenario) args.push("--scenario", options.scenario);
1562
- const { runNodeScript: runNodeScript2 } = await import("./exec-K5C6LZP4.js");
2363
+ const { runNodeScript: runNodeScript2 } = await import("./exec-U2OJATR3.js");
1563
2364
  runNodeScript2(kitPath("examples", "smoke", "run-matrix.mjs"), args, {
1564
2365
  cwd: ctx.projectRoot,
1565
2366
  logger: ctx.logger,
@@ -1572,15 +2373,15 @@ async function runExamplesSmoke(ctx, options = {}) {
1572
2373
  }
1573
2374
 
1574
2375
  // src/cli/commands/fields.ts
1575
- import { cpSync as cpSync4, existsSync as existsSync11, readFileSync as readFileSync9, writeFileSync as writeFileSync4 } from "fs";
1576
- import { resolve as resolve15 } from "path";
2376
+ import { cpSync as cpSync4, existsSync as existsSync13, readFileSync as readFileSync12, writeFileSync as writeFileSync6 } from "fs";
2377
+ import { resolve as resolve18 } from "path";
1577
2378
  async function runFieldsSync(ctx, options = {}) {
1578
2379
  const direction = options.direction ?? "pull";
1579
- const fieldsPath = resolve15(ctx.projectRoot, ctx.config.dev.fieldsJson);
1580
- const cacheDir = resolve15(ctx.projectRoot, ctx.config.dev.moduleConfigDir, "cache", "fields-sync");
1581
- const cachePath = resolve15(cacheDir, "fields.json");
2380
+ const fieldsPath = resolve18(ctx.projectRoot, ctx.config.dev.fieldsJson);
2381
+ const cacheDir = resolve18(ctx.projectRoot, ctx.config.dev.moduleConfigDir, "cache", "fields-sync");
2382
+ const cachePath = resolve18(cacheDir, "fields.json");
1582
2383
  if (direction === "pull") {
1583
- if (!existsSync11(fieldsPath)) {
2384
+ if (!existsSync13(fieldsPath)) {
1584
2385
  throw new WpHsError(`Missing ${ctx.config.dev.fieldsJson}`, {
1585
2386
  exitCode: ExitCode.USER_ERROR,
1586
2387
  remediation: "Add fields.json or run wp init."
@@ -1590,13 +2391,13 @@ async function runFieldsSync(ctx, options = {}) {
1590
2391
  ctx.logger.info(`[dry-run] Would cache ${fieldsPath} \u2192 ${cachePath}`);
1591
2392
  return ExitCode.SUCCESS;
1592
2393
  }
1593
- const { mkdirSync: mkdirSync4 } = await import("fs");
1594
- mkdirSync4(cacheDir, { recursive: true });
2394
+ const { mkdirSync: mkdirSync5 } = await import("fs");
2395
+ mkdirSync5(cacheDir, { recursive: true });
1595
2396
  cpSync4(fieldsPath, cachePath);
1596
2397
  ctx.logger.success(`Cached fields \u2192 ${cachePath}`);
1597
2398
  return ExitCode.SUCCESS;
1598
2399
  }
1599
- if (!existsSync11(cachePath)) {
2400
+ if (!existsSync13(cachePath)) {
1600
2401
  throw new WpHsError("No cached fields \u2014 run wp fields sync pull first.", {
1601
2402
  exitCode: ExitCode.USER_ERROR
1602
2403
  });
@@ -1605,14 +2406,14 @@ async function runFieldsSync(ctx, options = {}) {
1605
2406
  ctx.logger.info(`[dry-run] Would write ${cachePath} \u2192 ${fieldsPath}`);
1606
2407
  return ExitCode.SUCCESS;
1607
2408
  }
1608
- writeFileSync4(fieldsPath, readFileSync9(cachePath, "utf8"));
2409
+ writeFileSync6(fieldsPath, readFileSync12(cachePath, "utf8"));
1609
2410
  ctx.logger.success(`Restored fields from cache \u2192 ${fieldsPath}`);
1610
2411
  return ExitCode.SUCCESS;
1611
2412
  }
1612
2413
 
1613
2414
  // src/cli/commands/regression.ts
1614
- import { existsSync as existsSync12 } from "fs";
1615
- import { resolve as resolve16 } from "path";
2415
+ import { existsSync as existsSync14 } from "fs";
2416
+ import { resolve as resolve19 } from "path";
1616
2417
  var LEGACY_SHIM_SCRIPTS = [
1617
2418
  "build-cdn.mjs",
1618
2419
  "run-vite-build.mjs",
@@ -1635,21 +2436,21 @@ async function runRegressionSmoke(ctx, options = {}) {
1635
2436
  if (!validateOk) failures.push("wp config validate failed");
1636
2437
  if (options.checkLegacyShims !== false) {
1637
2438
  for (const script of LEGACY_SHIM_SCRIPTS) {
1638
- const shim = resolve16(ctx.projectRoot, "scripts", script);
2439
+ const shim = resolve19(ctx.projectRoot, "scripts", script);
1639
2440
  const kitScript = kitPath("scripts", script);
1640
- const shimOk = existsSync12(shim) && existsSync12(kitScript);
2441
+ const shimOk = existsSync14(shim) && existsSync14(kitScript);
1641
2442
  checks.push({ name: `shim:${script}`, ok: shimOk });
1642
2443
  if (!shimOk) failures.push(`Missing legacy shim or kit script: ${script}`);
1643
2444
  }
1644
2445
  }
1645
2446
  for (const rel of FORBIDDEN_CONSUMER_PATHS) {
1646
- const forbidden = resolve16(ctx.projectRoot, rel);
1647
- const absent = !existsSync12(forbidden);
2447
+ const forbidden = resolve19(ctx.projectRoot, rel);
2448
+ const absent = !existsSync14(forbidden);
1648
2449
  checks.push({ name: `no-${rel}`, ok: absent });
1649
2450
  if (!absent) failures.push(`Forbidden dev path still present: ${rel}`);
1650
2451
  }
1651
- const woodscliConfig = resolve16(ctx.projectRoot, WOODSCLI_CONFIG_FILE);
1652
- const configOk = existsSync12(woodscliConfig);
2452
+ const woodscliConfig = resolve19(ctx.projectRoot, WOODSCLI_CONFIG_FILE);
2453
+ const configOk = existsSync14(woodscliConfig);
1653
2454
  checks.push({ name: WOODSCLI_CONFIG_FILE, ok: configOk });
1654
2455
  if (!configOk) failures.push(`${WOODSCLI_CONFIG_FILE} missing`);
1655
2456
  if (ctx.global.json) {
@@ -1673,43 +2474,260 @@ async function runRegressionSmoke(ctx, options = {}) {
1673
2474
  return ExitCode.SUCCESS;
1674
2475
  }
1675
2476
 
2477
+ // src/cli/commands/setup.ts
2478
+ async function runSetup(ctx, options = {}) {
2479
+ let results = [];
2480
+ await runCliTasks(
2481
+ ctx,
2482
+ [
2483
+ {
2484
+ title: "Preparing .wphs workspace",
2485
+ task: async () => {
2486
+ if (ctx.global.dryRun) return;
2487
+ try {
2488
+ runNodeScript(kitPath("scripts", "ensure-wphs-workspace.mjs"), [], {
2489
+ cwd: ctx.projectRoot,
2490
+ logger: ctx.logger,
2491
+ dryRun: false
2492
+ });
2493
+ } catch {
2494
+ }
2495
+ }
2496
+ },
2497
+ {
2498
+ title: "Copying missing config files",
2499
+ task: async () => {
2500
+ results = applyProjectSetup(ctx, {
2501
+ tier: options.tier,
2502
+ dryRun: ctx.global.dryRun
2503
+ });
2504
+ }
2505
+ }
2506
+ ],
2507
+ options
2508
+ );
2509
+ const copied = results.filter((r) => r.action === "copied");
2510
+ const manual = results.filter((r) => r.action === "manual");
2511
+ if (ctx.global.json) {
2512
+ ctx.logger.json({
2513
+ command: "setup",
2514
+ ok: manual.length === 0,
2515
+ copied: copied.length,
2516
+ skipped: results.filter((r) => r.action === "skipped").length,
2517
+ manual,
2518
+ results
2519
+ });
2520
+ } else {
2521
+ for (const result of results) {
2522
+ if (result.action === "skipped") continue;
2523
+ if (result.action === "copied") {
2524
+ ctx.logger.success(result.detail);
2525
+ } else {
2526
+ ctx.logger.warn(`${result.path}: ${result.detail}`);
2527
+ }
2528
+ }
2529
+ if (copied.length === 0 && manual.length === 0) {
2530
+ printCommandDone(ctx, "Nothing to copy \u2014 required files already present");
2531
+ } else if (copied.length > 0) {
2532
+ printCommandDone(ctx, `Setup complete \u2014 ${copied.length} file(s) copied`);
2533
+ }
2534
+ const hint = formatSetupHint(results);
2535
+ if (hint) {
2536
+ ctx.logger.info(`\u2192 ${hint}`);
2537
+ }
2538
+ if (manual.length > 0) {
2539
+ ctx.logger.info("\u2192 Run wp doctor for the full checklist");
2540
+ }
2541
+ }
2542
+ return ExitCode.SUCCESS;
2543
+ }
2544
+
2545
+ // src/cli/commands/portal.ts
2546
+ import * as p4 from "@clack/prompts";
2547
+
2548
+ // src/lib/hubspot-portals.ts
2549
+ import { existsSync as existsSync15, readFileSync as readFileSync13 } from "fs";
2550
+ import { resolve as resolve20 } from "path";
2551
+ function stripYamlScalar(value) {
2552
+ const trimmed = value.trim();
2553
+ if (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'")) {
2554
+ return trimmed.slice(1, -1);
2555
+ }
2556
+ return trimmed;
2557
+ }
2558
+ function parseHubSpotConfigPortals(raw) {
2559
+ const defaultMatch = raw.match(/^defaultPortal:\s*(.+)\s*$/m);
2560
+ const defaultPortal = defaultMatch ? stripYamlScalar(defaultMatch[1]) : null;
2561
+ const portals = [];
2562
+ let current = null;
2563
+ let inPortals = false;
2564
+ for (const line of raw.split("\n")) {
2565
+ const trimmed = line.trim();
2566
+ if (trimmed === "portals:") {
2567
+ inPortals = true;
2568
+ continue;
2569
+ }
2570
+ if (!inPortals) continue;
2571
+ const listItemName = line.match(/^\s+-\s+name:\s+(.+)\s*$/);
2572
+ const nestedName = line.match(/^\s+name:\s+(.+)\s*$/);
2573
+ const portalId = line.match(/^\s+portalId:\s+(\d+)\s*$/);
2574
+ if (listItemName || nestedName && !line.match(/^\s+-\s/)) {
2575
+ if (current) portals.push(current);
2576
+ const nameValue = listItemName?.[1] ?? nestedName?.[1];
2577
+ current = nameValue ? { name: stripYamlScalar(nameValue) } : null;
2578
+ continue;
2579
+ }
2580
+ if (portalId && current) {
2581
+ current.portalId = portalId[1];
2582
+ }
2583
+ }
2584
+ if (current) portals.push(current);
2585
+ return { defaultPortal, portals };
2586
+ }
2587
+ function readHubSpotPortals(projectRoot) {
2588
+ const configPath = resolve20(projectRoot, HUBSPOT_CONFIG_FILE);
2589
+ if (!existsSync15(configPath)) {
2590
+ throw new WpHsError(`${HUBSPOT_CONFIG_FILE} not found in project root.`, {
2591
+ exitCode: ExitCode.USER_ERROR,
2592
+ remediation: "Run wp setup, then add your portal + PAT."
2593
+ });
2594
+ }
2595
+ const raw = readFileSync13(configPath, "utf8");
2596
+ const { defaultPortal, portals } = parseHubSpotConfigPortals(raw);
2597
+ return { configPath, defaultPortal, portals };
2598
+ }
2599
+ function resolvePortalName(portals, nameOrId) {
2600
+ const needle = nameOrId.trim();
2601
+ return portals.find((p5) => p5.name === needle) ?? portals.find((p5) => p5.portalId === needle) ?? null;
2602
+ }
2603
+
2604
+ // src/cli/commands/portal.ts
2605
+ function formatPortalLine(portal, defaultPortal) {
2606
+ const id = portal.portalId ? `portalId ${portal.portalId}` : "portalId ?";
2607
+ const mark = portal.name === defaultPortal ? " \xB7 default" : "";
2608
+ return `${portal.name} (${id})${mark}`;
2609
+ }
2610
+ async function runPortalList(ctx, _options = {}) {
2611
+ const data = readHubSpotPortals(ctx.projectRoot);
2612
+ if (ctx.global.json) {
2613
+ ctx.logger.json({
2614
+ command: "portal:list",
2615
+ configPath: data.configPath,
2616
+ defaultPortal: data.defaultPortal,
2617
+ portals: data.portals
2618
+ });
2619
+ return ExitCode.SUCCESS;
2620
+ }
2621
+ if (data.portals.length === 0) {
2622
+ ctx.logger.warn(`No portals[] entries found in ${data.configPath}`);
2623
+ ctx.logger.info("\u2192 Add portals under hubspot.config.yml or run hs auth");
2624
+ return ExitCode.SUCCESS;
2625
+ }
2626
+ if (data.defaultPortal) {
2627
+ ctx.logger.info(`Default: ${data.defaultPortal}`);
2628
+ }
2629
+ for (const portal of data.portals) {
2630
+ ctx.logger.info(formatPortalLine(portal, data.defaultPortal));
2631
+ }
2632
+ return ExitCode.SUCCESS;
2633
+ }
2634
+ async function runPortalUse(ctx, options = {}) {
2635
+ const data = readHubSpotPortals(ctx.projectRoot);
2636
+ if (data.portals.length === 0) {
2637
+ throw new WpHsError("No portals configured in hubspot.config.yml", {
2638
+ exitCode: ExitCode.USER_ERROR,
2639
+ remediation: "Run wp auth or add portals[] to hubspot.config.yml"
2640
+ });
2641
+ }
2642
+ let targetName = options.name?.trim();
2643
+ if (!targetName) {
2644
+ if (ctx.global.ci || ctx.global.json) {
2645
+ throw new WpHsError("Portal name or id required in non-interactive mode", {
2646
+ exitCode: ExitCode.USER_ERROR,
2647
+ remediation: "wp portal use <name>"
2648
+ });
2649
+ }
2650
+ if (!isInteractiveCli(ctx)) {
2651
+ throw new WpHsError("Portal name or id required", {
2652
+ exitCode: ExitCode.USER_ERROR,
2653
+ remediation: "wp portal use <name> \xB7 wp portal list"
2654
+ });
2655
+ }
2656
+ const answer = await p4.select({
2657
+ message: "Set default HubSpot portal",
2658
+ options: data.portals.map((portal2) => ({
2659
+ value: portal2.name,
2660
+ label: portal2.name,
2661
+ hint: portal2.name === data.defaultPortal ? `current default \xB7 ${portal2.portalId ?? "no id"}` : portal2.portalId ?? void 0
2662
+ }))
2663
+ });
2664
+ if (p4.isCancel(answer)) {
2665
+ throw new WpHsError("Portal selection cancelled", { exitCode: ExitCode.USER_ERROR });
2666
+ }
2667
+ targetName = answer;
2668
+ }
2669
+ const portal = resolvePortalName(data.portals, targetName);
2670
+ if (!portal) {
2671
+ throw new WpHsError(`Unknown portal: ${targetName}`, {
2672
+ exitCode: ExitCode.USER_ERROR,
2673
+ remediation: "wp portal list"
2674
+ });
2675
+ }
2676
+ if (ctx.global.dryRun) {
2677
+ ctx.logger.info(`[dry-run] hs accounts use ${portal.name}`);
2678
+ return ExitCode.SUCCESS;
2679
+ }
2680
+ runCommand("hs", ["accounts", "use", portal.name], {
2681
+ cwd: ctx.projectRoot,
2682
+ logger: ctx.logger,
2683
+ dryRun: false
2684
+ });
2685
+ if (!ctx.global.json && !ctx.global.quiet) {
2686
+ printCommandDone(ctx, `Default portal \u2192 ${portal.name}`);
2687
+ } else if (ctx.global.json) {
2688
+ ctx.logger.json({
2689
+ command: "portal:use",
2690
+ ok: true,
2691
+ defaultPortal: portal.name,
2692
+ portalId: portal.portalId ?? null
2693
+ });
2694
+ }
2695
+ return ExitCode.SUCCESS;
2696
+ }
2697
+
1676
2698
  // src/cli/commands/watch.ts
1677
2699
  async function runWatch(ctx, options) {
1678
- ensureHubSpotCliSetup(ctx, "cms");
2700
+ ensureHubSpotConfigForCms(ctx);
1679
2701
  const tier = options.tier ?? "dev";
1680
- const args = [];
1681
- if (tier === "prod") args.push("--prod");
2702
+ const args = ["--tier", tier];
1682
2703
  if (options.rebuild) args.push("--rebuild");
1683
- try {
1684
- runNodeScript(kitPath("scripts", "watch-hubspot.mjs"), args, {
1685
- cwd: ctx.projectRoot,
1686
- env: { ...process.env, WP_HS_PROJECT_ROOT: ctx.projectRoot },
1687
- logger: ctx.logger,
1688
- dryRun: ctx.global.dryRun
1689
- });
1690
- return ExitCode.SUCCESS;
1691
- } catch (error) {
1692
- throw new WpHsError("Watch failed", {
2704
+ const code = await runNodeScriptLongRunning(kitPath("scripts", "watch-hubspot.mjs"), args, {
2705
+ cwd: ctx.projectRoot,
2706
+ env: { ...process.env, WP_HS_PROJECT_ROOT: ctx.projectRoot },
2707
+ logger: ctx.logger,
2708
+ dryRun: ctx.global.dryRun
2709
+ });
2710
+ if (code === 130 || code === 143) {
2711
+ return code;
2712
+ }
2713
+ if (code !== ExitCode.SUCCESS) {
2714
+ throw new WpHsError(`Watch exited with code ${code}`, {
1693
2715
  exitCode: ExitCode.BUILD_FAILED,
1694
- cause: error,
1695
- remediation: "Ensure HubSpot module is built once before watch, or pass --rebuild."
2716
+ remediation: "Run wp build once, then wp watch. For src/ changes use wp watch --rebuild. Check hubspot.config.yml and hs cms watch output."
1696
2717
  });
1697
2718
  }
2719
+ return ExitCode.SUCCESS;
1698
2720
  }
1699
2721
 
1700
2722
  // src/cli/program.ts
1701
2723
  function readVersion() {
1702
2724
  try {
1703
- const pkg = JSON.parse(readFileSync10(resolve17(getKitRoot(), "package.json"), "utf8"));
2725
+ const pkg = JSON.parse(readFileSync14(resolve21(getKitRoot(), "package.json"), "utf8"));
1704
2726
  return pkg.version ?? "0.0.0";
1705
2727
  } catch {
1706
2728
  return "0.0.0";
1707
2729
  }
1708
2730
  }
1709
- function parseTier(value) {
1710
- if (value === "dev" || value === "stg" || value === "prod") return value;
1711
- return "prod";
1712
- }
1713
2731
  function buildContext(global) {
1714
2732
  const logger = createLogger({
1715
2733
  verbose: global.verbose,
@@ -1750,33 +2768,45 @@ function createProgram() {
1750
2768
  const global = program2.opts();
1751
2769
  await handle(() => runDoctor(buildContext(global)));
1752
2770
  });
1753
- const configCmd = program2.command("config").description("Project configuration");
1754
- configCmd.command("validate").description("Validate woodscli.json").action(async () => {
1755
- const global = program2.opts();
1756
- await handle(() => runConfigValidate(buildContext(global)));
1757
- });
1758
- 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)").action(async (opts) => {
2771
+ program2.command("setup").description("Copy missing hubspot.config.yml, .env.*, CDN config, and vite shims from kit templates").option("--tier <tier>", "dev | stg | prod \u2014 copy only this .env file (default: all env.tiers)").action(async (opts) => {
1759
2772
  const global = program2.opts();
1760
- const logger = createLogger({
1761
- verbose: global.verbose,
1762
- quiet: global.quiet,
1763
- json: global.json,
1764
- ci: global.ci
1765
- });
1766
- const ctx = {
1767
- projectRoot: process.cwd(),
1768
- config: DEFAULT_WOODSPORTAL_HS,
1769
- logger,
1770
- global
1771
- };
1772
2773
  await handle(
1773
- () => runInit(ctx, {
1774
- template: opts.template,
1775
- name: opts.name,
1776
- directory: opts.directory
2774
+ () => runSetup(buildContext(global), {
2775
+ tier: opts.tier ? parseBuildTier(opts.tier) : void 0
1777
2776
  })
1778
2777
  );
1779
2778
  });
2779
+ const configCmd = program2.command("config").description("Project configuration");
2780
+ configCmd.command("validate").description("Validate woodscli.json").action(async () => {
2781
+ const global = program2.opts();
2782
+ await handle(() => runConfigValidate(buildContext(global)));
2783
+ });
2784
+ 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").action(
2785
+ async (opts) => {
2786
+ const global = program2.opts();
2787
+ const logger = createLogger({
2788
+ verbose: global.verbose,
2789
+ quiet: global.quiet,
2790
+ json: global.json,
2791
+ ci: global.ci
2792
+ });
2793
+ const ctx = {
2794
+ projectRoot: process.cwd(),
2795
+ config: DEFAULT_WOODSPORTAL_HS,
2796
+ logger,
2797
+ global
2798
+ };
2799
+ const routerMode = opts.router === "browser" || opts.router === "none" || opts.router === "hash" ? opts.router : void 0;
2800
+ await handle(
2801
+ () => runInit(ctx, {
2802
+ template: opts.template,
2803
+ name: opts.name,
2804
+ directory: opts.directory,
2805
+ router: routerMode
2806
+ })
2807
+ );
2808
+ }
2809
+ );
1780
2810
  program2.command("adopt").description("Detect existing module/theme repo and add WoodsCLI config + .wphs/").action(async () => {
1781
2811
  const global = program2.opts();
1782
2812
  await handle(() => runAdopt(buildContext(global)));
@@ -1789,12 +2819,12 @@ function createProgram() {
1789
2819
  const global = program2.opts();
1790
2820
  await handle(() => runDev(buildContext(global), { port: Number(opts.port) }));
1791
2821
  });
1792
- program2.command("build").description("Build CDN + HubSpot module (or theme no-op)").option("--tier <tier>", "dev | stg | prod", "prod").option("--cdn-only", "CDN chunks only").option("--hubspot-only", "HubSpot module only").option("--cdn-esm", "Build CDN via ESM preset").action(
2822
+ program2.command("build").description("Build CDN + HubSpot module (or theme no-op)").option("--tier <tier>", "dev | stg | prod", DEFAULT_BUILD_TIER).option("--cdn-only", "CDN chunks only").option("--hubspot-only", "HubSpot module only").option("--cdn-esm", "Build CDN via ESM preset").action(
1793
2823
  async (opts) => {
1794
2824
  const global = program2.opts();
1795
2825
  await handle(
1796
2826
  () => runBuild(buildContext(global), {
1797
- tier: parseTier(opts.tier),
2827
+ tier: parseBuildTier(opts.tier),
1798
2828
  cdnOnly: opts.cdnOnly,
1799
2829
  hubspotOnly: opts.hubspotOnly,
1800
2830
  cdnEsm: opts.cdnEsm
@@ -1802,20 +2832,20 @@ function createProgram() {
1802
2832
  );
1803
2833
  }
1804
2834
  );
1805
- program2.command("deploy").description("Build, publish CDN, upload HubSpot module").option("--tier <tier>", "dev | stg | prod", "prod").option("--skip-publish", "Skip CDN publish").action(async (opts) => {
2835
+ program2.command("deploy").description("Build, publish CDN, upload HubSpot module").option("--tier <tier>", "dev | stg | prod", DEFAULT_BUILD_TIER).option("--skip-publish", "Skip CDN publish").action(async (opts) => {
1806
2836
  const global = program2.opts();
1807
2837
  await handle(
1808
2838
  () => runDeploy(buildContext(global), {
1809
- tier: parseTier(opts.tier),
2839
+ tier: parseBuildTier(opts.tier),
1810
2840
  skipPublish: opts.skipPublish
1811
2841
  })
1812
2842
  );
1813
2843
  });
1814
- program2.command("publish").description("Publish CDN artifacts to jsDelivr mirror").option("--tier <tier>", "dev | stg | prod", "prod").option("--dry-run", "Print publish plan without pushing").action(async (opts) => {
2844
+ program2.command("publish").description("Publish CDN artifacts to jsDelivr mirror").option("--tier <tier>", "dev | stg | prod", DEFAULT_BUILD_TIER).option("--dry-run", "Print publish plan without pushing").action(async (opts) => {
1815
2845
  const global = program2.opts();
1816
2846
  await handle(
1817
2847
  () => runPublish(buildContext(global), {
1818
- tier: parseTier(opts.tier),
2848
+ tier: parseBuildTier(opts.tier),
1819
2849
  dryRun: opts.dryRun
1820
2850
  })
1821
2851
  );
@@ -1871,9 +2901,9 @@ function createProgram() {
1871
2901
  () => runRegressionSmoke(buildContext(global), { skipDoctor: opts.skipDoctor })
1872
2902
  );
1873
2903
  });
1874
- program2.command("watch").description("Watch HubSpot module or theme folder and re-upload on change").option("--tier <tier>", "dev | stg | prod", "dev").option("--rebuild", "Rebuild module before watch").action(async (opts) => {
2904
+ program2.command("watch").description("Watch HubSpot module or theme folder and re-upload on change").option("--tier <tier>", "dev | stg | prod", DEFAULT_BUILD_TIER).option("--rebuild", "Rebuild module before watch").action(async (opts) => {
1875
2905
  const global = program2.opts();
1876
- const tier = parseTier(opts.tier);
2906
+ const tier = parseBuildTier(opts.tier);
1877
2907
  await handle(
1878
2908
  () => runWatch(buildContext(global), {
1879
2909
  tier: tier === "prod" ? "prod" : tier === "stg" ? "stg" : "dev",
@@ -1881,16 +2911,16 @@ function createProgram() {
1881
2911
  })
1882
2912
  );
1883
2913
  });
1884
- program2.command("upload").description("Upload built HubSpot module or theme (hs cms upload)").option("--tier <tier>", "dev | stg | prod", "prod").action(async (opts) => {
2914
+ program2.command("upload").description("Upload built HubSpot module or theme (hs cms upload)").option("--tier <tier>", "dev | stg | prod", DEFAULT_BUILD_TIER).action(async (opts) => {
1885
2915
  const global = program2.opts();
1886
2916
  await handle(
1887
- () => runUpload(buildContext(global), { tier: parseTier(opts.tier) })
2917
+ () => runUpload(buildContext(global), { tier: parseBuildTier(opts.tier) })
1888
2918
  );
1889
2919
  });
1890
2920
  program2.command("auth").description("Authenticate HubSpot CLI (hs auth)").action(async () => {
1891
2921
  const global = program2.opts();
1892
2922
  const ctx = buildContext(global);
1893
- const { runCommand: runCommand2 } = await import("./exec-K5C6LZP4.js");
2923
+ const { runCommand: runCommand2 } = await import("./exec-U2OJATR3.js");
1894
2924
  runCommand2("hs", ["auth"], {
1895
2925
  cwd: ctx.projectRoot,
1896
2926
  logger: ctx.logger,
@@ -1898,6 +2928,15 @@ function createProgram() {
1898
2928
  });
1899
2929
  process.exit(ExitCode.SUCCESS);
1900
2930
  });
2931
+ const portal = program2.command("portal").description("HubSpot default portal selection");
2932
+ portal.command("list").alias("ls").description("List portals from hubspot.config.yml").action(async () => {
2933
+ const global = program2.opts();
2934
+ await handle(() => runPortalList(buildContext(global)));
2935
+ });
2936
+ portal.command("use [name]").description("Set defaultPortal (hs accounts use)").action(async (name) => {
2937
+ const global = program2.opts();
2938
+ await handle(() => runPortalUse(buildContext(global), { name }));
2939
+ });
1901
2940
  return program2;
1902
2941
  }
1903
2942