canopycms 0.0.11 → 0.0.13

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 (143) hide show
  1. package/README.md +24 -27
  2. package/dist/ai/handler.d.ts +3 -3
  3. package/dist/ai/handler.d.ts.map +1 -1
  4. package/dist/ai/handler.js +6 -9
  5. package/dist/ai/handler.js.map +1 -1
  6. package/dist/ai/resolve-branch.d.ts +1 -2
  7. package/dist/ai/resolve-branch.d.ts.map +1 -1
  8. package/dist/ai/resolve-branch.js +8 -9
  9. package/dist/ai/resolve-branch.js.map +1 -1
  10. package/dist/api/branch.js +2 -2
  11. package/dist/api/branch.js.map +1 -1
  12. package/dist/api/comments.d.ts +2 -2
  13. package/dist/api/content.d.ts +4 -4
  14. package/dist/api/content.d.ts.map +1 -1
  15. package/dist/api/content.js +2 -2
  16. package/dist/api/content.js.map +1 -1
  17. package/dist/api/entries.d.ts +2 -2
  18. package/dist/api/entries.d.ts.map +1 -1
  19. package/dist/api/entries.js +2 -2
  20. package/dist/api/entries.js.map +1 -1
  21. package/dist/api/github-sync.js +0 -2
  22. package/dist/api/github-sync.js.map +1 -1
  23. package/dist/api/settings-helpers.d.ts +3 -5
  24. package/dist/api/settings-helpers.d.ts.map +1 -1
  25. package/dist/api/settings-helpers.js +6 -19
  26. package/dist/api/settings-helpers.js.map +1 -1
  27. package/dist/api/validators.d.ts +3 -13
  28. package/dist/api/validators.d.ts.map +1 -1
  29. package/dist/api/validators.js +3 -26
  30. package/dist/api/validators.js.map +1 -1
  31. package/dist/auth/caching-auth-plugin.d.ts +7 -1
  32. package/dist/auth/caching-auth-plugin.d.ts.map +1 -1
  33. package/dist/auth/caching-auth-plugin.js +31 -3
  34. package/dist/auth/caching-auth-plugin.js.map +1 -1
  35. package/dist/auth/plugin.d.ts +1 -1
  36. package/dist/authorization/types.d.ts +1 -1
  37. package/dist/branch-registry.js +1 -1
  38. package/dist/branch-registry.js.map +1 -1
  39. package/dist/branch-schema-cache.d.ts +8 -13
  40. package/dist/branch-schema-cache.d.ts.map +1 -1
  41. package/dist/branch-schema-cache.js +55 -44
  42. package/dist/branch-schema-cache.js.map +1 -1
  43. package/dist/cli/cli.d.ts +20 -0
  44. package/dist/cli/cli.d.ts.map +1 -0
  45. package/dist/cli/cli.js +196 -0
  46. package/dist/cli/cli.js.map +1 -0
  47. package/dist/cli/generate-ai-content.js +1508 -733
  48. package/dist/cli/init.d.ts +2 -3
  49. package/dist/cli/init.d.ts.map +1 -1
  50. package/dist/cli/init.js +258 -2861
  51. package/dist/cli/init.js.map +1 -1
  52. package/dist/cli/sync.d.ts +33 -0
  53. package/dist/cli/sync.d.ts.map +1 -0
  54. package/dist/cli/sync.js +510 -0
  55. package/dist/cli/sync.js.map +1 -0
  56. package/dist/config/schemas/config.d.ts +5 -5
  57. package/dist/config/schemas/config.d.ts.map +1 -1
  58. package/dist/config/schemas/config.js +1 -1
  59. package/dist/config/schemas/config.js.map +1 -1
  60. package/dist/config-test.d.ts.map +1 -1
  61. package/dist/config-test.js +0 -1
  62. package/dist/config-test.js.map +1 -1
  63. package/dist/content-id-index.d.ts +3 -3
  64. package/dist/content-id-index.d.ts.map +1 -1
  65. package/dist/content-id-index.js +7 -7
  66. package/dist/content-id-index.js.map +1 -1
  67. package/dist/content-listing.d.ts +3 -3
  68. package/dist/content-listing.d.ts.map +1 -1
  69. package/dist/content-listing.js +1 -1
  70. package/dist/content-listing.js.map +1 -1
  71. package/dist/content-reader.d.ts +2 -2
  72. package/dist/content-reader.d.ts.map +1 -1
  73. package/dist/content-reader.js +1 -1
  74. package/dist/content-reader.js.map +1 -1
  75. package/dist/content-store.d.ts +8 -8
  76. package/dist/content-store.d.ts.map +1 -1
  77. package/dist/content-store.js +6 -9
  78. package/dist/content-store.js.map +1 -1
  79. package/dist/content-tree.d.ts +2 -2
  80. package/dist/content-tree.d.ts.map +1 -1
  81. package/dist/context.js +1 -1
  82. package/dist/context.js.map +1 -1
  83. package/dist/editor/BranchManager.d.ts.map +1 -1
  84. package/dist/editor/BranchManager.js +1 -3
  85. package/dist/editor/BranchManager.js.map +1 -1
  86. package/dist/git-manager.d.ts +2 -3
  87. package/dist/git-manager.d.ts.map +1 -1
  88. package/dist/git-manager.js +12 -4
  89. package/dist/git-manager.js.map +1 -1
  90. package/dist/operating-mode/client-safe-strategy.d.ts +1 -12
  91. package/dist/operating-mode/client-safe-strategy.d.ts.map +1 -1
  92. package/dist/operating-mode/client-safe-strategy.js +5 -42
  93. package/dist/operating-mode/client-safe-strategy.js.map +1 -1
  94. package/dist/operating-mode/client-unsafe-strategy.d.ts.map +1 -1
  95. package/dist/operating-mode/client-unsafe-strategy.js +10 -68
  96. package/dist/operating-mode/client-unsafe-strategy.js.map +1 -1
  97. package/dist/operating-mode/index.d.ts +3 -3
  98. package/dist/operating-mode/index.d.ts.map +1 -1
  99. package/dist/operating-mode/index.js +2 -2
  100. package/dist/operating-mode/types.d.ts +2 -6
  101. package/dist/operating-mode/types.d.ts.map +1 -1
  102. package/dist/paths/index.d.ts +1 -1
  103. package/dist/paths/index.d.ts.map +1 -1
  104. package/dist/paths/index.js.map +1 -1
  105. package/dist/paths/test-utils.d.ts +4 -6
  106. package/dist/paths/test-utils.d.ts.map +1 -1
  107. package/dist/paths/test-utils.js +3 -5
  108. package/dist/paths/test-utils.js.map +1 -1
  109. package/dist/paths/types.d.ts +4 -11
  110. package/dist/paths/types.d.ts.map +1 -1
  111. package/dist/paths/validation.d.ts +5 -6
  112. package/dist/paths/validation.d.ts.map +1 -1
  113. package/dist/paths/validation.js +9 -10
  114. package/dist/paths/validation.js.map +1 -1
  115. package/dist/reference-resolver.d.ts +2 -2
  116. package/dist/reference-resolver.d.ts.map +1 -1
  117. package/dist/reference-resolver.js.map +1 -1
  118. package/dist/services.d.ts +6 -0
  119. package/dist/services.d.ts.map +1 -1
  120. package/dist/services.js +52 -40
  121. package/dist/services.js.map +1 -1
  122. package/dist/settings-branch-utils.d.ts +2 -2
  123. package/dist/settings-branch-utils.js +3 -3
  124. package/dist/settings-branch-utils.js.map +1 -1
  125. package/dist/settings-workspace.d.ts +1 -2
  126. package/dist/settings-workspace.d.ts.map +1 -1
  127. package/dist/settings-workspace.js +1 -2
  128. package/dist/settings-workspace.js.map +1 -1
  129. package/dist/utils/fs.d.ts +3 -0
  130. package/dist/utils/fs.d.ts.map +1 -0
  131. package/dist/utils/fs.js +15 -0
  132. package/dist/utils/fs.js.map +1 -0
  133. package/dist/utils/git.d.ts +7 -0
  134. package/dist/utils/git.d.ts.map +1 -0
  135. package/dist/utils/git.js +17 -0
  136. package/dist/utils/git.js.map +1 -0
  137. package/dist/validation/deletion-checker.d.ts +2 -2
  138. package/dist/validation/deletion-checker.d.ts.map +1 -1
  139. package/dist/worker/task-queue-config.d.ts +2 -4
  140. package/dist/worker/task-queue-config.d.ts.map +1 -1
  141. package/dist/worker/task-queue-config.js +3 -7
  142. package/dist/worker/task-queue-config.js.map +1 -1
  143. package/package.json +4 -2
@@ -1,13 +1,701 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __esm = (fn, res) => function __init() {
4
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
5
+ };
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+
11
+ // dist/paths/normalize.js
12
+ function normalizeFilesystemPath(path13) {
13
+ return path13.split(/[\\/]+/).filter(Boolean).join("/");
14
+ }
15
+ function hasTraversalSequence(path13) {
16
+ const normalized = normalizeFilesystemPath(path13);
17
+ return normalized.includes("..");
18
+ }
19
+ function createLogicalPath(...segments) {
20
+ const normalized = segments.map((s) => normalizeFilesystemPath(s)).filter(Boolean).join("/");
21
+ if (hasTraversalSequence(normalized)) {
22
+ throw new Error(`Invalid path: contains traversal sequence: ${normalized}`);
23
+ }
24
+ return normalized;
25
+ }
26
+ var init_normalize = __esm({
27
+ "dist/paths/normalize.js"() {
28
+ "use strict";
29
+ }
30
+ });
31
+
32
+ // dist/operating-mode/client-safe-strategy.js
33
+ function clientOperatingStrategy(mode) {
34
+ const cached = clientStrategyCache.get(mode);
35
+ if (cached)
36
+ return cached;
37
+ let strategy;
38
+ switch (mode) {
39
+ case "prod":
40
+ strategy = new ProdClientSafeStrategy();
41
+ break;
42
+ case "dev":
43
+ strategy = new DevClientSafeStrategy();
44
+ break;
45
+ default: {
46
+ const _exhaustive = mode;
47
+ throw new Error(`Unknown operating mode: ${_exhaustive}`);
48
+ }
49
+ }
50
+ clientStrategyCache.set(mode, strategy);
51
+ return strategy;
52
+ }
53
+ function clearClientStrategyCache() {
54
+ clientStrategyCache.clear();
55
+ }
56
+ var ProdClientSafeStrategy, DevClientSafeStrategy, clientStrategyCache;
57
+ var init_client_safe_strategy = __esm({
58
+ "dist/operating-mode/client-safe-strategy.js"() {
59
+ "use strict";
60
+ ProdClientSafeStrategy = class {
61
+ constructor() {
62
+ this.mode = "prod";
63
+ }
64
+ // UI Feature Flags
65
+ supportsBranching() {
66
+ return true;
67
+ }
68
+ supportsStatusBadge() {
69
+ return true;
70
+ }
71
+ supportsComments() {
72
+ return true;
73
+ }
74
+ supportsPullRequests() {
75
+ return true;
76
+ }
77
+ // Simple Data
78
+ getPermissionsFileName() {
79
+ return "permissions.json";
80
+ }
81
+ getGroupsFileName() {
82
+ return "groups.json";
83
+ }
84
+ shouldCommit() {
85
+ return true;
86
+ }
87
+ shouldPush() {
88
+ return true;
89
+ }
90
+ };
91
+ DevClientSafeStrategy = class {
92
+ constructor() {
93
+ this.mode = "dev";
94
+ }
95
+ // UI Feature Flags
96
+ supportsBranching() {
97
+ return true;
98
+ }
99
+ supportsStatusBadge() {
100
+ return true;
101
+ }
102
+ supportsComments() {
103
+ return true;
104
+ }
105
+ supportsPullRequests() {
106
+ return false;
107
+ }
108
+ // Simple Data
109
+ getPermissionsFileName() {
110
+ return "permissions.json";
111
+ }
112
+ getGroupsFileName() {
113
+ return "groups.json";
114
+ }
115
+ shouldCommit() {
116
+ return true;
117
+ }
118
+ shouldPush() {
119
+ return true;
120
+ }
121
+ };
122
+ clientStrategyCache = /* @__PURE__ */ new Map();
123
+ }
124
+ });
125
+
126
+ // dist/config/types.js
127
+ var primitiveFieldTypes, fieldTypes;
128
+ var init_types = __esm({
129
+ "dist/config/types.js"() {
130
+ "use strict";
131
+ primitiveFieldTypes = [
132
+ "string",
133
+ "number",
134
+ "boolean",
135
+ "datetime",
136
+ "rich-text",
137
+ "markdown",
138
+ "mdx",
139
+ "image",
140
+ "code"
141
+ ];
142
+ fieldTypes = [
143
+ ...primitiveFieldTypes,
144
+ "select",
145
+ "reference",
146
+ "object",
147
+ "block"
148
+ ];
149
+ }
150
+ });
151
+
152
+ // dist/config/schemas/field.js
153
+ import { z } from "zod";
154
+ var fieldBaseSchema, selectOptionSchema, referenceOptionSchema, primitiveFieldSchema, selectFieldSchema, referenceFieldSchema, fieldHolder, blockSchema, blockFieldSchema, objectFieldSchema, customFieldSchema, knownFieldSchema, fieldSchema;
155
+ var init_field = __esm({
156
+ "dist/config/schemas/field.js"() {
157
+ "use strict";
158
+ init_types();
159
+ fieldBaseSchema = z.object({
160
+ name: z.string().min(1),
161
+ label: z.string().optional(),
162
+ description: z.string().optional(),
163
+ required: z.boolean().optional(),
164
+ list: z.boolean().optional()
165
+ });
166
+ selectOptionSchema = z.union([
167
+ z.string(),
168
+ z.object({
169
+ label: z.string().min(1),
170
+ value: z.string().min(1)
171
+ })
172
+ ]);
173
+ referenceOptionSchema = z.union([
174
+ z.string(),
175
+ z.object({
176
+ label: z.string().min(1),
177
+ value: z.string().min(1)
178
+ })
179
+ ]);
180
+ primitiveFieldSchema = fieldBaseSchema.extend({
181
+ type: z.enum(primitiveFieldTypes)
182
+ });
183
+ selectFieldSchema = fieldBaseSchema.extend({
184
+ type: z.literal("select"),
185
+ options: z.array(selectOptionSchema).min(1)
186
+ });
187
+ referenceFieldSchema = fieldBaseSchema.extend({
188
+ type: z.literal("reference"),
189
+ collections: z.array(z.string().min(1)).min(1),
190
+ displayField: z.string().min(1).optional(),
191
+ options: z.array(referenceOptionSchema).optional()
192
+ });
193
+ fieldHolder = [z.never()];
194
+ blockSchema = z.object({
195
+ name: z.string().min(1),
196
+ label: z.string().optional(),
197
+ description: z.string().optional(),
198
+ fields: z.array(z.lazy(() => fieldHolder[0])).min(1)
199
+ });
200
+ blockFieldSchema = fieldBaseSchema.extend({
201
+ type: z.literal("block"),
202
+ templates: z.array(blockSchema).min(1)
203
+ });
204
+ objectFieldSchema = fieldBaseSchema.extend({
205
+ type: z.literal("object"),
206
+ fields: z.array(z.lazy(() => fieldHolder[0])).min(1)
207
+ });
208
+ customFieldSchema = z.lazy(() => fieldBaseSchema.extend({
209
+ type: z.string().min(1).refine((val) => !fieldTypes.includes(val), {
210
+ message: "Custom field types must not conflict with built-in types"
211
+ })
212
+ }).passthrough());
213
+ knownFieldSchema = z.discriminatedUnion("type", [
214
+ primitiveFieldSchema,
215
+ selectFieldSchema,
216
+ referenceFieldSchema,
217
+ objectFieldSchema,
218
+ blockFieldSchema
219
+ ]);
220
+ fieldSchema = z.lazy(() => z.union([knownFieldSchema, customFieldSchema]));
221
+ fieldHolder[0] = fieldSchema;
222
+ }
223
+ });
224
+
225
+ // dist/config/schemas/collection.js
226
+ import { z as z2 } from "zod";
227
+ import { isAbsolute } from "pathe";
228
+ var relativePathSchema, entryTypeSchema, collectionSchema, rootCollectionSchema;
229
+ var init_collection = __esm({
230
+ "dist/config/schemas/collection.js"() {
231
+ "use strict";
232
+ init_field();
233
+ relativePathSchema = z2.string().min(1).refine((val) => !isAbsolute(val), { message: "Path must be relative" }).refine((val) => !val.split(/[\\/]+/).includes(".."), {
234
+ message: 'Path must not contain ".."'
235
+ }).transform((val) => val.split(/[\\/]+/).filter(Boolean).join("/"));
236
+ entryTypeSchema = z2.object({
237
+ name: z2.string().min(1),
238
+ format: z2.enum(["md", "mdx", "json"]),
239
+ schema: z2.array(z2.lazy(() => fieldSchema)).min(1),
240
+ label: z2.string().optional(),
241
+ description: z2.string().optional(),
242
+ default: z2.boolean().optional(),
243
+ maxItems: z2.number().int().positive().optional()
244
+ });
245
+ collectionSchema = z2.lazy(() => z2.object({
246
+ name: z2.string().min(1),
247
+ path: relativePathSchema,
248
+ label: z2.string().optional(),
249
+ description: z2.string().optional(),
250
+ entries: z2.array(entryTypeSchema).optional(),
251
+ collections: z2.array(collectionSchema).optional(),
252
+ order: z2.array(z2.string()).optional()
253
+ // Embedded IDs for ordering items
254
+ }).refine((data) => data.entries || data.collections, {
255
+ message: "Collection must have entries or collections"
256
+ }));
257
+ rootCollectionSchema = z2.object({
258
+ entries: z2.array(entryTypeSchema).optional(),
259
+ collections: z2.array(collectionSchema).optional(),
260
+ order: z2.array(z2.string()).optional()
261
+ // Embedded IDs for ordering items
262
+ });
263
+ }
264
+ });
265
+
266
+ // dist/config/schemas/media.js
267
+ import { z as z3 } from "zod";
268
+ var mediaSchema;
269
+ var init_media = __esm({
270
+ "dist/config/schemas/media.js"() {
271
+ "use strict";
272
+ mediaSchema = z3.union([
273
+ z3.object({
274
+ adapter: z3.literal("local"),
275
+ publicBaseUrl: z3.string().url().optional()
276
+ }),
277
+ z3.object({
278
+ adapter: z3.literal("s3"),
279
+ bucket: z3.string().min(1),
280
+ region: z3.string().min(1),
281
+ publicBaseUrl: z3.string().url().optional()
282
+ }),
283
+ z3.object({
284
+ adapter: z3.literal("lfs"),
285
+ publicBaseUrl: z3.string().url().optional()
286
+ }),
287
+ z3.object({
288
+ adapter: z3.string().min(1),
289
+ publicBaseUrl: z3.string().url().optional()
290
+ })
291
+ ]);
292
+ }
293
+ });
294
+
295
+ // dist/config/schemas/config.js
296
+ import { z as z4 } from "zod";
297
+ var defaultBranchAccessSchema, defaultPathAccessSchema, defaultBaseBranchSchema, defaultRemoteNameSchema, defaultRemoteUrlSchema, gitBotAuthorNameSchema, gitBotAuthorEmailSchema, githubTokenEnvVarSchema, operatingModeSchema, deployedAsSchema, contentRootSchema, sourceRootSchema, deploymentNameSchema, editorConfigSchema, CanopyConfigSchema, DEFAULT_PROD_WORKSPACE;
298
+ var init_config = __esm({
299
+ "dist/config/schemas/config.js"() {
300
+ "use strict";
301
+ init_collection();
302
+ init_media();
303
+ defaultBranchAccessSchema = z4.enum(["allow", "deny"]).default("deny");
304
+ defaultPathAccessSchema = z4.enum(["allow", "deny"]).default("deny");
305
+ defaultBaseBranchSchema = z4.string().default("main");
306
+ defaultRemoteNameSchema = z4.string().default("origin");
307
+ defaultRemoteUrlSchema = z4.string().min(1);
308
+ gitBotAuthorNameSchema = z4.string().min(1);
309
+ gitBotAuthorEmailSchema = z4.string().email();
310
+ githubTokenEnvVarSchema = z4.string().default("GITHUB_BOT_TOKEN");
311
+ operatingModeSchema = z4.enum(["prod", "dev"]).default("dev");
312
+ deployedAsSchema = z4.enum(["static", "server"]).default("server");
313
+ contentRootSchema = relativePathSchema.default("content");
314
+ sourceRootSchema = z4.string().min(1).optional();
315
+ deploymentNameSchema = z4.string().default("prod");
316
+ editorConfigSchema = z4.object({
317
+ title: z4.string().optional(),
318
+ subtitle: z4.string().optional(),
319
+ theme: z4.unknown().optional(),
320
+ previewBase: z4.record(z4.string()).optional(),
321
+ // UI handler functions (runtime only, don't serialize)
322
+ onAccountClick: z4.function().returns(z4.void()).optional(),
323
+ onLogoutClick: z4.function().returns(z4.void()).optional(),
324
+ // Optional: custom account component (e.g., Clerk's UserButton)
325
+ AccountComponent: z4.custom().optional()
326
+ });
327
+ CanopyConfigSchema = z4.object({
328
+ schema: rootCollectionSchema.optional(),
329
+ media: mediaSchema.optional(),
330
+ defaultBranchAccess: defaultBranchAccessSchema.optional(),
331
+ defaultPathAccess: defaultPathAccessSchema.optional(),
332
+ defaultBaseBranch: defaultBaseBranchSchema.optional(),
333
+ defaultRemoteName: defaultRemoteNameSchema.optional(),
334
+ defaultRemoteUrl: defaultRemoteUrlSchema.optional(),
335
+ gitBotAuthorName: gitBotAuthorNameSchema,
336
+ gitBotAuthorEmail: gitBotAuthorEmailSchema,
337
+ githubTokenEnvVar: githubTokenEnvVarSchema.optional(),
338
+ mode: operatingModeSchema,
339
+ // Has .default(), so not optional in output type
340
+ deployedAs: deployedAsSchema,
341
+ // Has .default('server'), so always present after validation
342
+ settingsBranch: z4.string().optional(),
343
+ autoCreateSettingsPR: z4.boolean().optional(),
344
+ deploymentName: deploymentNameSchema.optional(),
345
+ contentRoot: contentRootSchema.default("content"),
346
+ sourceRoot: sourceRootSchema.optional(),
347
+ editor: editorConfigSchema.optional(),
348
+ authPlugin: z4.custom().optional()
349
+ });
350
+ DEFAULT_PROD_WORKSPACE = "/mnt/efs/workspace";
351
+ }
352
+ });
353
+
354
+ // dist/config/schemas/permissions.js
355
+ import { z as z5 } from "zod";
356
+ var permissionTargetSchema, pathPermissionSchema;
357
+ var init_permissions = __esm({
358
+ "dist/config/schemas/permissions.js"() {
359
+ "use strict";
360
+ permissionTargetSchema = z5.object({
361
+ allowedUsers: z5.array(z5.string()).optional(),
362
+ allowedGroups: z5.array(z5.string()).optional()
363
+ });
364
+ pathPermissionSchema = z5.object({
365
+ path: z5.string().min(1),
366
+ read: permissionTargetSchema.optional(),
367
+ edit: permissionTargetSchema.optional(),
368
+ review: permissionTargetSchema.optional()
369
+ });
370
+ }
371
+ });
372
+
373
+ // dist/paths/types.js
374
+ var ROOT_COLLECTION_ID;
375
+ var init_types2 = __esm({
376
+ "dist/paths/types.js"() {
377
+ "use strict";
378
+ ROOT_COLLECTION_ID = "__rootcoll__";
379
+ }
380
+ });
381
+
382
+ // dist/config/flatten.js
383
+ import { join, normalize } from "pathe";
384
+ var normalizePathValue, flattenSchema;
385
+ var init_flatten = __esm({
386
+ "dist/config/flatten.js"() {
387
+ "use strict";
388
+ init_normalize();
389
+ init_types2();
390
+ normalizePathValue = (val) => normalize(val).split("/").filter(Boolean).join("/");
391
+ flattenSchema = (root, basePath = "") => {
392
+ const flat = [];
393
+ const base = normalizePathValue(basePath || "");
394
+ const walkCollection = (collection, parentPath) => {
395
+ const normalizedPath = normalizePathValue(collection.path);
396
+ let logicalPath;
397
+ if (parentPath && parentPath !== base) {
398
+ logicalPath = join(parentPath, collection.name);
399
+ } else if (parentPath === base) {
400
+ logicalPath = join(base, normalizedPath);
401
+ } else {
402
+ logicalPath = normalizedPath;
403
+ }
404
+ const normalizedFull = normalizePathValue(logicalPath);
405
+ flat.push({
406
+ type: "collection",
407
+ logicalPath: createLogicalPath(normalizedFull),
408
+ name: collection.name,
409
+ label: collection.label,
410
+ description: collection.description,
411
+ contentId: collection.contentId,
412
+ parentPath: parentPath ? createLogicalPath(parentPath) : void 0,
413
+ entries: collection.entries,
414
+ collections: collection.collections,
415
+ order: collection.order
416
+ });
417
+ if (collection.entries) {
418
+ for (const entryType of collection.entries) {
419
+ const entryTypePath = join(normalizedFull, entryType.name);
420
+ flat.push({
421
+ type: "entry-type",
422
+ logicalPath: createLogicalPath(normalizePathValue(entryTypePath)),
423
+ name: entryType.name,
424
+ label: entryType.label,
425
+ description: entryType.description,
426
+ parentPath: createLogicalPath(normalizedFull),
427
+ format: entryType.format,
428
+ schema: entryType.schema,
429
+ schemaRef: entryType.schemaRef,
430
+ default: entryType.default,
431
+ maxItems: entryType.maxItems
432
+ });
433
+ }
434
+ }
435
+ if (collection.collections) {
436
+ for (const child of collection.collections) {
437
+ walkCollection(child, normalizedFull);
438
+ }
439
+ }
440
+ };
441
+ if (base) {
442
+ flat.push({
443
+ type: "collection",
444
+ logicalPath: createLogicalPath(base),
445
+ name: base,
446
+ // Use base path as the name (e.g., 'content')
447
+ label: void 0,
448
+ // Root collection has no label
449
+ contentId: ROOT_COLLECTION_ID,
450
+ // Sentinel — root dir has no embedded ID
451
+ parentPath: void 0,
452
+ // No parent - this is the root
453
+ entries: root.entries,
454
+ collections: root.collections,
455
+ order: root.order
456
+ });
457
+ }
458
+ if (root.entries) {
459
+ for (const entryType of root.entries) {
460
+ const entryTypePath = base ? join(base, entryType.name) : entryType.name;
461
+ flat.push({
462
+ type: "entry-type",
463
+ logicalPath: createLogicalPath(normalizePathValue(entryTypePath)),
464
+ name: entryType.name,
465
+ label: entryType.label,
466
+ description: entryType.description,
467
+ parentPath: base ? createLogicalPath(base) : createLogicalPath(""),
468
+ // Now references the root collection (e.g., 'content')
469
+ format: entryType.format,
470
+ schema: entryType.schema,
471
+ schemaRef: entryType.schemaRef,
472
+ default: entryType.default,
473
+ maxItems: entryType.maxItems
474
+ });
475
+ }
476
+ }
477
+ if (root.collections) {
478
+ for (const collection of root.collections) {
479
+ walkCollection(collection, base || "");
480
+ }
481
+ }
482
+ return flat;
483
+ };
484
+ }
485
+ });
486
+
487
+ // dist/config/validation.js
488
+ var init_validation = __esm({
489
+ "dist/config/validation.js"() {
490
+ "use strict";
491
+ init_config();
492
+ init_flatten();
493
+ }
494
+ });
495
+
496
+ // dist/config/helpers.js
497
+ var init_helpers = __esm({
498
+ "dist/config/helpers.js"() {
499
+ "use strict";
500
+ init_validation();
501
+ }
502
+ });
503
+
504
+ // dist/config/index.js
505
+ var init_config2 = __esm({
506
+ "dist/config/index.js"() {
507
+ "use strict";
508
+ init_types();
509
+ init_config();
510
+ init_field();
511
+ init_collection();
512
+ init_permissions();
513
+ init_media();
514
+ init_flatten();
515
+ init_validation();
516
+ init_helpers();
517
+ }
518
+ });
519
+
520
+ // dist/config.js
521
+ var init_config3 = __esm({
522
+ "dist/config.js"() {
523
+ "use strict";
524
+ init_config2();
525
+ }
526
+ });
527
+
528
+ // dist/operating-mode/client-unsafe-strategy.js
529
+ import path3 from "node:path";
530
+ function operatingStrategy(mode) {
531
+ const cached = strategyCache.get(mode);
532
+ if (cached)
533
+ return cached;
534
+ let strategy;
535
+ switch (mode) {
536
+ case "prod":
537
+ strategy = new ProdStrategy();
538
+ break;
539
+ case "dev":
540
+ strategy = new DevStrategy();
541
+ break;
542
+ default: {
543
+ const _exhaustive = mode;
544
+ throw new Error(`Unknown operating mode: ${_exhaustive}`);
545
+ }
546
+ }
547
+ strategyCache.set(mode, strategy);
548
+ return strategy;
549
+ }
550
+ function clearStrategyCache() {
551
+ strategyCache.clear();
552
+ }
553
+ var ProdStrategy, DevStrategy, strategyCache;
554
+ var init_client_unsafe_strategy = __esm({
555
+ "dist/operating-mode/client-unsafe-strategy.js"() {
556
+ "use strict";
557
+ init_client_safe_strategy();
558
+ init_config3();
559
+ ProdStrategy = class extends ProdClientSafeStrategy {
560
+ // All client-safe methods inherited automatically from ProdClientSafeStrategy:
561
+ // - mode, supportsBranching(), supportsStatusBadge(), supportsComments()
562
+ // - supportsPullRequests(), getPermissionsFileName(), getGroupsFileName()
563
+ // - shouldCommit(), shouldPush()
564
+ // Add client-unsafe methods (use Node.js APIs)
565
+ getWorkspaceRoot(_sourceRoot) {
566
+ return path3.resolve(process.env.CANOPYCMS_WORKSPACE_ROOT ?? DEFAULT_PROD_WORKSPACE);
567
+ }
568
+ getContentRoot(sourceRoot) {
569
+ return path3.resolve(sourceRoot ?? process.cwd(), "content");
570
+ }
571
+ getContentBranchesRoot(sourceRoot) {
572
+ return path3.join(this.getWorkspaceRoot(sourceRoot), "content-branches");
573
+ }
574
+ getContentBranchRoot(branchName, sourceRoot) {
575
+ return path3.resolve(this.getContentBranchesRoot(sourceRoot), branchName);
576
+ }
577
+ getGitExcludePattern() {
578
+ return ".canopy-meta/";
579
+ }
580
+ getPermissionsFilePath(root) {
581
+ return path3.join(root, this.getPermissionsFileName());
582
+ }
583
+ getGroupsFilePath(root) {
584
+ return path3.join(root, this.getGroupsFileName());
585
+ }
586
+ getRemoteUrlConfig() {
587
+ return {
588
+ shouldAutoInitLocal: false,
589
+ defaultRemotePath: "",
590
+ envVarName: "CANOPYCMS_REMOTE_URL",
591
+ autoDetectRemotePath: path3.join(this.getWorkspaceRoot(), "remote.git")
592
+ };
593
+ }
594
+ requiresExistingRepo() {
595
+ return false;
596
+ }
597
+ getSettingsBranchName(config) {
598
+ if (config.settingsBranch)
599
+ return config.settingsBranch;
600
+ const deploymentName = config.deploymentName ?? "prod";
601
+ return `canopycms-settings-${deploymentName}`;
602
+ }
603
+ getSettingsRoot(sourceRoot) {
604
+ return path3.join(this.getWorkspaceRoot(sourceRoot), "settings");
605
+ }
606
+ usesSeparateSettingsBranch() {
607
+ return true;
608
+ }
609
+ validateConfig(config) {
610
+ if (!config.gitBotAuthorName || !config.gitBotAuthorEmail) {
611
+ throw new Error("gitBotAuthorName and gitBotAuthorEmail are required in prod mode");
612
+ }
613
+ }
614
+ shouldCreateSettingsPR(config) {
615
+ return config.autoCreateSettingsPR ?? true;
616
+ }
617
+ };
618
+ DevStrategy = class extends DevClientSafeStrategy {
619
+ // Inherits client-safe methods from DevClientSafeStrategy
620
+ getWorkspaceRoot(sourceRoot) {
621
+ return path3.resolve(sourceRoot ?? process.cwd(), ".canopy-dev");
622
+ }
623
+ getContentRoot(sourceRoot) {
624
+ return path3.resolve(sourceRoot ?? process.cwd(), "content");
625
+ }
626
+ getContentBranchesRoot(sourceRoot) {
627
+ return path3.join(this.getWorkspaceRoot(sourceRoot), "content-branches");
628
+ }
629
+ getContentBranchRoot(branchName, sourceRoot) {
630
+ return path3.resolve(this.getContentBranchesRoot(sourceRoot), branchName);
631
+ }
632
+ getGitExcludePattern() {
633
+ return ".canopy-meta/";
634
+ }
635
+ getPermissionsFilePath(root) {
636
+ return path3.join(root, this.getPermissionsFileName());
637
+ }
638
+ getGroupsFilePath(root) {
639
+ return path3.join(root, this.getGroupsFileName());
640
+ }
641
+ getRemoteUrlConfig() {
642
+ return {
643
+ shouldAutoInitLocal: true,
644
+ defaultRemotePath: ".canopy-dev/remote.git",
645
+ envVarName: "CANOPYCMS_REMOTE_URL"
646
+ };
647
+ }
648
+ requiresExistingRepo() {
649
+ return false;
650
+ }
651
+ getSettingsBranchName(config) {
652
+ if (config.settingsBranch)
653
+ return config.settingsBranch;
654
+ const deploymentName = config.deploymentName ?? "local";
655
+ return `canopycms-settings-${deploymentName}`;
656
+ }
657
+ getSettingsRoot(sourceRoot) {
658
+ return path3.join(this.getWorkspaceRoot(sourceRoot), "settings");
659
+ }
660
+ usesSeparateSettingsBranch() {
661
+ return true;
662
+ }
663
+ validateConfig(_config) {
664
+ }
665
+ shouldCreateSettingsPR(_config) {
666
+ return false;
667
+ }
668
+ };
669
+ strategyCache = /* @__PURE__ */ new Map();
670
+ }
671
+ });
672
+
673
+ // dist/operating-mode/index.js
674
+ var operating_mode_exports = {};
675
+ __export(operating_mode_exports, {
676
+ clearClientStrategyCache: () => clearClientStrategyCache,
677
+ clearStrategyCache: () => clearStrategyCache,
678
+ clientOperatingStrategy: () => clientOperatingStrategy,
679
+ operatingStrategy: () => operatingStrategy
680
+ });
681
+ var init_operating_mode = __esm({
682
+ "dist/operating-mode/index.js"() {
683
+ "use strict";
684
+ init_client_safe_strategy();
685
+ init_client_unsafe_strategy();
686
+ }
687
+ });
688
+
1
689
  // dist/cli/generate-ai-content.js
2
- import path11 from "node:path";
690
+ import path12 from "node:path";
3
691
  import { createJiti } from "jiti";
4
692
 
5
693
  // dist/build/generate-ai-content.js
6
- import fs8 from "node:fs/promises";
7
- import path10 from "node:path";
694
+ import fs10 from "node:fs/promises";
695
+ import path11 from "node:path";
8
696
 
9
697
  // dist/content-store.js
10
- import fs3 from "node:fs/promises";
698
+ import fs4 from "node:fs/promises";
11
699
  import path5 from "node:path";
12
700
  import matter from "gray-matter";
13
701
 
@@ -35,23 +723,8 @@ import path2 from "node:path";
35
723
  // dist/id.js
36
724
  import { generate } from "short-uuid";
37
725
 
38
- // dist/paths/normalize.js
39
- function normalizeFilesystemPath(path12) {
40
- return path12.split(/[\\/]+/).filter(Boolean).join("/");
41
- }
42
- function hasTraversalSequence(path12) {
43
- const normalized = normalizeFilesystemPath(path12);
44
- return normalized.includes("..");
45
- }
46
- function createLogicalPath(...segments) {
47
- const normalized = segments.map((s) => normalizeFilesystemPath(s)).filter(Boolean).join("/");
48
- if (hasTraversalSequence(normalized)) {
49
- throw new Error(`Invalid path: contains traversal sequence: ${normalized}`);
50
- }
51
- return normalized;
52
- }
53
-
54
726
  // dist/paths/validation.js
727
+ init_normalize();
55
728
  var BASE58_PATTERN = "[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]";
56
729
  var CONTENT_ID_PATTERN = new RegExp(`^${BASE58_PATTERN}{12}$`);
57
730
  var PHYSICAL_SEGMENT_PATTERN = new RegExp(`\\.${BASE58_PATTERN}{12}(?:\\.[a-z]+)?$`);
@@ -301,21 +974,21 @@ function extractIdFromFilename(filename) {
301
974
  return null;
302
975
  }
303
976
  async function resolveCollectionPath(root, logicalPath) {
304
- const fs9 = await import("node:fs/promises");
305
- const path12 = await import("node:path");
977
+ const fs11 = await import("node:fs/promises");
978
+ const path13 = await import("node:path");
306
979
  const segments = logicalPath.split("/").filter(Boolean);
307
980
  let currentPath = root;
308
981
  for (const segment of segments) {
309
982
  try {
310
- const entries = await fs9.readdir(currentPath, { withFileTypes: true });
983
+ const entries = await fs11.readdir(currentPath, { withFileTypes: true });
311
984
  const matchingDir = entries.find((entry) => {
312
985
  if (!entry.isDirectory())
313
986
  return false;
314
987
  const logicalName = extractSlugFromFilename(entry.name);
315
- return logicalName === segment;
988
+ return logicalName === segment.toLowerCase();
316
989
  });
317
990
  if (matchingDir) {
318
- currentPath = path12.join(currentPath, matchingDir.name);
991
+ currentPath = path13.join(currentPath, matchingDir.name);
319
992
  } else {
320
993
  return null;
321
994
  }
@@ -350,629 +1023,37 @@ function extractSlugFromFilename(filename, entryTypeName) {
350
1023
  } else if (parts.length >= 4 && slugParts.length > 1) {
351
1024
  slugParts = slugParts.slice(1);
352
1025
  }
353
- return slugParts.join(".");
1026
+ return slugParts.join(".").toLowerCase();
354
1027
  }
355
1028
  }
356
1029
  if (parts.length === 2) {
357
1030
  const possibleId = parts[parts.length - 1];
358
1031
  if (isValidId(possibleId)) {
359
- return parts[0];
1032
+ return parts[0].toLowerCase();
360
1033
  }
361
1034
  }
362
1035
  if (parts.length > 1) {
363
- return parts.slice(0, -1).join(".");
364
- }
365
- return filename;
366
- }
367
-
368
- // dist/utils/format.js
369
- var getFormatExtension = (format) => {
370
- if (format === "md")
371
- return ".md";
372
- if (format === "mdx")
373
- return ".mdx";
374
- return ".json";
375
- };
376
-
377
- // dist/paths/branch.js
378
- import path4 from "node:path";
379
-
380
- // dist/operating-mode/client-safe-strategy.js
381
- var ProdClientSafeStrategy = class {
382
- constructor() {
383
- this.mode = "prod";
384
- }
385
- // UI Feature Flags
386
- supportsBranching() {
387
- return true;
388
- }
389
- supportsStatusBadge() {
390
- return true;
391
- }
392
- supportsComments() {
393
- return true;
394
- }
395
- supportsPullRequests() {
396
- return true;
397
- }
398
- // Simple Data
399
- getPermissionsFileName() {
400
- return "permissions.json";
401
- }
402
- getGroupsFileName() {
403
- return "groups.json";
404
- }
405
- shouldCommit() {
406
- return true;
407
- }
408
- shouldPush() {
409
- return true;
410
- }
411
- };
412
- var LocalProdSimClientSafeStrategy = class {
413
- constructor() {
414
- this.mode = "prod-sim";
415
- }
416
- // UI Feature Flags
417
- supportsBranching() {
418
- return true;
419
- }
420
- supportsStatusBadge() {
421
- return true;
422
- }
423
- supportsComments() {
424
- return true;
425
- }
426
- supportsPullRequests() {
427
- return false;
428
- }
429
- // Simple Data
430
- getPermissionsFileName() {
431
- return "permissions.json";
432
- }
433
- getGroupsFileName() {
434
- return "groups.json";
435
- }
436
- shouldCommit() {
437
- return true;
438
- }
439
- shouldPush() {
440
- return true;
441
- }
442
- };
443
- var LocalSimpleClientSafeStrategy = class {
444
- constructor() {
445
- this.mode = "dev";
446
- }
447
- // UI Feature Flags
448
- supportsBranching() {
449
- return false;
450
- }
451
- supportsStatusBadge() {
452
- return false;
453
- }
454
- supportsComments() {
455
- return false;
456
- }
457
- supportsPullRequests() {
458
- return false;
459
- }
460
- // Simple Data
461
- getPermissionsFileName() {
462
- return "permissions.json";
463
- }
464
- getGroupsFileName() {
465
- return "groups.json";
466
- }
467
- shouldCommit() {
468
- return false;
469
- }
470
- shouldPush() {
471
- return false;
472
- }
473
- };
474
-
475
- // dist/operating-mode/client-unsafe-strategy.js
476
- import path3 from "node:path";
477
-
478
- // dist/config/types.js
479
- var primitiveFieldTypes = [
480
- "string",
481
- "number",
482
- "boolean",
483
- "datetime",
484
- "rich-text",
485
- "markdown",
486
- "mdx",
487
- "image",
488
- "code"
489
- ];
490
- var fieldTypes = [
491
- ...primitiveFieldTypes,
492
- "select",
493
- "reference",
494
- "object",
495
- "block"
496
- ];
497
-
498
- // dist/config/schemas/config.js
499
- import { z as z4 } from "zod";
500
-
501
- // dist/config/schemas/collection.js
502
- import { z as z2 } from "zod";
503
- import { isAbsolute } from "pathe";
504
-
505
- // dist/config/schemas/field.js
506
- import { z } from "zod";
507
- var fieldBaseSchema = z.object({
508
- name: z.string().min(1),
509
- label: z.string().optional(),
510
- description: z.string().optional(),
511
- required: z.boolean().optional(),
512
- list: z.boolean().optional()
513
- });
514
- var selectOptionSchema = z.union([
515
- z.string(),
516
- z.object({
517
- label: z.string().min(1),
518
- value: z.string().min(1)
519
- })
520
- ]);
521
- var referenceOptionSchema = z.union([
522
- z.string(),
523
- z.object({
524
- label: z.string().min(1),
525
- value: z.string().min(1)
526
- })
527
- ]);
528
- var primitiveFieldSchema = fieldBaseSchema.extend({
529
- type: z.enum(primitiveFieldTypes)
530
- });
531
- var selectFieldSchema = fieldBaseSchema.extend({
532
- type: z.literal("select"),
533
- options: z.array(selectOptionSchema).min(1)
534
- });
535
- var referenceFieldSchema = fieldBaseSchema.extend({
536
- type: z.literal("reference"),
537
- collections: z.array(z.string().min(1)).min(1),
538
- displayField: z.string().min(1).optional(),
539
- options: z.array(referenceOptionSchema).optional()
540
- });
541
- var fieldHolder = [z.never()];
542
- var blockSchema = z.object({
543
- name: z.string().min(1),
544
- label: z.string().optional(),
545
- description: z.string().optional(),
546
- fields: z.array(z.lazy(() => fieldHolder[0])).min(1)
547
- });
548
- var blockFieldSchema = fieldBaseSchema.extend({
549
- type: z.literal("block"),
550
- templates: z.array(blockSchema).min(1)
551
- });
552
- var objectFieldSchema = fieldBaseSchema.extend({
553
- type: z.literal("object"),
554
- fields: z.array(z.lazy(() => fieldHolder[0])).min(1)
555
- });
556
- var customFieldSchema = z.lazy(() => fieldBaseSchema.extend({
557
- type: z.string().min(1).refine((val) => !fieldTypes.includes(val), {
558
- message: "Custom field types must not conflict with built-in types"
559
- })
560
- }).passthrough());
561
- var knownFieldSchema = z.discriminatedUnion("type", [
562
- primitiveFieldSchema,
563
- selectFieldSchema,
564
- referenceFieldSchema,
565
- objectFieldSchema,
566
- blockFieldSchema
567
- ]);
568
- var fieldSchema = z.lazy(() => z.union([knownFieldSchema, customFieldSchema]));
569
- fieldHolder[0] = fieldSchema;
570
-
571
- // dist/config/schemas/collection.js
572
- var relativePathSchema = z2.string().min(1).refine((val) => !isAbsolute(val), { message: "Path must be relative" }).refine((val) => !val.split(/[\\/]+/).includes(".."), {
573
- message: 'Path must not contain ".."'
574
- }).transform((val) => val.split(/[\\/]+/).filter(Boolean).join("/"));
575
- var entryTypeSchema = z2.object({
576
- name: z2.string().min(1),
577
- format: z2.enum(["md", "mdx", "json"]),
578
- schema: z2.array(z2.lazy(() => fieldSchema)).min(1),
579
- label: z2.string().optional(),
580
- description: z2.string().optional(),
581
- default: z2.boolean().optional(),
582
- maxItems: z2.number().int().positive().optional()
583
- });
584
- var collectionSchema = z2.lazy(() => z2.object({
585
- name: z2.string().min(1),
586
- path: relativePathSchema,
587
- label: z2.string().optional(),
588
- description: z2.string().optional(),
589
- entries: z2.array(entryTypeSchema).optional(),
590
- collections: z2.array(collectionSchema).optional(),
591
- order: z2.array(z2.string()).optional()
592
- // Embedded IDs for ordering items
593
- }).refine((data) => data.entries || data.collections, {
594
- message: "Collection must have entries or collections"
595
- }));
596
- var rootCollectionSchema = z2.object({
597
- entries: z2.array(entryTypeSchema).optional(),
598
- collections: z2.array(collectionSchema).optional(),
599
- order: z2.array(z2.string()).optional()
600
- // Embedded IDs for ordering items
601
- });
602
-
603
- // dist/config/schemas/media.js
604
- import { z as z3 } from "zod";
605
- var mediaSchema = z3.union([
606
- z3.object({
607
- adapter: z3.literal("local"),
608
- publicBaseUrl: z3.string().url().optional()
609
- }),
610
- z3.object({
611
- adapter: z3.literal("s3"),
612
- bucket: z3.string().min(1),
613
- region: z3.string().min(1),
614
- publicBaseUrl: z3.string().url().optional()
615
- }),
616
- z3.object({
617
- adapter: z3.literal("lfs"),
618
- publicBaseUrl: z3.string().url().optional()
619
- }),
620
- z3.object({
621
- adapter: z3.string().min(1),
622
- publicBaseUrl: z3.string().url().optional()
623
- })
624
- ]);
625
-
626
- // dist/config/schemas/config.js
627
- var defaultBranchAccessSchema = z4.enum(["allow", "deny"]).default("deny");
628
- var defaultPathAccessSchema = z4.enum(["allow", "deny"]).default("deny");
629
- var defaultBaseBranchSchema = z4.string().default("main");
630
- var defaultRemoteNameSchema = z4.string().default("origin");
631
- var defaultRemoteUrlSchema = z4.string().min(1);
632
- var gitBotAuthorNameSchema = z4.string().min(1);
633
- var gitBotAuthorEmailSchema = z4.string().email();
634
- var githubTokenEnvVarSchema = z4.string().default("GITHUB_BOT_TOKEN");
635
- var operatingModeSchema = z4.enum(["prod", "prod-sim", "dev"]).default("dev");
636
- var deployedAsSchema = z4.enum(["static", "server"]).default("server");
637
- var contentRootSchema = relativePathSchema.default("content");
638
- var sourceRootSchema = z4.string().min(1).optional();
639
- var deploymentNameSchema = z4.string().default("prod");
640
- var editorConfigSchema = z4.object({
641
- title: z4.string().optional(),
642
- subtitle: z4.string().optional(),
643
- theme: z4.unknown().optional(),
644
- previewBase: z4.record(z4.string()).optional(),
645
- // UI handler functions (runtime only, don't serialize)
646
- onAccountClick: z4.function().returns(z4.void()).optional(),
647
- onLogoutClick: z4.function().returns(z4.void()).optional(),
648
- // Optional: custom account component (e.g., Clerk's UserButton)
649
- AccountComponent: z4.custom().optional()
650
- });
651
- var CanopyConfigSchema = z4.object({
652
- schema: rootCollectionSchema.optional(),
653
- media: mediaSchema.optional(),
654
- defaultBranchAccess: defaultBranchAccessSchema.optional(),
655
- defaultPathAccess: defaultPathAccessSchema.optional(),
656
- defaultBaseBranch: defaultBaseBranchSchema.optional(),
657
- defaultRemoteName: defaultRemoteNameSchema.optional(),
658
- defaultRemoteUrl: defaultRemoteUrlSchema.optional(),
659
- gitBotAuthorName: gitBotAuthorNameSchema,
660
- gitBotAuthorEmail: gitBotAuthorEmailSchema,
661
- githubTokenEnvVar: githubTokenEnvVarSchema.optional(),
662
- mode: operatingModeSchema,
663
- // Has .default(), so not optional in output type
664
- deployedAs: deployedAsSchema,
665
- // Has .default('server'), so always present after validation
666
- settingsBranch: z4.string().optional(),
667
- autoCreateSettingsPR: z4.boolean().optional(),
668
- deploymentName: deploymentNameSchema.optional(),
669
- contentRoot: contentRootSchema.default("content"),
670
- sourceRoot: sourceRootSchema.optional(),
671
- editor: editorConfigSchema.optional(),
672
- authPlugin: z4.custom().optional()
673
- });
674
- var DEFAULT_PROD_WORKSPACE = "/mnt/efs/workspace";
675
-
676
- // dist/config/schemas/permissions.js
677
- import { z as z5 } from "zod";
678
- var permissionTargetSchema = z5.object({
679
- allowedUsers: z5.array(z5.string()).optional(),
680
- allowedGroups: z5.array(z5.string()).optional()
681
- });
682
- var pathPermissionSchema = z5.object({
683
- path: z5.string().min(1),
684
- read: permissionTargetSchema.optional(),
685
- edit: permissionTargetSchema.optional(),
686
- review: permissionTargetSchema.optional()
687
- });
688
-
689
- // dist/config/flatten.js
690
- import { join, normalize } from "pathe";
691
-
692
- // dist/paths/types.js
693
- var ROOT_COLLECTION_ID = "__rootcoll__";
694
-
695
- // dist/config/flatten.js
696
- var normalizePathValue = (val) => normalize(val).split("/").filter(Boolean).join("/");
697
- var flattenSchema = (root, basePath = "") => {
698
- const flat = [];
699
- const base = normalizePathValue(basePath || "");
700
- const walkCollection = (collection, parentPath) => {
701
- const normalizedPath = normalizePathValue(collection.path);
702
- let logicalPath;
703
- if (parentPath && parentPath !== base) {
704
- logicalPath = join(parentPath, collection.name);
705
- } else if (parentPath === base) {
706
- logicalPath = join(base, normalizedPath);
707
- } else {
708
- logicalPath = normalizedPath;
709
- }
710
- const normalizedFull = normalizePathValue(logicalPath);
711
- flat.push({
712
- type: "collection",
713
- logicalPath: createLogicalPath(normalizedFull),
714
- name: collection.name,
715
- label: collection.label,
716
- description: collection.description,
717
- contentId: collection.contentId,
718
- parentPath: parentPath ? createLogicalPath(parentPath) : void 0,
719
- entries: collection.entries,
720
- collections: collection.collections,
721
- order: collection.order
722
- });
723
- if (collection.entries) {
724
- for (const entryType of collection.entries) {
725
- const entryTypePath = join(normalizedFull, entryType.name);
726
- flat.push({
727
- type: "entry-type",
728
- logicalPath: createLogicalPath(normalizePathValue(entryTypePath)),
729
- name: entryType.name,
730
- label: entryType.label,
731
- description: entryType.description,
732
- parentPath: createLogicalPath(normalizedFull),
733
- format: entryType.format,
734
- schema: entryType.schema,
735
- schemaRef: entryType.schemaRef,
736
- default: entryType.default,
737
- maxItems: entryType.maxItems
738
- });
739
- }
740
- }
741
- if (collection.collections) {
742
- for (const child of collection.collections) {
743
- walkCollection(child, normalizedFull);
744
- }
745
- }
746
- };
747
- if (base) {
748
- flat.push({
749
- type: "collection",
750
- logicalPath: createLogicalPath(base),
751
- name: base,
752
- // Use base path as the name (e.g., 'content')
753
- label: void 0,
754
- // Root collection has no label
755
- contentId: ROOT_COLLECTION_ID,
756
- // Sentinel — root dir has no embedded ID
757
- parentPath: void 0,
758
- // No parent - this is the root
759
- entries: root.entries,
760
- collections: root.collections,
761
- order: root.order
762
- });
763
- }
764
- if (root.entries) {
765
- for (const entryType of root.entries) {
766
- const entryTypePath = base ? join(base, entryType.name) : entryType.name;
767
- flat.push({
768
- type: "entry-type",
769
- logicalPath: createLogicalPath(normalizePathValue(entryTypePath)),
770
- name: entryType.name,
771
- label: entryType.label,
772
- description: entryType.description,
773
- parentPath: base ? createLogicalPath(base) : createLogicalPath(""),
774
- // Now references the root collection (e.g., 'content')
775
- format: entryType.format,
776
- schema: entryType.schema,
777
- schemaRef: entryType.schemaRef,
778
- default: entryType.default,
779
- maxItems: entryType.maxItems
780
- });
781
- }
782
- }
783
- if (root.collections) {
784
- for (const collection of root.collections) {
785
- walkCollection(collection, base || "");
786
- }
787
- }
788
- return flat;
789
- };
790
-
791
- // dist/operating-mode/client-unsafe-strategy.js
792
- var ProdStrategy = class extends ProdClientSafeStrategy {
793
- // All client-safe methods inherited automatically from ProdClientSafeStrategy:
794
- // - mode, supportsBranching(), supportsStatusBadge(), supportsComments()
795
- // - supportsPullRequests(), getPermissionsFileName(), getGroupsFileName()
796
- // - shouldCommit(), shouldPush()
797
- // Add client-unsafe methods (use Node.js APIs)
798
- getWorkspaceRoot(_sourceRoot) {
799
- return path3.resolve(process.env.CANOPYCMS_WORKSPACE_ROOT ?? DEFAULT_PROD_WORKSPACE);
800
- }
801
- getContentRoot(sourceRoot) {
802
- return path3.resolve(sourceRoot ?? process.cwd(), "content");
803
- }
804
- getContentBranchesRoot(sourceRoot) {
805
- return path3.join(this.getWorkspaceRoot(sourceRoot), "content-branches");
806
- }
807
- getContentBranchRoot(branchName, sourceRoot) {
808
- return path3.resolve(this.getContentBranchesRoot(sourceRoot), branchName);
809
- }
810
- getGitExcludePattern() {
811
- return ".canopy-meta/";
812
- }
813
- getPermissionsFilePath(root) {
814
- return path3.join(root, this.getPermissionsFileName());
815
- }
816
- getGroupsFilePath(root) {
817
- return path3.join(root, this.getGroupsFileName());
818
- }
819
- getRemoteUrlConfig() {
820
- return {
821
- shouldAutoInitLocal: false,
822
- defaultRemotePath: "",
823
- envVarName: "CANOPYCMS_REMOTE_URL",
824
- autoDetectRemotePath: path3.join(this.getWorkspaceRoot(), "remote.git")
825
- };
826
- }
827
- requiresExistingRepo() {
828
- return false;
829
- }
830
- getSettingsBranchName(config) {
831
- if (config.settingsBranch)
832
- return config.settingsBranch;
833
- const deploymentName = config.deploymentName ?? "prod";
834
- return `canopycms-settings-${deploymentName}`;
835
- }
836
- getSettingsRoot(sourceRoot) {
837
- return path3.join(this.getWorkspaceRoot(sourceRoot), "settings");
838
- }
839
- usesSeparateSettingsBranch() {
840
- return true;
841
- }
842
- validateConfig(config) {
843
- if (!config.gitBotAuthorName || !config.gitBotAuthorEmail) {
844
- throw new Error("gitBotAuthorName and gitBotAuthorEmail are required in prod mode");
845
- }
846
- }
847
- shouldCreateSettingsPR(config) {
848
- return config.autoCreateSettingsPR ?? true;
849
- }
850
- };
851
- var LocalProdSimStrategy = class extends LocalProdSimClientSafeStrategy {
852
- // Inherits client-safe methods from LocalProdSimClientSafeStrategy
853
- getWorkspaceRoot(sourceRoot) {
854
- return path3.resolve(sourceRoot ?? process.cwd(), ".canopy-prod-sim");
855
- }
856
- getContentRoot(sourceRoot) {
857
- return path3.resolve(sourceRoot ?? process.cwd(), "content");
858
- }
859
- getContentBranchesRoot(sourceRoot) {
860
- return path3.join(this.getWorkspaceRoot(sourceRoot), "content-branches");
861
- }
862
- getContentBranchRoot(branchName, sourceRoot) {
863
- return path3.resolve(this.getContentBranchesRoot(sourceRoot), branchName);
864
- }
865
- getGitExcludePattern() {
866
- return ".canopy-meta/";
867
- }
868
- getPermissionsFilePath(root) {
869
- return path3.join(root, this.getPermissionsFileName());
870
- }
871
- getGroupsFilePath(root) {
872
- return path3.join(root, this.getGroupsFileName());
873
- }
874
- getRemoteUrlConfig() {
875
- return {
876
- shouldAutoInitLocal: true,
877
- defaultRemotePath: ".canopy-prod-sim/remote.git",
878
- envVarName: "CANOPYCMS_REMOTE_URL"
879
- };
880
- }
881
- requiresExistingRepo() {
882
- return false;
883
- }
884
- getSettingsBranchName(config) {
885
- if (config.settingsBranch)
886
- return config.settingsBranch;
887
- const deploymentName = config.deploymentName ?? "prod";
888
- return `canopycms-settings-${deploymentName}`;
889
- }
890
- getSettingsRoot(sourceRoot) {
891
- return path3.join(this.getWorkspaceRoot(sourceRoot), "settings");
892
- }
893
- usesSeparateSettingsBranch() {
894
- return true;
895
- }
896
- validateConfig(_config) {
897
- }
898
- shouldCreateSettingsPR(_config) {
899
- return false;
900
- }
901
- };
902
- var LocalSimpleStrategy = class extends LocalSimpleClientSafeStrategy {
903
- // Inherits: supportsBranching() returns false, getPermissionsFileName() returns 'permissions.local.json'
904
- getWorkspaceRoot(sourceRoot) {
905
- return path3.resolve(sourceRoot ?? process.cwd(), ".canopy-dev");
906
- }
907
- getContentRoot(sourceRoot) {
908
- return path3.resolve(sourceRoot ?? process.cwd(), "content");
909
- }
910
- getContentBranchesRoot(_sourceRoot) {
911
- throw new Error("No branching in dev mode");
912
- }
913
- getContentBranchRoot(_branchName, _sourceRoot) {
914
- throw new Error("No branching in dev mode");
915
- }
916
- getGitExcludePattern() {
917
- return ".canopy-meta/";
918
- }
919
- getPermissionsFilePath(root) {
920
- return path3.join(this.getWorkspaceRoot(root), "settings", "permissions.json");
921
- }
922
- getGroupsFilePath(root) {
923
- return path3.join(this.getWorkspaceRoot(root), "settings", "groups.json");
924
- }
925
- getRemoteUrlConfig() {
926
- return {
927
- shouldAutoInitLocal: false,
928
- defaultRemotePath: "",
929
- envVarName: "CANOPYCMS_REMOTE_URL"
930
- };
931
- }
932
- requiresExistingRepo() {
933
- return true;
934
- }
935
- getSettingsBranchName(config) {
936
- return config.defaultBaseBranch ?? "main";
937
- }
938
- getSettingsRoot(sourceRoot) {
939
- return path3.join(this.getWorkspaceRoot(sourceRoot), "settings");
940
- }
941
- usesSeparateSettingsBranch() {
942
- return false;
943
- }
944
- validateConfig(_config) {
945
- }
946
- shouldCreateSettingsPR(_config) {
947
- return false;
948
- }
949
- };
950
- var strategyCache = /* @__PURE__ */ new Map();
951
- function operatingStrategy(mode) {
952
- const cached = strategyCache.get(mode);
953
- if (cached)
954
- return cached;
955
- let strategy;
956
- switch (mode) {
957
- case "prod":
958
- strategy = new ProdStrategy();
959
- break;
960
- case "prod-sim":
961
- strategy = new LocalProdSimStrategy();
962
- break;
963
- case "dev":
964
- strategy = new LocalSimpleStrategy();
965
- break;
966
- default: {
967
- const _exhaustive = mode;
968
- throw new Error(`Unknown operating mode: ${_exhaustive}`);
969
- }
1036
+ return parts.slice(0, -1).join(".").toLowerCase();
970
1037
  }
971
- strategyCache.set(mode, strategy);
972
- return strategy;
1038
+ return filename.toLowerCase();
973
1039
  }
974
1040
 
1041
+ // dist/utils/format.js
1042
+ var getFormatExtension = (format) => {
1043
+ if (format === "md")
1044
+ return ".md";
1045
+ if (format === "mdx")
1046
+ return ".mdx";
1047
+ return ".json";
1048
+ };
1049
+
1050
+ // dist/paths/index.js
1051
+ init_normalize();
1052
+
975
1053
  // dist/paths/branch.js
1054
+ init_operating_mode();
1055
+ import fs3 from "node:fs/promises";
1056
+ import path4 from "node:path";
976
1057
  var BranchPathError = class extends Error {
977
1058
  };
978
1059
  function sanitizeBranchName(branchName) {
@@ -1003,6 +1084,11 @@ function resolveBranchPath(options) {
1003
1084
  }
1004
1085
  return { branchRoot, baseRoot: normalizedBase, branchName: safeBranch };
1005
1086
  }
1087
+ async function ensureBranchRoot(options) {
1088
+ const result = resolveBranchPath(options);
1089
+ await fs3.mkdir(result.branchRoot, { recursive: true });
1090
+ return result;
1091
+ }
1006
1092
 
1007
1093
  // dist/content-store.js
1008
1094
  var ContentStoreError = class extends Error {
@@ -1045,11 +1131,11 @@ var ContentStore = class {
1045
1131
  getSchemaItems() {
1046
1132
  return this.schemaIndex.values();
1047
1133
  }
1048
- assertSchemaItem(path12) {
1049
- const normalized = normalizeFilesystemPath(path12);
1134
+ assertSchemaItem(path13) {
1135
+ const normalized = normalizeFilesystemPath(path13);
1050
1136
  const item = this.schemaIndex.get(normalized);
1051
1137
  if (!item) {
1052
- throw new ContentStoreError(`Unknown schema item: ${path12}`);
1138
+ throw new ContentStoreError(`Unknown schema item: ${path13}`);
1053
1139
  }
1054
1140
  return item;
1055
1141
  }
@@ -1090,7 +1176,7 @@ var ContentStore = class {
1090
1176
  });
1091
1177
  }
1092
1178
  if (schemaItem.type === "collection") {
1093
- const safeSlug = slug.replace(/^\/+/, "");
1179
+ const safeSlug = slug.replace(/^\/+/, "").toLowerCase();
1094
1180
  if (!safeSlug) {
1095
1181
  throw new ContentStoreError("Slug is required for collection entries");
1096
1182
  }
@@ -1118,7 +1204,7 @@ var ContentStore = class {
1118
1204
  let existingFilename;
1119
1205
  let existingEntryType;
1120
1206
  if (!id) {
1121
- const entries = await fs3.readdir(collectionRoot, { withFileTypes: true }).catch(() => []);
1207
+ const entries = await fs4.readdir(collectionRoot, { withFileTypes: true }).catch(() => []);
1122
1208
  const existingFile = entries.find((entry) => {
1123
1209
  if (entry.isDirectory())
1124
1210
  return false;
@@ -1134,7 +1220,7 @@ var ContentStore = class {
1134
1220
  }
1135
1221
  const finalEntryTypeName = existingEntryType || entryTypeName;
1136
1222
  let filename;
1137
- if (existingFilename && !id) {
1223
+ if (existingFilename) {
1138
1224
  filename = existingFilename;
1139
1225
  } else {
1140
1226
  if (!id) {
@@ -1164,7 +1250,7 @@ var ContentStore = class {
1164
1250
  throw new ContentStoreError("Empty path");
1165
1251
  }
1166
1252
  const logicalPath = pathSegments.join("/");
1167
- const slug = pathSegments[pathSegments.length - 1];
1253
+ const slug = pathSegments[pathSegments.length - 1].toLowerCase();
1168
1254
  const collectionPath = pathSegments.slice(0, -1).join("/");
1169
1255
  const normalizedCollection = normalizeFilesystemPath(collectionPath);
1170
1256
  const collection = this.schemaIndex.get(normalizedCollection);
@@ -1183,7 +1269,7 @@ var ContentStore = class {
1183
1269
  async read(collectionPath, slug = "", options = {}) {
1184
1270
  const schemaItem = this.assertSchemaItem(collectionPath);
1185
1271
  const { absolutePath, relativePath } = await this.buildPaths(schemaItem, slug);
1186
- const raw = await fs3.readFile(absolutePath, "utf8");
1272
+ const raw = await fs4.readFile(absolutePath, "utf8");
1187
1273
  let doc;
1188
1274
  let format;
1189
1275
  let fields;
@@ -1246,7 +1332,7 @@ var ContentStore = class {
1246
1332
  const { absolutePath, relativePath, id } = await this.buildPaths(schemaItem, slug, {
1247
1333
  entryTypeName
1248
1334
  });
1249
- await fs3.mkdir(path5.dirname(absolutePath), { recursive: true });
1335
+ await fs4.mkdir(path5.dirname(absolutePath), { recursive: true });
1250
1336
  if (input.format === "json") {
1251
1337
  const json = JSON.stringify(input.data ?? {}, null, 2);
1252
1338
  await atomicWriteFile(absolutePath, `${json}
@@ -1330,7 +1416,7 @@ var ContentStore = class {
1330
1416
  const collection = this.assertCollection(collectionPath);
1331
1417
  const { absolutePath, relativePath } = await this.buildPaths(collection, slug);
1332
1418
  const id = idIndex.findByPath(relativePath);
1333
- await fs3.unlink(absolutePath);
1419
+ await fs4.unlink(absolutePath);
1334
1420
  if (id) {
1335
1421
  idIndex.remove(id);
1336
1422
  }
@@ -1353,12 +1439,9 @@ var ContentStore = class {
1353
1439
  if (!safeNewSlug) {
1354
1440
  throw new ContentStoreError("New slug cannot be empty");
1355
1441
  }
1356
- if (!/^[a-z0-9][a-z0-9-]*$/.test(safeNewSlug)) {
1357
- throw new ContentStoreError("Slug must start with a letter or number and contain only lowercase letters, numbers, and hyphens");
1358
- }
1359
1442
  const { absolutePath: currentPath, relativePath: currentRelPath } = await this.buildPaths(collection, currentSlug);
1360
1443
  try {
1361
- await fs3.access(currentPath);
1444
+ await fs4.access(currentPath);
1362
1445
  } catch {
1363
1446
  throw new ContentStoreError(`Entry not found: ${currentSlug}`);
1364
1447
  }
@@ -1377,7 +1460,7 @@ var ContentStore = class {
1377
1460
  const parentDir = path5.dirname(currentPath);
1378
1461
  const newPath = path5.join(parentDir, newFilename);
1379
1462
  try {
1380
- const entries = await fs3.readdir(parentDir, { withFileTypes: true });
1463
+ const entries = await fs4.readdir(parentDir, { withFileTypes: true });
1381
1464
  for (const entry of entries) {
1382
1465
  if (entry.isDirectory())
1383
1466
  continue;
@@ -1391,7 +1474,7 @@ var ContentStore = class {
1391
1474
  throw err;
1392
1475
  }
1393
1476
  }
1394
- await fs3.rename(currentPath, newPath);
1477
+ await fs4.rename(currentPath, newPath);
1395
1478
  const newRelativePath = path5.relative(this.root, newPath);
1396
1479
  const entryId = idIndex.findByPath(currentRelPath);
1397
1480
  if (entryId) {
@@ -1509,11 +1592,11 @@ var ContentStore = class {
1509
1592
  };
1510
1593
 
1511
1594
  // dist/branch-schema-cache.js
1512
- import fs5 from "node:fs/promises";
1595
+ import fs6 from "node:fs/promises";
1513
1596
  import path6 from "node:path";
1514
1597
 
1515
1598
  // dist/schema/meta-loader.js
1516
- import { promises as fs4 } from "fs";
1599
+ import { promises as fs5 } from "fs";
1517
1600
  import { join as join2 } from "pathe";
1518
1601
  import { z as z6 } from "zod";
1519
1602
  import chokidar from "chokidar";
@@ -1547,7 +1630,7 @@ function stripEmbeddedIdFromName(name) {
1547
1630
  async function scanForCollectionMeta(baseDir, relativePath = "") {
1548
1631
  const collections = [];
1549
1632
  try {
1550
- const entries = await fs4.readdir(baseDir, { withFileTypes: true });
1633
+ const entries = await fs5.readdir(baseDir, { withFileTypes: true });
1551
1634
  for (const entry of entries) {
1552
1635
  if (!entry.isDirectory())
1553
1636
  continue;
@@ -1558,8 +1641,8 @@ async function scanForCollectionMeta(baseDir, relativePath = "") {
1558
1641
  const absolutePath = join2(baseDir, folderName);
1559
1642
  const metaPath = join2(absolutePath, ".collection.json");
1560
1643
  try {
1561
- await fs4.access(metaPath);
1562
- const content = await fs4.readFile(metaPath, "utf-8");
1644
+ await fs5.access(metaPath);
1645
+ const content = await fs5.readFile(metaPath, "utf-8");
1563
1646
  const parsed = JSON.parse(content);
1564
1647
  const meta = collectionMetaSchema.parse(parsed);
1565
1648
  collections.push({
@@ -1591,7 +1674,7 @@ async function loadCollectionMetaFiles(contentRoot) {
1591
1674
  let root = null;
1592
1675
  const rootMetaPath = join2(contentRoot, ".collection.json");
1593
1676
  try {
1594
- await fs4.access(rootMetaPath);
1677
+ await fs5.access(rootMetaPath);
1595
1678
  } catch (err) {
1596
1679
  if (err.code === "ENOENT") {
1597
1680
  } else {
@@ -1599,7 +1682,7 @@ async function loadCollectionMetaFiles(contentRoot) {
1599
1682
  }
1600
1683
  }
1601
1684
  try {
1602
- const content = await fs4.readFile(rootMetaPath, "utf-8");
1685
+ const content = await fs5.readFile(rootMetaPath, "utf-8");
1603
1686
  const parsed = JSON.parse(content);
1604
1687
  root = rootCollectionMetaSchema.parse(parsed);
1605
1688
  } catch (err) {
@@ -1690,35 +1773,44 @@ function isValidSchema(schema) {
1690
1773
  }
1691
1774
 
1692
1775
  // dist/branch-schema-cache.js
1776
+ init_flatten();
1693
1777
  var SCHEMA_CACHE_VERSION = 2;
1778
+ var MTIME_CHECK_DEBOUNCE_MS = 1e3;
1779
+ async function isStaleByMtime(dir, cachedAt) {
1780
+ let entries;
1781
+ try {
1782
+ entries = await fs6.readdir(dir, { recursive: true, encoding: "utf-8" });
1783
+ } catch {
1784
+ return true;
1785
+ }
1786
+ for (const entry of entries) {
1787
+ if (!entry.endsWith(".collection.json"))
1788
+ continue;
1789
+ const full = path6.join(dir, entry);
1790
+ try {
1791
+ const stat = await fs6.stat(full);
1792
+ if (stat.mtimeMs > cachedAt.getTime())
1793
+ return true;
1794
+ } catch {
1795
+ return true;
1796
+ }
1797
+ }
1798
+ return false;
1799
+ }
1694
1800
  var BranchSchemaCache = class {
1695
- constructor(mode) {
1696
- this.mode = mode;
1801
+ constructor(mode = "prod") {
1802
+ this.lastMtimeCheck = /* @__PURE__ */ new Map();
1803
+ this.devMode = mode === "dev";
1697
1804
  }
1698
1805
  /**
1699
1806
  * Get schema for a branch (loads from cache or resolves fresh).
1700
1807
  *
1701
- * @param branchRoot - Root directory of the branch (e.g., .canopy-prod-sim/content-branches/main)
1808
+ * @param branchRoot - Root directory of the branch (e.g., .canopy-dev/content-branches/main)
1702
1809
  * @param entrySchemaRegistry - Map of schema names to field definitions
1703
1810
  * @param contentRootName - Name of content directory (e.g., "content") from config
1704
1811
  * @returns Resolved schema tree and flattened schema
1705
1812
  */
1706
1813
  async getSchema(branchRoot, entrySchemaRegistry, contentRootName = "content") {
1707
- if (this.mode === "dev") {
1708
- if (!this.devModeCache) {
1709
- const contentRoot = path6.join(branchRoot, contentRootName);
1710
- const result = await resolveSchema(contentRoot, entrySchemaRegistry);
1711
- if (!isValidSchema(result.schema)) {
1712
- throw new Error(`No schema found in ${contentRoot}. Create .collection.json files with references to field schemas defined in your entry schema registry.`);
1713
- }
1714
- const flatSchema = flattenSchema(result.schema, contentRootName);
1715
- this.devModeCache = {
1716
- schema: result.schema,
1717
- flatSchema
1718
- };
1719
- }
1720
- return this.devModeCache;
1721
- }
1722
1814
  return this.loadFromCacheOrResolve(branchRoot, entrySchemaRegistry, contentRootName);
1723
1815
  }
1724
1816
  /**
@@ -1731,23 +1823,32 @@ var BranchSchemaCache = class {
1731
1823
  const stalePath = path6.join(cacheDir, "schema-cache.stale");
1732
1824
  let cacheData = null;
1733
1825
  try {
1734
- const staleExists = await fs5.access(stalePath).then(() => true).catch(() => false);
1826
+ const staleExists = await fs6.access(stalePath).then(() => true).catch(() => false);
1735
1827
  if (!staleExists) {
1736
- const cacheContent = await fs5.readFile(cachePath, "utf-8");
1828
+ const cacheContent = await fs6.readFile(cachePath, "utf-8");
1737
1829
  cacheData = JSON.parse(cacheContent);
1738
1830
  }
1739
1831
  } catch {
1740
1832
  cacheData = null;
1741
1833
  }
1742
1834
  if (cacheData && cacheData.version === SCHEMA_CACHE_VERSION) {
1743
- return { schema: cacheData.schema, flatSchema: cacheData.flatSchema };
1835
+ const now = Date.now();
1836
+ const lastCheck = this.lastMtimeCheck.get(contentRoot) ?? 0;
1837
+ if (this.devMode && now - lastCheck >= MTIME_CHECK_DEBOUNCE_MS && await isStaleByMtime(contentRoot, new Date(cacheData.cachedAt))) {
1838
+ this.lastMtimeCheck.set(contentRoot, now);
1839
+ cacheData = null;
1840
+ } else {
1841
+ if (this.devMode)
1842
+ this.lastMtimeCheck.set(contentRoot, now);
1843
+ return { schema: cacheData.schema, flatSchema: cacheData.flatSchema };
1844
+ }
1744
1845
  }
1745
1846
  const result = await resolveSchema(contentRoot, entrySchemaRegistry);
1746
1847
  if (!isValidSchema(result.schema)) {
1747
1848
  throw new Error(`No schema found in ${contentRoot}. Create .collection.json files with references to field schemas defined in your entry schema registry.`);
1748
1849
  }
1749
1850
  const flatSchema = flattenSchema(result.schema, contentRootName);
1750
- await fs5.mkdir(cacheDir, { recursive: true });
1851
+ await fs6.mkdir(cacheDir, { recursive: true });
1751
1852
  const newCache = {
1752
1853
  version: SCHEMA_CACHE_VERSION,
1753
1854
  schema: result.schema,
@@ -1755,10 +1856,10 @@ var BranchSchemaCache = class {
1755
1856
  cachedAt: (/* @__PURE__ */ new Date()).toISOString()
1756
1857
  };
1757
1858
  const tmpPath = path6.join(cacheDir, `schema-cache.tmp.${Date.now()}.${Math.random()}.json`);
1758
- await fs5.writeFile(tmpPath, JSON.stringify(newCache, null, 2), "utf-8");
1759
- await fs5.rename(tmpPath, cachePath);
1859
+ await fs6.writeFile(tmpPath, JSON.stringify(newCache, null, 2), "utf-8");
1860
+ await fs6.rename(tmpPath, cachePath);
1760
1861
  try {
1761
- await fs5.unlink(stalePath);
1862
+ await fs6.unlink(stalePath);
1762
1863
  } catch {
1763
1864
  }
1764
1865
  return { schema: result.schema, flatSchema };
@@ -1769,24 +1870,10 @@ var BranchSchemaCache = class {
1769
1870
  * @param branchRoot - Root directory of the branch
1770
1871
  */
1771
1872
  async invalidate(branchRoot) {
1772
- if (this.mode === "dev") {
1773
- this.devModeCache = void 0;
1774
- return;
1775
- }
1776
1873
  const cacheDir = path6.join(branchRoot, ".canopy-meta");
1777
1874
  const stalePath = path6.join(cacheDir, "schema-cache.stale");
1778
- await fs5.mkdir(cacheDir, { recursive: true });
1779
- await fs5.writeFile(stalePath, "", "utf-8");
1780
- }
1781
- /**
1782
- * Clear all caches (for testing).
1783
- * In dev mode, clears in-memory cache.
1784
- * In prod/prod-sim modes, this would need to traverse all branch directories.
1785
- */
1786
- async clearAll() {
1787
- if (this.mode === "dev") {
1788
- this.devModeCache = void 0;
1789
- }
1875
+ await fs6.mkdir(cacheDir, { recursive: true });
1876
+ await fs6.writeFile(stalePath, "", "utf-8");
1790
1877
  }
1791
1878
  };
1792
1879
 
@@ -2287,11 +2374,11 @@ function matchesBundleFilter(entry, filter, contentRoot) {
2287
2374
 
2288
2375
  // dist/branch-metadata.js
2289
2376
  import { randomUUID } from "node:crypto";
2290
- import fs7 from "node:fs/promises";
2377
+ import fs8 from "node:fs/promises";
2291
2378
  import path9 from "node:path";
2292
2379
 
2293
2380
  // dist/branch-registry.js
2294
- import fs6 from "node:fs/promises";
2381
+ import fs7 from "node:fs/promises";
2295
2382
  import path8 from "node:path";
2296
2383
  var REGISTRY_FILE = "branches.json";
2297
2384
  var REGISTRY_STALE_FILE = "branches.stale.json";
@@ -2309,7 +2396,7 @@ var BranchRegistry = class {
2309
2396
  */
2310
2397
  async list() {
2311
2398
  try {
2312
- const raw = await fs6.readFile(this.registryPath, "utf8");
2399
+ const raw = await fs7.readFile(this.registryPath, "utf8");
2313
2400
  const parsed = JSON.parse(raw);
2314
2401
  if (!parsed.version || !Array.isArray(parsed.branches)) {
2315
2402
  return await this.regenerate();
@@ -2335,7 +2422,7 @@ var BranchRegistry = class {
2335
2422
  */
2336
2423
  async invalidate() {
2337
2424
  try {
2338
- await fs6.rename(this.registryPath, this.stalePath);
2425
+ await fs7.rename(this.registryPath, this.stalePath);
2339
2426
  } catch (err) {
2340
2427
  if (!isNotFoundError(err)) {
2341
2428
  throw err;
@@ -2349,20 +2436,20 @@ var BranchRegistry = class {
2349
2436
  async regenerate() {
2350
2437
  const branches = await this.scanBranchDirectories();
2351
2438
  const uniqueTempPath = `${this.tempPath}.${Date.now()}.${Math.random().toString(36).slice(2)}`;
2352
- await fs6.mkdir(this.root, { recursive: true });
2439
+ await fs7.mkdir(this.root, { recursive: true });
2353
2440
  const snapshot = {
2354
2441
  version: REGISTRY_VERSION,
2355
2442
  branches
2356
2443
  };
2357
- await fs6.writeFile(uniqueTempPath, JSON.stringify(snapshot, null, 2) + "\n", "utf8");
2444
+ await fs7.writeFile(uniqueTempPath, JSON.stringify(snapshot, null, 2) + "\n", "utf8");
2358
2445
  try {
2359
- await fs6.rename(uniqueTempPath, this.registryPath);
2446
+ await fs7.rename(uniqueTempPath, this.registryPath);
2360
2447
  } catch (err) {
2361
- await fs6.unlink(uniqueTempPath).catch(() => {
2448
+ await fs7.unlink(uniqueTempPath).catch(() => {
2362
2449
  });
2363
2450
  throw err;
2364
2451
  }
2365
- await fs6.unlink(this.stalePath).catch(() => {
2452
+ await fs7.unlink(this.stalePath).catch(() => {
2366
2453
  });
2367
2454
  return branches;
2368
2455
  }
@@ -2372,7 +2459,7 @@ var BranchRegistry = class {
2372
2459
  async scanBranchDirectories() {
2373
2460
  const branches = [];
2374
2461
  try {
2375
- const entries = await fs6.readdir(this.root, { withFileTypes: true });
2462
+ const entries = await fs7.readdir(this.root, { withFileTypes: true });
2376
2463
  for (const entry of entries) {
2377
2464
  if (!entry.isDirectory() || entry.name.startsWith(".")) {
2378
2465
  continue;
@@ -2437,7 +2524,7 @@ var BranchMetadataFileManager = class _BranchMetadataFileManager {
2437
2524
  static async loadOnly(branchRoot) {
2438
2525
  const filePath = path9.join(path9.resolve(branchRoot), BRANCH_META_DIR, BRANCH_META_FILE);
2439
2526
  try {
2440
- const raw = await fs7.readFile(filePath, "utf8");
2527
+ const raw = await fs8.readFile(filePath, "utf8");
2441
2528
  return JSON.parse(raw);
2442
2529
  } catch (err) {
2443
2530
  if (isNotFoundError(err)) {
@@ -2455,7 +2542,7 @@ var BranchMetadataFileManager = class _BranchMetadataFileManager {
2455
2542
  }
2456
2543
  async load() {
2457
2544
  try {
2458
- const raw = await fs7.readFile(this.filePath, "utf8");
2545
+ const raw = await fs8.readFile(this.filePath, "utf8");
2459
2546
  const parsed = JSON.parse(raw);
2460
2547
  const version = parsed.version ?? 0;
2461
2548
  return { meta: parsed, version };
@@ -2479,11 +2566,11 @@ var BranchMetadataFileManager = class _BranchMetadataFileManager {
2479
2566
  version: newVersion,
2480
2567
  writeId
2481
2568
  };
2482
- await fs7.mkdir(path9.dirname(this.filePath), { recursive: true });
2569
+ await fs8.mkdir(path9.dirname(this.filePath), { recursive: true });
2483
2570
  const content = JSON.stringify(payload, null, 2) + "\n";
2484
2571
  if (expectedVersion === null) {
2485
2572
  try {
2486
- await fs7.writeFile(this.filePath, content, { flag: "wx" });
2573
+ await fs8.writeFile(this.filePath, content, { flag: "wx" });
2487
2574
  return { version: newVersion, writeId };
2488
2575
  } catch (err) {
2489
2576
  if (isFileExistsError(err)) {
@@ -2493,11 +2580,11 @@ var BranchMetadataFileManager = class _BranchMetadataFileManager {
2493
2580
  }
2494
2581
  }
2495
2582
  const tempPath = `${this.filePath}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
2496
- await fs7.writeFile(tempPath, content, "utf-8");
2583
+ await fs8.writeFile(tempPath, content, "utf-8");
2497
2584
  try {
2498
2585
  let currentVersion = null;
2499
2586
  try {
2500
- const current = JSON.parse(await fs7.readFile(this.filePath, "utf-8"));
2587
+ const current = JSON.parse(await fs8.readFile(this.filePath, "utf-8"));
2501
2588
  currentVersion = current.version ?? 0;
2502
2589
  } catch {
2503
2590
  currentVersion = null;
@@ -2505,13 +2592,13 @@ var BranchMetadataFileManager = class _BranchMetadataFileManager {
2505
2592
  if (currentVersion !== expectedVersion) {
2506
2593
  throw new BranchMetadataConflictError();
2507
2594
  }
2508
- await fs7.rename(tempPath, this.filePath);
2509
- const afterWrite = JSON.parse(await fs7.readFile(this.filePath, "utf-8"));
2595
+ await fs8.rename(tempPath, this.filePath);
2596
+ const afterWrite = JSON.parse(await fs8.readFile(this.filePath, "utf-8"));
2510
2597
  if (afterWrite.writeId !== writeId) {
2511
2598
  throw new BranchMetadataConflictError();
2512
2599
  }
2513
2600
  } catch (err) {
2514
- await fs7.unlink(tempPath).catch(() => {
2601
+ await fs8.unlink(tempPath).catch(() => {
2515
2602
  });
2516
2603
  throw err;
2517
2604
  }
@@ -2576,6 +2663,9 @@ var BranchMetadataFileManager = class _BranchMetadataFileManager {
2576
2663
  await registry.invalidate();
2577
2664
  }
2578
2665
  };
2666
+ var getBranchMetadataFileManager = (branchRoot, baseRoot) => {
2667
+ return BranchMetadataFileManager.get(branchRoot, baseRoot);
2668
+ };
2579
2669
  var loadBranchContext = async (options) => {
2580
2670
  const { branchRoot, baseRoot } = resolveBranchPath({
2581
2671
  branchName: options.branchName,
@@ -2605,19 +2695,704 @@ var STATIC_DEPLOY_USER = Object.freeze({
2605
2695
  name: "Static Deploy"
2606
2696
  });
2607
2697
 
2698
+ // dist/git-manager.js
2699
+ import fs9 from "node:fs/promises";
2700
+ import path10 from "node:path";
2701
+ import { simpleGit as simpleGit2 } from "simple-git";
2702
+
2703
+ // dist/utils/debug.js
2704
+ var LOG_LEVELS = {
2705
+ DEBUG: 0,
2706
+ INFO: 1,
2707
+ WARN: 2,
2708
+ ERROR: 3
2709
+ };
2710
+ var DebugLogger = class {
2711
+ constructor(options = {}) {
2712
+ this.timers = /* @__PURE__ */ new Map();
2713
+ this.options = options;
2714
+ }
2715
+ shouldLog(level) {
2716
+ const enabled = this.options.enabled ?? process.env.CANOPYCMS_DEBUG === "true";
2717
+ if (!enabled)
2718
+ return false;
2719
+ const minLevel = this.options.minLevel ?? "DEBUG";
2720
+ return LOG_LEVELS[level] >= LOG_LEVELS[minLevel];
2721
+ }
2722
+ formatMessage(level, category, message) {
2723
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
2724
+ const prefix = this.options.prefix ?? "CanopyCMS";
2725
+ return `[${timestamp}] [${prefix}:${category}] [${level}] ${message}`;
2726
+ }
2727
+ debug(category, message, data) {
2728
+ if (this.shouldLog("DEBUG")) {
2729
+ console.log(this.formatMessage("DEBUG", category, message), data ?? "");
2730
+ }
2731
+ }
2732
+ info(category, message, data) {
2733
+ if (this.shouldLog("INFO")) {
2734
+ console.log(this.formatMessage("INFO", category, message), data ?? "");
2735
+ }
2736
+ }
2737
+ warn(category, message, data) {
2738
+ if (this.shouldLog("WARN")) {
2739
+ console.warn(this.formatMessage("WARN", category, message), data ?? "");
2740
+ }
2741
+ }
2742
+ error(category, message, data) {
2743
+ const msg = this.formatMessage("ERROR", category, message);
2744
+ if (this.shouldLog("ERROR")) {
2745
+ console.error(msg, data ?? "");
2746
+ }
2747
+ const throwOnError = this.options.throwOnError ?? false;
2748
+ if (throwOnError) {
2749
+ const errorMsg = data ? `${message}: ${JSON.stringify(data)}` : message;
2750
+ throw new Error(errorMsg);
2751
+ }
2752
+ }
2753
+ /**
2754
+ * Start timing an operation
2755
+ */
2756
+ time(label) {
2757
+ this.timers.set(label, Date.now());
2758
+ }
2759
+ /**
2760
+ * End timing an operation and log the duration
2761
+ */
2762
+ timeEnd(category, label) {
2763
+ const start = this.timers.get(label);
2764
+ if (start === void 0) {
2765
+ this.warn(category, `Timer '${label}' does not exist`);
2766
+ return;
2767
+ }
2768
+ const duration = Date.now() - start;
2769
+ this.timers.delete(label);
2770
+ this.debug(category, `${label} completed`, { durationMs: duration });
2771
+ return duration;
2772
+ }
2773
+ /**
2774
+ * Wrap an async function with automatic timing
2775
+ */
2776
+ async timed(category, label, fn) {
2777
+ this.time(label);
2778
+ try {
2779
+ return await fn();
2780
+ } finally {
2781
+ this.timeEnd(category, label);
2782
+ }
2783
+ }
2784
+ };
2785
+ function createDebugLogger(options) {
2786
+ return new DebugLogger(options);
2787
+ }
2788
+ var testLogger = createDebugLogger({
2789
+ enabled: process.env.E2E_DEBUG === "true",
2790
+ prefix: "E2E",
2791
+ throwOnError: false
2792
+ });
2793
+
2794
+ // dist/utils/git.js
2795
+ import { simpleGit } from "simple-git";
2796
+ async function detectHeadBranch(repoRoot, fallback = "main") {
2797
+ try {
2798
+ const git = simpleGit({ baseDir: repoRoot });
2799
+ const head = (await git.revparse(["--abbrev-ref", "HEAD"])).trim();
2800
+ return head && head !== "HEAD" ? head : fallback;
2801
+ } catch {
2802
+ return fallback;
2803
+ }
2804
+ }
2805
+
2806
+ // dist/git-manager.js
2807
+ var log = createDebugLogger({ prefix: "GitManager" });
2808
+ var remoteInitLocks = /* @__PURE__ */ new Map();
2809
+ var GitManager = class _GitManager {
2810
+ constructor(options, gitOptions) {
2811
+ this.repoPath = path10.resolve(options.repoPath);
2812
+ this.baseBranch = options.baseBranch ?? "main";
2813
+ this.remote = options.remote ?? "origin";
2814
+ this.git = simpleGit2({ baseDir: this.repoPath, ...gitOptions });
2815
+ }
2816
+ static async cloneRepo(remoteUrl, targetPath, baseBranch = "main") {
2817
+ log.debug("git", "Cloning repository", {
2818
+ remoteUrl,
2819
+ targetPath,
2820
+ baseBranch
2821
+ });
2822
+ const git = simpleGit2();
2823
+ await git.clone(remoteUrl, targetPath, ["--branch", baseBranch, "--single-branch"]);
2824
+ log.debug("git", "Clone complete");
2825
+ }
2826
+ /**
2827
+ * Initializes a local bare git repository to simulate a remote for dev mode.
2828
+ *
2829
+ * This is idempotent - if the remote already exists, it will not be recreated.
2830
+ *
2831
+ * The remote is seeded with the current state of the baseBranch (e.g., 'main').
2832
+ * If you need to change the baseBranch or reset the simulation, delete
2833
+ * `.canopycms/remote.git` and `.canopycms/branches` and restart.
2834
+ *
2835
+ * @throws Error if not a git repo, no commits, or baseBranch doesn't exist
2836
+ */
2837
+ static async ensureLocalSimulatedRemote(options) {
2838
+ const existingLock = remoteInitLocks.get(options.remotePath);
2839
+ if (existingLock) {
2840
+ log.debug("git", "Waiting for existing remote initialization", {
2841
+ remotePath: options.remotePath
2842
+ });
2843
+ await existingLock;
2844
+ try {
2845
+ const stat = await fs9.stat(options.remotePath);
2846
+ if (stat.isDirectory()) {
2847
+ log.debug("git", "Remote exists after waiting for lock");
2848
+ return;
2849
+ }
2850
+ } catch (err) {
2851
+ if (!isNotFoundError(err))
2852
+ throw err;
2853
+ log.debug("git", "Remote does not exist after lock, will retry initialization");
2854
+ }
2855
+ }
2856
+ const lockPromise = log.timed("git", "ensureLocalSimulatedRemote", async () => {
2857
+ try {
2858
+ log.debug("git", "Initializing local simulated remote", {
2859
+ remotePath: options.remotePath,
2860
+ baseBranch: options.baseBranch
2861
+ });
2862
+ try {
2863
+ const stat = await fs9.stat(options.remotePath);
2864
+ if (stat.isDirectory()) {
2865
+ log.debug("git", "Remote already exists, skipping");
2866
+ return;
2867
+ }
2868
+ } catch (err) {
2869
+ if (!isNotFoundError(err))
2870
+ throw err;
2871
+ }
2872
+ let gitRoot = options.sourcePath;
2873
+ try {
2874
+ const sourceGit2 = simpleGit2({ baseDir: options.sourcePath });
2875
+ const result = await sourceGit2.raw(["rev-parse", "--show-toplevel"]);
2876
+ gitRoot = result.trim();
2877
+ } catch {
2878
+ gitRoot = options.sourcePath;
2879
+ }
2880
+ const sourceGit = simpleGit2({ baseDir: gitRoot });
2881
+ try {
2882
+ await sourceGit.status();
2883
+ } catch {
2884
+ throw new Error("Cannot initialize local simulated remote: current directory is not a git repository. Please initialize git or provide an explicit remoteUrl.");
2885
+ }
2886
+ let hasCommits = false;
2887
+ try {
2888
+ const log3 = await sourceGit.log(["-1"]);
2889
+ hasCommits = log3.total > 0;
2890
+ } catch {
2891
+ hasCommits = false;
2892
+ }
2893
+ if (!hasCommits) {
2894
+ throw new Error("Cannot initialize local simulated remote: repository has no commits. Please make an initial commit or provide an explicit remoteUrl.");
2895
+ }
2896
+ const branches = await sourceGit.branchLocal();
2897
+ if (!branches.all.includes(options.baseBranch)) {
2898
+ throw new Error(`Cannot initialize local simulated remote: base branch '${options.baseBranch}' does not exist locally. Please checkout '${options.baseBranch}' first or provide an explicit remoteUrl.`);
2899
+ }
2900
+ log.debug("git", "Creating bare remote repository");
2901
+ await fs9.mkdir(path10.dirname(options.remotePath), { recursive: true });
2902
+ await simpleGit2().raw([
2903
+ "init",
2904
+ "--bare",
2905
+ `--initial-branch=${options.baseBranch}`,
2906
+ options.remotePath
2907
+ ]);
2908
+ const tempRemoteName = `__canopycms_init_${Date.now()}__`;
2909
+ try {
2910
+ await sourceGit.addRemote(tempRemoteName, options.remotePath);
2911
+ if (options.subdirectory) {
2912
+ const splitBranch = `__canopycms_split_${Date.now()}__`;
2913
+ try {
2914
+ await sourceGit.raw([
2915
+ "subtree",
2916
+ "split",
2917
+ "--prefix",
2918
+ options.subdirectory,
2919
+ "-b",
2920
+ splitBranch
2921
+ ]);
2922
+ await sourceGit.push(tempRemoteName, `${splitBranch}:${options.baseBranch}`);
2923
+ await sourceGit.raw(["branch", "-D", splitBranch]);
2924
+ } catch (err) {
2925
+ try {
2926
+ await sourceGit.raw(["branch", "-D", splitBranch]);
2927
+ } catch {
2928
+ }
2929
+ throw err;
2930
+ }
2931
+ } else {
2932
+ await sourceGit.push(tempRemoteName, `${options.baseBranch}:${options.baseBranch}`);
2933
+ }
2934
+ } finally {
2935
+ try {
2936
+ await sourceGit.removeRemote(tempRemoteName);
2937
+ } catch {
2938
+ }
2939
+ }
2940
+ log.debug("git", "Remote initialization complete");
2941
+ } finally {
2942
+ remoteInitLocks.delete(options.remotePath);
2943
+ }
2944
+ });
2945
+ remoteInitLocks.set(options.remotePath, lockPromise);
2946
+ await lockPromise;
2947
+ }
2948
+ /**
2949
+ * Find the git root directory
2950
+ * @returns Path to git root, or cwd if not in a git repo
2951
+ */
2952
+ static async findGitRoot() {
2953
+ let gitRoot = process.cwd();
2954
+ try {
2955
+ const git = simpleGit2({ baseDir: process.cwd() });
2956
+ const result = await git.raw(["rev-parse", "--show-toplevel"]);
2957
+ gitRoot = result.trim();
2958
+ } catch {
2959
+ }
2960
+ return gitRoot;
2961
+ }
2962
+ /**
2963
+ * Validate that a git repository exists at the given path
2964
+ * @param repoPath - Path to check for .git directory
2965
+ * @throws Error if git repo doesn't exist
2966
+ */
2967
+ static async validateGitRepoExists(repoPath) {
2968
+ try {
2969
+ const stat = await fs9.stat(path10.join(repoPath, ".git"));
2970
+ if (!stat.isDirectory()) {
2971
+ throw new Error(`Expected git repo at ${repoPath}`);
2972
+ }
2973
+ } catch (err) {
2974
+ if (isNotFoundError(err)) {
2975
+ throw new Error(`Expected git repo at ${repoPath}`);
2976
+ }
2977
+ throw err;
2978
+ }
2979
+ }
2980
+ /**
2981
+ * Resolves the remote URL for git operations following the priority:
2982
+ * 1. Explicit remoteUrl parameter
2983
+ * 2. Config defaultRemoteUrl
2984
+ * 3. Environment variable (mode-specific)
2985
+ * 4. Auto-initialized local remote (for dev mode)
2986
+ *
2987
+ * Uses strategy flags to determine behavior, GitManager executes the logic.
2988
+ *
2989
+ * @param options.sourceRoot - Optional source directory for monorepos. When provided,
2990
+ * this directory (relative to git root) is used as the source for the simulated remote.
2991
+ * Defaults to process.cwd().
2992
+ *
2993
+ * @returns Remote URL or undefined if no remote is needed
2994
+ */
2995
+ static async resolveRemoteUrl(options) {
2996
+ const { operatingStrategy: operatingStrategy2 } = await Promise.resolve().then(() => (init_operating_mode(), operating_mode_exports));
2997
+ const strategy = operatingStrategy2(options.mode);
2998
+ const config = strategy.getRemoteUrlConfig();
2999
+ if (options.remoteUrl)
3000
+ return options.remoteUrl;
3001
+ if (options.defaultRemoteUrl)
3002
+ return options.defaultRemoteUrl;
3003
+ if (process.env[config.envVarName])
3004
+ return process.env[config.envVarName];
3005
+ if (config.autoDetectRemotePath) {
3006
+ try {
3007
+ const stat = await fs9.stat(config.autoDetectRemotePath);
3008
+ if (stat.isDirectory()) {
3009
+ log.debug("git", "Auto-detected local remote", {
3010
+ path: config.autoDetectRemotePath
3011
+ });
3012
+ return config.autoDetectRemotePath;
3013
+ }
3014
+ } catch {
3015
+ }
3016
+ }
3017
+ if (config.shouldAutoInitLocal) {
3018
+ const gitRoot = await this.findGitRoot();
3019
+ const sourceRoot = options.sourceRoot;
3020
+ const sourcePath = sourceRoot ? path10.resolve(gitRoot, sourceRoot) : gitRoot;
3021
+ const localRemotePath = path10.join(sourcePath, config.defaultRemotePath);
3022
+ await this.ensureLocalSimulatedRemote({
3023
+ remotePath: localRemotePath,
3024
+ sourcePath: gitRoot,
3025
+ baseBranch: options.baseBranch,
3026
+ subdirectory: sourceRoot
3027
+ });
3028
+ return localRemotePath;
3029
+ }
3030
+ return void 0;
3031
+ }
3032
+ /**
3033
+ * Ensures a git workspace is initialized and ready for use.
3034
+ * Handles cloning, remote configuration, and branch checkout/creation.
3035
+ *
3036
+ * This centralizes the common initialization sequence used by both BranchWorkspaceManager
3037
+ * and SettingsWorkspaceManager.
3038
+ *
3039
+ * Note: Does NOT configure git author - that should be done before commits, not during init.
3040
+ *
3041
+ * @returns Configured GitManager instance for the workspace
3042
+ */
3043
+ static async initializeWorkspace(options) {
3044
+ let baseBranch = options.baseBranch;
3045
+ if (!baseBranch && options.mode === "dev") {
3046
+ const sourceRoot = options.sourceRoot ? path10.resolve(process.cwd(), options.sourceRoot) : process.cwd();
3047
+ baseBranch = await detectHeadBranch(sourceRoot);
3048
+ }
3049
+ baseBranch = baseBranch ?? "main";
3050
+ const remoteName = options.remoteName ?? "origin";
3051
+ let repoExists = false;
3052
+ try {
3053
+ const stat = await fs9.stat(path10.join(options.workspacePath, ".git"));
3054
+ repoExists = stat.isDirectory();
3055
+ } catch (err) {
3056
+ if (!isNotFoundError(err))
3057
+ throw err;
3058
+ }
3059
+ let justCloned = false;
3060
+ if (!repoExists) {
3061
+ const remoteUrl = await _GitManager.resolveRemoteUrl({
3062
+ mode: options.mode,
3063
+ remoteUrl: options.remoteUrl,
3064
+ defaultRemoteUrl: options.defaultRemoteUrl,
3065
+ baseBranch,
3066
+ sourceRoot: options.sourceRoot
3067
+ });
3068
+ if (!remoteUrl) {
3069
+ throw new Error("CanopyCMS: defaultRemoteUrl (or CANOPYCMS_REMOTE_URL) is required to initialize workspace");
3070
+ }
3071
+ await _GitManager.cloneRepo(remoteUrl, options.workspacePath, baseBranch);
3072
+ justCloned = true;
3073
+ }
3074
+ const git = new _GitManager({
3075
+ repoPath: options.workspacePath,
3076
+ baseBranch,
3077
+ remote: remoteName
3078
+ });
3079
+ if (!justCloned) {
3080
+ const remoteUrl = await _GitManager.resolveRemoteUrl({
3081
+ mode: options.mode,
3082
+ remoteUrl: options.remoteUrl,
3083
+ defaultRemoteUrl: options.defaultRemoteUrl,
3084
+ baseBranch,
3085
+ sourceRoot: options.sourceRoot
3086
+ });
3087
+ if (remoteUrl) {
3088
+ await git.ensureRemote(remoteUrl);
3089
+ }
3090
+ }
3091
+ if (options.branchType === "orphan") {
3092
+ await git.createOrphanSettingsBranch(options.branchName, {});
3093
+ } else {
3094
+ await git.checkoutBranch(options.branchName);
3095
+ }
3096
+ await git.git.addConfig("canopycms.managed", "true");
3097
+ log.debug("git", "Marked workspace as CanopyCMS-managed", {
3098
+ workspacePath: options.workspacePath
3099
+ });
3100
+ return git;
3101
+ }
3102
+ async status() {
3103
+ const s = await this.git.status();
3104
+ return {
3105
+ files: s.files,
3106
+ ahead: s.ahead,
3107
+ behind: s.behind,
3108
+ current: s.current,
3109
+ tracking: s.tracking
3110
+ };
3111
+ }
3112
+ async checkoutBranch(branch) {
3113
+ const branches = await this.git.branch();
3114
+ if (branches.all.includes(branch)) {
3115
+ await this.git.checkout(branch);
3116
+ return;
3117
+ }
3118
+ const remoteRef = `${this.remote}/${this.baseBranch}`;
3119
+ try {
3120
+ await this.git.fetch(this.remote, this.baseBranch);
3121
+ } catch {
3122
+ }
3123
+ try {
3124
+ await this.git.checkoutBranch(branch, remoteRef);
3125
+ return;
3126
+ } catch {
3127
+ const baseExists = branches.all.includes(this.baseBranch);
3128
+ if (baseExists) {
3129
+ await this.git.checkout(["-B", branch, this.baseBranch]);
3130
+ return;
3131
+ }
3132
+ await this.git.checkoutLocalBranch(branch);
3133
+ }
3134
+ }
3135
+ async pullBase() {
3136
+ await this.git.fetch(this.remote, this.baseBranch);
3137
+ await this.git.merge([`${this.remote}/${this.baseBranch}`]);
3138
+ }
3139
+ async pullCurrentBranch() {
3140
+ const branches = await this.git.branch();
3141
+ const currentBranch = branches.current;
3142
+ await this.git.fetch(this.remote, currentBranch);
3143
+ await this.git.merge([`${this.remote}/${currentBranch}`]);
3144
+ }
3145
+ async rebaseOntoBase() {
3146
+ await this.git.fetch(this.remote, this.baseBranch);
3147
+ await this.git.rebase([`${this.remote}/${this.baseBranch}`]);
3148
+ }
3149
+ async add(files) {
3150
+ const fileArray = Array.isArray(files) ? files : [files];
3151
+ await this.git.add(fileArray);
3152
+ }
3153
+ async commit(message) {
3154
+ await this.git.commit(message);
3155
+ }
3156
+ async push(branch) {
3157
+ const target = branch ?? await this.git.revparse(["--abbrev-ref", "HEAD"]);
3158
+ await this.git.push(this.remote, `${target}:${target}`, ["--set-upstream"]);
3159
+ }
3160
+ async ensureAuthor(author) {
3161
+ const config = await this.git.listConfig();
3162
+ const isManaged = config.all["canopycms.managed"] === "true";
3163
+ if (!isManaged) {
3164
+ throw new Error(`Cannot set git bot author in non-managed repository (${this.repoPath}). Bot identity should only be set in CanopyCMS branch clones or test workspaces. If this is a test workspace, add "git config canopycms.managed true" to mark it as managed.`);
3165
+ }
3166
+ const currentName = config.all["user.name"];
3167
+ const currentEmail = config.all["user.email"];
3168
+ if (currentName !== author.name) {
3169
+ await this.git.addConfig("user.name", author.name);
3170
+ }
3171
+ if (currentEmail !== author.email) {
3172
+ await this.git.addConfig("user.email", author.email);
3173
+ }
3174
+ }
3175
+ async ensureRemote(remoteUrl) {
3176
+ const remotes = await this.git.getRemotes(true);
3177
+ const existing = remotes.find((r) => r.name === this.remote);
3178
+ if (!existing) {
3179
+ await this.git.addRemote(this.remote, remoteUrl);
3180
+ return;
3181
+ }
3182
+ const currentUrl = existing.refs.push ?? existing.refs.fetch;
3183
+ if (currentUrl && currentUrl !== remoteUrl) {
3184
+ await this.git.remote(["set-url", this.remote, remoteUrl]);
3185
+ }
3186
+ }
3187
+ /**
3188
+ * Check if working directory has uncommitted changes
3189
+ */
3190
+ async hasUncommittedChanges() {
3191
+ const status = await this.status();
3192
+ return status.files.length > 0;
3193
+ }
3194
+ /**
3195
+ * Get list of uncommitted file paths
3196
+ */
3197
+ async getUncommittedFiles() {
3198
+ const status = await this.status();
3199
+ return status.files.map((f) => f.path);
3200
+ }
3201
+ /**
3202
+ * Force push (use with caution - for PR updates only)
3203
+ * Uses --force-with-lease for safer force pushes
3204
+ */
3205
+ async forcePush(branch) {
3206
+ const target = branch ?? await this.git.revparse(["--abbrev-ref", "HEAD"]);
3207
+ await this.git.push(this.remote, target, ["--force-with-lease"]);
3208
+ }
3209
+ /**
3210
+ * Get remote URL for current repo
3211
+ */
3212
+ async getRemoteUrl() {
3213
+ const remotes = await this.git.getRemotes(true);
3214
+ const remote = remotes.find((r) => r.name === this.remote);
3215
+ return remote?.refs.push || remote?.refs.fetch;
3216
+ }
3217
+ /**
3218
+ * Add a pattern to .git/info/exclude to prevent it from being committed/pushed.
3219
+ * This is used to exclude .canopy-meta/ from content branch workspaces.
3220
+ *
3221
+ * .git/info/exclude is a per-repository gitignore that never gets committed.
3222
+ * Perfect for runtime metadata that should never leave the workspace.
3223
+ *
3224
+ * This is idempotent - if the pattern already exists, it won't be added again.
3225
+ */
3226
+ async ensureGitExclude(pattern) {
3227
+ const excludePath = path10.join(this.repoPath, ".git", "info", "exclude");
3228
+ await fs9.mkdir(path10.dirname(excludePath), { recursive: true });
3229
+ let content = "";
3230
+ try {
3231
+ content = await fs9.readFile(excludePath, "utf-8");
3232
+ } catch (err) {
3233
+ if (!isNotFoundError(err))
3234
+ throw err;
3235
+ }
3236
+ const lines = content.split("\n");
3237
+ if (lines.some((line) => line.trim() === pattern)) {
3238
+ log.debug("git", "Pattern already in .git/info/exclude", { pattern });
3239
+ return;
3240
+ }
3241
+ const needsLeadingNewline = content.length > 0 && !content.endsWith("\n");
3242
+ const newContent = content + (needsLeadingNewline ? "\n" : "") + pattern + "\n";
3243
+ await fs9.writeFile(excludePath, newContent, "utf-8");
3244
+ log.debug("git", "Added pattern to .git/info/exclude", { pattern });
3245
+ }
3246
+ /**
3247
+ * Create an orphan branch for settings (permissions/groups).
3248
+ *
3249
+ * Orphan branches have no shared history with other branches - they start fresh.
3250
+ * This is perfect for deployment-specific settings that shouldn't pollute content history.
3251
+ *
3252
+ * The branch contains only settings files in .canopy-meta/ (groups.json, permissions.json).
3253
+ *
3254
+ * @param branchName - Name of the orphan branch (e.g., 'canopycms-settings-prod')
3255
+ * @param initialFiles - Files to commit to the new branch (e.g., { 'permissions.json': '{}', 'groups.json': '{}' })
3256
+ */
3257
+ async createOrphanSettingsBranch(branchName, initialFiles) {
3258
+ log.debug("git", "Creating orphan settings branch", { branchName });
3259
+ const branches = await this.git.branch();
3260
+ if (branches.all.includes(branchName)) {
3261
+ log.debug("git", "Orphan branch already exists", { branchName });
3262
+ await this.git.checkout(branchName);
3263
+ return;
3264
+ }
3265
+ await this.git.raw(["checkout", "--orphan", branchName]);
3266
+ try {
3267
+ await this.git.raw(["rm", "-rf", "."]);
3268
+ } catch {
3269
+ }
3270
+ for (const [filePath, content] of Object.entries(initialFiles)) {
3271
+ const absolutePath = path10.join(this.repoPath, filePath);
3272
+ await fs9.mkdir(path10.dirname(absolutePath), { recursive: true });
3273
+ await fs9.writeFile(absolutePath, content, "utf-8");
3274
+ await this.git.add(filePath);
3275
+ }
3276
+ await this.git.commit("Initialize settings branch", ["--allow-empty"]);
3277
+ log.debug("git", "Orphan settings branch created", { branchName });
3278
+ }
3279
+ };
3280
+
3281
+ // dist/branch-workspace.js
3282
+ var log2 = createDebugLogger({ prefix: "BranchWorkspace" });
3283
+ var workspaceInitLocks = /* @__PURE__ */ new Map();
3284
+ var BranchWorkspaceManager = class {
3285
+ constructor(config) {
3286
+ this.config = config;
3287
+ }
3288
+ async ensureGitWorkspace(options) {
3289
+ return log2.timed("workspace", "ensureGitWorkspace", async () => {
3290
+ const existingLock = workspaceInitLocks.get(options.branchRoot);
3291
+ if (existingLock) {
3292
+ await existingLock;
3293
+ return;
3294
+ }
3295
+ const lockPromise = (async () => {
3296
+ try {
3297
+ log2.debug("workspace", "Ensuring git workspace", {
3298
+ branchName: options.branchName,
3299
+ mode: options.mode
3300
+ });
3301
+ await GitManager.initializeWorkspace({
3302
+ workspacePath: options.branchRoot,
3303
+ branchName: options.branchName,
3304
+ mode: options.mode,
3305
+ baseBranch: this.config.defaultBaseBranch,
3306
+ sourceRoot: this.config.sourceRoot,
3307
+ defaultRemoteUrl: this.config.defaultRemoteUrl,
3308
+ remoteUrl: options.remoteUrl,
3309
+ remoteName: this.config.defaultRemoteName,
3310
+ branchType: "content"
3311
+ });
3312
+ } finally {
3313
+ workspaceInitLocks.delete(options.branchRoot);
3314
+ }
3315
+ })();
3316
+ workspaceInitLocks.set(options.branchRoot, lockPromise);
3317
+ await lockPromise;
3318
+ });
3319
+ }
3320
+ async openOrCreateBranch(options) {
3321
+ const { branchName, mode, basePathOverride, title, description, access, createdBy, remoteUrl } = options;
3322
+ const { branchRoot, baseRoot, branchName: safeName } = await ensureBranchRoot({
3323
+ mode,
3324
+ branchName,
3325
+ basePathOverride
3326
+ });
3327
+ await this.ensureGitWorkspace({
3328
+ branchRoot,
3329
+ branchName: safeName,
3330
+ mode,
3331
+ remoteUrl
3332
+ });
3333
+ const metadata = getBranchMetadataFileManager(branchRoot, baseRoot);
3334
+ const meta = await metadata.save({
3335
+ branch: {
3336
+ name: safeName,
3337
+ title,
3338
+ description,
3339
+ access,
3340
+ createdBy
3341
+ }
3342
+ });
3343
+ return {
3344
+ branch: meta.branch,
3345
+ branchRoot,
3346
+ baseRoot
3347
+ };
3348
+ }
3349
+ };
3350
+ async function loadOrCreateBranchContext(options) {
3351
+ if (isDeployedStatic(options.config)) {
3352
+ const cwd = process.cwd();
3353
+ return {
3354
+ branch: {
3355
+ name: options.branchName,
3356
+ status: "editing",
3357
+ access: {},
3358
+ createdBy: "__static_deploy__",
3359
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
3360
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
3361
+ },
3362
+ branchRoot: cwd,
3363
+ baseRoot: cwd
3364
+ };
3365
+ }
3366
+ const existing = await loadBranchContext({
3367
+ branchName: options.branchName,
3368
+ mode: options.mode,
3369
+ basePathOverride: options.basePathOverride
3370
+ });
3371
+ if (existing)
3372
+ return existing;
3373
+ const manager = new BranchWorkspaceManager(options.config);
3374
+ return manager.openOrCreateBranch({
3375
+ branchName: options.branchName,
3376
+ mode: options.mode,
3377
+ basePathOverride: options.basePathOverride,
3378
+ createdBy: options.createdBy,
3379
+ remoteUrl: options.remoteUrl
3380
+ });
3381
+ }
3382
+
2608
3383
  // dist/ai/resolve-branch.js
2609
3384
  async function resolveBranchRoot(config) {
2610
- if (config.mode === "dev" || isDeployedStatic(config)) {
3385
+ if (isDeployedStatic(config)) {
2611
3386
  return process.cwd();
2612
3387
  }
2613
3388
  const baseBranch = config.defaultBaseBranch ?? "main";
2614
- const context = await loadBranchContext({
3389
+ const context = await loadOrCreateBranchContext({
3390
+ config,
2615
3391
  branchName: baseBranch,
2616
- mode: config.mode
3392
+ mode: config.mode,
3393
+ createdBy: "canopycms-ai",
3394
+ remoteUrl: config.defaultRemoteUrl
2617
3395
  });
2618
- if (!context) {
2619
- throw new Error(`Could not load branch context for "${baseBranch}". Ensure the branch exists and has been initialized.`);
2620
- }
2621
3396
  return context.branchRoot;
2622
3397
  }
2623
3398
 
@@ -2641,15 +3416,15 @@ async function generateAIContentFiles(options) {
2641
3416
  contentRoot: contentRootName,
2642
3417
  config: aiConfig
2643
3418
  });
2644
- const absoluteOutputDir = path10.resolve(outputDir) + path10.sep;
3419
+ const absoluteOutputDir = path11.resolve(outputDir) + path11.sep;
2645
3420
  let fileCount = 0;
2646
3421
  for (const [filePath, content] of result.files) {
2647
- const absolutePath = path10.resolve(path10.join(absoluteOutputDir, filePath));
3422
+ const absolutePath = path11.resolve(path11.join(absoluteOutputDir, filePath));
2648
3423
  if (!absolutePath.startsWith(absoluteOutputDir)) {
2649
3424
  throw new Error(`Path traversal detected in AI content output: ${filePath}`);
2650
3425
  }
2651
- await fs8.mkdir(path10.dirname(absolutePath), { recursive: true });
2652
- await fs8.writeFile(absolutePath, content, "utf-8");
3426
+ await fs10.mkdir(path11.dirname(absolutePath), { recursive: true });
3427
+ await fs10.writeFile(absolutePath, content, "utf-8");
2653
3428
  fileCount++;
2654
3429
  }
2655
3430
  return { fileCount, outputDir: absoluteOutputDir };
@@ -2660,7 +3435,7 @@ var jiti = createJiti(import.meta.url);
2660
3435
  async function generateAIContentCLI(options) {
2661
3436
  const { projectDir, outputDir = "public/ai", configPath, appDir = "app" } = options;
2662
3437
  console.log("\nCanopyCMS generate-ai-content\n");
2663
- const canopyConfigPath = path11.join(projectDir, "canopycms.config.ts");
3438
+ const canopyConfigPath = path12.join(projectDir, "canopycms.config.ts");
2664
3439
  let canopyConfigModule;
2665
3440
  try {
2666
3441
  canopyConfigModule = await jiti.import(canopyConfigPath);
@@ -2671,7 +3446,7 @@ async function generateAIContentCLI(options) {
2671
3446
  }
2672
3447
  const configExport = canopyConfigModule.default ?? canopyConfigModule.config ?? canopyConfigModule;
2673
3448
  const serverConfig = typeof configExport === "object" && configExport !== null && "server" in configExport ? configExport.server : configExport;
2674
- const schemasPath = path11.join(projectDir, appDir, "schemas.ts");
3449
+ const schemasPath = path12.join(projectDir, appDir, "schemas.ts");
2675
3450
  let entrySchemaRegistry = {};
2676
3451
  try {
2677
3452
  const schemasModule = await jiti.import(schemasPath);
@@ -2682,7 +3457,7 @@ async function generateAIContentCLI(options) {
2682
3457
  let aiConfig;
2683
3458
  if (configPath) {
2684
3459
  try {
2685
- const aiConfigModule = await jiti.import(path11.resolve(configPath));
3460
+ const aiConfigModule = await jiti.import(path12.resolve(configPath));
2686
3461
  aiConfig = aiConfigModule.aiContentConfig ?? aiConfigModule.default ?? aiConfigModule.config;
2687
3462
  } catch (err) {
2688
3463
  console.error(`Could not load AI config from ${configPath}`);
@@ -2699,7 +3474,7 @@ async function generateAIContentCLI(options) {
2699
3474
  console.error("Make sure canopycms.config.ts uses defineCanopyConfig().");
2700
3475
  process.exit(1);
2701
3476
  }
2702
- const resolvedOutput = path11.resolve(projectDir, outputDir);
3477
+ const resolvedOutput = path12.resolve(projectDir, outputDir);
2703
3478
  console.log(` Output: ${resolvedOutput}`);
2704
3479
  console.log(` Mode: ${serverConfig.mode ?? "dev"}`);
2705
3480
  const result = await generateAIContentFiles({