canopycms 0.0.11 → 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.
- package/README.md +24 -27
- package/dist/ai/handler.d.ts +3 -3
- package/dist/ai/handler.d.ts.map +1 -1
- package/dist/ai/handler.js +6 -9
- package/dist/ai/handler.js.map +1 -1
- package/dist/ai/resolve-branch.d.ts +1 -2
- package/dist/ai/resolve-branch.d.ts.map +1 -1
- package/dist/ai/resolve-branch.js +8 -9
- package/dist/ai/resolve-branch.js.map +1 -1
- package/dist/api/branch.js +2 -2
- package/dist/api/branch.js.map +1 -1
- package/dist/api/github-sync.js +0 -2
- package/dist/api/github-sync.js.map +1 -1
- package/dist/api/settings-helpers.d.ts +3 -5
- package/dist/api/settings-helpers.d.ts.map +1 -1
- package/dist/api/settings-helpers.js +6 -19
- package/dist/api/settings-helpers.js.map +1 -1
- package/dist/auth/caching-auth-plugin.d.ts +7 -1
- package/dist/auth/caching-auth-plugin.d.ts.map +1 -1
- package/dist/auth/caching-auth-plugin.js +31 -3
- package/dist/auth/caching-auth-plugin.js.map +1 -1
- package/dist/auth/plugin.d.ts +1 -1
- package/dist/authorization/types.d.ts +1 -1
- package/dist/branch-registry.js +1 -1
- package/dist/branch-registry.js.map +1 -1
- package/dist/branch-schema-cache.d.ts +8 -13
- package/dist/branch-schema-cache.d.ts.map +1 -1
- package/dist/branch-schema-cache.js +55 -44
- package/dist/branch-schema-cache.js.map +1 -1
- package/dist/cli/cli.d.ts +20 -0
- package/dist/cli/cli.d.ts.map +1 -0
- package/dist/cli/cli.js +196 -0
- package/dist/cli/cli.js.map +1 -0
- package/dist/cli/generate-ai-content.js +1501 -723
- package/dist/cli/init.d.ts +2 -3
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +258 -2861
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/sync.d.ts +33 -0
- package/dist/cli/sync.d.ts.map +1 -0
- package/dist/cli/sync.js +510 -0
- package/dist/cli/sync.js.map +1 -0
- package/dist/config/schemas/config.d.ts +5 -5
- package/dist/config/schemas/config.d.ts.map +1 -1
- package/dist/config/schemas/config.js +1 -1
- package/dist/config/schemas/config.js.map +1 -1
- package/dist/config-test.d.ts.map +1 -1
- package/dist/config-test.js +0 -1
- package/dist/config-test.js.map +1 -1
- package/dist/content-reader.js +1 -1
- package/dist/content-reader.js.map +1 -1
- package/dist/editor/BranchManager.d.ts.map +1 -1
- package/dist/editor/BranchManager.js +1 -3
- package/dist/editor/BranchManager.js.map +1 -1
- package/dist/git-manager.d.ts +2 -3
- package/dist/git-manager.d.ts.map +1 -1
- package/dist/git-manager.js +12 -4
- package/dist/git-manager.js.map +1 -1
- package/dist/operating-mode/client-safe-strategy.d.ts +1 -12
- package/dist/operating-mode/client-safe-strategy.d.ts.map +1 -1
- package/dist/operating-mode/client-safe-strategy.js +5 -42
- package/dist/operating-mode/client-safe-strategy.js.map +1 -1
- package/dist/operating-mode/client-unsafe-strategy.d.ts.map +1 -1
- package/dist/operating-mode/client-unsafe-strategy.js +10 -68
- package/dist/operating-mode/client-unsafe-strategy.js.map +1 -1
- package/dist/operating-mode/index.d.ts +3 -3
- package/dist/operating-mode/index.d.ts.map +1 -1
- package/dist/operating-mode/index.js +2 -2
- package/dist/operating-mode/types.d.ts +2 -6
- package/dist/operating-mode/types.d.ts.map +1 -1
- package/dist/services.d.ts +6 -0
- package/dist/services.d.ts.map +1 -1
- package/dist/services.js +52 -40
- package/dist/services.js.map +1 -1
- package/dist/settings-branch-utils.d.ts +2 -2
- package/dist/settings-branch-utils.js +3 -3
- package/dist/settings-branch-utils.js.map +1 -1
- package/dist/settings-workspace.d.ts +1 -2
- package/dist/settings-workspace.d.ts.map +1 -1
- package/dist/settings-workspace.js +1 -2
- package/dist/settings-workspace.js.map +1 -1
- package/dist/utils/fs.d.ts +3 -0
- package/dist/utils/fs.d.ts.map +1 -0
- package/dist/utils/fs.js +15 -0
- package/dist/utils/fs.js.map +1 -0
- package/dist/utils/git.d.ts +7 -0
- package/dist/utils/git.d.ts.map +1 -0
- package/dist/utils/git.js +17 -0
- package/dist/utils/git.js.map +1 -0
- package/dist/worker/task-queue-config.d.ts +2 -4
- package/dist/worker/task-queue-config.d.ts.map +1 -1
- package/dist/worker/task-queue-config.js +3 -7
- package/dist/worker/task-queue-config.js.map +1 -1
- 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", "
|
|
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
|
|
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
|
|
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 "
|
|
811
|
-
return path3.join(process.cwd(), ".canopy-
|
|
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
|
|
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
|
|
510
|
+
await fs3.mkdir(dir, { recursive: true });
|
|
927
511
|
const tempPath = `${filePath}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
928
|
-
await
|
|
512
|
+
await fs3.writeFile(tempPath, content, "utf-8");
|
|
929
513
|
try {
|
|
930
|
-
await
|
|
514
|
+
await fs3.rename(tempPath, filePath);
|
|
931
515
|
} catch (err) {
|
|
932
|
-
await
|
|
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
|
|
528
|
+
import fs4 from "node:fs/promises";
|
|
945
529
|
import path5 from "node:path";
|
|
946
530
|
import crypto from "node:crypto";
|
|
947
|
-
function
|
|
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
|
|
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
|
|
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 (
|
|
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
|
|
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
|
|
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 (
|
|
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
|
|
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
|
|
617
|
+
await fs4.unlink(processingPath).catch(() => {
|
|
1034
618
|
});
|
|
1035
619
|
return;
|
|
1036
620
|
}
|
|
1037
621
|
task = parsed;
|
|
1038
622
|
} catch (err) {
|
|
1039
|
-
if (
|
|
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
|
|
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
|
|
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
|
|
645
|
+
await fs4.unlink(processingPath).catch(() => {
|
|
1062
646
|
});
|
|
1063
647
|
return;
|
|
1064
648
|
}
|
|
1065
649
|
task = parsed;
|
|
1066
650
|
} catch (err) {
|
|
1067
|
-
if (
|
|
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
|
|
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
|
|
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
|
|
673
|
+
await fs4.unlink(processingPath).catch(() => {
|
|
1090
674
|
});
|
|
1091
675
|
return;
|
|
1092
676
|
}
|
|
1093
677
|
task = parsed;
|
|
1094
678
|
} catch (err) {
|
|
1095
|
-
if (
|
|
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
|
|
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
|
|
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
|
|
708
|
+
const stat = await fs4.stat(filePath);
|
|
1125
709
|
if (now - stat.mtimeMs >= maxAgeMs) {
|
|
1126
|
-
const content = await
|
|
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
|
|
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
|
|
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 (
|
|
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
|
|
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
|
|
755
|
+
const stat = await fs4.stat(filePath);
|
|
1172
756
|
if (now - stat.mtimeMs >= maxAgeMs) {
|
|
1173
|
-
await
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1264
|
-
await
|
|
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
|
|
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/
|
|
1322
|
-
|
|
1323
|
-
|
|
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/
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1357
|
-
return
|
|
919
|
+
supportsStatusBadge() {
|
|
920
|
+
return true;
|
|
1358
921
|
}
|
|
1359
|
-
|
|
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/
|
|
1377
|
-
import
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
return
|
|
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
|
-
|
|
1389
|
-
|
|
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
|
-
|
|
1395
|
-
|
|
1396
|
-
if (isValidId(candidate))
|
|
1397
|
-
return candidate;
|
|
989
|
+
getContentBranchesRoot(sourceRoot) {
|
|
990
|
+
return path.join(this.getWorkspaceRoot(sourceRoot), "content-branches");
|
|
1398
991
|
}
|
|
1399
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
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
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
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
|
-
|
|
1461
|
-
return
|
|
1032
|
+
shouldCreateSettingsPR(config) {
|
|
1033
|
+
return config.autoCreateSettingsPR ?? true;
|
|
1462
1034
|
}
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1760
|
-
|
|
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
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
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
|
-
|
|
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
|
-
})`;
|
|
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
|
-
|
|
3047
|
-
if (
|
|
3048
|
-
return
|
|
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
|
-
|
|
3051
|
-
|
|
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
|
-
|
|
3057
|
-
|
|
3058
|
-
return false;
|
|
1078
|
+
usesSeparateSettingsBranch() {
|
|
1079
|
+
return true;
|
|
3059
1080
|
}
|
|
3060
|
-
|
|
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
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
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/
|
|
3395
|
-
|
|
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/
|
|
3413
|
-
|
|
3414
|
-
|
|
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
|
-
|
|
3428
|
-
|
|
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/
|
|
3481
|
-
|
|
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
|
-
|
|
1123
|
+
await fs.stat(filePath);
|
|
1124
|
+
return true;
|
|
3494
1125
|
} catch (err) {
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
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 =
|
|
3570
|
-
if (await
|
|
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
|
|
3587
|
-
await
|
|
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(
|
|
3602
|
-
await writeFile(
|
|
3603
|
-
await writeFile(
|
|
3604
|
-
await writeFile(
|
|
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(
|
|
1172
|
+
await writeFile(path6.join(projectDir, appDir, "edit/page.tsx"), await editPage2({ configImport: configImportPath(appDir, 1) }), writeOpts);
|
|
3608
1173
|
if (ai) {
|
|
3609
|
-
await writeFile(
|
|
3610
|
-
await writeFile(
|
|
3611
|
-
}
|
|
3612
|
-
const gitignorePath =
|
|
3613
|
-
if (await
|
|
3614
|
-
const content = await
|
|
3615
|
-
if (!content.includes(".canopy-
|
|
3616
|
-
await
|
|
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(
|
|
3641
|
-
await writeFile(
|
|
3642
|
-
const nextConfigPath =
|
|
3643
|
-
const nextConfigMjsPath =
|
|
3644
|
-
const configPath = await
|
|
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
|
|
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 ${
|
|
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 =
|
|
3661
|
-
let mode = "
|
|
1225
|
+
const cfgPath = path6.join(options.projectDir, "canopycms.config.ts");
|
|
1226
|
+
let mode = "dev";
|
|
3662
1227
|
try {
|
|
3663
|
-
const configContent = await
|
|
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
|
-
|
|
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,
|