@woodsportal/hubspot-kit 1.0.27 → 1.0.30

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.
package/dist/cli.js CHANGED
@@ -9,8 +9,8 @@ import {
9
9
  } from "./chunk-GTTIJVAR.js";
10
10
 
11
11
  // src/cli/program.ts
12
- import { readFileSync as readFileSync14 } from "fs";
13
- import { resolve as resolve21 } from "path";
12
+ import { readFileSync as readFileSync15 } from "fs";
13
+ import { resolve as resolve23 } from "path";
14
14
  import { Command } from "commander";
15
15
 
16
16
  // src/lib/config.ts
@@ -185,14 +185,14 @@ function getKitRoot() {
185
185
  return kitRoot;
186
186
  }
187
187
  function findProjectRoot2(from = process.cwd()) {
188
- const root = findProjectRoot(from);
189
- if (!root) {
188
+ const resolved = findProjectRoot(from);
189
+ if (!resolved) {
190
190
  throw new WpHsError("Could not find woodscli.json in this directory or any parent.", {
191
191
  exitCode: ExitCode.USER_ERROR,
192
- remediation: "Run wp init, wp adopt, or pass --project <path>. Legacy config: wp migrate --yes."
192
+ remediation: "Run wp init, or pass --project <path>. Legacy config: wp migrate --yes."
193
193
  });
194
194
  }
195
- return root;
195
+ return resolved;
196
196
  }
197
197
  function kitPath(...segments) {
198
198
  return resolve2(kitRoot, ...segments);
@@ -205,7 +205,7 @@ function loadConfig(projectRoot) {
205
205
  if (!resolved) {
206
206
  throw new WpHsError(`Missing ${WOODSCLI_CONFIG_FILE}`, {
207
207
  exitCode: ExitCode.USER_ERROR,
208
- remediation: "Run wp init or wp adopt. Legacy woodsportal.hs.json \u2192 run wp migrate --yes."
208
+ remediation: "Run wp init. Legacy woodsportal.hs.json \u2192 run wp migrate --yes."
209
209
  });
210
210
  }
211
211
  let raw;
@@ -293,8 +293,8 @@ function defaultBaselineDir(projectRoot, config) {
293
293
  }
294
294
 
295
295
  // src/cli/commands/doctor.ts
296
- import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
297
- import { join, resolve as resolve5 } from "path";
296
+ import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
297
+ import { join, resolve as resolve6 } from "path";
298
298
 
299
299
  // src/lib/cli-spinner.ts
300
300
  import * as p from "@clack/prompts";
@@ -520,7 +520,7 @@ var VITE_DEV_PEER_DEPS = {
520
520
  tailwindcss: "^4.0.6"
521
521
  };
522
522
  var KIT_DEV_DEP = "@woodsportal/hubspot-kit";
523
- var KIT_DEV_DEP_VERSION = "^1.0.27";
523
+ var KIT_DEV_DEP_VERSION = "^1.0.30";
524
524
  var MODULE_CONFIG_UI_DEPS = [
525
525
  "@dnd-kit/core",
526
526
  "@dnd-kit/sortable",
@@ -533,6 +533,16 @@ function templateDevDependencies() {
533
533
  };
534
534
  }
535
535
 
536
+ // src/lib/upload-only-module.ts
537
+ import { existsSync as existsSync4 } from "fs";
538
+ import { resolve as resolve5 } from "path";
539
+ function isUploadOnlyPreexistingProject(projectRoot, moduleArtifactDirs) {
540
+ if (moduleArtifactDirs.length === 0) return false;
541
+ if (existsSync4(resolve5(projectRoot, "package.json"))) return false;
542
+ if (existsSync4(resolve5(projectRoot, "src"))) return false;
543
+ return moduleArtifactDirs.some((dir) => existsSync4(resolve5(dir, "fields.json")));
544
+ }
545
+
536
546
  // src/cli/commands/doctor.ts
537
547
  function checkNode() {
538
548
  const major = Number(process.versions.node.split(".")[0]);
@@ -598,8 +608,8 @@ function checkConfig(ctx) {
598
608
  function checkEnvFiles(ctx) {
599
609
  return ctx.config.env.tiers.map((tier) => {
600
610
  const file = tier === "dev" ? ".env.dev" : tier === "stg" ? ".env.stg" : ".env.prod";
601
- const path = resolve5(ctx.projectRoot, file);
602
- const ok = existsSync4(path);
611
+ const path = resolve6(ctx.projectRoot, file);
612
+ const ok = existsSync5(path);
603
613
  return {
604
614
  name: `env-${tier}`,
605
615
  ok,
@@ -611,8 +621,8 @@ function checkEnvFiles(ctx) {
611
621
  }
612
622
  function checkBuildId(ctx) {
613
623
  if (isThemeProject(ctx.config) || !usesCdnPipeline(ctx.config)) return null;
614
- const file = resolve5(ctx.projectRoot, ctx.config.cdn.buildIdFile);
615
- const ok = existsSync4(file);
624
+ const file = resolve6(ctx.projectRoot, ctx.config.cdn.buildIdFile);
625
+ const ok = existsSync5(file);
616
626
  return {
617
627
  name: "build-id",
618
628
  ok,
@@ -621,9 +631,23 @@ function checkBuildId(ctx) {
621
631
  severity: "warn"
622
632
  };
623
633
  }
634
+ function isUploadOnlyProject(ctx) {
635
+ const moduleDirs = [resolve6(ctx.projectRoot, ctx.config.module.uploadPath)];
636
+ return isUploadOnlyPreexistingProject(ctx.projectRoot, moduleDirs);
637
+ }
638
+ function checkUploadOnlyNote(ctx) {
639
+ if (!isUploadOnlyProject(ctx)) return null;
640
+ return {
641
+ name: "upload-only-module",
642
+ ok: true,
643
+ message: "Upload-only module (no local dev app)",
644
+ remediation: "Add package.json + src/ via wp init in a new folder, or continue upload-only with wp upload",
645
+ severity: "warn"
646
+ };
647
+ }
624
648
  function checkDevViteConfig(ctx) {
625
- const path = resolve5(ctx.projectRoot, "vite.config.dev.mjs");
626
- const ok = existsSync4(path);
649
+ const path = resolve6(ctx.projectRoot, "vite.config.dev.mjs");
650
+ const ok = existsSync5(path);
627
651
  return {
628
652
  name: "vite-dev-config",
629
653
  ok,
@@ -634,8 +658,8 @@ function checkDevViteConfig(ctx) {
634
658
  }
635
659
  function checkDevEntry(ctx) {
636
660
  const checks = [];
637
- const indexHtml = resolve5(ctx.projectRoot, "index.html");
638
- const hasIndex = existsSync4(indexHtml);
661
+ const indexHtml = resolve6(ctx.projectRoot, "index.html");
662
+ const hasIndex = existsSync5(indexHtml);
639
663
  checks.push({
640
664
  name: "index-html",
641
665
  ok: hasIndex,
@@ -643,8 +667,8 @@ function checkDevEntry(ctx) {
643
667
  remediation: hasIndex ? void 0 : "Re-run wp init or copy index.html from kit templates",
644
668
  severity: "error"
645
669
  });
646
- const localDev = resolve5(ctx.projectRoot, ctx.config.entries.localDev);
647
- const hasEntry = existsSync4(localDev);
670
+ const localDev = resolve6(ctx.projectRoot, ctx.config.entries.localDev);
671
+ const hasEntry = existsSync5(localDev);
648
672
  checks.push({
649
673
  name: "local-dev-entry",
650
674
  ok: hasEntry,
@@ -671,7 +695,7 @@ function checkDevEntry(ctx) {
671
695
  }
672
696
  function checkKitScripts() {
673
697
  const script = kitPath("scripts", "build-cdn.mjs");
674
- const ok = existsSync4(script);
698
+ const ok = existsSync5(script);
675
699
  return {
676
700
  name: "kit-scripts",
677
701
  ok,
@@ -684,8 +708,8 @@ function checkWphsWorkspace(ctx) {
684
708
  const workspace = ctx.config.dev.moduleConfigDir;
685
709
  const checks = [];
686
710
  for (const legacy of [".dev", ".woodsportal"]) {
687
- const legacyPath = resolve5(ctx.projectRoot, legacy);
688
- if (existsSync4(legacyPath)) {
711
+ const legacyPath = resolve6(ctx.projectRoot, legacy);
712
+ if (existsSync5(legacyPath)) {
689
713
  checks.push({
690
714
  name: `legacy-${legacy}`,
691
715
  ok: false,
@@ -695,7 +719,7 @@ function checkWphsWorkspace(ctx) {
695
719
  });
696
720
  }
697
721
  }
698
- if (existsSync4(resolve5(ctx.projectRoot, ".wp-hs-baseline"))) {
722
+ if (existsSync5(resolve6(ctx.projectRoot, ".wp-hs-baseline"))) {
699
723
  checks.push({
700
724
  name: "legacy-baseline",
701
725
  ok: false,
@@ -714,10 +738,10 @@ function checkWphsWorkspace(ctx) {
714
738
  });
715
739
  return checks;
716
740
  }
717
- const wphsRoot = resolve5(ctx.projectRoot, workspace);
741
+ const wphsRoot = resolve6(ctx.projectRoot, workspace);
718
742
  const required = ["config", "assets/branding", "fixtures", "cache", "audit/baseline", "state"];
719
743
  for (const sub of required) {
720
- const ok = existsSync4(join(wphsRoot, sub));
744
+ const ok = existsSync5(join(wphsRoot, sub));
721
745
  checks.push({
722
746
  name: `wphs-${sub.replace(/\//g, "-")}`,
723
747
  ok,
@@ -730,20 +754,20 @@ function checkWphsWorkspace(ctx) {
730
754
  }
731
755
  function checkAdapter(ctx) {
732
756
  if (isThemeProject(ctx.config)) return null;
733
- const adapterPath = resolve5(ctx.projectRoot, ctx.config.dev.adapter);
734
- const ok = existsSync4(adapterPath);
757
+ const adapterPath = resolve6(ctx.projectRoot, ctx.config.dev.adapter);
758
+ const ok = existsSync5(adapterPath);
735
759
  return {
736
760
  name: "wphs-adapter",
737
761
  ok,
738
762
  message: ok ? `${ctx.config.dev.adapter} present` : `${ctx.config.dev.adapter} missing`,
739
- remediation: ok ? void 0 : "Run wp adopt or create wphs.adapter.ts",
763
+ remediation: ok ? void 0 : "Create wphs.adapter.ts (see kit templates)",
740
764
  severity: "warn"
741
765
  };
742
766
  }
743
767
  function checkModuleConfigUiDeps(ctx) {
744
768
  if (isThemeProject(ctx.config)) return [];
745
- const pkgPath = resolve5(ctx.projectRoot, "package.json");
746
- if (!existsSync4(pkgPath)) return [];
769
+ const pkgPath = resolve6(ctx.projectRoot, "package.json");
770
+ if (!existsSync5(pkgPath)) return [];
747
771
  let deps = {};
748
772
  try {
749
773
  const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
@@ -763,11 +787,11 @@ function checkModuleConfigUiDeps(ctx) {
763
787
  });
764
788
  }
765
789
  function checkViteInstalled(ctx) {
766
- if (isThemeProject(ctx.config) && !existsSync4(resolve5(ctx.projectRoot, "vite.config.dev.mjs"))) {
790
+ if (isThemeProject(ctx.config) && !existsSync5(resolve6(ctx.projectRoot, "vite.config.dev.mjs"))) {
767
791
  return null;
768
792
  }
769
- const localBin = resolve5(ctx.projectRoot, "node_modules", ".bin", "vite");
770
- const ok = existsSync4(localBin);
793
+ const localBin = resolve6(ctx.projectRoot, "node_modules", ".bin", "vite");
794
+ const ok = existsSync5(localBin);
771
795
  return {
772
796
  name: "vite",
773
797
  ok,
@@ -796,10 +820,16 @@ async function gatherDoctorChecks(ctx) {
796
820
  {
797
821
  title: "Checking Node, Vite, and kit scripts",
798
822
  task: async () => {
799
- checks.push(checkNode(), checkKitScripts(), checkDevViteConfig(ctx), ...checkDevEntry(ctx));
800
- const viteCheck = checkViteInstalled(ctx);
801
- if (viteCheck) checks.push(viteCheck);
802
- checks.push(...checkModuleConfigUiDeps(ctx));
823
+ checks.push(checkNode(), checkKitScripts());
824
+ const uploadOnlyNote = checkUploadOnlyNote(ctx);
825
+ if (uploadOnlyNote) {
826
+ checks.push(uploadOnlyNote);
827
+ } else {
828
+ checks.push(checkDevViteConfig(ctx), ...checkDevEntry(ctx));
829
+ const viteCheck = checkViteInstalled(ctx);
830
+ if (viteCheck) checks.push(viteCheck);
831
+ checks.push(...checkModuleConfigUiDeps(ctx));
832
+ }
803
833
  }
804
834
  },
805
835
  {
@@ -900,7 +930,7 @@ async function runConfigValidate(ctx) {
900
930
  }
901
931
 
902
932
  // src/cli/commands/build.ts
903
- import { resolve as resolve6 } from "path";
933
+ import { resolve as resolve7 } from "path";
904
934
 
905
935
  // src/lib/build-tier.ts
906
936
  var DEFAULT_BUILD_TIER = "dev";
@@ -926,7 +956,7 @@ function buildTasks(ctx, options, run) {
926
956
  title: `Building monolith module (${tier})`,
927
957
  task: async () => {
928
958
  run("run-vite-build.mjs", [
929
- resolve6(ctx.projectRoot, "vite.config.monolith.mjs"),
959
+ resolve7(ctx.projectRoot, "vite.config.monolith.mjs"),
930
960
  ...tierToStgFlag(tier)
931
961
  ]);
932
962
  run("run-hubspot-module-postbuild.mjs");
@@ -940,7 +970,7 @@ function buildTasks(ctx, options, run) {
940
970
  task: async () => {
941
971
  if (options.cdnEsm) {
942
972
  run("run-vite-build.mjs", [
943
- resolve6(ctx.projectRoot, "vite.config.cdn-esm.mjs"),
973
+ resolve7(ctx.projectRoot, "vite.config.cdn-esm.mjs"),
944
974
  ...tierToStgFlag(tier)
945
975
  ]);
946
976
  } else {
@@ -955,7 +985,7 @@ function buildTasks(ctx, options, run) {
955
985
  title: `Building HubSpot module (${tier})`,
956
986
  task: async () => {
957
987
  run("run-vite-build.mjs", [
958
- resolve6(ctx.projectRoot, "vite.config.hubspot.mjs"),
988
+ resolve7(ctx.projectRoot, "vite.config.hubspot.mjs"),
959
989
  ...tierToStgFlag(tier)
960
990
  ]);
961
991
  run("run-hubspot-module-postbuild.mjs");
@@ -1011,7 +1041,7 @@ async function runBuild(ctx, options) {
1011
1041
  command: "build",
1012
1042
  tier,
1013
1043
  ok: true,
1014
- outDir: resolve6(ctx.projectRoot, ctx.config.module.outDir)
1044
+ outDir: resolve7(ctx.projectRoot, ctx.config.module.outDir)
1015
1045
  });
1016
1046
  } else {
1017
1047
  printCommandDone(ctx, "Build complete");
@@ -1025,8 +1055,8 @@ async function runBuild(ctx, options) {
1025
1055
  }
1026
1056
 
1027
1057
  // src/cli/commands/dev.ts
1028
- import { existsSync as existsSync6 } from "fs";
1029
- import { resolve as resolve9 } from "path";
1058
+ import { existsSync as existsSync7 } from "fs";
1059
+ import { resolve as resolve10 } from "path";
1030
1060
  import { spawn } from "child_process";
1031
1061
 
1032
1062
  // src/lib/cli-banner.ts
@@ -1034,14 +1064,14 @@ import pc3 from "picocolors";
1034
1064
 
1035
1065
  // src/lib/kit-version.ts
1036
1066
  import { readFileSync as readFileSync5 } from "fs";
1037
- import { resolve as resolve7 } from "path";
1067
+ import { resolve as resolve8 } from "path";
1038
1068
  var cachedVersion = null;
1039
1069
  function readKitVersion() {
1040
1070
  if (cachedVersion) {
1041
1071
  return cachedVersion;
1042
1072
  }
1043
1073
  try {
1044
- const pkg = JSON.parse(readFileSync5(resolve7(getKitRoot(), "package.json"), "utf8"));
1074
+ const pkg = JSON.parse(readFileSync5(resolve8(getKitRoot(), "package.json"), "utf8"));
1045
1075
  cachedVersion = pkg.version ?? "0.0.0";
1046
1076
  } catch {
1047
1077
  cachedVersion = "0.0.0";
@@ -1079,7 +1109,7 @@ function formatProjectLabel(config) {
1079
1109
  return `${config.module.name} \xB7 ${config.projectType}${routerNote}`;
1080
1110
  }
1081
1111
  function section(color, title, body) {
1082
- const bold = (text2) => color ? pc3.bold(text2) : text2;
1112
+ const bold = (text3) => color ? pc3.bold(text3) : text3;
1083
1113
  return ` ${bold(title)}
1084
1114
  ${body}`;
1085
1115
  }
@@ -1088,10 +1118,10 @@ function printWoodsCliDevBanner(options) {
1088
1118
  return;
1089
1119
  }
1090
1120
  const color = useCliColor();
1091
- const cyan = (text2) => color ? pc3.cyan(text2) : text2;
1092
- const dim = (text2) => color ? pc3.dim(text2) : text2;
1093
- const bold = (text2) => color ? pc3.bold(text2) : text2;
1094
- const yellow = (text2) => color ? pc3.yellow(text2) : text2;
1121
+ const cyan = (text3) => color ? pc3.cyan(text3) : text3;
1122
+ const dim = (text3) => color ? pc3.dim(text3) : text3;
1123
+ const bold = (text3) => color ? pc3.bold(text3) : text3;
1124
+ const yellow = (text3) => color ? pc3.yellow(text3) : text3;
1095
1125
  const version = options.version ?? readKitVersion();
1096
1126
  const project = formatProjectLabel(options.config);
1097
1127
  const isTheme = isThemeProject(options.config);
@@ -1150,19 +1180,19 @@ ${tips}
1150
1180
  }
1151
1181
 
1152
1182
  // src/vite/resolve-dev-config.ts
1153
- import { existsSync as existsSync5 } from "fs";
1154
- import { resolve as resolve8 } from "path";
1183
+ import { existsSync as existsSync6 } from "fs";
1184
+ import { resolve as resolve9 } from "path";
1155
1185
  var CONSUMER_DEV_CONFIG = "vite.config.dev.mjs";
1156
1186
  function resolveDevViteConfigPath(projectRoot) {
1157
- const consumerConfig = resolve8(projectRoot, CONSUMER_DEV_CONFIG);
1158
- if (existsSync5(consumerConfig)) {
1187
+ const consumerConfig = resolve9(projectRoot, CONSUMER_DEV_CONFIG);
1188
+ if (existsSync6(consumerConfig)) {
1159
1189
  return consumerConfig;
1160
1190
  }
1161
1191
  return kitPath("vite", "vite.config.dev.mjs");
1162
1192
  }
1163
1193
  function resolveProjectViteBin(projectRoot) {
1164
- const localBin = resolve8(projectRoot, "node_modules", ".bin", "vite");
1165
- if (existsSync5(localBin)) {
1194
+ const localBin = resolve9(projectRoot, "node_modules", ".bin", "vite");
1195
+ if (existsSync6(localBin)) {
1166
1196
  return localBin;
1167
1197
  }
1168
1198
  return "vite";
@@ -1187,8 +1217,8 @@ async function runDev(ctx, options) {
1187
1217
  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)."
1188
1218
  });
1189
1219
  }
1190
- const consumerConfig = resolve9(ctx.projectRoot, "vite.config.dev.mjs");
1191
- if (!existsSync6(consumerConfig)) {
1220
+ const consumerConfig = resolve10(ctx.projectRoot, "vite.config.dev.mjs");
1221
+ if (!existsSync7(consumerConfig)) {
1192
1222
  throw new WpHsError("vite.config.dev.mjs is missing in the project root.", {
1193
1223
  exitCode: ExitCode.USER_ERROR,
1194
1224
  remediation: "Add vite.config.dev.mjs (see @woodsportal/hubspot-kit templates/module-hybrid-cdn)."
@@ -1217,7 +1247,76 @@ async function runDev(ctx, options) {
1217
1247
  }
1218
1248
 
1219
1249
  // src/cli/commands/deploy.ts
1250
+ import { resolve as resolve13 } from "path";
1251
+
1252
+ // src/lib/project-env-files.ts
1253
+ import { existsSync as existsSync8, readFileSync as readFileSync6, writeFileSync } from "fs";
1220
1254
  import { resolve as resolve11 } from "path";
1255
+ var ENV_LOCAL_FILE = ".env.local";
1256
+ var ENV_GITIGNORE_LINES = [".env", ".env.*", "!.env*.example", ENV_LOCAL_FILE];
1257
+ function parseEnvFile(content) {
1258
+ const result = {};
1259
+ for (const line of content.split("\n")) {
1260
+ const trimmed = line.trim();
1261
+ if (!trimmed || trimmed.startsWith("#")) continue;
1262
+ const eq = trimmed.indexOf("=");
1263
+ if (eq <= 0) continue;
1264
+ const key = trimmed.slice(0, eq).trim();
1265
+ let value = trimmed.slice(eq + 1).trim();
1266
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
1267
+ value = value.slice(1, -1);
1268
+ }
1269
+ result[key] = value;
1270
+ }
1271
+ return result;
1272
+ }
1273
+ function readEnvLocal(projectRoot) {
1274
+ const path = resolve11(projectRoot, ENV_LOCAL_FILE);
1275
+ if (!existsSync8(path)) return {};
1276
+ return parseEnvFile(readFileSync6(path, "utf8"));
1277
+ }
1278
+ function formatEnvLocal(entries) {
1279
+ const lines = [
1280
+ "# Local secrets \u2014 gitignored. Created by wp init / wp setup (hybrid CDN).",
1281
+ "# PORTAL_CDN_PUBLISH_TOKEN needs repo push on your portal.cdn.json mirror.",
1282
+ ""
1283
+ ];
1284
+ for (const [key, value] of Object.entries(entries)) {
1285
+ if (value.includes(" ") || value.includes("#")) {
1286
+ lines.push(`${key}="${value.replace(/"/g, '\\"')}"`);
1287
+ } else {
1288
+ lines.push(`${key}=${value}`);
1289
+ }
1290
+ }
1291
+ return `${lines.join("\n").trimEnd()}
1292
+ `;
1293
+ }
1294
+ function writeEnvLocal(projectRoot, entries) {
1295
+ const path = resolve11(projectRoot, ENV_LOCAL_FILE);
1296
+ const existing = existsSync8(path) ? parseEnvFile(readFileSync6(path, "utf8")) : {};
1297
+ writeFileSync(path, formatEnvLocal({ ...existing, ...entries }));
1298
+ }
1299
+ function mergeProjectEnv(projectRoot, base = process.env) {
1300
+ const local = readEnvLocal(projectRoot);
1301
+ const merged = { ...base };
1302
+ for (const [key, value] of Object.entries(local)) {
1303
+ if (merged[key] == null || merged[key] === "") {
1304
+ merged[key] = value;
1305
+ }
1306
+ }
1307
+ return merged;
1308
+ }
1309
+ function ensureEnvFilesGitignore(projectRoot) {
1310
+ const gitignorePath = resolve11(projectRoot, ".gitignore");
1311
+ const existing = existsSync8(gitignorePath) ? readFileSync6(gitignorePath, "utf8") : "";
1312
+ const toAppend = ENV_GITIGNORE_LINES.filter((line) => !existing.includes(line)).join("\n");
1313
+ if (!toAppend) return;
1314
+ writeFileSync(gitignorePath, `${existing.trimEnd()}
1315
+
1316
+ # Env files (secrets)
1317
+ ${toAppend}
1318
+ `);
1319
+ }
1221
1320
 
1222
1321
  // src/cli/commands/publish.ts
1223
1322
  async function runPublish(ctx, options) {
@@ -1228,7 +1327,10 @@ async function runPublish(ctx, options) {
1228
1327
  });
1229
1328
  }
1230
1329
  const tier = options.tier ?? DEFAULT_BUILD_TIER;
1231
- const env = { ...process.env, WP_HS_PROJECT_ROOT: ctx.projectRoot };
1330
+ const env = mergeProjectEnv(ctx.projectRoot, {
1331
+ ...process.env,
1332
+ WP_HS_PROJECT_ROOT: ctx.projectRoot
1333
+ });
1232
1334
  const execOpts = {
1233
1335
  cwd: ctx.projectRoot,
1234
1336
  env,
@@ -1275,14 +1377,14 @@ async function runPublish(ctx, options) {
1275
1377
  }
1276
1378
 
1277
1379
  // src/cli/commands/upload.ts
1278
- import { resolve as resolve10 } from "path";
1380
+ import { resolve as resolve12 } from "path";
1279
1381
  async function uploadModuleOrTheme(ctx) {
1280
1382
  if (isThemeProject(ctx.config)) {
1281
1383
  const theme = ctx.config.theme;
1282
1384
  if (!theme) {
1283
1385
  throw new WpHsError("theme block missing in woodscli.json", { exitCode: ExitCode.USER_ERROR });
1284
1386
  }
1285
- const srcDir = resolve10(ctx.projectRoot, theme.srcDir);
1387
+ const srcDir = resolve12(ctx.projectRoot, theme.srcDir);
1286
1388
  runCommand("hs", ["cms", "upload", srcDir, theme.uploadPath], {
1287
1389
  cwd: ctx.projectRoot,
1288
1390
  logger: ctx.logger,
@@ -1290,7 +1392,7 @@ async function uploadModuleOrTheme(ctx) {
1290
1392
  });
1291
1393
  return `Uploaded theme ${srcDir} \u2192 ${theme.uploadPath}`;
1292
1394
  }
1293
- const outDir = resolve10(ctx.projectRoot, ctx.config.module.outDir);
1395
+ const outDir = resolve12(ctx.projectRoot, ctx.config.module.outDir);
1294
1396
  const uploadPath = ctx.config.module.uploadPath;
1295
1397
  runCommand("hs", ["cms", "upload", outDir, uploadPath], {
1296
1398
  cwd: ctx.projectRoot,
@@ -1402,7 +1504,7 @@ async function runDeploy(ctx, options) {
1402
1504
  if (!ctx.global.json) {
1403
1505
  printCommandDone(
1404
1506
  ctx,
1405
- `Deploy complete (${resolve11(ctx.projectRoot, ctx.config.module.outDir)} \u2192 ${ctx.config.module.uploadPath})`
1507
+ `Deploy complete (${resolve13(ctx.projectRoot, ctx.config.module.outDir)} \u2192 ${ctx.config.module.uploadPath})`
1406
1508
  );
1407
1509
  }
1408
1510
  return ExitCode.SUCCESS;
@@ -1410,8 +1512,8 @@ async function runDeploy(ctx, options) {
1410
1512
 
1411
1513
  // src/cli/commands/audit.ts
1412
1514
  import { createHash } from "crypto";
1413
- import { cpSync, existsSync as existsSync7, mkdirSync, readFileSync as readFileSync6, readdirSync, rmSync, statSync } from "fs";
1414
- import { resolve as resolve12, join as join2 } from "path";
1515
+ import { cpSync, existsSync as existsSync9, mkdirSync, readFileSync as readFileSync7, readdirSync, rmSync, statSync } from "fs";
1516
+ import { resolve as resolve14, join as join2 } from "path";
1415
1517
  var FORBIDDEN_SHIP_TOKENS = [
1416
1518
  "ModuleConfigOverlay",
1417
1519
  "module-config-store",
@@ -1420,10 +1522,10 @@ var FORBIDDEN_SHIP_TOKENS = [
1420
1522
  "dev.module-config"
1421
1523
  ];
1422
1524
  function hashFile(path) {
1423
- return createHash("sha256").update(readFileSync6(path)).digest("hex");
1525
+ return createHash("sha256").update(readFileSync7(path)).digest("hex");
1424
1526
  }
1425
1527
  function walkFiles(dir) {
1426
- if (!existsSync7(dir)) return [];
1528
+ if (!existsSync9(dir)) return [];
1427
1529
  const entries = readdirSync(dir);
1428
1530
  const files = [];
1429
1531
  for (const entry of entries) {
@@ -1439,14 +1541,14 @@ function walkFiles(dir) {
1439
1541
  async function runAuditShipBundle(ctx) {
1440
1542
  const root = ctx.projectRoot;
1441
1543
  const targets = [
1442
- resolve12(root, "dist/cdn"),
1443
- resolve12(root, ctx.config.module.outDir)
1544
+ resolve14(root, "dist/cdn"),
1545
+ resolve14(root, ctx.config.module.outDir)
1444
1546
  ];
1445
1547
  const violations = [];
1446
1548
  for (const dir of targets) {
1447
1549
  for (const file of walkFiles(dir)) {
1448
1550
  if (!/\.(js|css|html)$/i.test(file)) continue;
1449
- const content = readFileSync6(file, "utf8");
1551
+ const content = readFileSync7(file, "utf8");
1450
1552
  for (const token of FORBIDDEN_SHIP_TOKENS) {
1451
1553
  if (content.includes(token)) {
1452
1554
  violations.push(`${file}: contains forbidden token "${token}"`);
@@ -1473,21 +1575,21 @@ async function runAuditBaselineCapture(ctx, options) {
1473
1575
  const root = ctx.projectRoot;
1474
1576
  const baseline = options.baselineDir ?? defaultBaselineDir(root, ctx.config);
1475
1577
  const sources = [
1476
- { label: "cdn", path: resolve12(root, "dist/cdn") },
1477
- { label: "hubspot", path: resolve12(root, ctx.config.module.outDir) }
1578
+ { label: "cdn", path: resolve14(root, "dist/cdn") },
1579
+ { label: "hubspot", path: resolve14(root, ctx.config.module.outDir) }
1478
1580
  ];
1479
1581
  if (ctx.global.dryRun) {
1480
1582
  ctx.logger.info(`[dry-run] Would capture baseline to ${baseline}`);
1481
1583
  return ExitCode.SUCCESS;
1482
1584
  }
1483
1585
  for (const { label, path } of sources) {
1484
- if (!existsSync7(path)) {
1586
+ if (!existsSync9(path)) {
1485
1587
  throw new WpHsError(`Cannot capture baseline \u2014 missing ${path}`, {
1486
1588
  exitCode: ExitCode.USER_ERROR,
1487
1589
  remediation: "Run wp build before wp audit baseline --capture."
1488
1590
  });
1489
1591
  }
1490
- const target = resolve12(baseline, label);
1592
+ const target = resolve14(baseline, label);
1491
1593
  rmSync(target, { recursive: true, force: true });
1492
1594
  mkdirSync(baseline, { recursive: true });
1493
1595
  cpSync(path, target, { recursive: true });
@@ -1503,13 +1605,13 @@ async function runAuditParity(ctx, options) {
1503
1605
  const root = ctx.projectRoot;
1504
1606
  const baseline = options.baselineDir ?? defaultBaselineDir(root, ctx.config);
1505
1607
  const currentDirs = [
1506
- { label: "cdn", path: resolve12(root, "dist/cdn") },
1507
- { label: "hubspot", path: resolve12(root, ctx.config.module.outDir) }
1608
+ { label: "cdn", path: resolve14(root, "dist/cdn") },
1609
+ { label: "hubspot", path: resolve14(root, ctx.config.module.outDir) }
1508
1610
  ];
1509
1611
  const mismatches = [];
1510
1612
  for (const { label, path } of currentDirs) {
1511
- const baselinePath = resolve12(baseline, label);
1512
- if (!existsSync7(baselinePath)) {
1613
+ const baselinePath = resolve14(baseline, label);
1614
+ if (!existsSync9(baselinePath)) {
1513
1615
  ctx.logger.warn(`No baseline at ${baselinePath} \u2014 skipping ${label} parity`);
1514
1616
  continue;
1515
1617
  }
@@ -1524,8 +1626,8 @@ async function runAuditParity(ctx, options) {
1524
1626
  }
1525
1627
  for (const file of baselineFiles) {
1526
1628
  const rel = file.replace(baselinePath, "");
1527
- const current = resolve12(path, rel.slice(1));
1528
- if (!existsSync7(current)) continue;
1629
+ const current = resolve14(path, rel.slice(1));
1630
+ if (!existsSync9(current)) continue;
1529
1631
  const bh = hashFile(file);
1530
1632
  const ch = hashFile(current);
1531
1633
  if (bh !== ch) {
@@ -1552,13 +1654,13 @@ var SECRET_PATTERNS = [
1552
1654
  ];
1553
1655
  async function runAuditSecrets(ctx) {
1554
1656
  const root = ctx.projectRoot;
1555
- const scanRoots = ["src", "public", ".wphs/config"].map((d) => resolve12(root, d));
1657
+ const scanRoots = ["src", "public", ".wphs/config"].map((d) => resolve14(root, d));
1556
1658
  const findings = [];
1557
1659
  for (const dir of scanRoots) {
1558
1660
  for (const file of walkFiles(dir)) {
1559
1661
  if (!/\.(ts|tsx|js|jsx|json|env|yml|yaml|html)$/i.test(file)) continue;
1560
1662
  if (file.includes("node_modules")) continue;
1561
- const content = readFileSync6(file, "utf8");
1663
+ const content = readFileSync7(file, "utf8");
1562
1664
  for (const pattern of SECRET_PATTERNS) {
1563
1665
  pattern.lastIndex = 0;
1564
1666
  if (pattern.test(content)) {
@@ -1584,13 +1686,13 @@ async function runAuditSecrets(ctx) {
1584
1686
  }
1585
1687
 
1586
1688
  // src/cli/commands/init.ts
1587
- import { cpSync as cpSync2, existsSync as existsSync10, mkdirSync as mkdirSync3, readdirSync as readdirSync2, readFileSync as readFileSync8, writeFileSync as writeFileSync3 } from "fs";
1588
- import { basename as basename2, resolve as resolve15 } from "path";
1589
- import * as p2 from "@clack/prompts";
1689
+ import { cpSync as cpSync2, existsSync as existsSync13, mkdirSync as mkdirSync3, readdirSync as readdirSync2, readFileSync as readFileSync10, writeFileSync as writeFileSync5 } from "fs";
1690
+ import { basename as basename2, resolve as resolve18 } from "path";
1691
+ import * as p3 from "@clack/prompts";
1590
1692
 
1591
1693
  // src/lib/init-router-scaffold.ts
1592
- import { existsSync as existsSync8, mkdirSync as mkdirSync2, readFileSync as readFileSync7, rmSync as rmSync2, writeFileSync } from "fs";
1593
- import { resolve as resolve13 } from "path";
1694
+ import { existsSync as existsSync10, mkdirSync as mkdirSync2, readFileSync as readFileSync8, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
1695
+ import { resolve as resolve15 } from "path";
1594
1696
  var ROUTER_FILES = [
1595
1697
  "src/routes",
1596
1698
  "src/routeTree.gen.ts",
@@ -1604,10 +1706,10 @@ function isModuleTemplate(template) {
1604
1706
  return template === "module-hybrid-cdn" || template === "module-monolith";
1605
1707
  }
1606
1708
  function writeRouterConfig(targetDir, mode) {
1607
- const configDir = resolve13(targetDir, "src/config");
1709
+ const configDir = resolve15(targetDir, "src/config");
1608
1710
  mkdirSync2(configDir, { recursive: true });
1609
- writeFileSync(
1610
- resolve13(configDir, "router.ts"),
1711
+ writeFileSync2(
1712
+ resolve15(configDir, "router.ts"),
1611
1713
  `/** Router history \u2014 set by \`wp init\`. Hash matches woodsportal-client-frontend (HubSpot CMS embeds). */
1612
1714
  export type RouterHistoryMode = 'hash' | 'browser'
1613
1715
 
@@ -1616,8 +1718,8 @@ export const ROUTER_HISTORY: RouterHistoryMode = '${mode}'
1616
1718
  );
1617
1719
  }
1618
1720
  function writeMountDevPreviewRouter(targetDir) {
1619
- writeFileSync(
1620
- resolve13(targetDir, "src/portal/mount-dev-preview.tsx"),
1721
+ writeFileSync2(
1722
+ resolve15(targetDir, "src/portal/mount-dev-preview.tsx"),
1621
1723
  `import { mountGreenfieldDev } from '../../.wphs/.kit-bootstrap-greenfield.tsx'
1622
1724
 
1623
1725
  import { App } from '@/app'
@@ -1630,8 +1732,8 @@ export async function mountDevPreview() {
1630
1732
  );
1631
1733
  }
1632
1734
  function writeMountDevPreviewSinglePage(targetDir) {
1633
- writeFileSync(
1634
- resolve13(targetDir, "src/portal/mount-dev-preview.tsx"),
1735
+ writeFileSync2(
1736
+ resolve15(targetDir, "src/portal/mount-dev-preview.tsx"),
1635
1737
  `import { mountGreenfieldDev } from '../../.wphs/.kit-bootstrap-greenfield.tsx'
1636
1738
 
1637
1739
  import { StarterSinglePage } from '@/features/starter'
@@ -1644,8 +1746,8 @@ export async function mountDevPreview() {
1644
1746
  );
1645
1747
  }
1646
1748
  function writeMountPortalRouter(targetDir) {
1647
- writeFileSync(
1648
- resolve13(targetDir, "src/portal/mount-portal.tsx"),
1749
+ writeFileSync2(
1750
+ resolve15(targetDir, "src/portal/mount-portal.tsx"),
1649
1751
  `import ReactDOM from 'react-dom/client'
1650
1752
 
1651
1753
  import { App } from '@/app'
@@ -1662,8 +1764,8 @@ export async function mountPortal() {
1662
1764
  );
1663
1765
  }
1664
1766
  function writeMountPortalSinglePage(targetDir) {
1665
- writeFileSync(
1666
- resolve13(targetDir, "src/portal/mount-portal.tsx"),
1767
+ writeFileSync2(
1768
+ resolve15(targetDir, "src/portal/mount-portal.tsx"),
1667
1769
  `import ReactDOM from 'react-dom/client'
1668
1770
 
1669
1771
  import { StarterSinglePage } from '@/features/starter'
@@ -1680,11 +1782,11 @@ export async function mountPortal() {
1680
1782
  );
1681
1783
  }
1682
1784
  function writeStarterSinglePage(targetDir) {
1683
- const file = resolve13(targetDir, "src/features/starter/components/starter-single-page.tsx");
1684
- if (existsSync8(file)) {
1785
+ const file = resolve15(targetDir, "src/features/starter/components/starter-single-page.tsx");
1786
+ if (existsSync10(file)) {
1685
1787
  return;
1686
1788
  }
1687
- writeFileSync(
1789
+ writeFileSync2(
1688
1790
  file,
1689
1791
  `import { StarterShell } from '@/components/layout/starter-shell'
1690
1792
 
@@ -1707,25 +1809,25 @@ export function StarterSinglePage() {
1707
1809
  );
1708
1810
  }
1709
1811
  function removePath(targetDir, relativePath) {
1710
- const full = resolve13(targetDir, relativePath);
1711
- if (!existsSync8(full)) {
1812
+ const full = resolve15(targetDir, relativePath);
1813
+ if (!existsSync10(full)) {
1712
1814
  return;
1713
1815
  }
1714
1816
  rmSync2(full, { recursive: true, force: true });
1715
1817
  }
1716
1818
  function stripTanStackFromPackage(targetDir) {
1717
- const pkgPath = resolve13(targetDir, "package.json");
1718
- if (!existsSync8(pkgPath)) {
1819
+ const pkgPath = resolve15(targetDir, "package.json");
1820
+ if (!existsSync10(pkgPath)) {
1719
1821
  return;
1720
1822
  }
1721
- const pkg = JSON.parse(readFileSync7(pkgPath, "utf8"));
1823
+ const pkg = JSON.parse(readFileSync8(pkgPath, "utf8"));
1722
1824
  if (!pkg.devDependencies) {
1723
1825
  return;
1724
1826
  }
1725
1827
  for (const dep of TANSTACK_DEPS) {
1726
1828
  delete pkg.devDependencies[dep];
1727
1829
  }
1728
- writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}
1830
+ writeFileSync2(pkgPath, `${JSON.stringify(pkg, null, 2)}
1729
1831
  `);
1730
1832
  }
1731
1833
  function applyRouterScaffold(targetDir, mode, template) {
@@ -1745,22 +1847,141 @@ function applyRouterScaffold(targetDir, mode, template) {
1745
1847
  writeRouterConfig(targetDir, mode);
1746
1848
  writeMountDevPreviewRouter(targetDir);
1747
1849
  writeMountPortalRouter(targetDir);
1748
- const featuresIndex = resolve13(targetDir, "src/features/starter/index.ts");
1749
- if (existsSync8(featuresIndex)) {
1750
- let content = readFileSync7(featuresIndex, "utf8");
1850
+ const featuresIndex = resolve15(targetDir, "src/features/starter/index.ts");
1851
+ if (existsSync10(featuresIndex)) {
1852
+ let content = readFileSync8(featuresIndex, "utf8");
1751
1853
  if (!content.includes("StarterSinglePage")) {
1752
1854
  content = content.replace(
1753
1855
  "export type { StarterPreviewValues } from './hooks/use-starter-preview'\n",
1754
1856
  "export type { StarterPreviewValues } from './hooks/use-starter-preview'\nexport { StarterSinglePage } from './components/starter-single-page'\n"
1755
1857
  );
1756
- writeFileSync(featuresIndex, content);
1858
+ writeFileSync2(featuresIndex, content);
1757
1859
  }
1758
1860
  }
1759
1861
  }
1760
1862
 
1863
+ // src/lib/cdn-github-setup.ts
1864
+ import { existsSync as existsSync11, readFileSync as readFileSync9, writeFileSync as writeFileSync3 } from "fs";
1865
+ import { resolve as resolve16 } from "path";
1866
+ import * as p2 from "@clack/prompts";
1867
+ var PORTAL_CDN_PUBLISH_TOKEN_ENV = "PORTAL_CDN_PUBLISH_TOKEN";
1868
+ var PORTAL_CDN_JSON = "portal.cdn.json";
1869
+ var PLACEHOLDER_GITHUB = /your-org|your-portal-cdn-mirror/i;
1870
+ function defaultPortalCdnJson() {
1871
+ const examplePath = kitPath("templates", "shared", "portal.cdn.json.example");
1872
+ if (existsSync11(examplePath)) {
1873
+ return JSON.parse(readFileSync9(examplePath, "utf8"));
1874
+ }
1875
+ return {
1876
+ publishMode: "jsdelivr",
1877
+ github: "your-org/your-portal-cdn-mirror",
1878
+ gitRemote: "https://github.com/your-org/your-portal-cdn-mirror.git",
1879
+ cdnPathInRepo: "dist/cdn",
1880
+ defaultBranch: "main"
1881
+ };
1882
+ }
1883
+ function readPortalCdnJson(projectRoot) {
1884
+ const path = resolve16(projectRoot, PORTAL_CDN_JSON);
1885
+ if (!existsSync11(path)) return null;
1886
+ try {
1887
+ return JSON.parse(readFileSync9(path, "utf8"));
1888
+ } catch {
1889
+ return null;
1890
+ }
1891
+ }
1892
+ function normalizeGithubSlug(input) {
1893
+ const trimmed = input.trim().replace(/^https:\/\/github\.com\//, "").replace(/\.git$/, "");
1894
+ const parts = trimmed.split("/").filter(Boolean);
1895
+ if (parts.length !== 2) return null;
1896
+ return `${parts[0]}/${parts[1]}`;
1897
+ }
1898
+ function portalCdnJsonFromGithub(github, base) {
1899
+ const slug = normalizeGithubSlug(github);
1900
+ if (!slug) {
1901
+ throw new Error(`Invalid GitHub repo \u2014 use org/repo (got "${github}")`);
1902
+ }
1903
+ return {
1904
+ publishMode: base?.publishMode ?? "jsdelivr",
1905
+ github: slug,
1906
+ gitRemote: `https://github.com/${slug}.git`,
1907
+ cdnPathInRepo: base?.cdnPathInRepo ?? "dist/cdn",
1908
+ defaultBranch: base?.defaultBranch ?? "main"
1909
+ };
1910
+ }
1911
+ function hasCdnPublishToken(projectRoot) {
1912
+ if (process.env[PORTAL_CDN_PUBLISH_TOKEN_ENV]?.trim()) return true;
1913
+ if (process.env.GITHUB_TOKEN?.trim()) return true;
1914
+ return Boolean(readEnvLocal(projectRoot)[PORTAL_CDN_PUBLISH_TOKEN_ENV]?.trim());
1915
+ }
1916
+ function hasConfiguredCdnGithubMirror(projectRoot) {
1917
+ const config = readPortalCdnJson(projectRoot);
1918
+ const github = config?.github ?? "";
1919
+ return Boolean(github) && !PLACEHOLDER_GITHUB.test(github);
1920
+ }
1921
+ function githubDefaultForPrompt(projectRoot) {
1922
+ const existing = readPortalCdnJson(projectRoot);
1923
+ if (existing?.github && !PLACEHOLDER_GITHUB.test(existing.github)) {
1924
+ return existing.github;
1925
+ }
1926
+ return defaultPortalCdnJson().github;
1927
+ }
1928
+ function needsCdnGitHubSetup(projectRoot) {
1929
+ return !hasConfiguredCdnGithubMirror(projectRoot) || !hasCdnPublishToken(projectRoot);
1930
+ }
1931
+ function applyCdnGitHubSetup(projectRoot, input) {
1932
+ const existing = readPortalCdnJson(projectRoot) ?? defaultPortalCdnJson();
1933
+ const updated = portalCdnJsonFromGithub(input.github, existing);
1934
+ writeFileSync3(resolve16(projectRoot, PORTAL_CDN_JSON), `${JSON.stringify(updated, null, 2)}
1935
+ `);
1936
+ if (input.token?.trim()) {
1937
+ writeEnvLocal(projectRoot, { [PORTAL_CDN_PUBLISH_TOKEN_ENV]: input.token.trim() });
1938
+ }
1939
+ ensureEnvFilesGitignore(projectRoot);
1940
+ }
1941
+ async function promptCdnGitHubSetup(options = {}) {
1942
+ if (options.skip) return null;
1943
+ const projectRoot = options.projectRoot ?? process.cwd();
1944
+ if (!needsCdnGitHubSetup(projectRoot)) {
1945
+ return null;
1946
+ }
1947
+ let github = options.github?.trim();
1948
+ if (!github && hasConfiguredCdnGithubMirror(projectRoot)) {
1949
+ github = readPortalCdnJson(projectRoot)?.github;
1950
+ }
1951
+ if (!github) {
1952
+ const answer = await p2.text({
1953
+ message: "Public GitHub CDN mirror (org/repo)",
1954
+ placeholder: options.githubDefault ?? "your-org/your-portal-cdn-mirror",
1955
+ initialValue: options.githubDefault,
1956
+ validate: (value) => {
1957
+ const slug = normalizeGithubSlug(value ?? "");
1958
+ return slug ? void 0 : "Enter a public repo as org/repo (jsDelivr serves from it)";
1959
+ }
1960
+ });
1961
+ if (p2.isCancel(answer)) return null;
1962
+ github = normalizeGithubSlug(answer) ?? void 0;
1963
+ }
1964
+ if (!github) return null;
1965
+ if (hasCdnPublishToken(projectRoot)) {
1966
+ return { github };
1967
+ }
1968
+ const tokenAnswer = await p2.password({
1969
+ message: "GitHub token (repo push on CDN mirror)",
1970
+ validate: (value) => {
1971
+ if (!value?.trim()) return "Required for wp publish \u2014 create a fine-grained PAT with Contents: Read and write";
1972
+ return void 0;
1973
+ }
1974
+ });
1975
+ if (p2.isCancel(tokenAnswer)) return null;
1976
+ return { github, token: tokenAnswer.trim() };
1977
+ }
1978
+ function formatCdnSetupNote(_projectRoot) {
1979
+ return `CDN config written \u2014 edit ${PORTAL_CDN_JSON} if needed; token saved to ${ENV_LOCAL_FILE} (gitignored)`;
1980
+ }
1981
+
1761
1982
  // src/lib/project-setup.ts
1762
- import { copyFileSync, existsSync as existsSync9, writeFileSync as writeFileSync2 } from "fs";
1763
- import { basename, resolve as resolve14 } from "path";
1983
+ import { copyFileSync, existsSync as existsSync12, writeFileSync as writeFileSync4 } from "fs";
1984
+ import { basename, resolve as resolve17 } from "path";
1764
1985
  function envFileForTier(tier) {
1765
1986
  return tier === "dev" ? ".env.dev" : tier === "stg" ? ".env.stg" : ".env.prod";
1766
1987
  }
@@ -1770,13 +1991,13 @@ function tiersToSetup(config, tier) {
1770
1991
  }
1771
1992
  function firstExisting(paths) {
1772
1993
  for (const path of paths) {
1773
- if (existsSync9(path)) return path;
1994
+ if (existsSync12(path)) return path;
1774
1995
  }
1775
1996
  return null;
1776
1997
  }
1777
1998
  function copyMissingFile(projectRoot, targetName, sourceCandidates, dryRun) {
1778
- const target = resolve14(projectRoot, targetName);
1779
- if (existsSync9(target)) {
1999
+ const target = resolve17(projectRoot, targetName);
2000
+ if (existsSync12(target)) {
1780
2001
  return { action: "skipped", path: targetName, detail: "already present" };
1781
2002
  }
1782
2003
  const source = firstExisting(sourceCandidates);
@@ -1798,12 +2019,12 @@ function copyMissingFile(projectRoot, targetName, sourceCandidates, dryRun) {
1798
2019
  };
1799
2020
  }
1800
2021
  function writeMissingFile(projectRoot, targetName, content, dryRun) {
1801
- const target = resolve14(projectRoot, targetName);
1802
- if (existsSync9(target)) {
2022
+ const target = resolve17(projectRoot, targetName);
2023
+ if (existsSync12(target)) {
1803
2024
  return { action: "skipped", path: targetName, detail: "already present" };
1804
2025
  }
1805
2026
  if (!dryRun) {
1806
- writeFileSync2(target, content);
2027
+ writeFileSync4(target, content);
1807
2028
  }
1808
2029
  return {
1809
2030
  action: "copied",
@@ -1829,8 +2050,8 @@ function applyProjectSetup(ctx, options = {}) {
1829
2050
  const templateDir = kitTemplateDir(config);
1830
2051
  results.push(
1831
2052
  copyMissingFile(projectRoot, HUBSPOT_CONFIG_FILE, [
1832
- resolve14(projectRoot, HUBSPOT_CONFIG_EXAMPLE),
1833
- resolve14(templateDir, HUBSPOT_CONFIG_EXAMPLE),
2053
+ resolve17(projectRoot, HUBSPOT_CONFIG_EXAMPLE),
2054
+ resolve17(templateDir, HUBSPOT_CONFIG_EXAMPLE),
1834
2055
  kitPath("templates", HUBSPOT_CONFIG_EXAMPLE)
1835
2056
  ], dryRun)
1836
2057
  );
@@ -1839,8 +2060,8 @@ function applyProjectSetup(ctx, options = {}) {
1839
2060
  const envExample = `${envFile}.example`;
1840
2061
  results.push(
1841
2062
  copyMissingFile(projectRoot, envFile, [
1842
- resolve14(projectRoot, envExample),
1843
- resolve14(sharedDir, envExample)
2063
+ resolve17(projectRoot, envExample),
2064
+ resolve17(sharedDir, envExample)
1844
2065
  ], dryRun)
1845
2066
  );
1846
2067
  }
@@ -1848,8 +2069,8 @@ function applyProjectSetup(ctx, options = {}) {
1848
2069
  const mirror = config.cdn.mirror;
1849
2070
  results.push(
1850
2071
  copyMissingFile(projectRoot, mirror, [
1851
- resolve14(projectRoot, `${mirror}.example`),
1852
- resolve14(sharedDir, "portal.cdn.json.example")
2072
+ resolve17(projectRoot, `${mirror}.example`),
2073
+ resolve17(sharedDir, "portal.cdn.json.example")
1853
2074
  ], dryRun)
1854
2075
  );
1855
2076
  const buildIdFile = config.cdn.buildIdFile;
@@ -1859,15 +2080,15 @@ function applyProjectSetup(ctx, options = {}) {
1859
2080
  }
1860
2081
  for (const file of templateViteFiles(config)) {
1861
2082
  results.push(
1862
- copyMissingFile(projectRoot, file, [resolve14(templateDir, file)], dryRun)
2083
+ copyMissingFile(projectRoot, file, [resolve17(templateDir, file)], dryRun)
1863
2084
  );
1864
2085
  }
1865
2086
  if (isModuleProject(config) && !isThemeProject(config)) {
1866
2087
  const adapter = config.dev.adapter;
1867
- const adapterPath = resolve14(projectRoot, adapter);
1868
- if (!existsSync9(adapterPath)) {
1869
- const templateAdapter = resolve14(templateDir, "wphs.adapter.ts");
1870
- if (existsSync9(templateAdapter)) {
2088
+ const adapterPath = resolve17(projectRoot, adapter);
2089
+ if (!existsSync12(adapterPath)) {
2090
+ const templateAdapter = resolve17(templateDir, "wphs.adapter.ts");
2091
+ if (existsSync12(templateAdapter)) {
1871
2092
  results.push(copyMissingFile(projectRoot, adapter, [templateAdapter], dryRun));
1872
2093
  } else {
1873
2094
  results.push({
@@ -1884,26 +2105,26 @@ function applyProjectSetup(ctx, options = {}) {
1884
2105
  }
1885
2106
  function copySharedSetupExamples(targetDir) {
1886
2107
  const sharedDir = kitPath("templates", "shared");
1887
- if (!existsSync9(sharedDir)) return;
2108
+ if (!existsSync12(sharedDir)) return;
1888
2109
  for (const name of [
1889
2110
  ".env.dev.example",
1890
2111
  ".env.stg.example",
1891
2112
  ".env.prod.example",
1892
2113
  "portal.cdn.json.example"
1893
2114
  ]) {
1894
- const src = resolve14(sharedDir, name);
1895
- const dest = resolve14(targetDir, name);
1896
- if (existsSync9(src) && !existsSync9(dest)) {
2115
+ const src = resolve17(sharedDir, name);
2116
+ const dest = resolve17(targetDir, name);
2117
+ if (existsSync12(src) && !existsSync12(dest)) {
1897
2118
  copyFileSync(src, dest);
1898
2119
  }
1899
2120
  }
1900
2121
  const hubspotExample = firstExisting([
1901
- resolve14(sharedDir, HUBSPOT_CONFIG_EXAMPLE),
2122
+ resolve17(sharedDir, HUBSPOT_CONFIG_EXAMPLE),
1902
2123
  kitPath("templates", HUBSPOT_CONFIG_EXAMPLE)
1903
2124
  ]);
1904
2125
  if (!hubspotExample) return;
1905
- const destHubspot = resolve14(targetDir, HUBSPOT_CONFIG_EXAMPLE);
1906
- if (!existsSync9(destHubspot) && !existsSync9(resolve14(targetDir, HUBSPOT_CONFIG_FILE))) {
2126
+ const destHubspot = resolve17(targetDir, HUBSPOT_CONFIG_EXAMPLE);
2127
+ if (!existsSync12(destHubspot) && !existsSync12(resolve17(targetDir, HUBSPOT_CONFIG_FILE))) {
1907
2128
  copyFileSync(hubspotExample, destHubspot);
1908
2129
  }
1909
2130
  }
@@ -1916,28 +2137,14 @@ function formatSetupHint(results) {
1916
2137
  if (envCopied) {
1917
2138
  return "Edit .env.* files \u2014 set VITE_PUBLIC_REST_API_ENDPOINT and portal IDs for your environment";
1918
2139
  }
1919
- return void 0;
1920
- }
1921
-
1922
- // src/lib/project-name.ts
1923
- var PROJECT_NAME_PATTERN = /^[a-z][a-z0-9-]*$/;
1924
- function normalizeProjectName(input) {
1925
- return input.trim().toLowerCase().replace(/[\s_]+/g, "-").replace(/[^a-z0-9-]/g, "").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
1926
- }
1927
- function validateProjectName(name) {
1928
- if (!name) return "Project name is required";
1929
- if (!PROJECT_NAME_PATTERN.test(name)) {
1930
- return "Use lowercase letters, numbers, and hyphens (e.g. my-portal-module)";
2140
+ const cdnMirror = results.find((r) => r.path === "portal.cdn.json" && r.action === "copied");
2141
+ if (cdnMirror) {
2142
+ return "Hybrid CDN: set public GitHub mirror in portal.cdn.json; token in .env.local (wp init prompts)";
1931
2143
  }
1932
- if (name.length > 64) return "Project name is too long (max 64 characters)";
1933
2144
  return void 0;
1934
2145
  }
1935
- function moduleOutDirFromSlug(slug) {
1936
- const pascal = slug.split("-").filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
1937
- return `dist/${pascal || "HubSpot"}.Module`;
1938
- }
1939
2146
 
1940
- // src/cli/commands/init.ts
2147
+ // src/lib/template-scripts.ts
1941
2148
  var TEMPLATE_SCRIPTS = {
1942
2149
  "module-hybrid-cdn": {
1943
2150
  dev: "wp dev",
@@ -1964,6 +2171,26 @@ var TEMPLATE_SCRIPTS = {
1964
2171
  doctor: "wp doctor"
1965
2172
  }
1966
2173
  };
2174
+
2175
+ // src/lib/project-name.ts
2176
+ var PROJECT_NAME_PATTERN = /^[a-z][a-z0-9-]*$/;
2177
+ function normalizeProjectName(input) {
2178
+ return input.trim().toLowerCase().replace(/[\s_]+/g, "-").replace(/[^a-z0-9-]/g, "").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
2179
+ }
2180
+ function validateProjectName(name) {
2181
+ if (!name) return "Project name is required";
2182
+ if (!PROJECT_NAME_PATTERN.test(name)) {
2183
+ return "Use lowercase letters, numbers, and hyphens (e.g. my-portal-module)";
2184
+ }
2185
+ if (name.length > 64) return "Project name is too long (max 64 characters)";
2186
+ return void 0;
2187
+ }
2188
+ function moduleOutDirFromSlug(slug) {
2189
+ const pascal = slug.split("-").filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
2190
+ return `dist/${pascal || "HubSpot"}.Module`;
2191
+ }
2192
+
2193
+ // src/cli/commands/init.ts
1967
2194
  function buildConfigForProject(template, projectName, router) {
1968
2195
  const base = { ...DEFAULT_WOODSPORTAL_HS, projectType: template };
1969
2196
  if (template === "theme-starter") {
@@ -1995,7 +2222,7 @@ function buildConfigForProject(template, projectName, router) {
1995
2222
  return config;
1996
2223
  }
1997
2224
  function assertTargetDirIsScaffoldable(targetDir) {
1998
- if (!existsSync10(targetDir)) return;
2225
+ if (!existsSync13(targetDir)) return;
1999
2226
  const entries = readdirSync2(targetDir).filter((entry) => entry !== ".DS_Store");
2000
2227
  if (entries.length > 0) {
2001
2228
  throw new WpHsError(`Target directory is not empty: ${targetDir}`, {
@@ -2006,7 +2233,7 @@ function assertTargetDirIsScaffoldable(targetDir) {
2006
2233
  }
2007
2234
  function resolveInitTargetDir(options, interactiveName) {
2008
2235
  if (options.directory) {
2009
- return resolve15(options.directory);
2236
+ return resolve18(options.directory);
2010
2237
  }
2011
2238
  const parentDir = process.cwd();
2012
2239
  const slug = options.name ?? interactiveName;
@@ -2020,7 +2247,7 @@ function resolveInitTargetDir(options, interactiveName) {
2020
2247
  if (error) {
2021
2248
  throw new WpHsError(error, { exitCode: ExitCode.USER_ERROR });
2022
2249
  }
2023
- return resolve15(parentDir, slug);
2250
+ return resolve18(parentDir, slug);
2024
2251
  }
2025
2252
  async function runInit(ctx, options) {
2026
2253
  const nonInteractive = ctx.global.ci || ctx.global.yes || ctx.global.json;
@@ -2029,7 +2256,7 @@ async function runInit(ctx, options) {
2029
2256
  let router = options.router;
2030
2257
  if (!nonInteractive) {
2031
2258
  if (!template) {
2032
- const type = await p2.select({
2259
+ const type = await p3.select({
2033
2260
  message: "Project type",
2034
2261
  options: [
2035
2262
  {
@@ -2041,22 +2268,22 @@ async function runInit(ctx, options) {
2041
2268
  { value: "theme-starter", label: "Theme starter", hint: "CMS theme templates" }
2042
2269
  ]
2043
2270
  });
2044
- if (p2.isCancel(type)) throw new WpHsError("Init cancelled", { exitCode: ExitCode.USER_ERROR });
2271
+ if (p3.isCancel(type)) throw new WpHsError("Init cancelled", { exitCode: ExitCode.USER_ERROR });
2045
2272
  template = type;
2046
2273
  }
2047
2274
  if (!options.directory && !projectName) {
2048
- const nameAnswer = await p2.text({
2275
+ const nameAnswer = await p3.text({
2049
2276
  message: "Project name (folder name)",
2050
2277
  placeholder: "my-portal-module",
2051
2278
  validate: (value) => validateProjectName(normalizeProjectName(value ?? ""))
2052
2279
  });
2053
- if (p2.isCancel(nameAnswer)) {
2280
+ if (p3.isCancel(nameAnswer)) {
2054
2281
  throw new WpHsError("Init cancelled", { exitCode: ExitCode.USER_ERROR });
2055
2282
  }
2056
2283
  projectName = normalizeProjectName(nameAnswer);
2057
2284
  }
2058
2285
  if (!router && template && isModuleTemplate(template)) {
2059
- const routerAnswer = await p2.select({
2286
+ const routerAnswer = await p3.select({
2060
2287
  message: "Client-side routing",
2061
2288
  initialValue: "hash",
2062
2289
  options: [
@@ -2077,7 +2304,7 @@ async function runInit(ctx, options) {
2077
2304
  }
2078
2305
  ]
2079
2306
  });
2080
- if (p2.isCancel(routerAnswer)) {
2307
+ if (p3.isCancel(routerAnswer)) {
2081
2308
  throw new WpHsError("Init cancelled", { exitCode: ExitCode.USER_ERROR });
2082
2309
  }
2083
2310
  router = routerAnswer;
@@ -2093,7 +2320,7 @@ async function runInit(ctx, options) {
2093
2320
  const slug = projectName ?? basename2(targetDir);
2094
2321
  assertTargetDirIsScaffoldable(targetDir);
2095
2322
  const templateDir = kitPath("templates", template);
2096
- if (!existsSync10(templateDir)) {
2323
+ if (!existsSync13(templateDir)) {
2097
2324
  throw new WpHsError(`Template not found: ${template}`, { exitCode: ExitCode.USER_ERROR });
2098
2325
  }
2099
2326
  if (ctx.global.dryRun) {
@@ -2104,18 +2331,18 @@ async function runInit(ctx, options) {
2104
2331
  cpSync2(templateDir, targetDir, { recursive: true });
2105
2332
  copySharedSetupExamples(targetDir);
2106
2333
  const config = buildConfigForProject(template, slug, router);
2107
- writeFileSync3(resolve15(targetDir, WOODSCLI_CONFIG_FILE), `${JSON.stringify(config, null, 2)}
2334
+ writeFileSync5(resolve18(targetDir, WOODSCLI_CONFIG_FILE), `${JSON.stringify(config, null, 2)}
2108
2335
  `);
2109
- const pkgPath = resolve15(targetDir, "package.json");
2110
- if (existsSync10(pkgPath)) {
2111
- const pkg = JSON.parse(readFileSync8(pkgPath, "utf8"));
2336
+ const pkgPath = resolve18(targetDir, "package.json");
2337
+ if (existsSync13(pkgPath)) {
2338
+ const pkg = JSON.parse(readFileSync10(pkgPath, "utf8"));
2112
2339
  pkg.name = slug;
2113
2340
  pkg.scripts = { ...pkg.scripts, ...TEMPLATE_SCRIPTS[template] };
2114
2341
  pkg.devDependencies = {
2115
2342
  ...pkg.devDependencies,
2116
2343
  ...templateDevDependencies()
2117
2344
  };
2118
- writeFileSync3(pkgPath, `${JSON.stringify(pkg, null, 2)}
2345
+ writeFileSync5(pkgPath, `${JSON.stringify(pkg, null, 2)}
2119
2346
  `);
2120
2347
  }
2121
2348
  if (router && isModuleTemplate(template)) {
@@ -2126,7 +2353,7 @@ async function runInit(ctx, options) {
2126
2353
  logger: ctx.logger,
2127
2354
  dryRun: false
2128
2355
  });
2129
- const gitignorePath = resolve15(targetDir, ".gitignore");
2356
+ const gitignorePath = resolve18(targetDir, ".gitignore");
2130
2357
  const gitignoreExtras = [
2131
2358
  "hubspot.config.yml",
2132
2359
  ".wphs/config/module-config.local.json",
@@ -2139,149 +2366,81 @@ async function runInit(ctx, options) {
2139
2366
  ".wphs/.kit-bootstrap.tsx",
2140
2367
  ".wphs/.kit-bootstrap-greenfield.tsx"
2141
2368
  ];
2142
- const existing = existsSync10(gitignorePath) ? readFileSync8(gitignorePath, "utf8") : "";
2369
+ const existing = existsSync13(gitignorePath) ? readFileSync10(gitignorePath, "utf8") : "";
2143
2370
  const toAppend = gitignoreExtras.filter((line) => !existing.includes(line)).join("\n");
2144
2371
  if (toAppend) {
2145
- writeFileSync3(gitignorePath, `${existing.trimEnd()}
2372
+ writeFileSync5(gitignorePath, `${existing.trimEnd()}
2146
2373
  ${toAppend}
2147
2374
  `);
2148
2375
  }
2376
+ if (template === "module-hybrid-cdn" && !ctx.global.dryRun && needsCdnGitHubSetup(targetDir)) {
2377
+ const githubDefault = githubDefaultForPrompt(targetDir);
2378
+ let cdnSetup = null;
2379
+ if (nonInteractive) {
2380
+ if (options.cdnGithub) {
2381
+ cdnSetup = { github: options.cdnGithub };
2382
+ }
2383
+ } else {
2384
+ p3.log.step("Hybrid CDN \u2014 GitHub mirror (public repo for jsDelivr)");
2385
+ cdnSetup = await promptCdnGitHubSetup({
2386
+ projectRoot: targetDir,
2387
+ githubDefault,
2388
+ github: options.cdnGithub
2389
+ });
2390
+ }
2391
+ if (cdnSetup) {
2392
+ applyCdnGitHubSetup(targetDir, cdnSetup);
2393
+ if (!nonInteractive) {
2394
+ p3.log.success(formatCdnSetupNote(targetDir));
2395
+ } else {
2396
+ ctx.logger.success(formatCdnSetupNote(targetDir));
2397
+ }
2398
+ }
2399
+ }
2149
2400
  if (!nonInteractive) {
2150
- p2.log.success(`Scaffolded ${template} in ${targetDir}`);
2151
- p2.note(`cd ${slug} && yarn install && wp setup && wp doctor && wp dev`, "Getting started");
2401
+ p3.log.success(`Scaffolded ${template} in ${targetDir}`);
2402
+ p3.note(`cd ${slug} && yarn install && wp setup && wp doctor && wp dev`, "Getting started");
2152
2403
  } else {
2153
2404
  ctx.logger.success(`Scaffolded ${template} in ${targetDir}`);
2154
2405
  }
2155
2406
  return ExitCode.SUCCESS;
2156
2407
  }
2157
2408
 
2158
- // src/cli/commands/adopt.ts
2159
- import { existsSync as existsSync11, readFileSync as readFileSync9, writeFileSync as writeFileSync4 } from "fs";
2160
- import { resolve as resolve16 } from "path";
2161
- import { pathToFileURL } from "url";
2162
- import * as p3 from "@clack/prompts";
2163
- async function loadDetectLayout() {
2164
- const mod = await import(pathToFileURL(kitPath("scripts", "detect-layout.mjs")).href);
2165
- return mod.detectProjectLayout;
2166
- }
2167
- async function runAdopt(ctx, _options = {}) {
2168
- const detectProjectLayout = await loadDetectLayout();
2169
- const layout = detectProjectLayout(ctx.projectRoot);
2170
- const configPath = resolve16(ctx.projectRoot, WOODSCLI_CONFIG_FILE);
2171
- if (layout.track === "unknown") {
2172
- throw new WpHsError("Could not detect module or theme layout.", {
2173
- exitCode: ExitCode.USER_ERROR,
2174
- remediation: "Ensure fields.json + module.html (module) or theme.json + templates/ (theme) exist."
2175
- });
2176
- }
2177
- let projectType;
2178
- if (layout.projectType) {
2179
- projectType = layout.projectType;
2180
- } else if (layout.track === "theme") {
2181
- projectType = "theme-starter";
2182
- } else if (existsSync11(resolve16(ctx.projectRoot, "vite.config.monolith.mjs"))) {
2183
- projectType = "module-monolith";
2184
- } else {
2185
- projectType = "module-hybrid-cdn";
2186
- }
2187
- const config = {
2188
- ...DEFAULT_WOODSPORTAL_HS,
2189
- projectType,
2190
- ...layout.track === "theme" ? {
2191
- theme: {
2192
- name: "my-theme",
2193
- uploadPath: "themes/my-theme",
2194
- srcDir: "src/theme",
2195
- themeJson: "src/theme/theme.json"
2196
- }
2197
- } : {}
2198
- };
2199
- if (ctx.global.dryRun) {
2200
- ctx.logger.info(`[dry-run] Would adopt as ${projectType} (${layout.track})`);
2201
- return ExitCode.SUCCESS;
2202
- }
2203
- if (!existsSync11(configPath)) {
2204
- writeFileSync4(configPath, `${JSON.stringify(config, null, 2)}
2205
- `);
2206
- ctx.logger.success(`Created ${WOODSCLI_CONFIG_FILE}`);
2207
- } else if (!layout.projectType) {
2208
- const existing = JSON.parse(readFileSync9(configPath, "utf8"));
2209
- writeFileSync4(configPath, `${JSON.stringify({ ...existing, projectType }, null, 2)}
2210
- `);
2211
- ctx.logger.info(`Set projectType \u2192 ${projectType}`);
2212
- }
2213
- runNodeScript(kitPath("scripts/ensure-wphs-workspace.mjs"), [], {
2214
- cwd: ctx.projectRoot,
2215
- logger: ctx.logger,
2216
- dryRun: false
2217
- });
2218
- const adapterPath = resolve16(ctx.projectRoot, "wphs.adapter.ts");
2219
- if (!existsSync11(adapterPath) && isModuleProject(config)) {
2220
- writeFileSync4(
2221
- adapterPath,
2222
- `import type { HubSpotKitAdapter } from '@woodsportal/hubspot-kit'
2223
-
2224
- const adapter: HubSpotKitAdapter = {
2225
- syncLiveData(built) {
2226
- ;(window as Window & { __WPHS_DATA__?: Record<string, unknown> }).__WPHS_DATA__ = built
2227
- },
2228
- }
2229
-
2230
- export default adapter
2231
- `
2232
- );
2233
- ctx.logger.info("Created wphs.adapter.ts stub");
2234
- }
2235
- if (!ctx.global.ci && !ctx.global.quiet) {
2236
- p3.note(
2237
- [
2238
- `Review ${WOODSCLI_CONFIG_FILE} upload paths`,
2239
- "Implement wphs.adapter.ts for your app (if module)",
2240
- "Run wp setup (copies hubspot.config.yml, .env.*, vite shims)",
2241
- "Patch main.dev.css @source to .wphs/.kit-module-config/**"
2242
- ].join("\n"),
2243
- "Manual follow-ups"
2244
- );
2245
- }
2246
- ctx.logger.success(`Adopted ${layout.track} project (${projectType})`);
2247
- return ExitCode.SUCCESS;
2248
- }
2249
-
2250
2409
  // src/cli/commands/migrate.ts
2251
- import { cpSync as cpSync3, existsSync as existsSync12, mkdirSync as mkdirSync4, readFileSync as readFileSync10, readdirSync as readdirSync3, renameSync, rmSync as rmSync3, writeFileSync as writeFileSync5 } from "fs";
2252
- import { join as join3, resolve as resolve17 } from "path";
2410
+ 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";
2411
+ import { join as join3, resolve as resolve19 } from "path";
2253
2412
  var LEGACY_DIRS = [".dev", ".woodsportal"];
2254
2413
  var LEGACY_BASELINE = ".wp-hs-baseline";
2255
2414
  async function runMigrate(ctx, _options = {}) {
2256
2415
  const root = ctx.projectRoot;
2257
- const wphsRoot = resolve17(root, WPHS_WORKSPACE_DIR);
2416
+ const wphsRoot = resolve19(root, WPHS_WORKSPACE_DIR);
2258
2417
  const actions = [];
2259
2418
  for (const legacy of LEGACY_DIRS) {
2260
- const legacyPath = resolve17(root, legacy);
2261
- if (!existsSync12(legacyPath)) continue;
2419
+ const legacyPath = resolve19(root, legacy);
2420
+ if (!existsSync14(legacyPath)) continue;
2262
2421
  actions.push(`Move ${legacy}/ \u2192 .wphs/`);
2263
2422
  if (ctx.global.dryRun) continue;
2264
2423
  mkdirSync4(join3(wphsRoot, "config"), { recursive: true });
2265
2424
  mkdirSync4(join3(wphsRoot, "assets", "branding"), { recursive: true });
2266
2425
  const localConfig = join3(legacyPath, "module-config.local.json");
2267
2426
  const exampleConfig = join3(legacyPath, "module-config.example.json");
2268
- if (existsSync12(localConfig)) {
2427
+ if (existsSync14(localConfig)) {
2269
2428
  cpSync3(localConfig, join3(wphsRoot, "config", "module-config.local.json"));
2270
2429
  }
2271
- if (existsSync12(exampleConfig)) {
2430
+ if (existsSync14(exampleConfig)) {
2272
2431
  cpSync3(exampleConfig, join3(wphsRoot, "config", "module-config.example.json"));
2273
2432
  }
2274
2433
  const branding = join3(legacyPath, "branding-assets");
2275
- if (existsSync12(branding)) {
2434
+ if (existsSync14(branding)) {
2276
2435
  for (const file of readdirSync3(branding)) {
2277
2436
  cpSync3(join3(branding, file), join3(wphsRoot, "assets", "branding", file));
2278
2437
  }
2279
2438
  }
2280
2439
  rmSync3(legacyPath, { recursive: true, force: true });
2281
2440
  }
2282
- const legacyBaseline = resolve17(root, LEGACY_BASELINE);
2441
+ const legacyBaseline = resolve19(root, LEGACY_BASELINE);
2283
2442
  const targetBaseline = join3(wphsRoot, "audit", "baseline");
2284
- if (existsSync12(legacyBaseline)) {
2443
+ if (existsSync14(legacyBaseline)) {
2285
2444
  actions.push(`Move ${LEGACY_BASELINE}/ \u2192 .wphs/audit/baseline/`);
2286
2445
  if (!ctx.global.dryRun) {
2287
2446
  mkdirSync4(targetBaseline, { recursive: true });
@@ -2289,26 +2448,26 @@ async function runMigrate(ctx, _options = {}) {
2289
2448
  rmSync3(legacyBaseline, { recursive: true, force: true });
2290
2449
  }
2291
2450
  }
2292
- const legacyConfigPath2 = resolve17(root, LEGACY_WOODSPORTAL_HS_FILE);
2293
- const woodscliPath = resolve17(root, WOODSCLI_CONFIG_FILE);
2294
- if (existsSync12(legacyConfigPath2) && !existsSync12(woodscliPath)) {
2451
+ const legacyConfigPath2 = resolve19(root, LEGACY_WOODSPORTAL_HS_FILE);
2452
+ const woodscliPath = resolve19(root, WOODSCLI_CONFIG_FILE);
2453
+ if (existsSync14(legacyConfigPath2) && !existsSync14(woodscliPath)) {
2295
2454
  actions.push(`Rename ${LEGACY_WOODSPORTAL_HS_FILE} \u2192 ${WOODSCLI_CONFIG_FILE}`);
2296
2455
  if (!ctx.global.dryRun) {
2297
2456
  renameSync(legacyConfigPath2, woodscliPath);
2298
2457
  }
2299
- } else if (existsSync12(legacyConfigPath2) && existsSync12(woodscliPath)) {
2458
+ } else if (existsSync14(legacyConfigPath2) && existsSync14(woodscliPath)) {
2300
2459
  actions.push(`Remove duplicate ${LEGACY_WOODSPORTAL_HS_FILE}`);
2301
2460
  if (!ctx.global.dryRun) {
2302
2461
  rmSync3(legacyConfigPath2, { force: true });
2303
2462
  }
2304
2463
  }
2305
- if (existsSync12(woodscliPath)) {
2306
- const raw = JSON.parse(readFileSync10(woodscliPath, "utf8"));
2464
+ if (existsSync14(woodscliPath)) {
2465
+ const raw = JSON.parse(readFileSync11(woodscliPath, "utf8"));
2307
2466
  if (raw?.dev?.moduleConfigDir && raw.dev.moduleConfigDir !== WPHS_WORKSPACE_DIR) {
2308
2467
  actions.push(`Update dev.moduleConfigDir \u2192 .wphs in project config`);
2309
2468
  if (!ctx.global.dryRun) {
2310
2469
  raw.dev.moduleConfigDir = WPHS_WORKSPACE_DIR;
2311
- writeFileSync5(woodscliPath, `${JSON.stringify(raw, null, 2)}
2470
+ writeFileSync6(woodscliPath, `${JSON.stringify(raw, null, 2)}
2312
2471
  `);
2313
2472
  }
2314
2473
  }
@@ -2337,10 +2496,10 @@ async function runMigrate(ctx, _options = {}) {
2337
2496
  }
2338
2497
 
2339
2498
  // src/cli/commands/examples.ts
2340
- import { readFileSync as readFileSync11 } from "fs";
2499
+ import { readFileSync as readFileSync12 } from "fs";
2341
2500
  function loadManifest() {
2342
2501
  const manifestPath = kitPath("examples", "manifest.json");
2343
- const raw = JSON.parse(readFileSync11(manifestPath, "utf8"));
2502
+ const raw = JSON.parse(readFileSync12(manifestPath, "utf8"));
2344
2503
  return raw.scenarios;
2345
2504
  }
2346
2505
  async function runExamplesList(ctx, options = {}) {
@@ -2373,15 +2532,15 @@ async function runExamplesSmoke(ctx, options = {}) {
2373
2532
  }
2374
2533
 
2375
2534
  // src/cli/commands/fields.ts
2376
- import { cpSync as cpSync4, existsSync as existsSync13, readFileSync as readFileSync12, writeFileSync as writeFileSync6 } from "fs";
2377
- import { resolve as resolve18 } from "path";
2535
+ import { cpSync as cpSync4, existsSync as existsSync15, readFileSync as readFileSync13, writeFileSync as writeFileSync7 } from "fs";
2536
+ import { resolve as resolve20 } from "path";
2378
2537
  async function runFieldsSync(ctx, options = {}) {
2379
2538
  const direction = options.direction ?? "pull";
2380
- const fieldsPath = resolve18(ctx.projectRoot, ctx.config.dev.fieldsJson);
2381
- const cacheDir = resolve18(ctx.projectRoot, ctx.config.dev.moduleConfigDir, "cache", "fields-sync");
2382
- const cachePath = resolve18(cacheDir, "fields.json");
2539
+ const fieldsPath = resolve20(ctx.projectRoot, ctx.config.dev.fieldsJson);
2540
+ const cacheDir = resolve20(ctx.projectRoot, ctx.config.dev.moduleConfigDir, "cache", "fields-sync");
2541
+ const cachePath = resolve20(cacheDir, "fields.json");
2383
2542
  if (direction === "pull") {
2384
- if (!existsSync13(fieldsPath)) {
2543
+ if (!existsSync15(fieldsPath)) {
2385
2544
  throw new WpHsError(`Missing ${ctx.config.dev.fieldsJson}`, {
2386
2545
  exitCode: ExitCode.USER_ERROR,
2387
2546
  remediation: "Add fields.json or run wp init."
@@ -2397,7 +2556,7 @@ async function runFieldsSync(ctx, options = {}) {
2397
2556
  ctx.logger.success(`Cached fields \u2192 ${cachePath}`);
2398
2557
  return ExitCode.SUCCESS;
2399
2558
  }
2400
- if (!existsSync13(cachePath)) {
2559
+ if (!existsSync15(cachePath)) {
2401
2560
  throw new WpHsError("No cached fields \u2014 run wp fields sync pull first.", {
2402
2561
  exitCode: ExitCode.USER_ERROR
2403
2562
  });
@@ -2406,14 +2565,14 @@ async function runFieldsSync(ctx, options = {}) {
2406
2565
  ctx.logger.info(`[dry-run] Would write ${cachePath} \u2192 ${fieldsPath}`);
2407
2566
  return ExitCode.SUCCESS;
2408
2567
  }
2409
- writeFileSync6(fieldsPath, readFileSync12(cachePath, "utf8"));
2568
+ writeFileSync7(fieldsPath, readFileSync13(cachePath, "utf8"));
2410
2569
  ctx.logger.success(`Restored fields from cache \u2192 ${fieldsPath}`);
2411
2570
  return ExitCode.SUCCESS;
2412
2571
  }
2413
2572
 
2414
2573
  // src/cli/commands/regression.ts
2415
- import { existsSync as existsSync14 } from "fs";
2416
- import { resolve as resolve19 } from "path";
2574
+ import { existsSync as existsSync16 } from "fs";
2575
+ import { resolve as resolve21 } from "path";
2417
2576
  var LEGACY_SHIM_SCRIPTS = [
2418
2577
  "build-cdn.mjs",
2419
2578
  "run-vite-build.mjs",
@@ -2436,21 +2595,21 @@ async function runRegressionSmoke(ctx, options = {}) {
2436
2595
  if (!validateOk) failures.push("wp config validate failed");
2437
2596
  if (options.checkLegacyShims !== false) {
2438
2597
  for (const script of LEGACY_SHIM_SCRIPTS) {
2439
- const shim = resolve19(ctx.projectRoot, "scripts", script);
2598
+ const shim = resolve21(ctx.projectRoot, "scripts", script);
2440
2599
  const kitScript = kitPath("scripts", script);
2441
- const shimOk = existsSync14(shim) && existsSync14(kitScript);
2600
+ const shimOk = existsSync16(shim) && existsSync16(kitScript);
2442
2601
  checks.push({ name: `shim:${script}`, ok: shimOk });
2443
2602
  if (!shimOk) failures.push(`Missing legacy shim or kit script: ${script}`);
2444
2603
  }
2445
2604
  }
2446
2605
  for (const rel of FORBIDDEN_CONSUMER_PATHS) {
2447
- const forbidden = resolve19(ctx.projectRoot, rel);
2448
- const absent = !existsSync14(forbidden);
2606
+ const forbidden = resolve21(ctx.projectRoot, rel);
2607
+ const absent = !existsSync16(forbidden);
2449
2608
  checks.push({ name: `no-${rel}`, ok: absent });
2450
2609
  if (!absent) failures.push(`Forbidden dev path still present: ${rel}`);
2451
2610
  }
2452
- const woodscliConfig = resolve19(ctx.projectRoot, WOODSCLI_CONFIG_FILE);
2453
- const configOk = existsSync14(woodscliConfig);
2611
+ const woodscliConfig = resolve21(ctx.projectRoot, WOODSCLI_CONFIG_FILE);
2612
+ const configOk = existsSync16(woodscliConfig);
2454
2613
  checks.push({ name: WOODSCLI_CONFIG_FILE, ok: configOk });
2455
2614
  if (!configOk) failures.push(`${WOODSCLI_CONFIG_FILE} missing`);
2456
2615
  if (ctx.global.json) {
@@ -2508,6 +2667,17 @@ async function runSetup(ctx, options = {}) {
2508
2667
  );
2509
2668
  const copied = results.filter((r) => r.action === "copied");
2510
2669
  const manual = results.filter((r) => r.action === "manual");
2670
+ const nonInteractive = ctx.global.ci || ctx.global.yes || ctx.global.json || ctx.global.dryRun;
2671
+ if (usesCdnPipeline(ctx.config) && needsCdnGitHubSetup(ctx.projectRoot) && !nonInteractive) {
2672
+ const cdnSetup = await promptCdnGitHubSetup({
2673
+ projectRoot: ctx.projectRoot,
2674
+ githubDefault: githubDefaultForPrompt(ctx.projectRoot)
2675
+ });
2676
+ if (cdnSetup) {
2677
+ applyCdnGitHubSetup(ctx.projectRoot, cdnSetup);
2678
+ ctx.logger.success(formatCdnSetupNote(ctx.projectRoot));
2679
+ }
2680
+ }
2511
2681
  if (ctx.global.json) {
2512
2682
  ctx.logger.json({
2513
2683
  command: "setup",
@@ -2546,8 +2716,8 @@ async function runSetup(ctx, options = {}) {
2546
2716
  import * as p4 from "@clack/prompts";
2547
2717
 
2548
2718
  // src/lib/hubspot-portals.ts
2549
- import { existsSync as existsSync15, readFileSync as readFileSync13 } from "fs";
2550
- import { resolve as resolve20 } from "path";
2719
+ import { existsSync as existsSync17, readFileSync as readFileSync14 } from "fs";
2720
+ import { resolve as resolve22 } from "path";
2551
2721
  function stripYamlScalar(value) {
2552
2722
  const trimmed = value.trim();
2553
2723
  if (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'")) {
@@ -2585,14 +2755,14 @@ function parseHubSpotConfigPortals(raw) {
2585
2755
  return { defaultPortal, portals };
2586
2756
  }
2587
2757
  function readHubSpotPortals(projectRoot) {
2588
- const configPath = resolve20(projectRoot, HUBSPOT_CONFIG_FILE);
2589
- if (!existsSync15(configPath)) {
2758
+ const configPath = resolve22(projectRoot, HUBSPOT_CONFIG_FILE);
2759
+ if (!existsSync17(configPath)) {
2590
2760
  throw new WpHsError(`${HUBSPOT_CONFIG_FILE} not found in project root.`, {
2591
2761
  exitCode: ExitCode.USER_ERROR,
2592
2762
  remediation: "Run wp setup, then add your portal + PAT."
2593
2763
  });
2594
2764
  }
2595
- const raw = readFileSync13(configPath, "utf8");
2765
+ const raw = readFileSync14(configPath, "utf8");
2596
2766
  const { defaultPortal, portals } = parseHubSpotConfigPortals(raw);
2597
2767
  return { configPath, defaultPortal, portals };
2598
2768
  }
@@ -2722,7 +2892,7 @@ async function runWatch(ctx, options) {
2722
2892
  // src/cli/program.ts
2723
2893
  function readVersion() {
2724
2894
  try {
2725
- const pkg = JSON.parse(readFileSync14(resolve21(getKitRoot(), "package.json"), "utf8"));
2895
+ const pkg = JSON.parse(readFileSync15(resolve23(getKitRoot(), "package.json"), "utf8"));
2726
2896
  return pkg.version ?? "0.0.0";
2727
2897
  } catch {
2728
2898
  return "0.0.0";
@@ -2781,7 +2951,10 @@ function createProgram() {
2781
2951
  const global = program2.opts();
2782
2952
  await handle(() => runConfigValidate(buildContext(global)));
2783
2953
  });
2784
- program2.command("init").description("Scaffold a new module or theme project").option("-t, --template <type>", "module-hybrid-cdn | module-monolith | theme-starter").option("-n, --name <slug>", "Project name (creates ./<name>/ under cwd)").option("-d, --directory <path>", "Full target directory (skips name \u2192 folder prompt)").option("--router <mode>", "hash (client-frontend default) | browser | none \u2014 modules only").action(
2954
+ program2.command("init").description("Scaffold a new module or theme project").option("-t, --template <type>", "module-hybrid-cdn | module-monolith | theme-starter").option("-n, --name <slug>", "Project name (creates ./<name>/ under cwd)").option("-d, --directory <path>", "Full target directory (skips name \u2192 folder prompt)").option("--router <mode>", "hash (client-frontend default) | browser | none \u2014 modules only").option(
2955
+ "--cdn-github <org/repo>",
2956
+ "Public GitHub CDN mirror repo (hybrid CDN; use with --ci / -y)"
2957
+ ).action(
2785
2958
  async (opts) => {
2786
2959
  const global = program2.opts();
2787
2960
  const logger = createLogger({
@@ -2802,15 +2975,12 @@ function createProgram() {
2802
2975
  template: opts.template,
2803
2976
  name: opts.name,
2804
2977
  directory: opts.directory,
2805
- router: routerMode
2978
+ router: routerMode,
2979
+ cdnGithub: opts.cdnGithub
2806
2980
  })
2807
2981
  );
2808
2982
  }
2809
2983
  );
2810
- program2.command("adopt").description("Detect existing module/theme repo and add WoodsCLI config + .wphs/").action(async () => {
2811
- const global = program2.opts();
2812
- await handle(() => runAdopt(buildContext(global)));
2813
- });
2814
2984
  program2.command("migrate").description("Migrate legacy .dev/ or .wp-hs-baseline/ to .wphs/").action(async () => {
2815
2985
  const global = program2.opts();
2816
2986
  await handle(() => runMigrate(buildContext(global)));