@woodsportal/hubspot-kit 1.0.31 → 1.0.34

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 (57) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/dist/cli.js +798 -281
  3. package/dist/cli.js.map +1 -1
  4. package/dist/index.js +2 -2
  5. package/dist/index.js.map +1 -1
  6. package/examples/fixtures/adopted-module-hybrid/woodscli.json +1 -1
  7. package/examples/fixtures/adopted-module-monolith/woodscli.json +1 -1
  8. package/examples/greenfield/module-hybrid-cdn/public/fields.json +26 -20
  9. package/examples/greenfield/module-hybrid-cdn/public/meta.json +9 -0
  10. package/examples/greenfield/module-hybrid-cdn/public/module.html +3 -4
  11. package/examples/greenfield/module-hybrid-cdn/src/assets/css/main.prod.css +6 -0
  12. package/examples/greenfield/module-hybrid-cdn/src/cdn/app-entry.ts +5 -0
  13. package/examples/greenfield/module-hybrid-cdn/src/hubspot/bootstrap.ts +6 -0
  14. package/examples/greenfield/module-hybrid-cdn/src/portal/ensure-portal-cdn-stylesheet.ts +58 -0
  15. package/examples/greenfield/module-hybrid-cdn/woodscli.json +1 -1
  16. package/examples/greenfield/module-monolith/woodscli.json +1 -1
  17. package/package.json +2 -2
  18. package/scripts/build-cdn.mjs +6 -1
  19. package/scripts/bump-module-build-id.mjs +28 -0
  20. package/scripts/hubspot-module-postbuild.mjs +35 -1
  21. package/scripts/load-portal-cdn-config.mjs +21 -3
  22. package/scripts/portal-build-id.mjs +346 -0
  23. package/scripts/portal-build-id.test.mjs +132 -0
  24. package/scripts/publish-cdn-github.mjs +60 -15
  25. package/scripts/resolve-cdn-base-url.mjs +2 -1
  26. package/scripts/resolve-module-build-id-meta.mjs +7 -0
  27. package/scripts/resolve-portal-build-id.mjs +16 -56
  28. package/scripts/tailwind/prefix-plugin.js +46 -8
  29. package/scripts/tailwind/tailwind-content-plugin.js +0 -1
  30. package/scripts/vite/portal-build-shared.js +3 -1
  31. package/src/dev-module-config/__tests__/flatten-visible-fields.test.ts +107 -0
  32. package/src/dev-module-config/runtime/module-config-bridge.ts +10 -2
  33. package/src/dev-module-config/schema/types.ts +10 -0
  34. package/src/dev-module-config/schema/visibility.ts +42 -4
  35. package/src/dev-module-config/ui/ModuleConfigShell.tsx +10 -9
  36. package/src/dev-module-config/ui/hubspot/HsOccurrenceGroupEditor.tsx +3 -3
  37. package/templates/module-hybrid-cdn/public/fields.json +52 -41
  38. package/templates/module-hybrid-cdn/public/meta.json +9 -0
  39. package/templates/module-hybrid-cdn/public/module.html +18 -4
  40. package/templates/module-hybrid-cdn/src/assets/css/main.dev.css +8 -0
  41. package/templates/module-hybrid-cdn/src/assets/css/main.prod.css +12 -0
  42. package/templates/module-hybrid-cdn/src/assets/css/tailwind.css.config.css +7 -0
  43. package/templates/module-hybrid-cdn/src/cdn/app-entry.ts +14 -3
  44. package/templates/module-hybrid-cdn/src/features/starter/components/starter-home.tsx +9 -11
  45. package/templates/module-hybrid-cdn/src/features/starter/hooks/starter-preview-values.ts +23 -5
  46. package/templates/module-hybrid-cdn/src/features/starter/hooks/use-starter-preview.dev.ts +1 -0
  47. package/templates/module-hybrid-cdn/src/hubspot/bootstrap.ts +6 -0
  48. package/templates/module-hybrid-cdn/src/portal/ensure-portal-cdn-stylesheet.ts +115 -0
  49. package/templates/module-hybrid-cdn/src/portal/mount-portal.tsx +35 -5
  50. package/templates/module-hybrid-cdn/woodscli.json +1 -1
  51. package/templates/module-monolith/public/fields.json +23 -16
  52. package/templates/module-monolith/public/meta.json +9 -0
  53. package/templates/module-monolith/src/features/starter/components/starter-home.tsx +9 -11
  54. package/templates/module-monolith/src/features/starter/hooks/starter-preview-values.ts +23 -5
  55. package/templates/module-monolith/src/features/starter/hooks/use-starter-preview.dev.ts +1 -0
  56. package/templates/module-monolith/woodscli.json +1 -1
  57. package/vite/vite.config.dev.mjs +16 -10
package/dist/cli.js CHANGED
@@ -11,8 +11,8 @@ import {
11
11
  } from "./chunk-4GOJPOW3.js";
12
12
 
13
13
  // src/cli/program.ts
14
- import { readFileSync as readFileSync15 } from "fs";
15
- import { resolve as resolve23 } from "path";
14
+ import { readFileSync as readFileSync16 } from "fs";
15
+ import { resolve as resolve24 } from "path";
16
16
  import { Command } from "commander";
17
17
 
18
18
  // src/lib/config.ts
@@ -90,7 +90,7 @@ var WoodsPortalHsSchema = z.object({
90
90
  }).optional(),
91
91
  cdn: z.object({
92
92
  mode: z.enum(["jsdelivr", "custom", "local", "none"]).default("jsdelivr"),
93
- buildIdFile: z.string().default(".portal-build-id"),
93
+ buildIdFile: z.string().default(".module-build-id"),
94
94
  mirror: z.string().default("portal.cdn.json")
95
95
  }).default({}),
96
96
  entries: z.object({
@@ -125,7 +125,7 @@ var DEFAULT_WOODSPORTAL_HS = {
125
125
  },
126
126
  cdn: {
127
127
  mode: "jsdelivr",
128
- buildIdFile: ".portal-build-id",
128
+ buildIdFile: ".module-build-id",
129
129
  mirror: "portal.cdn.json"
130
130
  },
131
131
  entries: {
@@ -302,8 +302,8 @@ function defaultBaselineDir(projectRoot, config) {
302
302
  }
303
303
 
304
304
  // src/cli/commands/doctor.ts
305
- import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
306
- import { join, resolve as resolve6 } from "path";
305
+ import { existsSync as existsSync7, readFileSync as readFileSync5 } from "fs";
306
+ import { join, resolve as resolve8 } from "path";
307
307
 
308
308
  // src/lib/cli-spinner.ts
309
309
  import * as p from "@clack/prompts";
@@ -321,11 +321,11 @@ function isVerboseStepLog(ctx) {
321
321
  return ctx.global.verbose === true && !ctx.global.quiet && !ctx.global.json;
322
322
  }
323
323
  async function yieldForSpinnerPaint() {
324
- await new Promise((resolve24) => {
325
- setImmediate(resolve24);
324
+ await new Promise((resolve25) => {
325
+ setImmediate(resolve25);
326
326
  });
327
- await new Promise((resolve24) => {
328
- setTimeout(resolve24, 48);
327
+ await new Promise((resolve25) => {
328
+ setTimeout(resolve25, 48);
329
329
  });
330
330
  }
331
331
  async function runCliTasks(ctx, tasks, options = {}) {
@@ -653,6 +653,13 @@ function validateFieldNode(node, path, issues) {
653
653
  message: type === "textarea" ? `'textarea' is not a HubSpot field type \u2014 use "type": "text" with "display": "textarea"` : `'${type}' is not a valid HubSpot field type`
654
654
  });
655
655
  }
656
+ const tab = node.tab;
657
+ if (tab === "STYLE" && type !== "group") {
658
+ issues.push({
659
+ path: `${path}.tab`,
660
+ message: 'STYLE tab fields must live inside a "type": "group" with "tab": "STYLE" (HubSpot page editor Styles tab)'
661
+ });
662
+ }
656
663
  if (type !== "group") {
657
664
  const name = node.name;
658
665
  if (typeof name !== "string" || name.trim().length === 0) {
@@ -684,14 +691,148 @@ function validateHubSpotFieldsJson(fields) {
684
691
  return issues;
685
692
  }
686
693
 
687
- // src/lib/upload-only-module.ts
688
- import { existsSync as existsSync4 } from "fs";
694
+ // src/lib/developer-slug.ts
695
+ import { existsSync as existsSync4, mkdirSync, readFileSync as readFileSync4, writeFileSync } from "fs";
689
696
  import { resolve as resolve5 } from "path";
697
+ import * as p2 from "@clack/prompts";
698
+ var SLUG_MAX_LEN = 12;
699
+ var SLUG_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,10}[a-z0-9])?$/;
700
+ var DEVELOPER_CONFIG_FILE = "developer.json";
701
+ function developerConfigPath(projectRoot) {
702
+ return resolve5(projectRoot, ".wphs", "config", DEVELOPER_CONFIG_FILE);
703
+ }
704
+ function sanitizeDevSlug(input) {
705
+ if (!input?.trim()) return void 0;
706
+ const slug = input.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, SLUG_MAX_LEN);
707
+ if (!slug || !SLUG_PATTERN.test(slug)) return void 0;
708
+ return slug;
709
+ }
710
+ function readDeveloperSlug(projectRoot) {
711
+ const fromEnv = sanitizeDevSlug(process.env.PORTAL_CDN_DEV_SLUG) ?? sanitizeDevSlug(process.env.MODULE_CDN_DEV_SLUG);
712
+ if (fromEnv) return fromEnv;
713
+ const path = developerConfigPath(projectRoot);
714
+ if (!existsSync4(path)) return void 0;
715
+ try {
716
+ const raw = JSON.parse(readFileSync4(path, "utf8"));
717
+ return sanitizeDevSlug(raw.slug);
718
+ } catch {
719
+ return void 0;
720
+ }
721
+ }
722
+ function writeDeveloperSlug(projectRoot, slug) {
723
+ const normalized = sanitizeDevSlug(slug);
724
+ if (!normalized) {
725
+ throw new Error("Invalid CDN dev slug \u2014 use lowercase letters, numbers, and hyphens (max 12 chars)");
726
+ }
727
+ const path = developerConfigPath(projectRoot);
728
+ mkdirSync(resolve5(path, ".."), { recursive: true });
729
+ writeFileSync(path, `${JSON.stringify({ slug: normalized }, null, 2)}
730
+ `);
731
+ }
732
+ function needsDeveloperSlugSetup(projectRoot) {
733
+ return !readDeveloperSlug(projectRoot);
734
+ }
735
+ async function promptDeveloperSlug(projectRoot) {
736
+ if (!needsDeveloperSlugSetup(projectRoot)) {
737
+ return readDeveloperSlug(projectRoot);
738
+ }
739
+ const slug = await p2.text({
740
+ message: "CDN dev slug (unique per developer for shared CDN mirror \u2014 e.g. your first name)",
741
+ placeholder: "manab",
742
+ validate: (value) => {
743
+ if (!sanitizeDevSlug(value)) {
744
+ return "Use 1\u201312 lowercase letters, numbers, or hyphens";
745
+ }
746
+ return void 0;
747
+ }
748
+ });
749
+ if (p2.isCancel(slug) || typeof slug !== "string") {
750
+ return void 0;
751
+ }
752
+ writeDeveloperSlug(projectRoot, slug);
753
+ return sanitizeDevSlug(slug);
754
+ }
755
+
756
+ // src/lib/module-build-id.ts
757
+ import { spawnSync as spawnSync2 } from "child_process";
758
+ function runKitScript(script, args, options) {
759
+ const result = spawnSync2(process.execPath, [kitPath("scripts", script), ...args], {
760
+ cwd: options.cwd,
761
+ env: {
762
+ ...options.env,
763
+ WP_HS_PROJECT_ROOT: options.cwd
764
+ },
765
+ encoding: "utf8"
766
+ });
767
+ if (result.status !== 0) {
768
+ const stderr = result.stderr?.trim() || result.stdout?.trim();
769
+ throw new Error(stderr || `Failed running ${script}`);
770
+ }
771
+ return (result.stdout ?? "").trim();
772
+ }
773
+ function bumpModuleBuildIdForCli(projectRoot, options = {}) {
774
+ const args = [...options.tier ? [`--tier=${options.tier}`] : [], ...options.dryRun ? ["--dry-run"] : []];
775
+ const stdout = runKitScript("bump-module-build-id.mjs", args, {
776
+ cwd: projectRoot,
777
+ env: options.env ?? process.env,
778
+ dryRun: options.dryRun
779
+ });
780
+ return JSON.parse(stdout);
781
+ }
782
+ function resolveModuleBuildIdMetaForCli(projectRoot, env = process.env) {
783
+ const stdout = runKitScript("resolve-module-build-id-meta.mjs", [], {
784
+ cwd: projectRoot,
785
+ env
786
+ });
787
+ return JSON.parse(stdout);
788
+ }
789
+
790
+ // src/lib/upload-only-module.ts
791
+ import { existsSync as existsSync5 } from "fs";
792
+ import { resolve as resolve6 } from "path";
690
793
  function isUploadOnlyPreexistingProject(projectRoot, moduleArtifactDirs) {
691
794
  if (moduleArtifactDirs.length === 0) return false;
692
- if (existsSync4(resolve5(projectRoot, "package.json"))) return false;
693
- if (existsSync4(resolve5(projectRoot, "src"))) return false;
694
- return moduleArtifactDirs.some((dir) => existsSync4(resolve5(dir, "fields.json")));
795
+ if (existsSync5(resolve6(projectRoot, "package.json"))) return false;
796
+ if (existsSync5(resolve6(projectRoot, "src"))) return false;
797
+ return moduleArtifactDirs.some((dir) => existsSync5(resolve6(dir, "fields.json")));
798
+ }
799
+
800
+ // src/vite/resolve-dev-config.ts
801
+ import { existsSync as existsSync6 } from "fs";
802
+ import { platform } from "os";
803
+ import { resolve as resolve7 } from "path";
804
+ var CONSUMER_DEV_CONFIG = "vite.config.dev.mjs";
805
+ var VITE_JS_SEGMENTS = ["node_modules", "vite", "bin", "vite.js"];
806
+ function resolveDevViteConfigPath(projectRoot) {
807
+ const consumerConfig = resolve7(projectRoot, CONSUMER_DEV_CONFIG);
808
+ if (existsSync6(consumerConfig)) {
809
+ return consumerConfig;
810
+ }
811
+ return kitPath("vite", "vite.config.dev.mjs");
812
+ }
813
+ function isProjectViteInstalled(projectRoot) {
814
+ return existsSync6(resolve7(projectRoot, ...VITE_JS_SEGMENTS));
815
+ }
816
+ function resolveProjectViteSpawn(projectRoot) {
817
+ const viteJs = resolve7(projectRoot, ...VITE_JS_SEGMENTS);
818
+ if (existsSync6(viteJs)) {
819
+ return { command: process.execPath, prefixArgs: [viteJs] };
820
+ }
821
+ const binDir = resolve7(projectRoot, "node_modules", ".bin");
822
+ if (platform() === "win32") {
823
+ const winCmd = resolve7(binDir, "vite.cmd");
824
+ if (existsSync6(winCmd)) {
825
+ return { command: winCmd, prefixArgs: [] };
826
+ }
827
+ }
828
+ const posixShim = resolve7(binDir, "vite");
829
+ if (existsSync6(posixShim)) {
830
+ return { command: posixShim, prefixArgs: [] };
831
+ }
832
+ return null;
833
+ }
834
+ function formatProjectViteSpawn(spawn2) {
835
+ return [spawn2.command, ...spawn2.prefixArgs].join(" ");
695
836
  }
696
837
 
697
838
  // src/cli/commands/doctor.ts
@@ -759,8 +900,8 @@ function checkConfig(ctx) {
759
900
  function checkEnvFiles(ctx) {
760
901
  return ctx.config.env.tiers.map((tier) => {
761
902
  const file = tier === "dev" ? ".env.dev" : tier === "stg" ? ".env.stg" : ".env.prod";
762
- const path = resolve6(ctx.projectRoot, file);
763
- const ok = existsSync5(path);
903
+ const path = resolve8(ctx.projectRoot, file);
904
+ const ok = existsSync7(path);
764
905
  return {
765
906
  name: `env-${tier}`,
766
907
  ok,
@@ -770,20 +911,76 @@ function checkEnvFiles(ctx) {
770
911
  };
771
912
  });
772
913
  }
914
+ function checkLegacyBuildIdFile(ctx) {
915
+ if (isThemeProject(ctx.config) || !usesCdnPipeline(ctx.config)) return null;
916
+ const legacy = resolve8(ctx.projectRoot, ".portal-build-id");
917
+ if (!existsSync7(legacy)) return null;
918
+ return {
919
+ name: "legacy-build-id",
920
+ ok: false,
921
+ message: "Legacy .portal-build-id present \u2014 use .module-build-id",
922
+ remediation: "Run wp migrate --yes",
923
+ severity: "warn"
924
+ };
925
+ }
773
926
  function checkBuildId(ctx) {
774
927
  if (isThemeProject(ctx.config) || !usesCdnPipeline(ctx.config)) return null;
775
- const file = resolve6(ctx.projectRoot, ctx.config.cdn.buildIdFile);
776
- const ok = existsSync5(file);
928
+ const file = resolve8(ctx.projectRoot, ctx.config.cdn.buildIdFile);
929
+ const ok = existsSync7(file);
777
930
  return {
778
931
  name: "build-id",
779
932
  ok,
780
933
  message: ok ? `${ctx.config.cdn.buildIdFile} present` : `${ctx.config.cdn.buildIdFile} missing`,
781
- remediation: ok ? void 0 : "wp setup (creates default) or set your release tag (e.g. v0.0.1-dev.1)",
934
+ remediation: ok ? void 0 : "wp setup (creates channel baseline e.g. v0.0.1-dev)",
935
+ severity: "warn"
936
+ };
937
+ }
938
+ function checkModuleBuildIdResolved(ctx) {
939
+ if (isThemeProject(ctx.config) || !usesCdnPipeline(ctx.config)) return null;
940
+ try {
941
+ const meta = resolveModuleBuildIdMetaForCli(ctx.projectRoot, process.env);
942
+ const detail = meta.source === "state" ? `${meta.id} (state: ${meta.slug} #${meta.seq})` : `${meta.id} (source: ${meta.source})`;
943
+ return {
944
+ name: "module-build-id",
945
+ ok: true,
946
+ message: `Effective module build id: ${detail}`,
947
+ severity: "warn"
948
+ };
949
+ } catch (error) {
950
+ const message = error instanceof Error ? error.message : String(error);
951
+ return {
952
+ name: "module-build-id",
953
+ ok: false,
954
+ message: `Could not resolve module build id \u2014 ${message}`,
955
+ remediation: "Run wp setup and set a CDN dev slug (or PORTAL_CDN_DEV_SLUG)",
956
+ severity: "warn"
957
+ };
958
+ }
959
+ }
960
+ function checkDevSlug(ctx) {
961
+ if (isThemeProject(ctx.config) || !usesCdnPipeline(ctx.config)) return null;
962
+ if (process.env.PORTAL_BUILD_ID?.trim() || process.env.MODULE_BUILD_ID?.trim()) {
963
+ return null;
964
+ }
965
+ const slug = readDeveloperSlug(ctx.projectRoot);
966
+ if (slug) {
967
+ return {
968
+ name: "cdn-dev-slug",
969
+ ok: true,
970
+ message: `CDN dev slug: ${slug}`,
971
+ severity: "warn"
972
+ };
973
+ }
974
+ return {
975
+ name: "cdn-dev-slug",
976
+ ok: false,
977
+ message: "CDN dev slug not configured (shared CDN mirror needs per-developer ids)",
978
+ remediation: "Run wp setup, set PORTAL_CDN_DEV_SLUG, or configure git user.name for automatic slug",
782
979
  severity: "warn"
783
980
  };
784
981
  }
785
982
  function isUploadOnlyProject(ctx) {
786
- const moduleDirs = [resolve6(ctx.projectRoot, ctx.config.module.uploadPath)];
983
+ const moduleDirs = [resolve8(ctx.projectRoot, ctx.config.module.uploadPath)];
787
984
  return isUploadOnlyPreexistingProject(ctx.projectRoot, moduleDirs);
788
985
  }
789
986
  function checkUploadOnlyNote(ctx) {
@@ -797,8 +994,8 @@ function checkUploadOnlyNote(ctx) {
797
994
  };
798
995
  }
799
996
  function checkDevViteConfig(ctx) {
800
- const path = resolve6(ctx.projectRoot, "vite.config.dev.mjs");
801
- const ok = existsSync5(path);
997
+ const path = resolve8(ctx.projectRoot, "vite.config.dev.mjs");
998
+ const ok = existsSync7(path);
802
999
  return {
803
1000
  name: "vite-dev-config",
804
1001
  ok,
@@ -807,10 +1004,154 @@ function checkDevViteConfig(ctx) {
807
1004
  severity: "error"
808
1005
  };
809
1006
  }
1007
+ function checkMetaJson(ctx) {
1008
+ if (isThemeProject(ctx.config)) return null;
1009
+ const path = resolve8(ctx.projectRoot, "public/meta.json");
1010
+ if (!existsSync7(path)) {
1011
+ return {
1012
+ name: "meta-json",
1013
+ ok: false,
1014
+ message: "public/meta.json missing",
1015
+ remediation: "Copy meta.json from @woodsportal/hubspot-kit templates (content_types + module label for page editor)",
1016
+ severity: "warn"
1017
+ };
1018
+ }
1019
+ try {
1020
+ const meta = JSON.parse(readFileSync5(path, "utf8"));
1021
+ const hasContentTypes = Array.isArray(meta.content_types) && meta.content_types.length > 0;
1022
+ return {
1023
+ name: "meta-json",
1024
+ ok: hasContentTypes,
1025
+ message: hasContentTypes ? "public/meta.json includes content_types" : "public/meta.json missing content_types (module may not appear on site pages)",
1026
+ remediation: hasContentTypes ? void 0 : 'Add content_types: ["SITE_PAGE", "LANDING_PAGE", ...] \u2014 see templates/module-hybrid-cdn/public/meta.json',
1027
+ severity: "warn"
1028
+ };
1029
+ } catch (error) {
1030
+ const message = error instanceof Error ? error.message : String(error);
1031
+ return {
1032
+ name: "meta-json",
1033
+ ok: false,
1034
+ message: `public/meta.json is invalid JSON \u2014 ${message}`,
1035
+ remediation: "Fix JSON syntax in public/meta.json",
1036
+ severity: "warn"
1037
+ };
1038
+ }
1039
+ }
1040
+ async function checkCdnMirrorPublic(ctx) {
1041
+ if (isThemeProject(ctx.config) || !usesCdnPipeline(ctx.config)) return null;
1042
+ const mirrorPath = resolve8(ctx.projectRoot, ctx.config.cdn?.mirror ?? "portal.cdn.json");
1043
+ if (!existsSync7(mirrorPath)) {
1044
+ return {
1045
+ name: "cdn-mirror-public",
1046
+ ok: false,
1047
+ message: "portal.cdn.json missing (hybrid CDN publish config)",
1048
+ remediation: "Run wp setup or wp init to configure the public GitHub CDN mirror",
1049
+ severity: "error"
1050
+ };
1051
+ }
1052
+ let github;
1053
+ try {
1054
+ const mirror = JSON.parse(readFileSync5(mirrorPath, "utf8"));
1055
+ github = mirror.github?.trim();
1056
+ } catch {
1057
+ return {
1058
+ name: "cdn-mirror-public",
1059
+ ok: false,
1060
+ message: "portal.cdn.json is invalid JSON",
1061
+ remediation: "Fix portal.cdn.json or re-run wp setup",
1062
+ severity: "error"
1063
+ };
1064
+ }
1065
+ if (!github) {
1066
+ return {
1067
+ name: "cdn-mirror-public",
1068
+ ok: false,
1069
+ message: "portal.cdn.json missing github (org/repo)",
1070
+ remediation: "Set github to a public repo for jsDelivr (e.g. Digital-Woods/woodsportal-client-frontend-cdn)",
1071
+ severity: "error"
1072
+ };
1073
+ }
1074
+ try {
1075
+ const res = await fetch(`https://api.github.com/repos/${github}`, {
1076
+ headers: { Accept: "application/vnd.github+json" }
1077
+ });
1078
+ if (res.status === 404) {
1079
+ return {
1080
+ name: "cdn-mirror-public",
1081
+ ok: false,
1082
+ message: `GitHub CDN repo ${github} not found`,
1083
+ remediation: "Create the repo or fix portal.cdn.json github slug",
1084
+ severity: "error"
1085
+ };
1086
+ }
1087
+ const data = await res.json();
1088
+ if (data.private === true) {
1089
+ return {
1090
+ name: "cdn-mirror-public",
1091
+ ok: false,
1092
+ message: `CDN mirror ${github} is private \u2014 jsDelivr returns 404 (blank HubSpot preview)`,
1093
+ remediation: "Make the GitHub repo public (Settings \u2192 Change visibility) or point portal.cdn.json at a public mirror",
1094
+ severity: "error"
1095
+ };
1096
+ }
1097
+ } catch (error) {
1098
+ const message = error instanceof Error ? error.message : String(error);
1099
+ return {
1100
+ name: "cdn-mirror-public",
1101
+ ok: false,
1102
+ message: `Could not verify CDN repo ${github} \u2014 ${message}`,
1103
+ severity: "warn"
1104
+ };
1105
+ }
1106
+ const manifestPath = resolve8(ctx.projectRoot, "dist/cdn/manifest.json");
1107
+ if (!existsSync7(manifestPath)) {
1108
+ return {
1109
+ name: "cdn-mirror-public",
1110
+ ok: true,
1111
+ message: `CDN mirror ${github} is public (run wp build to verify jsDelivr URLs)`,
1112
+ severity: "error"
1113
+ };
1114
+ }
1115
+ try {
1116
+ const manifest = JSON.parse(readFileSync5(manifestPath, "utf8"));
1117
+ const appUrl = manifest.scripts?.find((s) => s.name === "app")?.url;
1118
+ if (!appUrl) {
1119
+ return {
1120
+ name: "cdn-mirror-public",
1121
+ ok: true,
1122
+ message: `CDN mirror ${github} is public`,
1123
+ severity: "error"
1124
+ };
1125
+ }
1126
+ const head = await fetch(appUrl, { method: "HEAD" });
1127
+ if (!head.ok) {
1128
+ return {
1129
+ name: "cdn-mirror-public",
1130
+ ok: false,
1131
+ message: `jsDelivr app chunk not reachable (HTTP ${head.status}) \u2014 HubSpot preview will be blank`,
1132
+ remediation: "Run wp deploy (publish CDN + upload module) after making the mirror repo public",
1133
+ severity: "error"
1134
+ };
1135
+ }
1136
+ return {
1137
+ name: "cdn-mirror-public",
1138
+ ok: true,
1139
+ message: `CDN mirror ${github} is public and jsDelivr app chunk is reachable`,
1140
+ severity: "error"
1141
+ };
1142
+ } catch {
1143
+ return {
1144
+ name: "cdn-mirror-public",
1145
+ ok: true,
1146
+ message: `CDN mirror ${github} is public`,
1147
+ severity: "error"
1148
+ };
1149
+ }
1150
+ }
810
1151
  function checkFieldsJson(ctx) {
811
1152
  if (isThemeProject(ctx.config)) return null;
812
- const path = resolve6(ctx.projectRoot, "public/fields.json");
813
- if (!existsSync5(path)) {
1153
+ const path = resolve8(ctx.projectRoot, "public/fields.json");
1154
+ if (!existsSync7(path)) {
814
1155
  return {
815
1156
  name: "fields-json",
816
1157
  ok: false,
@@ -820,7 +1161,7 @@ function checkFieldsJson(ctx) {
820
1161
  };
821
1162
  }
822
1163
  try {
823
- const fields = JSON.parse(readFileSync4(path, "utf8"));
1164
+ const fields = JSON.parse(readFileSync5(path, "utf8"));
824
1165
  const issues = validateHubSpotFieldsJson(fields);
825
1166
  if (issues.length === 0) {
826
1167
  return {
@@ -849,10 +1190,97 @@ function checkFieldsJson(ctx) {
849
1190
  };
850
1191
  }
851
1192
  }
1193
+ function readProjectFile(projectRoot, relativePath) {
1194
+ const path = resolve8(projectRoot, relativePath);
1195
+ if (!existsSync7(path)) {
1196
+ return null;
1197
+ }
1198
+ return readFileSync5(path, "utf8");
1199
+ }
1200
+ function checkCssPipeline(ctx) {
1201
+ if (isThemeProject(ctx.config) || isUploadOnlyProject(ctx)) {
1202
+ return [];
1203
+ }
1204
+ const checks = [];
1205
+ const mainDev = readProjectFile(ctx.projectRoot, "src/assets/css/main.dev.css");
1206
+ const mainProd = readProjectFile(ctx.projectRoot, "src/assets/css/main.prod.css");
1207
+ const appEntry = readProjectFile(ctx.projectRoot, "src/cdn/app-entry.ts");
1208
+ if (mainDev) {
1209
+ const withoutComments = mainDev.replace(/\/\*[\s\S]*?\*\//g, "").replace(/^\s*\/\/.*$/gm, "");
1210
+ const hasPrefix = /@import\s+['"]tailwindcss['"]\s+prefix\s*\(\s*dw\s*\)/i.test(
1211
+ withoutComments
1212
+ );
1213
+ checks.push({
1214
+ name: "css-main-dev-unprefixed",
1215
+ ok: !hasPrefix,
1216
+ message: hasPrefix ? "main.dev.css uses prefix(dw) \u2014 local wp dev will not match HubSpot builds" : "main.dev.css is unprefixed (local dev)",
1217
+ remediation: hasPrefix ? "Remove prefix(dw) from main.dev.css; use main.prod.css for HubSpot/CDN builds only" : void 0,
1218
+ severity: "error"
1219
+ });
1220
+ }
1221
+ if (mainProd) {
1222
+ const withoutComments = mainProd.replace(/\/\*[\s\S]*?\*\//g, "").replace(/^\s*\/\/.*$/gm, "");
1223
+ const hasPrefix = /@import\s+['"]tailwindcss['"]\s+prefix\s*\(\s*dw\s*\)/i.test(
1224
+ withoutComments
1225
+ );
1226
+ const scansTemp = /@source\s+['"][^'"]*\.temp/.test(mainProd);
1227
+ checks.push({
1228
+ name: "css-main-prod-prefixed",
1229
+ ok: hasPrefix,
1230
+ message: hasPrefix ? "main.prod.css uses prefix(dw) (HubSpot/CDN)" : "main.prod.css missing prefix(dw)",
1231
+ remediation: hasPrefix ? void 0 : "Add @import 'tailwindcss' prefix(dw) to main.prod.css",
1232
+ severity: "error"
1233
+ });
1234
+ checks.push({
1235
+ name: "css-main-prod-temp-source",
1236
+ ok: scansTemp,
1237
+ message: scansTemp ? "main.prod.css scans .temp/ for prefixed utilities" : "main.prod.css missing @source for .temp/",
1238
+ remediation: scansTemp ? void 0 : "Add @source '../../../.temp/**/*.{js,jsx,ts,tsx}' to main.prod.css",
1239
+ severity: "error"
1240
+ });
1241
+ }
1242
+ if (usesCdnPipeline(ctx.config) && appEntry) {
1243
+ const skipsRuntimeCss = /skipRuntimeCss\s*:\s*true/.test(appEntry) || /mountPortal\s*\(\s*\{[^}]*skipRuntimeCss\s*:\s*true/.test(appEntry);
1244
+ checks.push({
1245
+ name: "css-app-entry-skip-runtime",
1246
+ ok: skipsRuntimeCss,
1247
+ message: skipsRuntimeCss ? "cdn/app-entry.ts skips runtime CSS (relies on module.css)" : "cdn/app-entry.ts should call mountPortal({ skipRuntimeCss: true })",
1248
+ remediation: skipsRuntimeCss ? void 0 : "Update src/cdn/app-entry.ts \u2014 see templates/module-hybrid-cdn/src/cdn/app-entry.ts",
1249
+ severity: "error"
1250
+ });
1251
+ }
1252
+ const moduleHtml = readProjectFile(ctx.projectRoot, "public/module.html");
1253
+ if (moduleHtml && usesCdnPipeline(ctx.config)) {
1254
+ const loadsCdnAppCss = /require_css\s*\([^)]*portal_app_css/i.test(moduleHtml) || /require_css\s*\([^)]*PORTAL_APP_CSS/i.test(moduleHtml);
1255
+ checks.push({
1256
+ name: "css-module-html-no-cdn-require",
1257
+ ok: !loadsCdnAppCss,
1258
+ message: loadsCdnAppCss ? "module.html require_css for CDN app CSS may double-load with merged module.css" : "module.html relies on merged module.css (no CDN require_css)",
1259
+ remediation: loadsCdnAppCss ? "Remove require_css for app.*.css; wp build merges app CSS into module.css" : void 0,
1260
+ severity: "warn"
1261
+ });
1262
+ }
1263
+ const moduleOutDir = ctx.config.module?.outDir;
1264
+ if (moduleOutDir && usesCdnPipeline(ctx.config)) {
1265
+ const moduleCssPath = resolve8(ctx.projectRoot, moduleOutDir, "module.css");
1266
+ if (existsSync7(moduleCssPath)) {
1267
+ const css = readFileSync5(moduleCssPath, "utf8");
1268
+ const hasDwUtilities = /dw\\:/.test(css) || /\.dw:/.test(css);
1269
+ checks.push({
1270
+ name: "css-module-dist-prefixed",
1271
+ ok: hasDwUtilities,
1272
+ message: hasDwUtilities ? `${moduleOutDir}/module.css contains dw-prefixed utilities` : `${moduleOutDir}/module.css missing dw: utilities \u2014 run wp build`,
1273
+ remediation: hasDwUtilities ? void 0 : "Run wp build --tier dev (or stg/prod) before upload",
1274
+ severity: "warn"
1275
+ });
1276
+ }
1277
+ }
1278
+ return checks;
1279
+ }
852
1280
  function checkHybridModuleHtml(ctx) {
853
1281
  if (isThemeProject(ctx.config) || !usesCdnPipeline(ctx.config)) return null;
854
- const path = resolve6(ctx.projectRoot, "public/module.html");
855
- if (!existsSync5(path)) {
1282
+ const path = resolve8(ctx.projectRoot, "public/module.html");
1283
+ if (!existsSync7(path)) {
856
1284
  return {
857
1285
  name: "module-html",
858
1286
  ok: false,
@@ -861,8 +1289,8 @@ function checkHybridModuleHtml(ctx) {
861
1289
  severity: "error"
862
1290
  };
863
1291
  }
864
- const html = readFileSync4(path, "utf8");
865
- const needs = ["__PORTAL_CDN_BASE__", "require_js", "require_css", "hubSpotData", 'id="app"'];
1292
+ const html = readFileSync5(path, "utf8");
1293
+ const needs = ["__PORTAL_CDN_BASE__", "require_js", "hubSpotData", 'id="app"'];
866
1294
  const missing = needs.filter((token) => !html.includes(token));
867
1295
  const usesUndeclaredCdnFields = /module\.portal_(cdn_base_url|vendor_script|app_script|app_css)/.test(
868
1296
  html
@@ -879,8 +1307,8 @@ function checkHybridModuleHtml(ctx) {
879
1307
  function checkCdnViteConfigs(ctx) {
880
1308
  if (isThemeProject(ctx.config) || !usesCdnPipeline(ctx.config)) return [];
881
1309
  return ["vite.config.cdn-vendor.mjs", "vite.config.cdn-app.mjs"].map((file) => {
882
- const path = resolve6(ctx.projectRoot, file);
883
- const ok = existsSync5(path);
1310
+ const path = resolve8(ctx.projectRoot, file);
1311
+ const ok = existsSync7(path);
884
1312
  return {
885
1313
  name: file.replace(/\./g, "-"),
886
1314
  ok,
@@ -892,8 +1320,8 @@ function checkCdnViteConfigs(ctx) {
892
1320
  }
893
1321
  function checkDevEntry(ctx) {
894
1322
  const checks = [];
895
- const indexHtml = resolve6(ctx.projectRoot, "index.html");
896
- const hasIndex = existsSync5(indexHtml);
1323
+ const indexHtml = resolve8(ctx.projectRoot, "index.html");
1324
+ const hasIndex = existsSync7(indexHtml);
897
1325
  checks.push({
898
1326
  name: "index-html",
899
1327
  ok: hasIndex,
@@ -901,8 +1329,8 @@ function checkDevEntry(ctx) {
901
1329
  remediation: hasIndex ? void 0 : "Re-run wp init or copy index.html from kit templates",
902
1330
  severity: "error"
903
1331
  });
904
- const localDev = resolve6(ctx.projectRoot, ctx.config.entries.localDev);
905
- const hasEntry = existsSync5(localDev);
1332
+ const localDev = resolve8(ctx.projectRoot, ctx.config.entries.localDev);
1333
+ const hasEntry = existsSync7(localDev);
906
1334
  checks.push({
907
1335
  name: "local-dev-entry",
908
1336
  ok: hasEntry,
@@ -912,7 +1340,7 @@ function checkDevEntry(ctx) {
912
1340
  });
913
1341
  if (hasEntry) {
914
1342
  try {
915
- const source = readFileSync4(localDev, "utf8");
1343
+ const source = readFileSync5(localDev, "utf8");
916
1344
  const usesKitNodeBootstrap = source.includes("@woodsportal/hubspot-kit/dev/bootstrap-greenfield") || source.includes("@woodsportal/hubspot-kit/dev/bootstrap");
917
1345
  const usesWphsShim = source.includes(".wphs/.kit-bootstrap-greenfield") || source.includes(".wphs/.kit-bootstrap") || source.includes("@wphs/bootstrap-greenfield") || source.includes("@wphs/bootstrap");
918
1346
  checks.push({
@@ -929,7 +1357,7 @@ function checkDevEntry(ctx) {
929
1357
  }
930
1358
  function checkKitScripts() {
931
1359
  const script = kitPath("scripts", "build-cdn.mjs");
932
- const ok = existsSync5(script);
1360
+ const ok = existsSync7(script);
933
1361
  return {
934
1362
  name: "kit-scripts",
935
1363
  ok,
@@ -942,8 +1370,8 @@ function checkWphsWorkspace(ctx) {
942
1370
  const workspace = ctx.config.dev.moduleConfigDir;
943
1371
  const checks = [];
944
1372
  for (const legacy of [".dev", ".woodsportal"]) {
945
- const legacyPath = resolve6(ctx.projectRoot, legacy);
946
- if (existsSync5(legacyPath)) {
1373
+ const legacyPath = resolve8(ctx.projectRoot, legacy);
1374
+ if (existsSync7(legacyPath)) {
947
1375
  checks.push({
948
1376
  name: `legacy-${legacy}`,
949
1377
  ok: false,
@@ -953,7 +1381,7 @@ function checkWphsWorkspace(ctx) {
953
1381
  });
954
1382
  }
955
1383
  }
956
- if (existsSync5(resolve6(ctx.projectRoot, ".wp-hs-baseline"))) {
1384
+ if (existsSync7(resolve8(ctx.projectRoot, ".wp-hs-baseline"))) {
957
1385
  checks.push({
958
1386
  name: "legacy-baseline",
959
1387
  ok: false,
@@ -972,10 +1400,10 @@ function checkWphsWorkspace(ctx) {
972
1400
  });
973
1401
  return checks;
974
1402
  }
975
- const wphsRoot = resolve6(ctx.projectRoot, workspace);
1403
+ const wphsRoot = resolve8(ctx.projectRoot, workspace);
976
1404
  const required = ["config", "assets/branding", "fixtures", "cache", "audit/baseline", "state"];
977
1405
  for (const sub of required) {
978
- const ok = existsSync5(join(wphsRoot, sub));
1406
+ const ok = existsSync7(join(wphsRoot, sub));
979
1407
  checks.push({
980
1408
  name: `wphs-${sub.replace(/\//g, "-")}`,
981
1409
  ok,
@@ -988,8 +1416,8 @@ function checkWphsWorkspace(ctx) {
988
1416
  }
989
1417
  function checkAdapter(ctx) {
990
1418
  if (isThemeProject(ctx.config)) return null;
991
- const adapterPath = resolve6(ctx.projectRoot, ctx.config.dev.adapter);
992
- const ok = existsSync5(adapterPath);
1419
+ const adapterPath = resolve8(ctx.projectRoot, ctx.config.dev.adapter);
1420
+ const ok = existsSync7(adapterPath);
993
1421
  return {
994
1422
  name: "wphs-adapter",
995
1423
  ok,
@@ -1000,11 +1428,11 @@ function checkAdapter(ctx) {
1000
1428
  }
1001
1429
  function checkModuleConfigUiDeps(ctx) {
1002
1430
  if (isThemeProject(ctx.config)) return [];
1003
- const pkgPath = resolve6(ctx.projectRoot, "package.json");
1004
- if (!existsSync5(pkgPath)) return [];
1431
+ const pkgPath = resolve8(ctx.projectRoot, "package.json");
1432
+ if (!existsSync7(pkgPath)) return [];
1005
1433
  let deps = {};
1006
1434
  try {
1007
- const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
1435
+ const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
1008
1436
  deps = { ...pkg.dependencies, ...pkg.devDependencies };
1009
1437
  } catch {
1010
1438
  return [];
@@ -1021,11 +1449,10 @@ function checkModuleConfigUiDeps(ctx) {
1021
1449
  });
1022
1450
  }
1023
1451
  function checkViteInstalled(ctx) {
1024
- if (isThemeProject(ctx.config) && !existsSync5(resolve6(ctx.projectRoot, "vite.config.dev.mjs"))) {
1452
+ if (isThemeProject(ctx.config) && !existsSync7(resolve8(ctx.projectRoot, "vite.config.dev.mjs"))) {
1025
1453
  return null;
1026
1454
  }
1027
- const localBin = resolve6(ctx.projectRoot, "node_modules", ".bin", "vite");
1028
- const ok = existsSync5(localBin);
1455
+ const ok = isProjectViteInstalled(ctx.projectRoot);
1029
1456
  return {
1030
1457
  name: "vite",
1031
1458
  ok,
@@ -1060,6 +1487,7 @@ async function gatherDoctorChecks(ctx) {
1060
1487
  checks.push(uploadOnlyNote);
1061
1488
  } else {
1062
1489
  checks.push(checkDevViteConfig(ctx), ...checkCdnViteConfigs(ctx), ...checkDevEntry(ctx));
1490
+ checks.push(...checkCssPipeline(ctx));
1063
1491
  const hybridHtml = checkHybridModuleHtml(ctx);
1064
1492
  if (hybridHtml) checks.push(hybridHtml);
1065
1493
  const viteCheck = checkViteInstalled(ctx);
@@ -1074,6 +1502,8 @@ async function gatherDoctorChecks(ctx) {
1074
1502
  checks.push(...checkConfig(ctx), ...checkWphsWorkspace(ctx));
1075
1503
  const fieldsCheck = checkFieldsJson(ctx);
1076
1504
  if (fieldsCheck) checks.push(fieldsCheck);
1505
+ const metaCheck = checkMetaJson(ctx);
1506
+ if (metaCheck) checks.push(metaCheck);
1077
1507
  const adapterCheck = checkAdapter(ctx);
1078
1508
  if (adapterCheck) checks.push(adapterCheck);
1079
1509
  }
@@ -1087,9 +1517,17 @@ async function gatherDoctorChecks(ctx) {
1087
1517
  {
1088
1518
  title: "Checking environment and CDN files",
1089
1519
  task: async () => {
1520
+ const legacyBuildId = checkLegacyBuildIdFile(ctx);
1521
+ if (legacyBuildId) checks.push(legacyBuildId);
1090
1522
  const buildIdCheck = checkBuildId(ctx);
1091
1523
  if (buildIdCheck) checks.push(buildIdCheck);
1524
+ const resolvedBuildId = checkModuleBuildIdResolved(ctx);
1525
+ if (resolvedBuildId) checks.push(resolvedBuildId);
1526
+ const devSlugCheck = checkDevSlug(ctx);
1527
+ if (devSlugCheck) checks.push(devSlugCheck);
1092
1528
  checks.push(...checkEnvFiles(ctx));
1529
+ const cdnMirrorCheck = await checkCdnMirrorPublic(ctx);
1530
+ if (cdnMirrorCheck) checks.push(cdnMirrorCheck);
1093
1531
  }
1094
1532
  }
1095
1533
  ]);
@@ -1168,21 +1606,21 @@ async function runConfigValidate(ctx) {
1168
1606
  }
1169
1607
 
1170
1608
  // src/cli/commands/build.ts
1171
- import { resolve as resolve8 } from "path";
1609
+ import { resolve as resolve10 } from "path";
1172
1610
 
1173
1611
  // src/lib/cli-banner.ts
1174
1612
  import pc3 from "picocolors";
1175
1613
 
1176
1614
  // src/lib/kit-version.ts
1177
- import { readFileSync as readFileSync5 } from "fs";
1178
- import { resolve as resolve7 } from "path";
1615
+ import { readFileSync as readFileSync6 } from "fs";
1616
+ import { resolve as resolve9 } from "path";
1179
1617
  var cachedVersion = null;
1180
1618
  function readKitVersion() {
1181
1619
  if (cachedVersion) {
1182
1620
  return cachedVersion;
1183
1621
  }
1184
1622
  try {
1185
- const pkg = JSON.parse(readFileSync5(resolve7(getKitRoot(), "package.json"), "utf8"));
1623
+ const pkg = JSON.parse(readFileSync6(resolve9(getKitRoot(), "package.json"), "utf8"));
1186
1624
  cachedVersion = pkg.version ?? "0.0.0";
1187
1625
  } catch {
1188
1626
  cachedVersion = "0.0.0";
@@ -1220,7 +1658,7 @@ function formatProjectLabel(config) {
1220
1658
  return `${config.module.name} \xB7 ${config.projectType}${routerNote}`;
1221
1659
  }
1222
1660
  function section(color, title, body) {
1223
- const bold = (text3) => color ? pc3.bold(text3) : text3;
1661
+ const bold = (text4) => color ? pc3.bold(text4) : text4;
1224
1662
  return ` ${bold(title)}
1225
1663
  ${body}`;
1226
1664
  }
@@ -1229,10 +1667,10 @@ function printWoodsCliDevBanner(options) {
1229
1667
  return;
1230
1668
  }
1231
1669
  const color = useCliColor();
1232
- const cyan = (text3) => color ? pc3.cyan(text3) : text3;
1233
- const dim = (text3) => color ? pc3.dim(text3) : text3;
1234
- const bold = (text3) => color ? pc3.bold(text3) : text3;
1235
- const yellow = (text3) => color ? pc3.yellow(text3) : text3;
1670
+ const cyan = (text4) => color ? pc3.cyan(text4) : text4;
1671
+ const dim = (text4) => color ? pc3.dim(text4) : text4;
1672
+ const bold = (text4) => color ? pc3.bold(text4) : text4;
1673
+ const yellow = (text4) => color ? pc3.yellow(text4) : text4;
1236
1674
  const version = options.version ?? readKitVersion();
1237
1675
  const project = formatProjectLabel(options.config);
1238
1676
  const isTheme = isThemeProject(options.config);
@@ -1314,7 +1752,7 @@ function buildTasks(ctx, options, run) {
1314
1752
  title: `Building monolith module (${tier})`,
1315
1753
  task: async () => {
1316
1754
  await run("run-vite-build.mjs", [
1317
- resolve8(ctx.projectRoot, "vite.config.monolith.mjs"),
1755
+ resolve10(ctx.projectRoot, "vite.config.monolith.mjs"),
1318
1756
  ...tierToStgFlag(tier)
1319
1757
  ]);
1320
1758
  await run("run-hubspot-module-postbuild.mjs");
@@ -1328,7 +1766,7 @@ function buildTasks(ctx, options, run) {
1328
1766
  task: async () => {
1329
1767
  if (options.cdnEsm) {
1330
1768
  await run("run-vite-build.mjs", [
1331
- resolve8(ctx.projectRoot, "vite.config.cdn-esm.mjs"),
1769
+ resolve10(ctx.projectRoot, "vite.config.cdn-esm.mjs"),
1332
1770
  ...tierToStgFlag(tier)
1333
1771
  ]);
1334
1772
  } else {
@@ -1343,7 +1781,7 @@ function buildTasks(ctx, options, run) {
1343
1781
  title: `Building HubSpot module (${tier})`,
1344
1782
  task: async () => {
1345
1783
  await run("run-vite-build.mjs", [
1346
- resolve8(ctx.projectRoot, "vite.config.hubspot.mjs"),
1784
+ resolve10(ctx.projectRoot, "vite.config.hubspot.mjs"),
1347
1785
  ...tierToStgFlag(tier)
1348
1786
  ]);
1349
1787
  await run("run-hubspot-module-postbuild.mjs");
@@ -1371,14 +1809,35 @@ function buildIntroLine(ctx, tier, options) {
1371
1809
  if (parts.length === 0) return void 0;
1372
1810
  return `${formatProjectLabel(ctx.config)} \xB7 ${parts.join(" + ")} (${tier})`;
1373
1811
  }
1812
+ async function maybeBumpModuleBuildId(ctx, tier, options, env) {
1813
+ const useCdn = usesCdnPipeline(ctx.config) && !isThemeProject(ctx.config);
1814
+ const shouldBump = useCdn && !options.hubspotOnly && !ctx.global.noBump && !options.buildIdBumped && !env.PORTAL_BUILD_ID?.trim() && !env.MODULE_BUILD_ID?.trim() && !env.GITHUB_REF_NAME?.trim();
1815
+ if (!shouldBump) {
1816
+ return env;
1817
+ }
1818
+ const result = bumpModuleBuildIdForCli(ctx.projectRoot, {
1819
+ tier,
1820
+ dryRun: ctx.global.dryRun,
1821
+ env
1822
+ });
1823
+ if (!ctx.global.quiet && !ctx.global.json && result.bumped) {
1824
+ ctx.logger.info(`Module build id \u2192 ${result.id}`);
1825
+ }
1826
+ return {
1827
+ ...env,
1828
+ PORTAL_BUILD_ID: result.id,
1829
+ MODULE_BUILD_ID: result.id
1830
+ };
1831
+ }
1374
1832
  async function runBuild(ctx, options) {
1375
1833
  const tier = options.tier ?? DEFAULT_BUILD_TIER;
1376
1834
  const quietSubprocess = shouldQuietSubprocess(ctx, options);
1377
- const env = {
1378
- ...process.env,
1835
+ let env = {
1836
+ ...options.env ?? process.env,
1379
1837
  WP_HS_PROJECT_ROOT: ctx.projectRoot,
1380
1838
  ...quietSubprocess ? { WP_HS_QUIET_BUILD: "1" } : {}
1381
1839
  };
1840
+ env = await maybeBumpModuleBuildId(ctx, tier, options, env);
1382
1841
  const isTheme = isThemeProject(ctx.config);
1383
1842
  const execOpts = {
1384
1843
  cwd: ctx.projectRoot,
@@ -1419,7 +1878,7 @@ async function runBuild(ctx, options) {
1419
1878
  command: "build",
1420
1879
  tier,
1421
1880
  ok: true,
1422
- outDir: resolve8(ctx.projectRoot, ctx.config.module.outDir)
1881
+ outDir: resolve10(ctx.projectRoot, ctx.config.module.outDir)
1423
1882
  });
1424
1883
  } else if (!options.quiet) {
1425
1884
  printCommandDone(ctx, "Build complete");
@@ -1433,30 +1892,9 @@ async function runBuild(ctx, options) {
1433
1892
  }
1434
1893
 
1435
1894
  // src/cli/commands/dev.ts
1436
- import { existsSync as existsSync7 } from "fs";
1437
- import { resolve as resolve10 } from "path";
1895
+ import { existsSync as existsSync8 } from "fs";
1896
+ import { resolve as resolve11 } from "path";
1438
1897
  import { spawn } from "child_process";
1439
-
1440
- // src/vite/resolve-dev-config.ts
1441
- import { existsSync as existsSync6 } from "fs";
1442
- import { resolve as resolve9 } from "path";
1443
- var CONSUMER_DEV_CONFIG = "vite.config.dev.mjs";
1444
- function resolveDevViteConfigPath(projectRoot) {
1445
- const consumerConfig = resolve9(projectRoot, CONSUMER_DEV_CONFIG);
1446
- if (existsSync6(consumerConfig)) {
1447
- return consumerConfig;
1448
- }
1449
- return kitPath("vite", "vite.config.dev.mjs");
1450
- }
1451
- function resolveProjectViteBin(projectRoot) {
1452
- const localBin = resolve9(projectRoot, "node_modules", ".bin", "vite");
1453
- if (existsSync6(localBin)) {
1454
- return localBin;
1455
- }
1456
- return "vite";
1457
- }
1458
-
1459
- // src/cli/commands/dev.ts
1460
1898
  async function runDev(ctx, options) {
1461
1899
  const port = options.port ?? 3e3;
1462
1900
  const env = {
@@ -1464,19 +1902,20 @@ async function runDev(ctx, options) {
1464
1902
  WP_HS_PROJECT_ROOT: ctx.projectRoot
1465
1903
  };
1466
1904
  const configPath = resolveDevViteConfigPath(ctx.projectRoot);
1467
- const viteBin = resolveProjectViteBin(ctx.projectRoot);
1905
+ const viteSpawn = resolveProjectViteSpawn(ctx.projectRoot);
1468
1906
  if (ctx.global.dryRun) {
1469
- ctx.logger.info(`[dry-run] ${viteBin} --config ${configPath} --port ${port}`);
1907
+ const dryRunTarget = viteSpawn ? formatProjectViteSpawn(viteSpawn) : "vite";
1908
+ ctx.logger.info(`[dry-run] ${dryRunTarget} --config ${configPath} --port ${port}`);
1470
1909
  return ExitCode.SUCCESS;
1471
1910
  }
1472
- if (viteBin === "vite") {
1911
+ if (!viteSpawn) {
1473
1912
  throw new WpHsError("vite is not installed in this project.", {
1474
1913
  exitCode: ExitCode.PREREQUISITE,
1475
1914
  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)."
1476
1915
  });
1477
1916
  }
1478
- const consumerConfig = resolve10(ctx.projectRoot, "vite.config.dev.mjs");
1479
- if (!existsSync7(consumerConfig)) {
1917
+ const consumerConfig = resolve11(ctx.projectRoot, "vite.config.dev.mjs");
1918
+ if (!existsSync8(consumerConfig)) {
1480
1919
  throw new WpHsError("vite.config.dev.mjs is missing in the project root.", {
1481
1920
  exitCode: ExitCode.USER_ERROR,
1482
1921
  remediation: "Add vite.config.dev.mjs (see @woodsportal/hubspot-kit templates/module-hybrid-cdn)."
@@ -1489,8 +1928,16 @@ async function runDev(ctx, options) {
1489
1928
  });
1490
1929
  return new Promise((resolvePromise) => {
1491
1930
  const child = spawn(
1492
- viteBin,
1493
- ["--config", configPath, "--port", String(port), "--mode", "dev"],
1931
+ viteSpawn.command,
1932
+ [
1933
+ ...viteSpawn.prefixArgs,
1934
+ "--config",
1935
+ configPath,
1936
+ "--port",
1937
+ String(port),
1938
+ "--mode",
1939
+ "dev"
1940
+ ],
1494
1941
  {
1495
1942
  cwd: ctx.projectRoot,
1496
1943
  env,
@@ -1505,11 +1952,11 @@ async function runDev(ctx, options) {
1505
1952
  }
1506
1953
 
1507
1954
  // src/cli/commands/deploy.ts
1508
- import { resolve as resolve13 } from "path";
1955
+ import { resolve as resolve14 } from "path";
1509
1956
 
1510
1957
  // src/lib/project-env-files.ts
1511
- import { existsSync as existsSync8, readFileSync as readFileSync6, writeFileSync } from "fs";
1512
- import { resolve as resolve11 } from "path";
1958
+ import { existsSync as existsSync9, readFileSync as readFileSync7, writeFileSync as writeFileSync2 } from "fs";
1959
+ import { resolve as resolve12 } from "path";
1513
1960
  var ENV_LOCAL_FILE = ".env.local";
1514
1961
  var ENV_GITIGNORE_LINES = [".env", ".env.*", "!.env*.example", ENV_LOCAL_FILE];
1515
1962
  function parseEnvFile(content) {
@@ -1529,9 +1976,9 @@ function parseEnvFile(content) {
1529
1976
  return result;
1530
1977
  }
1531
1978
  function readEnvLocal(projectRoot) {
1532
- const path = resolve11(projectRoot, ENV_LOCAL_FILE);
1533
- if (!existsSync8(path)) return {};
1534
- return parseEnvFile(readFileSync6(path, "utf8"));
1979
+ const path = resolve12(projectRoot, ENV_LOCAL_FILE);
1980
+ if (!existsSync9(path)) return {};
1981
+ return parseEnvFile(readFileSync7(path, "utf8"));
1535
1982
  }
1536
1983
  function formatEnvLocal(entries) {
1537
1984
  const lines = [
@@ -1550,9 +1997,9 @@ function formatEnvLocal(entries) {
1550
1997
  `;
1551
1998
  }
1552
1999
  function writeEnvLocal(projectRoot, entries) {
1553
- const path = resolve11(projectRoot, ENV_LOCAL_FILE);
1554
- const existing = existsSync8(path) ? parseEnvFile(readFileSync6(path, "utf8")) : {};
1555
- writeFileSync(path, formatEnvLocal({ ...existing, ...entries }));
2000
+ const path = resolve12(projectRoot, ENV_LOCAL_FILE);
2001
+ const existing = existsSync9(path) ? parseEnvFile(readFileSync7(path, "utf8")) : {};
2002
+ writeFileSync2(path, formatEnvLocal({ ...existing, ...entries }));
1556
2003
  }
1557
2004
  function mergeProjectEnv(projectRoot, base = process.env) {
1558
2005
  const local = readEnvLocal(projectRoot);
@@ -1565,11 +2012,11 @@ function mergeProjectEnv(projectRoot, base = process.env) {
1565
2012
  return merged;
1566
2013
  }
1567
2014
  function ensureEnvFilesGitignore(projectRoot) {
1568
- const gitignorePath = resolve11(projectRoot, ".gitignore");
1569
- const existing = existsSync8(gitignorePath) ? readFileSync6(gitignorePath, "utf8") : "";
2015
+ const gitignorePath = resolve12(projectRoot, ".gitignore");
2016
+ const existing = existsSync9(gitignorePath) ? readFileSync7(gitignorePath, "utf8") : "";
1570
2017
  const toAppend = ENV_GITIGNORE_LINES.filter((line) => !existing.includes(line)).join("\n");
1571
2018
  if (!toAppend) return;
1572
- writeFileSync(gitignorePath, `${existing.trimEnd()}
2019
+ writeFileSync2(gitignorePath, `${existing.trimEnd()}
1573
2020
 
1574
2021
  # Env files (secrets)
1575
2022
  ${toAppend}
@@ -1577,14 +2024,14 @@ ${toAppend}
1577
2024
  }
1578
2025
 
1579
2026
  // src/cli/commands/upload.ts
1580
- import { resolve as resolve12 } from "path";
2027
+ import { resolve as resolve13 } from "path";
1581
2028
  async function uploadModuleOrTheme(ctx) {
1582
2029
  if (isThemeProject(ctx.config)) {
1583
2030
  const theme = ctx.config.theme;
1584
2031
  if (!theme) {
1585
2032
  throw new WpHsError("theme block missing in woodscli.json", { exitCode: ExitCode.USER_ERROR });
1586
2033
  }
1587
- const srcDir = resolve12(ctx.projectRoot, theme.srcDir);
2034
+ const srcDir = resolve13(ctx.projectRoot, theme.srcDir);
1588
2035
  await runCommandAsync("hs", ["cms", "upload", srcDir, theme.uploadPath], {
1589
2036
  cwd: ctx.projectRoot,
1590
2037
  projectRoot: ctx.projectRoot,
@@ -1594,7 +2041,7 @@ async function uploadModuleOrTheme(ctx) {
1594
2041
  });
1595
2042
  return `Uploaded theme ${srcDir} \u2192 ${theme.uploadPath}`;
1596
2043
  }
1597
- const outDir = resolve12(ctx.projectRoot, ctx.config.module.outDir);
2044
+ const outDir = resolve13(ctx.projectRoot, ctx.config.module.outDir);
1598
2045
  const uploadPath = ctx.config.module.uploadPath;
1599
2046
  await runCommandAsync("hs", ["cms", "upload", outDir, uploadPath], {
1600
2047
  cwd: ctx.projectRoot,
@@ -1661,11 +2108,29 @@ async function runDeploy(ctx, options) {
1661
2108
  quiet: true,
1662
2109
  quietSubprocess: true
1663
2110
  };
1664
- const baseEnv = mergeProjectEnv(ctx.projectRoot, {
2111
+ const useCdn = usesCdnPipeline(ctx.config) && !isThemeProject(ctx.config);
2112
+ let baseEnv = mergeProjectEnv(ctx.projectRoot, {
1665
2113
  ...process.env,
1666
2114
  WP_HS_PROJECT_ROOT: ctx.projectRoot,
1667
2115
  ...quietSubprocess ? { WP_HS_QUIET_BUILD: "1" } : {}
1668
2116
  });
2117
+ let buildIdBumped = false;
2118
+ if (useCdn && !ctx.global.noBump && !baseEnv.PORTAL_BUILD_ID?.trim() && !baseEnv.MODULE_BUILD_ID?.trim() && !baseEnv.GITHUB_REF_NAME?.trim()) {
2119
+ const bump = bumpModuleBuildIdForCli(ctx.projectRoot, {
2120
+ tier,
2121
+ dryRun: ctx.global.dryRun,
2122
+ env: baseEnv
2123
+ });
2124
+ baseEnv = {
2125
+ ...baseEnv,
2126
+ PORTAL_BUILD_ID: bump.id,
2127
+ MODULE_BUILD_ID: bump.id
2128
+ };
2129
+ buildIdBumped = bump.bumped;
2130
+ if (!ctx.global.quiet && !ctx.global.json && bump.bumped) {
2131
+ ctx.logger.info(`Module build id \u2192 ${bump.id}`);
2132
+ }
2133
+ }
1669
2134
  const execOpts = {
1670
2135
  cwd: ctx.projectRoot,
1671
2136
  projectRoot: ctx.projectRoot,
@@ -1674,7 +2139,6 @@ async function runDeploy(ctx, options) {
1674
2139
  dryRun: ctx.global.dryRun,
1675
2140
  streamOutput: ctx.global.verbose
1676
2141
  };
1677
- const useCdn = usesCdnPipeline(ctx.config) && !isThemeProject(ctx.config);
1678
2142
  const verifyFlag = tier === "prod" ? ["--prod"] : ["--stg"];
1679
2143
  const tasks = [
1680
2144
  {
@@ -1688,7 +2152,7 @@ async function runDeploy(ctx, options) {
1688
2152
  tasks.push({
1689
2153
  title: `Building CDN (${tier})`,
1690
2154
  task: async () => {
1691
- await runBuild(ctx, { tier, cdnOnly: true, ...nested });
2155
+ await runBuild(ctx, { tier, cdnOnly: true, buildIdBumped, env: baseEnv, ...nested });
1692
2156
  }
1693
2157
  });
1694
2158
  if (!options.skipPublish) {
@@ -1708,7 +2172,7 @@ async function runDeploy(ctx, options) {
1708
2172
  tasks.push({
1709
2173
  title: `Building HubSpot module (${tier})`,
1710
2174
  task: async () => {
1711
- await runBuild(ctx, { tier, hubspotOnly: true, ...nested });
2175
+ await runBuild(ctx, { tier, hubspotOnly: true, env: baseEnv, ...nested });
1712
2176
  }
1713
2177
  });
1714
2178
  } else {
@@ -1745,7 +2209,7 @@ async function runDeploy(ctx, options) {
1745
2209
  if (!ctx.global.json) {
1746
2210
  printCommandDone(
1747
2211
  ctx,
1748
- `Deploy complete (${resolve13(ctx.projectRoot, ctx.config.module.outDir)} \u2192 ${ctx.config.module.uploadPath})`
2212
+ `Deploy complete (${resolve14(ctx.projectRoot, ctx.config.module.outDir)} \u2192 ${ctx.config.module.uploadPath})`
1749
2213
  );
1750
2214
  }
1751
2215
  return ExitCode.SUCCESS;
@@ -1818,8 +2282,8 @@ async function runPublish(ctx, options) {
1818
2282
 
1819
2283
  // src/cli/commands/audit.ts
1820
2284
  import { createHash } from "crypto";
1821
- import { cpSync, existsSync as existsSync9, mkdirSync, readFileSync as readFileSync7, readdirSync, rmSync, statSync } from "fs";
1822
- import { resolve as resolve14, join as join2 } from "path";
2285
+ import { cpSync, existsSync as existsSync10, mkdirSync as mkdirSync2, readFileSync as readFileSync8, readdirSync, rmSync, statSync } from "fs";
2286
+ import { resolve as resolve15, join as join2 } from "path";
1823
2287
  var FORBIDDEN_SHIP_TOKENS = [
1824
2288
  "ModuleConfigOverlay",
1825
2289
  "module-config-store",
@@ -1828,10 +2292,10 @@ var FORBIDDEN_SHIP_TOKENS = [
1828
2292
  "dev.module-config"
1829
2293
  ];
1830
2294
  function hashFile(path) {
1831
- return createHash("sha256").update(readFileSync7(path)).digest("hex");
2295
+ return createHash("sha256").update(readFileSync8(path)).digest("hex");
1832
2296
  }
1833
2297
  function walkFiles(dir) {
1834
- if (!existsSync9(dir)) return [];
2298
+ if (!existsSync10(dir)) return [];
1835
2299
  const entries = readdirSync(dir);
1836
2300
  const files = [];
1837
2301
  for (const entry of entries) {
@@ -1847,14 +2311,14 @@ function walkFiles(dir) {
1847
2311
  async function runAuditShipBundle(ctx) {
1848
2312
  const root = ctx.projectRoot;
1849
2313
  const targets = [
1850
- resolve14(root, "dist/cdn"),
1851
- resolve14(root, ctx.config.module.outDir)
2314
+ resolve15(root, "dist/cdn"),
2315
+ resolve15(root, ctx.config.module.outDir)
1852
2316
  ];
1853
2317
  const violations = [];
1854
2318
  for (const dir of targets) {
1855
2319
  for (const file of walkFiles(dir)) {
1856
2320
  if (!/\.(js|css|html)$/i.test(file)) continue;
1857
- const content = readFileSync7(file, "utf8");
2321
+ const content = readFileSync8(file, "utf8");
1858
2322
  for (const token of FORBIDDEN_SHIP_TOKENS) {
1859
2323
  if (content.includes(token)) {
1860
2324
  violations.push(`${file}: contains forbidden token "${token}"`);
@@ -1881,23 +2345,23 @@ async function runAuditBaselineCapture(ctx, options) {
1881
2345
  const root = ctx.projectRoot;
1882
2346
  const baseline = options.baselineDir ?? defaultBaselineDir(root, ctx.config);
1883
2347
  const sources = [
1884
- { label: "cdn", path: resolve14(root, "dist/cdn") },
1885
- { label: "hubspot", path: resolve14(root, ctx.config.module.outDir) }
2348
+ { label: "cdn", path: resolve15(root, "dist/cdn") },
2349
+ { label: "hubspot", path: resolve15(root, ctx.config.module.outDir) }
1886
2350
  ];
1887
2351
  if (ctx.global.dryRun) {
1888
2352
  ctx.logger.info(`[dry-run] Would capture baseline to ${baseline}`);
1889
2353
  return ExitCode.SUCCESS;
1890
2354
  }
1891
2355
  for (const { label, path } of sources) {
1892
- if (!existsSync9(path)) {
2356
+ if (!existsSync10(path)) {
1893
2357
  throw new WpHsError(`Cannot capture baseline \u2014 missing ${path}`, {
1894
2358
  exitCode: ExitCode.USER_ERROR,
1895
2359
  remediation: "Run wp build before wp audit baseline --capture."
1896
2360
  });
1897
2361
  }
1898
- const target = resolve14(baseline, label);
2362
+ const target = resolve15(baseline, label);
1899
2363
  rmSync(target, { recursive: true, force: true });
1900
- mkdirSync(baseline, { recursive: true });
2364
+ mkdirSync2(baseline, { recursive: true });
1901
2365
  cpSync(path, target, { recursive: true });
1902
2366
  }
1903
2367
  if (ctx.global.json) {
@@ -1911,13 +2375,13 @@ async function runAuditParity(ctx, options) {
1911
2375
  const root = ctx.projectRoot;
1912
2376
  const baseline = options.baselineDir ?? defaultBaselineDir(root, ctx.config);
1913
2377
  const currentDirs = [
1914
- { label: "cdn", path: resolve14(root, "dist/cdn") },
1915
- { label: "hubspot", path: resolve14(root, ctx.config.module.outDir) }
2378
+ { label: "cdn", path: resolve15(root, "dist/cdn") },
2379
+ { label: "hubspot", path: resolve15(root, ctx.config.module.outDir) }
1916
2380
  ];
1917
2381
  const mismatches = [];
1918
2382
  for (const { label, path } of currentDirs) {
1919
- const baselinePath = resolve14(baseline, label);
1920
- if (!existsSync9(baselinePath)) {
2383
+ const baselinePath = resolve15(baseline, label);
2384
+ if (!existsSync10(baselinePath)) {
1921
2385
  ctx.logger.warn(`No baseline at ${baselinePath} \u2014 skipping ${label} parity`);
1922
2386
  continue;
1923
2387
  }
@@ -1932,8 +2396,8 @@ async function runAuditParity(ctx, options) {
1932
2396
  }
1933
2397
  for (const file of baselineFiles) {
1934
2398
  const rel = file.replace(baselinePath, "");
1935
- const current = resolve14(path, rel.slice(1));
1936
- if (!existsSync9(current)) continue;
2399
+ const current = resolve15(path, rel.slice(1));
2400
+ if (!existsSync10(current)) continue;
1937
2401
  const bh = hashFile(file);
1938
2402
  const ch = hashFile(current);
1939
2403
  if (bh !== ch) {
@@ -1960,13 +2424,13 @@ var SECRET_PATTERNS = [
1960
2424
  ];
1961
2425
  async function runAuditSecrets(ctx) {
1962
2426
  const root = ctx.projectRoot;
1963
- const scanRoots = ["src", "public", ".wphs/config"].map((d) => resolve14(root, d));
2427
+ const scanRoots = ["src", "public", ".wphs/config"].map((d) => resolve15(root, d));
1964
2428
  const findings = [];
1965
2429
  for (const dir of scanRoots) {
1966
2430
  for (const file of walkFiles(dir)) {
1967
2431
  if (!/\.(ts|tsx|js|jsx|json|env|yml|yaml|html)$/i.test(file)) continue;
1968
2432
  if (file.includes("node_modules")) continue;
1969
- const content = readFileSync7(file, "utf8");
2433
+ const content = readFileSync8(file, "utf8");
1970
2434
  for (const pattern of SECRET_PATTERNS) {
1971
2435
  pattern.lastIndex = 0;
1972
2436
  if (pattern.test(content)) {
@@ -1992,13 +2456,13 @@ async function runAuditSecrets(ctx) {
1992
2456
  }
1993
2457
 
1994
2458
  // src/cli/commands/init.ts
1995
- import { cpSync as cpSync2, existsSync as existsSync13, mkdirSync as mkdirSync3, readdirSync as readdirSync2, readFileSync as readFileSync10, writeFileSync as writeFileSync5 } from "fs";
1996
- import { basename as basename2, resolve as resolve18 } from "path";
1997
- import * as p3 from "@clack/prompts";
2459
+ import { cpSync as cpSync2, existsSync as existsSync14, mkdirSync as mkdirSync4, readdirSync as readdirSync2, readFileSync as readFileSync11, writeFileSync as writeFileSync6 } from "fs";
2460
+ import { basename as basename2, resolve as resolve19 } from "path";
2461
+ import * as p4 from "@clack/prompts";
1998
2462
 
1999
2463
  // src/lib/init-router-scaffold.ts
2000
- import { existsSync as existsSync10, mkdirSync as mkdirSync2, readFileSync as readFileSync8, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
2001
- import { resolve as resolve15 } from "path";
2464
+ import { existsSync as existsSync11, mkdirSync as mkdirSync3, readFileSync as readFileSync9, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
2465
+ import { resolve as resolve16 } from "path";
2002
2466
  var ROUTER_FILES = [
2003
2467
  "src/routes",
2004
2468
  "src/routeTree.gen.ts",
@@ -2012,10 +2476,10 @@ function isModuleTemplate(template) {
2012
2476
  return template === "module-hybrid-cdn" || template === "module-monolith";
2013
2477
  }
2014
2478
  function writeRouterConfig(targetDir, mode) {
2015
- const configDir = resolve15(targetDir, "src/config");
2016
- mkdirSync2(configDir, { recursive: true });
2017
- writeFileSync2(
2018
- resolve15(configDir, "router.ts"),
2479
+ const configDir = resolve16(targetDir, "src/config");
2480
+ mkdirSync3(configDir, { recursive: true });
2481
+ writeFileSync3(
2482
+ resolve16(configDir, "router.ts"),
2019
2483
  `/** Router history \u2014 set by \`wp init\`. Hash matches woodsportal-client-frontend (HubSpot CMS embeds). */
2020
2484
  export type RouterHistoryMode = 'hash' | 'browser'
2021
2485
 
@@ -2024,8 +2488,8 @@ export const ROUTER_HISTORY: RouterHistoryMode = '${mode}'
2024
2488
  );
2025
2489
  }
2026
2490
  function writeMountDevPreviewRouter(targetDir) {
2027
- writeFileSync2(
2028
- resolve15(targetDir, "src/portal/mount-dev-preview.tsx"),
2491
+ writeFileSync3(
2492
+ resolve16(targetDir, "src/portal/mount-dev-preview.tsx"),
2029
2493
  `import { mountGreenfieldDev } from '../../.wphs/.kit-bootstrap-greenfield.tsx'
2030
2494
 
2031
2495
  import { App } from '@/app'
@@ -2038,8 +2502,8 @@ export async function mountDevPreview() {
2038
2502
  );
2039
2503
  }
2040
2504
  function writeMountDevPreviewSinglePage(targetDir) {
2041
- writeFileSync2(
2042
- resolve15(targetDir, "src/portal/mount-dev-preview.tsx"),
2505
+ writeFileSync3(
2506
+ resolve16(targetDir, "src/portal/mount-dev-preview.tsx"),
2043
2507
  `import { mountGreenfieldDev } from '../../.wphs/.kit-bootstrap-greenfield.tsx'
2044
2508
 
2045
2509
  import { StarterSinglePage } from '@/features/starter'
@@ -2052,8 +2516,8 @@ export async function mountDevPreview() {
2052
2516
  );
2053
2517
  }
2054
2518
  function writeMountPortalRouter(targetDir) {
2055
- writeFileSync2(
2056
- resolve15(targetDir, "src/portal/mount-portal.tsx"),
2519
+ writeFileSync3(
2520
+ resolve16(targetDir, "src/portal/mount-portal.tsx"),
2057
2521
  `import ReactDOM from 'react-dom/client'
2058
2522
 
2059
2523
  import { App } from '@/app'
@@ -2070,8 +2534,8 @@ export async function mountPortal() {
2070
2534
  );
2071
2535
  }
2072
2536
  function writeMountPortalSinglePage(targetDir) {
2073
- writeFileSync2(
2074
- resolve15(targetDir, "src/portal/mount-portal.tsx"),
2537
+ writeFileSync3(
2538
+ resolve16(targetDir, "src/portal/mount-portal.tsx"),
2075
2539
  `import ReactDOM from 'react-dom/client'
2076
2540
 
2077
2541
  import { StarterSinglePage } from '@/features/starter'
@@ -2088,11 +2552,11 @@ export async function mountPortal() {
2088
2552
  );
2089
2553
  }
2090
2554
  function writeStarterSinglePage(targetDir) {
2091
- const file = resolve15(targetDir, "src/features/starter/components/starter-single-page.tsx");
2092
- if (existsSync10(file)) {
2555
+ const file = resolve16(targetDir, "src/features/starter/components/starter-single-page.tsx");
2556
+ if (existsSync11(file)) {
2093
2557
  return;
2094
2558
  }
2095
- writeFileSync2(
2559
+ writeFileSync3(
2096
2560
  file,
2097
2561
  `import { StarterShell } from '@/components/layout/starter-shell'
2098
2562
 
@@ -2115,25 +2579,25 @@ export function StarterSinglePage() {
2115
2579
  );
2116
2580
  }
2117
2581
  function removePath(targetDir, relativePath) {
2118
- const full = resolve15(targetDir, relativePath);
2119
- if (!existsSync10(full)) {
2582
+ const full = resolve16(targetDir, relativePath);
2583
+ if (!existsSync11(full)) {
2120
2584
  return;
2121
2585
  }
2122
2586
  rmSync2(full, { recursive: true, force: true });
2123
2587
  }
2124
2588
  function stripTanStackFromPackage(targetDir) {
2125
- const pkgPath = resolve15(targetDir, "package.json");
2126
- if (!existsSync10(pkgPath)) {
2589
+ const pkgPath = resolve16(targetDir, "package.json");
2590
+ if (!existsSync11(pkgPath)) {
2127
2591
  return;
2128
2592
  }
2129
- const pkg = JSON.parse(readFileSync8(pkgPath, "utf8"));
2593
+ const pkg = JSON.parse(readFileSync9(pkgPath, "utf8"));
2130
2594
  if (!pkg.devDependencies) {
2131
2595
  return;
2132
2596
  }
2133
2597
  for (const dep of TANSTACK_DEPS) {
2134
2598
  delete pkg.devDependencies[dep];
2135
2599
  }
2136
- writeFileSync2(pkgPath, `${JSON.stringify(pkg, null, 2)}
2600
+ writeFileSync3(pkgPath, `${JSON.stringify(pkg, null, 2)}
2137
2601
  `);
2138
2602
  }
2139
2603
  function applyRouterScaffold(targetDir, mode, template) {
@@ -2153,30 +2617,30 @@ function applyRouterScaffold(targetDir, mode, template) {
2153
2617
  writeRouterConfig(targetDir, mode);
2154
2618
  writeMountDevPreviewRouter(targetDir);
2155
2619
  writeMountPortalRouter(targetDir);
2156
- const featuresIndex = resolve15(targetDir, "src/features/starter/index.ts");
2157
- if (existsSync10(featuresIndex)) {
2158
- let content = readFileSync8(featuresIndex, "utf8");
2620
+ const featuresIndex = resolve16(targetDir, "src/features/starter/index.ts");
2621
+ if (existsSync11(featuresIndex)) {
2622
+ let content = readFileSync9(featuresIndex, "utf8");
2159
2623
  if (!content.includes("StarterSinglePage")) {
2160
2624
  content = content.replace(
2161
2625
  "export type { StarterPreviewValues } from './hooks/use-starter-preview'\n",
2162
2626
  "export type { StarterPreviewValues } from './hooks/use-starter-preview'\nexport { StarterSinglePage } from './components/starter-single-page'\n"
2163
2627
  );
2164
- writeFileSync2(featuresIndex, content);
2628
+ writeFileSync3(featuresIndex, content);
2165
2629
  }
2166
2630
  }
2167
2631
  }
2168
2632
 
2169
2633
  // src/lib/cdn-github-setup.ts
2170
- import { existsSync as existsSync11, readFileSync as readFileSync9, writeFileSync as writeFileSync3 } from "fs";
2171
- import { resolve as resolve16 } from "path";
2172
- import * as p2 from "@clack/prompts";
2634
+ import { existsSync as existsSync12, readFileSync as readFileSync10, writeFileSync as writeFileSync4 } from "fs";
2635
+ import { resolve as resolve17 } from "path";
2636
+ import * as p3 from "@clack/prompts";
2173
2637
  var PORTAL_CDN_PUBLISH_TOKEN_ENV = "PORTAL_CDN_PUBLISH_TOKEN";
2174
2638
  var PORTAL_CDN_JSON = "portal.cdn.json";
2175
2639
  var PLACEHOLDER_GITHUB = /your-org|your-portal-cdn-mirror/i;
2176
2640
  function defaultPortalCdnJson() {
2177
2641
  const examplePath = kitPath("templates", "shared", "portal.cdn.json.example");
2178
- if (existsSync11(examplePath)) {
2179
- return JSON.parse(readFileSync9(examplePath, "utf8"));
2642
+ if (existsSync12(examplePath)) {
2643
+ return JSON.parse(readFileSync10(examplePath, "utf8"));
2180
2644
  }
2181
2645
  return {
2182
2646
  publishMode: "jsdelivr",
@@ -2187,10 +2651,10 @@ function defaultPortalCdnJson() {
2187
2651
  };
2188
2652
  }
2189
2653
  function readPortalCdnJson(projectRoot) {
2190
- const path = resolve16(projectRoot, PORTAL_CDN_JSON);
2191
- if (!existsSync11(path)) return null;
2654
+ const path = resolve17(projectRoot, PORTAL_CDN_JSON);
2655
+ if (!existsSync12(path)) return null;
2192
2656
  try {
2193
- return JSON.parse(readFileSync9(path, "utf8"));
2657
+ return JSON.parse(readFileSync10(path, "utf8"));
2194
2658
  } catch {
2195
2659
  return null;
2196
2660
  }
@@ -2237,7 +2701,7 @@ function needsCdnGitHubSetup(projectRoot) {
2237
2701
  function applyCdnGitHubSetup(projectRoot, input) {
2238
2702
  const existing = readPortalCdnJson(projectRoot) ?? defaultPortalCdnJson();
2239
2703
  const updated = portalCdnJsonFromGithub(input.github, existing);
2240
- writeFileSync3(resolve16(projectRoot, PORTAL_CDN_JSON), `${JSON.stringify(updated, null, 2)}
2704
+ writeFileSync4(resolve17(projectRoot, PORTAL_CDN_JSON), `${JSON.stringify(updated, null, 2)}
2241
2705
  `);
2242
2706
  if (input.token?.trim()) {
2243
2707
  writeEnvLocal(projectRoot, { [PORTAL_CDN_PUBLISH_TOKEN_ENV]: input.token.trim() });
@@ -2255,7 +2719,7 @@ async function promptCdnGitHubSetup(options = {}) {
2255
2719
  github = readPortalCdnJson(projectRoot)?.github;
2256
2720
  }
2257
2721
  if (!github) {
2258
- const answer = await p2.text({
2722
+ const answer = await p3.text({
2259
2723
  message: "Public GitHub CDN mirror (org/repo)",
2260
2724
  placeholder: options.githubDefault ?? "your-org/your-portal-cdn-mirror",
2261
2725
  initialValue: options.githubDefault,
@@ -2264,21 +2728,21 @@ async function promptCdnGitHubSetup(options = {}) {
2264
2728
  return slug ? void 0 : "Enter a public repo as org/repo (jsDelivr serves from it)";
2265
2729
  }
2266
2730
  });
2267
- if (p2.isCancel(answer)) return null;
2731
+ if (p3.isCancel(answer)) return null;
2268
2732
  github = normalizeGithubSlug(answer) ?? void 0;
2269
2733
  }
2270
2734
  if (!github) return null;
2271
2735
  if (hasCdnPublishToken(projectRoot)) {
2272
2736
  return { github };
2273
2737
  }
2274
- const tokenAnswer = await p2.password({
2738
+ const tokenAnswer = await p3.password({
2275
2739
  message: "GitHub token (repo push on CDN mirror)",
2276
2740
  validate: (value) => {
2277
2741
  if (!value?.trim()) return "Required for wp publish \u2014 create a fine-grained PAT with Contents: Read and write";
2278
2742
  return void 0;
2279
2743
  }
2280
2744
  });
2281
- if (p2.isCancel(tokenAnswer)) return null;
2745
+ if (p3.isCancel(tokenAnswer)) return null;
2282
2746
  return { github, token: tokenAnswer.trim() };
2283
2747
  }
2284
2748
  function formatCdnSetupNote(_projectRoot) {
@@ -2286,8 +2750,8 @@ function formatCdnSetupNote(_projectRoot) {
2286
2750
  }
2287
2751
 
2288
2752
  // src/lib/project-setup.ts
2289
- import { copyFileSync, existsSync as existsSync12, writeFileSync as writeFileSync4 } from "fs";
2290
- import { basename, resolve as resolve17 } from "path";
2753
+ import { copyFileSync, existsSync as existsSync13, writeFileSync as writeFileSync5 } from "fs";
2754
+ import { basename, resolve as resolve18 } from "path";
2291
2755
  function envFileForTier(tier) {
2292
2756
  return tier === "dev" ? ".env.dev" : tier === "stg" ? ".env.stg" : ".env.prod";
2293
2757
  }
@@ -2297,13 +2761,13 @@ function tiersToSetup(config, tier) {
2297
2761
  }
2298
2762
  function firstExisting(paths) {
2299
2763
  for (const path of paths) {
2300
- if (existsSync12(path)) return path;
2764
+ if (existsSync13(path)) return path;
2301
2765
  }
2302
2766
  return null;
2303
2767
  }
2304
2768
  function copyMissingFile(projectRoot, targetName, sourceCandidates, dryRun) {
2305
- const target = resolve17(projectRoot, targetName);
2306
- if (existsSync12(target)) {
2769
+ const target = resolve18(projectRoot, targetName);
2770
+ if (existsSync13(target)) {
2307
2771
  return { action: "skipped", path: targetName, detail: "already present" };
2308
2772
  }
2309
2773
  const source = firstExisting(sourceCandidates);
@@ -2325,12 +2789,12 @@ function copyMissingFile(projectRoot, targetName, sourceCandidates, dryRun) {
2325
2789
  };
2326
2790
  }
2327
2791
  function writeMissingFile(projectRoot, targetName, content, dryRun) {
2328
- const target = resolve17(projectRoot, targetName);
2329
- if (existsSync12(target)) {
2792
+ const target = resolve18(projectRoot, targetName);
2793
+ if (existsSync13(target)) {
2330
2794
  return { action: "skipped", path: targetName, detail: "already present" };
2331
2795
  }
2332
2796
  if (!dryRun) {
2333
- writeFileSync4(target, content);
2797
+ writeFileSync5(target, content);
2334
2798
  }
2335
2799
  return {
2336
2800
  action: "copied",
@@ -2356,8 +2820,8 @@ function applyProjectSetup(ctx, options = {}) {
2356
2820
  const templateDir = kitTemplateDir(config);
2357
2821
  results.push(
2358
2822
  copyMissingFile(projectRoot, HUBSPOT_CONFIG_FILE, [
2359
- resolve17(projectRoot, HUBSPOT_CONFIG_EXAMPLE),
2360
- resolve17(templateDir, HUBSPOT_CONFIG_EXAMPLE),
2823
+ resolve18(projectRoot, HUBSPOT_CONFIG_EXAMPLE),
2824
+ resolve18(templateDir, HUBSPOT_CONFIG_EXAMPLE),
2361
2825
  kitPath("templates", HUBSPOT_CONFIG_EXAMPLE)
2362
2826
  ], dryRun)
2363
2827
  );
@@ -2366,8 +2830,8 @@ function applyProjectSetup(ctx, options = {}) {
2366
2830
  const envExample = `${envFile}.example`;
2367
2831
  results.push(
2368
2832
  copyMissingFile(projectRoot, envFile, [
2369
- resolve17(projectRoot, envExample),
2370
- resolve17(sharedDir, envExample)
2833
+ resolve18(projectRoot, envExample),
2834
+ resolve18(sharedDir, envExample)
2371
2835
  ], dryRun)
2372
2836
  );
2373
2837
  }
@@ -2375,26 +2839,31 @@ function applyProjectSetup(ctx, options = {}) {
2375
2839
  const mirror = config.cdn.mirror;
2376
2840
  results.push(
2377
2841
  copyMissingFile(projectRoot, mirror, [
2378
- resolve17(projectRoot, `${mirror}.example`),
2379
- resolve17(sharedDir, "portal.cdn.json.example")
2842
+ resolve18(projectRoot, `${mirror}.example`),
2843
+ resolve18(sharedDir, "portal.cdn.json.example")
2380
2844
  ], dryRun)
2381
2845
  );
2382
2846
  const buildIdFile = config.cdn.buildIdFile;
2383
2847
  results.push(
2384
- writeMissingFile(projectRoot, buildIdFile, "v0.0.1-dev.1\n", dryRun)
2848
+ writeMissingFile(
2849
+ projectRoot,
2850
+ buildIdFile,
2851
+ "# Channel baseline for local CDN deploys (wp deploy auto-bumps per developer).\n# Example resolved id: v0.0.1-dev.you.1\nv0.0.1-dev\n",
2852
+ dryRun
2853
+ )
2385
2854
  );
2386
2855
  }
2387
2856
  for (const file of templateViteFiles(config)) {
2388
2857
  results.push(
2389
- copyMissingFile(projectRoot, file, [resolve17(templateDir, file)], dryRun)
2858
+ copyMissingFile(projectRoot, file, [resolve18(templateDir, file)], dryRun)
2390
2859
  );
2391
2860
  }
2392
2861
  if (isModuleProject(config) && !isThemeProject(config)) {
2393
2862
  const adapter = config.dev.adapter;
2394
- const adapterPath = resolve17(projectRoot, adapter);
2395
- if (!existsSync12(adapterPath)) {
2396
- const templateAdapter = resolve17(templateDir, "wphs.adapter.ts");
2397
- if (existsSync12(templateAdapter)) {
2863
+ const adapterPath = resolve18(projectRoot, adapter);
2864
+ if (!existsSync13(adapterPath)) {
2865
+ const templateAdapter = resolve18(templateDir, "wphs.adapter.ts");
2866
+ if (existsSync13(templateAdapter)) {
2398
2867
  results.push(copyMissingFile(projectRoot, adapter, [templateAdapter], dryRun));
2399
2868
  } else {
2400
2869
  results.push({
@@ -2411,26 +2880,26 @@ function applyProjectSetup(ctx, options = {}) {
2411
2880
  }
2412
2881
  function copySharedSetupExamples(targetDir) {
2413
2882
  const sharedDir = kitPath("templates", "shared");
2414
- if (!existsSync12(sharedDir)) return;
2883
+ if (!existsSync13(sharedDir)) return;
2415
2884
  for (const name of [
2416
2885
  ".env.dev.example",
2417
2886
  ".env.stg.example",
2418
2887
  ".env.prod.example",
2419
2888
  "portal.cdn.json.example"
2420
2889
  ]) {
2421
- const src = resolve17(sharedDir, name);
2422
- const dest = resolve17(targetDir, name);
2423
- if (existsSync12(src) && !existsSync12(dest)) {
2890
+ const src = resolve18(sharedDir, name);
2891
+ const dest = resolve18(targetDir, name);
2892
+ if (existsSync13(src) && !existsSync13(dest)) {
2424
2893
  copyFileSync(src, dest);
2425
2894
  }
2426
2895
  }
2427
2896
  const hubspotExample = firstExisting([
2428
- resolve17(sharedDir, HUBSPOT_CONFIG_EXAMPLE),
2897
+ resolve18(sharedDir, HUBSPOT_CONFIG_EXAMPLE),
2429
2898
  kitPath("templates", HUBSPOT_CONFIG_EXAMPLE)
2430
2899
  ]);
2431
2900
  if (!hubspotExample) return;
2432
- const destHubspot = resolve17(targetDir, HUBSPOT_CONFIG_EXAMPLE);
2433
- if (!existsSync12(destHubspot) && !existsSync12(resolve17(targetDir, HUBSPOT_CONFIG_FILE))) {
2901
+ const destHubspot = resolve18(targetDir, HUBSPOT_CONFIG_EXAMPLE);
2902
+ if (!existsSync13(destHubspot) && !existsSync13(resolve18(targetDir, HUBSPOT_CONFIG_FILE))) {
2434
2903
  copyFileSync(hubspotExample, destHubspot);
2435
2904
  }
2436
2905
  }
@@ -2528,7 +2997,7 @@ function buildConfigForProject(template, projectName, router) {
2528
2997
  return config;
2529
2998
  }
2530
2999
  function assertTargetDirIsScaffoldable(targetDir) {
2531
- if (!existsSync13(targetDir)) return;
3000
+ if (!existsSync14(targetDir)) return;
2532
3001
  const entries = readdirSync2(targetDir).filter((entry) => entry !== ".DS_Store");
2533
3002
  if (entries.length > 0) {
2534
3003
  throw new WpHsError(`Target directory is not empty: ${targetDir}`, {
@@ -2539,7 +3008,7 @@ function assertTargetDirIsScaffoldable(targetDir) {
2539
3008
  }
2540
3009
  function resolveInitTargetDir(options, interactiveName) {
2541
3010
  if (options.directory) {
2542
- return resolve18(options.directory);
3011
+ return resolve19(options.directory);
2543
3012
  }
2544
3013
  const parentDir = process.cwd();
2545
3014
  const slug = options.name ?? interactiveName;
@@ -2553,7 +3022,7 @@ function resolveInitTargetDir(options, interactiveName) {
2553
3022
  if (error) {
2554
3023
  throw new WpHsError(error, { exitCode: ExitCode.USER_ERROR });
2555
3024
  }
2556
- return resolve18(parentDir, slug);
3025
+ return resolve19(parentDir, slug);
2557
3026
  }
2558
3027
  async function runInit(ctx, options) {
2559
3028
  const nonInteractive = ctx.global.ci || ctx.global.yes || ctx.global.json;
@@ -2562,7 +3031,7 @@ async function runInit(ctx, options) {
2562
3031
  let router = options.router;
2563
3032
  if (!nonInteractive) {
2564
3033
  if (!template) {
2565
- const type = await p3.select({
3034
+ const type = await p4.select({
2566
3035
  message: "Project type",
2567
3036
  options: [
2568
3037
  {
@@ -2574,22 +3043,22 @@ async function runInit(ctx, options) {
2574
3043
  { value: "theme-starter", label: "Theme starter", hint: "CMS theme templates" }
2575
3044
  ]
2576
3045
  });
2577
- if (p3.isCancel(type)) throw new WpHsError("Init cancelled", { exitCode: ExitCode.USER_ERROR });
3046
+ if (p4.isCancel(type)) throw new WpHsError("Init cancelled", { exitCode: ExitCode.USER_ERROR });
2578
3047
  template = type;
2579
3048
  }
2580
3049
  if (!options.directory && !projectName) {
2581
- const nameAnswer = await p3.text({
3050
+ const nameAnswer = await p4.text({
2582
3051
  message: "Project name (folder name)",
2583
3052
  placeholder: "my-portal-module",
2584
3053
  validate: (value) => validateProjectName(normalizeProjectName(value ?? ""))
2585
3054
  });
2586
- if (p3.isCancel(nameAnswer)) {
3055
+ if (p4.isCancel(nameAnswer)) {
2587
3056
  throw new WpHsError("Init cancelled", { exitCode: ExitCode.USER_ERROR });
2588
3057
  }
2589
3058
  projectName = normalizeProjectName(nameAnswer);
2590
3059
  }
2591
3060
  if (!router && template && isModuleTemplate(template)) {
2592
- const routerAnswer = await p3.select({
3061
+ const routerAnswer = await p4.select({
2593
3062
  message: "Client-side routing",
2594
3063
  initialValue: "hash",
2595
3064
  options: [
@@ -2610,7 +3079,7 @@ async function runInit(ctx, options) {
2610
3079
  }
2611
3080
  ]
2612
3081
  });
2613
- if (p3.isCancel(routerAnswer)) {
3082
+ if (p4.isCancel(routerAnswer)) {
2614
3083
  throw new WpHsError("Init cancelled", { exitCode: ExitCode.USER_ERROR });
2615
3084
  }
2616
3085
  router = routerAnswer;
@@ -2626,29 +3095,29 @@ async function runInit(ctx, options) {
2626
3095
  const slug = projectName ?? basename2(targetDir);
2627
3096
  assertTargetDirIsScaffoldable(targetDir);
2628
3097
  const templateDir = kitPath("templates", template);
2629
- if (!existsSync13(templateDir)) {
3098
+ if (!existsSync14(templateDir)) {
2630
3099
  throw new WpHsError(`Template not found: ${template}`, { exitCode: ExitCode.USER_ERROR });
2631
3100
  }
2632
3101
  if (ctx.global.dryRun) {
2633
3102
  ctx.logger.info(`[dry-run] Would scaffold ${template} in ${targetDir}`);
2634
3103
  return ExitCode.SUCCESS;
2635
3104
  }
2636
- mkdirSync3(targetDir, { recursive: true });
3105
+ mkdirSync4(targetDir, { recursive: true });
2637
3106
  cpSync2(templateDir, targetDir, { recursive: true });
2638
3107
  copySharedSetupExamples(targetDir);
2639
3108
  const config = buildConfigForProject(template, slug, router);
2640
- writeFileSync5(resolve18(targetDir, WOODSCLI_CONFIG_FILE), `${JSON.stringify(config, null, 2)}
3109
+ writeFileSync6(resolve19(targetDir, WOODSCLI_CONFIG_FILE), `${JSON.stringify(config, null, 2)}
2641
3110
  `);
2642
- const pkgPath = resolve18(targetDir, "package.json");
2643
- if (existsSync13(pkgPath)) {
2644
- const pkg = JSON.parse(readFileSync10(pkgPath, "utf8"));
3111
+ const pkgPath = resolve19(targetDir, "package.json");
3112
+ if (existsSync14(pkgPath)) {
3113
+ const pkg = JSON.parse(readFileSync11(pkgPath, "utf8"));
2645
3114
  pkg.name = slug;
2646
3115
  pkg.scripts = { ...pkg.scripts, ...TEMPLATE_SCRIPTS[template] };
2647
3116
  pkg.devDependencies = {
2648
3117
  ...pkg.devDependencies,
2649
3118
  ...templateDevDependencies()
2650
3119
  };
2651
- writeFileSync5(pkgPath, `${JSON.stringify(pkg, null, 2)}
3120
+ writeFileSync6(pkgPath, `${JSON.stringify(pkg, null, 2)}
2652
3121
  `);
2653
3122
  }
2654
3123
  if (router && isModuleTemplate(template)) {
@@ -2659,7 +3128,7 @@ async function runInit(ctx, options) {
2659
3128
  logger: ctx.logger,
2660
3129
  dryRun: false
2661
3130
  });
2662
- const gitignorePath = resolve18(targetDir, ".gitignore");
3131
+ const gitignorePath = resolve19(targetDir, ".gitignore");
2663
3132
  const gitignoreExtras = [
2664
3133
  "hubspot.config.yml",
2665
3134
  ".wphs/config/module-config.local.json",
@@ -2672,10 +3141,10 @@ async function runInit(ctx, options) {
2672
3141
  ".wphs/.kit-bootstrap.tsx",
2673
3142
  ".wphs/.kit-bootstrap-greenfield.tsx"
2674
3143
  ];
2675
- const existing = existsSync13(gitignorePath) ? readFileSync10(gitignorePath, "utf8") : "";
3144
+ const existing = existsSync14(gitignorePath) ? readFileSync11(gitignorePath, "utf8") : "";
2676
3145
  const toAppend = gitignoreExtras.filter((line) => !existing.includes(line)).join("\n");
2677
3146
  if (toAppend) {
2678
- writeFileSync5(gitignorePath, `${existing.trimEnd()}
3147
+ writeFileSync6(gitignorePath, `${existing.trimEnd()}
2679
3148
  ${toAppend}
2680
3149
  `);
2681
3150
  }
@@ -2687,7 +3156,7 @@ ${toAppend}
2687
3156
  cdnSetup = { github: options.cdnGithub };
2688
3157
  }
2689
3158
  } else {
2690
- p3.log.step("Hybrid CDN \u2014 GitHub mirror (public repo for jsDelivr)");
3159
+ p4.log.step("Hybrid CDN \u2014 GitHub mirror (public repo for jsDelivr)");
2691
3160
  cdnSetup = await promptCdnGitHubSetup({
2692
3161
  projectRoot: targetDir,
2693
3162
  githubDefault,
@@ -2697,15 +3166,15 @@ ${toAppend}
2697
3166
  if (cdnSetup) {
2698
3167
  applyCdnGitHubSetup(targetDir, cdnSetup);
2699
3168
  if (!nonInteractive) {
2700
- p3.log.success(formatCdnSetupNote(targetDir));
3169
+ p4.log.success(formatCdnSetupNote(targetDir));
2701
3170
  } else {
2702
3171
  ctx.logger.success(formatCdnSetupNote(targetDir));
2703
3172
  }
2704
3173
  }
2705
3174
  }
2706
3175
  if (!nonInteractive) {
2707
- p3.log.success(`Scaffolded ${template} in ${targetDir}`);
2708
- p3.note(`cd ${slug} && yarn install && wp setup && wp doctor && wp dev`, "Getting started");
3176
+ p4.log.success(`Scaffolded ${template} in ${targetDir}`);
3177
+ p4.note(`cd ${slug} && yarn install && wp setup && wp doctor && wp dev`, "Getting started");
2709
3178
  } else {
2710
3179
  ctx.logger.success(`Scaffolded ${template} in ${targetDir}`);
2711
3180
  }
@@ -2713,70 +3182,112 @@ ${toAppend}
2713
3182
  }
2714
3183
 
2715
3184
  // src/cli/commands/migrate.ts
2716
- import { cpSync as cpSync3, existsSync as existsSync14, mkdirSync as mkdirSync4, readFileSync as readFileSync11, readdirSync as readdirSync3, renameSync, rmSync as rmSync3, writeFileSync as writeFileSync6 } from "fs";
2717
- import { join as join3, resolve as resolve19 } from "path";
3185
+ import { cpSync as cpSync3, existsSync as existsSync15, mkdirSync as mkdirSync5, readFileSync as readFileSync12, readdirSync as readdirSync3, renameSync, rmSync as rmSync3, writeFileSync as writeFileSync7 } from "fs";
3186
+ import { join as join3, resolve as resolve20 } from "path";
2718
3187
  var LEGACY_DIRS = [".dev", ".woodsportal"];
2719
3188
  var LEGACY_BASELINE = ".wp-hs-baseline";
3189
+ var LEGACY_BUILD_ID_FILE = ".portal-build-id";
3190
+ var MODULE_BUILD_ID_FILE = ".module-build-id";
3191
+ function readFirstNonCommentLine(filePath) {
3192
+ if (!existsSync15(filePath)) return void 0;
3193
+ const line = readFileSync12(filePath, "utf8").split("\n").map((l) => l.trim()).find((l) => l.length > 0 && !l.startsWith("#"));
3194
+ return line || void 0;
3195
+ }
3196
+ function stripToBaseline(raw) {
3197
+ const line = raw?.trim();
3198
+ if (!line) return void 0;
3199
+ const perDev = line.match(/^(v\d+\.\d+\.\d+(?:-[a-z0-9]+)?)\.[a-z0-9-]{1,12}\.\d+$/i);
3200
+ if (perDev) return perDev[1];
3201
+ const numericSuffix = line.match(/^(v\d+\.\d+\.\d+(?:-[a-z0-9]+)?)\.\d+$/);
3202
+ if (numericSuffix) return numericSuffix[1];
3203
+ return line;
3204
+ }
2720
3205
  async function runMigrate(ctx, _options = {}) {
2721
3206
  const root = ctx.projectRoot;
2722
- const wphsRoot = resolve19(root, WPHS_WORKSPACE_DIR);
3207
+ const wphsRoot = resolve20(root, WPHS_WORKSPACE_DIR);
2723
3208
  const actions = [];
2724
3209
  for (const legacy of LEGACY_DIRS) {
2725
- const legacyPath = resolve19(root, legacy);
2726
- if (!existsSync14(legacyPath)) continue;
3210
+ const legacyPath = resolve20(root, legacy);
3211
+ if (!existsSync15(legacyPath)) continue;
2727
3212
  actions.push(`Move ${legacy}/ \u2192 .wphs/`);
2728
3213
  if (ctx.global.dryRun) continue;
2729
- mkdirSync4(join3(wphsRoot, "config"), { recursive: true });
2730
- mkdirSync4(join3(wphsRoot, "assets", "branding"), { recursive: true });
3214
+ mkdirSync5(join3(wphsRoot, "config"), { recursive: true });
3215
+ mkdirSync5(join3(wphsRoot, "assets", "branding"), { recursive: true });
2731
3216
  const localConfig = join3(legacyPath, "module-config.local.json");
2732
3217
  const exampleConfig = join3(legacyPath, "module-config.example.json");
2733
- if (existsSync14(localConfig)) {
3218
+ if (existsSync15(localConfig)) {
2734
3219
  cpSync3(localConfig, join3(wphsRoot, "config", "module-config.local.json"));
2735
3220
  }
2736
- if (existsSync14(exampleConfig)) {
3221
+ if (existsSync15(exampleConfig)) {
2737
3222
  cpSync3(exampleConfig, join3(wphsRoot, "config", "module-config.example.json"));
2738
3223
  }
2739
3224
  const branding = join3(legacyPath, "branding-assets");
2740
- if (existsSync14(branding)) {
3225
+ if (existsSync15(branding)) {
2741
3226
  for (const file of readdirSync3(branding)) {
2742
3227
  cpSync3(join3(branding, file), join3(wphsRoot, "assets", "branding", file));
2743
3228
  }
2744
3229
  }
2745
3230
  rmSync3(legacyPath, { recursive: true, force: true });
2746
3231
  }
2747
- const legacyBaseline = resolve19(root, LEGACY_BASELINE);
3232
+ const legacyBaseline = resolve20(root, LEGACY_BASELINE);
2748
3233
  const targetBaseline = join3(wphsRoot, "audit", "baseline");
2749
- if (existsSync14(legacyBaseline)) {
3234
+ if (existsSync15(legacyBaseline)) {
2750
3235
  actions.push(`Move ${LEGACY_BASELINE}/ \u2192 .wphs/audit/baseline/`);
2751
3236
  if (!ctx.global.dryRun) {
2752
- mkdirSync4(targetBaseline, { recursive: true });
3237
+ mkdirSync5(targetBaseline, { recursive: true });
2753
3238
  cpSync3(legacyBaseline, targetBaseline, { recursive: true });
2754
3239
  rmSync3(legacyBaseline, { recursive: true, force: true });
2755
3240
  }
2756
3241
  }
2757
- const legacyConfigPath2 = resolve19(root, LEGACY_WOODSPORTAL_HS_FILE);
2758
- const woodscliPath = resolve19(root, WOODSCLI_CONFIG_FILE);
2759
- if (existsSync14(legacyConfigPath2) && !existsSync14(woodscliPath)) {
3242
+ const legacyConfigPath2 = resolve20(root, LEGACY_WOODSPORTAL_HS_FILE);
3243
+ const woodscliPath = resolve20(root, WOODSCLI_CONFIG_FILE);
3244
+ if (existsSync15(legacyConfigPath2) && !existsSync15(woodscliPath)) {
2760
3245
  actions.push(`Rename ${LEGACY_WOODSPORTAL_HS_FILE} \u2192 ${WOODSCLI_CONFIG_FILE}`);
2761
3246
  if (!ctx.global.dryRun) {
2762
3247
  renameSync(legacyConfigPath2, woodscliPath);
2763
3248
  }
2764
- } else if (existsSync14(legacyConfigPath2) && existsSync14(woodscliPath)) {
3249
+ } else if (existsSync15(legacyConfigPath2) && existsSync15(woodscliPath)) {
2765
3250
  actions.push(`Remove duplicate ${LEGACY_WOODSPORTAL_HS_FILE}`);
2766
3251
  if (!ctx.global.dryRun) {
2767
3252
  rmSync3(legacyConfigPath2, { force: true });
2768
3253
  }
2769
3254
  }
2770
- if (existsSync14(woodscliPath)) {
2771
- const raw = JSON.parse(readFileSync11(woodscliPath, "utf8"));
3255
+ const legacyBuildIdPath = resolve20(root, LEGACY_BUILD_ID_FILE);
3256
+ const moduleBuildIdPath = resolve20(root, MODULE_BUILD_ID_FILE);
3257
+ if (existsSync15(legacyBuildIdPath) && !existsSync15(moduleBuildIdPath)) {
3258
+ const baseline = stripToBaseline(readFirstNonCommentLine(legacyBuildIdPath)) ?? "v0.0.1-dev";
3259
+ actions.push(`Migrate ${LEGACY_BUILD_ID_FILE} \u2192 ${MODULE_BUILD_ID_FILE} (baseline ${baseline})`);
3260
+ if (!ctx.global.dryRun) {
3261
+ writeFileSync7(
3262
+ moduleBuildIdPath,
3263
+ `# Channel baseline for local CDN deploys (wp deploy auto-bumps per developer).
3264
+ ${baseline}
3265
+ `
3266
+ );
3267
+ rmSync3(legacyBuildIdPath, { force: true });
3268
+ }
3269
+ }
3270
+ if (existsSync15(woodscliPath)) {
3271
+ const raw = JSON.parse(readFileSync12(woodscliPath, "utf8"));
3272
+ let configChanged = false;
2772
3273
  if (raw?.dev?.moduleConfigDir && raw.dev.moduleConfigDir !== WPHS_WORKSPACE_DIR) {
2773
3274
  actions.push(`Update dev.moduleConfigDir \u2192 .wphs in project config`);
2774
3275
  if (!ctx.global.dryRun) {
2775
3276
  raw.dev.moduleConfigDir = WPHS_WORKSPACE_DIR;
2776
- writeFileSync6(woodscliPath, `${JSON.stringify(raw, null, 2)}
2777
- `);
3277
+ configChanged = true;
3278
+ }
3279
+ }
3280
+ if (raw?.cdn?.buildIdFile === LEGACY_BUILD_ID_FILE) {
3281
+ actions.push(`Update cdn.buildIdFile \u2192 ${MODULE_BUILD_ID_FILE}`);
3282
+ if (!ctx.global.dryRun) {
3283
+ raw.cdn.buildIdFile = MODULE_BUILD_ID_FILE;
3284
+ configChanged = true;
2778
3285
  }
2779
3286
  }
3287
+ if (configChanged && !ctx.global.dryRun) {
3288
+ writeFileSync7(woodscliPath, `${JSON.stringify(raw, null, 2)}
3289
+ `);
3290
+ }
2780
3291
  }
2781
3292
  if (actions.length === 0) {
2782
3293
  ctx.logger.success("Nothing to migrate \u2014 already on .wphs / woodscli.json");
@@ -2802,10 +3313,10 @@ async function runMigrate(ctx, _options = {}) {
2802
3313
  }
2803
3314
 
2804
3315
  // src/cli/commands/examples.ts
2805
- import { readFileSync as readFileSync12 } from "fs";
3316
+ import { readFileSync as readFileSync13 } from "fs";
2806
3317
  function loadManifest() {
2807
3318
  const manifestPath = kitPath("examples", "manifest.json");
2808
- const raw = JSON.parse(readFileSync12(manifestPath, "utf8"));
3319
+ const raw = JSON.parse(readFileSync13(manifestPath, "utf8"));
2809
3320
  return raw.scenarios;
2810
3321
  }
2811
3322
  async function runExamplesList(ctx, options = {}) {
@@ -2838,15 +3349,15 @@ async function runExamplesSmoke(ctx, options = {}) {
2838
3349
  }
2839
3350
 
2840
3351
  // src/cli/commands/fields.ts
2841
- import { cpSync as cpSync4, existsSync as existsSync15, readFileSync as readFileSync13, writeFileSync as writeFileSync7 } from "fs";
2842
- import { resolve as resolve20 } from "path";
3352
+ import { cpSync as cpSync4, existsSync as existsSync16, readFileSync as readFileSync14, writeFileSync as writeFileSync8 } from "fs";
3353
+ import { resolve as resolve21 } from "path";
2843
3354
  async function runFieldsSync(ctx, options = {}) {
2844
3355
  const direction = options.direction ?? "pull";
2845
- const fieldsPath = resolve20(ctx.projectRoot, ctx.config.dev.fieldsJson);
2846
- const cacheDir = resolve20(ctx.projectRoot, ctx.config.dev.moduleConfigDir, "cache", "fields-sync");
2847
- const cachePath = resolve20(cacheDir, "fields.json");
3356
+ const fieldsPath = resolve21(ctx.projectRoot, ctx.config.dev.fieldsJson);
3357
+ const cacheDir = resolve21(ctx.projectRoot, ctx.config.dev.moduleConfigDir, "cache", "fields-sync");
3358
+ const cachePath = resolve21(cacheDir, "fields.json");
2848
3359
  if (direction === "pull") {
2849
- if (!existsSync15(fieldsPath)) {
3360
+ if (!existsSync16(fieldsPath)) {
2850
3361
  throw new WpHsError(`Missing ${ctx.config.dev.fieldsJson}`, {
2851
3362
  exitCode: ExitCode.USER_ERROR,
2852
3363
  remediation: "Add fields.json or run wp init."
@@ -2856,13 +3367,13 @@ async function runFieldsSync(ctx, options = {}) {
2856
3367
  ctx.logger.info(`[dry-run] Would cache ${fieldsPath} \u2192 ${cachePath}`);
2857
3368
  return ExitCode.SUCCESS;
2858
3369
  }
2859
- const { mkdirSync: mkdirSync5 } = await import("fs");
2860
- mkdirSync5(cacheDir, { recursive: true });
3370
+ const { mkdirSync: mkdirSync6 } = await import("fs");
3371
+ mkdirSync6(cacheDir, { recursive: true });
2861
3372
  cpSync4(fieldsPath, cachePath);
2862
3373
  ctx.logger.success(`Cached fields \u2192 ${cachePath}`);
2863
3374
  return ExitCode.SUCCESS;
2864
3375
  }
2865
- if (!existsSync15(cachePath)) {
3376
+ if (!existsSync16(cachePath)) {
2866
3377
  throw new WpHsError("No cached fields \u2014 run wp fields sync pull first.", {
2867
3378
  exitCode: ExitCode.USER_ERROR
2868
3379
  });
@@ -2871,14 +3382,14 @@ async function runFieldsSync(ctx, options = {}) {
2871
3382
  ctx.logger.info(`[dry-run] Would write ${cachePath} \u2192 ${fieldsPath}`);
2872
3383
  return ExitCode.SUCCESS;
2873
3384
  }
2874
- writeFileSync7(fieldsPath, readFileSync13(cachePath, "utf8"));
3385
+ writeFileSync8(fieldsPath, readFileSync14(cachePath, "utf8"));
2875
3386
  ctx.logger.success(`Restored fields from cache \u2192 ${fieldsPath}`);
2876
3387
  return ExitCode.SUCCESS;
2877
3388
  }
2878
3389
 
2879
3390
  // src/cli/commands/regression.ts
2880
- import { existsSync as existsSync16 } from "fs";
2881
- import { resolve as resolve21 } from "path";
3391
+ import { existsSync as existsSync17 } from "fs";
3392
+ import { resolve as resolve22 } from "path";
2882
3393
  var LEGACY_SHIM_SCRIPTS = [
2883
3394
  "build-cdn.mjs",
2884
3395
  "run-vite-build.mjs",
@@ -2901,21 +3412,21 @@ async function runRegressionSmoke(ctx, options = {}) {
2901
3412
  if (!validateOk) failures.push("wp config validate failed");
2902
3413
  if (options.checkLegacyShims !== false) {
2903
3414
  for (const script of LEGACY_SHIM_SCRIPTS) {
2904
- const shim = resolve21(ctx.projectRoot, "scripts", script);
3415
+ const shim = resolve22(ctx.projectRoot, "scripts", script);
2905
3416
  const kitScript = kitPath("scripts", script);
2906
- const shimOk = existsSync16(shim) && existsSync16(kitScript);
3417
+ const shimOk = existsSync17(shim) && existsSync17(kitScript);
2907
3418
  checks.push({ name: `shim:${script}`, ok: shimOk });
2908
3419
  if (!shimOk) failures.push(`Missing legacy shim or kit script: ${script}`);
2909
3420
  }
2910
3421
  }
2911
3422
  for (const rel of FORBIDDEN_CONSUMER_PATHS) {
2912
- const forbidden = resolve21(ctx.projectRoot, rel);
2913
- const absent = !existsSync16(forbidden);
3423
+ const forbidden = resolve22(ctx.projectRoot, rel);
3424
+ const absent = !existsSync17(forbidden);
2914
3425
  checks.push({ name: `no-${rel}`, ok: absent });
2915
3426
  if (!absent) failures.push(`Forbidden dev path still present: ${rel}`);
2916
3427
  }
2917
- const woodscliConfig = resolve21(ctx.projectRoot, WOODSCLI_CONFIG_FILE);
2918
- const configOk = existsSync16(woodscliConfig);
3428
+ const woodscliConfig = resolve22(ctx.projectRoot, WOODSCLI_CONFIG_FILE);
3429
+ const configOk = existsSync17(woodscliConfig);
2919
3430
  checks.push({ name: WOODSCLI_CONFIG_FILE, ok: configOk });
2920
3431
  if (!configOk) failures.push(`${WOODSCLI_CONFIG_FILE} missing`);
2921
3432
  if (ctx.global.json) {
@@ -2974,6 +3485,12 @@ async function runSetup(ctx, options = {}) {
2974
3485
  const copied = results.filter((r) => r.action === "copied");
2975
3486
  const manual = results.filter((r) => r.action === "manual");
2976
3487
  const nonInteractive = ctx.global.ci || ctx.global.yes || ctx.global.json || ctx.global.dryRun;
3488
+ if (usesCdnPipeline(ctx.config) && !nonInteractive && !ctx.global.dryRun) {
3489
+ const slug = await promptDeveloperSlug(ctx.projectRoot);
3490
+ if (slug) {
3491
+ ctx.logger.success(`CDN dev slug \u2192 ${slug} (.wphs/config/developer.json)`);
3492
+ }
3493
+ }
2977
3494
  if (usesCdnPipeline(ctx.config) && needsCdnGitHubSetup(ctx.projectRoot) && !nonInteractive) {
2978
3495
  const cdnSetup = await promptCdnGitHubSetup({
2979
3496
  projectRoot: ctx.projectRoot,
@@ -3019,11 +3536,11 @@ async function runSetup(ctx, options = {}) {
3019
3536
  }
3020
3537
 
3021
3538
  // src/cli/commands/portal.ts
3022
- import * as p4 from "@clack/prompts";
3539
+ import * as p5 from "@clack/prompts";
3023
3540
 
3024
3541
  // src/lib/hubspot-portals.ts
3025
- import { existsSync as existsSync17, readFileSync as readFileSync14 } from "fs";
3026
- import { resolve as resolve22 } from "path";
3542
+ import { existsSync as existsSync18, readFileSync as readFileSync15 } from "fs";
3543
+ import { resolve as resolve23 } from "path";
3027
3544
  function stripYamlScalar(value) {
3028
3545
  const trimmed = value.trim();
3029
3546
  if (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'")) {
@@ -3061,20 +3578,20 @@ function parseHubSpotConfigPortals(raw) {
3061
3578
  return { defaultPortal, portals };
3062
3579
  }
3063
3580
  function readHubSpotPortals(projectRoot) {
3064
- const configPath = resolve22(projectRoot, HUBSPOT_CONFIG_FILE);
3065
- if (!existsSync17(configPath)) {
3581
+ const configPath = resolve23(projectRoot, HUBSPOT_CONFIG_FILE);
3582
+ if (!existsSync18(configPath)) {
3066
3583
  throw new WpHsError(`${HUBSPOT_CONFIG_FILE} not found in project root.`, {
3067
3584
  exitCode: ExitCode.USER_ERROR,
3068
3585
  remediation: "Run wp setup, then add your portal + PAT."
3069
3586
  });
3070
3587
  }
3071
- const raw = readFileSync14(configPath, "utf8");
3588
+ const raw = readFileSync15(configPath, "utf8");
3072
3589
  const { defaultPortal, portals } = parseHubSpotConfigPortals(raw);
3073
3590
  return { configPath, defaultPortal, portals };
3074
3591
  }
3075
3592
  function resolvePortalName(portals, nameOrId) {
3076
3593
  const needle = nameOrId.trim();
3077
- return portals.find((p5) => p5.name === needle) ?? portals.find((p5) => p5.portalId === needle) ?? null;
3594
+ return portals.find((p6) => p6.name === needle) ?? portals.find((p6) => p6.portalId === needle) ?? null;
3078
3595
  }
3079
3596
 
3080
3597
  // src/cli/commands/portal.ts
@@ -3129,7 +3646,7 @@ async function runPortalUse(ctx, options = {}) {
3129
3646
  remediation: "wp portal use <name> \xB7 wp portal list"
3130
3647
  });
3131
3648
  }
3132
- const answer = await p4.select({
3649
+ const answer = await p5.select({
3133
3650
  message: "Set default HubSpot portal",
3134
3651
  options: data.portals.map((portal2) => ({
3135
3652
  value: portal2.name,
@@ -3137,7 +3654,7 @@ async function runPortalUse(ctx, options = {}) {
3137
3654
  hint: portal2.name === data.defaultPortal ? `current default \xB7 ${portal2.portalId ?? "no id"}` : portal2.portalId ?? void 0
3138
3655
  }))
3139
3656
  });
3140
- if (p4.isCancel(answer)) {
3657
+ if (p5.isCancel(answer)) {
3141
3658
  throw new WpHsError("Portal selection cancelled", { exitCode: ExitCode.USER_ERROR });
3142
3659
  }
3143
3660
  targetName = answer;
@@ -3198,7 +3715,7 @@ async function runWatch(ctx, options) {
3198
3715
  // src/cli/program.ts
3199
3716
  function readVersion() {
3200
3717
  try {
3201
- const pkg = JSON.parse(readFileSync15(resolve23(getKitRoot(), "package.json"), "utf8"));
3718
+ const pkg = JSON.parse(readFileSync16(resolve24(getKitRoot(), "package.json"), "utf8"));
3202
3719
  return pkg.version ?? "0.0.0";
3203
3720
  } catch {
3204
3721
  return "0.0.0";
@@ -3216,7 +3733,7 @@ function buildContext(global) {
3216
3733
  return { projectRoot, config, logger, global };
3217
3734
  }
3218
3735
  function attachGlobals(command) {
3219
- command.option("--project <path>", "Project root (woodscli.json)").option("--ci", "Non-interactive CI mode", false).option("--json", "Machine-readable JSON output", false).option("--verbose", "Step-by-step logs and child-process output (disables spinners)", false).option("--quiet", "Errors only", false).option("--dry-run", "Print planned actions without executing", false).option("-y, --yes", "Assume yes (non-interactive)", false);
3736
+ command.option("--project <path>", "Project root (woodscli.json)").option("--ci", "Non-interactive CI mode", false).option("--json", "Machine-readable JSON output", false).option("--verbose", "Step-by-step logs and child-process output (disables spinners)", false).option("--quiet", "Errors only", false).option("--dry-run", "Print planned actions without executing", false).option("-y, --yes", "Assume yes (non-interactive)", false).option("--no-bump", "Do not auto-increment module build id before CDN build", false);
3220
3737
  }
3221
3738
  async function handle(fn) {
3222
3739
  try {