canopycms 0.0.10 → 0.0.12

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 (98) 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/github-sync.js +0 -2
  13. package/dist/api/github-sync.js.map +1 -1
  14. package/dist/api/settings-helpers.d.ts +3 -5
  15. package/dist/api/settings-helpers.d.ts.map +1 -1
  16. package/dist/api/settings-helpers.js +6 -19
  17. package/dist/api/settings-helpers.js.map +1 -1
  18. package/dist/auth/caching-auth-plugin.d.ts +7 -1
  19. package/dist/auth/caching-auth-plugin.d.ts.map +1 -1
  20. package/dist/auth/caching-auth-plugin.js +31 -3
  21. package/dist/auth/caching-auth-plugin.js.map +1 -1
  22. package/dist/auth/plugin.d.ts +1 -1
  23. package/dist/authorization/types.d.ts +1 -1
  24. package/dist/branch-registry.js +1 -1
  25. package/dist/branch-registry.js.map +1 -1
  26. package/dist/branch-schema-cache.d.ts +8 -13
  27. package/dist/branch-schema-cache.d.ts.map +1 -1
  28. package/dist/branch-schema-cache.js +55 -44
  29. package/dist/branch-schema-cache.js.map +1 -1
  30. package/dist/branch-workspace.d.ts +3 -0
  31. package/dist/branch-workspace.d.ts.map +1 -1
  32. package/dist/branch-workspace.js +20 -0
  33. package/dist/branch-workspace.js.map +1 -1
  34. package/dist/cli/cli.d.ts +20 -0
  35. package/dist/cli/cli.d.ts.map +1 -0
  36. package/dist/cli/cli.js +196 -0
  37. package/dist/cli/cli.js.map +1 -0
  38. package/dist/cli/generate-ai-content.js +1501 -723
  39. package/dist/cli/init.d.ts +2 -3
  40. package/dist/cli/init.d.ts.map +1 -1
  41. package/dist/cli/init.js +258 -2861
  42. package/dist/cli/init.js.map +1 -1
  43. package/dist/cli/sync.d.ts +33 -0
  44. package/dist/cli/sync.d.ts.map +1 -0
  45. package/dist/cli/sync.js +510 -0
  46. package/dist/cli/sync.js.map +1 -0
  47. package/dist/config/schemas/config.d.ts +5 -5
  48. package/dist/config/schemas/config.d.ts.map +1 -1
  49. package/dist/config/schemas/config.js +1 -1
  50. package/dist/config/schemas/config.js.map +1 -1
  51. package/dist/config-test.d.ts.map +1 -1
  52. package/dist/config-test.js +0 -1
  53. package/dist/config-test.js.map +1 -1
  54. package/dist/content-reader.js +1 -1
  55. package/dist/content-reader.js.map +1 -1
  56. package/dist/editor/BranchManager.d.ts.map +1 -1
  57. package/dist/editor/BranchManager.js +1 -3
  58. package/dist/editor/BranchManager.js.map +1 -1
  59. package/dist/git-manager.d.ts +2 -3
  60. package/dist/git-manager.d.ts.map +1 -1
  61. package/dist/git-manager.js +12 -4
  62. package/dist/git-manager.js.map +1 -1
  63. package/dist/operating-mode/client-safe-strategy.d.ts +1 -12
  64. package/dist/operating-mode/client-safe-strategy.d.ts.map +1 -1
  65. package/dist/operating-mode/client-safe-strategy.js +5 -42
  66. package/dist/operating-mode/client-safe-strategy.js.map +1 -1
  67. package/dist/operating-mode/client-unsafe-strategy.d.ts.map +1 -1
  68. package/dist/operating-mode/client-unsafe-strategy.js +10 -68
  69. package/dist/operating-mode/client-unsafe-strategy.js.map +1 -1
  70. package/dist/operating-mode/index.d.ts +3 -3
  71. package/dist/operating-mode/index.d.ts.map +1 -1
  72. package/dist/operating-mode/index.js +2 -2
  73. package/dist/operating-mode/types.d.ts +2 -6
  74. package/dist/operating-mode/types.d.ts.map +1 -1
  75. package/dist/services.d.ts +6 -0
  76. package/dist/services.d.ts.map +1 -1
  77. package/dist/services.js +52 -40
  78. package/dist/services.js.map +1 -1
  79. package/dist/settings-branch-utils.d.ts +2 -2
  80. package/dist/settings-branch-utils.js +3 -3
  81. package/dist/settings-branch-utils.js.map +1 -1
  82. package/dist/settings-workspace.d.ts +1 -2
  83. package/dist/settings-workspace.d.ts.map +1 -1
  84. package/dist/settings-workspace.js +1 -2
  85. package/dist/settings-workspace.js.map +1 -1
  86. package/dist/utils/fs.d.ts +3 -0
  87. package/dist/utils/fs.d.ts.map +1 -0
  88. package/dist/utils/fs.js +15 -0
  89. package/dist/utils/fs.js.map +1 -0
  90. package/dist/utils/git.d.ts +7 -0
  91. package/dist/utils/git.d.ts.map +1 -0
  92. package/dist/utils/git.js +17 -0
  93. package/dist/utils/git.js.map +1 -0
  94. package/dist/worker/task-queue-config.d.ts +2 -4
  95. package/dist/worker/task-queue-config.d.ts.map +1 -1
  96. package/dist/worker/task-queue-config.js +3 -7
  97. package/dist/worker/task-queue-config.js.map +1 -1
  98. 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,13 +974,13 @@ 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;
@@ -315,7 +988,7 @@ async function resolveCollectionPath(root, logicalPath) {
315
988
  return logicalName === segment;
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
  }
@@ -362,617 +1035,25 @@ function extractSlugFromFilename(filename, entryTypeName) {
362
1035
  if (parts.length > 1) {
363
1036
  return parts.slice(0, -1).join(".");
364
1037
  }
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
- }
970
- }
971
- strategyCache.set(mode, strategy);
972
- return strategy;
1038
+ return filename;
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
  }
@@ -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;
@@ -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
  }
@@ -1358,7 +1444,7 @@ var ContentStore = class {
1358
1444
  }
1359
1445
  const { absolutePath: currentPath, relativePath: currentRelPath } = await this.buildPaths(collection, currentSlug);
1360
1446
  try {
1361
- await fs3.access(currentPath);
1447
+ await fs4.access(currentPath);
1362
1448
  } catch {
1363
1449
  throw new ContentStoreError(`Entry not found: ${currentSlug}`);
1364
1450
  }
@@ -1377,7 +1463,7 @@ var ContentStore = class {
1377
1463
  const parentDir = path5.dirname(currentPath);
1378
1464
  const newPath = path5.join(parentDir, newFilename);
1379
1465
  try {
1380
- const entries = await fs3.readdir(parentDir, { withFileTypes: true });
1466
+ const entries = await fs4.readdir(parentDir, { withFileTypes: true });
1381
1467
  for (const entry of entries) {
1382
1468
  if (entry.isDirectory())
1383
1469
  continue;
@@ -1391,7 +1477,7 @@ var ContentStore = class {
1391
1477
  throw err;
1392
1478
  }
1393
1479
  }
1394
- await fs3.rename(currentPath, newPath);
1480
+ await fs4.rename(currentPath, newPath);
1395
1481
  const newRelativePath = path5.relative(this.root, newPath);
1396
1482
  const entryId = idIndex.findByPath(currentRelPath);
1397
1483
  if (entryId) {
@@ -1509,11 +1595,11 @@ var ContentStore = class {
1509
1595
  };
1510
1596
 
1511
1597
  // dist/branch-schema-cache.js
1512
- import fs5 from "node:fs/promises";
1598
+ import fs6 from "node:fs/promises";
1513
1599
  import path6 from "node:path";
1514
1600
 
1515
1601
  // dist/schema/meta-loader.js
1516
- import { promises as fs4 } from "fs";
1602
+ import { promises as fs5 } from "fs";
1517
1603
  import { join as join2 } from "pathe";
1518
1604
  import { z as z6 } from "zod";
1519
1605
  import chokidar from "chokidar";
@@ -1547,7 +1633,7 @@ function stripEmbeddedIdFromName(name) {
1547
1633
  async function scanForCollectionMeta(baseDir, relativePath = "") {
1548
1634
  const collections = [];
1549
1635
  try {
1550
- const entries = await fs4.readdir(baseDir, { withFileTypes: true });
1636
+ const entries = await fs5.readdir(baseDir, { withFileTypes: true });
1551
1637
  for (const entry of entries) {
1552
1638
  if (!entry.isDirectory())
1553
1639
  continue;
@@ -1558,8 +1644,8 @@ async function scanForCollectionMeta(baseDir, relativePath = "") {
1558
1644
  const absolutePath = join2(baseDir, folderName);
1559
1645
  const metaPath = join2(absolutePath, ".collection.json");
1560
1646
  try {
1561
- await fs4.access(metaPath);
1562
- const content = await fs4.readFile(metaPath, "utf-8");
1647
+ await fs5.access(metaPath);
1648
+ const content = await fs5.readFile(metaPath, "utf-8");
1563
1649
  const parsed = JSON.parse(content);
1564
1650
  const meta = collectionMetaSchema.parse(parsed);
1565
1651
  collections.push({
@@ -1591,7 +1677,7 @@ async function loadCollectionMetaFiles(contentRoot) {
1591
1677
  let root = null;
1592
1678
  const rootMetaPath = join2(contentRoot, ".collection.json");
1593
1679
  try {
1594
- await fs4.access(rootMetaPath);
1680
+ await fs5.access(rootMetaPath);
1595
1681
  } catch (err) {
1596
1682
  if (err.code === "ENOENT") {
1597
1683
  } else {
@@ -1599,7 +1685,7 @@ async function loadCollectionMetaFiles(contentRoot) {
1599
1685
  }
1600
1686
  }
1601
1687
  try {
1602
- const content = await fs4.readFile(rootMetaPath, "utf-8");
1688
+ const content = await fs5.readFile(rootMetaPath, "utf-8");
1603
1689
  const parsed = JSON.parse(content);
1604
1690
  root = rootCollectionMetaSchema.parse(parsed);
1605
1691
  } catch (err) {
@@ -1690,35 +1776,44 @@ function isValidSchema(schema) {
1690
1776
  }
1691
1777
 
1692
1778
  // dist/branch-schema-cache.js
1779
+ init_flatten();
1693
1780
  var SCHEMA_CACHE_VERSION = 2;
1781
+ var MTIME_CHECK_DEBOUNCE_MS = 1e3;
1782
+ async function isStaleByMtime(dir, cachedAt) {
1783
+ let entries;
1784
+ try {
1785
+ entries = await fs6.readdir(dir, { recursive: true, encoding: "utf-8" });
1786
+ } catch {
1787
+ return true;
1788
+ }
1789
+ for (const entry of entries) {
1790
+ if (!entry.endsWith(".collection.json"))
1791
+ continue;
1792
+ const full = path6.join(dir, entry);
1793
+ try {
1794
+ const stat = await fs6.stat(full);
1795
+ if (stat.mtimeMs > cachedAt.getTime())
1796
+ return true;
1797
+ } catch {
1798
+ return true;
1799
+ }
1800
+ }
1801
+ return false;
1802
+ }
1694
1803
  var BranchSchemaCache = class {
1695
- constructor(mode) {
1696
- this.mode = mode;
1804
+ constructor(mode = "prod") {
1805
+ this.lastMtimeCheck = /* @__PURE__ */ new Map();
1806
+ this.devMode = mode === "dev";
1697
1807
  }
1698
1808
  /**
1699
1809
  * Get schema for a branch (loads from cache or resolves fresh).
1700
1810
  *
1701
- * @param branchRoot - Root directory of the branch (e.g., .canopy-prod-sim/content-branches/main)
1811
+ * @param branchRoot - Root directory of the branch (e.g., .canopy-dev/content-branches/main)
1702
1812
  * @param entrySchemaRegistry - Map of schema names to field definitions
1703
1813
  * @param contentRootName - Name of content directory (e.g., "content") from config
1704
1814
  * @returns Resolved schema tree and flattened schema
1705
1815
  */
1706
1816
  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
1817
  return this.loadFromCacheOrResolve(branchRoot, entrySchemaRegistry, contentRootName);
1723
1818
  }
1724
1819
  /**
@@ -1731,23 +1826,32 @@ var BranchSchemaCache = class {
1731
1826
  const stalePath = path6.join(cacheDir, "schema-cache.stale");
1732
1827
  let cacheData = null;
1733
1828
  try {
1734
- const staleExists = await fs5.access(stalePath).then(() => true).catch(() => false);
1829
+ const staleExists = await fs6.access(stalePath).then(() => true).catch(() => false);
1735
1830
  if (!staleExists) {
1736
- const cacheContent = await fs5.readFile(cachePath, "utf-8");
1831
+ const cacheContent = await fs6.readFile(cachePath, "utf-8");
1737
1832
  cacheData = JSON.parse(cacheContent);
1738
1833
  }
1739
1834
  } catch {
1740
1835
  cacheData = null;
1741
1836
  }
1742
1837
  if (cacheData && cacheData.version === SCHEMA_CACHE_VERSION) {
1743
- return { schema: cacheData.schema, flatSchema: cacheData.flatSchema };
1838
+ const now = Date.now();
1839
+ const lastCheck = this.lastMtimeCheck.get(contentRoot) ?? 0;
1840
+ if (this.devMode && now - lastCheck >= MTIME_CHECK_DEBOUNCE_MS && await isStaleByMtime(contentRoot, new Date(cacheData.cachedAt))) {
1841
+ this.lastMtimeCheck.set(contentRoot, now);
1842
+ cacheData = null;
1843
+ } else {
1844
+ if (this.devMode)
1845
+ this.lastMtimeCheck.set(contentRoot, now);
1846
+ return { schema: cacheData.schema, flatSchema: cacheData.flatSchema };
1847
+ }
1744
1848
  }
1745
1849
  const result = await resolveSchema(contentRoot, entrySchemaRegistry);
1746
1850
  if (!isValidSchema(result.schema)) {
1747
1851
  throw new Error(`No schema found in ${contentRoot}. Create .collection.json files with references to field schemas defined in your entry schema registry.`);
1748
1852
  }
1749
1853
  const flatSchema = flattenSchema(result.schema, contentRootName);
1750
- await fs5.mkdir(cacheDir, { recursive: true });
1854
+ await fs6.mkdir(cacheDir, { recursive: true });
1751
1855
  const newCache = {
1752
1856
  version: SCHEMA_CACHE_VERSION,
1753
1857
  schema: result.schema,
@@ -1755,10 +1859,10 @@ var BranchSchemaCache = class {
1755
1859
  cachedAt: (/* @__PURE__ */ new Date()).toISOString()
1756
1860
  };
1757
1861
  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);
1862
+ await fs6.writeFile(tmpPath, JSON.stringify(newCache, null, 2), "utf-8");
1863
+ await fs6.rename(tmpPath, cachePath);
1760
1864
  try {
1761
- await fs5.unlink(stalePath);
1865
+ await fs6.unlink(stalePath);
1762
1866
  } catch {
1763
1867
  }
1764
1868
  return { schema: result.schema, flatSchema };
@@ -1769,24 +1873,10 @@ var BranchSchemaCache = class {
1769
1873
  * @param branchRoot - Root directory of the branch
1770
1874
  */
1771
1875
  async invalidate(branchRoot) {
1772
- if (this.mode === "dev") {
1773
- this.devModeCache = void 0;
1774
- return;
1775
- }
1776
1876
  const cacheDir = path6.join(branchRoot, ".canopy-meta");
1777
1877
  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
- }
1878
+ await fs6.mkdir(cacheDir, { recursive: true });
1879
+ await fs6.writeFile(stalePath, "", "utf-8");
1790
1880
  }
1791
1881
  };
1792
1882
 
@@ -2287,11 +2377,11 @@ function matchesBundleFilter(entry, filter, contentRoot) {
2287
2377
 
2288
2378
  // dist/branch-metadata.js
2289
2379
  import { randomUUID } from "node:crypto";
2290
- import fs7 from "node:fs/promises";
2380
+ import fs8 from "node:fs/promises";
2291
2381
  import path9 from "node:path";
2292
2382
 
2293
2383
  // dist/branch-registry.js
2294
- import fs6 from "node:fs/promises";
2384
+ import fs7 from "node:fs/promises";
2295
2385
  import path8 from "node:path";
2296
2386
  var REGISTRY_FILE = "branches.json";
2297
2387
  var REGISTRY_STALE_FILE = "branches.stale.json";
@@ -2309,7 +2399,7 @@ var BranchRegistry = class {
2309
2399
  */
2310
2400
  async list() {
2311
2401
  try {
2312
- const raw = await fs6.readFile(this.registryPath, "utf8");
2402
+ const raw = await fs7.readFile(this.registryPath, "utf8");
2313
2403
  const parsed = JSON.parse(raw);
2314
2404
  if (!parsed.version || !Array.isArray(parsed.branches)) {
2315
2405
  return await this.regenerate();
@@ -2335,7 +2425,7 @@ var BranchRegistry = class {
2335
2425
  */
2336
2426
  async invalidate() {
2337
2427
  try {
2338
- await fs6.rename(this.registryPath, this.stalePath);
2428
+ await fs7.rename(this.registryPath, this.stalePath);
2339
2429
  } catch (err) {
2340
2430
  if (!isNotFoundError(err)) {
2341
2431
  throw err;
@@ -2349,20 +2439,20 @@ var BranchRegistry = class {
2349
2439
  async regenerate() {
2350
2440
  const branches = await this.scanBranchDirectories();
2351
2441
  const uniqueTempPath = `${this.tempPath}.${Date.now()}.${Math.random().toString(36).slice(2)}`;
2352
- await fs6.mkdir(this.root, { recursive: true });
2442
+ await fs7.mkdir(this.root, { recursive: true });
2353
2443
  const snapshot = {
2354
2444
  version: REGISTRY_VERSION,
2355
2445
  branches
2356
2446
  };
2357
- await fs6.writeFile(uniqueTempPath, JSON.stringify(snapshot, null, 2) + "\n", "utf8");
2447
+ await fs7.writeFile(uniqueTempPath, JSON.stringify(snapshot, null, 2) + "\n", "utf8");
2358
2448
  try {
2359
- await fs6.rename(uniqueTempPath, this.registryPath);
2449
+ await fs7.rename(uniqueTempPath, this.registryPath);
2360
2450
  } catch (err) {
2361
- await fs6.unlink(uniqueTempPath).catch(() => {
2451
+ await fs7.unlink(uniqueTempPath).catch(() => {
2362
2452
  });
2363
2453
  throw err;
2364
2454
  }
2365
- await fs6.unlink(this.stalePath).catch(() => {
2455
+ await fs7.unlink(this.stalePath).catch(() => {
2366
2456
  });
2367
2457
  return branches;
2368
2458
  }
@@ -2372,7 +2462,7 @@ var BranchRegistry = class {
2372
2462
  async scanBranchDirectories() {
2373
2463
  const branches = [];
2374
2464
  try {
2375
- const entries = await fs6.readdir(this.root, { withFileTypes: true });
2465
+ const entries = await fs7.readdir(this.root, { withFileTypes: true });
2376
2466
  for (const entry of entries) {
2377
2467
  if (!entry.isDirectory() || entry.name.startsWith(".")) {
2378
2468
  continue;
@@ -2437,7 +2527,7 @@ var BranchMetadataFileManager = class _BranchMetadataFileManager {
2437
2527
  static async loadOnly(branchRoot) {
2438
2528
  const filePath = path9.join(path9.resolve(branchRoot), BRANCH_META_DIR, BRANCH_META_FILE);
2439
2529
  try {
2440
- const raw = await fs7.readFile(filePath, "utf8");
2530
+ const raw = await fs8.readFile(filePath, "utf8");
2441
2531
  return JSON.parse(raw);
2442
2532
  } catch (err) {
2443
2533
  if (isNotFoundError(err)) {
@@ -2455,7 +2545,7 @@ var BranchMetadataFileManager = class _BranchMetadataFileManager {
2455
2545
  }
2456
2546
  async load() {
2457
2547
  try {
2458
- const raw = await fs7.readFile(this.filePath, "utf8");
2548
+ const raw = await fs8.readFile(this.filePath, "utf8");
2459
2549
  const parsed = JSON.parse(raw);
2460
2550
  const version = parsed.version ?? 0;
2461
2551
  return { meta: parsed, version };
@@ -2479,11 +2569,11 @@ var BranchMetadataFileManager = class _BranchMetadataFileManager {
2479
2569
  version: newVersion,
2480
2570
  writeId
2481
2571
  };
2482
- await fs7.mkdir(path9.dirname(this.filePath), { recursive: true });
2572
+ await fs8.mkdir(path9.dirname(this.filePath), { recursive: true });
2483
2573
  const content = JSON.stringify(payload, null, 2) + "\n";
2484
2574
  if (expectedVersion === null) {
2485
2575
  try {
2486
- await fs7.writeFile(this.filePath, content, { flag: "wx" });
2576
+ await fs8.writeFile(this.filePath, content, { flag: "wx" });
2487
2577
  return { version: newVersion, writeId };
2488
2578
  } catch (err) {
2489
2579
  if (isFileExistsError(err)) {
@@ -2493,11 +2583,11 @@ var BranchMetadataFileManager = class _BranchMetadataFileManager {
2493
2583
  }
2494
2584
  }
2495
2585
  const tempPath = `${this.filePath}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
2496
- await fs7.writeFile(tempPath, content, "utf-8");
2586
+ await fs8.writeFile(tempPath, content, "utf-8");
2497
2587
  try {
2498
2588
  let currentVersion = null;
2499
2589
  try {
2500
- const current = JSON.parse(await fs7.readFile(this.filePath, "utf-8"));
2590
+ const current = JSON.parse(await fs8.readFile(this.filePath, "utf-8"));
2501
2591
  currentVersion = current.version ?? 0;
2502
2592
  } catch {
2503
2593
  currentVersion = null;
@@ -2505,13 +2595,13 @@ var BranchMetadataFileManager = class _BranchMetadataFileManager {
2505
2595
  if (currentVersion !== expectedVersion) {
2506
2596
  throw new BranchMetadataConflictError();
2507
2597
  }
2508
- await fs7.rename(tempPath, this.filePath);
2509
- const afterWrite = JSON.parse(await fs7.readFile(this.filePath, "utf-8"));
2598
+ await fs8.rename(tempPath, this.filePath);
2599
+ const afterWrite = JSON.parse(await fs8.readFile(this.filePath, "utf-8"));
2510
2600
  if (afterWrite.writeId !== writeId) {
2511
2601
  throw new BranchMetadataConflictError();
2512
2602
  }
2513
2603
  } catch (err) {
2514
- await fs7.unlink(tempPath).catch(() => {
2604
+ await fs8.unlink(tempPath).catch(() => {
2515
2605
  });
2516
2606
  throw err;
2517
2607
  }
@@ -2576,6 +2666,9 @@ var BranchMetadataFileManager = class _BranchMetadataFileManager {
2576
2666
  await registry.invalidate();
2577
2667
  }
2578
2668
  };
2669
+ var getBranchMetadataFileManager = (branchRoot, baseRoot) => {
2670
+ return BranchMetadataFileManager.get(branchRoot, baseRoot);
2671
+ };
2579
2672
  var loadBranchContext = async (options) => {
2580
2673
  const { branchRoot, baseRoot } = resolveBranchPath({
2581
2674
  branchName: options.branchName,
@@ -2605,19 +2698,704 @@ var STATIC_DEPLOY_USER = Object.freeze({
2605
2698
  name: "Static Deploy"
2606
2699
  });
2607
2700
 
2701
+ // dist/git-manager.js
2702
+ import fs9 from "node:fs/promises";
2703
+ import path10 from "node:path";
2704
+ import { simpleGit as simpleGit2 } from "simple-git";
2705
+
2706
+ // dist/utils/debug.js
2707
+ var LOG_LEVELS = {
2708
+ DEBUG: 0,
2709
+ INFO: 1,
2710
+ WARN: 2,
2711
+ ERROR: 3
2712
+ };
2713
+ var DebugLogger = class {
2714
+ constructor(options = {}) {
2715
+ this.timers = /* @__PURE__ */ new Map();
2716
+ this.options = options;
2717
+ }
2718
+ shouldLog(level) {
2719
+ const enabled = this.options.enabled ?? process.env.CANOPYCMS_DEBUG === "true";
2720
+ if (!enabled)
2721
+ return false;
2722
+ const minLevel = this.options.minLevel ?? "DEBUG";
2723
+ return LOG_LEVELS[level] >= LOG_LEVELS[minLevel];
2724
+ }
2725
+ formatMessage(level, category, message) {
2726
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
2727
+ const prefix = this.options.prefix ?? "CanopyCMS";
2728
+ return `[${timestamp}] [${prefix}:${category}] [${level}] ${message}`;
2729
+ }
2730
+ debug(category, message, data) {
2731
+ if (this.shouldLog("DEBUG")) {
2732
+ console.log(this.formatMessage("DEBUG", category, message), data ?? "");
2733
+ }
2734
+ }
2735
+ info(category, message, data) {
2736
+ if (this.shouldLog("INFO")) {
2737
+ console.log(this.formatMessage("INFO", category, message), data ?? "");
2738
+ }
2739
+ }
2740
+ warn(category, message, data) {
2741
+ if (this.shouldLog("WARN")) {
2742
+ console.warn(this.formatMessage("WARN", category, message), data ?? "");
2743
+ }
2744
+ }
2745
+ error(category, message, data) {
2746
+ const msg = this.formatMessage("ERROR", category, message);
2747
+ if (this.shouldLog("ERROR")) {
2748
+ console.error(msg, data ?? "");
2749
+ }
2750
+ const throwOnError = this.options.throwOnError ?? false;
2751
+ if (throwOnError) {
2752
+ const errorMsg = data ? `${message}: ${JSON.stringify(data)}` : message;
2753
+ throw new Error(errorMsg);
2754
+ }
2755
+ }
2756
+ /**
2757
+ * Start timing an operation
2758
+ */
2759
+ time(label) {
2760
+ this.timers.set(label, Date.now());
2761
+ }
2762
+ /**
2763
+ * End timing an operation and log the duration
2764
+ */
2765
+ timeEnd(category, label) {
2766
+ const start = this.timers.get(label);
2767
+ if (start === void 0) {
2768
+ this.warn(category, `Timer '${label}' does not exist`);
2769
+ return;
2770
+ }
2771
+ const duration = Date.now() - start;
2772
+ this.timers.delete(label);
2773
+ this.debug(category, `${label} completed`, { durationMs: duration });
2774
+ return duration;
2775
+ }
2776
+ /**
2777
+ * Wrap an async function with automatic timing
2778
+ */
2779
+ async timed(category, label, fn) {
2780
+ this.time(label);
2781
+ try {
2782
+ return await fn();
2783
+ } finally {
2784
+ this.timeEnd(category, label);
2785
+ }
2786
+ }
2787
+ };
2788
+ function createDebugLogger(options) {
2789
+ return new DebugLogger(options);
2790
+ }
2791
+ var testLogger = createDebugLogger({
2792
+ enabled: process.env.E2E_DEBUG === "true",
2793
+ prefix: "E2E",
2794
+ throwOnError: false
2795
+ });
2796
+
2797
+ // dist/utils/git.js
2798
+ import { simpleGit } from "simple-git";
2799
+ async function detectHeadBranch(repoRoot, fallback = "main") {
2800
+ try {
2801
+ const git = simpleGit({ baseDir: repoRoot });
2802
+ const head = (await git.revparse(["--abbrev-ref", "HEAD"])).trim();
2803
+ return head && head !== "HEAD" ? head : fallback;
2804
+ } catch {
2805
+ return fallback;
2806
+ }
2807
+ }
2808
+
2809
+ // dist/git-manager.js
2810
+ var log = createDebugLogger({ prefix: "GitManager" });
2811
+ var remoteInitLocks = /* @__PURE__ */ new Map();
2812
+ var GitManager = class _GitManager {
2813
+ constructor(options, gitOptions) {
2814
+ this.repoPath = path10.resolve(options.repoPath);
2815
+ this.baseBranch = options.baseBranch ?? "main";
2816
+ this.remote = options.remote ?? "origin";
2817
+ this.git = simpleGit2({ baseDir: this.repoPath, ...gitOptions });
2818
+ }
2819
+ static async cloneRepo(remoteUrl, targetPath, baseBranch = "main") {
2820
+ log.debug("git", "Cloning repository", {
2821
+ remoteUrl,
2822
+ targetPath,
2823
+ baseBranch
2824
+ });
2825
+ const git = simpleGit2();
2826
+ await git.clone(remoteUrl, targetPath, ["--branch", baseBranch, "--single-branch"]);
2827
+ log.debug("git", "Clone complete");
2828
+ }
2829
+ /**
2830
+ * Initializes a local bare git repository to simulate a remote for dev mode.
2831
+ *
2832
+ * This is idempotent - if the remote already exists, it will not be recreated.
2833
+ *
2834
+ * The remote is seeded with the current state of the baseBranch (e.g., 'main').
2835
+ * If you need to change the baseBranch or reset the simulation, delete
2836
+ * `.canopycms/remote.git` and `.canopycms/branches` and restart.
2837
+ *
2838
+ * @throws Error if not a git repo, no commits, or baseBranch doesn't exist
2839
+ */
2840
+ static async ensureLocalSimulatedRemote(options) {
2841
+ const existingLock = remoteInitLocks.get(options.remotePath);
2842
+ if (existingLock) {
2843
+ log.debug("git", "Waiting for existing remote initialization", {
2844
+ remotePath: options.remotePath
2845
+ });
2846
+ await existingLock;
2847
+ try {
2848
+ const stat = await fs9.stat(options.remotePath);
2849
+ if (stat.isDirectory()) {
2850
+ log.debug("git", "Remote exists after waiting for lock");
2851
+ return;
2852
+ }
2853
+ } catch (err) {
2854
+ if (!isNotFoundError(err))
2855
+ throw err;
2856
+ log.debug("git", "Remote does not exist after lock, will retry initialization");
2857
+ }
2858
+ }
2859
+ const lockPromise = log.timed("git", "ensureLocalSimulatedRemote", async () => {
2860
+ try {
2861
+ log.debug("git", "Initializing local simulated remote", {
2862
+ remotePath: options.remotePath,
2863
+ baseBranch: options.baseBranch
2864
+ });
2865
+ try {
2866
+ const stat = await fs9.stat(options.remotePath);
2867
+ if (stat.isDirectory()) {
2868
+ log.debug("git", "Remote already exists, skipping");
2869
+ return;
2870
+ }
2871
+ } catch (err) {
2872
+ if (!isNotFoundError(err))
2873
+ throw err;
2874
+ }
2875
+ let gitRoot = options.sourcePath;
2876
+ try {
2877
+ const sourceGit2 = simpleGit2({ baseDir: options.sourcePath });
2878
+ const result = await sourceGit2.raw(["rev-parse", "--show-toplevel"]);
2879
+ gitRoot = result.trim();
2880
+ } catch {
2881
+ gitRoot = options.sourcePath;
2882
+ }
2883
+ const sourceGit = simpleGit2({ baseDir: gitRoot });
2884
+ try {
2885
+ await sourceGit.status();
2886
+ } catch {
2887
+ throw new Error("Cannot initialize local simulated remote: current directory is not a git repository. Please initialize git or provide an explicit remoteUrl.");
2888
+ }
2889
+ let hasCommits = false;
2890
+ try {
2891
+ const log3 = await sourceGit.log(["-1"]);
2892
+ hasCommits = log3.total > 0;
2893
+ } catch {
2894
+ hasCommits = false;
2895
+ }
2896
+ if (!hasCommits) {
2897
+ throw new Error("Cannot initialize local simulated remote: repository has no commits. Please make an initial commit or provide an explicit remoteUrl.");
2898
+ }
2899
+ const branches = await sourceGit.branchLocal();
2900
+ if (!branches.all.includes(options.baseBranch)) {
2901
+ 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.`);
2902
+ }
2903
+ log.debug("git", "Creating bare remote repository");
2904
+ await fs9.mkdir(path10.dirname(options.remotePath), { recursive: true });
2905
+ await simpleGit2().raw([
2906
+ "init",
2907
+ "--bare",
2908
+ `--initial-branch=${options.baseBranch}`,
2909
+ options.remotePath
2910
+ ]);
2911
+ const tempRemoteName = `__canopycms_init_${Date.now()}__`;
2912
+ try {
2913
+ await sourceGit.addRemote(tempRemoteName, options.remotePath);
2914
+ if (options.subdirectory) {
2915
+ const splitBranch = `__canopycms_split_${Date.now()}__`;
2916
+ try {
2917
+ await sourceGit.raw([
2918
+ "subtree",
2919
+ "split",
2920
+ "--prefix",
2921
+ options.subdirectory,
2922
+ "-b",
2923
+ splitBranch
2924
+ ]);
2925
+ await sourceGit.push(tempRemoteName, `${splitBranch}:${options.baseBranch}`);
2926
+ await sourceGit.raw(["branch", "-D", splitBranch]);
2927
+ } catch (err) {
2928
+ try {
2929
+ await sourceGit.raw(["branch", "-D", splitBranch]);
2930
+ } catch {
2931
+ }
2932
+ throw err;
2933
+ }
2934
+ } else {
2935
+ await sourceGit.push(tempRemoteName, `${options.baseBranch}:${options.baseBranch}`);
2936
+ }
2937
+ } finally {
2938
+ try {
2939
+ await sourceGit.removeRemote(tempRemoteName);
2940
+ } catch {
2941
+ }
2942
+ }
2943
+ log.debug("git", "Remote initialization complete");
2944
+ } finally {
2945
+ remoteInitLocks.delete(options.remotePath);
2946
+ }
2947
+ });
2948
+ remoteInitLocks.set(options.remotePath, lockPromise);
2949
+ await lockPromise;
2950
+ }
2951
+ /**
2952
+ * Find the git root directory
2953
+ * @returns Path to git root, or cwd if not in a git repo
2954
+ */
2955
+ static async findGitRoot() {
2956
+ let gitRoot = process.cwd();
2957
+ try {
2958
+ const git = simpleGit2({ baseDir: process.cwd() });
2959
+ const result = await git.raw(["rev-parse", "--show-toplevel"]);
2960
+ gitRoot = result.trim();
2961
+ } catch {
2962
+ }
2963
+ return gitRoot;
2964
+ }
2965
+ /**
2966
+ * Validate that a git repository exists at the given path
2967
+ * @param repoPath - Path to check for .git directory
2968
+ * @throws Error if git repo doesn't exist
2969
+ */
2970
+ static async validateGitRepoExists(repoPath) {
2971
+ try {
2972
+ const stat = await fs9.stat(path10.join(repoPath, ".git"));
2973
+ if (!stat.isDirectory()) {
2974
+ throw new Error(`Expected git repo at ${repoPath}`);
2975
+ }
2976
+ } catch (err) {
2977
+ if (isNotFoundError(err)) {
2978
+ throw new Error(`Expected git repo at ${repoPath}`);
2979
+ }
2980
+ throw err;
2981
+ }
2982
+ }
2983
+ /**
2984
+ * Resolves the remote URL for git operations following the priority:
2985
+ * 1. Explicit remoteUrl parameter
2986
+ * 2. Config defaultRemoteUrl
2987
+ * 3. Environment variable (mode-specific)
2988
+ * 4. Auto-initialized local remote (for dev mode)
2989
+ *
2990
+ * Uses strategy flags to determine behavior, GitManager executes the logic.
2991
+ *
2992
+ * @param options.sourceRoot - Optional source directory for monorepos. When provided,
2993
+ * this directory (relative to git root) is used as the source for the simulated remote.
2994
+ * Defaults to process.cwd().
2995
+ *
2996
+ * @returns Remote URL or undefined if no remote is needed
2997
+ */
2998
+ static async resolveRemoteUrl(options) {
2999
+ const { operatingStrategy: operatingStrategy2 } = await Promise.resolve().then(() => (init_operating_mode(), operating_mode_exports));
3000
+ const strategy = operatingStrategy2(options.mode);
3001
+ const config = strategy.getRemoteUrlConfig();
3002
+ if (options.remoteUrl)
3003
+ return options.remoteUrl;
3004
+ if (options.defaultRemoteUrl)
3005
+ return options.defaultRemoteUrl;
3006
+ if (process.env[config.envVarName])
3007
+ return process.env[config.envVarName];
3008
+ if (config.autoDetectRemotePath) {
3009
+ try {
3010
+ const stat = await fs9.stat(config.autoDetectRemotePath);
3011
+ if (stat.isDirectory()) {
3012
+ log.debug("git", "Auto-detected local remote", {
3013
+ path: config.autoDetectRemotePath
3014
+ });
3015
+ return config.autoDetectRemotePath;
3016
+ }
3017
+ } catch {
3018
+ }
3019
+ }
3020
+ if (config.shouldAutoInitLocal) {
3021
+ const gitRoot = await this.findGitRoot();
3022
+ const sourceRoot = options.sourceRoot;
3023
+ const sourcePath = sourceRoot ? path10.resolve(gitRoot, sourceRoot) : gitRoot;
3024
+ const localRemotePath = path10.join(sourcePath, config.defaultRemotePath);
3025
+ await this.ensureLocalSimulatedRemote({
3026
+ remotePath: localRemotePath,
3027
+ sourcePath: gitRoot,
3028
+ baseBranch: options.baseBranch,
3029
+ subdirectory: sourceRoot
3030
+ });
3031
+ return localRemotePath;
3032
+ }
3033
+ return void 0;
3034
+ }
3035
+ /**
3036
+ * Ensures a git workspace is initialized and ready for use.
3037
+ * Handles cloning, remote configuration, and branch checkout/creation.
3038
+ *
3039
+ * This centralizes the common initialization sequence used by both BranchWorkspaceManager
3040
+ * and SettingsWorkspaceManager.
3041
+ *
3042
+ * Note: Does NOT configure git author - that should be done before commits, not during init.
3043
+ *
3044
+ * @returns Configured GitManager instance for the workspace
3045
+ */
3046
+ static async initializeWorkspace(options) {
3047
+ let baseBranch = options.baseBranch;
3048
+ if (!baseBranch && options.mode === "dev") {
3049
+ const sourceRoot = options.sourceRoot ? path10.resolve(process.cwd(), options.sourceRoot) : process.cwd();
3050
+ baseBranch = await detectHeadBranch(sourceRoot);
3051
+ }
3052
+ baseBranch = baseBranch ?? "main";
3053
+ const remoteName = options.remoteName ?? "origin";
3054
+ let repoExists = false;
3055
+ try {
3056
+ const stat = await fs9.stat(path10.join(options.workspacePath, ".git"));
3057
+ repoExists = stat.isDirectory();
3058
+ } catch (err) {
3059
+ if (!isNotFoundError(err))
3060
+ throw err;
3061
+ }
3062
+ let justCloned = false;
3063
+ if (!repoExists) {
3064
+ const remoteUrl = await _GitManager.resolveRemoteUrl({
3065
+ mode: options.mode,
3066
+ remoteUrl: options.remoteUrl,
3067
+ defaultRemoteUrl: options.defaultRemoteUrl,
3068
+ baseBranch,
3069
+ sourceRoot: options.sourceRoot
3070
+ });
3071
+ if (!remoteUrl) {
3072
+ throw new Error("CanopyCMS: defaultRemoteUrl (or CANOPYCMS_REMOTE_URL) is required to initialize workspace");
3073
+ }
3074
+ await _GitManager.cloneRepo(remoteUrl, options.workspacePath, baseBranch);
3075
+ justCloned = true;
3076
+ }
3077
+ const git = new _GitManager({
3078
+ repoPath: options.workspacePath,
3079
+ baseBranch,
3080
+ remote: remoteName
3081
+ });
3082
+ if (!justCloned) {
3083
+ const remoteUrl = await _GitManager.resolveRemoteUrl({
3084
+ mode: options.mode,
3085
+ remoteUrl: options.remoteUrl,
3086
+ defaultRemoteUrl: options.defaultRemoteUrl,
3087
+ baseBranch,
3088
+ sourceRoot: options.sourceRoot
3089
+ });
3090
+ if (remoteUrl) {
3091
+ await git.ensureRemote(remoteUrl);
3092
+ }
3093
+ }
3094
+ if (options.branchType === "orphan") {
3095
+ await git.createOrphanSettingsBranch(options.branchName, {});
3096
+ } else {
3097
+ await git.checkoutBranch(options.branchName);
3098
+ }
3099
+ await git.git.addConfig("canopycms.managed", "true");
3100
+ log.debug("git", "Marked workspace as CanopyCMS-managed", {
3101
+ workspacePath: options.workspacePath
3102
+ });
3103
+ return git;
3104
+ }
3105
+ async status() {
3106
+ const s = await this.git.status();
3107
+ return {
3108
+ files: s.files,
3109
+ ahead: s.ahead,
3110
+ behind: s.behind,
3111
+ current: s.current,
3112
+ tracking: s.tracking
3113
+ };
3114
+ }
3115
+ async checkoutBranch(branch) {
3116
+ const branches = await this.git.branch();
3117
+ if (branches.all.includes(branch)) {
3118
+ await this.git.checkout(branch);
3119
+ return;
3120
+ }
3121
+ const remoteRef = `${this.remote}/${this.baseBranch}`;
3122
+ try {
3123
+ await this.git.fetch(this.remote, this.baseBranch);
3124
+ } catch {
3125
+ }
3126
+ try {
3127
+ await this.git.checkoutBranch(branch, remoteRef);
3128
+ return;
3129
+ } catch {
3130
+ const baseExists = branches.all.includes(this.baseBranch);
3131
+ if (baseExists) {
3132
+ await this.git.checkout(["-B", branch, this.baseBranch]);
3133
+ return;
3134
+ }
3135
+ await this.git.checkoutLocalBranch(branch);
3136
+ }
3137
+ }
3138
+ async pullBase() {
3139
+ await this.git.fetch(this.remote, this.baseBranch);
3140
+ await this.git.merge([`${this.remote}/${this.baseBranch}`]);
3141
+ }
3142
+ async pullCurrentBranch() {
3143
+ const branches = await this.git.branch();
3144
+ const currentBranch = branches.current;
3145
+ await this.git.fetch(this.remote, currentBranch);
3146
+ await this.git.merge([`${this.remote}/${currentBranch}`]);
3147
+ }
3148
+ async rebaseOntoBase() {
3149
+ await this.git.fetch(this.remote, this.baseBranch);
3150
+ await this.git.rebase([`${this.remote}/${this.baseBranch}`]);
3151
+ }
3152
+ async add(files) {
3153
+ const fileArray = Array.isArray(files) ? files : [files];
3154
+ await this.git.add(fileArray);
3155
+ }
3156
+ async commit(message) {
3157
+ await this.git.commit(message);
3158
+ }
3159
+ async push(branch) {
3160
+ const target = branch ?? await this.git.revparse(["--abbrev-ref", "HEAD"]);
3161
+ await this.git.push(this.remote, `${target}:${target}`, ["--set-upstream"]);
3162
+ }
3163
+ async ensureAuthor(author) {
3164
+ const config = await this.git.listConfig();
3165
+ const isManaged = config.all["canopycms.managed"] === "true";
3166
+ if (!isManaged) {
3167
+ 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.`);
3168
+ }
3169
+ const currentName = config.all["user.name"];
3170
+ const currentEmail = config.all["user.email"];
3171
+ if (currentName !== author.name) {
3172
+ await this.git.addConfig("user.name", author.name);
3173
+ }
3174
+ if (currentEmail !== author.email) {
3175
+ await this.git.addConfig("user.email", author.email);
3176
+ }
3177
+ }
3178
+ async ensureRemote(remoteUrl) {
3179
+ const remotes = await this.git.getRemotes(true);
3180
+ const existing = remotes.find((r) => r.name === this.remote);
3181
+ if (!existing) {
3182
+ await this.git.addRemote(this.remote, remoteUrl);
3183
+ return;
3184
+ }
3185
+ const currentUrl = existing.refs.push ?? existing.refs.fetch;
3186
+ if (currentUrl && currentUrl !== remoteUrl) {
3187
+ await this.git.remote(["set-url", this.remote, remoteUrl]);
3188
+ }
3189
+ }
3190
+ /**
3191
+ * Check if working directory has uncommitted changes
3192
+ */
3193
+ async hasUncommittedChanges() {
3194
+ const status = await this.status();
3195
+ return status.files.length > 0;
3196
+ }
3197
+ /**
3198
+ * Get list of uncommitted file paths
3199
+ */
3200
+ async getUncommittedFiles() {
3201
+ const status = await this.status();
3202
+ return status.files.map((f) => f.path);
3203
+ }
3204
+ /**
3205
+ * Force push (use with caution - for PR updates only)
3206
+ * Uses --force-with-lease for safer force pushes
3207
+ */
3208
+ async forcePush(branch) {
3209
+ const target = branch ?? await this.git.revparse(["--abbrev-ref", "HEAD"]);
3210
+ await this.git.push(this.remote, target, ["--force-with-lease"]);
3211
+ }
3212
+ /**
3213
+ * Get remote URL for current repo
3214
+ */
3215
+ async getRemoteUrl() {
3216
+ const remotes = await this.git.getRemotes(true);
3217
+ const remote = remotes.find((r) => r.name === this.remote);
3218
+ return remote?.refs.push || remote?.refs.fetch;
3219
+ }
3220
+ /**
3221
+ * Add a pattern to .git/info/exclude to prevent it from being committed/pushed.
3222
+ * This is used to exclude .canopy-meta/ from content branch workspaces.
3223
+ *
3224
+ * .git/info/exclude is a per-repository gitignore that never gets committed.
3225
+ * Perfect for runtime metadata that should never leave the workspace.
3226
+ *
3227
+ * This is idempotent - if the pattern already exists, it won't be added again.
3228
+ */
3229
+ async ensureGitExclude(pattern) {
3230
+ const excludePath = path10.join(this.repoPath, ".git", "info", "exclude");
3231
+ await fs9.mkdir(path10.dirname(excludePath), { recursive: true });
3232
+ let content = "";
3233
+ try {
3234
+ content = await fs9.readFile(excludePath, "utf-8");
3235
+ } catch (err) {
3236
+ if (!isNotFoundError(err))
3237
+ throw err;
3238
+ }
3239
+ const lines = content.split("\n");
3240
+ if (lines.some((line) => line.trim() === pattern)) {
3241
+ log.debug("git", "Pattern already in .git/info/exclude", { pattern });
3242
+ return;
3243
+ }
3244
+ const needsLeadingNewline = content.length > 0 && !content.endsWith("\n");
3245
+ const newContent = content + (needsLeadingNewline ? "\n" : "") + pattern + "\n";
3246
+ await fs9.writeFile(excludePath, newContent, "utf-8");
3247
+ log.debug("git", "Added pattern to .git/info/exclude", { pattern });
3248
+ }
3249
+ /**
3250
+ * Create an orphan branch for settings (permissions/groups).
3251
+ *
3252
+ * Orphan branches have no shared history with other branches - they start fresh.
3253
+ * This is perfect for deployment-specific settings that shouldn't pollute content history.
3254
+ *
3255
+ * The branch contains only settings files in .canopy-meta/ (groups.json, permissions.json).
3256
+ *
3257
+ * @param branchName - Name of the orphan branch (e.g., 'canopycms-settings-prod')
3258
+ * @param initialFiles - Files to commit to the new branch (e.g., { 'permissions.json': '{}', 'groups.json': '{}' })
3259
+ */
3260
+ async createOrphanSettingsBranch(branchName, initialFiles) {
3261
+ log.debug("git", "Creating orphan settings branch", { branchName });
3262
+ const branches = await this.git.branch();
3263
+ if (branches.all.includes(branchName)) {
3264
+ log.debug("git", "Orphan branch already exists", { branchName });
3265
+ await this.git.checkout(branchName);
3266
+ return;
3267
+ }
3268
+ await this.git.raw(["checkout", "--orphan", branchName]);
3269
+ try {
3270
+ await this.git.raw(["rm", "-rf", "."]);
3271
+ } catch {
3272
+ }
3273
+ for (const [filePath, content] of Object.entries(initialFiles)) {
3274
+ const absolutePath = path10.join(this.repoPath, filePath);
3275
+ await fs9.mkdir(path10.dirname(absolutePath), { recursive: true });
3276
+ await fs9.writeFile(absolutePath, content, "utf-8");
3277
+ await this.git.add(filePath);
3278
+ }
3279
+ await this.git.commit("Initialize settings branch", ["--allow-empty"]);
3280
+ log.debug("git", "Orphan settings branch created", { branchName });
3281
+ }
3282
+ };
3283
+
3284
+ // dist/branch-workspace.js
3285
+ var log2 = createDebugLogger({ prefix: "BranchWorkspace" });
3286
+ var workspaceInitLocks = /* @__PURE__ */ new Map();
3287
+ var BranchWorkspaceManager = class {
3288
+ constructor(config) {
3289
+ this.config = config;
3290
+ }
3291
+ async ensureGitWorkspace(options) {
3292
+ return log2.timed("workspace", "ensureGitWorkspace", async () => {
3293
+ const existingLock = workspaceInitLocks.get(options.branchRoot);
3294
+ if (existingLock) {
3295
+ await existingLock;
3296
+ return;
3297
+ }
3298
+ const lockPromise = (async () => {
3299
+ try {
3300
+ log2.debug("workspace", "Ensuring git workspace", {
3301
+ branchName: options.branchName,
3302
+ mode: options.mode
3303
+ });
3304
+ await GitManager.initializeWorkspace({
3305
+ workspacePath: options.branchRoot,
3306
+ branchName: options.branchName,
3307
+ mode: options.mode,
3308
+ baseBranch: this.config.defaultBaseBranch,
3309
+ sourceRoot: this.config.sourceRoot,
3310
+ defaultRemoteUrl: this.config.defaultRemoteUrl,
3311
+ remoteUrl: options.remoteUrl,
3312
+ remoteName: this.config.defaultRemoteName,
3313
+ branchType: "content"
3314
+ });
3315
+ } finally {
3316
+ workspaceInitLocks.delete(options.branchRoot);
3317
+ }
3318
+ })();
3319
+ workspaceInitLocks.set(options.branchRoot, lockPromise);
3320
+ await lockPromise;
3321
+ });
3322
+ }
3323
+ async openOrCreateBranch(options) {
3324
+ const { branchName, mode, basePathOverride, title, description, access, createdBy, remoteUrl } = options;
3325
+ const { branchRoot, baseRoot, branchName: safeName } = await ensureBranchRoot({
3326
+ mode,
3327
+ branchName,
3328
+ basePathOverride
3329
+ });
3330
+ await this.ensureGitWorkspace({
3331
+ branchRoot,
3332
+ branchName: safeName,
3333
+ mode,
3334
+ remoteUrl
3335
+ });
3336
+ const metadata = getBranchMetadataFileManager(branchRoot, baseRoot);
3337
+ const meta = await metadata.save({
3338
+ branch: {
3339
+ name: safeName,
3340
+ title,
3341
+ description,
3342
+ access,
3343
+ createdBy
3344
+ }
3345
+ });
3346
+ return {
3347
+ branch: meta.branch,
3348
+ branchRoot,
3349
+ baseRoot
3350
+ };
3351
+ }
3352
+ };
3353
+ async function loadOrCreateBranchContext(options) {
3354
+ if (isDeployedStatic(options.config)) {
3355
+ const cwd = process.cwd();
3356
+ return {
3357
+ branch: {
3358
+ name: options.branchName,
3359
+ status: "editing",
3360
+ access: {},
3361
+ createdBy: "__static_deploy__",
3362
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
3363
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
3364
+ },
3365
+ branchRoot: cwd,
3366
+ baseRoot: cwd
3367
+ };
3368
+ }
3369
+ const existing = await loadBranchContext({
3370
+ branchName: options.branchName,
3371
+ mode: options.mode,
3372
+ basePathOverride: options.basePathOverride
3373
+ });
3374
+ if (existing)
3375
+ return existing;
3376
+ const manager = new BranchWorkspaceManager(options.config);
3377
+ return manager.openOrCreateBranch({
3378
+ branchName: options.branchName,
3379
+ mode: options.mode,
3380
+ basePathOverride: options.basePathOverride,
3381
+ createdBy: options.createdBy,
3382
+ remoteUrl: options.remoteUrl
3383
+ });
3384
+ }
3385
+
2608
3386
  // dist/ai/resolve-branch.js
2609
3387
  async function resolveBranchRoot(config) {
2610
- if (config.mode === "dev" || isDeployedStatic(config)) {
3388
+ if (isDeployedStatic(config)) {
2611
3389
  return process.cwd();
2612
3390
  }
2613
3391
  const baseBranch = config.defaultBaseBranch ?? "main";
2614
- const context = await loadBranchContext({
3392
+ const context = await loadOrCreateBranchContext({
3393
+ config,
2615
3394
  branchName: baseBranch,
2616
- mode: config.mode
3395
+ mode: config.mode,
3396
+ createdBy: "canopycms-ai",
3397
+ remoteUrl: config.defaultRemoteUrl
2617
3398
  });
2618
- if (!context) {
2619
- throw new Error(`Could not load branch context for "${baseBranch}". Ensure the branch exists and has been initialized.`);
2620
- }
2621
3399
  return context.branchRoot;
2622
3400
  }
2623
3401
 
@@ -2641,15 +3419,15 @@ async function generateAIContentFiles(options) {
2641
3419
  contentRoot: contentRootName,
2642
3420
  config: aiConfig
2643
3421
  });
2644
- const absoluteOutputDir = path10.resolve(outputDir) + path10.sep;
3422
+ const absoluteOutputDir = path11.resolve(outputDir) + path11.sep;
2645
3423
  let fileCount = 0;
2646
3424
  for (const [filePath, content] of result.files) {
2647
- const absolutePath = path10.resolve(path10.join(absoluteOutputDir, filePath));
3425
+ const absolutePath = path11.resolve(path11.join(absoluteOutputDir, filePath));
2648
3426
  if (!absolutePath.startsWith(absoluteOutputDir)) {
2649
3427
  throw new Error(`Path traversal detected in AI content output: ${filePath}`);
2650
3428
  }
2651
- await fs8.mkdir(path10.dirname(absolutePath), { recursive: true });
2652
- await fs8.writeFile(absolutePath, content, "utf-8");
3429
+ await fs10.mkdir(path11.dirname(absolutePath), { recursive: true });
3430
+ await fs10.writeFile(absolutePath, content, "utf-8");
2653
3431
  fileCount++;
2654
3432
  }
2655
3433
  return { fileCount, outputDir: absoluteOutputDir };
@@ -2660,7 +3438,7 @@ var jiti = createJiti(import.meta.url);
2660
3438
  async function generateAIContentCLI(options) {
2661
3439
  const { projectDir, outputDir = "public/ai", configPath, appDir = "app" } = options;
2662
3440
  console.log("\nCanopyCMS generate-ai-content\n");
2663
- const canopyConfigPath = path11.join(projectDir, "canopycms.config.ts");
3441
+ const canopyConfigPath = path12.join(projectDir, "canopycms.config.ts");
2664
3442
  let canopyConfigModule;
2665
3443
  try {
2666
3444
  canopyConfigModule = await jiti.import(canopyConfigPath);
@@ -2671,7 +3449,7 @@ async function generateAIContentCLI(options) {
2671
3449
  }
2672
3450
  const configExport = canopyConfigModule.default ?? canopyConfigModule.config ?? canopyConfigModule;
2673
3451
  const serverConfig = typeof configExport === "object" && configExport !== null && "server" in configExport ? configExport.server : configExport;
2674
- const schemasPath = path11.join(projectDir, appDir, "schemas.ts");
3452
+ const schemasPath = path12.join(projectDir, appDir, "schemas.ts");
2675
3453
  let entrySchemaRegistry = {};
2676
3454
  try {
2677
3455
  const schemasModule = await jiti.import(schemasPath);
@@ -2682,7 +3460,7 @@ async function generateAIContentCLI(options) {
2682
3460
  let aiConfig;
2683
3461
  if (configPath) {
2684
3462
  try {
2685
- const aiConfigModule = await jiti.import(path11.resolve(configPath));
3463
+ const aiConfigModule = await jiti.import(path12.resolve(configPath));
2686
3464
  aiConfig = aiConfigModule.aiContentConfig ?? aiConfigModule.default ?? aiConfigModule.config;
2687
3465
  } catch (err) {
2688
3466
  console.error(`Could not load AI config from ${configPath}`);
@@ -2699,7 +3477,7 @@ async function generateAIContentCLI(options) {
2699
3477
  console.error("Make sure canopycms.config.ts uses defineCanopyConfig().");
2700
3478
  process.exit(1);
2701
3479
  }
2702
- const resolvedOutput = path11.resolve(projectDir, outputDir);
3480
+ const resolvedOutput = path12.resolve(projectDir, outputDir);
2703
3481
  console.log(` Output: ${resolvedOutput}`);
2704
3482
  console.log(` Mode: ${serverConfig.mode ?? "dev"}`);
2705
3483
  const result = await generateAIContentFiles({