canopycms 0.0.61-int.75 → 0.0.61

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 (271) hide show
  1. package/dist/ai/handler.js +1 -1
  2. package/dist/ai/handler.js.map +1 -1
  3. package/dist/api/assets.d.ts.map +1 -1
  4. package/dist/api/assets.js +36 -4
  5. package/dist/api/assets.js.map +1 -1
  6. package/dist/api/branch-status.d.ts.map +1 -1
  7. package/dist/api/branch-status.js +56 -0
  8. package/dist/api/branch-status.js.map +1 -1
  9. package/dist/api/branch-withdraw.d.ts.map +1 -1
  10. package/dist/api/branch-withdraw.js +14 -3
  11. package/dist/api/branch-withdraw.js.map +1 -1
  12. package/dist/api/branch.d.ts +31 -2
  13. package/dist/api/branch.d.ts.map +1 -1
  14. package/dist/api/branch.js +252 -13
  15. package/dist/api/branch.js.map +1 -1
  16. package/dist/api/content.d.ts.map +1 -1
  17. package/dist/api/content.js +12 -4
  18. package/dist/api/content.js.map +1 -1
  19. package/dist/api/entries.d.ts.map +1 -1
  20. package/dist/api/entries.js +11 -9
  21. package/dist/api/entries.js.map +1 -1
  22. package/dist/api/guards.d.ts.map +1 -1
  23. package/dist/api/guards.js +12 -5
  24. package/dist/api/guards.js.map +1 -1
  25. package/dist/api/permissions.d.ts +2 -2
  26. package/dist/api/permissions.d.ts.map +1 -1
  27. package/dist/api/permissions.js +51 -3
  28. package/dist/api/permissions.js.map +1 -1
  29. package/dist/api/reference-options.d.ts.map +1 -1
  30. package/dist/api/reference-options.js +10 -17
  31. package/dist/api/reference-options.js.map +1 -1
  32. package/dist/api/resolve-references.d.ts.map +1 -1
  33. package/dist/api/resolve-references.js +3 -1
  34. package/dist/api/resolve-references.js.map +1 -1
  35. package/dist/api/schema.d.ts.map +1 -1
  36. package/dist/api/schema.js +3 -2
  37. package/dist/api/schema.js.map +1 -1
  38. package/dist/api/settings-helpers.d.ts.map +1 -1
  39. package/dist/api/settings-helpers.js +6 -5
  40. package/dist/api/settings-helpers.js.map +1 -1
  41. package/dist/assets/pipeline.d.ts +11 -2
  42. package/dist/assets/pipeline.d.ts.map +1 -1
  43. package/dist/assets/pipeline.js +114 -2
  44. package/dist/assets/pipeline.js.map +1 -1
  45. package/dist/assets/transform-directives.d.ts +18 -0
  46. package/dist/assets/transform-directives.d.ts.map +1 -1
  47. package/dist/assets/transform-directives.js +18 -0
  48. package/dist/assets/transform-directives.js.map +1 -1
  49. package/dist/assets/transform.d.ts.map +1 -1
  50. package/dist/assets/transform.js +1 -10
  51. package/dist/assets/transform.js.map +1 -1
  52. package/dist/auth/types.d.ts +29 -0
  53. package/dist/auth/types.d.ts.map +1 -1
  54. package/dist/authorization/index.d.ts +1 -1
  55. package/dist/authorization/index.d.ts.map +1 -1
  56. package/dist/authorization/index.js +1 -1
  57. package/dist/authorization/index.js.map +1 -1
  58. package/dist/authorization/protected-branch.d.ts +75 -0
  59. package/dist/authorization/protected-branch.d.ts.map +1 -1
  60. package/dist/authorization/protected-branch.js +39 -0
  61. package/dist/authorization/protected-branch.js.map +1 -1
  62. package/dist/branch-registry.d.ts.map +1 -1
  63. package/dist/branch-registry.js +5 -1
  64. package/dist/branch-registry.js.map +1 -1
  65. package/dist/branch-schema-cache.d.ts.map +1 -1
  66. package/dist/branch-schema-cache.js +9 -0
  67. package/dist/branch-schema-cache.js.map +1 -1
  68. package/dist/build/generate-ai-content.js +1 -1
  69. package/dist/build/generate-ai-content.js.map +1 -1
  70. package/dist/cli/cli.js +1442 -828
  71. package/dist/cli/generate-ai-content.js +404 -136
  72. package/dist/cli/init.d.ts +8 -2
  73. package/dist/cli/init.d.ts.map +1 -1
  74. package/dist/cli/init.js +440 -151
  75. package/dist/cli/init.js.map +1 -1
  76. package/dist/cli/project-detect.d.ts +89 -0
  77. package/dist/cli/project-detect.d.ts.map +1 -0
  78. package/dist/cli/project-detect.js +240 -0
  79. package/dist/cli/project-detect.js.map +1 -0
  80. package/dist/cli/template-files/Dockerfile.cms.template +11 -6
  81. package/dist/cli/template-files/cdk-app.ts.template +82 -0
  82. package/dist/cli/template-files/cdk.json.template +9 -0
  83. package/dist/cli/template-files/cms-stack.ts.template +124 -0
  84. package/dist/cli/template-files/deploy-cms.yml.template +125 -13
  85. package/dist/cli/template-files/dockerignore.template +2 -1
  86. package/dist/cli/templates.d.ts +26 -2
  87. package/dist/cli/templates.d.ts.map +1 -1
  88. package/dist/cli/templates.js +36 -4
  89. package/dist/cli/templates.js.map +1 -1
  90. package/dist/config/schemas/config.d.ts +2 -3
  91. package/dist/config/schemas/config.d.ts.map +1 -1
  92. package/dist/config/schemas/config.js +21 -1
  93. package/dist/config/schemas/config.js.map +1 -1
  94. package/dist/config/validation.d.ts +9 -0
  95. package/dist/config/validation.d.ts.map +1 -1
  96. package/dist/config/validation.js +22 -12
  97. package/dist/config/validation.js.map +1 -1
  98. package/dist/content-reader.js +1 -1
  99. package/dist/content-reader.js.map +1 -1
  100. package/dist/content-store.d.ts +11 -0
  101. package/dist/content-store.d.ts.map +1 -1
  102. package/dist/content-store.js +2 -1
  103. package/dist/content-store.js.map +1 -1
  104. package/dist/dev-content-watcher.d.ts.map +1 -1
  105. package/dist/dev-content-watcher.js +4 -1
  106. package/dist/dev-content-watcher.js.map +1 -1
  107. package/dist/editor/BranchManager.d.ts +15 -0
  108. package/dist/editor/BranchManager.d.ts.map +1 -1
  109. package/dist/editor/BranchManager.js +43 -9
  110. package/dist/editor/BranchManager.js.map +1 -1
  111. package/dist/editor/CanopyEditor.d.ts.map +1 -1
  112. package/dist/editor/CanopyEditor.js +2 -2
  113. package/dist/editor/CanopyEditor.js.map +1 -1
  114. package/dist/editor/Editor.d.ts.map +1 -1
  115. package/dist/editor/Editor.js +110 -48
  116. package/dist/editor/Editor.js.map +1 -1
  117. package/dist/editor/admin/SystemHealthPanel.d.ts.map +1 -1
  118. package/dist/editor/admin/SystemHealthPanel.js +9 -8
  119. package/dist/editor/admin/SystemHealthPanel.js.map +1 -1
  120. package/dist/editor/components/EditorHeader.d.ts +6 -0
  121. package/dist/editor/components/EditorHeader.d.ts.map +1 -1
  122. package/dist/editor/components/EditorHeader.js +76 -23
  123. package/dist/editor/components/EditorHeader.js.map +1 -1
  124. package/dist/editor/components/EntryCreateModal.d.ts.map +1 -1
  125. package/dist/editor/components/EntryCreateModal.js +21 -6
  126. package/dist/editor/components/EntryCreateModal.js.map +1 -1
  127. package/dist/editor/context/SWRProvider.d.ts +31 -0
  128. package/dist/editor/context/SWRProvider.d.ts.map +1 -0
  129. package/dist/editor/context/SWRProvider.js +12 -0
  130. package/dist/editor/context/SWRProvider.js.map +1 -0
  131. package/dist/editor/context/index.d.ts +3 -0
  132. package/dist/editor/context/index.d.ts.map +1 -1
  133. package/dist/editor/context/index.js +3 -0
  134. package/dist/editor/context/index.js.map +1 -1
  135. package/dist/editor/editor-utils.d.ts.map +1 -1
  136. package/dist/editor/editor-utils.js +8 -2
  137. package/dist/editor/editor-utils.js.map +1 -1
  138. package/dist/editor/fields/ImageField.d.ts.map +1 -1
  139. package/dist/editor/fields/ImageField.js +27 -3
  140. package/dist/editor/fields/ImageField.js.map +1 -1
  141. package/dist/editor/fields/ReferenceField.d.ts.map +1 -1
  142. package/dist/editor/fields/ReferenceField.js +52 -31
  143. package/dist/editor/fields/ReferenceField.js.map +1 -1
  144. package/dist/editor/hooks/__test__/test-utils.d.ts +38 -3
  145. package/dist/editor/hooks/__test__/test-utils.d.ts.map +1 -1
  146. package/dist/editor/hooks/__test__/test-utils.js +50 -4
  147. package/dist/editor/hooks/__test__/test-utils.js.map +1 -1
  148. package/dist/editor/hooks/index.d.ts +4 -1
  149. package/dist/editor/hooks/index.d.ts.map +1 -1
  150. package/dist/editor/hooks/index.js +4 -1
  151. package/dist/editor/hooks/index.js.map +1 -1
  152. package/dist/editor/hooks/useBranchManager.d.ts +21 -1
  153. package/dist/editor/hooks/useBranchManager.d.ts.map +1 -1
  154. package/dist/editor/hooks/useBranchManager.js +68 -32
  155. package/dist/editor/hooks/useBranchManager.js.map +1 -1
  156. package/dist/editor/hooks/useBranchesData.d.ts +57 -0
  157. package/dist/editor/hooks/useBranchesData.d.ts.map +1 -0
  158. package/dist/editor/hooks/useBranchesData.js +69 -0
  159. package/dist/editor/hooks/useBranchesData.js.map +1 -0
  160. package/dist/editor/hooks/useCommentSystem.d.ts.map +1 -1
  161. package/dist/editor/hooks/useCommentSystem.js +24 -22
  162. package/dist/editor/hooks/useCommentSystem.js.map +1 -1
  163. package/dist/editor/hooks/useCommentsData.d.ts +29 -0
  164. package/dist/editor/hooks/useCommentsData.d.ts.map +1 -0
  165. package/dist/editor/hooks/useCommentsData.js +40 -0
  166. package/dist/editor/hooks/useCommentsData.js.map +1 -0
  167. package/dist/editor/hooks/useDraftManager.d.ts.map +1 -1
  168. package/dist/editor/hooks/useDraftManager.js +32 -18
  169. package/dist/editor/hooks/useDraftManager.js.map +1 -1
  170. package/dist/editor/hooks/useEntriesData.d.ts +35 -0
  171. package/dist/editor/hooks/useEntriesData.d.ts.map +1 -0
  172. package/dist/editor/hooks/useEntriesData.js +96 -0
  173. package/dist/editor/hooks/useEntriesData.js.map +1 -0
  174. package/dist/editor/hooks/useEntryManager.d.ts +8 -15
  175. package/dist/editor/hooks/useEntryManager.d.ts.map +1 -1
  176. package/dist/editor/hooks/useEntryManager.js +259 -117
  177. package/dist/editor/hooks/useEntryManager.js.map +1 -1
  178. package/dist/editor/hooks/usePermissionManager.d.ts +2 -2
  179. package/dist/editor/hooks/usePermissionManager.d.ts.map +1 -1
  180. package/dist/editor/hooks/usePermissionManager.js +11 -9
  181. package/dist/editor/hooks/usePermissionManager.js.map +1 -1
  182. package/dist/editor/hooks/useReferenceResolution.d.ts.map +1 -1
  183. package/dist/editor/hooks/useReferenceResolution.js +24 -1
  184. package/dist/editor/hooks/useReferenceResolution.js.map +1 -1
  185. package/dist/editor/media/MediaLibraryBody.d.ts.map +1 -1
  186. package/dist/editor/media/MediaLibraryBody.js +12 -2
  187. package/dist/editor/media/MediaLibraryBody.js.map +1 -1
  188. package/dist/editor/permission-manager/GroupSelector.d.ts.map +1 -1
  189. package/dist/editor/permission-manager/GroupSelector.js +7 -2
  190. package/dist/editor/permission-manager/GroupSelector.js.map +1 -1
  191. package/dist/editor/permission-manager/hooks/useGroupsAndUsers.d.ts +3 -3
  192. package/dist/editor/permission-manager/hooks/useGroupsAndUsers.d.ts.map +1 -1
  193. package/dist/editor/permission-manager/hooks/useGroupsAndUsers.js +1 -0
  194. package/dist/editor/permission-manager/hooks/useGroupsAndUsers.js.map +1 -1
  195. package/dist/editor/permission-manager/types.d.ts +6 -4
  196. package/dist/editor/permission-manager/types.d.ts.map +1 -1
  197. package/dist/editor/test-setup.js +8 -0
  198. package/dist/editor/test-setup.js.map +1 -1
  199. package/dist/git-manager.d.ts +126 -9
  200. package/dist/git-manager.d.ts.map +1 -1
  201. package/dist/git-manager.js +251 -27
  202. package/dist/git-manager.js.map +1 -1
  203. package/dist/github-service.d.ts.map +1 -1
  204. package/dist/github-service.js +13 -7
  205. package/dist/github-service.js.map +1 -1
  206. package/dist/http/handler.d.ts.map +1 -1
  207. package/dist/http/handler.js +8 -2
  208. package/dist/http/handler.js.map +1 -1
  209. package/dist/operating-mode/client-unsafe-strategy.d.ts.map +1 -1
  210. package/dist/operating-mode/client-unsafe-strategy.js +7 -8
  211. package/dist/operating-mode/client-unsafe-strategy.js.map +1 -1
  212. package/dist/operating-mode/deployment-name.d.ts +28 -0
  213. package/dist/operating-mode/deployment-name.d.ts.map +1 -0
  214. package/dist/operating-mode/deployment-name.js +78 -0
  215. package/dist/operating-mode/deployment-name.js.map +1 -0
  216. package/dist/operating-mode/index.d.ts +5 -1
  217. package/dist/operating-mode/index.d.ts.map +1 -1
  218. package/dist/operating-mode/index.js +7 -1
  219. package/dist/operating-mode/index.js.map +1 -1
  220. package/dist/operating-mode/types.d.ts +13 -3
  221. package/dist/operating-mode/types.d.ts.map +1 -1
  222. package/dist/paths/branch-name.d.ts +38 -0
  223. package/dist/paths/branch-name.d.ts.map +1 -1
  224. package/dist/paths/branch-name.js +46 -0
  225. package/dist/paths/branch-name.js.map +1 -1
  226. package/dist/paths/index.d.ts +1 -1
  227. package/dist/paths/index.d.ts.map +1 -1
  228. package/dist/paths/index.js +1 -1
  229. package/dist/paths/index.js.map +1 -1
  230. package/dist/reference-resolver.d.ts +6 -2
  231. package/dist/reference-resolver.d.ts.map +1 -1
  232. package/dist/reference-resolver.js +8 -1
  233. package/dist/reference-resolver.js.map +1 -1
  234. package/dist/schema/meta-loader.d.ts.map +1 -1
  235. package/dist/schema/meta-loader.js +10 -2
  236. package/dist/schema/meta-loader.js.map +1 -1
  237. package/dist/schema/schema-store.d.ts +11 -2
  238. package/dist/schema/schema-store.d.ts.map +1 -1
  239. package/dist/schema/schema-store.js +23 -11
  240. package/dist/schema/schema-store.js.map +1 -1
  241. package/dist/settings-workspace.d.ts +0 -15
  242. package/dist/settings-workspace.d.ts.map +1 -1
  243. package/dist/settings-workspace.js +97 -1
  244. package/dist/settings-workspace.js.map +1 -1
  245. package/dist/types.d.ts +63 -2
  246. package/dist/types.d.ts.map +1 -1
  247. package/dist/utils/debug.d.ts +11 -0
  248. package/dist/utils/debug.d.ts.map +1 -1
  249. package/dist/utils/debug.js +12 -1
  250. package/dist/utils/debug.js.map +1 -1
  251. package/dist/utils/git.d.ts +38 -0
  252. package/dist/utils/git.d.ts.map +1 -1
  253. package/dist/utils/git.js +69 -0
  254. package/dist/utils/git.js.map +1 -1
  255. package/dist/utils/logger.d.ts +71 -0
  256. package/dist/utils/logger.d.ts.map +1 -0
  257. package/dist/utils/logger.js +85 -0
  258. package/dist/utils/logger.js.map +1 -0
  259. package/dist/validation/entry-type-reference-validator.d.ts +33 -0
  260. package/dist/validation/entry-type-reference-validator.d.ts.map +1 -0
  261. package/dist/validation/entry-type-reference-validator.js +110 -0
  262. package/dist/validation/entry-type-reference-validator.js.map +1 -0
  263. package/dist/worker/cms-worker.d.ts +232 -3
  264. package/dist/worker/cms-worker.d.ts.map +1 -1
  265. package/dist/worker/cms-worker.js +856 -87
  266. package/dist/worker/cms-worker.js.map +1 -1
  267. package/dist/worker/log.d.ts +61 -0
  268. package/dist/worker/log.d.ts.map +1 -0
  269. package/dist/worker/log.js +74 -0
  270. package/dist/worker/log.js.map +1 -0
  271. package/package.json +3 -1
package/dist/cli/cli.js CHANGED
@@ -95,22 +95,50 @@ import { z as z4 } from "zod";
95
95
  import { z as z5 } from "zod";
96
96
  import { join, normalize } from "pathe";
97
97
  import fs2 from "node:fs/promises";
98
+ import fs22 from "node:fs/promises";
98
99
  import path2 from "node:path";
99
100
  import { fileURLToPath } from "node:url";
100
- import path3 from "node:path";
101
101
  import fs3 from "node:fs/promises";
102
+ import path3 from "node:path";
103
+ import { simpleGit } from "simple-git";
102
104
  import path4 from "node:path";
103
105
  import fs4 from "node:fs/promises";
104
106
  import path5 from "node:path";
105
- import crypto from "node:crypto";
106
107
  import fs5 from "node:fs/promises";
107
108
  import path6 from "node:path";
109
+ import crypto from "node:crypto";
110
+ import fs6 from "node:fs/promises";
111
+ import path7 from "node:path";
108
112
  import * as p from "@clack/prompts";
109
113
  import { createJiti } from "jiti";
110
- import path7 from "node:path";
111
- import fs6 from "node:fs/promises";
114
+ import path8 from "node:path";
115
+ function getErrorMessage2(err) {
116
+ if (err instanceof Error) {
117
+ return err.message;
118
+ }
119
+ if (typeof err === "string") {
120
+ return err;
121
+ }
122
+ return String(err);
123
+ }
124
+ function isNodeError2(err) {
125
+ return err instanceof Error && "code" in err;
126
+ }
127
+ function isNotFoundError2(err) {
128
+ return isNodeError2(err) && err.code === "ENOENT";
129
+ }
130
+ async function filePathExists2(filePath) {
131
+ try {
132
+ await fs2.stat(filePath);
133
+ return true;
134
+ } catch (err) {
135
+ if (isNotFoundError2(err))
136
+ return false;
137
+ throw err;
138
+ }
139
+ }
112
140
  async function readTemplate(name) {
113
- return fs2.readFile(path2.join(TEMPLATES_DIR, name), "utf-8");
141
+ return fs22.readFile(path2.join(TEMPLATES_DIR, name), "utf-8");
114
142
  }
115
143
  async function canopyCmsConfig(options) {
116
144
  const template = await readTemplate("canopycms.config.ts.template");
@@ -161,23 +189,142 @@ async function nextConfig(options) {
161
189
  const templateName = options.staticBuild ? "next.config-static.ts.template" : "next.config.ts.template";
162
190
  return readTemplate(templateName);
163
191
  }
164
- async function dockerfileCms() {
165
- return readTemplate("Dockerfile.cms.template");
192
+ async function dockerfileCms(options) {
193
+ const template = await readTemplate("Dockerfile.cms.template");
194
+ return template.replace("{{DOCKER_COPY}}", options.copy).replace("{{DOCKER_INSTALL}}", options.install).replace("{{DOCKER_BUILD}}", options.build);
166
195
  }
167
196
  async function dockerignore() {
168
197
  return readTemplate("dockerignore.template");
169
198
  }
170
- async function githubWorkflowCms() {
171
- return readTemplate("deploy-cms.yml.template");
199
+ async function githubWorkflowCms(options) {
200
+ const template = await readTemplate("deploy-cms.yml.template");
201
+ return template.replace("{{DEFAULT_BRANCH}}", options.defaultBranch).replace("{{LOCKFILE}}", options.lockfile).replace("{{CI_INSTALL}}", options.ciInstall).replaceAll("{{ADD_DEV}}", options.addDev).replace("{{STACK_NAME}}", SCAFFOLD_STACK_NAME);
202
+ }
203
+ async function cdkJson() {
204
+ return readTemplate("cdk.json.template");
205
+ }
206
+ async function cdkApp(options) {
207
+ const template = await readTemplate("cdk-app.ts.template");
208
+ return template.replace("{{STACK_NAME}}", SCAFFOLD_STACK_NAME).replace("{{GITHUB_OWNER}}", options.githubOwner).replace("{{GITHUB_REPO}}", options.githubRepo);
209
+ }
210
+ async function cmsStack() {
211
+ return readTemplate("cms-stack.ts.template");
212
+ }
213
+ function commandsFor(manager) {
214
+ return COMMANDS[manager];
215
+ }
216
+ async function detectPackageManager(projectDir) {
217
+ for (const dir of await workspaceSearchPath(projectDir)) {
218
+ const declared = await readDeclaredPackageManager(dir);
219
+ if (declared)
220
+ return declared;
221
+ if (await filePathExists2(path3.join(dir, "pnpm-lock.yaml")))
222
+ return "pnpm";
223
+ if (await filePathExists2(path3.join(dir, "yarn.lock"))) {
224
+ return await isYarnBerryLockfile(dir) ? "yarn-berry" : "yarn";
225
+ }
226
+ }
227
+ return "npm";
228
+ }
229
+ async function workspaceSearchPath(projectDir) {
230
+ const start = path3.resolve(projectDir);
231
+ const dirs = [start];
232
+ if (!await filePathExists2(path3.join(start, "package.json")))
233
+ return dirs;
234
+ let dir = start;
235
+ for (let depth = 0; depth < MAX_WORKSPACE_DEPTH; depth++) {
236
+ if (await filePathExists2(path3.join(dir, ".git")))
237
+ break;
238
+ const parent = path3.dirname(dir);
239
+ if (parent === dir)
240
+ break;
241
+ dir = parent;
242
+ dirs.push(dir);
243
+ }
244
+ return dirs;
245
+ }
246
+ async function readDeclaredPackageManager(projectDir) {
247
+ const pkg = await readPackageJson(projectDir);
248
+ const declared = pkg?.["packageManager"];
249
+ if (typeof declared !== "string")
250
+ return null;
251
+ if (declared.startsWith("pnpm@"))
252
+ return "pnpm";
253
+ if (declared.startsWith("npm@"))
254
+ return "npm";
255
+ if (declared.startsWith("yarn@")) {
256
+ const major = Number.parseInt(declared.slice("yarn@".length), 10);
257
+ if (Number.isFinite(major) && major >= 2)
258
+ return "yarn-berry";
259
+ if (Number.isFinite(major))
260
+ return "yarn";
261
+ return await isYarnBerryLockfile(projectDir) ? "yarn-berry" : "yarn";
262
+ }
263
+ return null;
264
+ }
265
+ async function isYarnBerryLockfile(projectDir) {
266
+ try {
267
+ const content = await fs3.readFile(path3.join(projectDir, "yarn.lock"), "utf-8");
268
+ return content.includes("__metadata:");
269
+ } catch {
270
+ return false;
271
+ }
272
+ }
273
+ async function detectDefaultBranch(projectDir) {
274
+ const head = await gitRaw(projectDir, ["symbolic-ref", "--short", "refs/remotes/origin/HEAD"]);
275
+ if (!head)
276
+ return "main";
277
+ const slash = head.indexOf("/");
278
+ const branch = slash === -1 ? head : head.slice(slash + 1);
279
+ return branch || "main";
280
+ }
281
+ async function detectGitHubRepo(projectDir) {
282
+ const url = await gitRaw(projectDir, ["remote", "get-url", "origin"]);
283
+ if (!url)
284
+ return null;
285
+ const match = /github\.com[:/]+([^/]+)\/(.+?)(?:\.git)?\/?$/.exec(url);
286
+ if (!match)
287
+ return null;
288
+ return { owner: match[1], repo: match[2] };
289
+ }
290
+ async function gitRaw(projectDir, args) {
291
+ try {
292
+ const output = await simpleGit({ baseDir: projectDir }).raw(args);
293
+ const trimmed = output.trim();
294
+ return trimmed.length > 0 ? trimmed : null;
295
+ } catch {
296
+ return null;
297
+ }
298
+ }
299
+ async function readPackageJson(projectDir) {
300
+ try {
301
+ const raw = await fs3.readFile(path3.join(projectDir, "package.json"), "utf-8");
302
+ const parsed = JSON.parse(raw);
303
+ return typeof parsed === "object" && parsed !== null ? parsed : null;
304
+ } catch {
305
+ return null;
306
+ }
307
+ }
308
+ async function missingCdkDependencies(projectDir) {
309
+ const pkg = await readPackageJson(projectDir);
310
+ const declared = /* @__PURE__ */ new Set();
311
+ for (const field of ["dependencies", "devDependencies", "peerDependencies"]) {
312
+ const section = pkg?.[field];
313
+ if (typeof section === "object" && section !== null) {
314
+ for (const name of Object.keys(section))
315
+ declared.add(name);
316
+ }
317
+ }
318
+ return CDK_DEPENDENCIES.filter((name) => !declared.has(name));
172
319
  }
173
320
  function getTaskQueueDir(config) {
174
321
  switch (config.mode) {
175
322
  case "prod": {
176
323
  const workspace = process.env.CANOPYCMS_WORKSPACE_ROOT ?? DEFAULT_PROD_WORKSPACE;
177
- return path3.join(path3.resolve(workspace), ".tasks");
324
+ return path4.join(path4.resolve(workspace), ".tasks");
178
325
  }
179
326
  case "dev": {
180
- return path3.join(process.cwd(), ".canopy-dev", ".tasks");
327
+ return path4.join(process.cwd(), ".canopy-dev", ".tasks");
181
328
  }
182
329
  }
183
330
  }
@@ -185,24 +332,24 @@ function createDebugLogger(options) {
185
332
  return new DebugLogger(options);
186
333
  }
187
334
  async function atomicWriteFile(filePath, content) {
188
- const dir = path4.dirname(filePath);
189
- await fs3.mkdir(dir, { recursive: true });
335
+ const dir = path5.dirname(filePath);
336
+ await fs4.mkdir(dir, { recursive: true });
190
337
  const tempPath = `${filePath}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
191
- await fs3.writeFile(tempPath, content, typeof content === "string" ? "utf-8" : void 0);
338
+ await fs4.writeFile(tempPath, content, typeof content === "string" ? "utf-8" : void 0);
192
339
  try {
193
- await fs3.rename(tempPath, filePath);
340
+ await fs4.rename(tempPath, filePath);
194
341
  } catch (err) {
195
- await fs3.unlink(tempPath).catch(() => {
342
+ await fs4.unlink(tempPath).catch(() => {
196
343
  });
197
344
  throw err;
198
345
  }
199
346
  }
200
- function isNotFoundError2(err) {
347
+ function isNotFoundError22(err) {
201
348
  return err instanceof Error && "code" in err && err.code === "ENOENT";
202
349
  }
203
350
  async function enqueueTask(taskDir, task, logger = nullLogger) {
204
351
  const id = crypto.randomUUID();
205
- const pendingDir = path5.join(taskDir, "pending");
352
+ const pendingDir = path6.join(taskDir, "pending");
206
353
  const queuedTask = {
207
354
  id,
208
355
  action: task.action,
@@ -212,17 +359,17 @@ async function enqueueTask(taskDir, task, logger = nullLogger) {
212
359
  retryCount: 0,
213
360
  maxRetries: task.maxRetries ?? DEFAULT_MAX_RETRIES
214
361
  };
215
- const filePath = path5.join(pendingDir, `${id}.json`);
362
+ const filePath = path6.join(pendingDir, `${id}.json`);
216
363
  await atomicWriteFile(filePath, JSON.stringify(queuedTask, null, 2));
217
364
  logger.debug("Enqueued task", { id, action: task.action });
218
365
  return id;
219
366
  }
220
367
  async function dequeueTask(taskDir, logger = nullLogger) {
221
- const pendingDir = path5.join(taskDir, "pending");
222
- const processingDir = path5.join(taskDir, "processing");
368
+ const pendingDir = path6.join(taskDir, "pending");
369
+ const processingDir = path6.join(taskDir, "processing");
223
370
  let files;
224
371
  try {
225
- files = await fs4.readdir(pendingDir);
372
+ files = await fs5.readdir(pendingDir);
226
373
  } catch {
227
374
  return null;
228
375
  }
@@ -233,7 +380,7 @@ async function dequeueTask(taskDir, logger = nullLogger) {
233
380
  const tasks = [];
234
381
  for (const fileName of jsonFiles) {
235
382
  try {
236
- const content = await fs4.readFile(path5.join(pendingDir, fileName), "utf-8");
383
+ const content = await fs5.readFile(path6.join(pendingDir, fileName), "utf-8");
237
384
  const task = parseTaskJson(content);
238
385
  if (!task) {
239
386
  await moveToCorrupt(taskDir, pendingDir, fileName, "Invalid JSON", logger);
@@ -244,7 +391,7 @@ async function dequeueTask(taskDir, logger = nullLogger) {
244
391
  }
245
392
  tasks.push({ fileName, task });
246
393
  } catch (err) {
247
- if (isNotFoundError2(err))
394
+ if (isNotFoundError22(err))
248
395
  continue;
249
396
  throw err;
250
397
  }
@@ -254,21 +401,21 @@ async function dequeueTask(taskDir, logger = nullLogger) {
254
401
  tasks.sort((a, b) => a.task.createdAt.localeCompare(b.task.createdAt) || a.task.id.localeCompare(b.task.id));
255
402
  for (const { fileName, task } of tasks) {
256
403
  if (await taskExistsIn(taskDir, task.id, ["completed", "failed"])) {
257
- await fs4.unlink(path5.join(pendingDir, fileName)).catch(() => {
404
+ await fs5.unlink(path6.join(pendingDir, fileName)).catch(() => {
258
405
  });
259
406
  logger.debug("Skipped already-finished task", { id: task.id });
260
407
  continue;
261
408
  }
262
- const sourcePath = path5.join(pendingDir, fileName);
263
- const destPath = path5.join(processingDir, fileName);
409
+ const sourcePath = path6.join(pendingDir, fileName);
410
+ const destPath = path6.join(processingDir, fileName);
264
411
  try {
265
412
  task.status = "processing";
266
413
  await atomicWriteFile(destPath, JSON.stringify(task, null, 2));
267
- await fs4.unlink(sourcePath);
414
+ await fs5.unlink(sourcePath);
268
415
  logger.debug("Dequeued task", { id: task.id, action: task.action });
269
416
  return task;
270
417
  } catch (err) {
271
- if (isNotFoundError2(err))
418
+ if (isNotFoundError22(err))
272
419
  continue;
273
420
  throw err;
274
421
  }
@@ -276,22 +423,22 @@ async function dequeueTask(taskDir, logger = nullLogger) {
276
423
  return null;
277
424
  }
278
425
  async function completeTask(taskDir, taskId, result, logger = nullLogger) {
279
- const processingPath = path5.join(taskDir, "processing", `${taskId}.json`);
280
- const completedDir = path5.join(taskDir, "completed");
281
- const completedPath = path5.join(completedDir, `${taskId}.json`);
426
+ const processingPath = path6.join(taskDir, "processing", `${taskId}.json`);
427
+ const completedDir = path6.join(taskDir, "completed");
428
+ const completedPath = path6.join(completedDir, `${taskId}.json`);
282
429
  let task;
283
430
  try {
284
- const content = await fs4.readFile(processingPath, "utf-8");
431
+ const content = await fs5.readFile(processingPath, "utf-8");
285
432
  const parsed = parseTaskJson(content);
286
433
  if (!parsed) {
287
434
  logger.debug("Corrupt task file in processing, removing", { id: taskId });
288
- await fs4.unlink(processingPath).catch(() => {
435
+ await fs5.unlink(processingPath).catch(() => {
289
436
  });
290
437
  return;
291
438
  }
292
439
  task = parsed;
293
440
  } catch (err) {
294
- if (isNotFoundError2(err))
441
+ if (isNotFoundError22(err))
295
442
  return;
296
443
  throw err;
297
444
  }
@@ -299,27 +446,27 @@ async function completeTask(taskDir, taskId, result, logger = nullLogger) {
299
446
  task.completedAt = (/* @__PURE__ */ new Date()).toISOString();
300
447
  task.result = result;
301
448
  await atomicWriteFile(completedPath, JSON.stringify(task, null, 2));
302
- await fs4.unlink(processingPath).catch(() => {
449
+ await fs5.unlink(processingPath).catch(() => {
303
450
  });
304
451
  logger.debug("Completed task", { id: taskId });
305
452
  }
306
453
  async function failTask(taskDir, taskId, error, logger = nullLogger) {
307
- const processingPath = path5.join(taskDir, "processing", `${taskId}.json`);
308
- const failedDir = path5.join(taskDir, "failed");
309
- const failedPath = path5.join(failedDir, `${taskId}.json`);
454
+ const processingPath = path6.join(taskDir, "processing", `${taskId}.json`);
455
+ const failedDir = path6.join(taskDir, "failed");
456
+ const failedPath = path6.join(failedDir, `${taskId}.json`);
310
457
  let task;
311
458
  try {
312
- const content = await fs4.readFile(processingPath, "utf-8");
459
+ const content = await fs5.readFile(processingPath, "utf-8");
313
460
  const parsed = parseTaskJson(content);
314
461
  if (!parsed) {
315
462
  logger.debug("Corrupt task file in processing, removing", { id: taskId });
316
- await fs4.unlink(processingPath).catch(() => {
463
+ await fs5.unlink(processingPath).catch(() => {
317
464
  });
318
465
  return;
319
466
  }
320
467
  task = parsed;
321
468
  } catch (err) {
322
- if (isNotFoundError2(err))
469
+ if (isNotFoundError22(err))
323
470
  return;
324
471
  throw err;
325
472
  }
@@ -327,27 +474,27 @@ async function failTask(taskDir, taskId, error, logger = nullLogger) {
327
474
  task.completedAt = (/* @__PURE__ */ new Date()).toISOString();
328
475
  task.error = error;
329
476
  await atomicWriteFile(failedPath, JSON.stringify(task, null, 2));
330
- await fs4.unlink(processingPath).catch(() => {
477
+ await fs5.unlink(processingPath).catch(() => {
331
478
  });
332
479
  logger.debug("Failed task", { id: taskId, error });
333
480
  }
334
481
  async function retryTask(taskDir, taskId, error, logger = nullLogger) {
335
- const processingPath = path5.join(taskDir, "processing", `${taskId}.json`);
336
- const pendingDir = path5.join(taskDir, "pending");
337
- const pendingPath = path5.join(pendingDir, `${taskId}.json`);
482
+ const processingPath = path6.join(taskDir, "processing", `${taskId}.json`);
483
+ const pendingDir = path6.join(taskDir, "pending");
484
+ const pendingPath = path6.join(pendingDir, `${taskId}.json`);
338
485
  let task;
339
486
  try {
340
- const content = await fs4.readFile(processingPath, "utf-8");
487
+ const content = await fs5.readFile(processingPath, "utf-8");
341
488
  const parsed = parseTaskJson(content);
342
489
  if (!parsed) {
343
490
  logger.debug("Corrupt task file in processing, removing", { id: taskId });
344
- await fs4.unlink(processingPath).catch(() => {
491
+ await fs5.unlink(processingPath).catch(() => {
345
492
  });
346
493
  return;
347
494
  }
348
495
  task = parsed;
349
496
  } catch (err) {
350
- if (isNotFoundError2(err))
497
+ if (isNotFoundError22(err))
351
498
  return;
352
499
  throw err;
353
500
  }
@@ -358,17 +505,17 @@ async function retryTask(taskDir, taskId, error, logger = nullLogger) {
358
505
  task.retryAfter = new Date(Date.now() + backoffMs).toISOString();
359
506
  task.error = error;
360
507
  await atomicWriteFile(pendingPath, JSON.stringify(task, null, 2));
361
- await fs4.unlink(processingPath).catch(() => {
508
+ await fs5.unlink(processingPath).catch(() => {
362
509
  });
363
510
  logger.debug("Retrying task", { id: taskId, retryCount, backoffMs });
364
511
  }
365
512
  async function requeueFailedTask(taskDir, taskId, logger = nullLogger) {
366
- const failedPath = path5.join(taskDir, "failed", `${taskId}.json`);
513
+ const failedPath = path6.join(taskDir, "failed", `${taskId}.json`);
367
514
  let content;
368
515
  try {
369
- content = await fs4.readFile(failedPath, "utf-8");
516
+ content = await fs5.readFile(failedPath, "utf-8");
370
517
  } catch (err) {
371
- if (isNotFoundError2(err))
518
+ if (isNotFoundError22(err))
372
519
  return { error: "not-found" };
373
520
  throw err;
374
521
  }
@@ -385,37 +532,37 @@ async function requeueFailedTask(taskDir, taskId, logger = nullLogger) {
385
532
  retryCount: 0,
386
533
  maxRetries: original.maxRetries
387
534
  };
388
- const pendingPath = path5.join(taskDir, "pending", `${newTaskId}.json`);
535
+ const pendingPath = path6.join(taskDir, "pending", `${newTaskId}.json`);
389
536
  await atomicWriteFile(pendingPath, JSON.stringify(newTask, null, 2));
390
- await fs4.unlink(failedPath).catch(() => {
537
+ await fs5.unlink(failedPath).catch(() => {
391
538
  });
392
539
  logger.debug("Requeued failed task", { originalId: taskId, newTaskId });
393
540
  return { newTaskId };
394
541
  }
395
542
  async function recoverOrphanedTasks(taskDir, maxAgeMs = 5 * 6e4, logger = nullLogger) {
396
- const processingDir = path5.join(taskDir, "processing");
397
- const pendingDir = path5.join(taskDir, "pending");
543
+ const processingDir = path6.join(taskDir, "processing");
544
+ const pendingDir = path6.join(taskDir, "pending");
398
545
  let files;
399
546
  try {
400
- files = await fs4.readdir(processingDir);
547
+ files = await fs5.readdir(processingDir);
401
548
  } catch {
402
549
  return 0;
403
550
  }
404
551
  const now = Date.now();
405
552
  let recovered = 0;
406
553
  for (const fileName of files.filter((f) => f.endsWith(".json"))) {
407
- const filePath = path5.join(processingDir, fileName);
554
+ const filePath = path6.join(processingDir, fileName);
408
555
  try {
409
- const stat = await fs4.stat(filePath);
556
+ const stat = await fs5.stat(filePath);
410
557
  if (now - stat.mtimeMs >= maxAgeMs) {
411
- const content = await fs4.readFile(filePath, "utf-8");
558
+ const content = await fs5.readFile(filePath, "utf-8");
412
559
  const task = parseTaskJson(content);
413
560
  if (!task) {
414
561
  await moveToCorrupt(taskDir, processingDir, fileName, "Invalid JSON during recovery", logger);
415
562
  continue;
416
563
  }
417
564
  if (await taskExistsIn(taskDir, task.id, ["completed", "failed"])) {
418
- await fs4.unlink(filePath).catch(() => {
565
+ await fs5.unlink(filePath).catch(() => {
419
566
  });
420
567
  logger.debug("Cleaned up orphaned task (already finished)", {
421
568
  id: task.id
@@ -423,8 +570,8 @@ async function recoverOrphanedTasks(taskDir, maxAgeMs = 5 * 6e4, logger = nullLo
423
570
  continue;
424
571
  }
425
572
  task.status = "pending";
426
- await atomicWriteFile(path5.join(pendingDir, fileName), JSON.stringify(task, null, 2));
427
- await fs4.unlink(filePath);
573
+ await atomicWriteFile(path6.join(pendingDir, fileName), JSON.stringify(task, null, 2));
574
+ await fs5.unlink(filePath);
428
575
  logger.debug("Recovered orphaned task", {
429
576
  id: task.id,
430
577
  action: task.action
@@ -432,7 +579,7 @@ async function recoverOrphanedTasks(taskDir, maxAgeMs = 5 * 6e4, logger = nullLo
432
579
  recovered++;
433
580
  }
434
581
  } catch (err) {
435
- if (isNotFoundError2(err))
582
+ if (isNotFoundError22(err))
436
583
  continue;
437
584
  logger.debug("Failed to recover task", { fileName });
438
585
  }
@@ -443,19 +590,19 @@ async function cleanupOldTasks(taskDir, maxAgeMs = 30 * 24 * 60 * 6e4, logger =
443
590
  const now = Date.now();
444
591
  let cleaned = 0;
445
592
  for (const subdir of ["completed", "failed"]) {
446
- const dir = path5.join(taskDir, subdir);
593
+ const dir = path6.join(taskDir, subdir);
447
594
  let files;
448
595
  try {
449
- files = await fs4.readdir(dir);
596
+ files = await fs5.readdir(dir);
450
597
  } catch {
451
598
  continue;
452
599
  }
453
600
  for (const fileName of files.filter((f) => f.endsWith(".json"))) {
454
601
  try {
455
- const filePath = path5.join(dir, fileName);
456
- const stat = await fs4.stat(filePath);
602
+ const filePath = path6.join(dir, fileName);
603
+ const stat = await fs5.stat(filePath);
457
604
  if (now - stat.mtimeMs >= maxAgeMs) {
458
- await fs4.unlink(filePath);
605
+ await fs5.unlink(filePath);
459
606
  cleaned++;
460
607
  }
461
608
  } catch {
@@ -469,9 +616,9 @@ async function cleanupOldTasks(taskDir, maxAgeMs = 30 * 24 * 60 * 6e4, logger =
469
616
  }
470
617
  async function getTask(taskDir, taskId) {
471
618
  for (const subdir of ["completed", "failed", "processing", "pending"]) {
472
- const filePath = path5.join(taskDir, subdir, `${taskId}.json`);
619
+ const filePath = path6.join(taskDir, subdir, `${taskId}.json`);
473
620
  try {
474
- const content = await fs4.readFile(filePath, "utf-8");
621
+ const content = await fs5.readFile(filePath, "utf-8");
475
622
  return parseTaskJson(content);
476
623
  } catch {
477
624
  continue;
@@ -480,10 +627,10 @@ async function getTask(taskDir, taskId) {
480
627
  return null;
481
628
  }
482
629
  async function listTasks(taskDir, status, limit = 100) {
483
- const dir = path5.join(taskDir, status);
630
+ const dir = path6.join(taskDir, status);
484
631
  let files;
485
632
  try {
486
- files = await fs4.readdir(dir);
633
+ files = await fs5.readdir(dir);
487
634
  } catch {
488
635
  return [];
489
636
  }
@@ -492,7 +639,7 @@ async function listTasks(taskDir, status, limit = 100) {
492
639
  if (tasks.length >= limit)
493
640
  break;
494
641
  try {
495
- const content = await fs4.readFile(path5.join(dir, fileName), "utf-8");
642
+ const content = await fs5.readFile(path6.join(dir, fileName), "utf-8");
496
643
  const task = parseTaskJson(content);
497
644
  if (task)
498
645
  tasks.push(task);
@@ -512,9 +659,9 @@ async function getQueueStats(taskDir) {
512
659
  corrupt: 0
513
660
  };
514
661
  for (const status of ["pending", "processing", "completed", "failed", "corrupt"]) {
515
- const dir = path5.join(taskDir, status);
662
+ const dir = path6.join(taskDir, status);
516
663
  try {
517
- const files = await fs4.readdir(dir);
664
+ const files = await fs5.readdir(dir);
518
665
  stats[status] = files.filter((f) => f.endsWith(".json")).length;
519
666
  } catch {
520
667
  }
@@ -522,20 +669,20 @@ async function getQueueStats(taskDir) {
522
669
  return stats;
523
670
  }
524
671
  async function listCorruptTaskFiles(taskDir, limit = 50, logger = nullLogger) {
525
- const corruptDir = path5.join(taskDir, "corrupt");
672
+ const corruptDir = path6.join(taskDir, "corrupt");
526
673
  let files;
527
674
  try {
528
- files = await fs4.readdir(corruptDir);
675
+ files = await fs5.readdir(corruptDir);
529
676
  } catch (err) {
530
- if (isNotFoundError2(err))
677
+ if (isNotFoundError22(err))
531
678
  return [];
532
679
  throw err;
533
680
  }
534
681
  const entries = [];
535
682
  for (const fileName of files.filter((f) => f.endsWith(".json"))) {
536
- const filePath = path5.join(corruptDir, fileName);
683
+ const filePath = path6.join(corruptDir, fileName);
537
684
  try {
538
- const stat = await fs4.stat(filePath);
685
+ const stat = await fs5.stat(filePath);
539
686
  entries.push({
540
687
  fileName,
541
688
  size: stat.size,
@@ -543,7 +690,7 @@ async function listCorruptTaskFiles(taskDir, limit = 50, logger = nullLogger) {
543
690
  rawSnippet: await readSnippet(filePath, 500)
544
691
  });
545
692
  } catch (err) {
546
- if (isNotFoundError2(err))
693
+ if (isNotFoundError22(err))
547
694
  continue;
548
695
  logger.debug("Failed to read corrupt task file", { fileName });
549
696
  }
@@ -552,7 +699,7 @@ async function listCorruptTaskFiles(taskDir, limit = 50, logger = nullLogger) {
552
699
  return entries.slice(0, limit);
553
700
  }
554
701
  async function readSnippet(filePath, maxBytes) {
555
- const handle = await fs4.open(filePath, "r");
702
+ const handle = await fs5.open(filePath, "r");
556
703
  try {
557
704
  const buffer = Buffer.alloc(maxBytes);
558
705
  const { bytesRead } = await handle.read(buffer, 0, maxBytes, 0);
@@ -575,7 +722,7 @@ function parseTaskJson(content) {
575
722
  async function taskExistsIn(taskDir, taskId, subdirs) {
576
723
  for (const subdir of subdirs) {
577
724
  try {
578
- await fs4.stat(path5.join(taskDir, subdir, `${taskId}.json`));
725
+ await fs5.stat(path6.join(taskDir, subdir, `${taskId}.json`));
579
726
  return true;
580
727
  } catch {
581
728
  }
@@ -583,16 +730,33 @@ async function taskExistsIn(taskDir, taskId, subdirs) {
583
730
  return false;
584
731
  }
585
732
  async function moveToCorrupt(taskDir, sourceDir, fileName, reason, logger) {
586
- const corruptDir = path5.join(taskDir, "corrupt");
733
+ const corruptDir = path6.join(taskDir, "corrupt");
587
734
  try {
588
- await fs4.mkdir(corruptDir, { recursive: true });
589
- await fs4.rename(path5.join(sourceDir, fileName), path5.join(corruptDir, fileName));
735
+ await fs5.mkdir(corruptDir, { recursive: true });
736
+ await fs5.rename(path6.join(sourceDir, fileName), path6.join(corruptDir, fileName));
590
737
  logger.debug("Moved corrupt task file", { fileName, reason });
591
738
  } catch {
592
- await fs4.unlink(path5.join(sourceDir, fileName)).catch(() => {
739
+ await fs5.unlink(path6.join(sourceDir, fileName)).catch(() => {
593
740
  });
594
741
  }
595
742
  }
743
+ function canopyLogWarn(...args) {
744
+ active.warn(...args);
745
+ }
746
+ function resolveDeploymentName(config, modeDefault) {
747
+ const envValue = process.env.CANOPYCMS_DEPLOYMENT_NAME?.trim();
748
+ const configValue = config.deploymentName;
749
+ if (envValue && configValue && envValue !== configValue && !warned) {
750
+ canopyLogWarn(`CanopyCMS: CANOPYCMS_DEPLOYMENT_NAME ("${envValue}") differs from config.deploymentName ("${configValue}") \u2014 using the env var (infra-stamped env wins over shared-repo config by design; see resolveDeploymentName's doc comment). Update config.deploymentName to match if this mismatch was unintentional.`);
751
+ warned = true;
752
+ }
753
+ const resolved = envValue || configValue || modeDefault;
754
+ if (!isValidDeploymentName(resolved)) {
755
+ const source = envValue ? "CANOPYCMS_DEPLOYMENT_NAME" : configValue ? "config.deploymentName" : "the mode default";
756
+ throw new Error(`CanopyCMS: invalid deploymentName ${JSON.stringify(resolved)} (from ${source}). It is interpolated into the settings branch name (canopycms-settings-<deploymentName>), so it must start with a letter or digit, contain only letters, digits, '.', '_' or '-', and must not contain '..' or end with '.' or '.lock'.`);
757
+ }
758
+ return resolved;
759
+ }
596
760
  function operatingStrategy(mode) {
597
761
  const cached = strategyCache.get(mode);
598
762
  if (cached)
@@ -618,33 +782,8 @@ function assertAuthPluginAllowedForMode(plugin, mode) {
618
782
  throw new Error("CanopyCMS: an auth plugin was configured with mode: 'prod' but does not affirm `verifiesCredentials: true`. This plugin performs no real credential verification, so anyone could impersonate any user (including admins). Configure a verifying auth plugin for production (e.g. createClerkAuthPlugin from 'canopycms-auth-clerk' with CLERK_SECRET_KEY set), or run with mode: 'dev' for local development.");
619
783
  }
620
784
  }
621
- function getErrorMessage2(err) {
622
- if (err instanceof Error) {
623
- return err.message;
624
- }
625
- if (typeof err === "string") {
626
- return err;
627
- }
628
- return String(err);
629
- }
630
- function isNodeError2(err) {
631
- return err instanceof Error && "code" in err;
632
- }
633
- function isNotFoundError3(err) {
634
- return isNodeError2(err) && err.code === "ENOENT";
635
- }
636
- async function filePathExists2(filePath) {
637
- try {
638
- await fs6.stat(filePath);
639
- return true;
640
- } catch (err) {
641
- if (isNotFoundError3(err))
642
- return false;
643
- throw err;
644
- }
645
- }
646
785
  async function writeFile(filePath, content, options) {
647
- const relativePath = path6.relative(process.cwd(), filePath);
786
+ const relativePath = path7.relative(process.cwd(), filePath);
648
787
  if (await filePathExists2(filePath)) {
649
788
  if (options.force) {
650
789
  } else if (options.nonInteractive) {
@@ -661,8 +800,8 @@ async function writeFile(filePath, content, options) {
661
800
  }
662
801
  }
663
802
  }
664
- await fs5.mkdir(path6.dirname(filePath), { recursive: true });
665
- await fs5.writeFile(filePath, content, "utf-8");
803
+ await fs6.mkdir(path7.dirname(filePath), { recursive: true });
804
+ await fs6.writeFile(filePath, content, "utf-8");
666
805
  p.log.success(`created: ${relativePath}`);
667
806
  return true;
668
807
  }
@@ -714,24 +853,24 @@ async function init(options) {
714
853
  }
715
854
  const serverPageExt = staticBuild ? "page.server.tsx" : "page.tsx";
716
855
  const serverRouteExt = staticBuild ? "route.server.ts" : "route.ts";
717
- await writeFile(path6.join(projectDir, "canopycms.config.ts"), await canopyCmsConfig2({ mode, staticBuild }), writeOpts);
718
- await writeFile(path6.join(projectDir, appDir, "lib/canopy.ts"), await canopyContext2({ configImport: configImportPath(appDir, 1), authProvider }), writeOpts);
719
- await writeFile(path6.join(projectDir, appDir, "schemas.ts"), await schemasTemplate2(), writeOpts);
720
- await writeFile(path6.join(projectDir, appDir, `api/canopycms/[...canopycms]/${serverRouteExt}`), await apiRoute2({
856
+ await writeFile(path7.join(projectDir, "canopycms.config.ts"), await canopyCmsConfig2({ mode, staticBuild }), writeOpts);
857
+ await writeFile(path7.join(projectDir, appDir, "lib/canopy.ts"), await canopyContext2({ configImport: configImportPath(appDir, 1), authProvider }), writeOpts);
858
+ await writeFile(path7.join(projectDir, appDir, "schemas.ts"), await schemasTemplate2(), writeOpts);
859
+ await writeFile(path7.join(projectDir, appDir, `api/canopycms/[...canopycms]/${serverRouteExt}`), await apiRoute2({
721
860
  canopyImport: "../".repeat(3) + "lib/canopy"
722
861
  }), writeOpts);
723
- await writeFile(path6.join(projectDir, appDir, `edit/${serverPageExt}`), await editPage2({ configImport: configImportPath(appDir, 1), authProvider }), writeOpts);
862
+ await writeFile(path7.join(projectDir, appDir, `edit/${serverPageExt}`), await editPage2({ configImport: configImportPath(appDir, 1), authProvider }), writeOpts);
724
863
  if (ai) {
725
- await writeFile(path6.join(projectDir, appDir, "ai/config.ts"), await aiConfig2(), writeOpts);
726
- await writeFile(path6.join(projectDir, appDir, `ai/[...path]/${serverRouteExt}`), await aiRoute2({ configImport: configImportPath(appDir, 2) }), writeOpts);
864
+ await writeFile(path7.join(projectDir, appDir, "ai/config.ts"), await aiConfig2(), writeOpts);
865
+ await writeFile(path7.join(projectDir, appDir, `ai/[...path]/${serverRouteExt}`), await aiRoute2({ configImport: configImportPath(appDir, 2) }), writeOpts);
727
866
  }
728
- await writeFile(path6.join(projectDir, "next.config.ts"), await nextConfig2({ staticBuild }), writeOpts);
729
- await writeFile(path6.join(projectDir, "middleware.ts"), await middleware2({ authProvider }), writeOpts);
730
- const gitignorePath = path6.join(projectDir, ".gitignore");
867
+ await writeFile(path7.join(projectDir, "next.config.ts"), await nextConfig2({ staticBuild }), writeOpts);
868
+ await writeFile(path7.join(projectDir, "middleware.ts"), await middleware2({ authProvider }), writeOpts);
869
+ const gitignorePath = path7.join(projectDir, ".gitignore");
731
870
  if (await filePathExists2(gitignorePath)) {
732
- const content = await fs5.readFile(gitignorePath, "utf-8");
871
+ const content = await fs6.readFile(gitignorePath, "utf-8");
733
872
  if (!content.includes(".canopy-dev")) {
734
- await fs5.appendFile(gitignorePath, "\n# CanopyCMS\n.canopy-dev/\n");
873
+ await fs6.appendFile(gitignorePath, "\n# CanopyCMS\n.canopy-dev/\n");
735
874
  p.log.success("updated: .gitignore");
736
875
  }
737
876
  }
@@ -756,36 +895,80 @@ async function init(options) {
756
895
  async function initDeployAws(options) {
757
896
  const { projectDir, force, nonInteractive } = options;
758
897
  const writeOpts = { force, nonInteractive };
759
- const { dockerfileCms: dockerfileCms2, dockerignore: dockerignore2, githubWorkflowCms: githubWorkflowCms2 } = await Promise.resolve().then(() => (init_templates(), templates_exports));
898
+ const { dockerfileCms: dockerfileCms2, dockerignore: dockerignore2, githubWorkflowCms: githubWorkflowCms2, cdkJson: cdkJson2, cdkApp: cdkApp2, cmsStack: cmsStack2 } = await Promise.resolve().then(() => (init_templates(), templates_exports));
899
+ const { detectPackageManager: detectPackageManager2, detectDefaultBranch: detectDefaultBranch2, detectGitHubRepo: detectGitHubRepo2, commandsFor: commandsFor2, missingCdkDependencies: missingCdkDependencies2, CDK_DEPENDENCIES: CDK_DEPENDENCIES2 } = await Promise.resolve().then(() => (init_project_detect(), project_detect_exports));
760
900
  p.intro("CanopyCMS init-deploy aws");
761
- await writeFile(path6.join(projectDir, "Dockerfile.cms"), await dockerfileCms2(), writeOpts);
762
- const dockerignoreWritten = await writeFile(path6.join(projectDir, ".dockerignore"), await dockerignore2(), writeOpts);
901
+ const packageManager = await detectPackageManager2(projectDir);
902
+ const pm = commandsFor2(packageManager);
903
+ const defaultBranch = await detectDefaultBranch2(projectDir);
904
+ const repo = await detectGitHubRepo2(projectDir);
905
+ await writeFile(path7.join(projectDir, "Dockerfile.cms"), await dockerfileCms2({ copy: pm.dockerCopy, install: pm.dockerInstall, build: pm.build }), writeOpts);
906
+ const dockerignoreWritten = await writeFile(path7.join(projectDir, ".dockerignore"), await dockerignore2(), writeOpts);
763
907
  if (!dockerignoreWritten) {
764
- p.log.warn("Existing .dockerignore kept \u2014 verify it excludes .env* (secrets would otherwise enter the build context) and does NOT exclude vendor/ (needed for `npm ci` with file: deps).");
908
+ p.log.warn("Existing .dockerignore kept \u2014 verify it excludes .env* (secrets would otherwise enter the build context) and does NOT exclude vendor/ (needed by the Dockerfile.cms install step with file: deps).");
765
909
  }
766
- await writeFile(path6.join(projectDir, ".github/workflows/deploy-cms.yml"), await githubWorkflowCms2(), writeOpts);
767
- const nextConfigPath = path6.join(projectDir, "next.config.ts");
768
- const nextConfigMjsPath = path6.join(projectDir, "next.config.mjs");
910
+ await writeFile(path7.join(projectDir, ".github/workflows/deploy-cms.yml"), await githubWorkflowCms2({
911
+ defaultBranch,
912
+ lockfile: pm.lockfile,
913
+ ciInstall: pm.ciInstall,
914
+ addDev: pm.addDev
915
+ }), writeOpts);
916
+ const cdkJsonPath = path7.join(projectDir, "cdk.json");
917
+ const existingCdkJson = await filePathExists2(cdkJsonPath) ? await fs6.readFile(cdkJsonPath, "utf-8") : null;
918
+ const cdkJsonWritten = await writeFile(cdkJsonPath, await cdkJson2(), writeOpts);
919
+ await writeFile(path7.join(projectDir, "infrastructure/bin/app.ts"), await cdkApp2({
920
+ githubOwner: repo?.owner ?? "your-org",
921
+ githubRepo: repo?.repo ?? "your-docs-site"
922
+ }), writeOpts);
923
+ await writeFile(path7.join(projectDir, "infrastructure/lib/cms-stack.ts"), await cmsStack2(), writeOpts);
924
+ if (!cdkJsonWritten && existingCdkJson && !existingCdkJson.includes("infrastructure/bin/app.ts")) {
925
+ p.log.warn("Existing cdk.json kept, and it does not point at infrastructure/bin/app.ts \u2014 the scaffolded CDK app will not be deployed. Either wire CmsStack into your own app entry point, or re-run with --force to replace cdk.json.");
926
+ }
927
+ const nextConfigPath = path7.join(projectDir, "next.config.ts");
928
+ const nextConfigMjsPath = path7.join(projectDir, "next.config.mjs");
769
929
  const configPath = await filePathExists2(nextConfigPath) ? nextConfigPath : await filePathExists2(nextConfigMjsPath) ? nextConfigMjsPath : null;
770
930
  if (configPath) {
771
- const content = await fs5.readFile(configPath, "utf-8");
931
+ const content = await fs6.readFile(configPath, "utf-8");
772
932
  if (!content.includes("CANOPY_BUILD")) {
773
933
  p.note([
774
- `Add dual build support to ${path6.basename(configPath)}:`,
934
+ `Add dual build support to ${path7.basename(configPath)}:`,
775
935
  "",
776
936
  " output: process.env.CANOPY_BUILD === 'cms' ? 'standalone' : 'export',"
777
937
  ].join("\n"), "Manual step");
778
938
  }
779
939
  }
780
- p.note("CDK constructs are available via the canopycms-cdk package.\nSee the deployment plan for CDK stack setup.", "AWS deployment");
940
+ if (packageManager === "yarn" || packageManager === "yarn-berry") {
941
+ p.log.warn(`Yarn detected (${packageManager === "yarn-berry" ? "Berry" : "classic"}). The generated install lines are a best effort and are not exercised by CanopyCMS tests \u2014 review them in Dockerfile.cms and .github/workflows/deploy-cms.yml.` + (packageManager === "yarn-berry" ? " Berry additionally requires nodeLinker: node-modules; PnP is incompatible with Next.js standalone output, which the CMS image's runner stage copies." : ""));
942
+ }
943
+ const missing = await missingCdkDependencies2(projectDir);
944
+ if (missing.length > 0) {
945
+ p.log.warn(`Not installed: ${missing.join(", ")}. cdk.json runs \`node --import tsx infrastructure/bin/app.ts\`, so the deploy fails without them:
946
+ ${pm.addDev} ${CDK_DEPENDENCIES2.join(" ")}`);
947
+ }
948
+ p.note([
949
+ "1. Install the CDK dependencies (if you have not already):",
950
+ ` ${pm.addDev} ${CDK_DEPENDENCIES2.join(" ")}`,
951
+ "",
952
+ "2. Fill in infrastructure/lib/cms-stack.ts and infrastructure/bin/app.ts",
953
+ ` (worker repo currently set to ${repo ? `${repo.owner}/${repo.repo}` : "your-org/your-docs-site"})`,
954
+ "",
955
+ "3. Bootstrap CDK in the target account/region: cdk bootstrap",
956
+ "",
957
+ "4. Set the repository secrets and variables listed at the top of",
958
+ " .github/workflows/deploy-cms.yml \u2014 the deploy fails at synth without them.",
959
+ "",
960
+ `Deploys trigger on pushes to '${defaultBranch}'; edit the workflow to change that.`,
961
+ "",
962
+ "Full walkthrough: docs/deploying-to-aws.md"
963
+ ].join("\n"), "AWS deployment");
781
964
  p.outro("Done!");
782
965
  }
783
966
  async function detectMode(projectDir) {
784
- const cfgPath = path6.join(projectDir, "canopycms.config.ts");
967
+ const cfgPath = path7.join(projectDir, "canopycms.config.ts");
785
968
  try {
786
- await fs5.stat(cfgPath);
969
+ await fs6.stat(cfgPath);
787
970
  } catch (err) {
788
- if (isNotFoundError3(err))
971
+ if (isNotFoundError2(err))
789
972
  return "dev";
790
973
  throw err;
791
974
  }
@@ -814,7 +997,7 @@ async function workerRunOnce(options) {
814
997
  assertAuthPluginAllowedForMode(options.authPlugin, mode);
815
998
  }
816
999
  const taskDir = getTaskQueueDir2({ mode });
817
- const cachePath = process.env.CANOPY_AUTH_CACHE_PATH ?? path6.join(operatingStrategy(mode).getWorkspaceRoot(options.projectDir), ".cache");
1000
+ const cachePath = process.env.CANOPY_AUTH_CACHE_PATH ?? path7.join(operatingStrategy(mode).getWorkspaceRoot(options.projectDir), ".cache");
818
1001
  let refreshAuthCache;
819
1002
  const authMode = process.env.CANOPY_AUTH_MODE || "dev";
820
1003
  if (options.authPlugin?.createCacheRefresher) {
@@ -864,7 +1047,7 @@ CanopyCMS worker run-once (mode: ${mode}, auth: ${authMode})
864
1047
  }
865
1048
  console.log("\nDone");
866
1049
  }
867
- var __defProp2, __getOwnPropNames2, __esm2, __export2, primitiveFieldTypes, fieldTypes, init_types, fieldBaseSchema, selectOptionSchema, referenceOptionSchema, primitiveFieldSchema, ASPECT_RATIO_RE, imageFieldSchema, selectFieldSchema, referenceFieldSchema, fieldHolder, blockSchema, blockFieldSchema, objectFieldSchema, inlineGroupFieldSchema, customFieldSchema, knownFieldSchema, fieldSchema, init_field, relativePathSchema, entryTypeSchema, collectionSchema, init_collection, mediaSchema, init_media, defaultBranchAccessSchema, defaultPathAccessLevelSchema, defaultPathAccessLevelsSchema, defaultPathAccessSchema, defaultBaseBranchSchema, defaultRemoteNameSchema, defaultRemoteUrlSchema, gitBotAuthorNameSchema, gitBotAuthorEmailSchema, githubTokenEnvVarSchema, operatingModeSchema, deployedAsSchema, contentRootSchema, sourceRootSchema, deploymentNameSchema, devContentSyncSchema, devConfigSchema, editorConfigSchema, CanopyConfigSchema, DEFAULT_PROD_WORKSPACE, init_config, permissionTargetSchema, pathPermissionSchema, init_permissions, init_normalize, init_types2, init_flatten, init_validation, init_helpers, init_config2, templates_exports, __dirname, TEMPLATES_DIR, init_templates, task_queue_config_exports, init_task_queue_config, LOG_LEVELS, DebugLogger, testLogger, init_debug, init_atomic_write, DEFAULT_MAX_RETRIES, nullLogger, init_task_queue, init_task_queue2, task_queue_exports, debugLogger, cmsTaskQueueLogger, init_task_queue3, ProdClientSafeStrategy, DevClientSafeStrategy, ProdStrategy, DevStrategy, strategyCache;
1050
+ var __defProp2, __getOwnPropNames2, __esm2, __export2, primitiveFieldTypes, fieldTypes, init_types, fieldBaseSchema, selectOptionSchema, referenceOptionSchema, primitiveFieldSchema, ASPECT_RATIO_RE, imageFieldSchema, selectFieldSchema, referenceFieldSchema, fieldHolder, blockSchema, blockFieldSchema, objectFieldSchema, inlineGroupFieldSchema, customFieldSchema, knownFieldSchema, fieldSchema, init_field, relativePathSchema, entryTypeSchema, collectionSchema, init_collection, mediaSchema, init_media, defaultBranchAccessSchema, defaultPathAccessLevelSchema, defaultPathAccessLevelsSchema, defaultPathAccessSchema, defaultBaseBranchSchema, defaultRemoteNameSchema, defaultRemoteUrlSchema, gitBotAuthorNameSchema, gitBotAuthorEmailSchema, githubTokenEnvVarSchema, operatingModeSchema, deployedAsSchema, contentRootSchema, sourceRootSchema, deploymentNameSchema, devContentSyncSchema, devConfigSchema, editorConfigSchema, CanopyConfigSchema, DEFAULT_PROD_WORKSPACE, init_config, permissionTargetSchema, pathPermissionSchema, init_permissions, init_normalize, init_types2, init_flatten, init_validation, init_helpers, init_config2, init_error2, init_fs2, templates_exports, __dirname, TEMPLATES_DIR, SCAFFOLD_STACK_NAME, init_templates, project_detect_exports, COMMANDS, MAX_WORKSPACE_DEPTH, CDK_DEPENDENCIES, init_project_detect, task_queue_config_exports, init_task_queue_config, LOG_LEVELS, DebugLogger, testLogger, init_debug, init_atomic_write, DEFAULT_MAX_RETRIES, nullLogger, init_task_queue, init_task_queue2, task_queue_exports, debugLogger, cmsTaskQueueLogger, init_task_queue3, ProdClientSafeStrategy, DevClientSafeStrategy, active, warned, VALID_DEPLOYMENT_NAME, isValidDeploymentName, ProdStrategy, DevStrategy, strategyCache;
868
1051
  var init_init = __esm({
869
1052
  "dist/cli/init.js"() {
870
1053
  "use strict";
@@ -1116,6 +1299,16 @@ var init_init = __esm({
1116
1299
  allowNetworkRemoteInProd: z4.boolean().optional(),
1117
1300
  settingsBranch: z4.string().optional(),
1118
1301
  autoCreateSettingsPR: z4.boolean().optional(),
1302
+ // Deliberately `deploymentNameSchema.optional()`, NOT `deploymentNameSchema` alone.
1303
+ // deploymentNameSchema.default('prod') would make `parse(undefined)` resolve to
1304
+ // the literal 'prod' instead of staying `undefined` — collapsing the
1305
+ // env > config > modeDefault precedence chain that resolveDeploymentName
1306
+ // (operating-mode/deployment-name.ts) implements: config would then always
1307
+ // "win" over modeDefault (masking dev's real default of 'local'), and the env
1308
+ // var would just be racing config's baked-in 'prod' instead of a true absence.
1309
+ // `.optional()` here is what keeps an omitted deploymentName reaching
1310
+ // resolveDeploymentName as `undefined`, so it can fall through to modeDefault.
1311
+ // Do not "fix" this by removing `.optional()`.
1119
1312
  deploymentName: deploymentNameSchema.optional(),
1120
1313
  contentRoot: contentRootSchema.default("content"),
1121
1314
  sourceRoot: sourceRootSchema.optional(),
@@ -1187,13 +1380,28 @@ var init_init = __esm({
1187
1380
  init_helpers();
1188
1381
  }
1189
1382
  });
1383
+ init_error2 = __esm2({
1384
+ "dist/utils/error.js"() {
1385
+ "use strict";
1386
+ }
1387
+ });
1388
+ init_fs2 = __esm2({
1389
+ "dist/utils/fs.js"() {
1390
+ "use strict";
1391
+ init_error2();
1392
+ }
1393
+ });
1190
1394
  templates_exports = {};
1191
1395
  __export2(templates_exports, {
1396
+ SCAFFOLD_STACK_NAME: () => SCAFFOLD_STACK_NAME,
1192
1397
  aiConfig: () => aiConfig,
1193
1398
  aiRoute: () => aiRoute,
1194
1399
  apiRoute: () => apiRoute,
1195
1400
  canopyCmsConfig: () => canopyCmsConfig,
1196
1401
  canopyContext: () => canopyContext,
1402
+ cdkApp: () => cdkApp,
1403
+ cdkJson: () => cdkJson,
1404
+ cmsStack: () => cmsStack,
1197
1405
  dockerfileCms: () => dockerfileCms,
1198
1406
  dockerignore: () => dockerignore,
1199
1407
  editPage: () => editPage,
@@ -1207,6 +1415,65 @@ var init_init = __esm({
1207
1415
  "use strict";
1208
1416
  __dirname = path2.dirname(fileURLToPath(import.meta.url));
1209
1417
  TEMPLATES_DIR = path2.join(__dirname, "template-files");
1418
+ SCAFFOLD_STACK_NAME = "CanopyCms";
1419
+ }
1420
+ });
1421
+ project_detect_exports = {};
1422
+ __export2(project_detect_exports, {
1423
+ CDK_DEPENDENCIES: () => CDK_DEPENDENCIES,
1424
+ commandsFor: () => commandsFor,
1425
+ detectDefaultBranch: () => detectDefaultBranch,
1426
+ detectGitHubRepo: () => detectGitHubRepo,
1427
+ detectPackageManager: () => detectPackageManager,
1428
+ missingCdkDependencies: () => missingCdkDependencies,
1429
+ readPackageJson: () => readPackageJson
1430
+ });
1431
+ init_project_detect = __esm2({
1432
+ "dist/cli/project-detect.js"() {
1433
+ "use strict";
1434
+ init_fs2();
1435
+ COMMANDS = {
1436
+ npm: {
1437
+ lockfile: "package-lock.json",
1438
+ ciInstall: "npm ci",
1439
+ dockerCopy: "COPY package*.json ./",
1440
+ dockerInstall: "RUN npm ci",
1441
+ build: "npm run build",
1442
+ addDev: "npm install --save-dev"
1443
+ },
1444
+ pnpm: {
1445
+ lockfile: "pnpm-lock.yaml",
1446
+ ciInstall: "corepack enable && pnpm install --frozen-lockfile",
1447
+ dockerCopy: "COPY package.json pnpm-lock.yaml ./",
1448
+ dockerInstall: "RUN corepack enable && pnpm install --frozen-lockfile",
1449
+ build: "pnpm run build",
1450
+ addDev: "pnpm add -D"
1451
+ },
1452
+ yarn: {
1453
+ lockfile: "yarn.lock",
1454
+ ciInstall: "corepack enable && yarn install --frozen-lockfile",
1455
+ dockerCopy: "COPY package.json yarn.lock ./",
1456
+ dockerInstall: "RUN corepack enable && yarn install --frozen-lockfile",
1457
+ build: "yarn build",
1458
+ addDev: "yarn add -D"
1459
+ },
1460
+ "yarn-berry": {
1461
+ lockfile: "yarn.lock",
1462
+ ciInstall: "corepack enable && yarn install --immutable",
1463
+ dockerCopy: "COPY package.json yarn.lock .yarnrc.yml ./",
1464
+ dockerInstall: "RUN corepack enable && yarn install --immutable",
1465
+ build: "yarn build",
1466
+ addDev: "yarn add -D"
1467
+ }
1468
+ };
1469
+ MAX_WORKSPACE_DEPTH = 8;
1470
+ CDK_DEPENDENCIES = [
1471
+ "canopycms-cdk",
1472
+ "aws-cdk-lib",
1473
+ "constructs",
1474
+ "tsx",
1475
+ "aws-cdk"
1476
+ ];
1210
1477
  }
1211
1478
  });
1212
1479
  task_queue_config_exports = {};
@@ -1240,10 +1507,21 @@ var init_init = __esm({
1240
1507
  const minLevel = this.options.minLevel ?? "DEBUG";
1241
1508
  return LOG_LEVELS[level] >= LOG_LEVELS[minLevel];
1242
1509
  }
1510
+ /**
1511
+ * The ISO-8601 timestamp leads the line BARE - not wrapped in brackets as it
1512
+ * once was. `CANOPYCMS_DEBUG=true` can be set on the EC2 worker, whose stdout
1513
+ * appends to /var/log/canopy-worker/worker.log; the CloudWatch agent's
1514
+ * `multi_line_start_pattern` matches the timestamp at the START of a line
1515
+ * (see worker/log.ts's INVARIANT and cms-service.ts's agent config), so a
1516
+ * leading `[` meant every debug line was silently folded into the previous
1517
+ * event instead of starting its own. The bracketed `[prefix:category]` and
1518
+ * `[level]` fields still follow, so the human-readable shape is unchanged
1519
+ * apart from those two characters.
1520
+ */
1243
1521
  formatMessage(level, category, message) {
1244
1522
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
1245
1523
  const prefix = this.options.prefix ?? "CanopyCMS";
1246
- return `[${timestamp}] [${prefix}:${category}] [${level}] ${message}`;
1524
+ return `${timestamp} [${prefix}:${category}] [${level}] ${message}`;
1247
1525
  }
1248
1526
  debug(category, message, data) {
1249
1527
  if (this.shouldLog("DEBUG")) {
@@ -1422,6 +1700,10 @@ var init_init = __esm({
1422
1700
  return true;
1423
1701
  }
1424
1702
  };
1703
+ active = console;
1704
+ warned = false;
1705
+ VALID_DEPLOYMENT_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
1706
+ isValidDeploymentName = (name) => VALID_DEPLOYMENT_NAME.test(name) && !name.includes("..") && !name.endsWith(".") && !name.endsWith(".lock");
1425
1707
  init_config2();
1426
1708
  ProdStrategy = class extends ProdClientSafeStrategy {
1427
1709
  // All client-safe methods inherited automatically from ProdClientSafeStrategy:
@@ -1430,32 +1712,32 @@ var init_init = __esm({
1430
1712
  // - shouldCommit(), shouldPush()
1431
1713
  // Add client-unsafe methods (use Node.js APIs)
1432
1714
  getWorkspaceRoot(_sourceRoot) {
1433
- return path7.resolve(process.env.CANOPYCMS_WORKSPACE_ROOT ?? DEFAULT_PROD_WORKSPACE);
1715
+ return path8.resolve(process.env.CANOPYCMS_WORKSPACE_ROOT ?? DEFAULT_PROD_WORKSPACE);
1434
1716
  }
1435
- getContentRoot(sourceRoot) {
1436
- return path7.resolve(sourceRoot ?? process.cwd(), "content");
1717
+ getContentRoot(contentRoot, sourceRoot) {
1718
+ return path8.resolve(sourceRoot ?? process.cwd(), contentRoot);
1437
1719
  }
1438
1720
  getContentBranchesRoot(sourceRoot) {
1439
- return path7.join(this.getWorkspaceRoot(sourceRoot), "content-branches");
1721
+ return path8.join(this.getWorkspaceRoot(sourceRoot), "content-branches");
1440
1722
  }
1441
1723
  getContentBranchRoot(branchName, sourceRoot) {
1442
- return path7.resolve(this.getContentBranchesRoot(sourceRoot), branchName);
1724
+ return path8.resolve(this.getContentBranchesRoot(sourceRoot), branchName);
1443
1725
  }
1444
1726
  getGitExcludePattern() {
1445
1727
  return ".canopy-meta/";
1446
1728
  }
1447
1729
  getPermissionsFilePath(root) {
1448
- return path7.join(root, this.getPermissionsFileName());
1730
+ return path8.join(root, this.getPermissionsFileName());
1449
1731
  }
1450
1732
  getGroupsFilePath(root) {
1451
- return path7.join(root, this.getGroupsFileName());
1733
+ return path8.join(root, this.getGroupsFileName());
1452
1734
  }
1453
1735
  getRemoteUrlConfig() {
1454
1736
  return {
1455
1737
  shouldAutoInitLocal: false,
1456
1738
  defaultRemotePath: "",
1457
1739
  envVarName: "CANOPYCMS_REMOTE_URL",
1458
- autoDetectRemotePath: path7.join(this.getWorkspaceRoot(), "remote.git")
1740
+ autoDetectRemotePath: path8.join(this.getWorkspaceRoot(), "remote.git")
1459
1741
  };
1460
1742
  }
1461
1743
  requiresExistingRepo() {
@@ -1464,11 +1746,10 @@ var init_init = __esm({
1464
1746
  getSettingsBranchName(config) {
1465
1747
  if (config.settingsBranch)
1466
1748
  return config.settingsBranch;
1467
- const deploymentName = config.deploymentName ?? "prod";
1468
- return `canopycms-settings-${deploymentName}`;
1749
+ return `canopycms-settings-${resolveDeploymentName(config, "prod")}`;
1469
1750
  }
1470
1751
  getSettingsRoot(sourceRoot) {
1471
- return path7.join(this.getWorkspaceRoot(sourceRoot), "settings");
1752
+ return path8.join(this.getWorkspaceRoot(sourceRoot), "settings");
1472
1753
  }
1473
1754
  usesSeparateSettingsBranch() {
1474
1755
  return true;
@@ -1485,25 +1766,25 @@ var init_init = __esm({
1485
1766
  DevStrategy = class extends DevClientSafeStrategy {
1486
1767
  // Inherits client-safe methods from DevClientSafeStrategy
1487
1768
  getWorkspaceRoot(sourceRoot) {
1488
- return path7.resolve(sourceRoot ?? process.cwd(), ".canopy-dev");
1769
+ return path8.resolve(sourceRoot ?? process.cwd(), ".canopy-dev");
1489
1770
  }
1490
- getContentRoot(sourceRoot) {
1491
- return path7.resolve(sourceRoot ?? process.cwd(), "content");
1771
+ getContentRoot(contentRoot, sourceRoot) {
1772
+ return path8.resolve(sourceRoot ?? process.cwd(), contentRoot);
1492
1773
  }
1493
1774
  getContentBranchesRoot(sourceRoot) {
1494
- return path7.join(this.getWorkspaceRoot(sourceRoot), "content-branches");
1775
+ return path8.join(this.getWorkspaceRoot(sourceRoot), "content-branches");
1495
1776
  }
1496
1777
  getContentBranchRoot(branchName, sourceRoot) {
1497
- return path7.resolve(this.getContentBranchesRoot(sourceRoot), branchName);
1778
+ return path8.resolve(this.getContentBranchesRoot(sourceRoot), branchName);
1498
1779
  }
1499
1780
  getGitExcludePattern() {
1500
1781
  return ".canopy-meta/";
1501
1782
  }
1502
1783
  getPermissionsFilePath(root) {
1503
- return path7.join(root, this.getPermissionsFileName());
1784
+ return path8.join(root, this.getPermissionsFileName());
1504
1785
  }
1505
1786
  getGroupsFilePath(root) {
1506
- return path7.join(root, this.getGroupsFileName());
1787
+ return path8.join(root, this.getGroupsFileName());
1507
1788
  }
1508
1789
  getRemoteUrlConfig() {
1509
1790
  return {
@@ -1518,11 +1799,10 @@ var init_init = __esm({
1518
1799
  getSettingsBranchName(config) {
1519
1800
  if (config.settingsBranch)
1520
1801
  return config.settingsBranch;
1521
- const deploymentName = config.deploymentName ?? "local";
1522
- return `canopycms-settings-${deploymentName}`;
1802
+ return `canopycms-settings-${resolveDeploymentName(config, "local")}`;
1523
1803
  }
1524
1804
  getSettingsRoot(sourceRoot) {
1525
- return path7.join(this.getWorkspaceRoot(sourceRoot), "settings");
1805
+ return path8.join(this.getWorkspaceRoot(sourceRoot), "settings");
1526
1806
  }
1527
1807
  usesSeparateSettingsBranch() {
1528
1808
  return true;
@@ -1534,6 +1814,8 @@ var init_init = __esm({
1534
1814
  }
1535
1815
  };
1536
1816
  strategyCache = /* @__PURE__ */ new Map();
1817
+ init_fs2();
1818
+ init_error2();
1537
1819
  }
1538
1820
  });
1539
1821
 
@@ -1555,12 +1837,12 @@ import { createJiti as createJiti2 } from "jiti";
1555
1837
  import fs16 from "node:fs/promises";
1556
1838
  import path17 from "node:path";
1557
1839
  import fs8 from "node:fs/promises";
1558
- import path8 from "node:path";
1840
+ import path82 from "node:path";
1559
1841
  import matter from "gray-matter";
1560
1842
  import { parse as yamlParse, stringify as yamlStringify } from "yaml";
1561
1843
  import fs7 from "node:fs/promises";
1562
1844
  import path9 from "node:path";
1563
- import fs22 from "node:fs/promises";
1845
+ import fs23 from "node:fs/promises";
1564
1846
  import path22 from "node:path";
1565
1847
  import { generate } from "short-uuid";
1566
1848
  import path32 from "node:path";
@@ -1592,7 +1874,7 @@ import lockfile from "proper-lockfile";
1592
1874
  import fs14 from "node:fs/promises";
1593
1875
  import path14 from "node:path";
1594
1876
  import { simpleGit as simpleGit2 } from "simple-git";
1595
- import { simpleGit } from "simple-git";
1877
+ import { simpleGit as simpleGit3 } from "simple-git";
1596
1878
  import fs13 from "node:fs/promises";
1597
1879
  import path13 from "node:path";
1598
1880
  import lockfile2 from "proper-lockfile";
@@ -1623,6 +1905,12 @@ function trimSlashes(path192) {
1623
1905
  end--;
1624
1906
  return path192.slice(start, end);
1625
1907
  }
1908
+ function canopyLogWarn2(...args) {
1909
+ active2.warn(...args);
1910
+ }
1911
+ function canopyLogError(...args) {
1912
+ active2.error(...args);
1913
+ }
1626
1914
  function clientOperatingStrategy(mode) {
1627
1915
  const cached = clientStrategyCache.get(mode);
1628
1916
  if (cached)
@@ -1646,6 +1934,20 @@ function clientOperatingStrategy(mode) {
1646
1934
  function clearClientStrategyCache() {
1647
1935
  clientStrategyCache.clear();
1648
1936
  }
1937
+ function resolveDeploymentName2(config, modeDefault) {
1938
+ const envValue = process.env.CANOPYCMS_DEPLOYMENT_NAME?.trim();
1939
+ const configValue = config.deploymentName;
1940
+ if (envValue && configValue && envValue !== configValue && !warned2) {
1941
+ canopyLogWarn2(`CanopyCMS: CANOPYCMS_DEPLOYMENT_NAME ("${envValue}") differs from config.deploymentName ("${configValue}") \u2014 using the env var (infra-stamped env wins over shared-repo config by design; see resolveDeploymentName's doc comment). Update config.deploymentName to match if this mismatch was unintentional.`);
1942
+ warned2 = true;
1943
+ }
1944
+ const resolved = envValue || configValue || modeDefault;
1945
+ if (!isValidDeploymentName2(resolved)) {
1946
+ const source = envValue ? "CANOPYCMS_DEPLOYMENT_NAME" : configValue ? "config.deploymentName" : "the mode default";
1947
+ throw new Error(`CanopyCMS: invalid deploymentName ${JSON.stringify(resolved)} (from ${source}). It is interpolated into the settings branch name (canopycms-settings-<deploymentName>), so it must start with a letter or digit, contain only letters, digits, '.', '_' or '-', and must not contain '..' or end with '.' or '.lock'.`);
1948
+ }
1949
+ return resolved;
1950
+ }
1649
1951
  function operatingStrategy2(mode) {
1650
1952
  const cached = strategyCache2.get(mode);
1651
1953
  if (cached)
@@ -1735,7 +2037,7 @@ function getErrorMessage3(err) {
1735
2037
  function isNodeError3(err) {
1736
2038
  return err instanceof Error && "code" in err;
1737
2039
  }
1738
- function isNotFoundError4(err) {
2040
+ function isNotFoundError3(err) {
1739
2041
  return isNodeError3(err) && err.code === "ENOENT";
1740
2042
  }
1741
2043
  function isFileExistsError2(err) {
@@ -1783,7 +2085,7 @@ async function resolveCollectionPath(root, logicalPath) {
1783
2085
  return null;
1784
2086
  }
1785
2087
  } catch (err) {
1786
- if (isNotFoundError4(err))
2088
+ if (isNotFoundError3(err))
1787
2089
  return null;
1788
2090
  throw err;
1789
2091
  }
@@ -1923,7 +2225,7 @@ async function scanForCollectionMeta(baseDir, relativePath = "") {
1923
2225
  collections.push(...nestedCollections);
1924
2226
  } catch (err) {
1925
2227
  if (err.code !== "ENOENT") {
1926
- console.error(`Error loading ${metaPath}:`, err);
2228
+ canopyLogError(`Error loading ${metaPath}:`, err);
1927
2229
  throw new Error(`Invalid .collection.json in ${folderPath}: ${err.message}`);
1928
2230
  }
1929
2231
  const nestedCollections = await scanForCollectionMeta(absolutePath, folderPath);
@@ -2075,7 +2377,7 @@ async function filePathExists3(filePath) {
2075
2377
  await fs62.stat(filePath);
2076
2378
  return true;
2077
2379
  } catch (err) {
2078
- if (isNotFoundError4(err))
2380
+ if (isNotFoundError3(err))
2079
2381
  return false;
2080
2382
  throw err;
2081
2383
  }
@@ -3149,7 +3451,7 @@ function isNetworkRemoteUrl(url) {
3149
3451
  }
3150
3452
  async function detectHeadBranch(repoRoot, fallback = "main") {
3151
3453
  try {
3152
- const git = simpleGit({ baseDir: repoRoot });
3454
+ const git = simpleGit3({ baseDir: repoRoot });
3153
3455
  const head = (await git.revparse(["--abbrev-ref", "HEAD"])).trim();
3154
3456
  return head && head !== "HEAD" ? head : fallback;
3155
3457
  } catch {
@@ -3178,7 +3480,7 @@ function gitChildEnv(overrides) {
3178
3480
  if (value !== void 0 && GIT_ENV_PASSTHROUGH.test(key))
3179
3481
  env[key] = value;
3180
3482
  }
3181
- return { ...env, ...overrides };
3483
+ return { ...env, ...FORCE_C_LOCALE, ...overrides };
3182
3484
  }
3183
3485
  async function ensureGitExcludePattern(repoPath, pattern) {
3184
3486
  const excludePath = path14.join(repoPath, ".git", "info", "exclude");
@@ -3187,7 +3489,7 @@ async function ensureGitExcludePattern(repoPath, pattern) {
3187
3489
  try {
3188
3490
  content = await fs14.readFile(excludePath, "utf-8");
3189
3491
  } catch (err) {
3190
- if (!isNotFoundError4(err))
3492
+ if (!isNotFoundError3(err))
3191
3493
  throw err;
3192
3494
  }
3193
3495
  const lines = content.split("\n");
@@ -3567,7 +3869,7 @@ async function generateAIContentFiles(options) {
3567
3869
  }
3568
3870
  const entriesForValidation = await listEntries(branchRoot, flatSchema, contentRootName);
3569
3871
  assertBuildEntriesValid(entriesForValidation, "AI content generation");
3570
- const store = new ContentStore(branchRoot, flatSchema);
3872
+ const store = new ContentStore(branchRoot, flatSchema, { contentRootName });
3571
3873
  const result = await generateAIContent({
3572
3874
  store,
3573
3875
  flatSchema,
@@ -3644,7 +3946,7 @@ async function generateAIContentCLI(options) {
3644
3946
  console.log(` Output: ${result.outputDir}
3645
3947
  `);
3646
3948
  }
3647
- var __defProp3, __getOwnPropNames3, __esm3, __export3, init_normalize2, ROOT_COLLECTION_ID, init_types3, normalizePathValue, flattenSchema, init_flatten2, ProdClientSafeStrategy2, DevClientSafeStrategy2, clientStrategyCache, init_client_safe_strategy, primitiveFieldTypes2, fieldTypes2, init_types22, fieldBaseSchema2, selectOptionSchema2, referenceOptionSchema2, primitiveFieldSchema2, ASPECT_RATIO_RE2, imageFieldSchema2, selectFieldSchema2, referenceFieldSchema2, fieldHolder2, blockSchema2, blockFieldSchema2, objectFieldSchema2, inlineGroupFieldSchema2, customFieldSchema2, knownFieldSchema2, fieldSchema2, init_field2, relativePathSchema2, entryTypeSchema2, collectionSchema2, init_collection2, mediaSchema2, init_media2, defaultBranchAccessSchema2, defaultPathAccessLevelSchema2, defaultPathAccessLevelsSchema2, defaultPathAccessSchema2, defaultBaseBranchSchema2, defaultRemoteNameSchema2, defaultRemoteUrlSchema2, gitBotAuthorNameSchema2, gitBotAuthorEmailSchema2, githubTokenEnvVarSchema2, operatingModeSchema2, deployedAsSchema2, contentRootSchema2, sourceRootSchema2, deploymentNameSchema2, devContentSyncSchema2, devConfigSchema2, editorConfigSchema2, CanopyConfigSchema2, DEFAULT_PROD_WORKSPACE2, init_config3, permissionTargetSchema2, pathPermissionSchema2, init_permissions2, init_validation2, init_helpers2, init_config22, ProdStrategy2, DevStrategy2, strategyCache2, init_client_unsafe_strategy, operating_mode_exports, init_operating_mode, locks, BASE58_PATTERN, CONTENT_ID_PATTERN, PHYSICAL_SEGMENT_PATTERN, isValidId, EMPTY_LOGICAL_PATH, ContentIdIndex, registry, finalization, LOG_LEVELS2, DebugLogger2, testLogger2, log2, META_DIR, entryTypeMetaSchema, collectionMetaSchema, rootCollectionMetaSchema, isDeployedStatic, isBuildMode, STATIC_DEPLOY_USER, SCHEMA_CACHE_VERSION, MTIME_CHECK_DEBOUNCE_MS, SCHEMA_GENERATION_RESOURCE, BranchSchemaCache, RESOURCE, getFormatExtension, isDataOnlyFormat, asRecord, BranchPathError, resolveContentBranchesRoot, ContentStoreError, ContentConflictError, DEFAULT_INDEX_FRESHNESS_INTERVAL_MS, FORCED_REFRESH_MIN_INTERVAL_MS, STALE_LOOKUP, ContentStore, ATTR_CONTENT, BLOCK_PREFIX, BLOCK_SUFFIX, BLOCK_RESTORE_RE, INLINE_PREFIX, INLINE_SUFFIX, INLINE_RESTORE_RE, FLAT_CELL_TYPES, log22, BASE58_CHAR, ENTRY_LINK_PATTERN, log3, REGISTRY_FILE, REGISTRY_STALE_FILE, REGISTRY_TEMP_FILE, REGISTRY_VERSION, RESOURCE2, GET_MISS_REFRESH_MIN_INTERVAL_MS, BranchRegistry, log4, OccWriteConflictError, BRANCH_META_DIR, BRANCH_META_FILE, CURRENT_SCHEMA_VERSION, BranchMetadataConflictError, BranchMetadataCorruptError, BranchMetadataFileManager, getBranchMetadataFileManager, loadBranchContext, NETWORK_SCHEME_PATTERN, TRANSPORT_HELPER_PATTERN, LEADING_OPTION_PATTERN, WINDOWS_DRIVE_PATTERN, COLON_BEFORE_SLASH_PATTERN, log5, GIT_ENV_PASSTHROUGH, remoteInitLocks, GitConflictError, GitManager, log6, workspaceInitLocks, BranchWorkspaceManager, log7, normalizePath, readEntryData, parseTypedFilename, listCollectionEntries, MAX_INPUT_PIXELS, KNOWN_FIELD_TYPES, STRING_FIELD_TYPES, selectOptionValues, jiti;
3949
+ var __defProp3, __getOwnPropNames3, __esm3, __export3, init_normalize2, active2, init_logger, ROOT_COLLECTION_ID, init_types3, normalizePathValue, flattenSchema, init_flatten2, primitiveFieldTypes2, fieldTypes2, init_types22, fieldBaseSchema2, selectOptionSchema2, referenceOptionSchema2, primitiveFieldSchema2, ASPECT_RATIO_RE2, imageFieldSchema2, selectFieldSchema2, referenceFieldSchema2, fieldHolder2, blockSchema2, blockFieldSchema2, objectFieldSchema2, inlineGroupFieldSchema2, customFieldSchema2, knownFieldSchema2, fieldSchema2, init_field2, relativePathSchema2, entryTypeSchema2, collectionSchema2, init_collection2, mediaSchema2, init_media2, defaultBranchAccessSchema2, defaultPathAccessLevelSchema2, defaultPathAccessLevelsSchema2, defaultPathAccessSchema2, defaultBaseBranchSchema2, defaultRemoteNameSchema2, defaultRemoteUrlSchema2, gitBotAuthorNameSchema2, gitBotAuthorEmailSchema2, githubTokenEnvVarSchema2, operatingModeSchema2, deployedAsSchema2, contentRootSchema2, sourceRootSchema2, deploymentNameSchema2, devContentSyncSchema2, devConfigSchema2, editorConfigSchema2, CanopyConfigSchema2, DEFAULT_PROD_WORKSPACE2, init_config3, forEachReferenceField, init_validation2, ProdClientSafeStrategy2, DevClientSafeStrategy2, clientStrategyCache, init_client_safe_strategy, warned2, VALID_DEPLOYMENT_NAME2, isValidDeploymentName2, init_deployment_name, permissionTargetSchema2, pathPermissionSchema2, init_permissions2, init_helpers2, init_config22, ProdStrategy2, DevStrategy2, strategyCache2, init_client_unsafe_strategy, operating_mode_exports, init_operating_mode, locks, BASE58_PATTERN, CONTENT_ID_PATTERN, PHYSICAL_SEGMENT_PATTERN, isValidId, EMPTY_LOGICAL_PATH, ContentIdIndex, registry, finalization, LOG_LEVELS2, DebugLogger2, testLogger2, log2, META_DIR, entryTypeMetaSchema, collectionMetaSchema, rootCollectionMetaSchema, editDistance, closestName, forEachEntryType, collectEntryTypeNames, validateReferenceEntryTypes, isDeployedStatic, isBuildMode, STATIC_DEPLOY_USER, SCHEMA_CACHE_VERSION, MTIME_CHECK_DEBOUNCE_MS, SCHEMA_GENERATION_RESOURCE, BranchSchemaCache, RESOURCE, getFormatExtension, isDataOnlyFormat, asRecord, BranchPathError, resolveContentBranchesRoot, ContentStoreError, ContentConflictError, DEFAULT_INDEX_FRESHNESS_INTERVAL_MS, FORCED_REFRESH_MIN_INTERVAL_MS, STALE_LOOKUP, ContentStore, ATTR_CONTENT, BLOCK_PREFIX, BLOCK_SUFFIX, BLOCK_RESTORE_RE, INLINE_PREFIX, INLINE_SUFFIX, INLINE_RESTORE_RE, FLAT_CELL_TYPES, log22, BASE58_CHAR, ENTRY_LINK_PATTERN, log3, REGISTRY_FILE, REGISTRY_STALE_FILE, REGISTRY_TEMP_FILE, REGISTRY_VERSION, RESOURCE2, GET_MISS_REFRESH_MIN_INTERVAL_MS, BranchRegistry, log4, OccWriteConflictError, BRANCH_META_DIR, BRANCH_META_FILE, CURRENT_SCHEMA_VERSION, BranchMetadataConflictError, BranchMetadataCorruptError, BranchMetadataFileManager, getBranchMetadataFileManager, loadBranchContext, NETWORK_SCHEME_PATTERN, TRANSPORT_HELPER_PATTERN, LEADING_OPTION_PATTERN, WINDOWS_DRIVE_PATTERN, COLON_BEFORE_SLASH_PATTERN, log5, GIT_ENV_PASSTHROUGH, FORCE_C_LOCALE, remoteInitLocks, GITHUB_TRACKING_REF_PREFIX, GitConflictError, GitManager, log6, workspaceInitLocks, BranchWorkspaceManager, log7, normalizePath, readEntryData, parseTypedFilename, listCollectionEntries, MAX_INPUT_PIXELS, KNOWN_FIELD_TYPES, STRING_FIELD_TYPES, selectOptionValues, jiti;
3648
3950
  var init_generate_ai_content = __esm({
3649
3951
  "dist/cli/generate-ai-content.js"() {
3650
3952
  "use strict";
@@ -3662,6 +3964,12 @@ var init_generate_ai_content = __esm({
3662
3964
  "use strict";
3663
3965
  }
3664
3966
  });
3967
+ init_logger = __esm3({
3968
+ "dist/utils/logger.js"() {
3969
+ "use strict";
3970
+ active2 = console;
3971
+ }
3972
+ });
3665
3973
  init_types3 = __esm3({
3666
3974
  "dist/paths/types.js"() {
3667
3975
  "use strict";
@@ -3767,74 +4075,6 @@ var init_generate_ai_content = __esm({
3767
4075
  };
3768
4076
  }
3769
4077
  });
3770
- init_client_safe_strategy = __esm3({
3771
- "dist/operating-mode/client-safe-strategy.js"() {
3772
- "use strict";
3773
- ProdClientSafeStrategy2 = class {
3774
- constructor() {
3775
- this.mode = "prod";
3776
- }
3777
- // UI Feature Flags
3778
- supportsBranching() {
3779
- return true;
3780
- }
3781
- supportsStatusBadge() {
3782
- return true;
3783
- }
3784
- supportsComments() {
3785
- return true;
3786
- }
3787
- supportsPullRequests() {
3788
- return true;
3789
- }
3790
- // Simple Data
3791
- getPermissionsFileName() {
3792
- return "permissions.json";
3793
- }
3794
- getGroupsFileName() {
3795
- return "groups.json";
3796
- }
3797
- shouldCommit() {
3798
- return true;
3799
- }
3800
- shouldPush() {
3801
- return true;
3802
- }
3803
- };
3804
- DevClientSafeStrategy2 = class {
3805
- constructor() {
3806
- this.mode = "dev";
3807
- }
3808
- // UI Feature Flags
3809
- supportsBranching() {
3810
- return true;
3811
- }
3812
- supportsStatusBadge() {
3813
- return true;
3814
- }
3815
- supportsComments() {
3816
- return true;
3817
- }
3818
- supportsPullRequests() {
3819
- return false;
3820
- }
3821
- // Simple Data
3822
- getPermissionsFileName() {
3823
- return "permissions.json";
3824
- }
3825
- getGroupsFileName() {
3826
- return "groups.json";
3827
- }
3828
- shouldCommit() {
3829
- return true;
3830
- }
3831
- shouldPush() {
3832
- return true;
3833
- }
3834
- };
3835
- clientStrategyCache = /* @__PURE__ */ new Map();
3836
- }
3837
- });
3838
4078
  init_types22 = __esm3({
3839
4079
  "dist/config/types.js"() {
3840
4080
  "use strict";
@@ -4074,6 +4314,16 @@ var init_generate_ai_content = __esm({
4074
4314
  allowNetworkRemoteInProd: z52.boolean().optional(),
4075
4315
  settingsBranch: z52.string().optional(),
4076
4316
  autoCreateSettingsPR: z52.boolean().optional(),
4317
+ // Deliberately `deploymentNameSchema.optional()`, NOT `deploymentNameSchema` alone.
4318
+ // deploymentNameSchema.default('prod') would make `parse(undefined)` resolve to
4319
+ // the literal 'prod' instead of staying `undefined` — collapsing the
4320
+ // env > config > modeDefault precedence chain that resolveDeploymentName
4321
+ // (operating-mode/deployment-name.ts) implements: config would then always
4322
+ // "win" over modeDefault (masking dev's real default of 'local'), and the env
4323
+ // var would just be racing config's baked-in 'prod' instead of a true absence.
4324
+ // `.optional()` here is what keeps an omitted deploymentName reaching
4325
+ // resolveDeploymentName as `undefined`, so it can fall through to modeDefault.
4326
+ // Do not "fix" this by removing `.optional()`.
4077
4327
  deploymentName: deploymentNameSchema2.optional(),
4078
4328
  contentRoot: contentRootSchema2.default("content"),
4079
4329
  sourceRoot: sourceRootSchema2.optional(),
@@ -4086,42 +4336,137 @@ var init_generate_ai_content = __esm({
4086
4336
  DEFAULT_PROD_WORKSPACE2 = "/mnt/efs/workspace";
4087
4337
  }
4088
4338
  });
4089
- init_permissions2 = __esm3({
4090
- "dist/config/schemas/permissions.js"() {
4091
- "use strict";
4092
- permissionTargetSchema2 = z6.object({
4093
- allowedUsers: z6.array(z6.string()).optional(),
4094
- allowedGroups: z6.array(z6.string()).optional()
4095
- });
4096
- pathPermissionSchema2 = z6.object({
4097
- path: z6.string().min(1),
4098
- read: permissionTargetSchema2.optional(),
4099
- edit: permissionTargetSchema2.optional(),
4100
- review: permissionTargetSchema2.optional()
4101
- });
4102
- }
4103
- });
4104
4339
  init_validation2 = __esm3({
4105
4340
  "dist/config/validation.js"() {
4106
4341
  "use strict";
4107
4342
  init_config3();
4108
4343
  init_flatten2();
4344
+ forEachReferenceField = (fields, visit) => {
4345
+ if (!Array.isArray(fields))
4346
+ return;
4347
+ for (const field of fields) {
4348
+ const f = field;
4349
+ if (f?.type === "reference") {
4350
+ visit(f);
4351
+ }
4352
+ if (f?.type === "group" || f?.type === "object") {
4353
+ forEachReferenceField(f.fields, visit);
4354
+ }
4355
+ if (f?.type === "block" && Array.isArray(f.templates)) {
4356
+ for (const template of f.templates) {
4357
+ forEachReferenceField(template.fields, visit);
4358
+ }
4359
+ }
4360
+ }
4361
+ };
4109
4362
  }
4110
4363
  });
4111
- init_helpers2 = __esm3({
4112
- "dist/config/helpers.js"() {
4113
- "use strict";
4114
- init_validation2();
4115
- }
4116
- });
4117
- init_config22 = __esm3({
4118
- "dist/config/index.js"() {
4364
+ init_client_safe_strategy = __esm3({
4365
+ "dist/operating-mode/client-safe-strategy.js"() {
4119
4366
  "use strict";
4120
- init_types22();
4121
- init_config3();
4122
- init_field2();
4123
- init_collection2();
4124
- init_permissions2();
4367
+ ProdClientSafeStrategy2 = class {
4368
+ constructor() {
4369
+ this.mode = "prod";
4370
+ }
4371
+ // UI Feature Flags
4372
+ supportsBranching() {
4373
+ return true;
4374
+ }
4375
+ supportsStatusBadge() {
4376
+ return true;
4377
+ }
4378
+ supportsComments() {
4379
+ return true;
4380
+ }
4381
+ supportsPullRequests() {
4382
+ return true;
4383
+ }
4384
+ // Simple Data
4385
+ getPermissionsFileName() {
4386
+ return "permissions.json";
4387
+ }
4388
+ getGroupsFileName() {
4389
+ return "groups.json";
4390
+ }
4391
+ shouldCommit() {
4392
+ return true;
4393
+ }
4394
+ shouldPush() {
4395
+ return true;
4396
+ }
4397
+ };
4398
+ DevClientSafeStrategy2 = class {
4399
+ constructor() {
4400
+ this.mode = "dev";
4401
+ }
4402
+ // UI Feature Flags
4403
+ supportsBranching() {
4404
+ return true;
4405
+ }
4406
+ supportsStatusBadge() {
4407
+ return true;
4408
+ }
4409
+ supportsComments() {
4410
+ return true;
4411
+ }
4412
+ supportsPullRequests() {
4413
+ return false;
4414
+ }
4415
+ // Simple Data
4416
+ getPermissionsFileName() {
4417
+ return "permissions.json";
4418
+ }
4419
+ getGroupsFileName() {
4420
+ return "groups.json";
4421
+ }
4422
+ shouldCommit() {
4423
+ return true;
4424
+ }
4425
+ shouldPush() {
4426
+ return true;
4427
+ }
4428
+ };
4429
+ clientStrategyCache = /* @__PURE__ */ new Map();
4430
+ }
4431
+ });
4432
+ init_deployment_name = __esm3({
4433
+ "dist/operating-mode/deployment-name.js"() {
4434
+ "use strict";
4435
+ init_logger();
4436
+ warned2 = false;
4437
+ VALID_DEPLOYMENT_NAME2 = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
4438
+ isValidDeploymentName2 = (name) => VALID_DEPLOYMENT_NAME2.test(name) && !name.includes("..") && !name.endsWith(".") && !name.endsWith(".lock");
4439
+ }
4440
+ });
4441
+ init_permissions2 = __esm3({
4442
+ "dist/config/schemas/permissions.js"() {
4443
+ "use strict";
4444
+ permissionTargetSchema2 = z6.object({
4445
+ allowedUsers: z6.array(z6.string()).optional(),
4446
+ allowedGroups: z6.array(z6.string()).optional()
4447
+ });
4448
+ pathPermissionSchema2 = z6.object({
4449
+ path: z6.string().min(1),
4450
+ read: permissionTargetSchema2.optional(),
4451
+ edit: permissionTargetSchema2.optional(),
4452
+ review: permissionTargetSchema2.optional()
4453
+ });
4454
+ }
4455
+ });
4456
+ init_helpers2 = __esm3({
4457
+ "dist/config/helpers.js"() {
4458
+ "use strict";
4459
+ init_validation2();
4460
+ }
4461
+ });
4462
+ init_config22 = __esm3({
4463
+ "dist/config/index.js"() {
4464
+ "use strict";
4465
+ init_types22();
4466
+ init_config3();
4467
+ init_field2();
4468
+ init_collection2();
4469
+ init_permissions2();
4125
4470
  init_media2();
4126
4471
  init_flatten2();
4127
4472
  init_validation2();
@@ -4132,6 +4477,7 @@ var init_generate_ai_content = __esm({
4132
4477
  "dist/operating-mode/client-unsafe-strategy.js"() {
4133
4478
  "use strict";
4134
4479
  init_client_safe_strategy();
4480
+ init_deployment_name();
4135
4481
  init_config22();
4136
4482
  ProdStrategy2 = class extends ProdClientSafeStrategy2 {
4137
4483
  // All client-safe methods inherited automatically from ProdClientSafeStrategy:
@@ -4142,8 +4488,8 @@ var init_generate_ai_content = __esm({
4142
4488
  getWorkspaceRoot(_sourceRoot) {
4143
4489
  return path62.resolve(process.env.CANOPYCMS_WORKSPACE_ROOT ?? DEFAULT_PROD_WORKSPACE2);
4144
4490
  }
4145
- getContentRoot(sourceRoot) {
4146
- return path62.resolve(sourceRoot ?? process.cwd(), "content");
4491
+ getContentRoot(contentRoot, sourceRoot) {
4492
+ return path62.resolve(sourceRoot ?? process.cwd(), contentRoot);
4147
4493
  }
4148
4494
  getContentBranchesRoot(sourceRoot) {
4149
4495
  return path62.join(this.getWorkspaceRoot(sourceRoot), "content-branches");
@@ -4174,8 +4520,7 @@ var init_generate_ai_content = __esm({
4174
4520
  getSettingsBranchName(config) {
4175
4521
  if (config.settingsBranch)
4176
4522
  return config.settingsBranch;
4177
- const deploymentName = config.deploymentName ?? "prod";
4178
- return `canopycms-settings-${deploymentName}`;
4523
+ return `canopycms-settings-${resolveDeploymentName2(config, "prod")}`;
4179
4524
  }
4180
4525
  getSettingsRoot(sourceRoot) {
4181
4526
  return path62.join(this.getWorkspaceRoot(sourceRoot), "settings");
@@ -4197,8 +4542,8 @@ var init_generate_ai_content = __esm({
4197
4542
  getWorkspaceRoot(sourceRoot) {
4198
4543
  return path62.resolve(sourceRoot ?? process.cwd(), ".canopy-dev");
4199
4544
  }
4200
- getContentRoot(sourceRoot) {
4201
- return path62.resolve(sourceRoot ?? process.cwd(), "content");
4545
+ getContentRoot(contentRoot, sourceRoot) {
4546
+ return path62.resolve(sourceRoot ?? process.cwd(), contentRoot);
4202
4547
  }
4203
4548
  getContentBranchesRoot(sourceRoot) {
4204
4549
  return path62.join(this.getWorkspaceRoot(sourceRoot), "content-branches");
@@ -4228,8 +4573,7 @@ var init_generate_ai_content = __esm({
4228
4573
  getSettingsBranchName(config) {
4229
4574
  if (config.settingsBranch)
4230
4575
  return config.settingsBranch;
4231
- const deploymentName = config.deploymentName ?? "local";
4232
- return `canopycms-settings-${deploymentName}`;
4576
+ return `canopycms-settings-${resolveDeploymentName2(config, "local")}`;
4233
4577
  }
4234
4578
  getSettingsRoot(sourceRoot) {
4235
4579
  return path62.join(this.getWorkspaceRoot(sourceRoot), "settings");
@@ -4251,13 +4595,15 @@ var init_generate_ai_content = __esm({
4251
4595
  clearClientStrategyCache: () => clearClientStrategyCache,
4252
4596
  clearStrategyCache: () => clearStrategyCache,
4253
4597
  clientOperatingStrategy: () => clientOperatingStrategy,
4254
- operatingStrategy: () => operatingStrategy2
4598
+ operatingStrategy: () => operatingStrategy2,
4599
+ resolveDeploymentName: () => resolveDeploymentName2
4255
4600
  });
4256
4601
  init_operating_mode = __esm3({
4257
4602
  "dist/operating-mode/index.js"() {
4258
4603
  "use strict";
4259
4604
  init_client_safe_strategy();
4260
4605
  init_client_unsafe_strategy();
4606
+ init_deployment_name();
4261
4607
  }
4262
4608
  });
4263
4609
  locks = /* @__PURE__ */ new Map();
@@ -4284,7 +4630,7 @@ var init_generate_ai_content = __esm({
4284
4630
  async scanDirectory(relativePath) {
4285
4631
  const absoluteDir = path22.join(this.root, relativePath);
4286
4632
  try {
4287
- const entries = await fs22.readdir(absoluteDir, { withFileTypes: true });
4633
+ const entries = await fs23.readdir(absoluteDir, { withFileTypes: true });
4288
4634
  for (const entry of entries) {
4289
4635
  if (entry.name.startsWith(".") || entry.name === "_ids_") {
4290
4636
  continue;
@@ -4529,10 +4875,21 @@ var init_generate_ai_content = __esm({
4529
4875
  const minLevel = this.options.minLevel ?? "DEBUG";
4530
4876
  return LOG_LEVELS2[level] >= LOG_LEVELS2[minLevel];
4531
4877
  }
4878
+ /**
4879
+ * The ISO-8601 timestamp leads the line BARE - not wrapped in brackets as it
4880
+ * once was. `CANOPYCMS_DEBUG=true` can be set on the EC2 worker, whose stdout
4881
+ * appends to /var/log/canopy-worker/worker.log; the CloudWatch agent's
4882
+ * `multi_line_start_pattern` matches the timestamp at the START of a line
4883
+ * (see worker/log.ts's INVARIANT and cms-service.ts's agent config), so a
4884
+ * leading `[` meant every debug line was silently folded into the previous
4885
+ * event instead of starting its own. The bracketed `[prefix:category]` and
4886
+ * `[level]` fields still follow, so the human-readable shape is unchanged
4887
+ * apart from those two characters.
4888
+ */
4532
4889
  formatMessage(level, category, message) {
4533
4890
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
4534
4891
  const prefix = this.options.prefix ?? "CanopyCMS";
4535
- return `[${timestamp}] [${prefix}:${category}] [${level}] ${message}`;
4892
+ return `${timestamp} [${prefix}:${category}] [${level}] ${message}`;
4536
4893
  }
4537
4894
  debug(category, message, data) {
4538
4895
  if (this.shouldLog("DEBUG")) {
@@ -4599,6 +4956,7 @@ var init_generate_ai_content = __esm({
4599
4956
  });
4600
4957
  log2 = createDebugLogger2({ prefix: "ResourceGeneration" });
4601
4958
  META_DIR = ".canopy-meta";
4959
+ init_logger();
4602
4960
  entryTypeMetaSchema = z7.object({
4603
4961
  name: z7.string().min(1),
4604
4962
  format: z7.enum(["md", "mdx", "json", "yaml"]),
@@ -4626,6 +4984,72 @@ var init_generate_ai_content = __esm({
4626
4984
  // Embedded IDs for ordering items
4627
4985
  });
4628
4986
  init_flatten2();
4987
+ init_validation2();
4988
+ editDistance = (a, b) => {
4989
+ let prev = Array.from({ length: b.length + 1 }, (_, i) => i);
4990
+ for (let i = 1; i <= a.length; i++) {
4991
+ const curr = [i];
4992
+ for (let j = 1; j <= b.length; j++) {
4993
+ curr[j] = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
4994
+ }
4995
+ prev = curr;
4996
+ }
4997
+ return prev[b.length];
4998
+ };
4999
+ closestName = (name, known) => {
5000
+ let best;
5001
+ let bestDistance = Infinity;
5002
+ for (const candidate of known) {
5003
+ const distance = editDistance(name.toLowerCase(), candidate.toLowerCase());
5004
+ if (distance < bestDistance) {
5005
+ bestDistance = distance;
5006
+ best = candidate;
5007
+ }
5008
+ }
5009
+ const threshold = Math.max(2, Math.floor(name.length / 3));
5010
+ return best !== void 0 && bestDistance <= threshold ? best : void 0;
5011
+ };
5012
+ forEachEntryType = (schema, visit) => {
5013
+ const walk = (node, nodePath) => {
5014
+ for (const entryType of node.entries ?? []) {
5015
+ visit(entryType, nodePath);
5016
+ }
5017
+ for (const child of node.collections ?? []) {
5018
+ walk(child, child.path ?? (nodePath ? `${nodePath}/${child.name}` : child.name));
5019
+ }
5020
+ };
5021
+ walk(schema, "");
5022
+ };
5023
+ collectEntryTypeNames = (schema) => {
5024
+ const names = /* @__PURE__ */ new Set();
5025
+ forEachEntryType(schema, (entryType) => {
5026
+ if (typeof entryType.name === "string" && entryType.name.length > 0) {
5027
+ names.add(entryType.name);
5028
+ }
5029
+ });
5030
+ return names;
5031
+ };
5032
+ validateReferenceEntryTypes = (schema) => {
5033
+ const known = collectEntryTypeNames(schema);
5034
+ const knownList = [...known].sort();
5035
+ const issues = [];
5036
+ forEachEntryType(schema, (entryType, collectionPath) => {
5037
+ const location = collectionPath ? `${collectionPath}/${entryType.name}` : entryType.name;
5038
+ forEachReferenceField(entryType.schema, (field) => {
5039
+ const entryTypes = field.entryTypes;
5040
+ if (!Array.isArray(entryTypes))
5041
+ return;
5042
+ const fieldName = typeof field.name === "string" ? field.name : "unknown";
5043
+ for (const value of entryTypes) {
5044
+ if (typeof value !== "string" || known.has(value))
5045
+ continue;
5046
+ const suggestion = closestName(value, knownList);
5047
+ issues.push(`Reference field "${fieldName}" in "${location}" specifies entryType "${value}", which is not defined in any collection.` + (suggestion ? ` Did you mean "${suggestion}"?` : "") + (knownList.length > 0 ? ` Known entry types: ${knownList.join(", ")}.` : " No entry types are defined in this schema."));
5048
+ }
5049
+ });
5050
+ });
5051
+ return issues;
5052
+ };
4629
5053
  isDeployedStatic = (config) => {
4630
5054
  return config.deployedAs === "static";
4631
5055
  };
@@ -4737,6 +5161,11 @@ var init_generate_ai_content = __esm({
4737
5161
  if (!isValidSchema(result.schema)) {
4738
5162
  throw new Error(`No schema found in ${contentRoot}. Create .collection.json files with references to field schemas defined in your entry schema registry.`);
4739
5163
  }
5164
+ const entryTypeIssues = validateReferenceEntryTypes(result.schema);
5165
+ if (entryTypeIssues.length > 0) {
5166
+ throw new Error(`Invalid reference field entryTypes in ${contentRoot}:
5167
+ ` + entryTypeIssues.map((issue) => ` - ${issue}`).join("\n"));
5168
+ }
4740
5169
  const flatSchema = flattenSchema(result.schema, contentRootName);
4741
5170
  if (!skipDiskCache) {
4742
5171
  const cacheDir = path52.join(branchRoot, ".canopy-meta");
@@ -4854,7 +5283,8 @@ var init_generate_ai_content = __esm({
4854
5283
  this.lastDiskProbeMs = 0;
4855
5284
  this.lastForcedRefreshMs = 0;
4856
5285
  this.indexBuild = null;
4857
- this.root = path8.resolve(root);
5286
+ this.root = path82.resolve(root);
5287
+ this.contentRootName = options.contentRootName || "content";
4858
5288
  this.indexFreshnessIntervalMs = options.indexFreshnessIntervalMs ?? DEFAULT_INDEX_FRESHNESS_INTERVAL_MS;
4859
5289
  this.schemaIndex = new Map(flatSchema.map((item) => [item.logicalPath, item]));
4860
5290
  this._idIndex = new ContentIdIndex(this.root);
@@ -4896,7 +5326,7 @@ var init_generate_ai_content = __esm({
4896
5326
  const build = (async () => {
4897
5327
  const diskToken = await readContentIndexGeneration(this.root);
4898
5328
  const fresh = new ContentIdIndex(this.root);
4899
- await fresh.buildFromFilenames("content");
5329
+ await fresh.buildFromFilenames(this.contentRootName);
4900
5330
  return { diskToken, fresh };
4901
5331
  })();
4902
5332
  this.indexBuild = build;
@@ -4979,7 +5409,7 @@ var init_generate_ai_content = __esm({
4979
5409
  if (entry.isDirectory())
4980
5410
  continue;
4981
5411
  if (extractIdFromFilename(entry.name) === id) {
4982
- return path8.relative(this.root, path8.join(dir, entry.name));
5412
+ return path82.relative(this.root, path82.join(dir, entry.name));
4983
5413
  }
4984
5414
  }
4985
5415
  return null;
@@ -5078,7 +5508,7 @@ var init_generate_ai_content = __esm({
5078
5508
  * @param options.entryTypeName - For collections with multiple entry types, specify which one to use. Defaults to the default entry type.
5079
5509
  */
5080
5510
  async buildPaths(schemaItem, slug, options = {}) {
5081
- const rootWithSep = this.root.endsWith(path8.sep) ? this.root : `${this.root}${path8.sep}`;
5511
+ const rootWithSep = this.root.endsWith(path82.sep) ? this.root : `${this.root}${path82.sep}`;
5082
5512
  if (schemaItem.type === "entry-type") {
5083
5513
  const parentPath = schemaItem.parentPath || "";
5084
5514
  const parentCollection = this.schemaIndex.get(parentPath);
@@ -5111,7 +5541,7 @@ var init_generate_ai_content = __esm({
5111
5541
  const entryTypeName = entryTypeConfig?.name || "entry";
5112
5542
  let collectionRoot = await resolveCollectionPath(this.root, schemaItem.logicalPath);
5113
5543
  if (!collectionRoot) {
5114
- collectionRoot = path8.resolve(this.root, schemaItem.logicalPath);
5544
+ collectionRoot = path82.resolve(this.root, schemaItem.logicalPath);
5115
5545
  }
5116
5546
  if (!collectionRoot.startsWith(rootWithSep)) {
5117
5547
  throw new ContentStoreError("Path traversal detected", "VALIDATION");
@@ -5147,14 +5577,14 @@ var init_generate_ai_content = __esm({
5147
5577
  }
5148
5578
  filename = `${finalEntryTypeName}.${safeSlug}.${id}${ext}`;
5149
5579
  }
5150
- const resolved = path8.resolve(collectionRoot, filename);
5151
- const collectionRootWithSep = collectionRoot.endsWith(path8.sep) ? collectionRoot : `${collectionRoot}${path8.sep}`;
5580
+ const resolved = path82.resolve(collectionRoot, filename);
5581
+ const collectionRootWithSep = collectionRoot.endsWith(path82.sep) ? collectionRoot : `${collectionRoot}${path82.sep}`;
5152
5582
  if (!resolved.startsWith(collectionRootWithSep)) {
5153
5583
  throw new ContentStoreError("Path traversal detected", "VALIDATION");
5154
5584
  }
5155
5585
  return {
5156
5586
  absolutePath: resolved,
5157
- relativePath: path8.relative(this.root, resolved),
5587
+ relativePath: path82.relative(this.root, resolved),
5158
5588
  id,
5159
5589
  entryTypeName: finalEntryTypeName,
5160
5590
  existed
@@ -5287,7 +5717,7 @@ var init_generate_ai_content = __esm({
5287
5717
  return RETRY_KEY;
5288
5718
  }
5289
5719
  const { absolutePath, relativePath, id } = inLock;
5290
- await fs8.mkdir(path8.dirname(absolutePath), { recursive: true });
5720
+ await fs8.mkdir(path82.dirname(absolutePath), { recursive: true });
5291
5721
  if (input.expectedVersion !== void 0) {
5292
5722
  try {
5293
5723
  const existing = await fs8.stat(absolutePath);
@@ -5304,7 +5734,7 @@ var init_generate_ai_content = __esm({
5304
5734
  }
5305
5735
  }
5306
5736
  if (existingId && !await filePathExists3(absolutePath)) {
5307
- const actualRelPath = await this.findEntryPathById(path8.dirname(absolutePath), existingId);
5737
+ const actualRelPath = await this.findEntryPathById(path82.dirname(absolutePath), existingId);
5308
5738
  const indexedRelPath = this._idIndex.findById(existingId)?.relativePath ?? null;
5309
5739
  if (actualRelPath !== null && actualRelPath !== indexedRelPath) {
5310
5740
  throw new ContentConflictError();
@@ -5326,7 +5756,7 @@ var init_generate_ai_content = __esm({
5326
5756
  const existing = liveIndex.findById(id);
5327
5757
  if (existing) {
5328
5758
  if (existing.relativePath !== relativePath) {
5329
- staleOldAbsPath = path8.join(this.root, existing.relativePath);
5759
+ staleOldAbsPath = path82.join(this.root, existing.relativePath);
5330
5760
  liveIndex.updatePath(existing.id, relativePath);
5331
5761
  }
5332
5762
  } else {
@@ -5560,7 +5990,7 @@ var init_generate_ai_content = __esm({
5560
5990
  }
5561
5991
  destLockKey = currentDestKey;
5562
5992
  const { absolutePath: currentPath, relativePath: currentRelPath } = inLock;
5563
- const currentFilename = path8.basename(currentPath);
5993
+ const currentFilename = path82.basename(currentPath);
5564
5994
  const parts = currentFilename.split(".");
5565
5995
  if (parts.length < 4) {
5566
5996
  throw new ContentStoreError(`Invalid entry filename format: ${currentFilename}`, "VALIDATION");
@@ -5569,8 +5999,8 @@ var init_generate_ai_content = __esm({
5569
5999
  const contentId = parts[parts.length - 2];
5570
6000
  const ext = `.${parts[parts.length - 1]}`;
5571
6001
  const newFilename = `${entryTypeName}.${safeNewSlug}.${contentId}${ext}`;
5572
- const parentDir = path8.dirname(currentPath);
5573
- const newPath = path8.join(parentDir, newFilename);
6002
+ const parentDir = path82.dirname(currentPath);
6003
+ const newPath = path82.join(parentDir, newFilename);
5574
6004
  try {
5575
6005
  const entries = await fs8.readdir(parentDir, { withFileTypes: true });
5576
6006
  for (const entry of entries) {
@@ -5594,7 +6024,7 @@ var init_generate_ai_content = __esm({
5594
6024
  throw err;
5595
6025
  }
5596
6026
  await fs8.unlink(currentPath);
5597
- const newRelativePath = path8.relative(this.root, newPath);
6027
+ const newRelativePath = path82.relative(this.root, newPath);
5598
6028
  const liveIndex = this._idIndex;
5599
6029
  const entryId = liveIndex.findByPath(currentRelPath);
5600
6030
  if (entryId) {
@@ -5781,6 +6211,7 @@ var init_generate_ai_content = __esm({
5781
6211
  log22 = createDebugLogger2({ prefix: "EntryLinks" });
5782
6212
  BASE58_CHAR = "[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]";
5783
6213
  ENTRY_LINK_PATTERN = new RegExp(`entry:(${BASE58_CHAR}{12})(#[^\\s)>"']*)?`, "g");
6214
+ init_logger();
5784
6215
  log3 = createDebugLogger2({ prefix: "BranchRegistry" });
5785
6216
  REGISTRY_FILE = "branches.json";
5786
6217
  REGISTRY_STALE_FILE = "branches.stale.json";
@@ -5807,7 +6238,7 @@ var init_generate_ai_content = __esm({
5807
6238
  const raw = await fs10.readFile(this.registryPath, "utf8");
5808
6239
  parsed = JSON.parse(raw);
5809
6240
  } catch (err) {
5810
- if (isNotFoundError4(err)) {
6241
+ if (isNotFoundError3(err)) {
5811
6242
  return await this.regenerate();
5812
6243
  }
5813
6244
  throw err;
@@ -5933,7 +6364,7 @@ var init_generate_ai_content = __esm({
5933
6364
  try {
5934
6365
  meta = await BranchMetadataFileManager.loadOnly(branchRoot);
5935
6366
  } catch (err) {
5936
- console.warn(`CanopyCMS: Skipping branch directory '${entry.name}' during registry scan: ${getErrorMessage3(err)}`);
6367
+ canopyLogWarn2(`CanopyCMS: Skipping branch directory '${entry.name}' during registry scan: ${getErrorMessage3(err)}`);
5937
6368
  continue;
5938
6369
  }
5939
6370
  if (meta) {
@@ -5945,7 +6376,7 @@ var init_generate_ai_content = __esm({
5945
6376
  }
5946
6377
  }
5947
6378
  } catch (err) {
5948
- if (isNotFoundError4(err)) {
6379
+ if (isNotFoundError3(err)) {
5949
6380
  return [];
5950
6381
  }
5951
6382
  throw err;
@@ -5995,7 +6426,7 @@ var init_generate_ai_content = __esm({
5995
6426
  const raw = await fs12.readFile(filePath, "utf8");
5996
6427
  return JSON.parse(raw);
5997
6428
  } catch (err) {
5998
- if (isNotFoundError4(err)) {
6429
+ if (isNotFoundError3(err)) {
5999
6430
  return null;
6000
6431
  }
6001
6432
  if (err instanceof SyntaxError) {
@@ -6018,7 +6449,7 @@ var init_generate_ai_content = __esm({
6018
6449
  const version = parsed.version ?? 0;
6019
6450
  return { meta: parsed, version };
6020
6451
  } catch (err) {
6021
- if (isNotFoundError4(err)) {
6452
+ if (isNotFoundError3(err)) {
6022
6453
  return { meta: null, version: null };
6023
6454
  }
6024
6455
  throw err;
@@ -6077,7 +6508,7 @@ var init_generate_ai_content = __esm({
6077
6508
  try {
6078
6509
  await fs12.stat(this.branchRoot);
6079
6510
  } catch (err) {
6080
- if (isNotFoundError4(err)) {
6511
+ if (isNotFoundError3(err)) {
6081
6512
  throw new BranchMetadataConflictError("Branch no longer exists");
6082
6513
  }
6083
6514
  throw err;
@@ -6166,7 +6597,9 @@ var init_generate_ai_content = __esm({
6166
6597
  COLON_BEFORE_SLASH_PATTERN = /^[^/\\\s]+:/;
6167
6598
  log5 = createDebugLogger2({ prefix: "GitManager" });
6168
6599
  GIT_ENV_PASSTHROUGH = /^(PATH|HOME|USER|LANG|LC_[A-Z]+|TZ|TMPDIR|GIT_(AUTHOR|COMMITTER)_(NAME|EMAIL|DATE)|GIT_TERMINAL_PROMPT|GIT_TRACE[0-9A-Z_]*)$/;
6600
+ FORCE_C_LOCALE = { LC_ALL: "C", LANG: "C" };
6169
6601
  remoteInitLocks = /* @__PURE__ */ new Map();
6602
+ GITHUB_TRACKING_REF_PREFIX = "refs/remotes/github/";
6170
6603
  GitConflictError = class extends Error {
6171
6604
  constructor(conflictedFiles) {
6172
6605
  super(`Git conflict in ${conflictedFiles.length} file(s): ${conflictedFiles.join(", ")}`);
@@ -6230,7 +6663,7 @@ var init_generate_ai_content = __esm({
6230
6663
  const stat = await fs14.stat(options.remotePath);
6231
6664
  remoteExists = stat.isDirectory();
6232
6665
  } catch (err) {
6233
- if (!isNotFoundError4(err))
6666
+ if (!isNotFoundError3(err))
6234
6667
  throw err;
6235
6668
  }
6236
6669
  if (remoteExists && await _GitManager.bareRemoteHasBranch(options.remotePath, options.baseBranch)) {
@@ -6301,26 +6734,115 @@ var init_generate_ai_content = __esm({
6301
6734
  await lockPromise;
6302
6735
  }
6303
6736
  /**
6304
- * Whether a bare repository already has a local branch of the given name.
6737
+ * Whether a bare repository already has a branch of the given name, in
6738
+ * EITHER the local-heads namespace (`refs/heads/<branch>`) or the GitHub
6739
+ * tracking namespace (`GITHUB_TRACKING_REF_PREFIX<branch>`).
6740
+ *
6741
+ * Originally checked `refs/heads/*` only, for dev-mode's
6742
+ * `ensureLocalSimulatedRemote` (a simulated remote never gets a tracking
6743
+ * namespace, so that caller only ever needed the local-heads check).
6744
+ * Generalized/promoted to public for api/branch.ts's create-time collision
6745
+ * guard, which also needs the tracking namespace: `syncGit()` fetches
6746
+ * GitHub into `GITHUB_TRACKING_REF_PREFIX*` rather than `refs/heads/*`
6747
+ * directly (see that constant's doc comment above), and
6748
+ * `reconcileTrackedBranches()` (worker/cms-worker.ts) only
6749
+ * non-destructively brings `refs/heads/*` toward what's tracked there — so
6750
+ * a branch that another CanopyCMS deployment sharing this repo (or a
6751
+ * direct push to GitHub) just created can sit in the tracking namespace
6752
+ * for a while before, or without ever, gaining a local head here. Checking
6753
+ * `refs/heads/*` alone would miss exactly the two-deployments-one-repo
6754
+ * collision that guard exists to catch.
6305
6755
  *
6306
6756
  * Runs git with an explicit `--git-dir` instead of a cwd inside the repo:
6307
6757
  * environments with `safe.bareRepository=explicit` (sandboxed/CI git setups)
6308
6758
  * refuse cwd-based discovery of bare repos but expressly allow `--git-dir`.
6309
- * `branch --list` exits 0 whether or not the branch exists (no
6310
- * exception-based control flow — also why `rev-parse --verify --quiet`
6311
- * can't be used: simple-git only fails a task on stderr output, and --quiet
6312
- * suppresses exactly that). A failure here therefore means the remote
6313
- * itself is unreadable and is surfaced, NOT treated as "branch absent" —
6314
- * that would route to the push path against a repo we couldn't even read.
6759
+ * A single `for-each-ref` call checks both candidate refs at once (no
6760
+ * exception-based control flow — `for-each-ref` exits 0 whether or not
6761
+ * either ref exists, same reason the old implementation used `branch
6762
+ * --list` instead of `rev-parse --verify --quiet`: simple-git only fails a
6763
+ * task on stderr output, and `--quiet` suppresses exactly that).
6764
+ * `--end-of-options` guards the ref-name positionals the same way
6765
+ * `push`/`forcePush` below do, since `branch` here can be a sanitized but
6766
+ * otherwise caller-influenced string.
6767
+ *
6768
+ * A failure here therefore means the remote itself is unreadable and is
6769
+ * surfaced, NOT treated as "branch absent" — that would route
6770
+ * `ensureLocalSimulatedRemote` to the push path against a repo it couldn't
6771
+ * even read, and would silently skip api/branch.ts's collision check
6772
+ * instead of letting that caller distinguish "unreadable" from "absent"
6773
+ * and decide what to do.
6315
6774
  */
6316
- static async bareRemoteHasBranch(remotePath, branch) {
6775
+ static async bareRemoteHasBranch(remotePath, branch, options = {}) {
6317
6776
  let output;
6318
6777
  try {
6319
- output = await simpleGit2().raw(["--git-dir", remotePath, "branch", "--list", branch]);
6778
+ output = await simpleGit2().raw([
6779
+ "--git-dir",
6780
+ remotePath,
6781
+ "for-each-ref",
6782
+ "--format=%(refname)",
6783
+ "--end-of-options",
6784
+ `refs/heads/${branch}`,
6785
+ `${GITHUB_TRACKING_REF_PREFIX}${branch}`
6786
+ ]);
6320
6787
  } catch (err) {
6321
- throw new Error(`Cannot inspect simulated remote at ${remotePath}: ${getErrorMessage3(err)}`);
6788
+ throw new Error(`Cannot inspect remote mirror at ${remotePath}: ${getErrorMessage3(err)}`);
6789
+ }
6790
+ const refs = output.split("\n").map((line) => line.trim()).filter(Boolean);
6791
+ if (options.namespaces === "tracking") {
6792
+ return refs.includes(`${GITHUB_TRACKING_REF_PREFIX}${branch}`);
6322
6793
  }
6323
- return output.split("\n").map((line) => line.replace(/^[*+]\s*/, "").trim()).includes(branch);
6794
+ return refs.includes(`refs/heads/${branch}`) || refs.includes(`${GITHUB_TRACKING_REF_PREFIX}${branch}`);
6795
+ }
6796
+ /**
6797
+ * Delete `refs/heads/<branch>` from a bare local mirror, if present. A
6798
+ * no-op (not an error) when the ref doesn't exist.
6799
+ *
6800
+ * This is the "explicit path" for removing a deleted branch's local head
6801
+ * that the sync loop deliberately is not (see GITHUB_TRACKING_REF_PREFIX's
6802
+ * doc comment: reconcileTrackedBranches never deletes a head). Called by
6803
+ * api/branch.ts's deleteBranchHandler: without it, a deleted branch's head
6804
+ * lives in `remote.git` forever, and the ordinary create -> publish ->
6805
+ * squash-merge -> delete -> reuse-the-name cycle then has the REUSED
6806
+ * branch's first publish rejected non-fast-forward against the stale head
6807
+ * (`GitManager.push()` pushes `branch:branch`, and a squash-merged old tip
6808
+ * is not an ancestor of the new branch) -- a permanent, misleading 409.
6809
+ * Worse, a retried submit skips the local push (clean tree) and enqueues
6810
+ * the worker push of the STALE head, resurrecting the deleted branch's
6811
+ * content on GitHub as an apparent success.
6812
+ *
6813
+ * Deliberately leaves the tracking ref (`GITHUB_TRACKING_REF_PREFIX<branch>`)
6814
+ * alone: that namespace mirrors GitHub's view, and if the branch still
6815
+ * exists on GitHub, the create-time collision check SHOULD keep reporting
6816
+ * it until the remote side is actually gone.
6817
+ *
6818
+ * Same `--git-dir` invocation style as bareRemoteHasBranch above (works
6819
+ * under `safe.bareRepository=explicit`). The existence pre-check makes
6820
+ * "absent" deterministic instead of parsing update-ref's locale-dependent
6821
+ * failure text, and its captured SHA is passed to `update-ref -d` as the
6822
+ * expected old value -- same pattern as reconcileTrackedBranches'
6823
+ * guarded updates (worker/cms-worker.ts): a concurrent Lambda push
6824
+ * re-creating/moving this branch between the read and the delete makes
6825
+ * update-ref throw (surfaced as the caller's best-effort warning) instead
6826
+ * of silently deleting a commit that was just pushed. No
6827
+ * `--end-of-options` on update-ref (older gits don't accept it there);
6828
+ * the ref argument always begins with the literal `refs/heads/` prefix,
6829
+ * so it can never parse as an option.
6830
+ */
6831
+ static async deleteBareRemoteHead(remotePath, branch) {
6832
+ const ref = `refs/heads/${branch}`;
6833
+ const output = await simpleGit2().raw([
6834
+ "--git-dir",
6835
+ remotePath,
6836
+ "for-each-ref",
6837
+ "--format=%(refname) %(objectname)",
6838
+ "--end-of-options",
6839
+ ref
6840
+ ]);
6841
+ const match = output.split("\n").map((line) => line.trim()).filter(Boolean).map((line) => line.split(" ")).find(([refname]) => refname === ref);
6842
+ if (!match)
6843
+ return;
6844
+ const [, sha] = match;
6845
+ await simpleGit2().raw(["--git-dir", remotePath, "update-ref", "-d", ref, sha]);
6324
6846
  }
6325
6847
  /**
6326
6848
  * Push baseBranch from the source repo into the local bare remote via a
@@ -6395,7 +6917,7 @@ var init_generate_ai_content = __esm({
6395
6917
  throw new Error(`Expected git repo at ${repoPath}`);
6396
6918
  }
6397
6919
  } catch (err) {
6398
- if (isNotFoundError4(err)) {
6920
+ if (isNotFoundError3(err)) {
6399
6921
  throw new Error(`Expected git repo at ${repoPath}`);
6400
6922
  }
6401
6923
  throw err;
@@ -6493,6 +7015,30 @@ var init_generate_ai_content = __esm({
6493
7015
  }
6494
7016
  return void 0;
6495
7017
  }
7018
+ /**
7019
+ * Check whether a git repository is already initialized at `workspacePath`.
7020
+ *
7021
+ * Uses `rev-parse --git-dir` with `GIT_CEILING_DIRECTORIES` pinned to the
7022
+ * parent directory so a corrupt/missing `.git` can't make git silently
7023
+ * traverse upward and report a false positive from an ancestor repo (the
7024
+ * same protection `initializeWorkspace` relies on below).
7025
+ *
7026
+ * Shared by `initializeWorkspace` (clone-vs-reuse decision) and
7027
+ * `SettingsWorkspaceManager`'s rename guard (settings-workspace.ts), which
7028
+ * must know whether a settings workspace ALREADY exists before touching it —
7029
+ * touching an existing orphan settings branch under a different name wipes
7030
+ * permissions.json/groups.json (see that guard's doc comment).
7031
+ */
7032
+ static async repoExistsAt(workspacePath) {
7033
+ try {
7034
+ const checkGit = simpleGit2({ baseDir: workspacePath });
7035
+ checkGit.env(gitChildEnv({ GIT_CEILING_DIRECTORIES: path14.dirname(workspacePath) }));
7036
+ await checkGit.raw(["rev-parse", "--git-dir"]);
7037
+ return true;
7038
+ } catch {
7039
+ return false;
7040
+ }
7041
+ }
6496
7042
  /**
6497
7043
  * Ensures a git workspace is initialized and ready for use.
6498
7044
  * Handles cloning, remote configuration, and branch checkout/creation.
@@ -6511,13 +7057,8 @@ var init_generate_ai_content = __esm({
6511
7057
  detectFrom: options.sourceRoot ? path14.resolve(process.cwd(), options.sourceRoot) : process.cwd()
6512
7058
  });
6513
7059
  const remoteName = options.remoteName ?? "origin";
6514
- let repoExists = false;
6515
- try {
6516
- const checkGit = simpleGit2({ baseDir: options.workspacePath });
6517
- checkGit.env(gitChildEnv({ GIT_CEILING_DIRECTORIES: path14.dirname(options.workspacePath) }));
6518
- await checkGit.raw(["rev-parse", "--git-dir"]);
6519
- repoExists = true;
6520
- } catch {
7060
+ const repoExists = await _GitManager.repoExistsAt(options.workspacePath);
7061
+ if (!repoExists) {
6521
7062
  const gitPath = path14.join(options.workspacePath, ".git");
6522
7063
  try {
6523
7064
  const stat = await fs14.stat(gitPath);
@@ -6528,7 +7069,7 @@ var init_generate_ai_content = __esm({
6528
7069
  await fs14.rm(gitPath, { recursive: true });
6529
7070
  }
6530
7071
  } catch (cleanupErr) {
6531
- if (!isNotFoundError4(cleanupErr))
7072
+ if (!isNotFoundError3(cleanupErr))
6532
7073
  throw cleanupErr;
6533
7074
  }
6534
7075
  }
@@ -6987,7 +7528,7 @@ var init_generate_ai_content = __esm({
6987
7528
  }
6988
7529
  return data;
6989
7530
  } catch (err) {
6990
- if (isNotFoundError4(err))
7531
+ if (isNotFoundError3(err))
6991
7532
  return {};
6992
7533
  log7.warn("readEntryData", `Failed to read entry data from ${filePath}: ${getErrorMessage3(err)}`);
6993
7534
  return {};
@@ -7038,7 +7579,7 @@ var init_generate_ai_content = __esm({
7038
7579
  try {
7039
7580
  dirents = await fs15.readdir(collectionRoot, { withFileTypes: true });
7040
7581
  } catch (err) {
7041
- if (isNotFoundError4(err))
7582
+ if (isNotFoundError3(err))
7042
7583
  return [];
7043
7584
  throw err;
7044
7585
  }
@@ -7132,10 +7673,21 @@ var init_debug2 = __esm({
7132
7673
  const minLevel = this.options.minLevel ?? "DEBUG";
7133
7674
  return LOG_LEVELS3[level] >= LOG_LEVELS3[minLevel];
7134
7675
  }
7676
+ /**
7677
+ * The ISO-8601 timestamp leads the line BARE - not wrapped in brackets as it
7678
+ * once was. `CANOPYCMS_DEBUG=true` can be set on the EC2 worker, whose stdout
7679
+ * appends to /var/log/canopy-worker/worker.log; the CloudWatch agent's
7680
+ * `multi_line_start_pattern` matches the timestamp at the START of a line
7681
+ * (see worker/log.ts's INVARIANT and cms-service.ts's agent config), so a
7682
+ * leading `[` meant every debug line was silently folded into the previous
7683
+ * event instead of starting its own. The bracketed `[prefix:category]` and
7684
+ * `[level]` fields still follow, so the human-readable shape is unchanged
7685
+ * apart from those two characters.
7686
+ */
7135
7687
  formatMessage(level, category, message) {
7136
7688
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
7137
7689
  const prefix = this.options.prefix ?? "CanopyCMS";
7138
- return `[${timestamp}] [${prefix}:${category}] [${level}] ${message}`;
7690
+ return `${timestamp} [${prefix}:${category}] [${level}] ${message}`;
7139
7691
  }
7140
7692
  debug(category, message, data) {
7141
7693
  if (this.shouldLog("DEBUG")) {
@@ -7288,6 +7840,21 @@ var init_content_index_registry = __esm({
7288
7840
  }
7289
7841
  });
7290
7842
 
7843
+ // dist/utils/logger.js
7844
+ function canopyLogWarn3(...args) {
7845
+ active3.warn(...args);
7846
+ }
7847
+ function canopyLogError2(...args) {
7848
+ active3.error(...args);
7849
+ }
7850
+ var active3;
7851
+ var init_logger2 = __esm({
7852
+ "dist/utils/logger.js"() {
7853
+ "use strict";
7854
+ active3 = console;
7855
+ }
7856
+ });
7857
+
7291
7858
  // dist/paths/normalize.js
7292
7859
  var init_normalize3 = __esm({
7293
7860
  "dist/paths/normalize.js"() {
@@ -7443,7 +8010,7 @@ async function scanForCollectionMeta2(baseDir, relativePath = "") {
7443
8010
  collections.push(...nestedCollections);
7444
8011
  } catch (err) {
7445
8012
  if (err.code !== "ENOENT") {
7446
- console.error(`Error loading ${metaPath}:`, err);
8013
+ canopyLogError2(`Error loading ${metaPath}:`, err);
7447
8014
  throw new Error(`Invalid .collection.json in ${folderPath}: ${err.message}`);
7448
8015
  }
7449
8016
  const nestedCollections = await scanForCollectionMeta2(absolutePath, folderPath);
@@ -7487,6 +8054,7 @@ var init_meta_loader = __esm({
7487
8054
  "dist/schema/meta-loader.js"() {
7488
8055
  "use strict";
7489
8056
  init_error();
8057
+ init_logger2();
7490
8058
  init_content_id_index();
7491
8059
  entryTypeMetaSchema2 = z8.object({
7492
8060
  name: z8.string().min(1),
@@ -7542,87 +8110,386 @@ var init_flatten3 = __esm({
7542
8110
  }
7543
8111
  });
7544
8112
 
7545
- // dist/build-mode.js
7546
- var STATIC_DEPLOY_USER2;
7547
- var init_build_mode = __esm({
7548
- "dist/build-mode.js"() {
7549
- "use strict";
7550
- STATIC_DEPLOY_USER2 = Object.freeze({
7551
- type: "authenticated",
7552
- userId: "__static_deploy__",
7553
- groups: ["Admins"],
7554
- email: "static-deploy@canopycms",
7555
- name: "Static Deploy"
7556
- });
7557
- }
7558
- });
7559
-
7560
- // dist/branch-schema-cache.js
7561
- var SCHEMA_GENERATION_RESOURCE2;
7562
- var init_branch_schema_cache = __esm({
7563
- "dist/branch-schema-cache.js"() {
8113
+ // dist/config/types.js
8114
+ var primitiveFieldTypes3, fieldTypes3;
8115
+ var init_types5 = __esm({
8116
+ "dist/config/types.js"() {
7564
8117
  "use strict";
7565
- init_resolver();
7566
- init_flatten3();
7567
- init_build_mode();
7568
- init_resource_generation();
7569
- SCHEMA_GENERATION_RESOURCE2 = "schema";
8118
+ primitiveFieldTypes3 = [
8119
+ "string",
8120
+ "number",
8121
+ "boolean",
8122
+ "datetime",
8123
+ "rich-text",
8124
+ "markdown",
8125
+ "mdx",
8126
+ "code"
8127
+ ];
8128
+ fieldTypes3 = [
8129
+ ...primitiveFieldTypes3,
8130
+ "image",
8131
+ "select",
8132
+ "reference",
8133
+ "object",
8134
+ "block",
8135
+ "group"
8136
+ ];
7570
8137
  }
7571
8138
  });
7572
8139
 
7573
- // dist/content-index-generation.js
7574
- async function bumpContentIndexGeneration2(root) {
7575
- return bumpResourceGeneration2(root, RESOURCE3);
7576
- }
7577
- async function invalidateBranchContentCaches2(root) {
7578
- await bumpContentIndexGeneration2(root);
7579
- await bumpResourceGeneration2(root, SCHEMA_GENERATION_RESOURCE2);
7580
- invalidateContentIndexesForRoot2(root);
7581
- }
7582
- var RESOURCE3;
7583
- var init_content_index_generation = __esm({
7584
- "dist/content-index-generation.js"() {
8140
+ // dist/config/schemas/field.js
8141
+ import { z as z9 } from "zod";
8142
+ var fieldBaseSchema3, selectOptionSchema3, referenceOptionSchema3, primitiveFieldSchema3, ASPECT_RATIO_RE3, imageFieldSchema3, selectFieldSchema3, referenceFieldSchema3, fieldHolder3, blockSchema3, blockFieldSchema3, objectFieldSchema3, inlineGroupFieldSchema3, customFieldSchema3, knownFieldSchema3, fieldSchema3;
8143
+ var init_field3 = __esm({
8144
+ "dist/config/schemas/field.js"() {
7585
8145
  "use strict";
7586
- init_resource_generation();
7587
- init_content_index_registry();
7588
- init_branch_schema_cache();
7589
- RESOURCE3 = "content-index";
7590
- }
7591
- });
7592
-
7593
- // dist/sync-core.js
7594
- import fs20 from "node:fs/promises";
7595
- import path23 from "node:path";
7596
- import { simpleGit as simpleGit3 } from "simple-git";
7597
- function assertWithinDir(resolved, parent, label) {
7598
- const normalizedResolved = path23.resolve(resolved);
7599
- const normalizedParent = path23.resolve(parent);
7600
- if (!normalizedResolved.startsWith(normalizedParent + path23.sep) && normalizedResolved !== normalizedParent) {
7601
- throw new Error(`${label} escapes the expected directory: ${resolved}`);
7602
- }
7603
- }
7604
- async function safeReplaceDir(oldDir, newDir) {
7605
- const backupDir = `${oldDir}.sync-backup-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
7606
- const oldExists = await filePathExists(oldDir);
7607
- if (oldExists) {
7608
- await fs20.rename(oldDir, backupDir);
7609
- }
7610
- try {
7611
- await fs20.rename(newDir, oldDir);
7612
- } catch (err) {
7613
- if (oldExists) {
7614
- await fs20.rename(backupDir, oldDir).catch(() => {
7615
- });
7616
- }
7617
- throw err;
7618
- }
7619
- if (oldExists) {
7620
- await fs20.rm(backupDir, { recursive: true, force: true }).catch(() => {
7621
- });
7622
- }
7623
- }
7624
- async function listFilesRecursive(dir, prefix = "") {
7625
- const results = [];
8146
+ init_types5();
8147
+ fieldBaseSchema3 = z9.object({
8148
+ name: z9.string().min(1),
8149
+ label: z9.string().optional(),
8150
+ description: z9.string().optional(),
8151
+ required: z9.boolean().optional(),
8152
+ list: z9.boolean().optional(),
8153
+ isTitle: z9.boolean().optional(),
8154
+ isBody: z9.boolean().optional()
8155
+ });
8156
+ selectOptionSchema3 = z9.union([
8157
+ z9.string(),
8158
+ z9.object({
8159
+ label: z9.string().min(1),
8160
+ value: z9.string().min(1)
8161
+ })
8162
+ ]);
8163
+ referenceOptionSchema3 = z9.union([
8164
+ z9.string(),
8165
+ z9.object({
8166
+ label: z9.string().min(1),
8167
+ value: z9.string().min(1)
8168
+ })
8169
+ ]);
8170
+ primitiveFieldSchema3 = fieldBaseSchema3.extend({
8171
+ type: z9.enum(primitiveFieldTypes3)
8172
+ });
8173
+ ASPECT_RATIO_RE3 = /^[1-9][0-9]*:[1-9][0-9]*$/;
8174
+ imageFieldSchema3 = fieldBaseSchema3.extend({
8175
+ type: z9.literal("image"),
8176
+ aspect: z9.string().regex(ASPECT_RATIO_RE3, 'aspect must be "W:H" with positive integers (e.g. "16:9")').optional(),
8177
+ altOptional: z9.boolean().optional()
8178
+ });
8179
+ selectFieldSchema3 = fieldBaseSchema3.extend({
8180
+ type: z9.literal("select"),
8181
+ options: z9.array(selectOptionSchema3).min(1)
8182
+ });
8183
+ referenceFieldSchema3 = fieldBaseSchema3.extend({
8184
+ type: z9.literal("reference"),
8185
+ collections: z9.array(z9.string().min(1)).min(1).optional(),
8186
+ entryTypes: z9.array(z9.string().min(1)).min(1).optional(),
8187
+ displayField: z9.string().min(1).optional(),
8188
+ options: z9.array(referenceOptionSchema3).optional()
8189
+ });
8190
+ fieldHolder3 = [z9.never()];
8191
+ blockSchema3 = z9.object({
8192
+ name: z9.string().min(1),
8193
+ label: z9.string().optional(),
8194
+ description: z9.string().optional(),
8195
+ fields: z9.array(z9.lazy(() => fieldHolder3[0])).min(1)
8196
+ });
8197
+ blockFieldSchema3 = fieldBaseSchema3.extend({
8198
+ type: z9.literal("block"),
8199
+ templates: z9.array(blockSchema3).min(1)
8200
+ });
8201
+ objectFieldSchema3 = fieldBaseSchema3.extend({
8202
+ type: z9.literal("object"),
8203
+ fields: z9.array(z9.lazy(() => fieldHolder3[0])).min(1)
8204
+ });
8205
+ inlineGroupFieldSchema3 = z9.object({
8206
+ type: z9.literal("group"),
8207
+ name: z9.string().min(1),
8208
+ label: z9.string().optional(),
8209
+ description: z9.string().optional(),
8210
+ fields: z9.array(z9.lazy(() => fieldHolder3[0])).min(1)
8211
+ });
8212
+ customFieldSchema3 = z9.lazy(() => fieldBaseSchema3.extend({
8213
+ type: z9.string().min(1).refine((val) => !fieldTypes3.includes(val), {
8214
+ message: "Custom field types must not conflict with built-in types"
8215
+ })
8216
+ }).passthrough());
8217
+ knownFieldSchema3 = z9.discriminatedUnion("type", [
8218
+ primitiveFieldSchema3,
8219
+ selectFieldSchema3,
8220
+ referenceFieldSchema3,
8221
+ imageFieldSchema3,
8222
+ objectFieldSchema3,
8223
+ blockFieldSchema3,
8224
+ inlineGroupFieldSchema3
8225
+ ]);
8226
+ fieldSchema3 = z9.lazy(() => z9.union([knownFieldSchema3, customFieldSchema3]));
8227
+ fieldHolder3[0] = fieldSchema3;
8228
+ }
8229
+ });
8230
+
8231
+ // dist/config/schemas/collection.js
8232
+ import { z as z10 } from "zod";
8233
+ import { isAbsolute as isAbsolute3 } from "pathe";
8234
+ var relativePathSchema3, entryTypeSchema3, collectionSchema3;
8235
+ var init_collection3 = __esm({
8236
+ "dist/config/schemas/collection.js"() {
8237
+ "use strict";
8238
+ init_field3();
8239
+ relativePathSchema3 = z10.string().min(1).refine((val) => !isAbsolute3(val), { message: "Path must be relative" }).refine((val) => !val.split(/[\\/]+/).includes(".."), {
8240
+ message: 'Path must not contain ".."'
8241
+ }).transform((val) => val.split(/[\\/]+/).filter(Boolean).join("/"));
8242
+ entryTypeSchema3 = z10.object({
8243
+ name: z10.string().min(1),
8244
+ format: z10.enum(["md", "mdx", "json", "yaml"]),
8245
+ schema: z10.array(z10.lazy(() => fieldSchema3)).min(1),
8246
+ label: z10.string().optional(),
8247
+ description: z10.string().optional(),
8248
+ default: z10.boolean().optional(),
8249
+ maxItems: z10.number().int().positive().optional()
8250
+ });
8251
+ collectionSchema3 = z10.lazy(() => z10.object({
8252
+ name: z10.string().min(1),
8253
+ path: relativePathSchema3,
8254
+ label: z10.string().optional(),
8255
+ description: z10.string().optional(),
8256
+ entries: z10.array(entryTypeSchema3).optional(),
8257
+ collections: z10.array(collectionSchema3).optional(),
8258
+ order: z10.array(z10.string()).optional()
8259
+ // Embedded IDs for ordering items
8260
+ }).refine((data) => data.entries || data.collections, {
8261
+ message: "Collection must have entries or collections"
8262
+ }));
8263
+ }
8264
+ });
8265
+
8266
+ // dist/config/schemas/media.js
8267
+ import { z as z11 } from "zod";
8268
+ var mediaSchema3;
8269
+ var init_media3 = __esm({
8270
+ "dist/config/schemas/media.js"() {
8271
+ "use strict";
8272
+ init_collection3();
8273
+ mediaSchema3 = z11.discriminatedUnion("adapter", [
8274
+ z11.object({
8275
+ adapter: z11.literal("local"),
8276
+ publicBaseUrl: z11.string().url().optional(),
8277
+ /** Root directory for local asset storage. Defaults to the caller's dev-assets dir. */
8278
+ directory: relativePathSchema3.optional()
8279
+ }),
8280
+ z11.object({
8281
+ adapter: z11.literal("s3"),
8282
+ bucket: z11.string().min(1),
8283
+ region: z11.string().min(1),
8284
+ publicBaseUrl: z11.string().url().optional(),
8285
+ /** Max upload size in bytes for presigned direct uploads. Defaults to 50 MiB. */
8286
+ maxUploadBytes: z11.number().int().positive().optional()
8287
+ }),
8288
+ z11.object({
8289
+ adapter: z11.literal("lfs"),
8290
+ publicBaseUrl: z11.string().url().optional()
8291
+ })
8292
+ ]);
8293
+ }
8294
+ });
8295
+
8296
+ // dist/config/schemas/config.js
8297
+ import { z as z12 } from "zod";
8298
+ var defaultBranchAccessSchema3, defaultPathAccessLevelSchema3, defaultPathAccessLevelsSchema3, defaultPathAccessSchema3, defaultBaseBranchSchema3, defaultRemoteNameSchema3, defaultRemoteUrlSchema3, gitBotAuthorNameSchema3, gitBotAuthorEmailSchema3, githubTokenEnvVarSchema3, operatingModeSchema3, deployedAsSchema3, contentRootSchema3, sourceRootSchema3, deploymentNameSchema3, devContentSyncSchema3, devConfigSchema3, editorConfigSchema3, CanopyConfigSchema3, DEFAULT_PROD_WORKSPACE3;
8299
+ var init_config4 = __esm({
8300
+ "dist/config/schemas/config.js"() {
8301
+ "use strict";
8302
+ init_collection3();
8303
+ init_media3();
8304
+ defaultBranchAccessSchema3 = z12.enum(["allow", "deny"]).default("deny");
8305
+ defaultPathAccessLevelSchema3 = z12.enum(["allow", "deny"]);
8306
+ defaultPathAccessLevelsSchema3 = z12.object({
8307
+ read: defaultPathAccessLevelSchema3.optional(),
8308
+ edit: defaultPathAccessLevelSchema3.optional(),
8309
+ review: defaultPathAccessLevelSchema3.optional()
8310
+ }).strict();
8311
+ defaultPathAccessSchema3 = z12.union([defaultPathAccessLevelSchema3, defaultPathAccessLevelsSchema3]).default("deny");
8312
+ defaultBaseBranchSchema3 = z12.string().default("main");
8313
+ defaultRemoteNameSchema3 = z12.string().default("origin");
8314
+ defaultRemoteUrlSchema3 = z12.string().min(1);
8315
+ gitBotAuthorNameSchema3 = z12.string().min(1);
8316
+ gitBotAuthorEmailSchema3 = z12.string().email();
8317
+ githubTokenEnvVarSchema3 = z12.string().default("GITHUB_BOT_TOKEN");
8318
+ operatingModeSchema3 = z12.enum(["prod", "dev"]);
8319
+ deployedAsSchema3 = z12.enum(["static", "server"]).default("server");
8320
+ contentRootSchema3 = relativePathSchema3.default("content");
8321
+ sourceRootSchema3 = z12.string().min(1).optional();
8322
+ deploymentNameSchema3 = z12.string().default("prod");
8323
+ devContentSyncSchema3 = z12.enum(["off", "warn"]).default("warn");
8324
+ devConfigSchema3 = z12.object({
8325
+ contentSync: devContentSyncSchema3.optional()
8326
+ });
8327
+ editorConfigSchema3 = z12.object({
8328
+ title: z12.string().optional(),
8329
+ subtitle: z12.string().optional(),
8330
+ theme: z12.unknown().optional(),
8331
+ previewBase: z12.record(z12.string()).optional(),
8332
+ // UI handler functions (runtime only, don't serialize)
8333
+ onAccountClick: z12.function().returns(z12.void()).optional(),
8334
+ onLogoutClick: z12.function().returns(z12.void()).optional(),
8335
+ // Optional: custom account component (e.g., Clerk's UserButton)
8336
+ AccountComponent: z12.custom().optional()
8337
+ });
8338
+ CanopyConfigSchema3 = z12.object({
8339
+ media: mediaSchema3.optional(),
8340
+ // No outer .optional() here: defaultBranchAccessSchema/defaultPathAccessSchema already
8341
+ // have .default('deny'), so the field is optional on input but always resolves to
8342
+ // 'allow'/'deny' (never undefined) on output. An outer .optional() would short-circuit
8343
+ // before the inner default runs, defeating the fail-closed default (SCH-M1).
8344
+ // defaultPathAccessSchema's union keeps the same rule: the .default('deny') sits on the
8345
+ // OUTER union, not inside defaultPathAccessLevelsSchema, so an omitted top-level field
8346
+ // still resolves to 'deny' and an omitted level inside the object form stays undefined
8347
+ // (resolved to 'deny' at read time by resolveDefaultPathAccess).
8348
+ defaultBranchAccess: defaultBranchAccessSchema3,
8349
+ defaultPathAccess: defaultPathAccessSchema3,
8350
+ // .optional() deliberately defeats defaultBaseBranchSchema's .default('main'):
8351
+ // unset must stay undefined after parsing so dev mode can detect the fork
8352
+ // point from git HEAD (see resolveBaseBranch in utils/git.ts).
8353
+ defaultBaseBranch: defaultBaseBranchSchema3.optional(),
8354
+ defaultActiveBranch: z12.string().min(1).optional(),
8355
+ defaultRemoteName: defaultRemoteNameSchema3.optional(),
8356
+ defaultRemoteUrl: defaultRemoteUrlSchema3.optional(),
8357
+ gitBotAuthorName: gitBotAuthorNameSchema3,
8358
+ gitBotAuthorEmail: gitBotAuthorEmailSchema3,
8359
+ githubTokenEnvVar: githubTokenEnvVarSchema3.optional(),
8360
+ // Required by design (follow-up to SEC-C1): a prod deploy that omits `mode` must fail
8361
+ // validation loudly rather than silently running header-trusting dev auth semantics.
8362
+ mode: operatingModeSchema3,
8363
+ deployedAs: deployedAsSchema3,
8364
+ // Has .default('server'), so always present after validation
8365
+ // Escape hatch for prod hosts that genuinely have internet access and intentionally
8366
+ // run git against a network remote (see GitManager.resolveRemoteUrl's prod-mode guard).
8367
+ // Default false/unset — the standard AWS Lambda+worker topology must leave this unset.
8368
+ allowNetworkRemoteInProd: z12.boolean().optional(),
8369
+ settingsBranch: z12.string().optional(),
8370
+ autoCreateSettingsPR: z12.boolean().optional(),
8371
+ // Deliberately `deploymentNameSchema.optional()`, NOT `deploymentNameSchema` alone.
8372
+ // deploymentNameSchema.default('prod') would make `parse(undefined)` resolve to
8373
+ // the literal 'prod' instead of staying `undefined` — collapsing the
8374
+ // env > config > modeDefault precedence chain that resolveDeploymentName
8375
+ // (operating-mode/deployment-name.ts) implements: config would then always
8376
+ // "win" over modeDefault (masking dev's real default of 'local'), and the env
8377
+ // var would just be racing config's baked-in 'prod' instead of a true absence.
8378
+ // `.optional()` here is what keeps an omitted deploymentName reaching
8379
+ // resolveDeploymentName as `undefined`, so it can fall through to modeDefault.
8380
+ // Do not "fix" this by removing `.optional()`.
8381
+ deploymentName: deploymentNameSchema3.optional(),
8382
+ contentRoot: contentRootSchema3.default("content"),
8383
+ sourceRoot: sourceRootSchema3.optional(),
8384
+ editor: editorConfigSchema3.optional(),
8385
+ authPlugin: z12.custom().optional(),
8386
+ entryLinkUrl: z12.custom().optional(),
8387
+ validateEntry: z12.custom().optional(),
8388
+ dev: devConfigSchema3.optional()
8389
+ }).strict();
8390
+ DEFAULT_PROD_WORKSPACE3 = "/mnt/efs/workspace";
8391
+ }
8392
+ });
8393
+
8394
+ // dist/config/validation.js
8395
+ var init_validation4 = __esm({
8396
+ "dist/config/validation.js"() {
8397
+ "use strict";
8398
+ init_config4();
8399
+ init_flatten3();
8400
+ }
8401
+ });
8402
+
8403
+ // dist/validation/entry-type-reference-validator.js
8404
+ var init_entry_type_reference_validator = __esm({
8405
+ "dist/validation/entry-type-reference-validator.js"() {
8406
+ "use strict";
8407
+ init_validation4();
8408
+ }
8409
+ });
8410
+
8411
+ // dist/build-mode.js
8412
+ var STATIC_DEPLOY_USER2;
8413
+ var init_build_mode = __esm({
8414
+ "dist/build-mode.js"() {
8415
+ "use strict";
8416
+ STATIC_DEPLOY_USER2 = Object.freeze({
8417
+ type: "authenticated",
8418
+ userId: "__static_deploy__",
8419
+ groups: ["Admins"],
8420
+ email: "static-deploy@canopycms",
8421
+ name: "Static Deploy"
8422
+ });
8423
+ }
8424
+ });
8425
+
8426
+ // dist/branch-schema-cache.js
8427
+ var SCHEMA_GENERATION_RESOURCE2;
8428
+ var init_branch_schema_cache = __esm({
8429
+ "dist/branch-schema-cache.js"() {
8430
+ "use strict";
8431
+ init_resolver();
8432
+ init_flatten3();
8433
+ init_entry_type_reference_validator();
8434
+ init_build_mode();
8435
+ init_resource_generation();
8436
+ SCHEMA_GENERATION_RESOURCE2 = "schema";
8437
+ }
8438
+ });
8439
+
8440
+ // dist/content-index-generation.js
8441
+ async function bumpContentIndexGeneration2(root) {
8442
+ return bumpResourceGeneration2(root, RESOURCE3);
8443
+ }
8444
+ async function invalidateBranchContentCaches2(root) {
8445
+ await bumpContentIndexGeneration2(root);
8446
+ await bumpResourceGeneration2(root, SCHEMA_GENERATION_RESOURCE2);
8447
+ invalidateContentIndexesForRoot2(root);
8448
+ }
8449
+ var RESOURCE3;
8450
+ var init_content_index_generation = __esm({
8451
+ "dist/content-index-generation.js"() {
8452
+ "use strict";
8453
+ init_resource_generation();
8454
+ init_content_index_registry();
8455
+ init_branch_schema_cache();
8456
+ RESOURCE3 = "content-index";
8457
+ }
8458
+ });
8459
+
8460
+ // dist/sync-core.js
8461
+ import fs20 from "node:fs/promises";
8462
+ import path23 from "node:path";
8463
+ import { simpleGit as simpleGit4 } from "simple-git";
8464
+ function assertWithinDir(resolved, parent, label) {
8465
+ const normalizedResolved = path23.resolve(resolved);
8466
+ const normalizedParent = path23.resolve(parent);
8467
+ if (!normalizedResolved.startsWith(normalizedParent + path23.sep) && normalizedResolved !== normalizedParent) {
8468
+ throw new Error(`${label} escapes the expected directory: ${resolved}`);
8469
+ }
8470
+ }
8471
+ async function safeReplaceDir(oldDir, newDir) {
8472
+ const backupDir = `${oldDir}.sync-backup-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
8473
+ const oldExists = await filePathExists(oldDir);
8474
+ if (oldExists) {
8475
+ await fs20.rename(oldDir, backupDir);
8476
+ }
8477
+ try {
8478
+ await fs20.rename(newDir, oldDir);
8479
+ } catch (err) {
8480
+ if (oldExists) {
8481
+ await fs20.rename(backupDir, oldDir).catch(() => {
8482
+ });
8483
+ }
8484
+ throw err;
8485
+ }
8486
+ if (oldExists) {
8487
+ await fs20.rm(backupDir, { recursive: true, force: true }).catch(() => {
8488
+ });
8489
+ }
8490
+ }
8491
+ async function listFilesRecursive(dir, prefix = "") {
8492
+ const results = [];
7626
8493
  let entries;
7627
8494
  try {
7628
8495
  entries = await fs20.readdir(dir, { withFileTypes: true });
@@ -7677,7 +8544,7 @@ async function pushContentToWorkspace(options) {
7677
8544
  throw err;
7678
8545
  }
7679
8546
  try {
7680
- const wsGit = simpleGit3({ baseDir: branchPath });
8547
+ const wsGit = simpleGit4({ baseDir: branchPath });
7681
8548
  await wsGit.add("-A");
7682
8549
  const postStatus = await wsGit.status();
7683
8550
  if (postStatus.files.length === 0) {
@@ -7704,7 +8571,7 @@ var init_sync_core = __esm({
7704
8571
  });
7705
8572
 
7706
8573
  // dist/utils/git.js
7707
- import { simpleGit as simpleGit4 } from "simple-git";
8574
+ import { simpleGit as simpleGit5 } from "simple-git";
7708
8575
  var init_git = __esm({
7709
8576
  "dist/utils/git.js"() {
7710
8577
  "use strict";
@@ -7722,379 +8589,134 @@ var init_provisioning_lock = __esm({
7722
8589
  // dist/git-manager.js
7723
8590
  import fs21 from "node:fs/promises";
7724
8591
  import path24 from "node:path";
7725
- import { simpleGit as simpleGit5 } from "simple-git";
8592
+ import { simpleGit as simpleGit6 } from "simple-git";
7726
8593
  async function ensureGitExcludePattern2(repoPath, pattern) {
7727
- const excludePath = path24.join(repoPath, ".git", "info", "exclude");
7728
- await fs21.mkdir(path24.dirname(excludePath), { recursive: true });
7729
- let content = "";
7730
- try {
7731
- content = await fs21.readFile(excludePath, "utf-8");
7732
- } catch (err) {
7733
- if (!isNotFoundError(err))
7734
- throw err;
7735
- }
7736
- const lines = content.split("\n");
7737
- if (lines.some((line) => line.trim() === pattern)) {
7738
- log9.debug("git", "Pattern already in .git/info/exclude", { pattern });
7739
- return;
7740
- }
7741
- const needsLeadingNewline = content.length > 0 && !content.endsWith("\n");
7742
- const newContent = content + (needsLeadingNewline ? "\n" : "") + pattern + "\n";
7743
- await fs21.writeFile(excludePath, newContent, "utf-8");
7744
- log9.debug("git", "Added pattern to .git/info/exclude", { pattern });
7745
- }
7746
- var log9;
7747
- var init_git_manager = __esm({
7748
- "dist/git-manager.js"() {
7749
- "use strict";
7750
- init_content_index_registry();
7751
- init_content_index_generation();
7752
- init_debug2();
7753
- init_error();
7754
- init_git();
7755
- init_provisioning_lock();
7756
- log9 = createDebugLogger3({ prefix: "GitManager" });
7757
- }
7758
- });
7759
-
7760
- // dist/operating-mode/client-safe-strategy.js
7761
- var ProdClientSafeStrategy3, DevClientSafeStrategy3;
7762
- var init_client_safe_strategy2 = __esm({
7763
- "dist/operating-mode/client-safe-strategy.js"() {
7764
- "use strict";
7765
- ProdClientSafeStrategy3 = class {
7766
- constructor() {
7767
- this.mode = "prod";
7768
- }
7769
- // UI Feature Flags
7770
- supportsBranching() {
7771
- return true;
7772
- }
7773
- supportsStatusBadge() {
7774
- return true;
7775
- }
7776
- supportsComments() {
7777
- return true;
7778
- }
7779
- supportsPullRequests() {
7780
- return true;
7781
- }
7782
- // Simple Data
7783
- getPermissionsFileName() {
7784
- return "permissions.json";
7785
- }
7786
- getGroupsFileName() {
7787
- return "groups.json";
7788
- }
7789
- shouldCommit() {
7790
- return true;
7791
- }
7792
- shouldPush() {
7793
- return true;
7794
- }
7795
- };
7796
- DevClientSafeStrategy3 = class {
7797
- constructor() {
7798
- this.mode = "dev";
7799
- }
7800
- // UI Feature Flags
7801
- supportsBranching() {
7802
- return true;
7803
- }
7804
- supportsStatusBadge() {
7805
- return true;
7806
- }
7807
- supportsComments() {
7808
- return true;
7809
- }
7810
- supportsPullRequests() {
7811
- return false;
7812
- }
7813
- // Simple Data
7814
- getPermissionsFileName() {
7815
- return "permissions.json";
7816
- }
7817
- getGroupsFileName() {
7818
- return "groups.json";
7819
- }
7820
- shouldCommit() {
7821
- return true;
7822
- }
7823
- shouldPush() {
7824
- return true;
7825
- }
7826
- };
7827
- }
7828
- });
7829
-
7830
- // dist/config/types.js
7831
- var primitiveFieldTypes3, fieldTypes3;
7832
- var init_types5 = __esm({
7833
- "dist/config/types.js"() {
7834
- "use strict";
7835
- primitiveFieldTypes3 = [
7836
- "string",
7837
- "number",
7838
- "boolean",
7839
- "datetime",
7840
- "rich-text",
7841
- "markdown",
7842
- "mdx",
7843
- "code"
7844
- ];
7845
- fieldTypes3 = [
7846
- ...primitiveFieldTypes3,
7847
- "image",
7848
- "select",
7849
- "reference",
7850
- "object",
7851
- "block",
7852
- "group"
7853
- ];
7854
- }
7855
- });
7856
-
7857
- // dist/config/schemas/field.js
7858
- import { z as z9 } from "zod";
7859
- var fieldBaseSchema3, selectOptionSchema3, referenceOptionSchema3, primitiveFieldSchema3, ASPECT_RATIO_RE3, imageFieldSchema3, selectFieldSchema3, referenceFieldSchema3, fieldHolder3, blockSchema3, blockFieldSchema3, objectFieldSchema3, inlineGroupFieldSchema3, customFieldSchema3, knownFieldSchema3, fieldSchema3;
7860
- var init_field3 = __esm({
7861
- "dist/config/schemas/field.js"() {
7862
- "use strict";
7863
- init_types5();
7864
- fieldBaseSchema3 = z9.object({
7865
- name: z9.string().min(1),
7866
- label: z9.string().optional(),
7867
- description: z9.string().optional(),
7868
- required: z9.boolean().optional(),
7869
- list: z9.boolean().optional(),
7870
- isTitle: z9.boolean().optional(),
7871
- isBody: z9.boolean().optional()
7872
- });
7873
- selectOptionSchema3 = z9.union([
7874
- z9.string(),
7875
- z9.object({
7876
- label: z9.string().min(1),
7877
- value: z9.string().min(1)
7878
- })
7879
- ]);
7880
- referenceOptionSchema3 = z9.union([
7881
- z9.string(),
7882
- z9.object({
7883
- label: z9.string().min(1),
7884
- value: z9.string().min(1)
7885
- })
7886
- ]);
7887
- primitiveFieldSchema3 = fieldBaseSchema3.extend({
7888
- type: z9.enum(primitiveFieldTypes3)
7889
- });
7890
- ASPECT_RATIO_RE3 = /^[1-9][0-9]*:[1-9][0-9]*$/;
7891
- imageFieldSchema3 = fieldBaseSchema3.extend({
7892
- type: z9.literal("image"),
7893
- aspect: z9.string().regex(ASPECT_RATIO_RE3, 'aspect must be "W:H" with positive integers (e.g. "16:9")').optional(),
7894
- altOptional: z9.boolean().optional()
7895
- });
7896
- selectFieldSchema3 = fieldBaseSchema3.extend({
7897
- type: z9.literal("select"),
7898
- options: z9.array(selectOptionSchema3).min(1)
7899
- });
7900
- referenceFieldSchema3 = fieldBaseSchema3.extend({
7901
- type: z9.literal("reference"),
7902
- collections: z9.array(z9.string().min(1)).min(1).optional(),
7903
- entryTypes: z9.array(z9.string().min(1)).min(1).optional(),
7904
- displayField: z9.string().min(1).optional(),
7905
- options: z9.array(referenceOptionSchema3).optional()
7906
- });
7907
- fieldHolder3 = [z9.never()];
7908
- blockSchema3 = z9.object({
7909
- name: z9.string().min(1),
7910
- label: z9.string().optional(),
7911
- description: z9.string().optional(),
7912
- fields: z9.array(z9.lazy(() => fieldHolder3[0])).min(1)
7913
- });
7914
- blockFieldSchema3 = fieldBaseSchema3.extend({
7915
- type: z9.literal("block"),
7916
- templates: z9.array(blockSchema3).min(1)
7917
- });
7918
- objectFieldSchema3 = fieldBaseSchema3.extend({
7919
- type: z9.literal("object"),
7920
- fields: z9.array(z9.lazy(() => fieldHolder3[0])).min(1)
7921
- });
7922
- inlineGroupFieldSchema3 = z9.object({
7923
- type: z9.literal("group"),
7924
- name: z9.string().min(1),
7925
- label: z9.string().optional(),
7926
- description: z9.string().optional(),
7927
- fields: z9.array(z9.lazy(() => fieldHolder3[0])).min(1)
7928
- });
7929
- customFieldSchema3 = z9.lazy(() => fieldBaseSchema3.extend({
7930
- type: z9.string().min(1).refine((val) => !fieldTypes3.includes(val), {
7931
- message: "Custom field types must not conflict with built-in types"
7932
- })
7933
- }).passthrough());
7934
- knownFieldSchema3 = z9.discriminatedUnion("type", [
7935
- primitiveFieldSchema3,
7936
- selectFieldSchema3,
7937
- referenceFieldSchema3,
7938
- imageFieldSchema3,
7939
- objectFieldSchema3,
7940
- blockFieldSchema3,
7941
- inlineGroupFieldSchema3
7942
- ]);
7943
- fieldSchema3 = z9.lazy(() => z9.union([knownFieldSchema3, customFieldSchema3]));
7944
- fieldHolder3[0] = fieldSchema3;
8594
+ const excludePath = path24.join(repoPath, ".git", "info", "exclude");
8595
+ await fs21.mkdir(path24.dirname(excludePath), { recursive: true });
8596
+ let content = "";
8597
+ try {
8598
+ content = await fs21.readFile(excludePath, "utf-8");
8599
+ } catch (err) {
8600
+ if (!isNotFoundError(err))
8601
+ throw err;
7945
8602
  }
7946
- });
7947
-
7948
- // dist/config/schemas/collection.js
7949
- import { z as z10 } from "zod";
7950
- import { isAbsolute as isAbsolute3 } from "pathe";
7951
- var relativePathSchema3, entryTypeSchema3, collectionSchema3;
7952
- var init_collection3 = __esm({
7953
- "dist/config/schemas/collection.js"() {
8603
+ const lines = content.split("\n");
8604
+ if (lines.some((line) => line.trim() === pattern)) {
8605
+ log9.debug("git", "Pattern already in .git/info/exclude", { pattern });
8606
+ return;
8607
+ }
8608
+ const needsLeadingNewline = content.length > 0 && !content.endsWith("\n");
8609
+ const newContent = content + (needsLeadingNewline ? "\n" : "") + pattern + "\n";
8610
+ await fs21.writeFile(excludePath, newContent, "utf-8");
8611
+ log9.debug("git", "Added pattern to .git/info/exclude", { pattern });
8612
+ }
8613
+ var log9;
8614
+ var init_git_manager = __esm({
8615
+ "dist/git-manager.js"() {
7954
8616
  "use strict";
7955
- init_field3();
7956
- relativePathSchema3 = z10.string().min(1).refine((val) => !isAbsolute3(val), { message: "Path must be relative" }).refine((val) => !val.split(/[\\/]+/).includes(".."), {
7957
- message: 'Path must not contain ".."'
7958
- }).transform((val) => val.split(/[\\/]+/).filter(Boolean).join("/"));
7959
- entryTypeSchema3 = z10.object({
7960
- name: z10.string().min(1),
7961
- format: z10.enum(["md", "mdx", "json", "yaml"]),
7962
- schema: z10.array(z10.lazy(() => fieldSchema3)).min(1),
7963
- label: z10.string().optional(),
7964
- description: z10.string().optional(),
7965
- default: z10.boolean().optional(),
7966
- maxItems: z10.number().int().positive().optional()
7967
- });
7968
- collectionSchema3 = z10.lazy(() => z10.object({
7969
- name: z10.string().min(1),
7970
- path: relativePathSchema3,
7971
- label: z10.string().optional(),
7972
- description: z10.string().optional(),
7973
- entries: z10.array(entryTypeSchema3).optional(),
7974
- collections: z10.array(collectionSchema3).optional(),
7975
- order: z10.array(z10.string()).optional()
7976
- // Embedded IDs for ordering items
7977
- }).refine((data) => data.entries || data.collections, {
7978
- message: "Collection must have entries or collections"
7979
- }));
8617
+ init_content_index_registry();
8618
+ init_content_index_generation();
8619
+ init_debug2();
8620
+ init_error();
8621
+ init_git();
8622
+ init_provisioning_lock();
8623
+ log9 = createDebugLogger3({ prefix: "GitManager" });
7980
8624
  }
7981
8625
  });
7982
8626
 
7983
- // dist/config/schemas/media.js
7984
- import { z as z11 } from "zod";
7985
- var mediaSchema3;
7986
- var init_media3 = __esm({
7987
- "dist/config/schemas/media.js"() {
8627
+ // dist/operating-mode/client-safe-strategy.js
8628
+ var ProdClientSafeStrategy3, DevClientSafeStrategy3;
8629
+ var init_client_safe_strategy2 = __esm({
8630
+ "dist/operating-mode/client-safe-strategy.js"() {
7988
8631
  "use strict";
7989
- init_collection3();
7990
- mediaSchema3 = z11.discriminatedUnion("adapter", [
7991
- z11.object({
7992
- adapter: z11.literal("local"),
7993
- publicBaseUrl: z11.string().url().optional(),
7994
- /** Root directory for local asset storage. Defaults to the caller's dev-assets dir. */
7995
- directory: relativePathSchema3.optional()
7996
- }),
7997
- z11.object({
7998
- adapter: z11.literal("s3"),
7999
- bucket: z11.string().min(1),
8000
- region: z11.string().min(1),
8001
- publicBaseUrl: z11.string().url().optional(),
8002
- /** Max upload size in bytes for presigned direct uploads. Defaults to 50 MiB. */
8003
- maxUploadBytes: z11.number().int().positive().optional()
8004
- }),
8005
- z11.object({
8006
- adapter: z11.literal("lfs"),
8007
- publicBaseUrl: z11.string().url().optional()
8008
- })
8009
- ]);
8632
+ ProdClientSafeStrategy3 = class {
8633
+ constructor() {
8634
+ this.mode = "prod";
8635
+ }
8636
+ // UI Feature Flags
8637
+ supportsBranching() {
8638
+ return true;
8639
+ }
8640
+ supportsStatusBadge() {
8641
+ return true;
8642
+ }
8643
+ supportsComments() {
8644
+ return true;
8645
+ }
8646
+ supportsPullRequests() {
8647
+ return true;
8648
+ }
8649
+ // Simple Data
8650
+ getPermissionsFileName() {
8651
+ return "permissions.json";
8652
+ }
8653
+ getGroupsFileName() {
8654
+ return "groups.json";
8655
+ }
8656
+ shouldCommit() {
8657
+ return true;
8658
+ }
8659
+ shouldPush() {
8660
+ return true;
8661
+ }
8662
+ };
8663
+ DevClientSafeStrategy3 = class {
8664
+ constructor() {
8665
+ this.mode = "dev";
8666
+ }
8667
+ // UI Feature Flags
8668
+ supportsBranching() {
8669
+ return true;
8670
+ }
8671
+ supportsStatusBadge() {
8672
+ return true;
8673
+ }
8674
+ supportsComments() {
8675
+ return true;
8676
+ }
8677
+ supportsPullRequests() {
8678
+ return false;
8679
+ }
8680
+ // Simple Data
8681
+ getPermissionsFileName() {
8682
+ return "permissions.json";
8683
+ }
8684
+ getGroupsFileName() {
8685
+ return "groups.json";
8686
+ }
8687
+ shouldCommit() {
8688
+ return true;
8689
+ }
8690
+ shouldPush() {
8691
+ return true;
8692
+ }
8693
+ };
8010
8694
  }
8011
8695
  });
8012
8696
 
8013
- // dist/config/schemas/config.js
8014
- import { z as z12 } from "zod";
8015
- var defaultBranchAccessSchema3, defaultPathAccessLevelSchema3, defaultPathAccessLevelsSchema3, defaultPathAccessSchema3, defaultBaseBranchSchema3, defaultRemoteNameSchema3, defaultRemoteUrlSchema3, gitBotAuthorNameSchema3, gitBotAuthorEmailSchema3, githubTokenEnvVarSchema3, operatingModeSchema3, deployedAsSchema3, contentRootSchema3, sourceRootSchema3, deploymentNameSchema3, devContentSyncSchema3, devConfigSchema3, editorConfigSchema3, CanopyConfigSchema3, DEFAULT_PROD_WORKSPACE3;
8016
- var init_config4 = __esm({
8017
- "dist/config/schemas/config.js"() {
8697
+ // dist/operating-mode/deployment-name.js
8698
+ function resolveDeploymentName3(config, modeDefault) {
8699
+ const envValue = process.env.CANOPYCMS_DEPLOYMENT_NAME?.trim();
8700
+ const configValue = config.deploymentName;
8701
+ if (envValue && configValue && envValue !== configValue && !warned3) {
8702
+ canopyLogWarn3(`CanopyCMS: CANOPYCMS_DEPLOYMENT_NAME ("${envValue}") differs from config.deploymentName ("${configValue}") \u2014 using the env var (infra-stamped env wins over shared-repo config by design; see resolveDeploymentName's doc comment). Update config.deploymentName to match if this mismatch was unintentional.`);
8703
+ warned3 = true;
8704
+ }
8705
+ const resolved = envValue || configValue || modeDefault;
8706
+ if (!isValidDeploymentName3(resolved)) {
8707
+ const source = envValue ? "CANOPYCMS_DEPLOYMENT_NAME" : configValue ? "config.deploymentName" : "the mode default";
8708
+ throw new Error(`CanopyCMS: invalid deploymentName ${JSON.stringify(resolved)} (from ${source}). It is interpolated into the settings branch name (canopycms-settings-<deploymentName>), so it must start with a letter or digit, contain only letters, digits, '.', '_' or '-', and must not contain '..' or end with '.' or '.lock'.`);
8709
+ }
8710
+ return resolved;
8711
+ }
8712
+ var warned3, VALID_DEPLOYMENT_NAME3, isValidDeploymentName3;
8713
+ var init_deployment_name2 = __esm({
8714
+ "dist/operating-mode/deployment-name.js"() {
8018
8715
  "use strict";
8019
- init_collection3();
8020
- init_media3();
8021
- defaultBranchAccessSchema3 = z12.enum(["allow", "deny"]).default("deny");
8022
- defaultPathAccessLevelSchema3 = z12.enum(["allow", "deny"]);
8023
- defaultPathAccessLevelsSchema3 = z12.object({
8024
- read: defaultPathAccessLevelSchema3.optional(),
8025
- edit: defaultPathAccessLevelSchema3.optional(),
8026
- review: defaultPathAccessLevelSchema3.optional()
8027
- }).strict();
8028
- defaultPathAccessSchema3 = z12.union([defaultPathAccessLevelSchema3, defaultPathAccessLevelsSchema3]).default("deny");
8029
- defaultBaseBranchSchema3 = z12.string().default("main");
8030
- defaultRemoteNameSchema3 = z12.string().default("origin");
8031
- defaultRemoteUrlSchema3 = z12.string().min(1);
8032
- gitBotAuthorNameSchema3 = z12.string().min(1);
8033
- gitBotAuthorEmailSchema3 = z12.string().email();
8034
- githubTokenEnvVarSchema3 = z12.string().default("GITHUB_BOT_TOKEN");
8035
- operatingModeSchema3 = z12.enum(["prod", "dev"]);
8036
- deployedAsSchema3 = z12.enum(["static", "server"]).default("server");
8037
- contentRootSchema3 = relativePathSchema3.default("content");
8038
- sourceRootSchema3 = z12.string().min(1).optional();
8039
- deploymentNameSchema3 = z12.string().default("prod");
8040
- devContentSyncSchema3 = z12.enum(["off", "warn"]).default("warn");
8041
- devConfigSchema3 = z12.object({
8042
- contentSync: devContentSyncSchema3.optional()
8043
- });
8044
- editorConfigSchema3 = z12.object({
8045
- title: z12.string().optional(),
8046
- subtitle: z12.string().optional(),
8047
- theme: z12.unknown().optional(),
8048
- previewBase: z12.record(z12.string()).optional(),
8049
- // UI handler functions (runtime only, don't serialize)
8050
- onAccountClick: z12.function().returns(z12.void()).optional(),
8051
- onLogoutClick: z12.function().returns(z12.void()).optional(),
8052
- // Optional: custom account component (e.g., Clerk's UserButton)
8053
- AccountComponent: z12.custom().optional()
8054
- });
8055
- CanopyConfigSchema3 = z12.object({
8056
- media: mediaSchema3.optional(),
8057
- // No outer .optional() here: defaultBranchAccessSchema/defaultPathAccessSchema already
8058
- // have .default('deny'), so the field is optional on input but always resolves to
8059
- // 'allow'/'deny' (never undefined) on output. An outer .optional() would short-circuit
8060
- // before the inner default runs, defeating the fail-closed default (SCH-M1).
8061
- // defaultPathAccessSchema's union keeps the same rule: the .default('deny') sits on the
8062
- // OUTER union, not inside defaultPathAccessLevelsSchema, so an omitted top-level field
8063
- // still resolves to 'deny' and an omitted level inside the object form stays undefined
8064
- // (resolved to 'deny' at read time by resolveDefaultPathAccess).
8065
- defaultBranchAccess: defaultBranchAccessSchema3,
8066
- defaultPathAccess: defaultPathAccessSchema3,
8067
- // .optional() deliberately defeats defaultBaseBranchSchema's .default('main'):
8068
- // unset must stay undefined after parsing so dev mode can detect the fork
8069
- // point from git HEAD (see resolveBaseBranch in utils/git.ts).
8070
- defaultBaseBranch: defaultBaseBranchSchema3.optional(),
8071
- defaultActiveBranch: z12.string().min(1).optional(),
8072
- defaultRemoteName: defaultRemoteNameSchema3.optional(),
8073
- defaultRemoteUrl: defaultRemoteUrlSchema3.optional(),
8074
- gitBotAuthorName: gitBotAuthorNameSchema3,
8075
- gitBotAuthorEmail: gitBotAuthorEmailSchema3,
8076
- githubTokenEnvVar: githubTokenEnvVarSchema3.optional(),
8077
- // Required by design (follow-up to SEC-C1): a prod deploy that omits `mode` must fail
8078
- // validation loudly rather than silently running header-trusting dev auth semantics.
8079
- mode: operatingModeSchema3,
8080
- deployedAs: deployedAsSchema3,
8081
- // Has .default('server'), so always present after validation
8082
- // Escape hatch for prod hosts that genuinely have internet access and intentionally
8083
- // run git against a network remote (see GitManager.resolveRemoteUrl's prod-mode guard).
8084
- // Default false/unset — the standard AWS Lambda+worker topology must leave this unset.
8085
- allowNetworkRemoteInProd: z12.boolean().optional(),
8086
- settingsBranch: z12.string().optional(),
8087
- autoCreateSettingsPR: z12.boolean().optional(),
8088
- deploymentName: deploymentNameSchema3.optional(),
8089
- contentRoot: contentRootSchema3.default("content"),
8090
- sourceRoot: sourceRootSchema3.optional(),
8091
- editor: editorConfigSchema3.optional(),
8092
- authPlugin: z12.custom().optional(),
8093
- entryLinkUrl: z12.custom().optional(),
8094
- validateEntry: z12.custom().optional(),
8095
- dev: devConfigSchema3.optional()
8096
- }).strict();
8097
- DEFAULT_PROD_WORKSPACE3 = "/mnt/efs/workspace";
8716
+ init_logger2();
8717
+ warned3 = false;
8718
+ VALID_DEPLOYMENT_NAME3 = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
8719
+ isValidDeploymentName3 = (name) => VALID_DEPLOYMENT_NAME3.test(name) && !name.includes("..") && !name.endsWith(".") && !name.endsWith(".lock");
8098
8720
  }
8099
8721
  });
8100
8722
 
@@ -8117,15 +8739,6 @@ var init_permissions3 = __esm({
8117
8739
  }
8118
8740
  });
8119
8741
 
8120
- // dist/config/validation.js
8121
- var init_validation4 = __esm({
8122
- "dist/config/validation.js"() {
8123
- "use strict";
8124
- init_config4();
8125
- init_flatten3();
8126
- }
8127
- });
8128
-
8129
8742
  // dist/config/helpers.js
8130
8743
  var init_helpers3 = __esm({
8131
8744
  "dist/config/helpers.js"() {
@@ -8177,6 +8790,7 @@ var init_client_unsafe_strategy2 = __esm({
8177
8790
  "dist/operating-mode/client-unsafe-strategy.js"() {
8178
8791
  "use strict";
8179
8792
  init_client_safe_strategy2();
8793
+ init_deployment_name2();
8180
8794
  init_config5();
8181
8795
  ProdStrategy3 = class extends ProdClientSafeStrategy3 {
8182
8796
  // All client-safe methods inherited automatically from ProdClientSafeStrategy:
@@ -8187,8 +8801,8 @@ var init_client_unsafe_strategy2 = __esm({
8187
8801
  getWorkspaceRoot(_sourceRoot) {
8188
8802
  return path25.resolve(process.env.CANOPYCMS_WORKSPACE_ROOT ?? DEFAULT_PROD_WORKSPACE3);
8189
8803
  }
8190
- getContentRoot(sourceRoot) {
8191
- return path25.resolve(sourceRoot ?? process.cwd(), "content");
8804
+ getContentRoot(contentRoot, sourceRoot) {
8805
+ return path25.resolve(sourceRoot ?? process.cwd(), contentRoot);
8192
8806
  }
8193
8807
  getContentBranchesRoot(sourceRoot) {
8194
8808
  return path25.join(this.getWorkspaceRoot(sourceRoot), "content-branches");
@@ -8219,8 +8833,7 @@ var init_client_unsafe_strategy2 = __esm({
8219
8833
  getSettingsBranchName(config) {
8220
8834
  if (config.settingsBranch)
8221
8835
  return config.settingsBranch;
8222
- const deploymentName = config.deploymentName ?? "prod";
8223
- return `canopycms-settings-${deploymentName}`;
8836
+ return `canopycms-settings-${resolveDeploymentName3(config, "prod")}`;
8224
8837
  }
8225
8838
  getSettingsRoot(sourceRoot) {
8226
8839
  return path25.join(this.getWorkspaceRoot(sourceRoot), "settings");
@@ -8242,8 +8855,8 @@ var init_client_unsafe_strategy2 = __esm({
8242
8855
  getWorkspaceRoot(sourceRoot) {
8243
8856
  return path25.resolve(sourceRoot ?? process.cwd(), ".canopy-dev");
8244
8857
  }
8245
- getContentRoot(sourceRoot) {
8246
- return path25.resolve(sourceRoot ?? process.cwd(), "content");
8858
+ getContentRoot(contentRoot, sourceRoot) {
8859
+ return path25.resolve(sourceRoot ?? process.cwd(), contentRoot);
8247
8860
  }
8248
8861
  getContentBranchesRoot(sourceRoot) {
8249
8862
  return path25.join(this.getWorkspaceRoot(sourceRoot), "content-branches");
@@ -8273,8 +8886,7 @@ var init_client_unsafe_strategy2 = __esm({
8273
8886
  getSettingsBranchName(config) {
8274
8887
  if (config.settingsBranch)
8275
8888
  return config.settingsBranch;
8276
- const deploymentName = config.deploymentName ?? "local";
8277
- return `canopycms-settings-${deploymentName}`;
8889
+ return `canopycms-settings-${resolveDeploymentName3(config, "local")}`;
8278
8890
  }
8279
8891
  getSettingsRoot(sourceRoot) {
8280
8892
  return path25.join(this.getWorkspaceRoot(sourceRoot), "settings");
@@ -8298,6 +8910,7 @@ var init_operating_mode2 = __esm({
8298
8910
  "use strict";
8299
8911
  init_client_safe_strategy2();
8300
8912
  init_client_unsafe_strategy2();
8913
+ init_deployment_name2();
8301
8914
  }
8302
8915
  });
8303
8916
 
@@ -8307,12 +8920,12 @@ __export(sync_exports, {
8307
8920
  SyncError: () => SyncError,
8308
8921
  sync: () => sync
8309
8922
  });
8310
- import fs23 from "node:fs/promises";
8923
+ import fs24 from "node:fs/promises";
8311
8924
  import path26 from "node:path";
8312
- import { simpleGit as simpleGit6 } from "simple-git";
8925
+ import { simpleGit as simpleGit7 } from "simple-git";
8313
8926
  import * as p2 from "@clack/prompts";
8314
8927
  async function detectCurrentBranch(projectDir) {
8315
- const git = simpleGit6({ baseDir: projectDir });
8928
+ const git = simpleGit7({ baseDir: projectDir });
8316
8929
  const branch = (await git.revparse(["--abbrev-ref", "HEAD"])).trim();
8317
8930
  if (branch === "HEAD")
8318
8931
  throw new Error("Detached HEAD \u2014 use --branch to specify a branch name");
@@ -8321,7 +8934,7 @@ async function detectCurrentBranch(projectDir) {
8321
8934
  async function selectBranch(options, branchesDir) {
8322
8935
  let branches = [];
8323
8936
  if (await filePathExists(branchesDir)) {
8324
- const entries = await fs23.readdir(branchesDir, { withFileTypes: true });
8937
+ const entries = await fs24.readdir(branchesDir, { withFileTypes: true });
8325
8938
  branches = entries.filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => e.name);
8326
8939
  }
8327
8940
  let branchName = options.branch;
@@ -8353,8 +8966,8 @@ async function selectBranch(options, branchesDir) {
8353
8966
  assertWithinDir(branchPath, branchesDir, "--branch");
8354
8967
  if (!branches.includes(branchName)) {
8355
8968
  if (options.autoCreate) {
8356
- await fs23.mkdir(branchPath, { recursive: true });
8357
- const wsGit = simpleGit6({ baseDir: branchPath });
8969
+ await fs24.mkdir(branchPath, { recursive: true });
8970
+ const wsGit = simpleGit7({ baseDir: branchPath });
8358
8971
  await wsGit.init();
8359
8972
  await wsGit.checkoutLocalBranch(branchName);
8360
8973
  await wsGit.raw(["commit", "--allow-empty", "-m", "init: workspace created by sync"]);
@@ -8381,7 +8994,7 @@ async function syncPush(options) {
8381
8994
  return { fileCount: 0 };
8382
8995
  const branchPath = path26.join(branchesDir, branchName);
8383
8996
  assertWithinDir(branchPath, branchesDir, "--branch");
8384
- const wsGit = simpleGit6({ baseDir: branchPath });
8997
+ const wsGit = simpleGit7({ baseDir: branchPath });
8385
8998
  const status = await wsGit.status();
8386
8999
  if (status.conflicted.length > 0) {
8387
9000
  p2.log.error("Branch workspace has unresolved merge conflicts.");
@@ -8451,7 +9064,7 @@ async function syncPull(options) {
8451
9064
  p2.log.step(`Pulling content from branch: ${branchName}`);
8452
9065
  const destContentDir = path26.join(projectDir, contentRoot);
8453
9066
  assertWithinDir(destContentDir, projectDir, "--content-root");
8454
- const sourceGit = simpleGit6({ baseDir: projectDir });
9067
+ const sourceGit = simpleGit7({ baseDir: projectDir });
8455
9068
  const status = await sourceGit.status();
8456
9069
  const uncommittedContent = status.files.filter((f) => f.path.startsWith(contentRoot + "/") || f.path === contentRoot);
8457
9070
  const untrackedLosses = [];
@@ -8501,7 +9114,7 @@ async function syncPull(options) {
8501
9114
  await copyDir(branchContentDir, tmpDestDir);
8502
9115
  await safeReplaceDir(destContentDir, tmpDestDir);
8503
9116
  } catch (err) {
8504
- await fs23.rm(tmpDestDir, { recursive: true, force: true }).catch(() => {
9117
+ await fs24.rm(tmpDestDir, { recursive: true, force: true }).catch(() => {
8505
9118
  });
8506
9119
  throw err;
8507
9120
  }
@@ -8536,7 +9149,7 @@ async function syncBoth(options) {
8536
9149
  return { pushed: 0, pulled: 0 };
8537
9150
  const branchPath = path26.join(branchesDir, branchName);
8538
9151
  assertWithinDir(branchPath, branchesDir, "--branch");
8539
- const wsGit = simpleGit6({ baseDir: branchPath });
9152
+ const wsGit = simpleGit7({ baseDir: branchPath });
8540
9153
  const status = await wsGit.status();
8541
9154
  if (status.conflicted.length > 0) {
8542
9155
  p2.log.error("Branch workspace has unresolved merge conflicts.");
@@ -8576,7 +9189,7 @@ async function syncBoth(options) {
8576
9189
  await copyDir(srcContentDir, tmpDir);
8577
9190
  await safeReplaceDir(wsContentDir, tmpDir);
8578
9191
  } catch (err) {
8579
- await fs23.rm(tmpDir, { recursive: true, force: true }).catch(() => {
9192
+ await fs24.rm(tmpDir, { recursive: true, force: true }).catch(() => {
8580
9193
  });
8581
9194
  await wsGit.checkout(currentBranch);
8582
9195
  await wsGit.raw(["branch", "-D", incomingBranch]).catch(() => {
@@ -8639,7 +9252,7 @@ async function syncAbort(options) {
8639
9252
  return;
8640
9253
  const branchPath = path26.join(branchesDir, branchName);
8641
9254
  assertWithinDir(branchPath, branchesDir, "--branch");
8642
- const wsGit = simpleGit6({ baseDir: branchPath });
9255
+ const wsGit = simpleGit7({ baseDir: branchPath });
8643
9256
  const status = await wsGit.status();
8644
9257
  if (status.conflicted.length === 0) {
8645
9258
  p2.log.info(`Branch workspace "${branchName}" is not in a merge state \u2014 nothing to abort.`);
@@ -8765,7 +9378,7 @@ var init_async_mutex = __esm({
8765
9378
  });
8766
9379
 
8767
9380
  // dist/utils/occ-json-write.js
8768
- import fs24 from "node:fs/promises";
9381
+ import fs25 from "node:fs/promises";
8769
9382
  import path27 from "node:path";
8770
9383
  import { randomUUID as randomUUID4 } from "node:crypto";
8771
9384
  import lockfile4 from "proper-lockfile";
@@ -8776,28 +9389,28 @@ async function writeOccJsonFile2(filePath, payload, options) {
8776
9389
  const data = { ...payload, version: newVersion, writeId };
8777
9390
  const content = JSON.stringify(data, null, 2) + (trailingNewline ? "\n" : "");
8778
9391
  const dir = path27.dirname(filePath);
8779
- await fs24.mkdir(dir, { recursive: true });
9392
+ await fs25.mkdir(dir, { recursive: true });
8780
9393
  const tempPath = `${filePath}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
8781
- await fs24.writeFile(tempPath, content, "utf-8");
9394
+ await fs25.writeFile(tempPath, content, "utf-8");
8782
9395
  if (expectedVersion === null) {
8783
9396
  try {
8784
- await fs24.link(tempPath, filePath);
9397
+ await fs25.link(tempPath, filePath);
8785
9398
  } catch (err) {
8786
- await fs24.unlink(tempPath).catch(() => {
9399
+ await fs25.unlink(tempPath).catch(() => {
8787
9400
  });
8788
9401
  if (isFileExistsError(err)) {
8789
9402
  throw new OccWriteConflictError2();
8790
9403
  }
8791
9404
  throw err;
8792
9405
  }
8793
- await fs24.unlink(tempPath).catch(() => {
9406
+ await fs25.unlink(tempPath).catch(() => {
8794
9407
  });
8795
9408
  return { version: newVersion, writeId };
8796
9409
  }
8797
9410
  try {
8798
9411
  let currentVersion = null;
8799
9412
  try {
8800
- const current = JSON.parse(await fs24.readFile(filePath, "utf-8"));
9413
+ const current = JSON.parse(await fs25.readFile(filePath, "utf-8"));
8801
9414
  currentVersion = current.version ?? 0;
8802
9415
  } catch {
8803
9416
  currentVersion = null;
@@ -8805,16 +9418,16 @@ async function writeOccJsonFile2(filePath, payload, options) {
8805
9418
  if (currentVersion !== expectedVersion) {
8806
9419
  throw new OccWriteConflictError2();
8807
9420
  }
8808
- await fs24.rename(tempPath, filePath);
9421
+ await fs25.rename(tempPath, filePath);
8809
9422
  if (settleMs > 0) {
8810
9423
  await new Promise((resolve2) => setTimeout(resolve2, settleMs));
8811
9424
  }
8812
- const afterWrite = JSON.parse(await fs24.readFile(filePath, "utf-8"));
9425
+ const afterWrite = JSON.parse(await fs25.readFile(filePath, "utf-8"));
8813
9426
  if (afterWrite.writeId !== writeId) {
8814
9427
  throw new OccWriteConflictError2();
8815
9428
  }
8816
9429
  } catch (err) {
8817
- await fs24.unlink(tempPath).catch(() => {
9430
+ await fs25.unlink(tempPath).catch(() => {
8818
9431
  });
8819
9432
  throw err;
8820
9433
  }
@@ -8841,7 +9454,7 @@ async function withOccRetry2(operation, options) {
8841
9454
  }
8842
9455
  async function withOccFileLock2(filePath, fn) {
8843
9456
  const dir = path27.dirname(filePath);
8844
- await fs24.mkdir(dir, { recursive: true });
9457
+ await fs25.mkdir(dir, { recursive: true });
8845
9458
  const MAX_ATTEMPTS = 14;
8846
9459
  let release = null;
8847
9460
  for (let attempt = 1; release === null; attempt++) {
@@ -8900,7 +9513,7 @@ var init_occ_json_write = __esm({
8900
9513
  });
8901
9514
 
8902
9515
  // dist/branch-registry.js
8903
- import fs25 from "node:fs/promises";
9516
+ import fs26 from "node:fs/promises";
8904
9517
  import path28 from "node:path";
8905
9518
  var log12, REGISTRY_FILE2, REGISTRY_STALE_FILE2, REGISTRY_TEMP_FILE2, REGISTRY_VERSION2, RESOURCE4, GET_MISS_REFRESH_MIN_INTERVAL_MS2, BranchRegistry2;
8906
9519
  var init_branch_registry = __esm({
@@ -8909,6 +9522,7 @@ var init_branch_registry = __esm({
8909
9522
  init_branch_metadata();
8910
9523
  init_error();
8911
9524
  init_debug2();
9525
+ init_logger2();
8912
9526
  init_resource_generation();
8913
9527
  log12 = createDebugLogger3({ prefix: "BranchRegistry" });
8914
9528
  REGISTRY_FILE2 = "branches.json";
@@ -8933,7 +9547,7 @@ var init_branch_registry = __esm({
8933
9547
  async list() {
8934
9548
  let parsed;
8935
9549
  try {
8936
- const raw = await fs25.readFile(this.registryPath, "utf8");
9550
+ const raw = await fs26.readFile(this.registryPath, "utf8");
8937
9551
  parsed = JSON.parse(raw);
8938
9552
  } catch (err) {
8939
9553
  if (isNotFoundError(err)) {
@@ -9022,7 +9636,7 @@ var init_branch_registry = __esm({
9022
9636
  async performRegenerate() {
9023
9637
  const read = await readResourceGeneration2(this.root, RESOURCE4);
9024
9638
  const branches = await this.scanBranchDirectories();
9025
- await fs25.unlink(this.stalePath).catch(() => {
9639
+ await fs26.unlink(this.stalePath).catch(() => {
9026
9640
  });
9027
9641
  if (!read.ok) {
9028
9642
  return branches;
@@ -9033,12 +9647,12 @@ var init_branch_registry = __esm({
9033
9647
  generation: read.token
9034
9648
  };
9035
9649
  const uniqueTempPath = `${this.tempPath}.${Date.now()}.${Math.random().toString(36).slice(2)}`;
9036
- await fs25.mkdir(this.root, { recursive: true });
9037
- await fs25.writeFile(uniqueTempPath, JSON.stringify(snapshot, null, 2) + "\n", "utf8");
9650
+ await fs26.mkdir(this.root, { recursive: true });
9651
+ await fs26.writeFile(uniqueTempPath, JSON.stringify(snapshot, null, 2) + "\n", "utf8");
9038
9652
  try {
9039
- await fs25.rename(uniqueTempPath, this.registryPath);
9653
+ await fs26.rename(uniqueTempPath, this.registryPath);
9040
9654
  } catch (err) {
9041
- await fs25.unlink(uniqueTempPath).catch(() => {
9655
+ await fs26.unlink(uniqueTempPath).catch(() => {
9042
9656
  });
9043
9657
  throw err;
9044
9658
  }
@@ -9052,7 +9666,7 @@ var init_branch_registry = __esm({
9052
9666
  async scanBranchDirectories() {
9053
9667
  const branches = [];
9054
9668
  try {
9055
- const entries = await fs25.readdir(this.root, { withFileTypes: true });
9669
+ const entries = await fs26.readdir(this.root, { withFileTypes: true });
9056
9670
  for (const entry of entries) {
9057
9671
  if (!entry.isDirectory() || entry.name.startsWith(".")) {
9058
9672
  continue;
@@ -9062,7 +9676,7 @@ var init_branch_registry = __esm({
9062
9676
  try {
9063
9677
  meta = await BranchMetadataFileManager2.loadOnly(branchRoot);
9064
9678
  } catch (err) {
9065
- console.warn(`CanopyCMS: Skipping branch directory '${entry.name}' during registry scan: ${getErrorMessage(err)}`);
9679
+ canopyLogWarn3(`CanopyCMS: Skipping branch directory '${entry.name}' during registry scan: ${getErrorMessage(err)}`);
9066
9680
  continue;
9067
9681
  }
9068
9682
  if (meta) {
@@ -9086,7 +9700,7 @@ var init_branch_registry = __esm({
9086
9700
  });
9087
9701
 
9088
9702
  // dist/branch-metadata.js
9089
- import fs26 from "node:fs/promises";
9703
+ import fs27 from "node:fs/promises";
9090
9704
  import path29 from "node:path";
9091
9705
  var BRANCH_META_DIR2, BRANCH_META_FILE2, CURRENT_SCHEMA_VERSION2, BranchMetadataConflictError2, BranchMetadataCorruptError2, BranchMetadataFileManager2;
9092
9706
  var init_branch_metadata = __esm({
@@ -9129,7 +9743,7 @@ var init_branch_metadata = __esm({
9129
9743
  const resolvedRoot = path29.resolve(branchRoot);
9130
9744
  const filePath = path29.join(resolvedRoot, BRANCH_META_DIR2, BRANCH_META_FILE2);
9131
9745
  try {
9132
- const raw = await fs26.readFile(filePath, "utf8");
9746
+ const raw = await fs27.readFile(filePath, "utf8");
9133
9747
  return JSON.parse(raw);
9134
9748
  } catch (err) {
9135
9749
  if (isNotFoundError(err)) {
@@ -9150,7 +9764,7 @@ var init_branch_metadata = __esm({
9150
9764
  }
9151
9765
  async load() {
9152
9766
  try {
9153
- const raw = await fs26.readFile(this.filePath, "utf8");
9767
+ const raw = await fs27.readFile(this.filePath, "utf8");
9154
9768
  const parsed = JSON.parse(raw);
9155
9769
  const version = parsed.version ?? 0;
9156
9770
  return { meta: parsed, version };
@@ -9212,7 +9826,7 @@ var init_branch_metadata = __esm({
9212
9826
  */
9213
9827
  async save(incoming) {
9214
9828
  try {
9215
- await fs26.stat(this.branchRoot);
9829
+ await fs27.stat(this.branchRoot);
9216
9830
  } catch (err) {
9217
9831
  if (isNotFoundError(err)) {
9218
9832
  throw new BranchMetadataConflictError2("Branch no longer exists");
@@ -9287,7 +9901,7 @@ __export(migrate_exports, {
9287
9901
  migrate: () => migrate,
9288
9902
  slugifyName: () => slugifyName
9289
9903
  });
9290
- import fs27 from "node:fs/promises";
9904
+ import fs28 from "node:fs/promises";
9291
9905
  import path30 from "node:path";
9292
9906
  import * as p3 from "@clack/prompts";
9293
9907
  function slugifyName(name) {
@@ -9313,7 +9927,7 @@ function stripDirIdSuffix(dirName) {
9313
9927
  return dirName;
9314
9928
  }
9315
9929
  async function planDirectory(dirPath, isRoot, ctx) {
9316
- const entries = await fs27.readdir(dirPath, { withFileTypes: true });
9930
+ const entries = await fs28.readdir(dirPath, { withFileTypes: true });
9317
9931
  entries.sort((a, b) => a.name.localeCompare(b.name));
9318
9932
  const ops = [];
9319
9933
  let hasOwnFiles = false;
@@ -9440,9 +10054,9 @@ async function migrate(options) {
9440
10054
  const applyOps = async () => {
9441
10055
  for (const op of ops) {
9442
10056
  if (op.kind === "rename") {
9443
- await fs27.rename(op.from, op.to);
10057
+ await fs28.rename(op.from, op.to);
9444
10058
  } else {
9445
- await fs27.writeFile(op.filePath, op.content, "utf-8");
10059
+ await fs28.writeFile(op.filePath, op.content, "utf-8");
9446
10060
  }
9447
10061
  }
9448
10062
  };