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
package/dist/cli/init.js CHANGED
@@ -9,107 +9,6 @@ var __export = (target, all) => {
9
9
  __defProp(target, name, { get: all[name], enumerable: true });
10
10
  };
11
11
 
12
- // dist/operating-mode/client-safe-strategy.js
13
- var ProdClientSafeStrategy, LocalProdSimClientSafeStrategy, LocalSimpleClientSafeStrategy;
14
- var init_client_safe_strategy = __esm({
15
- "dist/operating-mode/client-safe-strategy.js"() {
16
- "use strict";
17
- ProdClientSafeStrategy = class {
18
- constructor() {
19
- this.mode = "prod";
20
- }
21
- // UI Feature Flags
22
- supportsBranching() {
23
- return true;
24
- }
25
- supportsStatusBadge() {
26
- return true;
27
- }
28
- supportsComments() {
29
- return true;
30
- }
31
- supportsPullRequests() {
32
- return true;
33
- }
34
- // Simple Data
35
- getPermissionsFileName() {
36
- return "permissions.json";
37
- }
38
- getGroupsFileName() {
39
- return "groups.json";
40
- }
41
- shouldCommit() {
42
- return true;
43
- }
44
- shouldPush() {
45
- return true;
46
- }
47
- };
48
- LocalProdSimClientSafeStrategy = class {
49
- constructor() {
50
- this.mode = "prod-sim";
51
- }
52
- // UI Feature Flags
53
- supportsBranching() {
54
- return true;
55
- }
56
- supportsStatusBadge() {
57
- return true;
58
- }
59
- supportsComments() {
60
- return true;
61
- }
62
- supportsPullRequests() {
63
- return false;
64
- }
65
- // Simple Data
66
- getPermissionsFileName() {
67
- return "permissions.json";
68
- }
69
- getGroupsFileName() {
70
- return "groups.json";
71
- }
72
- shouldCommit() {
73
- return true;
74
- }
75
- shouldPush() {
76
- return true;
77
- }
78
- };
79
- LocalSimpleClientSafeStrategy = class {
80
- constructor() {
81
- this.mode = "dev";
82
- }
83
- // UI Feature Flags
84
- supportsBranching() {
85
- return false;
86
- }
87
- supportsStatusBadge() {
88
- return false;
89
- }
90
- supportsComments() {
91
- return false;
92
- }
93
- supportsPullRequests() {
94
- return false;
95
- }
96
- // Simple Data
97
- getPermissionsFileName() {
98
- return "permissions.json";
99
- }
100
- getGroupsFileName() {
101
- return "groups.json";
102
- }
103
- shouldCommit() {
104
- return false;
105
- }
106
- shouldPush() {
107
- return false;
108
- }
109
- };
110
- }
111
- });
112
-
113
12
  // dist/config/types.js
114
13
  var primitiveFieldTypes, fieldTypes;
115
14
  var init_types = __esm({
@@ -295,7 +194,7 @@ var init_config = __esm({
295
194
  gitBotAuthorNameSchema = z4.string().min(1);
296
195
  gitBotAuthorEmailSchema = z4.string().email();
297
196
  githubTokenEnvVarSchema = z4.string().default("GITHUB_BOT_TOKEN");
298
- operatingModeSchema = z4.enum(["prod", "prod-sim", "dev"]).default("dev");
197
+ operatingModeSchema = z4.enum(["prod", "dev"]).default("dev");
299
198
  deployedAsSchema = z4.enum(["static", "server"]).default("server");
300
199
  contentRootSchema = relativePathSchema.default("content");
301
200
  sourceRootSchema = z4.string().min(1).optional();
@@ -358,20 +257,6 @@ var init_permissions = __esm({
358
257
  });
359
258
 
360
259
  // dist/paths/normalize.js
361
- function normalizeFilesystemPath(path16) {
362
- return path16.split(/[\\/]+/).filter(Boolean).join("/");
363
- }
364
- function hasTraversalSequence(path16) {
365
- const normalized = normalizeFilesystemPath(path16);
366
- return normalized.includes("..");
367
- }
368
- function createLogicalPath(...segments) {
369
- const normalized = segments.map((s) => normalizeFilesystemPath(s)).filter(Boolean).join("/");
370
- if (hasTraversalSequence(normalized)) {
371
- throw new Error(`Invalid path: contains traversal sequence: ${normalized}`);
372
- }
373
- return normalized;
374
- }
375
260
  var init_normalize = __esm({
376
261
  "dist/paths/normalize.js"() {
377
262
  "use strict";
@@ -379,116 +264,19 @@ var init_normalize = __esm({
379
264
  });
380
265
 
381
266
  // dist/paths/types.js
382
- var ROOT_COLLECTION_ID;
383
267
  var init_types2 = __esm({
384
268
  "dist/paths/types.js"() {
385
269
  "use strict";
386
- ROOT_COLLECTION_ID = "__rootcoll__";
387
270
  }
388
271
  });
389
272
 
390
273
  // dist/config/flatten.js
391
274
  import { join, normalize } from "pathe";
392
- var normalizePathValue, flattenSchema;
393
275
  var init_flatten = __esm({
394
276
  "dist/config/flatten.js"() {
395
277
  "use strict";
396
278
  init_normalize();
397
279
  init_types2();
398
- normalizePathValue = (val) => normalize(val).split("/").filter(Boolean).join("/");
399
- flattenSchema = (root, basePath = "") => {
400
- const flat = [];
401
- const base = normalizePathValue(basePath || "");
402
- const walkCollection = (collection, parentPath) => {
403
- const normalizedPath = normalizePathValue(collection.path);
404
- let logicalPath;
405
- if (parentPath && parentPath !== base) {
406
- logicalPath = join(parentPath, collection.name);
407
- } else if (parentPath === base) {
408
- logicalPath = join(base, normalizedPath);
409
- } else {
410
- logicalPath = normalizedPath;
411
- }
412
- const normalizedFull = normalizePathValue(logicalPath);
413
- flat.push({
414
- type: "collection",
415
- logicalPath: createLogicalPath(normalizedFull),
416
- name: collection.name,
417
- label: collection.label,
418
- description: collection.description,
419
- contentId: collection.contentId,
420
- parentPath: parentPath ? createLogicalPath(parentPath) : void 0,
421
- entries: collection.entries,
422
- collections: collection.collections,
423
- order: collection.order
424
- });
425
- if (collection.entries) {
426
- for (const entryType of collection.entries) {
427
- const entryTypePath = join(normalizedFull, entryType.name);
428
- flat.push({
429
- type: "entry-type",
430
- logicalPath: createLogicalPath(normalizePathValue(entryTypePath)),
431
- name: entryType.name,
432
- label: entryType.label,
433
- description: entryType.description,
434
- parentPath: createLogicalPath(normalizedFull),
435
- format: entryType.format,
436
- schema: entryType.schema,
437
- schemaRef: entryType.schemaRef,
438
- default: entryType.default,
439
- maxItems: entryType.maxItems
440
- });
441
- }
442
- }
443
- if (collection.collections) {
444
- for (const child of collection.collections) {
445
- walkCollection(child, normalizedFull);
446
- }
447
- }
448
- };
449
- if (base) {
450
- flat.push({
451
- type: "collection",
452
- logicalPath: createLogicalPath(base),
453
- name: base,
454
- // Use base path as the name (e.g., 'content')
455
- label: void 0,
456
- // Root collection has no label
457
- contentId: ROOT_COLLECTION_ID,
458
- // Sentinel — root dir has no embedded ID
459
- parentPath: void 0,
460
- // No parent - this is the root
461
- entries: root.entries,
462
- collections: root.collections,
463
- order: root.order
464
- });
465
- }
466
- if (root.entries) {
467
- for (const entryType of root.entries) {
468
- const entryTypePath = base ? join(base, entryType.name) : entryType.name;
469
- flat.push({
470
- type: "entry-type",
471
- logicalPath: createLogicalPath(normalizePathValue(entryTypePath)),
472
- name: entryType.name,
473
- label: entryType.label,
474
- description: entryType.description,
475
- parentPath: base ? createLogicalPath(base) : createLogicalPath(""),
476
- // Now references the root collection (e.g., 'content')
477
- format: entryType.format,
478
- schema: entryType.schema,
479
- schemaRef: entryType.schemaRef,
480
- default: entryType.default,
481
- maxItems: entryType.maxItems
482
- });
483
- }
484
- }
485
- if (root.collections) {
486
- for (const collection of root.collections) {
487
- walkCollection(collection, base || "");
488
- }
489
- }
490
- return flat;
491
- };
492
280
  }
493
281
  });
494
282
 
@@ -533,208 +321,6 @@ var init_config3 = __esm({
533
321
  }
534
322
  });
535
323
 
536
- // dist/operating-mode/client-unsafe-strategy.js
537
- import path from "node:path";
538
- function operatingStrategy(mode) {
539
- const cached = strategyCache.get(mode);
540
- if (cached)
541
- return cached;
542
- let strategy;
543
- switch (mode) {
544
- case "prod":
545
- strategy = new ProdStrategy();
546
- break;
547
- case "prod-sim":
548
- strategy = new LocalProdSimStrategy();
549
- break;
550
- case "dev":
551
- strategy = new LocalSimpleStrategy();
552
- break;
553
- default: {
554
- const _exhaustive = mode;
555
- throw new Error(`Unknown operating mode: ${_exhaustive}`);
556
- }
557
- }
558
- strategyCache.set(mode, strategy);
559
- return strategy;
560
- }
561
- var ProdStrategy, LocalProdSimStrategy, LocalSimpleStrategy, strategyCache;
562
- var init_client_unsafe_strategy = __esm({
563
- "dist/operating-mode/client-unsafe-strategy.js"() {
564
- "use strict";
565
- init_client_safe_strategy();
566
- init_config3();
567
- ProdStrategy = class extends ProdClientSafeStrategy {
568
- // All client-safe methods inherited automatically from ProdClientSafeStrategy:
569
- // - mode, supportsBranching(), supportsStatusBadge(), supportsComments()
570
- // - supportsPullRequests(), getPermissionsFileName(), getGroupsFileName()
571
- // - shouldCommit(), shouldPush()
572
- // Add client-unsafe methods (use Node.js APIs)
573
- getWorkspaceRoot(_sourceRoot) {
574
- return path.resolve(process.env.CANOPYCMS_WORKSPACE_ROOT ?? DEFAULT_PROD_WORKSPACE);
575
- }
576
- getContentRoot(sourceRoot) {
577
- return path.resolve(sourceRoot ?? process.cwd(), "content");
578
- }
579
- getContentBranchesRoot(sourceRoot) {
580
- return path.join(this.getWorkspaceRoot(sourceRoot), "content-branches");
581
- }
582
- getContentBranchRoot(branchName, sourceRoot) {
583
- return path.resolve(this.getContentBranchesRoot(sourceRoot), branchName);
584
- }
585
- getGitExcludePattern() {
586
- return ".canopy-meta/";
587
- }
588
- getPermissionsFilePath(root) {
589
- return path.join(root, this.getPermissionsFileName());
590
- }
591
- getGroupsFilePath(root) {
592
- return path.join(root, this.getGroupsFileName());
593
- }
594
- getRemoteUrlConfig() {
595
- return {
596
- shouldAutoInitLocal: false,
597
- defaultRemotePath: "",
598
- envVarName: "CANOPYCMS_REMOTE_URL",
599
- autoDetectRemotePath: path.join(this.getWorkspaceRoot(), "remote.git")
600
- };
601
- }
602
- requiresExistingRepo() {
603
- return false;
604
- }
605
- getSettingsBranchName(config) {
606
- if (config.settingsBranch)
607
- return config.settingsBranch;
608
- const deploymentName = config.deploymentName ?? "prod";
609
- return `canopycms-settings-${deploymentName}`;
610
- }
611
- getSettingsRoot(sourceRoot) {
612
- return path.join(this.getWorkspaceRoot(sourceRoot), "settings");
613
- }
614
- usesSeparateSettingsBranch() {
615
- return true;
616
- }
617
- validateConfig(config) {
618
- if (!config.gitBotAuthorName || !config.gitBotAuthorEmail) {
619
- throw new Error("gitBotAuthorName and gitBotAuthorEmail are required in prod mode");
620
- }
621
- }
622
- shouldCreateSettingsPR(config) {
623
- return config.autoCreateSettingsPR ?? true;
624
- }
625
- };
626
- LocalProdSimStrategy = class extends LocalProdSimClientSafeStrategy {
627
- // Inherits client-safe methods from LocalProdSimClientSafeStrategy
628
- getWorkspaceRoot(sourceRoot) {
629
- return path.resolve(sourceRoot ?? process.cwd(), ".canopy-prod-sim");
630
- }
631
- getContentRoot(sourceRoot) {
632
- return path.resolve(sourceRoot ?? process.cwd(), "content");
633
- }
634
- getContentBranchesRoot(sourceRoot) {
635
- return path.join(this.getWorkspaceRoot(sourceRoot), "content-branches");
636
- }
637
- getContentBranchRoot(branchName, sourceRoot) {
638
- return path.resolve(this.getContentBranchesRoot(sourceRoot), branchName);
639
- }
640
- getGitExcludePattern() {
641
- return ".canopy-meta/";
642
- }
643
- getPermissionsFilePath(root) {
644
- return path.join(root, this.getPermissionsFileName());
645
- }
646
- getGroupsFilePath(root) {
647
- return path.join(root, this.getGroupsFileName());
648
- }
649
- getRemoteUrlConfig() {
650
- return {
651
- shouldAutoInitLocal: true,
652
- defaultRemotePath: ".canopy-prod-sim/remote.git",
653
- envVarName: "CANOPYCMS_REMOTE_URL"
654
- };
655
- }
656
- requiresExistingRepo() {
657
- return false;
658
- }
659
- getSettingsBranchName(config) {
660
- if (config.settingsBranch)
661
- return config.settingsBranch;
662
- const deploymentName = config.deploymentName ?? "prod";
663
- return `canopycms-settings-${deploymentName}`;
664
- }
665
- getSettingsRoot(sourceRoot) {
666
- return path.join(this.getWorkspaceRoot(sourceRoot), "settings");
667
- }
668
- usesSeparateSettingsBranch() {
669
- return true;
670
- }
671
- validateConfig(_config) {
672
- }
673
- shouldCreateSettingsPR(_config) {
674
- return false;
675
- }
676
- };
677
- LocalSimpleStrategy = class extends LocalSimpleClientSafeStrategy {
678
- // Inherits: supportsBranching() returns false, getPermissionsFileName() returns 'permissions.local.json'
679
- getWorkspaceRoot(sourceRoot) {
680
- return path.resolve(sourceRoot ?? process.cwd(), ".canopy-dev");
681
- }
682
- getContentRoot(sourceRoot) {
683
- return path.resolve(sourceRoot ?? process.cwd(), "content");
684
- }
685
- getContentBranchesRoot(_sourceRoot) {
686
- throw new Error("No branching in dev mode");
687
- }
688
- getContentBranchRoot(_branchName, _sourceRoot) {
689
- throw new Error("No branching in dev mode");
690
- }
691
- getGitExcludePattern() {
692
- return ".canopy-meta/";
693
- }
694
- getPermissionsFilePath(root) {
695
- return path.join(this.getWorkspaceRoot(root), "settings", "permissions.json");
696
- }
697
- getGroupsFilePath(root) {
698
- return path.join(this.getWorkspaceRoot(root), "settings", "groups.json");
699
- }
700
- getRemoteUrlConfig() {
701
- return {
702
- shouldAutoInitLocal: false,
703
- defaultRemotePath: "",
704
- envVarName: "CANOPYCMS_REMOTE_URL"
705
- };
706
- }
707
- requiresExistingRepo() {
708
- return true;
709
- }
710
- getSettingsBranchName(config) {
711
- return config.defaultBaseBranch ?? "main";
712
- }
713
- getSettingsRoot(sourceRoot) {
714
- return path.join(this.getWorkspaceRoot(sourceRoot), "settings");
715
- }
716
- usesSeparateSettingsBranch() {
717
- return false;
718
- }
719
- validateConfig(_config) {
720
- }
721
- shouldCreateSettingsPR(_config) {
722
- return false;
723
- }
724
- };
725
- strategyCache = /* @__PURE__ */ new Map();
726
- }
727
- });
728
-
729
- // dist/operating-mode/index.js
730
- var init_operating_mode = __esm({
731
- "dist/operating-mode/index.js"() {
732
- "use strict";
733
- init_client_safe_strategy();
734
- init_client_unsafe_strategy();
735
- }
736
- });
737
-
738
324
  // dist/cli/templates.js
739
325
  var templates_exports = {};
740
326
  __export(templates_exports, {
@@ -748,11 +334,11 @@ __export(templates_exports, {
748
334
  githubWorkflowCms: () => githubWorkflowCms,
749
335
  schemasTemplate: () => schemasTemplate
750
336
  });
751
- import fs from "node:fs/promises";
337
+ import fs2 from "node:fs/promises";
752
338
  import path2 from "node:path";
753
339
  import { fileURLToPath } from "node:url";
754
340
  async function readTemplate(name) {
755
- return fs.readFile(path2.join(TEMPLATES_DIR, name), "utf-8");
341
+ return fs2.readFile(path2.join(TEMPLATES_DIR, name), "utf-8");
756
342
  }
757
343
  async function canopyCmsConfig(options) {
758
344
  const template = await readTemplate("canopycms.config.ts.template");
@@ -807,11 +393,9 @@ function getTaskQueueDir(config) {
807
393
  const workspace = process.env.CANOPYCMS_WORKSPACE_ROOT ?? DEFAULT_PROD_WORKSPACE;
808
394
  return path3.join(path3.resolve(workspace), ".tasks");
809
395
  }
810
- case "prod-sim": {
811
- return path3.join(process.cwd(), ".canopy-prod-sim", ".tasks");
396
+ case "dev": {
397
+ return path3.join(process.cwd(), ".canopy-dev", ".tasks");
812
398
  }
813
- case "dev":
814
- return null;
815
399
  }
816
400
  }
817
401
  var init_task_queue_config = __esm({
@@ -919,17 +503,17 @@ var init_debug = __esm({
919
503
  });
920
504
 
921
505
  // dist/utils/atomic-write.js
922
- import fs2 from "node:fs/promises";
506
+ import fs3 from "node:fs/promises";
923
507
  import path4 from "node:path";
924
508
  async function atomicWriteFile(filePath, content) {
925
509
  const dir = path4.dirname(filePath);
926
- await fs2.mkdir(dir, { recursive: true });
510
+ await fs3.mkdir(dir, { recursive: true });
927
511
  const tempPath = `${filePath}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
928
- await fs2.writeFile(tempPath, content, "utf-8");
512
+ await fs3.writeFile(tempPath, content, "utf-8");
929
513
  try {
930
- await fs2.rename(tempPath, filePath);
514
+ await fs3.rename(tempPath, filePath);
931
515
  } catch (err) {
932
- await fs2.unlink(tempPath).catch(() => {
516
+ await fs3.unlink(tempPath).catch(() => {
933
517
  });
934
518
  throw err;
935
519
  }
@@ -941,10 +525,10 @@ var init_atomic_write = __esm({
941
525
  });
942
526
 
943
527
  // dist/task-queue/task-queue.js
944
- import fs3 from "node:fs/promises";
528
+ import fs4 from "node:fs/promises";
945
529
  import path5 from "node:path";
946
530
  import crypto from "node:crypto";
947
- function isNotFoundError(err) {
531
+ function isNotFoundError2(err) {
948
532
  return err instanceof Error && "code" in err && err.code === "ENOENT";
949
533
  }
950
534
  async function enqueueTask(taskDir, task, logger = nullLogger) {
@@ -969,7 +553,7 @@ async function dequeueTask(taskDir, logger = nullLogger) {
969
553
  const processingDir = path5.join(taskDir, "processing");
970
554
  let files;
971
555
  try {
972
- files = await fs3.readdir(pendingDir);
556
+ files = await fs4.readdir(pendingDir);
973
557
  } catch {
974
558
  return null;
975
559
  }
@@ -980,7 +564,7 @@ async function dequeueTask(taskDir, logger = nullLogger) {
980
564
  const tasks = [];
981
565
  for (const fileName2 of jsonFiles) {
982
566
  try {
983
- const content = await fs3.readFile(path5.join(pendingDir, fileName2), "utf-8");
567
+ const content = await fs4.readFile(path5.join(pendingDir, fileName2), "utf-8");
984
568
  const task2 = parseTaskJson(content);
985
569
  if (!task2) {
986
570
  await moveToCorrupt(taskDir, pendingDir, fileName2, "Invalid JSON", logger);
@@ -991,7 +575,7 @@ async function dequeueTask(taskDir, logger = nullLogger) {
991
575
  }
992
576
  tasks.push({ fileName: fileName2, task: task2 });
993
577
  } catch (err) {
994
- if (isNotFoundError(err))
578
+ if (isNotFoundError2(err))
995
579
  continue;
996
580
  throw err;
997
581
  }
@@ -1001,7 +585,7 @@ async function dequeueTask(taskDir, logger = nullLogger) {
1001
585
  tasks.sort((a, b) => a.task.createdAt.localeCompare(b.task.createdAt) || a.task.id.localeCompare(b.task.id));
1002
586
  const { fileName, task } = tasks[0];
1003
587
  if (await taskExistsIn(taskDir, task.id, ["completed", "failed"])) {
1004
- await fs3.unlink(path5.join(pendingDir, fileName)).catch(() => {
588
+ await fs4.unlink(path5.join(pendingDir, fileName)).catch(() => {
1005
589
  });
1006
590
  logger.debug("Skipped already-finished task", { id: task.id });
1007
591
  return null;
@@ -1011,11 +595,11 @@ async function dequeueTask(taskDir, logger = nullLogger) {
1011
595
  try {
1012
596
  task.status = "processing";
1013
597
  await atomicWriteFile(destPath, JSON.stringify(task, null, 2));
1014
- await fs3.unlink(sourcePath);
598
+ await fs4.unlink(sourcePath);
1015
599
  logger.debug("Dequeued task", { id: task.id, action: task.action });
1016
600
  return task;
1017
601
  } catch (err) {
1018
- if (isNotFoundError(err))
602
+ if (isNotFoundError2(err))
1019
603
  return null;
1020
604
  throw err;
1021
605
  }
@@ -1026,17 +610,17 @@ async function completeTask(taskDir, taskId, result, logger = nullLogger) {
1026
610
  const completedPath = path5.join(completedDir, `${taskId}.json`);
1027
611
  let task;
1028
612
  try {
1029
- const content = await fs3.readFile(processingPath, "utf-8");
613
+ const content = await fs4.readFile(processingPath, "utf-8");
1030
614
  const parsed = parseTaskJson(content);
1031
615
  if (!parsed) {
1032
616
  logger.debug("Corrupt task file in processing, removing", { id: taskId });
1033
- await fs3.unlink(processingPath).catch(() => {
617
+ await fs4.unlink(processingPath).catch(() => {
1034
618
  });
1035
619
  return;
1036
620
  }
1037
621
  task = parsed;
1038
622
  } catch (err) {
1039
- if (isNotFoundError(err))
623
+ if (isNotFoundError2(err))
1040
624
  return;
1041
625
  throw err;
1042
626
  }
@@ -1044,7 +628,7 @@ async function completeTask(taskDir, taskId, result, logger = nullLogger) {
1044
628
  task.completedAt = (/* @__PURE__ */ new Date()).toISOString();
1045
629
  task.result = result;
1046
630
  await atomicWriteFile(completedPath, JSON.stringify(task, null, 2));
1047
- await fs3.unlink(processingPath).catch(() => {
631
+ await fs4.unlink(processingPath).catch(() => {
1048
632
  });
1049
633
  logger.debug("Completed task", { id: taskId });
1050
634
  }
@@ -1054,17 +638,17 @@ async function failTask(taskDir, taskId, error, logger = nullLogger) {
1054
638
  const failedPath = path5.join(failedDir, `${taskId}.json`);
1055
639
  let task;
1056
640
  try {
1057
- const content = await fs3.readFile(processingPath, "utf-8");
641
+ const content = await fs4.readFile(processingPath, "utf-8");
1058
642
  const parsed = parseTaskJson(content);
1059
643
  if (!parsed) {
1060
644
  logger.debug("Corrupt task file in processing, removing", { id: taskId });
1061
- await fs3.unlink(processingPath).catch(() => {
645
+ await fs4.unlink(processingPath).catch(() => {
1062
646
  });
1063
647
  return;
1064
648
  }
1065
649
  task = parsed;
1066
650
  } catch (err) {
1067
- if (isNotFoundError(err))
651
+ if (isNotFoundError2(err))
1068
652
  return;
1069
653
  throw err;
1070
654
  }
@@ -1072,7 +656,7 @@ async function failTask(taskDir, taskId, error, logger = nullLogger) {
1072
656
  task.completedAt = (/* @__PURE__ */ new Date()).toISOString();
1073
657
  task.error = error;
1074
658
  await atomicWriteFile(failedPath, JSON.stringify(task, null, 2));
1075
- await fs3.unlink(processingPath).catch(() => {
659
+ await fs4.unlink(processingPath).catch(() => {
1076
660
  });
1077
661
  logger.debug("Failed task", { id: taskId, error });
1078
662
  }
@@ -1082,17 +666,17 @@ async function retryTask(taskDir, taskId, error, logger = nullLogger) {
1082
666
  const pendingPath = path5.join(pendingDir, `${taskId}.json`);
1083
667
  let task;
1084
668
  try {
1085
- const content = await fs3.readFile(processingPath, "utf-8");
669
+ const content = await fs4.readFile(processingPath, "utf-8");
1086
670
  const parsed = parseTaskJson(content);
1087
671
  if (!parsed) {
1088
672
  logger.debug("Corrupt task file in processing, removing", { id: taskId });
1089
- await fs3.unlink(processingPath).catch(() => {
673
+ await fs4.unlink(processingPath).catch(() => {
1090
674
  });
1091
675
  return;
1092
676
  }
1093
677
  task = parsed;
1094
678
  } catch (err) {
1095
- if (isNotFoundError(err))
679
+ if (isNotFoundError2(err))
1096
680
  return;
1097
681
  throw err;
1098
682
  }
@@ -1103,7 +687,7 @@ async function retryTask(taskDir, taskId, error, logger = nullLogger) {
1103
687
  task.retryAfter = new Date(Date.now() + backoffMs).toISOString();
1104
688
  task.error = error;
1105
689
  await atomicWriteFile(pendingPath, JSON.stringify(task, null, 2));
1106
- await fs3.unlink(processingPath).catch(() => {
690
+ await fs4.unlink(processingPath).catch(() => {
1107
691
  });
1108
692
  logger.debug("Retrying task", { id: taskId, retryCount, backoffMs });
1109
693
  }
@@ -1112,7 +696,7 @@ async function recoverOrphanedTasks(taskDir, maxAgeMs = 5 * 6e4, logger = nullLo
1112
696
  const pendingDir = path5.join(taskDir, "pending");
1113
697
  let files;
1114
698
  try {
1115
- files = await fs3.readdir(processingDir);
699
+ files = await fs4.readdir(processingDir);
1116
700
  } catch {
1117
701
  return 0;
1118
702
  }
@@ -1121,16 +705,16 @@ async function recoverOrphanedTasks(taskDir, maxAgeMs = 5 * 6e4, logger = nullLo
1121
705
  for (const fileName of files.filter((f) => f.endsWith(".json"))) {
1122
706
  const filePath = path5.join(processingDir, fileName);
1123
707
  try {
1124
- const stat = await fs3.stat(filePath);
708
+ const stat = await fs4.stat(filePath);
1125
709
  if (now - stat.mtimeMs >= maxAgeMs) {
1126
- const content = await fs3.readFile(filePath, "utf-8");
710
+ const content = await fs4.readFile(filePath, "utf-8");
1127
711
  const task = parseTaskJson(content);
1128
712
  if (!task) {
1129
713
  await moveToCorrupt(taskDir, processingDir, fileName, "Invalid JSON during recovery", logger);
1130
714
  continue;
1131
715
  }
1132
716
  if (await taskExistsIn(taskDir, task.id, ["completed", "failed"])) {
1133
- await fs3.unlink(filePath).catch(() => {
717
+ await fs4.unlink(filePath).catch(() => {
1134
718
  });
1135
719
  logger.debug("Cleaned up orphaned task (already finished)", {
1136
720
  id: task.id
@@ -1139,7 +723,7 @@ async function recoverOrphanedTasks(taskDir, maxAgeMs = 5 * 6e4, logger = nullLo
1139
723
  }
1140
724
  task.status = "pending";
1141
725
  await atomicWriteFile(path5.join(pendingDir, fileName), JSON.stringify(task, null, 2));
1142
- await fs3.unlink(filePath);
726
+ await fs4.unlink(filePath);
1143
727
  logger.debug("Recovered orphaned task", {
1144
728
  id: task.id,
1145
729
  action: task.action
@@ -1147,7 +731,7 @@ async function recoverOrphanedTasks(taskDir, maxAgeMs = 5 * 6e4, logger = nullLo
1147
731
  recovered++;
1148
732
  }
1149
733
  } catch (err) {
1150
- if (isNotFoundError(err))
734
+ if (isNotFoundError2(err))
1151
735
  continue;
1152
736
  logger.debug("Failed to recover task", { fileName });
1153
737
  }
@@ -1161,16 +745,16 @@ async function cleanupOldTasks(taskDir, maxAgeMs = 30 * 24 * 60 * 6e4, logger =
1161
745
  const dir = path5.join(taskDir, subdir);
1162
746
  let files;
1163
747
  try {
1164
- files = await fs3.readdir(dir);
748
+ files = await fs4.readdir(dir);
1165
749
  } catch {
1166
750
  continue;
1167
751
  }
1168
752
  for (const fileName of files.filter((f) => f.endsWith(".json"))) {
1169
753
  try {
1170
754
  const filePath = path5.join(dir, fileName);
1171
- const stat = await fs3.stat(filePath);
755
+ const stat = await fs4.stat(filePath);
1172
756
  if (now - stat.mtimeMs >= maxAgeMs) {
1173
- await fs3.unlink(filePath);
757
+ await fs4.unlink(filePath);
1174
758
  cleaned++;
1175
759
  }
1176
760
  } catch {
@@ -1186,7 +770,7 @@ async function getTask(taskDir, taskId) {
1186
770
  for (const subdir of ["completed", "failed", "processing", "pending"]) {
1187
771
  const filePath = path5.join(taskDir, subdir, `${taskId}.json`);
1188
772
  try {
1189
- const content = await fs3.readFile(filePath, "utf-8");
773
+ const content = await fs4.readFile(filePath, "utf-8");
1190
774
  return parseTaskJson(content);
1191
775
  } catch {
1192
776
  continue;
@@ -1198,7 +782,7 @@ async function listTasks(taskDir, status, limit = 100) {
1198
782
  const dir = path5.join(taskDir, status);
1199
783
  let files;
1200
784
  try {
1201
- files = await fs3.readdir(dir);
785
+ files = await fs4.readdir(dir);
1202
786
  } catch {
1203
787
  return [];
1204
788
  }
@@ -1207,7 +791,7 @@ async function listTasks(taskDir, status, limit = 100) {
1207
791
  if (tasks.length >= limit)
1208
792
  break;
1209
793
  try {
1210
- const content = await fs3.readFile(path5.join(dir, fileName), "utf-8");
794
+ const content = await fs4.readFile(path5.join(dir, fileName), "utf-8");
1211
795
  const task = parseTaskJson(content);
1212
796
  if (task)
1213
797
  tasks.push(task);
@@ -1229,7 +813,7 @@ async function getQueueStats(taskDir) {
1229
813
  for (const status of ["pending", "processing", "completed", "failed", "corrupt"]) {
1230
814
  const dir = path5.join(taskDir, status);
1231
815
  try {
1232
- const files = await fs3.readdir(dir);
816
+ const files = await fs4.readdir(dir);
1233
817
  stats[status] = files.filter((f) => f.endsWith(".json")).length;
1234
818
  } catch {
1235
819
  }
@@ -1250,7 +834,7 @@ function parseTaskJson(content) {
1250
834
  async function taskExistsIn(taskDir, taskId, subdirs) {
1251
835
  for (const subdir of subdirs) {
1252
836
  try {
1253
- await fs3.stat(path5.join(taskDir, subdir, `${taskId}.json`));
837
+ await fs4.stat(path5.join(taskDir, subdir, `${taskId}.json`));
1254
838
  return true;
1255
839
  } catch {
1256
840
  }
@@ -1260,11 +844,11 @@ async function taskExistsIn(taskDir, taskId, subdirs) {
1260
844
  async function moveToCorrupt(taskDir, sourceDir, fileName, reason, logger) {
1261
845
  const corruptDir = path5.join(taskDir, "corrupt");
1262
846
  try {
1263
- await fs3.mkdir(corruptDir, { recursive: true });
1264
- await fs3.rename(path5.join(sourceDir, fileName), path5.join(corruptDir, fileName));
847
+ await fs4.mkdir(corruptDir, { recursive: true });
848
+ await fs4.rename(path5.join(sourceDir, fileName), path5.join(corruptDir, fileName));
1265
849
  logger.debug("Moved corrupt task file", { fileName, reason });
1266
850
  } catch {
1267
- await fs3.unlink(path5.join(sourceDir, fileName)).catch(() => {
851
+ await fs4.unlink(path5.join(sourceDir, fileName)).catch(() => {
1268
852
  });
1269
853
  }
1270
854
  }
@@ -1318,2256 +902,237 @@ var init_task_queue3 = __esm({
1318
902
  }
1319
903
  });
1320
904
 
1321
- // dist/paths/validation.js
1322
- function isValidContentId(id) {
1323
- return CONTENT_ID_PATTERN.test(id);
1324
- }
1325
- var BASE58_PATTERN, CONTENT_ID_PATTERN, PHYSICAL_SEGMENT_PATTERN;
1326
- var init_validation2 = __esm({
1327
- "dist/paths/validation.js"() {
1328
- "use strict";
1329
- init_normalize();
1330
- BASE58_PATTERN = "[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]";
1331
- CONTENT_ID_PATTERN = new RegExp(`^${BASE58_PATTERN}{12}$`);
1332
- PHYSICAL_SEGMENT_PATTERN = new RegExp(`\\.${BASE58_PATTERN}{12}(?:\\.[a-z]+)?$`);
1333
- }
1334
- });
905
+ // dist/cli/init.js
906
+ import fs5 from "node:fs/promises";
907
+ import path6 from "node:path";
908
+ import * as p from "@clack/prompts";
1335
909
 
1336
- // dist/id.js
1337
- import { generate } from "short-uuid";
1338
- function generateId() {
1339
- const full = generate();
1340
- return full.substring(0, 12);
1341
- }
1342
- var isValidId;
1343
- var init_id = __esm({
1344
- "dist/id.js"() {
1345
- "use strict";
1346
- init_validation2();
1347
- isValidId = isValidContentId;
910
+ // dist/operating-mode/client-safe-strategy.js
911
+ var ProdClientSafeStrategy = class {
912
+ constructor() {
913
+ this.mode = "prod";
1348
914
  }
1349
- });
1350
-
1351
- // dist/utils/error.js
1352
- function getErrorMessage(err) {
1353
- if (err instanceof Error) {
1354
- return err.message;
915
+ // UI Feature Flags
916
+ supportsBranching() {
917
+ return true;
1355
918
  }
1356
- if (typeof err === "string") {
1357
- return err;
919
+ supportsStatusBadge() {
920
+ return true;
1358
921
  }
1359
- return String(err);
1360
- }
1361
- function isNodeError(err) {
1362
- return err instanceof Error && "code" in err;
1363
- }
1364
- function isNotFoundError2(err) {
1365
- return isNodeError(err) && err.code === "ENOENT";
1366
- }
1367
- function isFileExistsError(err) {
1368
- return isNodeError(err) && err.code === "EEXIST";
1369
- }
1370
- var init_error = __esm({
1371
- "dist/utils/error.js"() {
1372
- "use strict";
922
+ supportsComments() {
923
+ return true;
1373
924
  }
1374
- });
925
+ supportsPullRequests() {
926
+ return true;
927
+ }
928
+ // Simple Data
929
+ getPermissionsFileName() {
930
+ return "permissions.json";
931
+ }
932
+ getGroupsFileName() {
933
+ return "groups.json";
934
+ }
935
+ shouldCommit() {
936
+ return true;
937
+ }
938
+ shouldPush() {
939
+ return true;
940
+ }
941
+ };
942
+ var DevClientSafeStrategy = class {
943
+ constructor() {
944
+ this.mode = "dev";
945
+ }
946
+ // UI Feature Flags
947
+ supportsBranching() {
948
+ return true;
949
+ }
950
+ supportsStatusBadge() {
951
+ return true;
952
+ }
953
+ supportsComments() {
954
+ return true;
955
+ }
956
+ supportsPullRequests() {
957
+ return false;
958
+ }
959
+ // Simple Data
960
+ getPermissionsFileName() {
961
+ return "permissions.json";
962
+ }
963
+ getGroupsFileName() {
964
+ return "groups.json";
965
+ }
966
+ shouldCommit() {
967
+ return true;
968
+ }
969
+ shouldPush() {
970
+ return true;
971
+ }
972
+ };
1375
973
 
1376
- // dist/content-id-index.js
1377
- import fs4 from "node:fs/promises";
1378
- import path6 from "node:path";
1379
- function toLogicalCollectionPath(physicalPath) {
1380
- if (physicalPath === ".")
1381
- return EMPTY_LOGICAL_PATH;
1382
- return physicalPath.split("/").map((seg) => extractSlugFromFilename(seg)).join("/");
1383
- }
1384
- function extractIdFromFilename(filename) {
1385
- if (filename.startsWith(".")) {
1386
- return null;
974
+ // dist/operating-mode/client-unsafe-strategy.js
975
+ import path from "node:path";
976
+ init_config3();
977
+ var ProdStrategy = class extends ProdClientSafeStrategy {
978
+ // All client-safe methods inherited automatically from ProdClientSafeStrategy:
979
+ // - mode, supportsBranching(), supportsStatusBadge(), supportsComments()
980
+ // - supportsPullRequests(), getPermissionsFileName(), getGroupsFileName()
981
+ // - shouldCommit(), shouldPush()
982
+ // Add client-unsafe methods (use Node.js APIs)
983
+ getWorkspaceRoot(_sourceRoot) {
984
+ return path.resolve(process.env.CANOPYCMS_WORKSPACE_ROOT ?? DEFAULT_PROD_WORKSPACE);
1387
985
  }
1388
- const parts = filename.split(".");
1389
- if (parts.length >= 3) {
1390
- const candidate = parts[parts.length - 2];
1391
- if (isValidId(candidate))
1392
- return candidate;
986
+ getContentRoot(sourceRoot) {
987
+ return path.resolve(sourceRoot ?? process.cwd(), "content");
1393
988
  }
1394
- if (parts.length === 2) {
1395
- const candidate = parts[parts.length - 1];
1396
- if (isValidId(candidate))
1397
- return candidate;
989
+ getContentBranchesRoot(sourceRoot) {
990
+ return path.join(this.getWorkspaceRoot(sourceRoot), "content-branches");
1398
991
  }
1399
- return null;
1400
- }
1401
- async function resolveCollectionPath(root, logicalPath) {
1402
- const fs12 = await import("node:fs/promises");
1403
- const path16 = await import("node:path");
1404
- const segments = logicalPath.split("/").filter(Boolean);
1405
- let currentPath = root;
1406
- for (const segment of segments) {
1407
- try {
1408
- const entries = await fs12.readdir(currentPath, { withFileTypes: true });
1409
- const matchingDir = entries.find((entry) => {
1410
- if (!entry.isDirectory())
1411
- return false;
1412
- const logicalName = extractSlugFromFilename(entry.name);
1413
- return logicalName === segment;
1414
- });
1415
- if (matchingDir) {
1416
- currentPath = path16.join(currentPath, matchingDir.name);
1417
- } else {
1418
- return null;
1419
- }
1420
- } catch (err) {
1421
- if (isNotFoundError2(err))
1422
- return null;
1423
- throw err;
1424
- }
992
+ getContentBranchRoot(branchName, sourceRoot) {
993
+ return path.resolve(this.getContentBranchesRoot(sourceRoot), branchName);
1425
994
  }
1426
- return currentPath;
1427
- }
1428
- function extractEntryTypeFromFilename(filename) {
1429
- if (filename.startsWith("."))
1430
- return null;
1431
- const parts = filename.split(".");
1432
- if (parts.length >= 4) {
1433
- const possibleId = parts[parts.length - 2];
1434
- if (isValidId(possibleId)) {
1435
- return parts[0];
1436
- }
995
+ getGitExcludePattern() {
996
+ return ".canopy-meta/";
1437
997
  }
1438
- return null;
1439
- }
1440
- function extractSlugFromFilename(filename, entryTypeName) {
1441
- const parts = filename.split(".");
1442
- if (parts.length >= 3) {
1443
- const possibleId = parts[parts.length - 2];
1444
- if (isValidId(possibleId)) {
1445
- let slugParts = parts.slice(0, parts.length - 2);
1446
- if (entryTypeName && slugParts.length > 1 && slugParts[0] === entryTypeName) {
1447
- slugParts = slugParts.slice(1);
1448
- } else if (parts.length >= 4 && slugParts.length > 1) {
1449
- slugParts = slugParts.slice(1);
1450
- }
1451
- return slugParts.join(".");
1452
- }
998
+ getPermissionsFilePath(root) {
999
+ return path.join(root, this.getPermissionsFileName());
1000
+ }
1001
+ getGroupsFilePath(root) {
1002
+ return path.join(root, this.getGroupsFileName());
1003
+ }
1004
+ getRemoteUrlConfig() {
1005
+ return {
1006
+ shouldAutoInitLocal: false,
1007
+ defaultRemotePath: "",
1008
+ envVarName: "CANOPYCMS_REMOTE_URL",
1009
+ autoDetectRemotePath: path.join(this.getWorkspaceRoot(), "remote.git")
1010
+ };
1011
+ }
1012
+ requiresExistingRepo() {
1013
+ return false;
1014
+ }
1015
+ getSettingsBranchName(config) {
1016
+ if (config.settingsBranch)
1017
+ return config.settingsBranch;
1018
+ const deploymentName = config.deploymentName ?? "prod";
1019
+ return `canopycms-settings-${deploymentName}`;
1453
1020
  }
1454
- if (parts.length === 2) {
1455
- const possibleId = parts[parts.length - 1];
1456
- if (isValidId(possibleId)) {
1457
- return parts[0];
1021
+ getSettingsRoot(sourceRoot) {
1022
+ return path.join(this.getWorkspaceRoot(sourceRoot), "settings");
1023
+ }
1024
+ usesSeparateSettingsBranch() {
1025
+ return true;
1026
+ }
1027
+ validateConfig(config) {
1028
+ if (!config.gitBotAuthorName || !config.gitBotAuthorEmail) {
1029
+ throw new Error("gitBotAuthorName and gitBotAuthorEmail are required in prod mode");
1458
1030
  }
1459
1031
  }
1460
- if (parts.length > 1) {
1461
- return parts.slice(0, -1).join(".");
1032
+ shouldCreateSettingsPR(config) {
1033
+ return config.autoCreateSettingsPR ?? true;
1462
1034
  }
1463
- return filename;
1464
- }
1465
- var EMPTY_LOGICAL_PATH, ContentIdIndex;
1466
- var init_content_id_index = __esm({
1467
- "dist/content-id-index.js"() {
1468
- "use strict";
1469
- init_id();
1470
- init_error();
1471
- EMPTY_LOGICAL_PATH = "";
1472
- ContentIdIndex = class {
1473
- constructor(root) {
1474
- this.idToLocation = /* @__PURE__ */ new Map();
1475
- this.pathToId = /* @__PURE__ */ new Map();
1476
- this.byCollection = /* @__PURE__ */ new Map();
1477
- this.root = path6.resolve(root);
1478
- }
1479
- /**
1480
- * Build index by scanning filenames recursively.
1481
- * Throws if duplicate IDs found (collision detection).
1482
- */
1483
- async buildFromFilenames(startPath = "") {
1484
- await this.scanDirectory(startPath);
1485
- }
1486
- async scanDirectory(relativePath) {
1487
- const absoluteDir = path6.join(this.root, relativePath);
1488
- try {
1489
- const entries = await fs4.readdir(absoluteDir, { withFileTypes: true });
1490
- for (const entry of entries) {
1491
- if (entry.name.startsWith(".") || entry.name === "_ids_") {
1492
- continue;
1493
- }
1494
- const fullRelativePath = path6.join(relativePath, entry.name);
1495
- const id = extractIdFromFilename(entry.name);
1496
- if (id) {
1497
- if (this.idToLocation.has(id)) {
1498
- const existing = this.idToLocation.get(id);
1499
- throw new Error(`ID collision detected: ${id}
1500
- File 1: ${existing.relativePath}
1501
- File 2: ${fullRelativePath}`);
1502
- }
1503
- const location = {
1504
- id,
1505
- // already ContentId from extractIdFromFilename
1506
- type: entry.isDirectory() ? "collection" : "entry",
1507
- relativePath: fullRelativePath
1508
- // filesystem path with embedded IDs
1509
- };
1510
- if (!entry.isDirectory()) {
1511
- const slug = extractSlugFromFilename(entry.name);
1512
- const physicalCollection = path6.dirname(fullRelativePath);
1513
- const collectionPath = toLogicalCollectionPath(physicalCollection);
1514
- location.slug = slug;
1515
- location.collection = collectionPath;
1516
- if (!this.byCollection.has(collectionPath)) {
1517
- this.byCollection.set(collectionPath, /* @__PURE__ */ new Set());
1518
- }
1519
- this.byCollection.get(collectionPath).add(id);
1520
- }
1521
- this.idToLocation.set(id, location);
1522
- this.pathToId.set(fullRelativePath, id);
1523
- }
1524
- if (entry.isDirectory()) {
1525
- await this.scanDirectory(fullRelativePath);
1526
- }
1527
- }
1528
- } catch (err) {
1529
- if (err.code !== "ENOENT") {
1530
- throw err;
1531
- }
1532
- }
1533
- }
1534
- /**
1535
- * Forward lookup: ID → location (O(1))
1536
- */
1537
- findById(id) {
1538
- return this.idToLocation.get(id) || null;
1539
- }
1540
- /**
1541
- * Reverse lookup: path → ID (O(1))
1542
- */
1543
- findByPath(relativePath) {
1544
- return this.pathToId.get(relativePath) || null;
1545
- }
1546
- /**
1547
- * Get all ID locations in the index.
1548
- * Useful for validation and checking references.
1549
- */
1550
- getAllLocations() {
1551
- return Array.from(this.idToLocation.values());
1552
- }
1553
- /**
1554
- * Get all entries in a collection by collection path.
1555
- *
1556
- * Performance: O(1) + O(m) where m is the number of entries in the collection.
1557
- *
1558
- * @param collectionPath - The collection path (e.g., "content/posts")
1559
- * @returns Array of IdLocation objects for entries in the collection
1560
- */
1561
- getEntriesInCollection(collectionPath) {
1562
- const idSet = this.byCollection.get(collectionPath);
1563
- if (!idSet) {
1564
- return [];
1565
- }
1566
- const locations = [];
1567
- for (const id of idSet) {
1568
- const location = this.idToLocation.get(id);
1569
- if (location) {
1570
- locations.push(location);
1571
- }
1572
- }
1573
- return locations;
1574
- }
1575
- /**
1576
- * Add a new entry or collection to the index.
1577
- * Note: This only updates the in-memory index. The file with embedded ID
1578
- * must already exist on disk (created by ContentStore).
1579
- */
1580
- add(location) {
1581
- const id = extractIdFromFilename(path6.basename(location.relativePath));
1582
- if (!id) {
1583
- throw new Error(`Cannot add location without ID in filename: ${location.relativePath}`);
1584
- }
1585
- if (this.idToLocation.has(id)) {
1586
- const existing = this.idToLocation.get(id);
1587
- throw new Error(`ID collision detected: ${id}
1588
- File 1: ${existing.relativePath}
1589
- File 2: ${location.relativePath}`);
1590
- }
1591
- const fullLocation = {
1592
- ...location,
1593
- id
1594
- // already ContentId from extractIdFromFilename
1595
- };
1596
- this.idToLocation.set(id, fullLocation);
1597
- this.pathToId.set(location.relativePath, id);
1598
- if (fullLocation.type === "entry" && fullLocation.collection) {
1599
- if (!this.byCollection.has(fullLocation.collection)) {
1600
- this.byCollection.set(fullLocation.collection, /* @__PURE__ */ new Set());
1601
- }
1602
- this.byCollection.get(fullLocation.collection).add(id);
1603
- }
1604
- }
1605
- /**
1606
- * Remove an entry or collection from the index by ID.
1607
- * Note: This only updates the in-memory index. The file must be deleted separately.
1608
- */
1609
- remove(id) {
1610
- const location = this.idToLocation.get(id);
1611
- if (!location)
1612
- return;
1613
- if (location.type === "entry" && location.collection) {
1614
- const idSet = this.byCollection.get(location.collection);
1615
- if (idSet) {
1616
- idSet.delete(id);
1617
- if (idSet.size === 0) {
1618
- this.byCollection.delete(location.collection);
1619
- }
1620
- }
1621
- }
1622
- this.idToLocation.delete(id);
1623
- this.pathToId.delete(location.relativePath);
1624
- }
1625
- /**
1626
- * Update the path for an existing ID (e.g., after file rename/move).
1627
- * This is used to keep the index in sync when files are renamed.
1628
- */
1629
- updatePath(id, newRelativePath) {
1630
- const location = this.idToLocation.get(id);
1631
- if (!location) {
1632
- throw new Error(`Cannot update path for unknown ID: ${id}`);
1633
- }
1634
- this.pathToId.delete(location.relativePath);
1635
- location.relativePath = newRelativePath;
1636
- if (location.type === "entry") {
1637
- const oldCollection = location.collection;
1638
- location.slug = extractSlugFromFilename(path6.basename(newRelativePath));
1639
- const physicalCollection = path6.dirname(newRelativePath);
1640
- location.collection = toLogicalCollectionPath(physicalCollection);
1641
- if (oldCollection !== location.collection) {
1642
- if (oldCollection) {
1643
- const oldSet = this.byCollection.get(oldCollection);
1644
- if (oldSet) {
1645
- oldSet.delete(id);
1646
- if (oldSet.size === 0) {
1647
- this.byCollection.delete(oldCollection);
1648
- }
1649
- }
1650
- }
1651
- if (location.collection) {
1652
- if (!this.byCollection.has(location.collection)) {
1653
- this.byCollection.set(location.collection, /* @__PURE__ */ new Set());
1654
- }
1655
- this.byCollection.get(location.collection).add(id);
1656
- }
1657
- }
1658
- }
1659
- this.pathToId.set(newRelativePath, id);
1660
- }
1661
- };
1662
- }
1663
- });
1664
-
1665
- // dist/utils/format.js
1666
- var getFormatExtension;
1667
- var init_format = __esm({
1668
- "dist/utils/format.js"() {
1669
- "use strict";
1670
- getFormatExtension = (format) => {
1671
- if (format === "md")
1672
- return ".md";
1673
- if (format === "mdx")
1674
- return ".mdx";
1675
- return ".json";
1676
- };
1035
+ };
1036
+ var DevStrategy = class extends DevClientSafeStrategy {
1037
+ // Inherits client-safe methods from DevClientSafeStrategy
1038
+ getWorkspaceRoot(sourceRoot) {
1039
+ return path.resolve(sourceRoot ?? process.cwd(), ".canopy-dev");
1677
1040
  }
1678
- });
1679
-
1680
- // dist/paths/normalize-server.js
1681
- var init_normalize_server = __esm({
1682
- "dist/paths/normalize-server.js"() {
1683
- "use strict";
1041
+ getContentRoot(sourceRoot) {
1042
+ return path.resolve(sourceRoot ?? process.cwd(), "content");
1684
1043
  }
1685
- });
1686
-
1687
- // dist/paths/resolve.js
1688
- var init_resolve = __esm({
1689
- "dist/paths/resolve.js"() {
1690
- "use strict";
1044
+ getContentBranchesRoot(sourceRoot) {
1045
+ return path.join(this.getWorkspaceRoot(sourceRoot), "content-branches");
1691
1046
  }
1692
- });
1693
-
1694
- // dist/paths/branch.js
1695
- import path7 from "node:path";
1696
- function sanitizeBranchName(branchName) {
1697
- const replaced = branchName.replace(/[^a-zA-Z0-9._-]/g, "-");
1698
- const squashed = replaced.replace(/-+/g, "-");
1699
- const trimmedDots = squashed.replace(/^\.+/, "").replace(/(?<!\.)\.+$/, "");
1700
- return trimmedDots || "branch";
1701
- }
1702
- function resolveBranchPath(options) {
1703
- if (options.branchName.includes("..")) {
1704
- throw new BranchPathError("Branch name cannot contain traversal segments");
1705
- }
1706
- const safeBranch = sanitizeBranchName(options.branchName);
1707
- const strategy = operatingStrategy(options.mode);
1708
- const baseRoot = resolveContentBranchesRoot(options.mode, options.basePathOverride);
1709
- const normalizedBase = path7.resolve(baseRoot);
1710
- const baseWithSep = normalizedBase.endsWith(path7.sep) ? normalizedBase : `${normalizedBase}${path7.sep}`;
1711
- const branchRoot = strategy.getContentBranchRoot(safeBranch, options.basePathOverride);
1712
- const withinBase = (target) => {
1713
- const resolved = path7.resolve(target);
1714
- return resolved === normalizedBase || resolved.startsWith(baseWithSep);
1715
- };
1716
- if (!withinBase(branchRoot)) {
1717
- throw new BranchPathError("Branch path resolves outside the base root");
1047
+ getContentBranchRoot(branchName, sourceRoot) {
1048
+ return path.resolve(this.getContentBranchesRoot(sourceRoot), branchName);
1718
1049
  }
1719
- return { branchRoot, baseRoot: normalizedBase, branchName: safeBranch };
1720
- }
1721
- var BranchPathError, resolveContentBranchesRoot;
1722
- var init_branch = __esm({
1723
- "dist/paths/branch.js"() {
1724
- "use strict";
1725
- init_operating_mode();
1726
- BranchPathError = class extends Error {
1727
- };
1728
- resolveContentBranchesRoot = (mode, override) => {
1729
- return operatingStrategy(mode).getContentBranchesRoot(override);
1730
- };
1050
+ getGitExcludePattern() {
1051
+ return ".canopy-meta/";
1731
1052
  }
1732
- });
1733
-
1734
- // dist/paths/index.js
1735
- var init_paths = __esm({
1736
- "dist/paths/index.js"() {
1737
- "use strict";
1738
- init_normalize();
1739
- init_normalize_server();
1740
- init_validation2();
1741
- init_resolve();
1742
- init_branch();
1743
- }
1744
- });
1745
-
1746
- // dist/content-store.js
1747
- import fs5 from "node:fs/promises";
1748
- import path8 from "node:path";
1749
- import matter from "gray-matter";
1750
- function getDefaultEntryType(entries) {
1751
- if (!entries || entries.length === 0)
1752
- return void 0;
1753
- return entries.find((e) => e.default) || entries[0];
1754
- }
1755
- function validateSlug(slug) {
1756
- if (slug.includes("/")) {
1757
- throw new ContentStoreError("Slugs cannot contain forward slashes. Use nested collections instead.");
1053
+ getPermissionsFilePath(root) {
1054
+ return path.join(root, this.getPermissionsFileName());
1758
1055
  }
1759
- if (slug.includes("\\")) {
1760
- throw new ContentStoreError("Slugs cannot contain backslashes. Use nested collections instead.");
1056
+ getGroupsFilePath(root) {
1057
+ return path.join(root, this.getGroupsFileName());
1761
1058
  }
1762
- }
1763
- var ContentStoreError, ContentStore;
1764
- var init_content_store = __esm({
1765
- "dist/content-store.js"() {
1766
- "use strict";
1767
- init_atomic_write();
1768
- init_content_id_index();
1769
- init_id();
1770
- init_format();
1771
- init_paths();
1772
- ContentStoreError = class extends Error {
1773
- };
1774
- ContentStore = class {
1775
- constructor(root, flatSchema) {
1776
- this.indexLoaded = false;
1777
- this.root = path8.resolve(root);
1778
- this.schemaIndex = new Map(flatSchema.map((item) => [item.logicalPath, item]));
1779
- this._idIndex = new ContentIdIndex(this.root);
1780
- }
1781
- /**
1782
- * Get the ID index, ensuring it's loaded first.
1783
- * This getter automatically loads the index on first access.
1784
- */
1785
- async idIndex() {
1786
- if (!this.indexLoaded) {
1787
- await this._idIndex.buildFromFilenames("content");
1788
- this.indexLoaded = true;
1789
- }
1790
- return this._idIndex;
1791
- }
1792
- /**
1793
- * Get all schema items for iteration.
1794
- * Used internally by ReferenceResolver for path matching.
1795
- */
1796
- getSchemaItems() {
1797
- return this.schemaIndex.values();
1798
- }
1799
- assertSchemaItem(path16) {
1800
- const normalized = normalizeFilesystemPath(path16);
1801
- const item = this.schemaIndex.get(normalized);
1802
- if (!item) {
1803
- throw new ContentStoreError(`Unknown schema item: ${path16}`);
1804
- }
1805
- return item;
1806
- }
1807
- assertCollection(collectionPath) {
1808
- const item = this.assertSchemaItem(collectionPath);
1809
- if (item.type !== "collection") {
1810
- throw new ContentStoreError(`Path is not a collection: ${collectionPath}`);
1811
- }
1812
- return item;
1813
- }
1814
- /**
1815
- * Build absolute and relative paths with security validation.
1816
- * All entries use the unified filename pattern: {type}.{slug}.{id}.{ext}
1817
- *
1818
- * SECURITY BOUNDARY: This method prevents path traversal attacks by:
1819
- * 1. Validating that resolved paths stay within the content root
1820
- * 2. Checking slugs for malicious patterns (via validateSlug)
1821
- * 3. Using path.resolve to normalize paths before validation
1822
- *
1823
- * This validation is performed BEFORE file I/O in resolveDocumentPath(),
1824
- * ensuring permission checks happen before any file system access.
1825
- *
1826
- * @param options.existingId - Optional ID to use (for edits). If not provided, generates new ID.
1827
- * @param options.entryTypeName - For collections with multiple entry types, specify which one to use. Defaults to the default entry type.
1828
- */
1829
- async buildPaths(schemaItem, slug, options = {}) {
1830
- const rootWithSep = this.root.endsWith(path8.sep) ? this.root : `${this.root}${path8.sep}`;
1831
- if (schemaItem.type === "entry-type") {
1832
- const parentPath = schemaItem.parentPath || "";
1833
- const parentCollection = this.schemaIndex.get(parentPath);
1834
- if (!parentCollection || parentCollection.type !== "collection") {
1835
- throw new ContentStoreError(`Parent collection not found for entry type: ${schemaItem.name}`);
1836
- }
1837
- const effectiveSlug = slug || schemaItem.name;
1838
- return this.buildPaths(parentCollection, effectiveSlug, {
1839
- ...options,
1840
- entryTypeName: schemaItem.name
1841
- });
1842
- }
1843
- if (schemaItem.type === "collection") {
1844
- const safeSlug = slug.replace(/^\/+/, "");
1845
- if (!safeSlug) {
1846
- throw new ContentStoreError("Slug is required for collection entries");
1847
- }
1848
- validateSlug(safeSlug);
1849
- let entryTypeConfig;
1850
- if (options.entryTypeName) {
1851
- entryTypeConfig = schemaItem.entries?.find((e) => e.name === options.entryTypeName);
1852
- if (!entryTypeConfig) {
1853
- throw new ContentStoreError(`Entry type '${options.entryTypeName}' not found in collection`);
1854
- }
1855
- } else {
1856
- entryTypeConfig = getDefaultEntryType(schemaItem.entries);
1857
- }
1858
- const format = entryTypeConfig?.format || "json";
1859
- const ext = getFormatExtension(format);
1860
- const entryTypeName = entryTypeConfig?.name || "entry";
1861
- let collectionRoot = await resolveCollectionPath(this.root, schemaItem.logicalPath);
1862
- if (!collectionRoot) {
1863
- collectionRoot = path8.resolve(this.root, schemaItem.logicalPath);
1864
- }
1865
- if (!collectionRoot.startsWith(rootWithSep)) {
1866
- throw new ContentStoreError("Path traversal detected");
1867
- }
1868
- let id = options.existingId;
1869
- let existingFilename;
1870
- let existingEntryType;
1871
- if (!id) {
1872
- const entries = await fs5.readdir(collectionRoot, { withFileTypes: true }).catch(() => []);
1873
- const existingFile = entries.find((entry) => {
1874
- if (entry.isDirectory())
1875
- return false;
1876
- const fileEntryType = extractEntryTypeFromFilename(entry.name);
1877
- const existingSlug = extractSlugFromFilename(entry.name, fileEntryType || void 0);
1878
- return existingSlug === safeSlug;
1879
- });
1880
- if (existingFile) {
1881
- id = extractIdFromFilename(existingFile.name) || void 0;
1882
- existingFilename = existingFile.name;
1883
- existingEntryType = extractEntryTypeFromFilename(existingFile.name) || void 0;
1884
- }
1885
- }
1886
- const finalEntryTypeName = existingEntryType || entryTypeName;
1887
- let filename;
1888
- if (existingFilename && !id) {
1889
- filename = existingFilename;
1890
- } else {
1891
- if (!id) {
1892
- id = generateId();
1893
- }
1894
- filename = `${finalEntryTypeName}.${safeSlug}.${id}${ext}`;
1895
- }
1896
- const resolved = path8.resolve(collectionRoot, filename);
1897
- const collectionRootWithSep = collectionRoot.endsWith(path8.sep) ? collectionRoot : `${collectionRoot}${path8.sep}`;
1898
- if (!resolved.startsWith(collectionRootWithSep)) {
1899
- throw new ContentStoreError("Path traversal detected");
1900
- }
1901
- return {
1902
- absolutePath: resolved,
1903
- relativePath: path8.relative(this.root, resolved),
1904
- id
1905
- };
1906
- }
1907
- throw new ContentStoreError("Invalid schema item type");
1908
- }
1909
- /**
1910
- * Path resolution: resolves a URL path to a schema item
1911
- * - Try as collection + slug (last segment = slug)
1912
- */
1913
- resolvePath(pathSegments) {
1914
- if (pathSegments.length === 0) {
1915
- throw new ContentStoreError("Empty path");
1916
- }
1917
- const logicalPath = pathSegments.join("/");
1918
- const slug = pathSegments[pathSegments.length - 1];
1919
- const collectionPath = pathSegments.slice(0, -1).join("/");
1920
- const normalizedCollection = normalizeFilesystemPath(collectionPath);
1921
- const collection = this.schemaIndex.get(normalizedCollection);
1922
- if (collection?.type === "collection" && collection.entries) {
1923
- return {
1924
- schemaItem: collection,
1925
- slug
1926
- };
1927
- }
1928
- throw new ContentStoreError(`No schema item found for path: ${logicalPath}`);
1929
- }
1930
- async resolveDocumentPath(schemaPath, slug = "") {
1931
- const schemaItem = this.assertSchemaItem(schemaPath);
1932
- return await this.buildPaths(schemaItem, slug);
1933
- }
1934
- async read(collectionPath, slug = "", options = {}) {
1935
- const schemaItem = this.assertSchemaItem(collectionPath);
1936
- const { absolutePath, relativePath } = await this.buildPaths(schemaItem, slug);
1937
- const raw = await fs5.readFile(absolutePath, "utf8");
1938
- let doc;
1939
- let format;
1940
- let fields;
1941
- if (schemaItem.type === "entry-type") {
1942
- format = schemaItem.format;
1943
- fields = schemaItem.schema;
1944
- } else {
1945
- const defaultEntry = getDefaultEntryType(schemaItem.entries);
1946
- format = defaultEntry?.format || "json";
1947
- fields = defaultEntry?.schema || [];
1948
- }
1949
- if (format === "json") {
1950
- const data = JSON.parse(raw);
1951
- doc = {
1952
- collection: schemaItem.logicalPath,
1953
- collectionName: schemaItem.name,
1954
- format: "json",
1955
- data,
1956
- relativePath,
1957
- absolutePath
1958
- };
1959
- } else {
1960
- const parsed = matter(raw);
1961
- doc = {
1962
- collection: schemaItem.logicalPath,
1963
- collectionName: schemaItem.name,
1964
- format,
1965
- data: parsed.data ?? {},
1966
- body: parsed.content,
1967
- relativePath,
1968
- absolutePath
1969
- };
1970
- }
1971
- if (options.resolveReferences !== false) {
1972
- doc.data = await this.resolveReferencesInData(doc.data, fields);
1973
- }
1974
- return doc;
1975
- }
1976
- async write(collectionPath, slug = "", input, entryTypeName) {
1977
- const idIndex = await this.idIndex();
1978
- const schemaItem = this.assertSchemaItem(collectionPath);
1979
- let expectedFormat;
1980
- if (schemaItem.type === "entry-type") {
1981
- expectedFormat = schemaItem.format;
1982
- } else {
1983
- let entryTypeConfig;
1984
- if (entryTypeName) {
1985
- entryTypeConfig = schemaItem.entries?.find((e) => e.name === entryTypeName);
1986
- if (!entryTypeConfig) {
1987
- throw new ContentStoreError(`Entry type '${entryTypeName}' not found in collection`);
1988
- }
1989
- } else {
1990
- entryTypeConfig = getDefaultEntryType(schemaItem.entries);
1991
- }
1992
- expectedFormat = entryTypeConfig?.format || "json";
1993
- }
1994
- if (expectedFormat !== input.format) {
1995
- throw new ContentStoreError(`Format mismatch: expects ${expectedFormat}, got ${input.format}`);
1996
- }
1997
- const { absolutePath, relativePath, id } = await this.buildPaths(schemaItem, slug, {
1998
- entryTypeName
1999
- });
2000
- await fs5.mkdir(path8.dirname(absolutePath), { recursive: true });
2001
- if (input.format === "json") {
2002
- const json = JSON.stringify(input.data ?? {}, null, 2);
2003
- await atomicWriteFile(absolutePath, `${json}
2004
- `);
2005
- if (id) {
2006
- const existing = idIndex.findById(id);
2007
- if (existing) {
2008
- if (existing.relativePath !== relativePath) {
2009
- idIndex.updatePath(existing.id, relativePath);
2010
- }
2011
- } else {
2012
- idIndex.add({
2013
- type: "entry",
2014
- relativePath,
2015
- collection: collectionPath,
2016
- slug: slug || void 0
2017
- });
2018
- }
2019
- }
2020
- return {
2021
- collection: schemaItem.logicalPath,
2022
- collectionName: schemaItem.name,
2023
- format: "json",
2024
- data: input.data ?? {},
2025
- relativePath,
2026
- absolutePath
2027
- };
2028
- }
2029
- const file = matter.stringify(input.body, input.data ?? {});
2030
- await atomicWriteFile(absolutePath, file);
2031
- if (id) {
2032
- const existing = idIndex.findById(id);
2033
- if (existing) {
2034
- if (existing.relativePath !== relativePath) {
2035
- idIndex.updatePath(existing.id, relativePath);
2036
- }
2037
- } else {
2038
- idIndex.add({
2039
- type: "entry",
2040
- relativePath,
2041
- collection: collectionPath,
2042
- slug: slug || void 0
2043
- });
2044
- }
2045
- }
2046
- return {
2047
- collection: schemaItem.logicalPath,
2048
- collectionName: schemaItem.name,
2049
- format: input.format,
2050
- data: input.data ?? {},
2051
- body: input.body,
2052
- relativePath,
2053
- absolutePath
2054
- };
2055
- }
2056
- /**
2057
- * Read an entry by its ID (UUID).
2058
- * Returns null if the ID doesn't exist or points to a collection.
2059
- */
2060
- async readById(id) {
2061
- const idIndex = await this.idIndex();
2062
- const location = idIndex.findById(id);
2063
- if (!location || location.type !== "entry")
2064
- return null;
2065
- return this.read(location.collection, location.slug);
2066
- }
2067
- /**
2068
- * Get the ID for an entry given its collection and slug.
2069
- * Returns null if no ID exists yet.
2070
- */
2071
- async getIdForEntry(collectionPath, slug) {
2072
- const idIndex = await this.idIndex();
2073
- const { relativePath } = await this.buildPaths(this.assertCollection(collectionPath), slug);
2074
- return idIndex.findByPath(relativePath);
2075
- }
2076
- /**
2077
- * Delete an entry and remove it from the index.
2078
- */
2079
- async delete(collectionPath, slug) {
2080
- const idIndex = await this.idIndex();
2081
- const collection = this.assertCollection(collectionPath);
2082
- const { absolutePath, relativePath } = await this.buildPaths(collection, slug);
2083
- const id = idIndex.findByPath(relativePath);
2084
- await fs5.unlink(absolutePath);
2085
- if (id) {
2086
- idIndex.remove(id);
2087
- }
2088
- }
2089
- /**
2090
- * Rename an entry by changing its slug (middle segment of filename).
2091
- * Entry filename pattern: {entryTypeName}.{slug}.{id}.{ext}
2092
- *
2093
- * @param collectionPath - Logical path to the collection
2094
- * @param currentSlug - Current slug of the entry
2095
- * @param newSlug - New slug (must be unique within collection)
2096
- * @returns Object with new logical path
2097
- * @throws ContentStoreError if entry doesn't exist, new slug conflicts, or validation fails
2098
- */
2099
- async renameEntry(collectionPath, currentSlug, newSlug) {
2100
- const idIndex = await this.idIndex();
2101
- const collection = this.assertCollection(collectionPath);
2102
- validateSlug(newSlug);
2103
- const safeNewSlug = newSlug.replace(/^\/+/, "");
2104
- if (!safeNewSlug) {
2105
- throw new ContentStoreError("New slug cannot be empty");
2106
- }
2107
- if (!/^[a-z0-9][a-z0-9-]*$/.test(safeNewSlug)) {
2108
- throw new ContentStoreError("Slug must start with a letter or number and contain only lowercase letters, numbers, and hyphens");
2109
- }
2110
- const { absolutePath: currentPath, relativePath: currentRelPath } = await this.buildPaths(collection, currentSlug);
2111
- try {
2112
- await fs5.access(currentPath);
2113
- } catch {
2114
- throw new ContentStoreError(`Entry not found: ${currentSlug}`);
2115
- }
2116
- if (currentSlug === safeNewSlug) {
2117
- return { newPath: `${collectionPath}/${currentSlug}` };
2118
- }
2119
- const currentFilename = path8.basename(currentPath);
2120
- const parts = currentFilename.split(".");
2121
- if (parts.length < 4) {
2122
- throw new ContentStoreError(`Invalid entry filename format: ${currentFilename}`);
2123
- }
2124
- const entryTypeName = parts[0];
2125
- const contentId = parts[parts.length - 2];
2126
- const ext = `.${parts[parts.length - 1]}`;
2127
- const newFilename = `${entryTypeName}.${safeNewSlug}.${contentId}${ext}`;
2128
- const parentDir = path8.dirname(currentPath);
2129
- const newPath = path8.join(parentDir, newFilename);
2130
- try {
2131
- const entries = await fs5.readdir(parentDir, { withFileTypes: true });
2132
- for (const entry of entries) {
2133
- if (entry.isDirectory())
2134
- continue;
2135
- const existingSlug = extractSlugFromFilename(entry.name, entryTypeName);
2136
- if (existingSlug === safeNewSlug) {
2137
- throw new ContentStoreError(`Entry with slug "${safeNewSlug}" already exists in collection "${collectionPath}"`);
2138
- }
2139
- }
2140
- } catch (err) {
2141
- if (err instanceof ContentStoreError) {
2142
- throw err;
2143
- }
2144
- }
2145
- await fs5.rename(currentPath, newPath);
2146
- const newRelativePath = path8.relative(this.root, newPath);
2147
- const entryId = idIndex.findByPath(currentRelPath);
2148
- if (entryId) {
2149
- idIndex.updatePath(entryId, newRelativePath);
2150
- }
2151
- const newLogicalPath = `${collectionPath}/${safeNewSlug}`;
2152
- return { newPath: newLogicalPath };
2153
- }
2154
- /**
2155
- * List all entries in a collection.
2156
- * Returns array of entry metadata (relativePath, collection, slug).
2157
- * Returns empty array if the collection doesn't exist.
2158
- */
2159
- async listCollectionEntries(collectionPath) {
2160
- const idIndex = await this.idIndex();
2161
- const normalized = normalizeFilesystemPath(collectionPath);
2162
- let item = this.schemaIndex.get(normalized);
2163
- if (!item) {
2164
- for (const schemaItem of this.schemaIndex.values()) {
2165
- if (schemaItem.type === "collection") {
2166
- const lastSegment = schemaItem.logicalPath.split("/").pop();
2167
- if (lastSegment === collectionPath) {
2168
- item = schemaItem;
2169
- break;
2170
- }
2171
- }
2172
- }
2173
- }
2174
- if (!item || item.type !== "collection") {
2175
- return [];
2176
- }
2177
- const collection = item;
2178
- const baseEntries = idIndex.getEntriesInCollection(collection.logicalPath);
2179
- const entries = [];
2180
- for (const location of baseEntries) {
2181
- if (location.type === "entry" && location.slug) {
2182
- if (location.collection === collection.logicalPath || location.collection?.startsWith(collection.logicalPath + "/")) {
2183
- entries.push({
2184
- relativePath: location.relativePath,
2185
- collection: location.collection,
2186
- slug: location.slug
2187
- });
2188
- }
2189
- }
2190
- }
2191
- return entries;
2192
- }
2193
- /**
2194
- * Recursively resolve reference fields in data.
2195
- * This traverses objects, arrays, and blocks to find and resolve all reference fields.
2196
- */
2197
- async resolveReferencesInData(data, fields) {
2198
- const resolved = { ...data };
2199
- const idIndex = await this.idIndex();
2200
- for (const field of fields) {
2201
- const value = data[field.name];
2202
- if (field.type === "reference") {
2203
- if (typeof value === "string" && value) {
2204
- resolved[field.name] = await this.resolveSingleReference(value, idIndex);
2205
- } else if (field.list && Array.isArray(value)) {
2206
- resolved[field.name] = await Promise.all(value.map((id) => typeof id === "string" ? this.resolveSingleReference(id, idIndex) : null));
2207
- }
2208
- } else if (field.type === "object" && value) {
2209
- const objectField = field;
2210
- if (!objectField.fields)
2211
- continue;
2212
- if (objectField.list && Array.isArray(value)) {
2213
- resolved[field.name] = await Promise.all(value.map((item) => typeof item === "object" && item !== null ? this.resolveReferencesInData(item, objectField.fields) : item));
2214
- } else if (typeof value === "object") {
2215
- resolved[field.name] = await this.resolveReferencesInData(value, objectField.fields);
2216
- }
2217
- } else if (field.type === "block" && Array.isArray(value)) {
2218
- const blockField = field;
2219
- resolved[field.name] = await Promise.all(value.map(async (block) => {
2220
- const b = block;
2221
- if (!b || typeof b.value !== "object")
2222
- return block;
2223
- const template = blockField.templates.find((t) => t.name === b.template);
2224
- if (!template)
2225
- return block;
2226
- return {
2227
- ...b,
2228
- value: await this.resolveReferencesInData(b.value, template.fields)
2229
- };
2230
- }));
2231
- }
2232
- }
2233
- return resolved;
2234
- }
2235
- /**
2236
- * Resolve a single reference ID to full entry data.
2237
- * Returns null if the reference is invalid or missing.
2238
- * Includes id, slug, and collection fields for debugging.
2239
- */
2240
- async resolveSingleReference(id, idIndex) {
2241
- try {
2242
- const location = idIndex.findById(id);
2243
- if (!location || location.type !== "entry" || !location.collection || !location.slug) {
2244
- return null;
2245
- }
2246
- const doc = await this.read(location.collection, location.slug, {
2247
- resolveReferences: false
2248
- });
2249
- return {
2250
- id,
2251
- slug: location.slug,
2252
- collection: location.collection,
2253
- ...doc.data
2254
- };
2255
- } catch (error) {
2256
- console.error(`Failed to resolve reference ${id}:`, error);
2257
- return null;
2258
- }
2259
- }
2260
- };
2261
- }
2262
- });
2263
-
2264
- // dist/schema/meta-loader.js
2265
- import { promises as fs6 } from "fs";
2266
- import { join as join2 } from "pathe";
2267
- import { z as z6 } from "zod";
2268
- import chokidar from "chokidar";
2269
- function stripEmbeddedIdFromName(name) {
2270
- return extractSlugFromFilename(name);
2271
- }
2272
- async function scanForCollectionMeta(baseDir, relativePath = "") {
2273
- const collections = [];
2274
- try {
2275
- const entries = await fs6.readdir(baseDir, { withFileTypes: true });
2276
- for (const entry of entries) {
2277
- if (!entry.isDirectory())
2278
- continue;
2279
- const folderName = entry.name;
2280
- const logicalName = stripEmbeddedIdFromName(folderName);
2281
- const collectionContentId = extractIdFromFilename(folderName) ?? void 0;
2282
- const folderPath = relativePath ? `${relativePath}/${logicalName}` : logicalName;
2283
- const absolutePath = join2(baseDir, folderName);
2284
- const metaPath = join2(absolutePath, ".collection.json");
2285
- try {
2286
- await fs6.access(metaPath);
2287
- const content = await fs6.readFile(metaPath, "utf-8");
2288
- const parsed = JSON.parse(content);
2289
- const meta = collectionMetaSchema.parse(parsed);
2290
- collections.push({
2291
- ...meta,
2292
- path: folderPath,
2293
- // Path derived from folder name
2294
- contentId: collectionContentId
2295
- });
2296
- const nestedCollections = await scanForCollectionMeta(absolutePath, folderPath);
2297
- collections.push(...nestedCollections);
2298
- } catch (err) {
2299
- if (err.code !== "ENOENT") {
2300
- console.error(`Error loading ${metaPath}:`, err);
2301
- throw new Error(`Invalid .collection.json in ${folderPath}: ${err.message}`);
2302
- }
2303
- const nestedCollections = await scanForCollectionMeta(absolutePath, folderPath);
2304
- collections.push(...nestedCollections);
2305
- }
2306
- }
2307
- return collections;
2308
- } catch (err) {
2309
- if (err.code === "ENOENT") {
2310
- return [];
2311
- }
2312
- throw err;
2313
- }
2314
- }
2315
- async function loadCollectionMetaFiles(contentRoot) {
2316
- let root = null;
2317
- const rootMetaPath = join2(contentRoot, ".collection.json");
2318
- try {
2319
- await fs6.access(rootMetaPath);
2320
- } catch (err) {
2321
- if (err.code === "ENOENT") {
2322
- } else {
2323
- throw err;
2324
- }
2325
- }
2326
- try {
2327
- const content = await fs6.readFile(rootMetaPath, "utf-8");
2328
- const parsed = JSON.parse(content);
2329
- root = rootCollectionMetaSchema.parse(parsed);
2330
- } catch (err) {
2331
- const errno = err.code;
2332
- if (errno !== "ENOENT") {
2333
- throw new Error(`Invalid root .collection.json`);
2334
- }
2335
- }
2336
- const collections = await scanForCollectionMeta(contentRoot);
2337
- return { root, collections };
2338
- }
2339
- function resolveEntryTypes(entryTypes, entrySchemaRegistry, contextName) {
2340
- return entryTypes.map((entryType) => {
2341
- const resolvedSchema = entrySchemaRegistry[entryType.schema];
2342
- if (!resolvedSchema) {
2343
- throw new Error(`Schema reference "${entryType.schema}" in entry type "${entryType.name}" (${contextName}) not found in registry. Available schemas: ${Object.keys(entrySchemaRegistry).join(", ")}`);
2344
- }
1059
+ getRemoteUrlConfig() {
2345
1060
  return {
2346
- name: entryType.name,
2347
- label: entryType.label,
2348
- format: entryType.format,
2349
- schema: resolvedSchema,
2350
- schemaRef: entryType.schema,
2351
- default: entryType.default,
2352
- maxItems: entryType.maxItems
1061
+ shouldAutoInitLocal: true,
1062
+ defaultRemotePath: ".canopy-dev/remote.git",
1063
+ envVarName: "CANOPYCMS_REMOTE_URL"
2353
1064
  };
2354
- });
2355
- }
2356
- function resolveCollectionMeta(meta, entrySchemaRegistry, allCollections) {
2357
- const entries = meta.entries && meta.entries.length > 0 ? resolveEntryTypes(meta.entries, entrySchemaRegistry, `collection "${meta.name}"`) : void 0;
2358
- const nestedCollections = allCollections.filter((col) => {
2359
- return col.path.startsWith(`${meta.path}/`) && col.path.split("/").length === meta.path.split("/").length + 1;
2360
- });
2361
- const collections = nestedCollections.length > 0 ? nestedCollections.map((nestedMeta) => resolveCollectionMeta(nestedMeta, entrySchemaRegistry, allCollections)) : void 0;
2362
- return {
2363
- name: meta.name,
2364
- label: meta.label,
2365
- path: meta.path,
2366
- contentId: meta.contentId,
2367
- ...entries && { entries },
2368
- ...meta.order && { order: meta.order },
2369
- ...collections && { collections }
2370
- };
2371
- }
2372
- function resolveCollectionReferences(metaFiles, entrySchemaRegistry) {
2373
- const result = {};
2374
- if (metaFiles.root?.label) {
2375
- result.label = metaFiles.root.label;
2376
- }
2377
- if (metaFiles.root?.entries && metaFiles.root.entries.length > 0) {
2378
- result.entries = resolveEntryTypes(metaFiles.root.entries, entrySchemaRegistry, "root collection");
2379
- }
2380
- if (metaFiles.root?.order) {
2381
- result.order = metaFiles.root.order;
2382
1065
  }
2383
- const topLevelCollections = metaFiles.collections.filter((meta) => !meta.path.includes("/"));
2384
- if (topLevelCollections.length > 0) {
2385
- result.collections = topLevelCollections.map((meta) => resolveCollectionMeta(meta, entrySchemaRegistry, metaFiles.collections));
2386
- }
2387
- return result;
2388
- }
2389
- var entryTypeMetaSchema, collectionMetaSchema, rootCollectionMetaSchema;
2390
- var init_meta_loader = __esm({
2391
- "dist/schema/meta-loader.js"() {
2392
- "use strict";
2393
- init_content_id_index();
2394
- entryTypeMetaSchema = z6.object({
2395
- name: z6.string().min(1),
2396
- format: z6.enum(["md", "mdx", "json"]),
2397
- schema: z6.string().min(1),
2398
- // Entry schema registry key (validated at resolution time)
2399
- label: z6.string().optional(),
2400
- default: z6.boolean().optional(),
2401
- maxItems: z6.number().int().positive().optional()
2402
- });
2403
- collectionMetaSchema = z6.object({
2404
- name: z6.string().min(1),
2405
- label: z6.string().optional(),
2406
- entries: z6.array(entryTypeMetaSchema).optional(),
2407
- order: z6.array(z6.string())
2408
- // Embedded IDs for ordering items (required)
2409
- }).refine((data) => data.entries && data.entries.length > 0, {
2410
- message: "Collection must have at least one entry type"
2411
- });
2412
- rootCollectionMetaSchema = z6.object({
2413
- label: z6.string().optional(),
2414
- entries: z6.array(entryTypeMetaSchema).optional(),
2415
- order: z6.array(z6.string()).optional()
2416
- // Embedded IDs for ordering items
2417
- });
2418
- }
2419
- });
2420
-
2421
- // dist/schema/resolver.js
2422
- async function resolveSchema(contentRoot, entrySchemaRegistry) {
2423
- const metaFiles = await loadCollectionMetaFiles(contentRoot);
2424
- const sources = [];
2425
- if (metaFiles.root) {
2426
- sources.push({
2427
- path: ".collection.json",
2428
- type: "root",
2429
- collections: []
2430
- });
2431
- }
2432
- for (const collection of metaFiles.collections) {
2433
- sources.push({
2434
- path: `${collection.path}/.collection.json`,
2435
- type: "collection",
2436
- collections: [collection.name]
2437
- });
2438
- }
2439
- const schema = resolveCollectionReferences(metaFiles, entrySchemaRegistry);
2440
- return { schema, sources };
2441
- }
2442
- function isValidSchema(schema) {
2443
- const hasEntries = !!(schema.entries && schema.entries.length > 0);
2444
- const hasCollections = !!(schema.collections && schema.collections.length > 0);
2445
- return hasEntries || hasCollections;
2446
- }
2447
- var init_resolver = __esm({
2448
- "dist/schema/resolver.js"() {
2449
- "use strict";
2450
- init_meta_loader();
2451
- }
2452
- });
2453
-
2454
- // dist/branch-schema-cache.js
2455
- import fs7 from "node:fs/promises";
2456
- import path9 from "node:path";
2457
- var SCHEMA_CACHE_VERSION, BranchSchemaCache;
2458
- var init_branch_schema_cache = __esm({
2459
- "dist/branch-schema-cache.js"() {
2460
- "use strict";
2461
- init_resolver();
2462
- init_flatten();
2463
- SCHEMA_CACHE_VERSION = 2;
2464
- BranchSchemaCache = class {
2465
- constructor(mode) {
2466
- this.mode = mode;
2467
- }
2468
- /**
2469
- * Get schema for a branch (loads from cache or resolves fresh).
2470
- *
2471
- * @param branchRoot - Root directory of the branch (e.g., .canopy-prod-sim/content-branches/main)
2472
- * @param entrySchemaRegistry - Map of schema names to field definitions
2473
- * @param contentRootName - Name of content directory (e.g., "content") from config
2474
- * @returns Resolved schema tree and flattened schema
2475
- */
2476
- async getSchema(branchRoot, entrySchemaRegistry, contentRootName = "content") {
2477
- if (this.mode === "dev") {
2478
- if (!this.devModeCache) {
2479
- const contentRoot = path9.join(branchRoot, contentRootName);
2480
- const result = await resolveSchema(contentRoot, entrySchemaRegistry);
2481
- if (!isValidSchema(result.schema)) {
2482
- throw new Error(`No schema found in ${contentRoot}. Create .collection.json files with references to field schemas defined in your entry schema registry.`);
2483
- }
2484
- const flatSchema = flattenSchema(result.schema, contentRootName);
2485
- this.devModeCache = {
2486
- schema: result.schema,
2487
- flatSchema
2488
- };
2489
- }
2490
- return this.devModeCache;
2491
- }
2492
- return this.loadFromCacheOrResolve(branchRoot, entrySchemaRegistry, contentRootName);
2493
- }
2494
- /**
2495
- * Load schema from cache or resolve fresh if cache is missing or stale.
2496
- */
2497
- async loadFromCacheOrResolve(branchRoot, entrySchemaRegistry, contentRootName) {
2498
- const contentRoot = path9.join(branchRoot, contentRootName);
2499
- const cacheDir = path9.join(branchRoot, ".canopy-meta");
2500
- const cachePath = path9.join(cacheDir, "schema-cache.json");
2501
- const stalePath = path9.join(cacheDir, "schema-cache.stale");
2502
- let cacheData = null;
2503
- try {
2504
- const staleExists = await fs7.access(stalePath).then(() => true).catch(() => false);
2505
- if (!staleExists) {
2506
- const cacheContent = await fs7.readFile(cachePath, "utf-8");
2507
- cacheData = JSON.parse(cacheContent);
2508
- }
2509
- } catch {
2510
- cacheData = null;
2511
- }
2512
- if (cacheData && cacheData.version === SCHEMA_CACHE_VERSION) {
2513
- return { schema: cacheData.schema, flatSchema: cacheData.flatSchema };
2514
- }
2515
- const result = await resolveSchema(contentRoot, entrySchemaRegistry);
2516
- if (!isValidSchema(result.schema)) {
2517
- throw new Error(`No schema found in ${contentRoot}. Create .collection.json files with references to field schemas defined in your entry schema registry.`);
2518
- }
2519
- const flatSchema = flattenSchema(result.schema, contentRootName);
2520
- await fs7.mkdir(cacheDir, { recursive: true });
2521
- const newCache = {
2522
- version: SCHEMA_CACHE_VERSION,
2523
- schema: result.schema,
2524
- flatSchema,
2525
- cachedAt: (/* @__PURE__ */ new Date()).toISOString()
2526
- };
2527
- const tmpPath = path9.join(cacheDir, `schema-cache.tmp.${Date.now()}.${Math.random()}.json`);
2528
- await fs7.writeFile(tmpPath, JSON.stringify(newCache, null, 2), "utf-8");
2529
- await fs7.rename(tmpPath, cachePath);
2530
- try {
2531
- await fs7.unlink(stalePath);
2532
- } catch {
2533
- }
2534
- return { schema: result.schema, flatSchema };
2535
- }
2536
- /**
2537
- * Invalidate cache for a branch (creates .stale marker).
2538
- *
2539
- * @param branchRoot - Root directory of the branch
2540
- */
2541
- async invalidate(branchRoot) {
2542
- if (this.mode === "dev") {
2543
- this.devModeCache = void 0;
2544
- return;
2545
- }
2546
- const cacheDir = path9.join(branchRoot, ".canopy-meta");
2547
- const stalePath = path9.join(cacheDir, "schema-cache.stale");
2548
- await fs7.mkdir(cacheDir, { recursive: true });
2549
- await fs7.writeFile(stalePath, "", "utf-8");
2550
- }
2551
- /**
2552
- * Clear all caches (for testing).
2553
- * In dev mode, clears in-memory cache.
2554
- * In prod/prod-sim modes, this would need to traverse all branch directories.
2555
- */
2556
- async clearAll() {
2557
- if (this.mode === "dev") {
2558
- this.devModeCache = void 0;
2559
- }
2560
- }
2561
- };
2562
- }
2563
- });
2564
-
2565
- // dist/ai/json-to-markdown.js
2566
- function entryToMarkdown(entry, config) {
2567
- const parts = [];
2568
- parts.push("---");
2569
- if (entry.data.title) {
2570
- parts.push(`title: ${yamlValue(String(entry.data.title))}`);
2571
- }
2572
- parts.push(`slug: ${yamlValue(entry.slug)}`);
2573
- parts.push(`collection: ${yamlValue(entry.collection)}`);
2574
- parts.push(`type: ${yamlValue(entry.entryType)}`);
2575
- parts.push("---");
2576
- parts.push("");
2577
- const skipFields = /* @__PURE__ */ new Set();
2578
- if (entry.data.title)
2579
- skipFields.add("title");
2580
- if (entry.format === "md" || entry.format === "mdx") {
2581
- parts.push(...renderMarkdownEntry(entry, config, skipFields));
2582
- } else {
2583
- parts.push(...renderJsonEntry(entry, config, skipFields));
2584
- }
2585
- return parts.join("\n");
2586
- }
2587
- function renderMarkdownEntry(entry, config, skipFields) {
2588
- const parts = [];
2589
- const bodyFieldTypes = /* @__PURE__ */ new Set(["rich-text", "markdown", "mdx"]);
2590
- const metadataFields = entry.fields.filter((f) => !bodyFieldTypes.has(f.type) && !skipFields.has(f.name));
2591
- for (const field of metadataFields) {
2592
- const value = entry.data[field.name];
2593
- if (value === void 0 || value === null)
2594
- continue;
2595
- const transformed = applyFieldTransform(entry, field, value, config);
2596
- if (transformed !== void 0) {
2597
- parts.push(transformed);
2598
- parts.push("");
2599
- continue;
2600
- }
2601
- const label = field.label || field.name;
2602
- parts.push(`**${label}:** ${formatInlineValue(field, value)}`);
2603
- }
2604
- if (parts.length > 0) {
2605
- parts.push("");
2606
- }
2607
- if (entry.body) {
2608
- parts.push(entry.body.trim());
2609
- parts.push("");
2610
- }
2611
- return parts;
2612
- }
2613
- function renderJsonEntry(entry, config, skipFields) {
2614
- const parts = [];
2615
- for (const field of entry.fields) {
2616
- if (skipFields.has(field.name))
2617
- continue;
2618
- const value = entry.data[field.name];
2619
- if (value === void 0 || value === null)
2620
- continue;
2621
- const rendered = renderField(field, value, 2, entry, config);
2622
- if (rendered) {
2623
- parts.push(rendered);
2624
- parts.push("");
2625
- }
2626
- }
2627
- return parts;
2628
- }
2629
- function renderField(field, value, depth, entry, config) {
2630
- const transformed = applyFieldTransform(entry, field, value, config);
2631
- if (transformed !== void 0) {
2632
- return transformed;
2633
- }
2634
- const label = field.label || field.name;
2635
- const heading = "#".repeat(Math.min(depth, 6));
2636
- const descriptionLine = "description" in field && field.description ? `
2637
-
2638
- *${field.description}*` : "";
2639
- if (field.list && Array.isArray(value)) {
2640
- return renderListField(field, value, depth, label, heading, descriptionLine, entry, config);
2641
- }
2642
- switch (field.type) {
2643
- case "string":
2644
- case "number":
2645
- case "datetime":
2646
- return `${heading} ${label}${descriptionLine}
2647
-
2648
- ${String(value)}`;
2649
- case "boolean":
2650
- return `${heading} ${label}${descriptionLine}
2651
-
2652
- ${value ? "Yes" : "No"}`;
2653
- case "rich-text":
2654
- case "markdown":
2655
- case "mdx":
2656
- return `${heading} ${label}${descriptionLine}
2657
-
2658
- ${String(value)}`;
2659
- case "image":
2660
- return `${heading} ${label}${descriptionLine}
2661
-
2662
- ![${label}](${String(value)})`;
2663
- case "code":
2664
- return `${heading} ${label}${descriptionLine}
2665
-
2666
- \`\`\`
2667
- ${String(value)}
2668
- \`\`\``;
2669
- case "select":
2670
- return renderSelectField(field, value, heading, label, descriptionLine);
2671
- case "reference":
2672
- return renderReferenceField(value, heading, label, descriptionLine);
2673
- case "object":
2674
- return renderObjectField(field, value, depth, heading, label, descriptionLine, entry, config);
2675
- case "block":
2676
- return renderBlockField(field, value, depth, heading, label, descriptionLine, entry, config);
2677
- default:
2678
- return `${heading} ${label}${descriptionLine}
2679
-
2680
- ${String(value)}`;
2681
- }
2682
- }
2683
- function renderListField(field, values, depth, label, heading, descriptionLine, entry, config) {
2684
- if (values.length === 0)
2685
- return "";
2686
- const isComplex = field.type === "object" || field.type === "block";
2687
- if (isComplex) {
2688
- const items2 = values.map((item, i) => {
2689
- const itemLabel = `${label} ${i + 1}`;
2690
- const itemHeading = "#".repeat(Math.min(depth + 1, 6));
2691
- if (field.type === "object" && typeof item === "object" && item !== null) {
2692
- const objectField = field;
2693
- const subFields = objectField.fields.map((f) => {
2694
- const v = item[f.name];
2695
- if (v === void 0 || v === null)
2696
- return "";
2697
- return renderField(f, v, depth + 2, entry, config);
2698
- }).filter(Boolean);
2699
- return `${itemHeading} ${itemLabel}
2700
-
2701
- ${subFields.join("\n\n")}`;
2702
- }
2703
- return `${itemHeading} ${itemLabel}
2704
-
2705
- ${String(item)}`;
2706
- }).filter(Boolean);
2707
- return `${heading} ${label}${descriptionLine}
2708
-
2709
- ${items2.join("\n\n")}`;
2710
- }
2711
- const items = values.map((v) => `- ${formatInlineValue(field, v)}`).join("\n");
2712
- return `${heading} ${label}${descriptionLine}
2713
-
2714
- ${items}`;
2715
- }
2716
- function renderSelectField(field, value, heading, label, descriptionLine) {
2717
- if (Array.isArray(value)) {
2718
- return `${heading} ${label}${descriptionLine}
2719
-
2720
- ${value.map((v) => resolveSelectLabel(field, v)).join(", ")}`;
2721
- }
2722
- return `${heading} ${label}${descriptionLine}
2723
-
2724
- ${resolveSelectLabel(field, value)}`;
2725
- }
2726
- function resolveSelectLabel(field, value) {
2727
- const strValue = String(value);
2728
- for (const opt of field.options) {
2729
- if (typeof opt === "string") {
2730
- if (opt === strValue)
2731
- return opt;
2732
- } else {
2733
- if (opt.value === strValue)
2734
- return opt.label;
2735
- }
2736
- }
2737
- return strValue;
2738
- }
2739
- function renderReferenceField(value, heading, label, descriptionLine) {
2740
- if (Array.isArray(value)) {
2741
- const items = value.map((v) => `- ${formatReference(v)}`).join("\n");
2742
- return `${heading} ${label}${descriptionLine}
2743
-
2744
- ${items}`;
2745
- }
2746
- return `${heading} ${label}${descriptionLine}
2747
-
2748
- ${formatReference(value)}`;
2749
- }
2750
- function formatReference(value) {
2751
- if (typeof value === "object" && value !== null) {
2752
- const ref = value;
2753
- const display = ref.title || ref.name || ref.slug || ref.id;
2754
- if (display)
2755
- return String(display);
2756
- }
2757
- return String(value);
2758
- }
2759
- function renderObjectField(field, value, depth, heading, label, descriptionLine, entry, config) {
2760
- if (typeof value !== "object" || value === null) {
2761
- return `${heading} ${label}${descriptionLine}
2762
-
2763
- ${String(value)}`;
2764
- }
2765
- const obj = value;
2766
- const subFields = field.fields.map((f) => {
2767
- const v = obj[f.name];
2768
- if (v === void 0 || v === null)
2769
- return "";
2770
- return renderField(f, v, depth + 1, entry, config);
2771
- }).filter(Boolean);
2772
- if (subFields.length === 0)
2773
- return "";
2774
- return `${heading} ${label}${descriptionLine}
2775
-
2776
- ${subFields.join("\n\n")}`;
2777
- }
2778
- function renderBlockField(field, value, depth, heading, label, descriptionLine, entry, config) {
2779
- if (!Array.isArray(value))
2780
- return "";
2781
- const items = value.map((item) => {
2782
- if (typeof item !== "object" || item === null)
2783
- return "";
2784
- const blockItem = item;
2785
- const templateName = blockItem._type || blockItem.template;
2786
- if (!templateName)
2787
- return "";
2788
- const template = field.templates.find((t) => t.name === templateName);
2789
- if (!template)
2790
- return "";
2791
- const blockHeading = "#".repeat(Math.min(depth + 1, 6));
2792
- const blockLabel = template.label || template.name;
2793
- const blockFields = template.fields.map((f) => {
2794
- const v = blockItem[f.name] ?? blockItem.value?.[f.name];
2795
- if (v === void 0 || v === null)
2796
- return "";
2797
- return renderField(f, v, depth + 2, entry, config);
2798
- }).filter(Boolean);
2799
- if (blockFields.length === 0)
2800
- return "";
2801
- return `${blockHeading} ${blockLabel}
2802
-
2803
- ${blockFields.join("\n\n")}`;
2804
- }).filter(Boolean);
2805
- if (items.length === 0)
2806
- return "";
2807
- return `${heading} ${label}${descriptionLine}
2808
-
2809
- ${items.join("\n\n")}`;
2810
- }
2811
- function applyFieldTransform(entry, field, value, config) {
2812
- if (!config?.fieldTransforms)
2813
- return void 0;
2814
- const typeTransforms = config.fieldTransforms[entry.entryType];
2815
- if (!typeTransforms)
2816
- return void 0;
2817
- const fn = typeTransforms[field.name];
2818
- if (!fn)
2819
- return void 0;
2820
- return fn(value, field);
2821
- }
2822
- function formatInlineValue(field, value) {
2823
- if (field.type === "boolean")
2824
- return value ? "Yes" : "No";
2825
- if (field.type === "reference")
2826
- return formatReference(value);
2827
- return String(value);
2828
- }
2829
- function yamlValue(value) {
2830
- if (/[:#{}[\],&*?|>!%@`]/.test(value) || value.includes("\n")) {
2831
- return `"${value.replace(/"/g, '\\"')}"`;
2832
- }
2833
- return value;
2834
- }
2835
- var init_json_to_markdown = __esm({
2836
- "dist/ai/json-to-markdown.js"() {
2837
- "use strict";
2838
- }
2839
- });
2840
-
2841
- // dist/ai/generate.js
2842
- import path10 from "node:path";
2843
- import { minimatch } from "minimatch";
2844
- async function generateAIContent(options) {
2845
- const { store, flatSchema, contentRoot, config } = options;
2846
- const files = /* @__PURE__ */ new Map();
2847
- const collections = flatSchema.filter((item) => item.type === "collection");
2848
- const allEntries = [];
2849
- const manifestCollections = [];
2850
- const rootEntries = [];
2851
- for (const collection of collections) {
2852
- if (collection.logicalPath === contentRoot)
2853
- continue;
2854
- if (isCollectionExcluded(collection.logicalPath, contentRoot, config))
2855
- continue;
2856
- if (collection.parentPath && collection.parentPath !== contentRoot)
2857
- continue;
2858
- const collectionResult = await processCollection(store, collection, flatSchema, contentRoot, config);
2859
- allEntries.push(...collectionResult.entries);
2860
- for (const [filePath, content] of collectionResult.files) {
2861
- files.set(filePath, content);
2862
- }
2863
- manifestCollections.push(collectionResult.manifestCollection);
2864
- }
2865
- const rootCollection = collections.find((c) => c.logicalPath === contentRoot);
2866
- if (rootCollection?.entries) {
2867
- const rootResult = await processRootEntries(store, rootCollection, contentRoot, config);
2868
- allEntries.push(...rootResult.entries);
2869
- for (const [filePath, content] of rootResult.files) {
2870
- files.set(filePath, content);
2871
- }
2872
- rootEntries.push(...rootResult.manifestEntries);
2873
- }
2874
- const manifestBundles = [];
2875
- if (config?.bundles) {
2876
- for (const bundle of config.bundles) {
2877
- if (/[/\\]|\.\./.test(bundle.name)) {
2878
- throw new Error(`Invalid bundle name "${bundle.name}": must not contain slashes or ".."`);
2879
- }
2880
- const matchingEntries = allEntries.filter((entry) => matchesBundleFilter(entry, bundle.filter, contentRoot));
2881
- if (matchingEntries.length > 0) {
2882
- const bundleContent = matchingEntries.map((e) => entryToMarkdown(e, config)).join("\n---\n\n");
2883
- const bundlePath = `bundles/${bundle.name}.md`;
2884
- files.set(bundlePath, bundleContent);
2885
- manifestBundles.push({
2886
- name: bundle.name,
2887
- description: bundle.description,
2888
- file: bundlePath,
2889
- entryCount: matchingEntries.length
2890
- });
2891
- }
2892
- }
2893
- }
2894
- const manifest = {
2895
- generated: (/* @__PURE__ */ new Date()).toISOString(),
2896
- entries: rootEntries,
2897
- collections: manifestCollections,
2898
- bundles: manifestBundles
2899
- };
2900
- files.set("manifest.json", JSON.stringify(manifest, null, 2));
2901
- return { manifest, files };
2902
- }
2903
- async function processCollection(store, collection, flatSchema, contentRoot, config) {
2904
- const files = /* @__PURE__ */ new Map();
2905
- const entries = [];
2906
- const cleanPath = stripContentRoot(collection.logicalPath, contentRoot);
2907
- const manifestEntries = [];
2908
- const listed = await store.listCollectionEntries(collection.logicalPath);
2909
- const directEntries = listed.filter((e) => e.collection === collection.logicalPath);
2910
- for (const listEntry of directEntries) {
2911
- const entryTypeName = extractEntryTypeFromFilename(path10.basename(listEntry.relativePath));
2912
- if (!entryTypeName)
2913
- continue;
2914
- if (config?.exclude?.entryTypes?.includes(entryTypeName))
2915
- continue;
2916
- const entryTypeConfig = findEntryType(collection, entryTypeName);
2917
- if (!entryTypeConfig)
2918
- continue;
2919
- try {
2920
- const doc = await store.read(listEntry.collection, listEntry.slug, {
2921
- resolveReferences: false
2922
- });
2923
- const aiEntry = docToAIEntry(doc, listEntry.slug, entryTypeName, entryTypeConfig, cleanPath);
2924
- if (config?.exclude?.where?.(aiEntry))
2925
- continue;
2926
- entries.push(aiEntry);
2927
- const entryFilePath = `${cleanPath}/${listEntry.slug}.md`;
2928
- const entryMarkdown = entryToMarkdown(aiEntry, config);
2929
- files.set(entryFilePath, entryMarkdown);
2930
- manifestEntries.push({
2931
- slug: listEntry.slug,
2932
- title: aiEntry.data.title ? String(aiEntry.data.title) : void 0,
2933
- file: entryFilePath
2934
- });
2935
- } catch (err) {
2936
- console.warn(`AI content: skipping entry "${listEntry.slug}" in ${collection.logicalPath}:`, getErrorMessage(err));
2937
- continue;
2938
- }
2939
- }
2940
- const subcollections = flatSchema.filter((item) => item.type === "collection" && item.parentPath === collection.logicalPath);
2941
- const manifestSubcollections = [];
2942
- for (const sub of subcollections) {
2943
- if (isCollectionExcluded(sub.logicalPath, contentRoot, config))
2944
- continue;
2945
- const subResult = await processCollection(store, sub, flatSchema, contentRoot, config);
2946
- entries.push(...subResult.entries);
2947
- for (const [filePath, content] of subResult.files) {
2948
- files.set(filePath, content);
2949
- }
2950
- manifestSubcollections.push(subResult.manifestCollection);
2951
- }
2952
- if (entries.length > 0) {
2953
- const allContent = entries.map((e) => entryToMarkdown(e, config)).join("\n---\n\n");
2954
- const allPath = `${cleanPath}/all.md`;
2955
- files.set(allPath, allContent);
2956
- }
2957
- const manifestCollection = {
2958
- name: collection.name,
2959
- label: collection.label,
2960
- description: collection.description,
2961
- path: cleanPath,
2962
- allFile: entries.length > 0 ? `${cleanPath}/all.md` : void 0,
2963
- entryCount: entries.length,
2964
- entries: manifestEntries,
2965
- subcollections: manifestSubcollections.length > 0 ? manifestSubcollections : void 0
2966
- };
2967
- return { entries, files, manifestCollection };
2968
- }
2969
- async function processRootEntries(store, rootCollection, contentRoot, config) {
2970
- const files = /* @__PURE__ */ new Map();
2971
- const entries = [];
2972
- const manifestEntries = [];
2973
- const listed = await store.listCollectionEntries(rootCollection.logicalPath);
2974
- const directEntries = listed.filter((e) => e.collection === rootCollection.logicalPath);
2975
- for (const listEntry of directEntries) {
2976
- const entryTypeName = extractEntryTypeFromFilename(path10.basename(listEntry.relativePath));
2977
- if (!entryTypeName)
2978
- continue;
2979
- if (config?.exclude?.entryTypes?.includes(entryTypeName))
2980
- continue;
2981
- const entryTypeConfig = findEntryType(rootCollection, entryTypeName);
2982
- if (!entryTypeConfig)
2983
- continue;
2984
- try {
2985
- const doc = await store.read(listEntry.collection, listEntry.slug, {
2986
- resolveReferences: false
2987
- });
2988
- const aiEntry = docToAIEntry(doc, listEntry.slug, entryTypeName, entryTypeConfig, "");
2989
- if (config?.exclude?.where?.(aiEntry))
2990
- continue;
2991
- entries.push(aiEntry);
2992
- const entryFilePath = `${listEntry.slug}.md`;
2993
- const entryMarkdown = entryToMarkdown(aiEntry, config);
2994
- files.set(entryFilePath, entryMarkdown);
2995
- manifestEntries.push({
2996
- slug: listEntry.slug,
2997
- title: aiEntry.data.title ? String(aiEntry.data.title) : void 0,
2998
- file: entryFilePath
2999
- });
3000
- } catch (err) {
3001
- console.warn(`AI content: skipping root entry "${listEntry.slug}":`, getErrorMessage(err));
3002
- continue;
3003
- }
3004
- }
3005
- return { entries, files, manifestEntries };
3006
- }
3007
- function stripContentRoot(logicalPath, contentRoot) {
3008
- if (logicalPath.startsWith(contentRoot + "/")) {
3009
- return logicalPath.slice(contentRoot.length + 1);
3010
- }
3011
- return logicalPath;
3012
- }
3013
- function isCollectionExcluded(logicalPath, contentRoot, config) {
3014
- if (!config?.exclude?.collections)
1066
+ requiresExistingRepo() {
3015
1067
  return false;
3016
- const cleanPath = stripContentRoot(logicalPath, contentRoot);
3017
- return config.exclude.collections.some((pattern) => (
3018
- // Match against clean path or full logical path
3019
- minimatch(cleanPath, pattern) || minimatch(logicalPath, pattern)
3020
- ));
3021
- }
3022
- function findEntryType(collection, entryTypeName) {
3023
- return collection.entries?.find((e) => e.name === entryTypeName);
3024
- }
3025
- function docToAIEntry(doc, slug, entryTypeName, entryTypeConfig, cleanCollectionPath) {
3026
- return {
3027
- slug,
3028
- collection: cleanCollectionPath,
3029
- collectionName: doc.collectionName,
3030
- entryType: entryTypeName,
3031
- format: doc.format,
3032
- data: doc.data,
3033
- body: doc.format !== "json" ? doc.body : void 0,
3034
- fields: entryTypeConfig.schema
3035
- };
3036
- }
3037
- function matchesBundleFilter(entry, filter, contentRoot) {
3038
- if (filter.collections) {
3039
- const matches = filter.collections.some((pattern) => {
3040
- const cleanPattern = stripContentRoot(pattern, contentRoot);
3041
- return entry.collection === cleanPattern || entry.collection === pattern || entry.collection.startsWith(cleanPattern + "/");
3042
- });
3043
- if (!matches)
3044
- return false;
3045
1068
  }
3046
- if (filter.entryTypes) {
3047
- if (!filter.entryTypes.includes(entry.entryType))
3048
- return false;
1069
+ getSettingsBranchName(config) {
1070
+ if (config.settingsBranch)
1071
+ return config.settingsBranch;
1072
+ const deploymentName = config.deploymentName ?? "local";
1073
+ return `canopycms-settings-${deploymentName}`;
3049
1074
  }
3050
- if (filter.paths) {
3051
- const entryPath = entry.collection ? `${entry.collection}/${entry.slug}` : entry.slug;
3052
- const matches = filter.paths.some((pattern) => minimatch(entryPath, pattern));
3053
- if (!matches)
3054
- return false;
1075
+ getSettingsRoot(sourceRoot) {
1076
+ return path.join(this.getWorkspaceRoot(sourceRoot), "settings");
3055
1077
  }
3056
- if (filter.where) {
3057
- if (!filter.where(entry))
3058
- return false;
1078
+ usesSeparateSettingsBranch() {
1079
+ return true;
3059
1080
  }
3060
- return true;
3061
- }
3062
- var init_generate = __esm({
3063
- "dist/ai/generate.js"() {
3064
- "use strict";
3065
- init_content_id_index();
3066
- init_error();
3067
- init_json_to_markdown();
1081
+ validateConfig(_config) {
3068
1082
  }
3069
- });
3070
-
3071
- // dist/branch-registry.js
3072
- import fs8 from "node:fs/promises";
3073
- import path11 from "node:path";
3074
- var REGISTRY_FILE, REGISTRY_STALE_FILE, REGISTRY_TEMP_FILE, REGISTRY_VERSION, BranchRegistry;
3075
- var init_branch_registry = __esm({
3076
- "dist/branch-registry.js"() {
3077
- "use strict";
3078
- init_branch_metadata();
3079
- init_error();
3080
- REGISTRY_FILE = "branches.json";
3081
- REGISTRY_STALE_FILE = "branches.stale.json";
3082
- REGISTRY_TEMP_FILE = "branches.tmp.json";
3083
- REGISTRY_VERSION = 1;
3084
- BranchRegistry = class {
3085
- constructor(root) {
3086
- this.root = path11.resolve(root);
3087
- this.registryPath = path11.join(this.root, REGISTRY_FILE);
3088
- this.stalePath = path11.join(this.root, REGISTRY_STALE_FILE);
3089
- this.tempPath = path11.join(this.root, REGISTRY_TEMP_FILE);
3090
- }
3091
- /**
3092
- * Returns all branches. Uses cache if fresh, regenerates if stale.
3093
- */
3094
- async list() {
3095
- try {
3096
- const raw = await fs8.readFile(this.registryPath, "utf8");
3097
- const parsed = JSON.parse(raw);
3098
- if (!parsed.version || !Array.isArray(parsed.branches)) {
3099
- return await this.regenerate();
3100
- }
3101
- return parsed.branches;
3102
- } catch (err) {
3103
- if (isNotFoundError2(err)) {
3104
- return await this.regenerate();
3105
- }
3106
- throw err;
3107
- }
3108
- }
3109
- /**
3110
- * Returns a single branch by name. Uses cache if available.
3111
- */
3112
- async get(name) {
3113
- const branches = await this.list();
3114
- return branches.find((b) => b.branch.name === name);
3115
- }
3116
- /**
3117
- * Marks the cache as stale. Next list() call will regenerate.
3118
- * Uses atomic rename for safety.
3119
- */
3120
- async invalidate() {
3121
- try {
3122
- await fs8.rename(this.registryPath, this.stalePath);
3123
- } catch (err) {
3124
- if (!isNotFoundError2(err)) {
3125
- throw err;
3126
- }
3127
- }
3128
- }
3129
- /**
3130
- * Scans branch directories and rebuilds the cache.
3131
- * Concurrent calls are safe - all produce identical content.
3132
- */
3133
- async regenerate() {
3134
- const branches = await this.scanBranchDirectories();
3135
- const uniqueTempPath = `${this.tempPath}.${Date.now()}.${Math.random().toString(36).slice(2)}`;
3136
- await fs8.mkdir(this.root, { recursive: true });
3137
- const snapshot = {
3138
- version: REGISTRY_VERSION,
3139
- branches
3140
- };
3141
- await fs8.writeFile(uniqueTempPath, JSON.stringify(snapshot, null, 2) + "\n", "utf8");
3142
- try {
3143
- await fs8.rename(uniqueTempPath, this.registryPath);
3144
- } catch (err) {
3145
- await fs8.unlink(uniqueTempPath).catch(() => {
3146
- });
3147
- throw err;
3148
- }
3149
- await fs8.unlink(this.stalePath).catch(() => {
3150
- });
3151
- return branches;
3152
- }
3153
- /**
3154
- * Scans the root directory for branch subdirectories with valid branch.json files.
3155
- */
3156
- async scanBranchDirectories() {
3157
- const branches = [];
3158
- try {
3159
- const entries = await fs8.readdir(this.root, { withFileTypes: true });
3160
- for (const entry of entries) {
3161
- if (!entry.isDirectory() || entry.name.startsWith(".")) {
3162
- continue;
3163
- }
3164
- const branchRoot = path11.join(this.root, entry.name);
3165
- const meta = await BranchMetadataFileManager.loadOnly(branchRoot);
3166
- if (meta) {
3167
- branches.push({
3168
- branch: meta.branch,
3169
- branchRoot,
3170
- baseRoot: this.root
3171
- });
3172
- }
3173
- }
3174
- } catch (err) {
3175
- if (isNotFoundError2(err)) {
3176
- return [];
3177
- }
3178
- throw err;
3179
- }
3180
- return branches;
3181
- }
3182
- };
1083
+ shouldCreateSettingsPR(_config) {
1084
+ return false;
3183
1085
  }
3184
- });
3185
-
3186
- // dist/branch-metadata.js
3187
- import { randomUUID } from "node:crypto";
3188
- import fs9 from "node:fs/promises";
3189
- import path12 from "node:path";
3190
- async function withFileLock(filePath, fn) {
3191
- while (fileLocks.has(filePath)) {
3192
- await fileLocks.get(filePath);
3193
- }
3194
- let resolve;
3195
- const lockPromise = new Promise((r) => {
3196
- resolve = r;
3197
- });
3198
- fileLocks.set(filePath, lockPromise);
3199
- try {
3200
- return await fn();
3201
- } finally {
3202
- fileLocks.delete(filePath);
3203
- resolve();
1086
+ };
1087
+ var strategyCache = /* @__PURE__ */ new Map();
1088
+ function operatingStrategy(mode) {
1089
+ const cached = strategyCache.get(mode);
1090
+ if (cached)
1091
+ return cached;
1092
+ let strategy;
1093
+ switch (mode) {
1094
+ case "prod":
1095
+ strategy = new ProdStrategy();
1096
+ break;
1097
+ case "dev":
1098
+ strategy = new DevStrategy();
1099
+ break;
1100
+ default: {
1101
+ const _exhaustive = mode;
1102
+ throw new Error(`Unknown operating mode: ${_exhaustive}`);
1103
+ }
3204
1104
  }
1105
+ strategyCache.set(mode, strategy);
1106
+ return strategy;
3205
1107
  }
3206
- var BRANCH_META_DIR, BRANCH_META_FILE, CURRENT_SCHEMA_VERSION, BranchMetadataConflictError, fileLocks, BranchMetadataFileManager, loadBranchContext;
3207
- var init_branch_metadata = __esm({
3208
- "dist/branch-metadata.js"() {
3209
- "use strict";
3210
- init_branch_registry();
3211
- init_paths();
3212
- init_error();
3213
- BRANCH_META_DIR = ".canopy-meta";
3214
- BRANCH_META_FILE = "branch.json";
3215
- CURRENT_SCHEMA_VERSION = 1;
3216
- BranchMetadataConflictError = class extends Error {
3217
- constructor() {
3218
- super("Concurrent modification detected in branch metadata");
3219
- this.name = "BranchMetadataConflictError";
3220
- }
3221
- };
3222
- fileLocks = /* @__PURE__ */ new Map();
3223
- BranchMetadataFileManager = class _BranchMetadataFileManager {
3224
- constructor(branchRoot, baseRoot) {
3225
- this.branchRoot = path12.resolve(branchRoot);
3226
- this.filePath = path12.join(this.branchRoot, BRANCH_META_DIR, BRANCH_META_FILE);
3227
- this.baseRoot = baseRoot;
3228
- }
3229
- /**
3230
- * Load branch metadata without requiring baseRoot.
3231
- * Use this for read-only access (e.g., in registry scanning or loadBranchContext).
3232
- */
3233
- static async loadOnly(branchRoot) {
3234
- const filePath = path12.join(path12.resolve(branchRoot), BRANCH_META_DIR, BRANCH_META_FILE);
3235
- try {
3236
- const raw = await fs9.readFile(filePath, "utf8");
3237
- return JSON.parse(raw);
3238
- } catch (err) {
3239
- if (isNotFoundError2(err)) {
3240
- return null;
3241
- }
3242
- throw err;
3243
- }
3244
- }
3245
- /**
3246
- * Get a BranchMetadataFileManager instance configured for registry invalidation.
3247
- * Use this in API handlers to ensure registry cache is invalidated on updates.
3248
- */
3249
- static get(branchRoot, baseRoot) {
3250
- return new _BranchMetadataFileManager(branchRoot, baseRoot);
3251
- }
3252
- async load() {
3253
- try {
3254
- const raw = await fs9.readFile(this.filePath, "utf8");
3255
- const parsed = JSON.parse(raw);
3256
- const version = parsed.version ?? 0;
3257
- return { meta: parsed, version };
3258
- } catch (err) {
3259
- if (isNotFoundError2(err)) {
3260
- return { meta: null, version: null };
3261
- }
3262
- throw err;
3263
- }
3264
- }
3265
- /**
3266
- * Atomic write using temp-file + rename + post-write verification.
3267
- * Follows the same pattern as CommentStore for EFS/NFS safety.
3268
- */
3269
- async write(meta, expectedVersion) {
3270
- const newVersion = expectedVersion === null ? 1 : expectedVersion + 1;
3271
- const writeId = randomUUID();
3272
- const payload = {
3273
- ...meta,
3274
- schemaVersion: meta.schemaVersion ?? CURRENT_SCHEMA_VERSION,
3275
- version: newVersion,
3276
- writeId
3277
- };
3278
- await fs9.mkdir(path12.dirname(this.filePath), { recursive: true });
3279
- const content = JSON.stringify(payload, null, 2) + "\n";
3280
- if (expectedVersion === null) {
3281
- try {
3282
- await fs9.writeFile(this.filePath, content, { flag: "wx" });
3283
- return { version: newVersion, writeId };
3284
- } catch (err) {
3285
- if (isFileExistsError(err)) {
3286
- throw new BranchMetadataConflictError();
3287
- }
3288
- throw err;
3289
- }
3290
- }
3291
- const tempPath = `${this.filePath}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
3292
- await fs9.writeFile(tempPath, content, "utf-8");
3293
- try {
3294
- let currentVersion = null;
3295
- try {
3296
- const current = JSON.parse(await fs9.readFile(this.filePath, "utf-8"));
3297
- currentVersion = current.version ?? 0;
3298
- } catch {
3299
- currentVersion = null;
3300
- }
3301
- if (currentVersion !== expectedVersion) {
3302
- throw new BranchMetadataConflictError();
3303
- }
3304
- await fs9.rename(tempPath, this.filePath);
3305
- const afterWrite = JSON.parse(await fs9.readFile(this.filePath, "utf-8"));
3306
- if (afterWrite.writeId !== writeId) {
3307
- throw new BranchMetadataConflictError();
3308
- }
3309
- } catch (err) {
3310
- await fs9.unlink(tempPath).catch(() => {
3311
- });
3312
- throw err;
3313
- }
3314
- return { version: newVersion, writeId };
3315
- }
3316
- async withRetry(operation, maxAttempts = 5) {
3317
- for (let attempt = 1; attempt <= maxAttempts; attempt++) {
3318
- try {
3319
- return await operation();
3320
- } catch (err) {
3321
- if (err instanceof BranchMetadataConflictError && attempt < maxAttempts) {
3322
- const baseDelay = Math.min(10 * Math.pow(2, attempt - 1), 100);
3323
- const jitter = Math.random() * baseDelay;
3324
- await new Promise((resolve) => setTimeout(resolve, baseDelay + jitter));
3325
- continue;
3326
- }
3327
- throw err;
3328
- }
3329
- }
3330
- throw new Error("Unreachable");
3331
- }
3332
- async save(incoming) {
3333
- return withFileLock(this.filePath, () => this.withRetry(async () => {
3334
- const { meta: existing, version } = await this.load();
3335
- const now = (/* @__PURE__ */ new Date()).toISOString();
3336
- const defaults = {
3337
- name: "unknown",
3338
- status: "editing",
3339
- access: {},
3340
- createdBy: "unknown",
3341
- createdAt: now,
3342
- updatedAt: now
3343
- };
3344
- const merged = {
3345
- schemaVersion: CURRENT_SCHEMA_VERSION,
3346
- version: version ?? 0,
3347
- branch: {
3348
- ...defaults,
3349
- ...existing?.branch,
3350
- ...incoming.branch,
3351
- access: {
3352
- ...existing?.branch?.access,
3353
- ...incoming.branch?.access
3354
- },
3355
- // Immutable after creation
3356
- createdBy: existing?.branch.createdBy ?? incoming.branch?.createdBy ?? defaults.createdBy,
3357
- createdAt: existing?.branch.createdAt ?? defaults.createdAt
3358
- }
3359
- };
3360
- const written = await this.write(merged, version);
3361
- merged.version = written.version;
3362
- merged.writeId = written.writeId;
3363
- await this.invalidateRegistry();
3364
- return merged;
3365
- }));
3366
- }
3367
- /**
3368
- * Invalidates the registry cache so next list() call regenerates from branch.json files.
3369
- */
3370
- async invalidateRegistry() {
3371
- const registry = new BranchRegistry(this.baseRoot);
3372
- await registry.invalidate();
3373
- }
3374
- };
3375
- loadBranchContext = async (options) => {
3376
- const { branchRoot, baseRoot } = resolveBranchPath({
3377
- branchName: options.branchName,
3378
- mode: options.mode,
3379
- basePathOverride: options.basePathOverride
3380
- });
3381
- const meta = await BranchMetadataFileManager.loadOnly(branchRoot);
3382
- if (!meta) {
3383
- return null;
3384
- }
3385
- return {
3386
- branch: meta.branch,
3387
- branchRoot,
3388
- baseRoot
3389
- };
3390
- };
3391
- }
3392
- });
3393
1108
 
3394
- // dist/build-mode.js
3395
- var isDeployedStatic, STATIC_DEPLOY_USER;
3396
- var init_build_mode = __esm({
3397
- "dist/build-mode.js"() {
3398
- "use strict";
3399
- isDeployedStatic = (config) => {
3400
- return config.deployedAs === "static";
3401
- };
3402
- STATIC_DEPLOY_USER = Object.freeze({
3403
- type: "authenticated",
3404
- userId: "__static_deploy__",
3405
- groups: ["Admins"],
3406
- email: "static-deploy@canopycms",
3407
- name: "Static Deploy"
3408
- });
3409
- }
3410
- });
1109
+ // dist/utils/fs.js
1110
+ import fs from "node:fs/promises";
3411
1111
 
3412
- // dist/ai/resolve-branch.js
3413
- async function resolveBranchRoot(config) {
3414
- if (config.mode === "dev" || isDeployedStatic(config)) {
3415
- return process.cwd();
3416
- }
3417
- const baseBranch = config.defaultBaseBranch ?? "main";
3418
- const context = await loadBranchContext({
3419
- branchName: baseBranch,
3420
- mode: config.mode
3421
- });
3422
- if (!context) {
3423
- throw new Error(`Could not load branch context for "${baseBranch}". Ensure the branch exists and has been initialized.`);
3424
- }
3425
- return context.branchRoot;
1112
+ // dist/utils/error.js
1113
+ function isNodeError(err) {
1114
+ return err instanceof Error && "code" in err;
3426
1115
  }
3427
- var init_resolve_branch = __esm({
3428
- "dist/ai/resolve-branch.js"() {
3429
- "use strict";
3430
- init_branch_metadata();
3431
- init_build_mode();
3432
- }
3433
- });
3434
-
3435
- // dist/build/generate-ai-content.js
3436
- import fs10 from "node:fs/promises";
3437
- import path13 from "node:path";
3438
- async function generateAIContentFiles(options) {
3439
- const { config, entrySchemaRegistry, outputDir, aiConfig: aiConfig2, _testFlatSchema } = options;
3440
- const contentRootName = config.contentRoot || "content";
3441
- const branchRoot = await resolveBranchRoot(config);
3442
- let flatSchema;
3443
- if (_testFlatSchema) {
3444
- flatSchema = _testFlatSchema;
3445
- } else {
3446
- const schemaCache = new BranchSchemaCache(config.mode);
3447
- const cached = await schemaCache.getSchema(branchRoot, entrySchemaRegistry, contentRootName);
3448
- flatSchema = cached.flatSchema;
3449
- }
3450
- const store = new ContentStore(branchRoot, flatSchema);
3451
- const result = await generateAIContent({
3452
- store,
3453
- flatSchema,
3454
- contentRoot: contentRootName,
3455
- config: aiConfig2
3456
- });
3457
- const absoluteOutputDir = path13.resolve(outputDir) + path13.sep;
3458
- let fileCount = 0;
3459
- for (const [filePath, content] of result.files) {
3460
- const absolutePath = path13.resolve(path13.join(absoluteOutputDir, filePath));
3461
- if (!absolutePath.startsWith(absoluteOutputDir)) {
3462
- throw new Error(`Path traversal detected in AI content output: ${filePath}`);
3463
- }
3464
- await fs10.mkdir(path13.dirname(absolutePath), { recursive: true });
3465
- await fs10.writeFile(absolutePath, content, "utf-8");
3466
- fileCount++;
3467
- }
3468
- return { fileCount, outputDir: absoluteOutputDir };
1116
+ function isNotFoundError(err) {
1117
+ return isNodeError(err) && err.code === "ENOENT";
3469
1118
  }
3470
- var init_generate_ai_content = __esm({
3471
- "dist/build/generate-ai-content.js"() {
3472
- "use strict";
3473
- init_content_store();
3474
- init_branch_schema_cache();
3475
- init_generate();
3476
- init_resolve_branch();
3477
- }
3478
- });
3479
1119
 
3480
- // dist/cli/generate-ai-content.js
3481
- var generate_ai_content_exports = {};
3482
- __export(generate_ai_content_exports, {
3483
- generateAIContentCLI: () => generateAIContentCLI
3484
- });
3485
- import path14 from "node:path";
3486
- import { createJiti } from "jiti";
3487
- async function generateAIContentCLI(options) {
3488
- const { projectDir, outputDir = "public/ai", configPath, appDir = "app" } = options;
3489
- console.log("\nCanopyCMS generate-ai-content\n");
3490
- const canopyConfigPath = path14.join(projectDir, "canopycms.config.ts");
3491
- let canopyConfigModule;
1120
+ // dist/utils/fs.js
1121
+ async function filePathExists(filePath) {
3492
1122
  try {
3493
- canopyConfigModule = await jiti.import(canopyConfigPath);
1123
+ await fs.stat(filePath);
1124
+ return true;
3494
1125
  } catch (err) {
3495
- console.error(`Could not load config from ${canopyConfigPath}`);
3496
- console.error(getErrorMessage(err));
3497
- process.exit(1);
3498
- }
3499
- const configExport = canopyConfigModule.default ?? canopyConfigModule.config ?? canopyConfigModule;
3500
- const serverConfig = typeof configExport === "object" && configExport !== null && "server" in configExport ? configExport.server : configExport;
3501
- const schemasPath = path14.join(projectDir, appDir, "schemas.ts");
3502
- let entrySchemaRegistry = {};
3503
- try {
3504
- const schemasModule = await jiti.import(schemasPath);
3505
- entrySchemaRegistry = schemasModule.entrySchemaRegistry ?? schemasModule;
3506
- } catch {
3507
- console.warn(` No ${appDir}/schemas.ts found, using empty entry schema registry`);
3508
- }
3509
- let aiConfig2;
3510
- if (configPath) {
3511
- try {
3512
- const aiConfigModule = await jiti.import(path14.resolve(configPath));
3513
- aiConfig2 = aiConfigModule.aiContentConfig ?? aiConfigModule.default ?? aiConfigModule.config;
3514
- } catch (err) {
3515
- console.error(`Could not load AI config from ${configPath}`);
3516
- console.error(getErrorMessage(err));
3517
- process.exit(1);
3518
- }
1126
+ if (isNotFoundError(err))
1127
+ return false;
1128
+ throw err;
3519
1129
  }
3520
- if (aiConfig2 !== void 0 && (typeof aiConfig2 !== "object" || aiConfig2 === null)) {
3521
- console.error("Invalid AI content config: expected an object.");
3522
- process.exit(1);
3523
- }
3524
- if (!serverConfig || typeof serverConfig !== "object" || !("mode" in serverConfig) || !("contentRoot" in serverConfig)) {
3525
- console.error("Invalid CanopyCMS config: expected an object with mode and contentRoot properties.");
3526
- console.error("Make sure canopycms.config.ts uses defineCanopyConfig().");
3527
- process.exit(1);
3528
- }
3529
- const resolvedOutput = path14.resolve(projectDir, outputDir);
3530
- console.log(` Output: ${resolvedOutput}`);
3531
- console.log(` Mode: ${serverConfig.mode ?? "dev"}`);
3532
- const result = await generateAIContentFiles({
3533
- config: serverConfig,
3534
- entrySchemaRegistry,
3535
- outputDir: resolvedOutput,
3536
- aiConfig: aiConfig2
3537
- });
3538
- console.log(`
3539
- Generated ${result.fileCount} files`);
3540
- console.log(` Output: ${result.outputDir}
3541
- `);
3542
1130
  }
3543
- var jiti;
3544
- var init_generate_ai_content2 = __esm({
3545
- "dist/cli/generate-ai-content.js"() {
3546
- "use strict";
3547
- init_generate_ai_content();
3548
- init_error();
3549
- jiti = createJiti(import.meta.url);
3550
- }
3551
- });
3552
1131
 
3553
1132
  // dist/cli/init.js
3554
- init_operating_mode();
3555
- import fs11 from "node:fs/promises";
3556
- import { realpathSync } from "node:fs";
3557
- import path15 from "node:path";
3558
- import { fileURLToPath as fileURLToPath2 } from "node:url";
3559
- import * as p from "@clack/prompts";
3560
- async function fileExists(filePath) {
3561
- try {
3562
- await fs11.stat(filePath);
3563
- return true;
3564
- } catch {
3565
- return false;
3566
- }
3567
- }
3568
1133
  async function writeFile(filePath, content, options) {
3569
- const relativePath = path15.relative(process.cwd(), filePath);
3570
- if (await fileExists(filePath)) {
1134
+ const relativePath = path6.relative(process.cwd(), filePath);
1135
+ if (await filePathExists(filePath)) {
3571
1136
  if (options.force) {
3572
1137
  } else if (options.nonInteractive) {
3573
1138
  p.log.warn(`skip: ${relativePath} (already exists)`);
@@ -3583,8 +1148,8 @@ async function writeFile(filePath, content, options) {
3583
1148
  }
3584
1149
  }
3585
1150
  }
3586
- await fs11.mkdir(path15.dirname(filePath), { recursive: true });
3587
- await fs11.writeFile(filePath, content, "utf-8");
1151
+ await fs5.mkdir(path6.dirname(filePath), { recursive: true });
1152
+ await fs5.writeFile(filePath, content, "utf-8");
3588
1153
  p.log.success(`created: ${relativePath}`);
3589
1154
  return true;
3590
1155
  }
@@ -3598,22 +1163,22 @@ async function init(options) {
3598
1163
  const writeOpts = { force, nonInteractive };
3599
1164
  const { canopyCmsConfig: canopyCmsConfig2, canopyContext: canopyContext2, schemasTemplate: schemasTemplate2, apiRoute: apiRoute2, editPage: editPage2, aiConfig: aiConfig2, aiRoute: aiRoute2 } = await Promise.resolve().then(() => (init_templates(), templates_exports));
3600
1165
  p.intro("CanopyCMS init");
3601
- await writeFile(path15.join(projectDir, "canopycms.config.ts"), await canopyCmsConfig2({ mode }), writeOpts);
3602
- await writeFile(path15.join(projectDir, appDir, "lib/canopy.ts"), await canopyContext2({ configImport: configImportPath(appDir, 1) }), writeOpts);
3603
- await writeFile(path15.join(projectDir, appDir, "schemas.ts"), await schemasTemplate2(), writeOpts);
3604
- await writeFile(path15.join(projectDir, appDir, "api/canopycms/[...canopycms]/route.ts"), await apiRoute2({
1166
+ await writeFile(path6.join(projectDir, "canopycms.config.ts"), await canopyCmsConfig2({ mode }), writeOpts);
1167
+ await writeFile(path6.join(projectDir, appDir, "lib/canopy.ts"), await canopyContext2({ configImport: configImportPath(appDir, 1) }), writeOpts);
1168
+ await writeFile(path6.join(projectDir, appDir, "schemas.ts"), await schemasTemplate2(), writeOpts);
1169
+ await writeFile(path6.join(projectDir, appDir, "api/canopycms/[...canopycms]/route.ts"), await apiRoute2({
3605
1170
  canopyImport: "../".repeat(3) + "lib/canopy"
3606
1171
  }), writeOpts);
3607
- await writeFile(path15.join(projectDir, appDir, "edit/page.tsx"), await editPage2({ configImport: configImportPath(appDir, 1) }), writeOpts);
1172
+ await writeFile(path6.join(projectDir, appDir, "edit/page.tsx"), await editPage2({ configImport: configImportPath(appDir, 1) }), writeOpts);
3608
1173
  if (ai) {
3609
- await writeFile(path15.join(projectDir, appDir, "ai/config.ts"), await aiConfig2(), writeOpts);
3610
- await writeFile(path15.join(projectDir, appDir, "ai/[...path]/route.ts"), await aiRoute2({ configImport: configImportPath(appDir, 2) }), writeOpts);
3611
- }
3612
- const gitignorePath = path15.join(projectDir, ".gitignore");
3613
- if (await fileExists(gitignorePath)) {
3614
- const content = await fs11.readFile(gitignorePath, "utf-8");
3615
- if (!content.includes(".canopy-prod-sim")) {
3616
- await fs11.appendFile(gitignorePath, "\n# CanopyCMS\n.canopy-prod-sim/\n.canopy-dev/\n");
1174
+ await writeFile(path6.join(projectDir, appDir, "ai/config.ts"), await aiConfig2(), writeOpts);
1175
+ await writeFile(path6.join(projectDir, appDir, "ai/[...path]/route.ts"), await aiRoute2({ configImport: configImportPath(appDir, 2) }), writeOpts);
1176
+ }
1177
+ const gitignorePath = path6.join(projectDir, ".gitignore");
1178
+ if (await filePathExists(gitignorePath)) {
1179
+ const content = await fs5.readFile(gitignorePath, "utf-8");
1180
+ if (!content.includes(".canopy-dev")) {
1181
+ await fs5.appendFile(gitignorePath, "\n# CanopyCMS\n.canopy-dev/\n");
3617
1182
  p.log.success("updated: .gitignore");
3618
1183
  }
3619
1184
  }
@@ -3637,16 +1202,16 @@ async function initDeployAws(options) {
3637
1202
  const writeOpts = { force, nonInteractive };
3638
1203
  const { dockerfileCms: dockerfileCms2, githubWorkflowCms: githubWorkflowCms2 } = await Promise.resolve().then(() => (init_templates(), templates_exports));
3639
1204
  p.intro("CanopyCMS init-deploy aws");
3640
- await writeFile(path15.join(projectDir, "Dockerfile.cms"), await dockerfileCms2(), writeOpts);
3641
- await writeFile(path15.join(projectDir, ".github/workflows/deploy-cms.yml"), await githubWorkflowCms2(), writeOpts);
3642
- const nextConfigPath = path15.join(projectDir, "next.config.ts");
3643
- const nextConfigMjsPath = path15.join(projectDir, "next.config.mjs");
3644
- const configPath = await fileExists(nextConfigPath) ? nextConfigPath : await fileExists(nextConfigMjsPath) ? nextConfigMjsPath : null;
1205
+ await writeFile(path6.join(projectDir, "Dockerfile.cms"), await dockerfileCms2(), writeOpts);
1206
+ await writeFile(path6.join(projectDir, ".github/workflows/deploy-cms.yml"), await githubWorkflowCms2(), writeOpts);
1207
+ const nextConfigPath = path6.join(projectDir, "next.config.ts");
1208
+ const nextConfigMjsPath = path6.join(projectDir, "next.config.mjs");
1209
+ const configPath = await filePathExists(nextConfigPath) ? nextConfigPath : await filePathExists(nextConfigMjsPath) ? nextConfigMjsPath : null;
3645
1210
  if (configPath) {
3646
- const content = await fs11.readFile(configPath, "utf-8");
1211
+ const content = await fs5.readFile(configPath, "utf-8");
3647
1212
  if (!content.includes("CANOPY_BUILD")) {
3648
1213
  p.note([
3649
- `Add dual build support to ${path15.basename(configPath)}:`,
1214
+ `Add dual build support to ${path6.basename(configPath)}:`,
3650
1215
  "",
3651
1216
  " output: process.env.CANOPY_BUILD === 'cms' ? 'standalone' : 'export',"
3652
1217
  ].join("\n"), "Manual step");
@@ -3657,21 +1222,17 @@ async function initDeployAws(options) {
3657
1222
  }
3658
1223
  async function workerRunOnce(options) {
3659
1224
  const { getTaskQueueDir: getTaskQueueDir2 } = await Promise.resolve().then(() => (init_task_queue_config(), task_queue_config_exports));
3660
- const cfgPath = path15.join(options.projectDir, "canopycms.config.ts");
3661
- let mode = "prod-sim";
1225
+ const cfgPath = path6.join(options.projectDir, "canopycms.config.ts");
1226
+ let mode = "dev";
3662
1227
  try {
3663
- const configContent = await fs11.readFile(cfgPath, "utf-8");
1228
+ const configContent = await fs5.readFile(cfgPath, "utf-8");
3664
1229
  if (/^\s*mode:\s*['"]prod['"]\s*[,}]/m.test(configContent)) {
3665
1230
  mode = "prod";
3666
1231
  }
3667
1232
  } catch {
3668
1233
  }
3669
1234
  const taskDir = getTaskQueueDir2({ mode });
3670
- if (!taskDir) {
3671
- console.log("Worker not needed in dev mode");
3672
- return;
3673
- }
3674
- const cachePath = process.env.CANOPY_AUTH_CACHE_PATH ?? path15.join(operatingStrategy(mode).getWorkspaceRoot(options.projectDir), ".cache");
1235
+ const cachePath = process.env.CANOPY_AUTH_CACHE_PATH ?? path6.join(operatingStrategy(mode).getWorkspaceRoot(options.projectDir), ".cache");
3675
1236
  let refreshAuthCache;
3676
1237
  const authMode = process.env.CANOPY_AUTH_MODE || "dev";
3677
1238
  if (options.authPlugin?.createCacheRefresher) {
@@ -3707,170 +1268,6 @@ CanopyCMS worker run-once (mode: ${mode}, auth: ${authMode})
3707
1268
  }
3708
1269
  console.log("\nDone");
3709
1270
  }
3710
- function parseFlags(args) {
3711
- const flags = {};
3712
- const positional = [];
3713
- for (let i = 0; i < args.length; i++) {
3714
- const arg = args[i];
3715
- if (arg.startsWith("--")) {
3716
- const key = arg.slice(2);
3717
- if (key === "force" || key === "non-interactive" || key === "no-ai") {
3718
- flags[key] = true;
3719
- } else if (i + 1 < args.length && !args[i + 1].startsWith("--")) {
3720
- flags[key] = args[++i];
3721
- }
3722
- } else {
3723
- positional.push(arg);
3724
- }
3725
- }
3726
- return { flags, positional };
3727
- }
3728
- async function main() {
3729
- const args = process.argv.slice(2);
3730
- const { flags, positional } = parseFlags(args);
3731
- const command = positional[0];
3732
- if (command === "init") {
3733
- const nonInteractive = flags["non-interactive"] === true;
3734
- const force = flags["force"] === true;
3735
- let mode;
3736
- if (flags["mode"] === "dev" || flags["mode"] === "prod-sim") {
3737
- mode = flags["mode"];
3738
- } else if (nonInteractive) {
3739
- mode = "dev";
3740
- } else {
3741
- const result = await p.select({
3742
- message: "Which operating mode?",
3743
- options: [
3744
- { value: "dev", label: "dev", hint: "Direct editing in current checkout" },
3745
- {
3746
- value: "prod-sim",
3747
- label: "prod-sim",
3748
- hint: "Simulates production with local branch clones"
3749
- }
3750
- ],
3751
- initialValue: "dev"
3752
- });
3753
- if (p.isCancel(result)) {
3754
- p.cancel("Init cancelled.");
3755
- process.exit(0);
3756
- }
3757
- mode = result;
3758
- }
3759
- let appDir;
3760
- if (typeof flags["app-dir"] === "string") {
3761
- appDir = flags["app-dir"];
3762
- } else if (nonInteractive) {
3763
- appDir = "app";
3764
- } else {
3765
- const result = await p.text({
3766
- message: "App directory?",
3767
- placeholder: "app",
3768
- defaultValue: "app"
3769
- });
3770
- if (p.isCancel(result)) {
3771
- p.cancel("Init cancelled.");
3772
- process.exit(0);
3773
- }
3774
- appDir = result;
3775
- }
3776
- let ai;
3777
- if (flags["no-ai"] === true) {
3778
- ai = false;
3779
- } else if (nonInteractive) {
3780
- ai = true;
3781
- } else {
3782
- const result = await p.confirm({
3783
- message: "Include AI content endpoint?",
3784
- initialValue: true
3785
- });
3786
- if (p.isCancel(result)) {
3787
- p.cancel("Init cancelled.");
3788
- process.exit(0);
3789
- }
3790
- ai = result;
3791
- }
3792
- await init({
3793
- mode,
3794
- appDir,
3795
- ai,
3796
- projectDir: process.cwd(),
3797
- force,
3798
- nonInteractive
3799
- });
3800
- } else if (command === "init-deploy") {
3801
- const cloud = positional[1];
3802
- if (cloud !== "aws") {
3803
- console.error("Usage: canopycms init-deploy aws");
3804
- console.error('Only "aws" is currently supported.');
3805
- process.exit(1);
3806
- }
3807
- await initDeployAws({
3808
- cloud: "aws",
3809
- projectDir: process.cwd(),
3810
- force: flags["force"] === true,
3811
- nonInteractive: flags["non-interactive"] === true
3812
- });
3813
- } else if (command === "worker") {
3814
- const subcommand = positional[1];
3815
- if (subcommand !== "run-once") {
3816
- console.error("Usage: canopycms worker run-once");
3817
- process.exit(1);
3818
- }
3819
- const authMode = process.env.CANOPY_AUTH_MODE || "dev";
3820
- let authPlugin;
3821
- try {
3822
- if (authMode === "clerk") {
3823
- const pkg = "canopycms-auth-clerk";
3824
- const { createClerkAuthPlugin } = await import(pkg);
3825
- authPlugin = createClerkAuthPlugin({});
3826
- } else if (authMode === "dev") {
3827
- const pkg = "canopycms-auth-dev";
3828
- const { createDevAuthPlugin } = await import(pkg);
3829
- authPlugin = createDevAuthPlugin();
3830
- }
3831
- } catch {
3832
- console.warn(`Could not load auth plugin for mode "${authMode}" \u2014 skipping cache refresh`);
3833
- }
3834
- await workerRunOnce({ projectDir: process.cwd(), authPlugin });
3835
- } else if (command === "generate-ai-content") {
3836
- const { generateAIContentCLI: generateAIContentCLI2 } = await Promise.resolve().then(() => (init_generate_ai_content2(), generate_ai_content_exports));
3837
- await generateAIContentCLI2({
3838
- projectDir: process.cwd(),
3839
- outputDir: typeof flags["output"] === "string" ? flags["output"] : void 0,
3840
- configPath: typeof flags["config"] === "string" ? flags["config"] : void 0,
3841
- appDir: typeof flags["app-dir"] === "string" ? flags["app-dir"] : void 0
3842
- });
3843
- } else {
3844
- console.log("CanopyCMS CLI");
3845
- console.log("");
3846
- console.log("Commands:");
3847
- console.log(" init Add CanopyCMS to a Next.js app");
3848
- console.log(" --mode <dev|prod-sim> Operating mode (default: dev)");
3849
- console.log(" --app-dir <path> App directory (default: app)");
3850
- console.log(" --no-ai Skip AI content endpoint generation");
3851
- console.log(" --force Overwrite existing files without asking");
3852
- console.log(" --non-interactive Use defaults, no prompts");
3853
- console.log("");
3854
- console.log(" init-deploy aws Generate AWS deployment artifacts");
3855
- console.log(" --force Overwrite existing files without asking");
3856
- console.log(" --non-interactive Use defaults, no prompts");
3857
- console.log("");
3858
- console.log(" worker run-once Process tasks, sync git, refresh auth cache");
3859
- console.log(" generate-ai-content Generate static AI-ready content files");
3860
- console.log(" --output <dir> Output directory (default: public/ai)");
3861
- console.log(" --config <path> Path to AI content config file");
3862
- console.log(" --app-dir <path> App directory (default: app)");
3863
- process.exit(0);
3864
- }
3865
- }
3866
- var __filename = fileURLToPath2(import.meta.url);
3867
- var isDirectRun = realpathSync(process.argv[1]) === realpathSync(__filename);
3868
- if (isDirectRun) {
3869
- main().catch((err) => {
3870
- console.error("Error:", err instanceof Error ? err.message : String(err));
3871
- process.exit(1);
3872
- });
3873
- }
3874
1271
  export {
3875
1272
  init,
3876
1273
  initDeployAws,