@fluid-app/fluid-cli-theme-dev 0.1.62 → 0.1.64

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 (137) hide show
  1. package/README.md +1 -1
  2. package/dist/dist-cli-preview/.vite/manifest.json +225 -225
  3. package/dist/dist-cli-preview/account-screens.js +1 -1
  4. package/dist/dist-cli-preview/chunks/{Breadcrumb-CZ-skiUy.js → Breadcrumb-BQKo8hPc.js} +1 -1
  5. package/dist/dist-cli-preview/chunks/{Combobox-BGiCYa56.js → Combobox-cf11L_T-.js} +4 -4
  6. package/dist/dist-cli-preview/chunks/{ContactsScreen-vsNgy_gn.js → ContactsScreen-gIiy4u7c.js} +1160 -1155
  7. package/dist/dist-cli-preview/chunks/{MemberManagementProviders-DMwkh3JW.js → MemberManagementProviders-C1bwc3tB.js} +258 -224
  8. package/dist/dist-cli-preview/chunks/{MessagingScreen-DzJOwPGH.js → MessagingScreen-B8ZxAE0L.js} +3904 -3927
  9. package/dist/dist-cli-preview/chunks/{MobileActionSheet-DqVPgFKI.js → MobileActionSheet-DMeTsLlK.js} +100 -100
  10. package/dist/dist-cli-preview/chunks/{MySiteScreen-BSmWlCKA.js → MySiteScreen-Dtw1rsF9.js} +69 -69
  11. package/dist/dist-cli-preview/chunks/OrdersScreen-C-b6i5jB.js +9 -0
  12. package/dist/dist-cli-preview/chunks/{OrdersScreen-CVEl9FnN.js → OrdersScreen-D9HfcTaa.js} +247 -249
  13. package/dist/dist-cli-preview/chunks/{ProfileScreen-BC5gyJDY.js → ProfileScreen-DwXozaLs.js} +1051 -965
  14. package/dist/dist-cli-preview/chunks/{SubscriptionsScreen-CFM4Pufg.js → SubscriptionsScreen-CIdYNpWT.js} +3 -3
  15. package/dist/dist-cli-preview/chunks/{account-screen-runtime-DxhJm6Qd.js → account-screen-runtime-DYMunv0A.js} +2482 -2407
  16. package/dist/dist-cli-preview/chunks/ar-DaN3xEy4.js +351 -0
  17. package/dist/dist-cli-preview/chunks/{arrow-left-UFKI-PQf.js → arrow-left-Bd7dJtIV.js} +1 -1
  18. package/dist/dist-cli-preview/chunks/bg-D3Np0de3.js +351 -0
  19. package/dist/dist-cli-preview/chunks/bn-BDSehnZ3.js +351 -0
  20. package/dist/dist-cli-preview/chunks/{cli-preview-DunPfnjU.js → cli-preview-BT_ARKjk.js} +13 -13
  21. package/dist/dist-cli-preview/chunks/{client.gen-CSTBgfxj.js → client.gen-Ck6Pm2jV.js} +246 -236
  22. package/dist/dist-cli-preview/chunks/contacts-ChSWj70z.js +4 -0
  23. package/dist/dist-cli-preview/chunks/{countries-api-adapter-B3usO25w.js → countries-api-adapter-lxSE748x.js} +2 -2
  24. package/dist/dist-cli-preview/chunks/{create-card-entry-container-DHgsbSyr.js → create-card-entry-container-D6kSC2Aw.js} +20726 -20509
  25. package/dist/dist-cli-preview/chunks/cs-CrmATj4L.js +351 -0
  26. package/dist/dist-cli-preview/chunks/da-CmJdErjd.js +351 -0
  27. package/dist/dist-cli-preview/chunks/de-C0KTNBZq.js +351 -0
  28. package/dist/dist-cli-preview/chunks/el-BOQs49GU.js +351 -0
  29. package/dist/dist-cli-preview/chunks/es-Dt3VSQPu.js +351 -0
  30. package/dist/dist-cli-preview/chunks/fi-B8UZ7eJL.js +351 -0
  31. package/dist/dist-cli-preview/chunks/fr-BomEiNL0.js +351 -0
  32. package/dist/dist-cli-preview/chunks/he-CfmnXjsr.js +351 -0
  33. package/dist/dist-cli-preview/chunks/{hey-api-client-DSvKWGPq.js → hey-api-client-DGRBsZ7Y.js} +3 -3
  34. package/dist/dist-cli-preview/chunks/hi-CTk5zGoJ.js +351 -0
  35. package/dist/dist-cli-preview/chunks/hr-DaNToqEm.js +351 -0
  36. package/dist/dist-cli-preview/chunks/hu-lcAuIwez.js +351 -0
  37. package/dist/dist-cli-preview/chunks/id-CEThcKZG.js +351 -0
  38. package/dist/dist-cli-preview/chunks/it-BCRCoDOg.js +351 -0
  39. package/dist/dist-cli-preview/chunks/ja-BIkFdIfq.js +351 -0
  40. package/dist/dist-cli-preview/chunks/ko-CD3bk6gv.js +351 -0
  41. package/dist/dist-cli-preview/chunks/{log-out-CBUq5Hd-.js → log-out-CSPZjXTK.js} +1 -1
  42. package/dist/dist-cli-preview/chunks/{map-pin-DHfjSyoR.js → map-pin-B2ScoijR.js} +1 -1
  43. package/dist/dist-cli-preview/chunks/messaging-Bq5UPkvG.js +4 -0
  44. package/dist/dist-cli-preview/chunks/{mount-account-screen-pc1VISwF.js → mount-account-screen-C_CdoGir.js} +2453 -2392
  45. package/dist/dist-cli-preview/chunks/ms-DI4mGa5X.js +351 -0
  46. package/dist/dist-cli-preview/chunks/mysite-DcpuKsuW.js +4 -0
  47. package/dist/dist-cli-preview/chunks/nl-B-XAD3Ra.js +351 -0
  48. package/dist/dist-cli-preview/chunks/no-C_NmYGbe.js +351 -0
  49. package/dist/dist-cli-preview/chunks/orders-B_57xKkq.js +4 -0
  50. package/dist/dist-cli-preview/chunks/pl-mQRzJZEW.js +351 -0
  51. package/dist/dist-cli-preview/chunks/{plus-C-lo0pFM.js → plus-CocoE5gC.js} +1 -1
  52. package/dist/dist-cli-preview/chunks/profile-BfM2KxKe.js +4 -0
  53. package/dist/dist-cli-preview/chunks/pt-BYMWPjog.js +351 -0
  54. package/dist/dist-cli-preview/chunks/ro-B98EuFQI.js +351 -0
  55. package/dist/dist-cli-preview/chunks/ru-DBO9IasC.js +351 -0
  56. package/dist/dist-cli-preview/chunks/schemas-D94nZk5x.js +5374 -0
  57. package/dist/dist-cli-preview/chunks/{screen-route-slug-utils-B254xI_J.js → screen-route-slug-utils-BDjSpDoe.js} +1 -1
  58. package/dist/dist-cli-preview/chunks/{sheet-haptics-context-D2WUiXJm.js → sheet-haptics-context-iOjKcNDj.js} +1 -1
  59. package/dist/dist-cli-preview/chunks/{sidebar-CMSWZCPS.js → sidebar-Ckzom4Xc.js} +12 -12
  60. package/dist/dist-cli-preview/chunks/sk-hRvNJfiN.js +351 -0
  61. package/dist/dist-cli-preview/chunks/{store-api-context-BIdILNWL.js → store-api-context-B96Ip2FB.js} +1 -1
  62. package/dist/dist-cli-preview/chunks/subscriptions-8VtIA4w9.js +4 -0
  63. package/dist/dist-cli-preview/chunks/sv-noYtTQbO.js +351 -0
  64. package/dist/dist-cli-preview/chunks/th-CJCAKsR-.js +351 -0
  65. package/dist/dist-cli-preview/chunks/tl-Bx8pJ7p7.js +351 -0
  66. package/dist/dist-cli-preview/chunks/tr-C4tCNiV7.js +351 -0
  67. package/dist/dist-cli-preview/chunks/{trash-2-Bq4IqH0j.js → trash-2-wVweQZMw.js} +4 -4
  68. package/dist/dist-cli-preview/chunks/uk-xFjXQDP3.js +351 -0
  69. package/dist/dist-cli-preview/chunks/{use-optional-store-Csor1Z3z.js → use-optional-store-lZUz976C.js} +2 -2
  70. package/dist/dist-cli-preview/chunks/use-store-DVmgsDXy.js +103 -0
  71. package/dist/dist-cli-preview/chunks/{useInfiniteQuery-BpFtQ7DH.js → useInfiniteQuery-Dtzov9uh.js} +1 -1
  72. package/dist/dist-cli-preview/chunks/{user-BGrQJ1GJ.js → user-B81IrSgp.js} +1 -1
  73. package/dist/dist-cli-preview/chunks/{users-DBL0isYy.js → users-BXTjoCUz.js} +1 -1
  74. package/dist/dist-cli-preview/chunks/vi-BAvhRTgb.js +351 -0
  75. package/dist/dist-cli-preview/chunks/zh_CN-Cyq9zeex.js +351 -0
  76. package/dist/dist-cli-preview/chunks/zh_TW-DFM5MhMA.js +351 -0
  77. package/dist/index.mjs +346 -32
  78. package/dist/index.mjs.map +1 -1
  79. package/dist/skills/themes-cart-feedback/SKILL.md +9 -14
  80. package/dist/skills/themes-review/SKILL.md +40 -34
  81. package/dist/skills/themes-review/references/blocks-vs-sections.md +2 -2
  82. package/dist/skills/themes-review/references/dead-code.md +1 -1
  83. package/dist/skills/themes-review/references/dynamism.md +9 -3
  84. package/dist/skills/themes-review/references/editor-attributes.md +25 -26
  85. package/dist/skills/themes-review/references/examples.md +3 -4
  86. package/dist/skills/themes-review/references/fairshare-attributes.md +5 -5
  87. package/dist/skills/themes-review/references/global-settings.md +56 -67
  88. package/dist/skills/themes-review/references/liquid-correctness.md +10 -15
  89. package/dist/skills/themes-review/references/media-tag.md +8 -7
  90. package/dist/skills/themes-review/references/navigation.md +18 -13
  91. package/dist/skills/themes-review/references/security-accessibility.md +11 -6
  92. package/dist/skills/themes-review/references/setting-types.md +136 -77
  93. package/dist/skills/themes-settings-schema/SKILL.md +15 -2
  94. package/package.json +2 -2
  95. package/dist/dist-cli-preview/chunks/OrdersScreen-d4fJE_dm.js +0 -9
  96. package/dist/dist-cli-preview/chunks/ar-B7NQmkZ1.js +0 -341
  97. package/dist/dist-cli-preview/chunks/bg-CxfL8rZN.js +0 -341
  98. package/dist/dist-cli-preview/chunks/bn-CdhHCK--.js +0 -341
  99. package/dist/dist-cli-preview/chunks/contacts-CpYlEgAq.js +0 -4
  100. package/dist/dist-cli-preview/chunks/cs-DTe-xGeI.js +0 -341
  101. package/dist/dist-cli-preview/chunks/da-B8Cl_vkl.js +0 -341
  102. package/dist/dist-cli-preview/chunks/de-D8cYy8G-.js +0 -341
  103. package/dist/dist-cli-preview/chunks/el-Chjin2JW.js +0 -341
  104. package/dist/dist-cli-preview/chunks/es-mp8RwFN4.js +0 -341
  105. package/dist/dist-cli-preview/chunks/fi-91gcpxoy.js +0 -341
  106. package/dist/dist-cli-preview/chunks/fr-ekvUCKTH.js +0 -341
  107. package/dist/dist-cli-preview/chunks/he-B7QiuquT.js +0 -341
  108. package/dist/dist-cli-preview/chunks/hi-DNNhi7ud.js +0 -341
  109. package/dist/dist-cli-preview/chunks/hr-CAwxMKX_.js +0 -341
  110. package/dist/dist-cli-preview/chunks/hu-D_oQwH4Q.js +0 -341
  111. package/dist/dist-cli-preview/chunks/id-oeAszBoo.js +0 -341
  112. package/dist/dist-cli-preview/chunks/isAfter-BUGgByEf.js +0 -1414
  113. package/dist/dist-cli-preview/chunks/it-Dme6GXSG.js +0 -341
  114. package/dist/dist-cli-preview/chunks/ja-CRnvQNCa.js +0 -341
  115. package/dist/dist-cli-preview/chunks/ko-8mk8A3_I.js +0 -341
  116. package/dist/dist-cli-preview/chunks/messaging-DkyH4CPP.js +0 -4
  117. package/dist/dist-cli-preview/chunks/ms-Bs0GvdOw.js +0 -341
  118. package/dist/dist-cli-preview/chunks/mysite-wvfITV7T.js +0 -4
  119. package/dist/dist-cli-preview/chunks/nl-tsI6lLxl.js +0 -341
  120. package/dist/dist-cli-preview/chunks/no-DMot6qX0.js +0 -341
  121. package/dist/dist-cli-preview/chunks/orders-c13r499A.js +0 -4
  122. package/dist/dist-cli-preview/chunks/pl-DmTxONNg.js +0 -341
  123. package/dist/dist-cli-preview/chunks/profile-CoWo9Sxe.js +0 -4
  124. package/dist/dist-cli-preview/chunks/pt-DgmwnAV-.js +0 -341
  125. package/dist/dist-cli-preview/chunks/ro-B4M17oRc.js +0 -341
  126. package/dist/dist-cli-preview/chunks/ru-BS2gZnbs.js +0 -341
  127. package/dist/dist-cli-preview/chunks/schemas-9_ZwC5nC.js +0 -3977
  128. package/dist/dist-cli-preview/chunks/sk-BdDjjHfn.js +0 -341
  129. package/dist/dist-cli-preview/chunks/subscriptions-BHALQrcU.js +0 -4
  130. package/dist/dist-cli-preview/chunks/sv-CW7FL5HA.js +0 -341
  131. package/dist/dist-cli-preview/chunks/th-BYBMKmSh.js +0 -341
  132. package/dist/dist-cli-preview/chunks/tl-CDigaLsK.js +0 -341
  133. package/dist/dist-cli-preview/chunks/tr-PxiurNCr.js +0 -341
  134. package/dist/dist-cli-preview/chunks/uk-B4aYnJ8t.js +0 -341
  135. package/dist/dist-cli-preview/chunks/vi-BoKSdzH3.js +0 -341
  136. package/dist/dist-cli-preview/chunks/zh_CN-DrCrXsZC.js +0 -341
  137. package/dist/dist-cli-preview/chunks/zh_TW-JmyC1W5K.js +0 -341
package/dist/index.mjs CHANGED
@@ -134,8 +134,10 @@ function createFetchClient(config) {
134
134
  const queryString = new URLSearchParams();
135
135
  Object.entries(params).forEach(([key, value]) => {
136
136
  if (value === void 0 || value === null) return;
137
- if (Array.isArray(value)) value.forEach((item) => queryString.append(`${key}[]`, String(item)));
138
- else if (typeof value === "object") Object.entries(value).forEach(([subKey, subValue]) => {
137
+ if (Array.isArray(value)) {
138
+ const arrayKey = key.endsWith("[]") ? key : `${key}[]`;
139
+ value.forEach((item) => queryString.append(arrayKey, String(item)));
140
+ } else if (typeof value === "object") Object.entries(value).forEach(([subKey, subValue]) => {
139
141
  if (subValue === void 0 || subValue === null) return;
140
142
  if (Array.isArray(subValue)) subValue.forEach((item) => queryString.append(`${key}[${subKey}][]`, String(item)));
141
143
  else queryString.append(`${key}[${subKey}]`, String(subValue));
@@ -611,7 +613,7 @@ async function listApplicationThemes(client, params) {
611
613
  }
612
614
  /**
613
615
  * Create an application theme
614
- *
616
+ * Creates a new theme for your company. Requires a company Bearer token with permission to update themes. The theme starts in `draft` status unless you pass another `status`. Pass `cover_image_url` to set the cover image. When the company has moved theme stylesheets to theme-level assets, sending `custom_stylesheet` or `global_stylesheet` returns 422. A Git repository is provisioned for the theme in the background when Git sync is available.
615
617
  *
616
618
  * @param client - Fetch client instance
617
619
  * @param body - body
@@ -621,7 +623,7 @@ async function createApplicationTheme(client, body) {
621
623
  }
622
624
  /**
623
625
  * Get an application theme
624
- *
626
+ * Returns one of your company's themes with its templates (including full Liquid content), versions, images, and Git repositories. Pass `version` to read the theme as it was at a past version. Requires a company Bearer token with permission to view themes. Themes belonging to another company return 404.
625
627
  *
626
628
  * @param client - Fetch client instance
627
629
  * @param id - id
@@ -632,7 +634,7 @@ async function getApplicationTheme(client, id, params) {
632
634
  }
633
635
  /**
634
636
  * Returns available themeables for a given type scoped to the theme's company
635
- *
637
+ * Lists the company resources of one type (for example `product`, `page`, `collection`, or `enrollment_pack`) that templates in this theme can be assigned to, sorted by title. Pass `themeable` to choose the type; unsupported types return 400. Pass `search` to filter by title. Results are paginated, with page details in `meta.pagination`, 20 per page by default. Requires a company Bearer token with permission to view themes.
636
638
  *
637
639
  * @param client - Fetch client instance
638
640
  * @param id - id
@@ -654,7 +656,7 @@ async function cloneApplicationThemeForDevelopment(client, id, body) {
654
656
  }
655
657
  /**
656
658
  * Publishes the theme
657
- *
659
+ * Publishes the theme so the storefront serves its latest content, then syncs it to any connected Git repository. Pass the optional `version` query parameter to publish a specific past version instead; an unknown version returns 404. Requires a company Bearer token with permission to update themes. Returns 422 when the theme's member configuration references a template or navigation item that does not exist.
658
660
  *
659
661
  * @param client - Fetch client instance
660
662
  * @param id - id
@@ -664,7 +666,7 @@ async function publishApplicationTheme(client, id) {
664
666
  }
665
667
  /**
666
668
  * Get theme assets
667
- *
669
+ * Lists the files (images, stylesheets, scripts, and other assets) uploaded to one of your company's themes. Requires a company Bearer token with permission to view themes.
668
670
  *
669
671
  * @param client - Fetch client instance
670
672
  * @param id - id
@@ -695,7 +697,7 @@ async function updateThemeResource(client, application_theme_id, body) {
695
697
  }
696
698
  /**
697
699
  * Deletes a theme resource
698
- *
700
+ * Deletes one theme file, identified by its `key`, as the theme CLI does when a local file is removed. Send the `base_sha` from your last pull to guard against overwriting newer server changes (409 when stale). The member site manifest `members_sites.json` cannot be deleted this way. Requires a company admin token with the `application_themes` update permission.
699
701
  *
700
702
  * @param client - Fetch client instance
701
703
  * @param application_theme_id - application_theme_id
@@ -706,7 +708,7 @@ async function deleteThemeResource(client, application_theme_id, body) {
706
708
  }
707
709
  /**
708
710
  * Creates a file resource
709
- *
711
+ * Uploads a file to the company's asset library. Provide exactly one source: `content` for a text file, a multipart `file` upload, or `dam_asset_code` to link an existing digital asset; otherwise supply `url` with `filename`, `content_type`, and `content_size`. To attach the file to a theme or template, send both `relateable_type` (`ApplicationTheme` or `ApplicationThemeTemplate`) and `relateable_id`. Requires the `file_resources` update permission.
710
712
  *
711
713
  * @param client - Fetch client instance
712
714
  * @param body - body
@@ -716,7 +718,7 @@ async function createFileResource(client, body) {
716
718
  }
717
719
  /**
718
720
  * Deletes a file resource
719
- *
721
+ * Deletes one of the company's uploaded files and returns its ID. Requires the `file_resources` update permission. Returns 404 when the file does not belong to the company.
720
722
  *
721
723
  * @param client - Fetch client instance
722
724
  * @param id - id
@@ -972,6 +974,23 @@ function readLegacyThemeConfig(themeRoot) {
972
974
  return null;
973
975
  }
974
976
  }
977
+ /**
978
+ * Whether a push or pull to `theme` may use this directory's own sync state
979
+ * and rebind it to `theme`.
980
+ *
981
+ * Development themes are scratch copies `fluid theme dev` creates and syncs.
982
+ * Using one here would move the project's sync baseline to the scratch copy
983
+ * and rebind the project, so a later Publish would update the copy instead of
984
+ * the theme the project was pulled from. Push syncs such a theme separately;
985
+ * pull refuses. An explicit `--theme` to any other theme still rebinds.
986
+ */
987
+ function mayBindTheme(existing, theme) {
988
+ return !existing || existing.themeId === theme.id || theme.status !== "development";
989
+ }
990
+ /** Why `mayBindTheme` refused, with the way forward. */
991
+ function developmentThemeRefusal(existing, theme) {
992
+ return `${theme.name} (#${theme.id}) is a development theme, and this project is bound to ${existing.themeName} (#${existing.themeId}). Pulling it here would replace this project's sync state. Use \`fluid theme dev\` or \`fluid theme push -t\` to update the development theme, or pull it into a separate directory with --root.`;
993
+ }
975
994
  /** Write `.fluid-theme.json` to a theme directory. */
976
995
  function writeThemeConfig(themeRoot, config) {
977
996
  writeFileSync(configPath(themeRoot), JSON.stringify(config, null, 2) + "\n", "utf-8");
@@ -1031,6 +1050,21 @@ const THEME_LEVEL_RESOURCE_KEYS = new Set([
1031
1050
  "variables.json",
1032
1051
  "members_sites.json"
1033
1052
  ]);
1053
+ /**
1054
+ * Template types Fluid has retired. The resources API answers 404 for them, so
1055
+ * a theme that still carries their folders must not try to upload them: member
1056
+ * home pages are `members_page` templates on the `/` route, sign-in is hosted,
1057
+ * and account pages became member pages.
1058
+ */
1059
+ const RETIRED_TEMPLATE_TYPES = new Set([
1060
+ "account_page",
1061
+ "members_dashboard",
1062
+ "members_login"
1063
+ ]);
1064
+ function isRetiredTemplateKey(relativePath) {
1065
+ const [prefix] = normalizeThemeResourceKey(relativePath).split("/");
1066
+ return prefix !== void 0 && RETIRED_TEMPLATE_TYPES.has(prefix);
1067
+ }
1034
1068
  const COMPOSITE_RESOURCE_FILE_NAMES = new Set([
1035
1069
  "index.liquid",
1036
1070
  "styles.css",
@@ -1051,6 +1085,7 @@ function isThemeResourceKey(relativePath) {
1051
1085
  const key = normalizeThemeResourceKey(relativePath);
1052
1086
  if (!hasSafeSegments(key)) return false;
1053
1087
  if (THEME_LEVEL_RESOURCE_KEYS.has(key)) return true;
1088
+ if (isRetiredTemplateKey(key)) return false;
1054
1089
  const segments = key.split("/");
1055
1090
  const prefix = segments[0];
1056
1091
  const fileName = segments.at(-1);
@@ -1665,7 +1700,7 @@ function injectHotReload(html, mode) {
1665
1700
  }
1666
1701
  //#endregion
1667
1702
  //#region src/theme/dev-server/member-preview-html.ts
1668
- function escapeHtml(value) {
1703
+ function escapeHtml$1(value) {
1669
1704
  return value.replace(/[&<>"']/g, (char) => ({
1670
1705
  "&": "&amp;",
1671
1706
  "<": "&lt;",
@@ -1723,9 +1758,9 @@ function renderMemberHtml(render, opts) {
1723
1758
  if (href) href.value = previewLink(href.value, opts);
1724
1759
  }
1725
1760
  }
1726
- const runtime = parseFragment(`<script type="module" src="${escapeHtml(opts.localOrigin)}/_members/_runtime/account-screens.js"><\/script>`);
1761
+ const runtime = parseFragment(`<script type="module" src="${escapeHtml$1(opts.localOrigin)}/_members/_runtime/account-screens.js"><\/script>`);
1727
1762
  head.childNodes.unshift(...runtime.childNodes);
1728
- const base = parseFragment(`<base href="${escapeHtml(new URL(opts.path, opts.origin).href)}">`);
1763
+ const base = parseFragment(`<base href="${escapeHtml$1(new URL(opts.path, opts.origin).href)}">`);
1729
1764
  head.childNodes.unshift(...base.childNodes);
1730
1765
  const styles = parseFragment(render.styles);
1731
1766
  head.childNodes.push(...styles.childNodes);
@@ -1859,7 +1894,7 @@ var MemberPreview = class {
1859
1894
  }));
1860
1895
  return true;
1861
1896
  }
1862
- if (url.pathname === "/_members" || url.pathname === "/_members/") {
1897
+ if ((url.pathname === "/_members" || url.pathname === "/_members/") && !url.searchParams.has("members_site_id")) {
1863
1898
  const screens = metadata.system_screens.filter((screen) => this.screenAssets.screens.includes(screen.key));
1864
1899
  const links = metadata.members_sites.map((site) => {
1865
1900
  const destinations = [...site.pages.map((page) => ({
@@ -1869,7 +1904,7 @@ var MemberPreview = class {
1869
1904
  path: `/${screen.key}`,
1870
1905
  title: screen.key + " (sample)"
1871
1906
  }))];
1872
- return `<h2>${escapeHtml(site.name)}</h2><ul>${destinations.map((page) => `<li><a href="${escapeHtml(memberPreviewUrl(page.path, { members_site_id: site.id }))}">${escapeHtml(page.title)}</a></li>`).join("")}</ul>`;
1907
+ return `<h2>${escapeHtml$1(site.name)}</h2><ul>${destinations.map((page) => `<li><a href="${escapeHtml$1(memberPreviewUrl(page.path, { members_site_id: site.id }))}">${escapeHtml$1(page.title)}</a></li>`).join("")}</ul>`;
1873
1908
  }).join("");
1874
1909
  res.setHeader("Content-Type", "text/html; charset=utf-8");
1875
1910
  res.end(req.method === "HEAD" ? void 0 : `<!doctype html><meta charset="utf-8"><title>Member preview</title><h1>Member preview</h1><p>Sample data. Select a page to preview its site's layout.</p>${links || "<p>No member pages in this theme.</p>"}`);
@@ -1878,9 +1913,9 @@ var MemberPreview = class {
1878
1913
  const selection = parseSelection(url.searchParams);
1879
1914
  const site = metadata.members_sites.find((s) => s.id === selection.members_site_id);
1880
1915
  if (!site) throw new MemberPreviewError("This site no longer belongs to the preview theme. Select a page at /_members.", 404);
1881
- const path = url.pathname.slice(9);
1882
- const page = site.pages.find((p) => p.path === path);
1883
- const screen = page ? void 0 : metadata.system_screens.find((s) => path === `/${s.key}` || path.startsWith(`/${s.key}/`));
1916
+ const path = url.pathname.slice(9) || "/";
1917
+ const screen = metadata.system_screens.find((s) => path === `/${s.key}` || path.startsWith(`/${s.key}/`));
1918
+ const page = screen ? void 0 : site.pages.find((p) => p.path === path);
1884
1919
  if (!page && !screen) throw new MemberPreviewError("This member page is unavailable in the selected site. Select a page at /_members.", 404);
1885
1920
  if (screen && !this.screenAssets.screens.includes(screen.key)) throw new MemberPreviewError("This system screen has no packaged sample preview. Rebuild or update the CLI.", 501);
1886
1921
  const html = renderMemberHtml(parseMemberRender(await this.api.post(`/api/v202604/themes/${this.theme.id}/member-preview`, {
@@ -1945,6 +1980,89 @@ var SSEStream = class {
1945
1980
  }
1946
1981
  };
1947
1982
  //#endregion
1983
+ //#region src/theme/dev-server/template-overrides.ts
1984
+ const TEMPLATE_OVERRIDE_PARAM = "theme_template_id";
1985
+ /**
1986
+ * Maps an explicit `theme_template_id` onto the dev theme the server renders.
1987
+ *
1988
+ * Mist, `theme navigate` and bookmarked URLs carry template ids from the
1989
+ * theme the project was pulled from. Forwarded unchanged next to
1990
+ * `x-fluid-theme: <dev theme>`, the storefront renders that original
1991
+ * template and the local edits never show. Resolve such an id to the dev
1992
+ * theme's template with the same themeable type and name instead. Ids that
1993
+ * already belong to the dev theme and global and extension templates (no
1994
+ * owning theme) pass through untouched. A failed lookup also passes through,
1995
+ * with a warning, since the page may then show the original template.
1996
+ */
1997
+ function createTemplateOverrideResolver(api, devThemeId) {
1998
+ const cache = /* @__PURE__ */ new Map();
1999
+ const warnLookupFailed = (requestedId, error) => {
2000
+ const message = error instanceof Error ? error.message : String(error);
2001
+ console.warn(`[Templates] Could not map template #${requestedId} to dev theme #${devThemeId} (${message}); rendering it unchanged, which may show the original theme's template.`);
2002
+ };
2003
+ /** `null` means the lookup failed: forward the id as-is, uncached. */
2004
+ const lookup = async (requestedId) => {
2005
+ let requested;
2006
+ try {
2007
+ requested = (await api.get(`/api/application_theme_templates/${encodeURIComponent(requestedId)}`)).application_theme_template;
2008
+ } catch (error) {
2009
+ warnLookupFailed(requestedId, error);
2010
+ return null;
2011
+ }
2012
+ if (!requested) return null;
2013
+ const { name, themeable_type: themeableType } = requested;
2014
+ const sourceThemeId = requested.application_theme_id;
2015
+ if (!sourceThemeId || sourceThemeId === devThemeId) return {
2016
+ kind: "use",
2017
+ id: requestedId
2018
+ };
2019
+ const params = new URLSearchParams({
2020
+ application_theme_id: String(devThemeId),
2021
+ themeable_type: themeableType
2022
+ });
2023
+ let candidates;
2024
+ try {
2025
+ candidates = (await api.get(`/api/application_theme_templates?${params}`)).templates ?? [];
2026
+ } catch (error) {
2027
+ warnLookupFailed(requestedId, error);
2028
+ return null;
2029
+ }
2030
+ const matches = candidates.filter((t) => t.application_theme_id === devThemeId && t.themeable_type === themeableType && t.name === name);
2031
+ const [counterpart] = matches;
2032
+ if (matches.length === 1 && counterpart) return {
2033
+ kind: "use",
2034
+ id: String(counterpart.id)
2035
+ };
2036
+ return {
2037
+ kind: "unresolved",
2038
+ reason: matches.length === 0 ? "missing" : "ambiguous",
2039
+ requestedId,
2040
+ themeableType,
2041
+ name,
2042
+ candidateIds: matches.map((t) => t.id)
2043
+ };
2044
+ };
2045
+ return {
2046
+ async resolve(requestedId) {
2047
+ const cached = cache.get(requestedId);
2048
+ if (cached) return {
2049
+ kind: "use",
2050
+ id: cached
2051
+ };
2052
+ const result = await lookup(requestedId);
2053
+ if (!result) return {
2054
+ kind: "use",
2055
+ id: requestedId
2056
+ };
2057
+ if (result.kind === "use") cache.set(requestedId, result.id);
2058
+ return result;
2059
+ },
2060
+ clear() {
2061
+ cache.clear();
2062
+ }
2063
+ };
2064
+ }
2065
+ //#endregion
1948
2066
  //#region src/theme/dev-server/proxy.ts
1949
2067
  const HOP_BY_HOP = new Set([
1950
2068
  "connection",
@@ -1968,6 +2086,20 @@ async function proxyRequest(req, res, opts) {
1968
2086
  const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
1969
2087
  url.searchParams.set("_fd", "0");
1970
2088
  url.searchParams.set("pb", "0");
2089
+ const requestedTemplateId = url.searchParams.get(TEMPLATE_OVERRIDE_PARAM);
2090
+ if (requestedTemplateId && opts.templateOverrides) {
2091
+ const override = await opts.templateOverrides.resolve(requestedTemplateId);
2092
+ if (override.kind === "use") url.searchParams.set(TEMPLATE_OVERRIDE_PARAM, override.id);
2093
+ else if (isPageRequest(req)) {
2094
+ const html = injectHotReload(unresolvedTemplatePage(override, opts.themeId), opts.reloadMode);
2095
+ res.writeHead(404, {
2096
+ "content-type": "text/html; charset=utf-8",
2097
+ "content-length": String(Buffer.byteLength(html))
2098
+ });
2099
+ res.end(html);
2100
+ return;
2101
+ }
2102
+ }
1971
2103
  const pending = opts.pendingFiles?.() ?? [];
1972
2104
  const isGet = req.method === "GET" || req.method === "HEAD";
1973
2105
  let method = req.method ?? "GET";
@@ -2022,6 +2154,26 @@ async function proxyRequest(req, res, opts) {
2022
2154
  proxyReq.end();
2023
2155
  });
2024
2156
  }
2157
+ const PAGE_FETCH_DESTS = new Set([
2158
+ "document",
2159
+ "iframe",
2160
+ "frame"
2161
+ ]);
2162
+ /** Browser navigations to a storefront page, as opposed to API or asset calls. */
2163
+ function isPageRequest(req) {
2164
+ if (req.method !== "GET" && req.method !== "HEAD") return false;
2165
+ if (((req.url ?? "/").split("?")[0] ?? "/").startsWith("/api/")) return false;
2166
+ const dest = req.headers["sec-fetch-dest"];
2167
+ if (typeof dest === "string") return PAGE_FETCH_DESTS.has(dest);
2168
+ const accept = req.headers["accept"];
2169
+ return typeof accept === "string" && accept.includes("text/html");
2170
+ }
2171
+ function unresolvedTemplatePage(override, devThemeId) {
2172
+ return `<!doctype html><title>Template not in dev theme</title><p>${`Template #${escapeHtml(override.requestedId)} (${escapeHtml(override.themeableType)} / ${escapeHtml(override.name)})`} belongs to another theme, and ${override.reason === "missing" ? `dev theme #${devThemeId} has no template with that type and name. Sync or create it locally, or` : `dev theme #${devThemeId} has several templates with that type and name (#${override.candidateIds.join(", #")}), so the edited copy is ambiguous. Remove the duplicates, or`} remove <code>${TEMPLATE_OVERRIDE_PARAM}</code> to use the default.</p>`;
2173
+ }
2174
+ function escapeHtml(value) {
2175
+ return value.replace(/[&<>"']/g, (c) => `&#${c.charCodeAt(0)};`);
2176
+ }
2025
2177
  function readBody(req) {
2026
2178
  return new Promise((resolve, reject) => {
2027
2179
  const chunks = [];
@@ -2346,12 +2498,13 @@ function isStylesheetKey(key) {
2346
2498
  //#endregion
2347
2499
  //#region src/theme/member-dependencies.ts
2348
2500
  /** The same file graph is used for uploads and incoming omission protection.
2349
- * Rails remains responsible for resolving company-specific roles and handles. */
2501
+ * Rails remains responsible for resolving company-specific roles and handles.
2502
+ * Retired template types are never synced, so they are never dependencies. */
2350
2503
  function memberDependencyPaths(document, availablePaths, deletedPaths = [], changedPaths = availablePaths) {
2351
2504
  const dependencies = /* @__PURE__ */ new Set();
2352
- for (const entry of [...records(document.templates), ...records(document.menu_references)]) if (typeof entry.path === "string") dependencies.add(entry.path);
2353
- for (const site of records(document.sites)) {
2354
- for (const page of records(site.pages)) if (typeof page.template_path === "string") dependencies.add(page.template_path);
2505
+ for (const entry of [...records$1(document.templates), ...records$1(document.menu_references)]) if (typeof entry.path === "string") dependencies.add(entry.path);
2506
+ for (const site of records$1(document.sites)) {
2507
+ for (const page of records$1(site.pages)) if (typeof page.template_path === "string") dependencies.add(page.template_path);
2355
2508
  const layout = site.layout_name;
2356
2509
  if (layout === "none") continue;
2357
2510
  if (typeof layout === "string" && layout && !["member", "theme"].includes(layout)) {
@@ -2372,9 +2525,10 @@ function memberDependencyPaths(document, availablePaths, deletedPaths = [], chan
2372
2525
  if ((suffix === "styles.css" ? changedPaths : availablePaths).has(sibling) || deletedPaths.includes(sibling)) dependencies.add(sibling);
2373
2526
  }
2374
2527
  }
2528
+ for (const path of dependencies) if (isRetiredTemplateKey(path)) dependencies.delete(path);
2375
2529
  return dependencies;
2376
2530
  }
2377
- function records(entries) {
2531
+ function records$1(entries) {
2378
2532
  return (Array.isArray(entries) ? entries : []).filter((entry) => typeof entry === "object" && entry !== null && !Array.isArray(entry));
2379
2533
  }
2380
2534
  //#endregion
@@ -2385,19 +2539,27 @@ function planThemeUploads(themeRoot, changedFiles, deletedPaths = []) {
2385
2539
  const localFiles = themeRoot.files();
2386
2540
  const manifest = localFiles.find((file) => file.relativePath === MEMBER_CONFIGURATION_KEY);
2387
2541
  if (!manifest || themeRoot.ignore.ignore("members_sites.json")) return individualUploads(changedFiles);
2388
- const content = manifest.read();
2389
- const document = JSON.parse(content);
2390
- if (Array.isArray(document)) return individualUploads(changedFiles);
2391
- if (!isRecord$3(document)) throw new Error("members_sites.json must contain a JSON object.");
2542
+ const manifestContent = manifest.read();
2543
+ const parsed = JSON.parse(manifestContent);
2544
+ if (Array.isArray(parsed)) return individualUploads(changedFiles);
2545
+ if (!isRecord$3(parsed)) throw new Error("members_sites.json must contain a JSON object.");
2546
+ const { document, removed } = withoutRetiredTemplates(parsed);
2547
+ const content = removed.length > 0 ? `${JSON.stringify(document, null, 2)}\n` : manifestContent;
2392
2548
  const dependencies = memberDependencyPaths(document, new Set(localFiles.map((file) => file.relativePath)), deletedPaths, new Set(changedFiles.map((file) => file.relativePath)));
2393
2549
  for (const path of deletedPaths) if (dependencies.has(path)) throw new Error(`Member dependency is still declared and cannot be deleted: ${path}`);
2394
- if (!changedFiles.some((file) => file.relativePath === "members_sites.json" || dependencies.has(file.relativePath))) return changedFiles.map((file) => ({ file }));
2550
+ const memberChanged = changedFiles.some((file) => file.relativePath === "members_sites.json" || dependencies.has(file.relativePath));
2551
+ if (!memberChanged && removed.length === 0) return changedFiles.map((file) => ({ file }));
2395
2552
  const availableFiles = new Map(localFiles.map((file) => [file.relativePath, file]));
2396
- const memberFiles = [...dependencies].map((path) => {
2553
+ const unavailable = [...dependencies].find((path) => {
2397
2554
  const file = availableFiles.get(path);
2398
- if (!file || !file.isText || themeRoot.ignore.ignore(path)) throw new Error(`Member dependency must be an available text file: ${path}`);
2399
- return file;
2555
+ return !file || !file.isText || themeRoot.ignore.ignore(path);
2400
2556
  });
2557
+ if (unavailable) {
2558
+ if (!memberChanged) return changedFiles.map((file) => ({ file }));
2559
+ throw new Error(`Member dependency must be an available text file: ${unavailable}`);
2560
+ }
2561
+ const memberFiles = [...dependencies].map((path) => availableFiles.get(path));
2562
+ if (removed.length > 0) manifest.write(content);
2401
2563
  const uploads = changedFiles.filter((file) => file.relativePath !== "members_sites.json" && !dependencies.has(file.relativePath)).map((file) => ({ file }));
2402
2564
  uploads.push({
2403
2565
  file: manifest,
@@ -2408,6 +2570,43 @@ function planThemeUploads(themeRoot, changedFiles, deletedPaths = []) {
2408
2570
  });
2409
2571
  return uploads;
2410
2572
  }
2573
+ /**
2574
+ * Drop references to retired template types. The server rejects the whole
2575
+ * document for one, which would also hold back every member site and menu it
2576
+ * carries.
2577
+ *
2578
+ * Only `templates` and `menu_references` can name one: a site page's template
2579
+ * must be a `members_page`. A menu left without any reference is dropped with
2580
+ * its reference, because the server rejects unreferenced menus too.
2581
+ */
2582
+ function withoutRetiredTemplates(document) {
2583
+ const removed = /* @__PURE__ */ new Set();
2584
+ const orphanCandidates = /* @__PURE__ */ new Set();
2585
+ const keep = (entry) => {
2586
+ if (!isRecord$3(entry) || typeof entry.path !== "string") return true;
2587
+ if (!isRetiredTemplateKey(entry.path)) return true;
2588
+ removed.add(entry.path);
2589
+ if ("menu_key" in entry) orphanCandidates.add(entry.menu_key);
2590
+ return false;
2591
+ };
2592
+ const next = { ...document };
2593
+ for (const key of ["templates", "menu_references"]) if (Array.isArray(next[key])) next[key] = next[key].filter(keep);
2594
+ if (removed.size === 0) return {
2595
+ document,
2596
+ removed: []
2597
+ };
2598
+ if (orphanCandidates.size > 0 && Array.isArray(next.menus)) {
2599
+ const used = new Set([...records(next.sites).map((site) => site.menu_key), ...records(next.menu_references).map((entry) => entry.menu_key)]);
2600
+ next.menus = next.menus.filter((menu) => !isRecord$3(menu) || !orphanCandidates.has(menu.key) || used.has(menu.key));
2601
+ }
2602
+ return {
2603
+ document: next,
2604
+ removed: [...removed]
2605
+ };
2606
+ }
2607
+ function records(entries) {
2608
+ return Array.isArray(entries) ? entries.filter(isRecord$3) : [];
2609
+ }
2411
2610
  function individualUploads(files) {
2412
2611
  return [...files].sort((left, right) => Number(left.relativePath === MEMBER_CONFIGURATION_KEY) - Number(right.relativePath === MEMBER_CONFIGURATION_KEY)).map((file) => ({ file }));
2413
2612
  }
@@ -3193,7 +3392,7 @@ var Syncer = class {
3193
3392
  if (opts.remoteState) this.useDevRemoteState(opts.remoteState);
3194
3393
  else await this.fetchChecksums();
3195
3394
  if (!opts.skipPreflight) await this.preflightPush(opts.baseSha);
3196
- let baseSha = opts.baseSha ?? null;
3395
+ let baseSha = opts.baseSha ?? (opts.guardFetchedRemote && !opts.remoteState ? this.remoteSha() : null);
3197
3396
  const localPaths = new Set(localFiles.map((file) => file.relativePath));
3198
3397
  if (opts.delete) for (const key of this.assetManifest.keys()) localPaths.add(key);
3199
3398
  const toDelete = opts.delete ? this.remoteKeys().filter((key) => key !== "members_sites.json" && this.canDeleteRemoteResource(key) && !localPaths.has(key) && !this.themeRoot.ignore.ignore(key)) : [];
@@ -3582,6 +3781,7 @@ async function startDevServer(api, theme, themeRoot, opts, onReady) {
3582
3781
  const sse = new SSEStream();
3583
3782
  const memberPreview = new MemberPreview(api, theme, opts.reloadMode);
3584
3783
  const syncer = new Syncer(api, theme.id, themeRoot);
3784
+ const templateOverrides = createTemplateOverrideResolver(api, theme.id);
3585
3785
  let remoteStateSafe = true;
3586
3786
  const invalidateRemoteState = () => {
3587
3787
  if (!remoteStateSafe) return;
@@ -3767,6 +3967,7 @@ async function startDevServer(api, theme, themeRoot, opts, onReady) {
3767
3967
  console.error("[Member preview] Could not refresh routes after sync. The next member request will retry.");
3768
3968
  }
3769
3969
  }
3970
+ if (added.length > 0 || removed.length > 0) templateOverrides.clear();
3770
3971
  if (removed.length > 0) sse.broadcast(JSON.stringify({ reload_page: true }));
3771
3972
  else if (uploads.length > 0) sse.broadcast(JSON.stringify({ modified: uploads.flatMap(({ file, memberConfiguration }) => [file, ...memberConfiguration?.files ?? []].map((updatedFile) => updatedFile.relativePath)) }));
3772
3973
  scheduleSync();
@@ -3787,6 +3988,7 @@ async function startDevServer(api, theme, themeRoot, opts, onReady) {
3787
3988
  company: theme.company,
3788
3989
  themeId: theme.id,
3789
3990
  reloadMode: opts.reloadMode,
3991
+ templateOverrides,
3790
3992
  pendingFiles: () => [...pendingUpdates].map((p) => themeRoot.file(p)).filter((f) => f.isText).map((f) => ({
3791
3993
  relativePath: f.relativePath,
3792
3994
  read: () => f.read()
@@ -4627,6 +4829,78 @@ function createDevCommand() {
4627
4829
  });
4628
4830
  }
4629
4831
  //#endregion
4832
+ //#region src/theme/development-push.ts
4833
+ /**
4834
+ * Push a project to a development theme without touching the project's own
4835
+ * sync state.
4836
+ *
4837
+ * A project directory keeps one shadow repo and one `.fluid-theme.json`, both
4838
+ * for the theme it is bound to. Opening the shadow repo for another theme
4839
+ * wipes it, so an ordinary push here would cost the bound theme its baseline:
4840
+ * the next Publish could no longer detect deletions or stale remotes. This
4841
+ * syncs the way `fluid theme dev` does instead: against the development
4842
+ * theme's own remote index and `.fluid-theme/dev-baseline.json`, which it
4843
+ * keeps current for the next dev session.
4844
+ */
4845
+ async function pushToDevelopmentTheme(api, themeId, themeRoot, opts) {
4846
+ const syncer = new Syncer(api, themeId, themeRoot);
4847
+ const fingerprint = () => new ThemeAssetManifest(themeRoot.root).fingerprint();
4848
+ const common = {
4849
+ delete: opts.delete,
4850
+ validate: opts.validate,
4851
+ linkManagedAssets: { replace: true },
4852
+ pendingBinaryAssets: true,
4853
+ ...opts.onProgress ? { onProgress: opts.onProgress } : {}
4854
+ };
4855
+ const saved = readDevRemoteBaseline(themeRoot.root, themeId, fingerprint());
4856
+ let trusted = false;
4857
+ if (saved) try {
4858
+ syncer.useDevRemoteState(saved);
4859
+ await syncer.preflightPush(saved.remoteSha);
4860
+ trusted = true;
4861
+ } catch (error) {
4862
+ if (!(error instanceof PushConflictError)) throw error;
4863
+ }
4864
+ let result;
4865
+ try {
4866
+ result = trusted && saved ? await syncer.uploadTheme({
4867
+ ...common,
4868
+ baseSha: syncer.remoteSha(),
4869
+ remoteState: saved,
4870
+ skipPreflight: true
4871
+ }) : await syncer.uploadTheme({
4872
+ ...common,
4873
+ guardFetchedRemote: true
4874
+ });
4875
+ } catch (error) {
4876
+ if (!(error instanceof PushConflictError)) throw error;
4877
+ removeDevRemoteBaseline(themeRoot.root);
4878
+ return {
4879
+ uploaded: 0,
4880
+ deleted: 0,
4881
+ linked: 0,
4882
+ errors: ["The development theme changed during this push. Run the push again to compare against its current state."],
4883
+ validationFailed: false,
4884
+ conflict: true
4885
+ };
4886
+ }
4887
+ const errors = [...result.errors];
4888
+ if (result.validationFailed || errors.length > 0) removeDevRemoteBaseline(themeRoot.root);
4889
+ else if (!await syncer.requestSync()) errors.push("Fluid did not accept sync finalization. Retry the push to commit it.");
4890
+ else {
4891
+ const state = syncer.devRemoteState(fingerprint());
4892
+ if (state) writeDevRemoteBaseline(themeRoot.root, state);
4893
+ }
4894
+ return {
4895
+ uploaded: result.uploaded,
4896
+ deleted: result.deleted,
4897
+ linked: result.linked,
4898
+ errors,
4899
+ validationFailed: result.validationFailed,
4900
+ conflict: false
4901
+ };
4902
+ }
4903
+ //#endregion
4630
4904
  //#region src/theme/merge-push.ts
4631
4905
  /**
4632
4906
  * Compute what changed locally since the last time the shadow repo
@@ -4969,7 +5243,39 @@ function createPushCommand() {
4969
5243
  else if (config) {
4970
5244
  console.log(` Using theme from .fluid-theme.json: ${chalk.bold(config.themeName)} (#${config.themeId})`);
4971
5245
  theme = (await getApplicationTheme(api, config.themeId)).application_theme;
5246
+ if (theme.status === "development") console.warn(chalk.yellow(` ${theme.name} (#${theme.id}) is a development theme: a scratch copy made by \`fluid theme dev\`, not a theme your storefront serves. This push updates only that copy. To update the real theme, run \`fluid theme pull -t <theme-id>\` here to bind the project to it, or pass --theme.`));
4972
5247
  } else theme = await selectTheme(api, "Select a theme to push to");
5248
+ if (config && !mayBindTheme(config, theme)) {
5249
+ const own = getDevTheme(devThemeKey(config.company, themeRoot.root));
5250
+ if (own?.id !== theme.id) {
5251
+ console.error(`${theme.name} (#${theme.id}) is a development theme, but not this project's` + (own ? ` (that is #${own.id})` : "") + `. Pushing would overwrite it with this directory. Run \`fluid theme dev\` here to sync this project's own development theme, or \`fluid theme dev --theme ${theme.id}\` to sync this one deliberately.`);
5252
+ process.exit(1);
5253
+ }
5254
+ const unresolvedDev = findUnresolvedConflicts(themeRoot.files());
5255
+ if (unresolvedDev.length > 0) {
5256
+ console.error(`✗ ${unresolvedDev.length} file(s) still contain unresolved conflict markers: ${unresolvedDev.join(", ")}. Resolve them before pushing.`);
5257
+ process.exit(1);
5258
+ }
5259
+ if (opts.publish) {
5260
+ console.error(`${theme.name} (#${theme.id}) is a development theme; --publish would make that scratch copy the live theme. Push without --publish, or publish ${config.themeName} (#${config.themeId}).`);
5261
+ process.exit(1);
5262
+ }
5263
+ const devSpinner = ora(`Pushing to development theme ${theme.name} (#${theme.id})…`).start();
5264
+ const result = await pushToDevelopmentTheme(api, theme.id, themeRoot, {
5265
+ validate: !opts.force,
5266
+ delete: !opts.nodelete && !opts.autoBaseline,
5267
+ onProgress: (done, total) => {
5268
+ devSpinner.text = `Pushing ${done}/${total} files to ${theme.name} (#${theme.id})…`;
5269
+ }
5270
+ });
5271
+ if (result.validationFailed) devSpinner.fail(`Schema validation failed (${result.errors.length} error(s)). Use --force to skip.`);
5272
+ else if (result.errors.length > 0) devSpinner.warn(`Pushed with ${result.errors.length} error(s).`);
5273
+ else devSpinner.succeed(`Pushed ${result.uploaded} file(s), removed ${result.deleted}, to development theme #${theme.id}.`);
5274
+ for (const e of result.errors) console.error(` ${e}`);
5275
+ console.log(` This project stays bound to ${config.themeName} (#${config.themeId}); Publish still pushes there.`);
5276
+ if (result.errors.length > 0) process.exitCode = 1;
5277
+ return;
5278
+ }
4973
5279
  const shadow = await ShadowRepo.open(themeRoot.root, theme.id);
4974
5280
  await migrateLegacyChecksumsIntoShadow({
4975
5281
  shadow,
@@ -5546,6 +5852,14 @@ function createPullCommand() {
5546
5852
  else root = `.`;
5547
5853
  const absoluteRoot = resolve(root);
5548
5854
  const existingConfig = readThemeConfig(absoluteRoot);
5855
+ if (existingConfig && !mayBindTheme(existingConfig, theme)) {
5856
+ if (opts.onlyIfClean) {
5857
+ console.log(`Skipped: this directory is bound to ${existingConfig.themeName} (#${existingConfig.themeId}), not development theme #${theme.id}.`);
5858
+ process.exit(3);
5859
+ }
5860
+ console.error(developmentThemeRefusal(existingConfig, theme));
5861
+ process.exit(1);
5862
+ }
5549
5863
  const backgroundGuard = opts.onlyIfClean ? new BackgroundPullGuard(absoluteRoot) : void 0;
5550
5864
  console.log();
5551
5865
  console.log(` Theme: ${chalk.bold(theme.name)} (#${theme.id})`);