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