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