canopycms 0.0.65 → 0.0.66-int.82
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -3
- package/dist/ai/generate.d.ts +13 -0
- package/dist/ai/generate.js +9 -3
- package/dist/ai/to-plain-text.js +4 -0
- package/dist/ai/types.d.ts +20 -1
- package/dist/api/admin-branch-health.js +13 -12
- package/dist/api/branch.js +2 -2
- package/dist/branch-health.js +1 -1
- package/dist/branch-metadata-file.d.ts +58 -0
- package/dist/branch-metadata-file.js +65 -0
- package/dist/branch-metadata.d.ts +2 -26
- package/dist/branch-metadata.js +6 -33
- package/dist/branch-registry.js +4 -2
- package/dist/build/generate-ai-content.js +103 -0
- package/dist/cli/cli.js +432 -317
- package/dist/cli/generate-ai-content.js +264 -160
- package/dist/content-id-index.js +22 -0
- package/dist/content-listing.js +3 -0
- package/dist/content-reader.d.ts +25 -1
- package/dist/content-reader.js +15 -0
- package/dist/content-store.d.ts +50 -1
- package/dist/content-store.js +102 -4
- package/dist/context.d.ts +36 -4
- package/dist/context.js +15 -4
- package/dist/editor/admin/SystemHealthPanel.js +2 -2
- package/dist/entry-schema.d.ts +22 -0
- package/dist/git-manager.d.ts +9 -8
- package/dist/git-manager.js +35 -9
- package/dist/github-service.d.ts +1 -1
- package/dist/github-service.js +1 -1
- package/dist/paths/branch-name.d.ts +1 -1
- package/dist/paths/branch-name.js +1 -1
- package/dist/schema/index.d.ts +1 -1
- package/dist/schema/index.js +1 -1
- package/dist/schema/meta-loader.js +1 -1
- package/dist/services.d.ts +22 -0
- package/dist/types.d.ts +1 -1
- package/dist/url-exclusivity-fixtures.d.ts +76 -0
- package/dist/url-exclusivity-fixtures.js +119 -0
- package/dist/url-path-resolver.js +8 -4
- package/dist/utils/content-write-lock.d.ts +2 -1
- package/dist/utils/content-write-lock.js +2 -1
- package/dist/utils/error.d.ts +17 -0
- package/dist/utils/error.js +17 -0
- package/dist/utils/git.d.ts +1 -1
- package/dist/utils/git.js +1 -1
- package/dist/utils/occ-json-write.js +1 -1
- package/dist/worker/cms-worker.d.ts +22 -288
- package/dist/worker/cms-worker.js +118 -1995
- package/dist/worker/git-sync.d.ts +166 -0
- package/dist/worker/git-sync.js +554 -0
- package/dist/worker/history-rewrite.d.ts +129 -0
- package/dist/worker/history-rewrite.js +216 -0
- package/dist/worker/rebase.d.ts +171 -0
- package/dist/worker/rebase.js +859 -0
- package/dist/worker/task-runner.d.ts +93 -0
- package/dist/worker/task-runner.js +529 -0
- package/dist/worker/worker-context.d.ts +133 -0
- package/dist/worker/worker-context.js +1 -0
- package/package.json +1 -1
|
@@ -24,11 +24,11 @@ var init_logger = __esm({
|
|
|
24
24
|
});
|
|
25
25
|
|
|
26
26
|
// dist/paths/normalize.js
|
|
27
|
-
function normalizeFilesystemPath(
|
|
28
|
-
return
|
|
27
|
+
function normalizeFilesystemPath(path22) {
|
|
28
|
+
return path22.split(/[\\/]+/).filter(Boolean).join("/");
|
|
29
29
|
}
|
|
30
|
-
function hasTraversalSequence(
|
|
31
|
-
const normalized = normalizeFilesystemPath(
|
|
30
|
+
function hasTraversalSequence(path22) {
|
|
31
|
+
const normalized = normalizeFilesystemPath(path22);
|
|
32
32
|
return normalized.includes("..");
|
|
33
33
|
}
|
|
34
34
|
function createLogicalPath(...segments) {
|
|
@@ -38,14 +38,14 @@ function createLogicalPath(...segments) {
|
|
|
38
38
|
}
|
|
39
39
|
return normalized;
|
|
40
40
|
}
|
|
41
|
-
function trimSlashes(
|
|
41
|
+
function trimSlashes(path22) {
|
|
42
42
|
let start = 0;
|
|
43
|
-
let end =
|
|
44
|
-
while (start < end &&
|
|
43
|
+
let end = path22.length;
|
|
44
|
+
while (start < end && path22[start] === "/")
|
|
45
45
|
start++;
|
|
46
|
-
while (end > start &&
|
|
46
|
+
while (end > start && path22[end - 1] === "/")
|
|
47
47
|
end--;
|
|
48
|
-
return
|
|
48
|
+
return path22.slice(start, end);
|
|
49
49
|
}
|
|
50
50
|
var init_normalize = __esm({
|
|
51
51
|
"dist/paths/normalize.js"() {
|
|
@@ -848,12 +848,12 @@ var init_operating_mode = __esm({
|
|
|
848
848
|
});
|
|
849
849
|
|
|
850
850
|
// dist/cli/generate-ai-content.js
|
|
851
|
-
import
|
|
851
|
+
import path21 from "node:path";
|
|
852
852
|
import { createJiti } from "jiti";
|
|
853
853
|
|
|
854
854
|
// dist/build/generate-ai-content.js
|
|
855
|
-
import
|
|
856
|
-
import
|
|
855
|
+
import fs18 from "node:fs/promises";
|
|
856
|
+
import path20 from "node:path";
|
|
857
857
|
|
|
858
858
|
// dist/utils/error.js
|
|
859
859
|
function getErrorMessage(err) {
|
|
@@ -1348,8 +1348,8 @@ function computeEntryUrl(collection, slug, contentRoot) {
|
|
|
1348
1348
|
if (slug && !isIndexSlug(slug)) {
|
|
1349
1349
|
segments.push(slug);
|
|
1350
1350
|
}
|
|
1351
|
-
const
|
|
1352
|
-
return
|
|
1351
|
+
const path22 = segments.length > 0 ? `/${segments.join("/")}` : "/";
|
|
1352
|
+
return path22.toLowerCase();
|
|
1353
1353
|
}
|
|
1354
1354
|
|
|
1355
1355
|
// dist/url-collision.js
|
|
@@ -1746,13 +1746,13 @@ function extractIdFromFilename(filename) {
|
|
|
1746
1746
|
return null;
|
|
1747
1747
|
}
|
|
1748
1748
|
async function resolveCollectionPath(root, logicalPath) {
|
|
1749
|
-
const
|
|
1750
|
-
const
|
|
1749
|
+
const fs19 = await import("node:fs/promises");
|
|
1750
|
+
const path22 = await import("node:path");
|
|
1751
1751
|
const segments = logicalPath.split("/").filter(Boolean);
|
|
1752
1752
|
let currentPath = root;
|
|
1753
1753
|
for (const segment of segments) {
|
|
1754
1754
|
try {
|
|
1755
|
-
const entries = await
|
|
1755
|
+
const entries = await fs19.readdir(currentPath, { withFileTypes: true });
|
|
1756
1756
|
const matchingDir = entries.find((entry) => {
|
|
1757
1757
|
if (!entry.isDirectory())
|
|
1758
1758
|
return false;
|
|
@@ -1760,7 +1760,7 @@ async function resolveCollectionPath(root, logicalPath) {
|
|
|
1760
1760
|
return logicalName === segment.toLowerCase();
|
|
1761
1761
|
});
|
|
1762
1762
|
if (matchingDir) {
|
|
1763
|
-
currentPath =
|
|
1763
|
+
currentPath = path22.join(currentPath, matchingDir.name);
|
|
1764
1764
|
} else {
|
|
1765
1765
|
return null;
|
|
1766
1766
|
}
|
|
@@ -2918,14 +2918,62 @@ var ContentStore = class {
|
|
|
2918
2918
|
getSchemaItems() {
|
|
2919
2919
|
return this.schemaIndex.values();
|
|
2920
2920
|
}
|
|
2921
|
-
assertSchemaItem(
|
|
2922
|
-
const normalized = normalizeFilesystemPath(
|
|
2921
|
+
assertSchemaItem(path22) {
|
|
2922
|
+
const normalized = normalizeFilesystemPath(path22);
|
|
2923
2923
|
const item = this.schemaIndex.get(normalized);
|
|
2924
2924
|
if (!item) {
|
|
2925
|
-
throw new ContentStoreError(`Unknown schema item: ${
|
|
2925
|
+
throw new ContentStoreError(`Unknown schema item: ${path22}`, "NO_SCHEMA_ITEM");
|
|
2926
2926
|
}
|
|
2927
2927
|
return item;
|
|
2928
2928
|
}
|
|
2929
|
+
/**
|
|
2930
|
+
* Is this path a COLLECTION schema item?
|
|
2931
|
+
*
|
|
2932
|
+
* The non-throwing form of `assertCollection`, reading the same `schemaIndex` -- which is the
|
|
2933
|
+
* point. A caller that gates on this cannot disagree with what `buildPaths` will then do: the
|
|
2934
|
+
* Map is last-wins, so where a subcollection's path collides with a parent's entry-type name
|
|
2935
|
+
* both this and `buildPaths` see the collection. A `find` over the flat schema LIST is
|
|
2936
|
+
* first-wins and would not.
|
|
2937
|
+
*
|
|
2938
|
+
* Type-only, deliberately -- `resolvePath` additionally requires `entries`, but a collection
|
|
2939
|
+
* with subcollections and no entries of its own is legal, and mirroring that stricter test here
|
|
2940
|
+
* would reject something `buildPaths` accepts.
|
|
2941
|
+
*
|
|
2942
|
+
* Exists for `readByUrlPath`'s URL-addressability gate; see `ReadContentInput`'s
|
|
2943
|
+
* `urlAddressableOnly` and the note on `buildPaths`' entry-type branch below.
|
|
2944
|
+
*/
|
|
2945
|
+
isCollectionPath(collectionPath) {
|
|
2946
|
+
return this.schemaIndex.get(normalizeFilesystemPath(collectionPath))?.type === "collection";
|
|
2947
|
+
}
|
|
2948
|
+
/**
|
|
2949
|
+
* Does this collection declare `entryTypeName` in its `entries` config?
|
|
2950
|
+
*
|
|
2951
|
+
* Mirrors `parseTypedFilename(filename, collection.entries)`, which is how `listEntries` decides
|
|
2952
|
+
* whether a file on disk is one of the collection's entries at all. `buildPaths`' own directory
|
|
2953
|
+
* scan deliberately does NOT check this -- it matches on slug alone, so that an entry whose type
|
|
2954
|
+
* was renamed out of the schema stays findable and therefore still editable, renameable and
|
|
2955
|
+
* deletable. Only URL resolution consults this, so what enumeration hides is not served.
|
|
2956
|
+
*
|
|
2957
|
+
* Returns FALSE when the collection declares no `entries` at all, which is stricter than
|
|
2958
|
+
* `parseTypedFilename`'s own `if (entryTypes && ...)` guard and deliberately so: the enumerating
|
|
2959
|
+
* surface is not `parseTypedFilename`, it is `listCollectionEntries`, and that returns `[]`
|
|
2960
|
+
* outright for a collection with no `entries`. A collections-only container therefore publishes
|
|
2961
|
+
* nothing, so a URL read that resolved a file sitting in one would be answering where nothing is
|
|
2962
|
+
* advertised -- the exact disagreement this predicate exists to close. Such a file cannot have
|
|
2963
|
+
* been created by the CMS (there is no entry type to create it as); it arrived by hand, by merge
|
|
2964
|
+
* or by retrofit, and it is invisible to the sitemap and to static params either way.
|
|
2965
|
+
*
|
|
2966
|
+
* Returns true for a path that is not a collection at all, because that is rule 1's question,
|
|
2967
|
+
* not this one's -- and under `urlAddressableOnly` rule 1 has already rejected it.
|
|
2968
|
+
*/
|
|
2969
|
+
declaresEntryType(collectionPath, entryTypeName) {
|
|
2970
|
+
const item = this.schemaIndex.get(normalizeFilesystemPath(collectionPath));
|
|
2971
|
+
if (item?.type !== "collection")
|
|
2972
|
+
return true;
|
|
2973
|
+
if (!item.entries)
|
|
2974
|
+
return false;
|
|
2975
|
+
return item.entries.some((e) => e.name === entryTypeName);
|
|
2976
|
+
}
|
|
2929
2977
|
assertCollection(collectionPath) {
|
|
2930
2978
|
const item = this.assertSchemaItem(collectionPath);
|
|
2931
2979
|
if (item.type !== "collection") {
|
|
@@ -3182,7 +3230,16 @@ var ContentStore = class {
|
|
|
3182
3230
|
collection: schemaItem.logicalPath,
|
|
3183
3231
|
collectionName: schemaItem.name,
|
|
3184
3232
|
format,
|
|
3185
|
-
|
|
3233
|
+
// Copy, never the object gray-matter hands back: its cache is
|
|
3234
|
+
// process-global and keyed by file content, so every caller parsing the
|
|
3235
|
+
// same bytes gets the SAME `data` instance (the hazard content-listing.ts
|
|
3236
|
+
// documents at length). Assigning it straight into `doc.data` was safe
|
|
3237
|
+
// only by accident -- `resolveReferencesInData` spreads its input, so the
|
|
3238
|
+
// shared object got replaced a few lines down. But that only happens when
|
|
3239
|
+
// resolution runs: with `resolveReferences: false` the caller was handed
|
|
3240
|
+
// the shared instance itself, and one mutation would poison every later
|
|
3241
|
+
// parse of those bytes.
|
|
3242
|
+
data: { ...parsed.data ?? {} },
|
|
3186
3243
|
body: parsed.content,
|
|
3187
3244
|
bodyFieldName: findBodyFieldName(fields),
|
|
3188
3245
|
relativePath,
|
|
@@ -4492,7 +4549,7 @@ function splitByCodeRegions(text) {
|
|
|
4492
4549
|
|
|
4493
4550
|
// dist/ai/generate.js
|
|
4494
4551
|
async function generateAIContent(options) {
|
|
4495
|
-
const { store, flatSchema, contentRoot, config, entryLinkUrl } = options;
|
|
4552
|
+
const { store, flatSchema, contentRoot, config, entryLinkUrl, generatedAt, buildId } = options;
|
|
4496
4553
|
const files = /* @__PURE__ */ new Map();
|
|
4497
4554
|
const idIndex = await store.idIndex();
|
|
4498
4555
|
const collections = flatSchema.filter((item) => item.type === "collection");
|
|
@@ -4543,7 +4600,8 @@ async function generateAIContent(options) {
|
|
|
4543
4600
|
}
|
|
4544
4601
|
}
|
|
4545
4602
|
const manifest = {
|
|
4546
|
-
|
|
4603
|
+
...buildId ? { buildId } : {},
|
|
4604
|
+
...generatedAt || !buildId ? { generated: generatedAt || (/* @__PURE__ */ new Date()).toISOString() } : {},
|
|
4547
4605
|
entries: rootEntries,
|
|
4548
4606
|
collections: manifestCollections,
|
|
4549
4607
|
bundles: manifestBundles
|
|
@@ -4757,15 +4815,47 @@ function matchesBundleFilter(entry, filter, contentRoot) {
|
|
|
4757
4815
|
}
|
|
4758
4816
|
|
|
4759
4817
|
// dist/branch-workspace.js
|
|
4760
|
-
import
|
|
4818
|
+
import path18 from "node:path";
|
|
4761
4819
|
|
|
4762
4820
|
// dist/branch-metadata.js
|
|
4763
|
-
import
|
|
4764
|
-
import
|
|
4821
|
+
import fs15 from "node:fs/promises";
|
|
4822
|
+
import path16 from "node:path";
|
|
4765
4823
|
|
|
4766
4824
|
// dist/branch-registry.js
|
|
4825
|
+
import fs13 from "node:fs/promises";
|
|
4826
|
+
import path14 from "node:path";
|
|
4827
|
+
|
|
4828
|
+
// dist/branch-metadata-file.js
|
|
4767
4829
|
import fs12 from "node:fs/promises";
|
|
4768
4830
|
import path13 from "node:path";
|
|
4831
|
+
var BRANCH_META_DIR = ".canopy-meta";
|
|
4832
|
+
var BRANCH_META_FILE = "branch.json";
|
|
4833
|
+
var BranchMetadataCorruptError = class extends Error {
|
|
4834
|
+
constructor(branchRoot, cause) {
|
|
4835
|
+
super(`Corrupt branch metadata in '${branchRoot}': ${cause}`);
|
|
4836
|
+
this.name = "BranchMetadataCorruptError";
|
|
4837
|
+
this.branchRoot = branchRoot;
|
|
4838
|
+
this.parseCause = cause;
|
|
4839
|
+
}
|
|
4840
|
+
};
|
|
4841
|
+
var branchMetadataFilePath = (branchRoot) => path13.join(path13.resolve(branchRoot), BRANCH_META_DIR, BRANCH_META_FILE);
|
|
4842
|
+
async function readBranchMetadataFile(branchRoot) {
|
|
4843
|
+
const resolvedRoot = path13.resolve(branchRoot);
|
|
4844
|
+
try {
|
|
4845
|
+
const raw = await fs12.readFile(branchMetadataFilePath(resolvedRoot), "utf8");
|
|
4846
|
+
return JSON.parse(raw);
|
|
4847
|
+
} catch (err) {
|
|
4848
|
+
if (isNotFoundError(err)) {
|
|
4849
|
+
return null;
|
|
4850
|
+
}
|
|
4851
|
+
if (err instanceof SyntaxError) {
|
|
4852
|
+
throw new BranchMetadataCorruptError(resolvedRoot, err.message);
|
|
4853
|
+
}
|
|
4854
|
+
throw err;
|
|
4855
|
+
}
|
|
4856
|
+
}
|
|
4857
|
+
|
|
4858
|
+
// dist/branch-registry.js
|
|
4769
4859
|
init_logger();
|
|
4770
4860
|
var log3 = createDebugLogger({ prefix: "BranchRegistry" });
|
|
4771
4861
|
var REGISTRY_FILE = "branches.json";
|
|
@@ -4778,10 +4868,10 @@ var BranchRegistry = class {
|
|
|
4778
4868
|
constructor(root) {
|
|
4779
4869
|
this.regenInFlight = null;
|
|
4780
4870
|
this.lastForcedRefreshMs = 0;
|
|
4781
|
-
this.root =
|
|
4782
|
-
this.registryPath =
|
|
4783
|
-
this.stalePath =
|
|
4784
|
-
this.tempPath =
|
|
4871
|
+
this.root = path14.resolve(root);
|
|
4872
|
+
this.registryPath = path14.join(this.root, REGISTRY_FILE);
|
|
4873
|
+
this.stalePath = path14.join(this.root, REGISTRY_STALE_FILE);
|
|
4874
|
+
this.tempPath = path14.join(this.root, REGISTRY_TEMP_FILE);
|
|
4785
4875
|
}
|
|
4786
4876
|
/**
|
|
4787
4877
|
* Returns all branches. Uses the cached snapshot if its embedded generation
|
|
@@ -4790,7 +4880,7 @@ var BranchRegistry = class {
|
|
|
4790
4880
|
async list() {
|
|
4791
4881
|
let parsed;
|
|
4792
4882
|
try {
|
|
4793
|
-
const raw = await
|
|
4883
|
+
const raw = await fs13.readFile(this.registryPath, "utf8");
|
|
4794
4884
|
parsed = JSON.parse(raw);
|
|
4795
4885
|
} catch (err) {
|
|
4796
4886
|
if (isNotFoundError(err)) {
|
|
@@ -4879,7 +4969,7 @@ var BranchRegistry = class {
|
|
|
4879
4969
|
async performRegenerate() {
|
|
4880
4970
|
const read = await readResourceGeneration(this.root, RESOURCE2);
|
|
4881
4971
|
const branches = await this.scanBranchDirectories();
|
|
4882
|
-
await
|
|
4972
|
+
await fs13.unlink(this.stalePath).catch(() => {
|
|
4883
4973
|
});
|
|
4884
4974
|
if (!read.ok) {
|
|
4885
4975
|
return branches;
|
|
@@ -4890,12 +4980,12 @@ var BranchRegistry = class {
|
|
|
4890
4980
|
generation: read.token
|
|
4891
4981
|
};
|
|
4892
4982
|
const uniqueTempPath = `${this.tempPath}.${Date.now()}.${Math.random().toString(36).slice(2)}`;
|
|
4893
|
-
await
|
|
4894
|
-
await
|
|
4983
|
+
await fs13.mkdir(this.root, { recursive: true });
|
|
4984
|
+
await fs13.writeFile(uniqueTempPath, JSON.stringify(snapshot, null, 2) + "\n", "utf8");
|
|
4895
4985
|
try {
|
|
4896
|
-
await
|
|
4986
|
+
await fs13.rename(uniqueTempPath, this.registryPath);
|
|
4897
4987
|
} catch (err) {
|
|
4898
|
-
await
|
|
4988
|
+
await fs13.unlink(uniqueTempPath).catch(() => {
|
|
4899
4989
|
});
|
|
4900
4990
|
throw err;
|
|
4901
4991
|
}
|
|
@@ -4909,15 +4999,15 @@ var BranchRegistry = class {
|
|
|
4909
4999
|
async scanBranchDirectories() {
|
|
4910
5000
|
const branches = [];
|
|
4911
5001
|
try {
|
|
4912
|
-
const entries = await
|
|
5002
|
+
const entries = await fs13.readdir(this.root, { withFileTypes: true });
|
|
4913
5003
|
for (const entry of entries) {
|
|
4914
5004
|
if (!entry.isDirectory() || entry.name.startsWith(".")) {
|
|
4915
5005
|
continue;
|
|
4916
5006
|
}
|
|
4917
|
-
const branchRoot =
|
|
5007
|
+
const branchRoot = path14.join(this.root, entry.name);
|
|
4918
5008
|
let meta;
|
|
4919
5009
|
try {
|
|
4920
|
-
meta = await
|
|
5010
|
+
meta = await readBranchMetadataFile(branchRoot);
|
|
4921
5011
|
} catch (err) {
|
|
4922
5012
|
canopyLogWarn(`CanopyCMS: Skipping branch directory '${entry.name}' during registry scan: ${getErrorMessage(err)}`);
|
|
4923
5013
|
continue;
|
|
@@ -4941,8 +5031,8 @@ var BranchRegistry = class {
|
|
|
4941
5031
|
};
|
|
4942
5032
|
|
|
4943
5033
|
// dist/utils/occ-json-write.js
|
|
4944
|
-
import
|
|
4945
|
-
import
|
|
5034
|
+
import fs14 from "node:fs/promises";
|
|
5035
|
+
import path15 from "node:path";
|
|
4946
5036
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
4947
5037
|
import lockfile2 from "proper-lockfile";
|
|
4948
5038
|
var log4 = createDebugLogger({ prefix: "OccJsonWrite" });
|
|
@@ -4958,29 +5048,29 @@ async function writeOccJsonFile(filePath, payload, options) {
|
|
|
4958
5048
|
const writeId = randomUUID2();
|
|
4959
5049
|
const data = { ...payload, version: newVersion, writeId };
|
|
4960
5050
|
const content = JSON.stringify(data, null, 2) + (trailingNewline ? "\n" : "");
|
|
4961
|
-
const dir =
|
|
4962
|
-
await
|
|
5051
|
+
const dir = path15.dirname(filePath);
|
|
5052
|
+
await fs14.mkdir(dir, { recursive: true });
|
|
4963
5053
|
const tempPath = `${filePath}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
4964
|
-
await
|
|
5054
|
+
await fs14.writeFile(tempPath, content, "utf-8");
|
|
4965
5055
|
if (expectedVersion === null) {
|
|
4966
5056
|
try {
|
|
4967
|
-
await
|
|
5057
|
+
await fs14.link(tempPath, filePath);
|
|
4968
5058
|
} catch (err) {
|
|
4969
|
-
await
|
|
5059
|
+
await fs14.unlink(tempPath).catch(() => {
|
|
4970
5060
|
});
|
|
4971
5061
|
if (isFileExistsError(err)) {
|
|
4972
5062
|
throw new OccWriteConflictError();
|
|
4973
5063
|
}
|
|
4974
5064
|
throw err;
|
|
4975
5065
|
}
|
|
4976
|
-
await
|
|
5066
|
+
await fs14.unlink(tempPath).catch(() => {
|
|
4977
5067
|
});
|
|
4978
5068
|
return { version: newVersion, writeId };
|
|
4979
5069
|
}
|
|
4980
5070
|
try {
|
|
4981
5071
|
let currentVersion = null;
|
|
4982
5072
|
try {
|
|
4983
|
-
const current = JSON.parse(await
|
|
5073
|
+
const current = JSON.parse(await fs14.readFile(filePath, "utf-8"));
|
|
4984
5074
|
currentVersion = current.version ?? 0;
|
|
4985
5075
|
} catch {
|
|
4986
5076
|
currentVersion = null;
|
|
@@ -4988,16 +5078,16 @@ async function writeOccJsonFile(filePath, payload, options) {
|
|
|
4988
5078
|
if (currentVersion !== expectedVersion) {
|
|
4989
5079
|
throw new OccWriteConflictError();
|
|
4990
5080
|
}
|
|
4991
|
-
await
|
|
5081
|
+
await fs14.rename(tempPath, filePath);
|
|
4992
5082
|
if (settleMs > 0) {
|
|
4993
5083
|
await new Promise((resolve2) => setTimeout(resolve2, settleMs));
|
|
4994
5084
|
}
|
|
4995
|
-
const afterWrite = JSON.parse(await
|
|
5085
|
+
const afterWrite = JSON.parse(await fs14.readFile(filePath, "utf-8"));
|
|
4996
5086
|
if (afterWrite.writeId !== writeId) {
|
|
4997
5087
|
throw new OccWriteConflictError();
|
|
4998
5088
|
}
|
|
4999
5089
|
} catch (err) {
|
|
5000
|
-
await
|
|
5090
|
+
await fs14.unlink(tempPath).catch(() => {
|
|
5001
5091
|
});
|
|
5002
5092
|
throw err;
|
|
5003
5093
|
}
|
|
@@ -5023,8 +5113,8 @@ async function withOccRetry(operation, options) {
|
|
|
5023
5113
|
throw new Error("Unreachable");
|
|
5024
5114
|
}
|
|
5025
5115
|
async function withOccFileLock(filePath, fn) {
|
|
5026
|
-
const dir =
|
|
5027
|
-
await
|
|
5116
|
+
const dir = path15.dirname(filePath);
|
|
5117
|
+
await fs14.mkdir(dir, { recursive: true });
|
|
5028
5118
|
const MAX_ATTEMPTS = 14;
|
|
5029
5119
|
let release = null;
|
|
5030
5120
|
for (let attempt = 1; release === null; attempt++) {
|
|
@@ -5068,8 +5158,6 @@ async function withOccFileLock(filePath, fn) {
|
|
|
5068
5158
|
}
|
|
5069
5159
|
|
|
5070
5160
|
// dist/branch-metadata.js
|
|
5071
|
-
var BRANCH_META_DIR = ".canopy-meta";
|
|
5072
|
-
var BRANCH_META_FILE = "branch.json";
|
|
5073
5161
|
var CURRENT_SCHEMA_VERSION = 1;
|
|
5074
5162
|
var BranchMetadataConflictError = class extends Error {
|
|
5075
5163
|
constructor(message = "Concurrent modification detected in branch metadata") {
|
|
@@ -5077,18 +5165,10 @@ var BranchMetadataConflictError = class extends Error {
|
|
|
5077
5165
|
this.name = "BranchMetadataConflictError";
|
|
5078
5166
|
}
|
|
5079
5167
|
};
|
|
5080
|
-
var BranchMetadataCorruptError = class extends Error {
|
|
5081
|
-
constructor(branchRoot, cause) {
|
|
5082
|
-
super(`Corrupt branch metadata in '${branchRoot}': ${cause}`);
|
|
5083
|
-
this.name = "BranchMetadataCorruptError";
|
|
5084
|
-
this.branchRoot = branchRoot;
|
|
5085
|
-
this.parseCause = cause;
|
|
5086
|
-
}
|
|
5087
|
-
};
|
|
5088
5168
|
var BranchMetadataFileManager = class _BranchMetadataFileManager {
|
|
5089
5169
|
constructor(branchRoot, baseRoot, options) {
|
|
5090
|
-
this.branchRoot =
|
|
5091
|
-
this.filePath =
|
|
5170
|
+
this.branchRoot = path16.resolve(branchRoot);
|
|
5171
|
+
this.filePath = path16.join(this.branchRoot, BRANCH_META_DIR, BRANCH_META_FILE);
|
|
5092
5172
|
this.baseRoot = baseRoot;
|
|
5093
5173
|
this.settleMs = options?.settleMs;
|
|
5094
5174
|
}
|
|
@@ -5097,20 +5177,7 @@ var BranchMetadataFileManager = class _BranchMetadataFileManager {
|
|
|
5097
5177
|
* Use this for read-only access (e.g., in registry scanning or loadBranchContext).
|
|
5098
5178
|
*/
|
|
5099
5179
|
static async loadOnly(branchRoot) {
|
|
5100
|
-
|
|
5101
|
-
const filePath = path15.join(resolvedRoot, BRANCH_META_DIR, BRANCH_META_FILE);
|
|
5102
|
-
try {
|
|
5103
|
-
const raw = await fs14.readFile(filePath, "utf8");
|
|
5104
|
-
return JSON.parse(raw);
|
|
5105
|
-
} catch (err) {
|
|
5106
|
-
if (isNotFoundError(err)) {
|
|
5107
|
-
return null;
|
|
5108
|
-
}
|
|
5109
|
-
if (err instanceof SyntaxError) {
|
|
5110
|
-
throw new BranchMetadataCorruptError(resolvedRoot, err.message);
|
|
5111
|
-
}
|
|
5112
|
-
throw err;
|
|
5113
|
-
}
|
|
5180
|
+
return readBranchMetadataFile(branchRoot);
|
|
5114
5181
|
}
|
|
5115
5182
|
/**
|
|
5116
5183
|
* Get a BranchMetadataFileManager instance configured for registry invalidation.
|
|
@@ -5121,7 +5188,7 @@ var BranchMetadataFileManager = class _BranchMetadataFileManager {
|
|
|
5121
5188
|
}
|
|
5122
5189
|
async load() {
|
|
5123
5190
|
try {
|
|
5124
|
-
const raw = await
|
|
5191
|
+
const raw = await fs15.readFile(this.filePath, "utf8");
|
|
5125
5192
|
const parsed = JSON.parse(raw);
|
|
5126
5193
|
const version = parsed.version ?? 0;
|
|
5127
5194
|
return { meta: parsed, version };
|
|
@@ -5183,7 +5250,7 @@ var BranchMetadataFileManager = class _BranchMetadataFileManager {
|
|
|
5183
5250
|
*/
|
|
5184
5251
|
async save(incoming) {
|
|
5185
5252
|
try {
|
|
5186
|
-
await
|
|
5253
|
+
await fs15.stat(this.branchRoot);
|
|
5187
5254
|
} catch (err) {
|
|
5188
5255
|
if (isNotFoundError(err)) {
|
|
5189
5256
|
throw new BranchMetadataConflictError("Branch no longer exists");
|
|
@@ -5271,8 +5338,8 @@ var loadBranchContext = async (options) => {
|
|
|
5271
5338
|
init_operating_mode();
|
|
5272
5339
|
|
|
5273
5340
|
// dist/git-manager.js
|
|
5274
|
-
import
|
|
5275
|
-
import
|
|
5341
|
+
import fs16 from "node:fs/promises";
|
|
5342
|
+
import path17 from "node:path";
|
|
5276
5343
|
import { simpleGit as simpleGit2 } from "simple-git";
|
|
5277
5344
|
|
|
5278
5345
|
// dist/utils/git.js
|
|
@@ -5332,11 +5399,11 @@ function gitChildEnv(overrides) {
|
|
|
5332
5399
|
var remoteInitLocks = /* @__PURE__ */ new Map();
|
|
5333
5400
|
var GITHUB_TRACKING_REF_PREFIX = "refs/remotes/github/";
|
|
5334
5401
|
async function ensureGitExcludePattern(repoPath, pattern) {
|
|
5335
|
-
const excludePath =
|
|
5336
|
-
await
|
|
5402
|
+
const excludePath = path17.join(repoPath, ".git", "info", "exclude");
|
|
5403
|
+
await fs16.mkdir(path17.dirname(excludePath), { recursive: true });
|
|
5337
5404
|
let content = "";
|
|
5338
5405
|
try {
|
|
5339
|
-
content = await
|
|
5406
|
+
content = await fs16.readFile(excludePath, "utf-8");
|
|
5340
5407
|
} catch (err) {
|
|
5341
5408
|
if (!isNotFoundError(err))
|
|
5342
5409
|
throw err;
|
|
@@ -5348,7 +5415,7 @@ async function ensureGitExcludePattern(repoPath, pattern) {
|
|
|
5348
5415
|
}
|
|
5349
5416
|
const needsLeadingNewline = content.length > 0 && !content.endsWith("\n");
|
|
5350
5417
|
const newContent = content + (needsLeadingNewline ? "\n" : "") + pattern + "\n";
|
|
5351
|
-
await
|
|
5418
|
+
await fs16.writeFile(excludePath, newContent, "utf-8");
|
|
5352
5419
|
log5.debug("git", "Added pattern to .git/info/exclude", { pattern });
|
|
5353
5420
|
}
|
|
5354
5421
|
var GitConflictError = class extends Error {
|
|
@@ -5369,12 +5436,12 @@ var GitRemoteRefMissingError = class extends Error {
|
|
|
5369
5436
|
};
|
|
5370
5437
|
var GitManager = class _GitManager {
|
|
5371
5438
|
constructor(options, gitOptions) {
|
|
5372
|
-
this.repoPath =
|
|
5439
|
+
this.repoPath = path17.resolve(options.repoPath);
|
|
5373
5440
|
this.baseBranch = options.baseBranch ?? "main";
|
|
5374
5441
|
this.remote = options.remote ?? "origin";
|
|
5375
5442
|
this.skipIndexMarker = options.skipIndexMarker ?? false;
|
|
5376
5443
|
this.git = simpleGit2({ baseDir: this.repoPath, ...gitOptions });
|
|
5377
|
-
this.git.env(gitChildEnv({ GIT_CEILING_DIRECTORIES:
|
|
5444
|
+
this.git.env(gitChildEnv({ GIT_CEILING_DIRECTORIES: path17.dirname(this.repoPath) }));
|
|
5378
5445
|
}
|
|
5379
5446
|
static async cloneRepo(remoteUrl, targetPath, baseBranch = "main") {
|
|
5380
5447
|
log5.debug("git", "Cloning repository", {
|
|
@@ -5416,11 +5483,11 @@ var GitManager = class _GitManager {
|
|
|
5416
5483
|
remotePath: options.remotePath,
|
|
5417
5484
|
baseBranch: options.baseBranch
|
|
5418
5485
|
});
|
|
5419
|
-
const remoteParent =
|
|
5486
|
+
const remoteParent = path17.dirname(options.remotePath);
|
|
5420
5487
|
releaseLock = await acquireProvisioningLock(remoteParent, ".remote-init.lock");
|
|
5421
5488
|
let remoteExists = false;
|
|
5422
5489
|
try {
|
|
5423
|
-
const stat = await
|
|
5490
|
+
const stat = await fs16.stat(options.remotePath);
|
|
5424
5491
|
remoteExists = stat.isDirectory();
|
|
5425
5492
|
} catch (err) {
|
|
5426
5493
|
if (!isNotFoundError(err))
|
|
@@ -5505,7 +5572,7 @@ var GitManager = class _GitManager {
|
|
|
5505
5572
|
* guard, which also needs the tracking namespace: `syncGit()` fetches
|
|
5506
5573
|
* GitHub into `GITHUB_TRACKING_REF_PREFIX*` rather than `refs/heads/*`
|
|
5507
5574
|
* directly (see that constant's doc comment above), and
|
|
5508
|
-
* `reconcileTrackedBranches()` (worker/
|
|
5575
|
+
* `reconcileTrackedBranches()` (worker/git-sync.ts) only
|
|
5509
5576
|
* non-destructively brings `refs/heads/*` toward what's tracked there — so
|
|
5510
5577
|
* a branch that another CanopyCMS deployment sharing this repo (or a
|
|
5511
5578
|
* direct push to GitHub) just created can sit in the tracking namespace
|
|
@@ -5580,7 +5647,7 @@ var GitManager = class _GitManager {
|
|
|
5580
5647
|
* "absent" deterministic instead of parsing update-ref's locale-dependent
|
|
5581
5648
|
* failure text, and its captured SHA is passed to `update-ref -d` as the
|
|
5582
5649
|
* expected old value -- same pattern as reconcileTrackedBranches'
|
|
5583
|
-
* guarded updates (worker/
|
|
5650
|
+
* guarded updates (worker/git-sync.ts): a concurrent Lambda push
|
|
5584
5651
|
* re-creating/moving this branch between the read and the delete makes
|
|
5585
5652
|
* update-ref throw (surfaced as the caller's best-effort warning) instead
|
|
5586
5653
|
* of silently deleting a commit that was just pushed. No
|
|
@@ -5672,7 +5739,7 @@ var GitManager = class _GitManager {
|
|
|
5672
5739
|
*/
|
|
5673
5740
|
static async validateGitRepoExists(repoPath) {
|
|
5674
5741
|
try {
|
|
5675
|
-
const stat = await
|
|
5742
|
+
const stat = await fs16.stat(path17.join(repoPath, ".git"));
|
|
5676
5743
|
if (!stat.isDirectory()) {
|
|
5677
5744
|
throw new Error(`Expected git repo at ${repoPath}`);
|
|
5678
5745
|
}
|
|
@@ -5750,7 +5817,7 @@ var GitManager = class _GitManager {
|
|
|
5750
5817
|
}
|
|
5751
5818
|
if (config.autoDetectRemotePath) {
|
|
5752
5819
|
try {
|
|
5753
|
-
const stat = await
|
|
5820
|
+
const stat = await fs16.stat(config.autoDetectRemotePath);
|
|
5754
5821
|
if (stat.isDirectory()) {
|
|
5755
5822
|
log5.debug("git", "Auto-detected local remote", {
|
|
5756
5823
|
path: config.autoDetectRemotePath
|
|
@@ -5763,8 +5830,8 @@ var GitManager = class _GitManager {
|
|
|
5763
5830
|
if (config.shouldAutoInitLocal) {
|
|
5764
5831
|
const gitRoot = await this.findGitRoot();
|
|
5765
5832
|
const sourceRoot = options.sourceRoot;
|
|
5766
|
-
const sourcePath = sourceRoot ?
|
|
5767
|
-
const localRemotePath =
|
|
5833
|
+
const sourcePath = sourceRoot ? path17.resolve(gitRoot, sourceRoot) : gitRoot;
|
|
5834
|
+
const localRemotePath = path17.join(sourcePath, config.defaultRemotePath);
|
|
5768
5835
|
await this.ensureLocalSimulatedRemote({
|
|
5769
5836
|
remotePath: localRemotePath,
|
|
5770
5837
|
sourcePath: gitRoot,
|
|
@@ -5792,7 +5859,7 @@ var GitManager = class _GitManager {
|
|
|
5792
5859
|
static async repoExistsAt(workspacePath) {
|
|
5793
5860
|
try {
|
|
5794
5861
|
const checkGit = simpleGit2({ baseDir: workspacePath });
|
|
5795
|
-
checkGit.env(gitChildEnv({ GIT_CEILING_DIRECTORIES:
|
|
5862
|
+
checkGit.env(gitChildEnv({ GIT_CEILING_DIRECTORIES: path17.dirname(workspacePath) }));
|
|
5796
5863
|
await checkGit.raw(["rev-parse", "--git-dir"]);
|
|
5797
5864
|
return true;
|
|
5798
5865
|
} catch {
|
|
@@ -5814,19 +5881,19 @@ var GitManager = class _GitManager {
|
|
|
5814
5881
|
const baseBranch = await resolveBaseBranch({
|
|
5815
5882
|
defaultBaseBranch: options.baseBranch,
|
|
5816
5883
|
mode: options.mode,
|
|
5817
|
-
detectFrom: options.sourceRoot ?
|
|
5884
|
+
detectFrom: options.sourceRoot ? path17.resolve(process.cwd(), options.sourceRoot) : process.cwd()
|
|
5818
5885
|
});
|
|
5819
5886
|
const remoteName = options.remoteName ?? "origin";
|
|
5820
5887
|
const repoExists = await _GitManager.repoExistsAt(options.workspacePath);
|
|
5821
5888
|
if (!repoExists) {
|
|
5822
|
-
const gitPath =
|
|
5889
|
+
const gitPath = path17.join(options.workspacePath, ".git");
|
|
5823
5890
|
try {
|
|
5824
|
-
const stat = await
|
|
5891
|
+
const stat = await fs16.stat(gitPath);
|
|
5825
5892
|
if (stat.isDirectory()) {
|
|
5826
5893
|
log5.debug("git", "Removing corrupt .git directory", {
|
|
5827
5894
|
workspacePath: options.workspacePath
|
|
5828
5895
|
});
|
|
5829
|
-
await
|
|
5896
|
+
await fs16.rm(gitPath, { recursive: true });
|
|
5830
5897
|
}
|
|
5831
5898
|
} catch (cleanupErr) {
|
|
5832
5899
|
if (!isNotFoundError(cleanupErr))
|
|
@@ -5853,7 +5920,7 @@ var GitManager = class _GitManager {
|
|
|
5853
5920
|
}
|
|
5854
5921
|
justCloned = true;
|
|
5855
5922
|
const freshGit = simpleGit2({ baseDir: options.workspacePath });
|
|
5856
|
-
freshGit.env(gitChildEnv({ GIT_CEILING_DIRECTORIES:
|
|
5923
|
+
freshGit.env(gitChildEnv({ GIT_CEILING_DIRECTORIES: path17.dirname(options.workspacePath) }));
|
|
5857
5924
|
await freshGit.addConfig("canopycms.managed", "true");
|
|
5858
5925
|
await freshGit.addConfig("user.name", options.gitBotAuthorName);
|
|
5859
5926
|
await freshGit.addConfig("user.email", options.gitBotAuthorEmail);
|
|
@@ -6204,9 +6271,9 @@ var GitManager = class _GitManager {
|
|
|
6204
6271
|
} catch {
|
|
6205
6272
|
}
|
|
6206
6273
|
for (const [filePath, content] of Object.entries(initialFiles)) {
|
|
6207
|
-
const absolutePath =
|
|
6208
|
-
await
|
|
6209
|
-
await
|
|
6274
|
+
const absolutePath = path17.join(this.repoPath, filePath);
|
|
6275
|
+
await fs16.mkdir(path17.dirname(absolutePath), { recursive: true });
|
|
6276
|
+
await fs16.writeFile(absolutePath, content, "utf-8");
|
|
6210
6277
|
await this.git.add(filePath);
|
|
6211
6278
|
}
|
|
6212
6279
|
await this.git.commit("Initialize settings branch", ["--allow-empty"]);
|
|
@@ -6235,7 +6302,7 @@ var BranchWorkspaceManager = class {
|
|
|
6235
6302
|
branchName: options.branchName,
|
|
6236
6303
|
mode: options.mode
|
|
6237
6304
|
});
|
|
6238
|
-
releaseLock = await acquireProvisioningLock(
|
|
6305
|
+
releaseLock = await acquireProvisioningLock(path18.dirname(options.branchRoot), `.${path18.basename(options.branchRoot)}.init.lock`);
|
|
6239
6306
|
await GitManager.initializeWorkspace({
|
|
6240
6307
|
workspacePath: options.branchRoot,
|
|
6241
6308
|
branchName: options.branchName,
|
|
@@ -6276,7 +6343,7 @@ var BranchWorkspaceManager = class {
|
|
|
6276
6343
|
const baseBranch = await resolveBaseBranch({
|
|
6277
6344
|
defaultBaseBranch: this.config.defaultBaseBranch,
|
|
6278
6345
|
mode,
|
|
6279
|
-
detectFrom: this.config.sourceRoot ?
|
|
6346
|
+
detectFrom: this.config.sourceRoot ? path18.resolve(process.cwd(), this.config.sourceRoot) : void 0
|
|
6280
6347
|
});
|
|
6281
6348
|
await this.ensureGitWorkspace({
|
|
6282
6349
|
branchRoot,
|
|
@@ -6360,8 +6427,8 @@ async function resolveBranchRoot(config) {
|
|
|
6360
6427
|
}
|
|
6361
6428
|
|
|
6362
6429
|
// dist/content-listing.js
|
|
6363
|
-
import
|
|
6364
|
-
import
|
|
6430
|
+
import fs17 from "node:fs/promises";
|
|
6431
|
+
import path19 from "node:path";
|
|
6365
6432
|
import matter3 from "gray-matter";
|
|
6366
6433
|
import { parse as yamlParse2 } from "yaml";
|
|
6367
6434
|
var log7 = createDebugLogger({ prefix: "ContentListing" });
|
|
@@ -6374,7 +6441,7 @@ var normalizePath = (root, target) => {
|
|
|
6374
6441
|
};
|
|
6375
6442
|
var readEntryData = async (filePath, format, bodyFieldName = "body") => {
|
|
6376
6443
|
try {
|
|
6377
|
-
const raw = await
|
|
6444
|
+
const raw = await fs17.readFile(filePath, "utf8");
|
|
6378
6445
|
if (format === "json") {
|
|
6379
6446
|
return asRecord(JSON.parse(raw));
|
|
6380
6447
|
}
|
|
@@ -6496,7 +6563,7 @@ var listCollectionEntries = async (root, collection, onSkip) => {
|
|
|
6496
6563
|
normalizePath(root, collectionRoot);
|
|
6497
6564
|
let dirents;
|
|
6498
6565
|
try {
|
|
6499
|
-
dirents = await
|
|
6566
|
+
dirents = await fs17.readdir(collectionRoot, { withFileTypes: true });
|
|
6500
6567
|
} catch (err) {
|
|
6501
6568
|
if (isNotFoundError(err))
|
|
6502
6569
|
return [];
|
|
@@ -6504,7 +6571,7 @@ var listCollectionEntries = async (root, collection, onSkip) => {
|
|
|
6504
6571
|
}
|
|
6505
6572
|
const files = dirents.filter((d) => d.isFile() && validExts.some((ext) => d.name.endsWith(ext)) && d.name !== ".collection.json").sort((a, b) => a.name.localeCompare(b.name));
|
|
6506
6573
|
const entries = await Promise.all(files.map(async (file) => {
|
|
6507
|
-
const absolutePath =
|
|
6574
|
+
const absolutePath = path19.join(collectionRoot, file.name);
|
|
6508
6575
|
const relativePath = normalizePath(root, absolutePath);
|
|
6509
6576
|
const parsed = parseTypedFilename(file.name, entryTypes);
|
|
6510
6577
|
if (!parsed) {
|
|
@@ -6519,7 +6586,7 @@ var listCollectionEntries = async (root, collection, onSkip) => {
|
|
|
6519
6586
|
const format = entryType?.format || "json";
|
|
6520
6587
|
const bodyField = entryType?.schema ? findBodyFieldName(entryType.schema) : "body";
|
|
6521
6588
|
const [stats, data] = await Promise.all([
|
|
6522
|
-
|
|
6589
|
+
fs17.stat(absolutePath),
|
|
6523
6590
|
readEntryData(absolutePath, format, bodyField)
|
|
6524
6591
|
]);
|
|
6525
6592
|
const item = {
|
|
@@ -6589,30 +6656,30 @@ function isValidImageCropValue(value) {
|
|
|
6589
6656
|
}
|
|
6590
6657
|
return isValidCropRect(x, y, w, h);
|
|
6591
6658
|
}
|
|
6592
|
-
function validateImageValue(field, value,
|
|
6659
|
+
function validateImageValue(field, value, path22) {
|
|
6593
6660
|
if (!isPlainRecord3(value)) {
|
|
6594
|
-
return [{ fieldPath:
|
|
6661
|
+
return [{ fieldPath: path22, message: "Expected an image object with { src, alt }" }];
|
|
6595
6662
|
}
|
|
6596
6663
|
const errors = [];
|
|
6597
6664
|
if (typeof value.src !== "string" || value.src.trim() === "") {
|
|
6598
|
-
errors.push({ fieldPath: `${
|
|
6665
|
+
errors.push({ fieldPath: `${path22}.src`, message: "Image src is required" });
|
|
6599
6666
|
}
|
|
6600
6667
|
if (typeof value.alt !== "string") {
|
|
6601
|
-
errors.push({ fieldPath: `${
|
|
6668
|
+
errors.push({ fieldPath: `${path22}.alt`, message: "Image alt text is required" });
|
|
6602
6669
|
} else if (value.alt.trim() === "" && field.altOptional !== true) {
|
|
6603
|
-
errors.push({ fieldPath: `${
|
|
6670
|
+
errors.push({ fieldPath: `${path22}.alt`, message: "Image alt text is required" });
|
|
6604
6671
|
}
|
|
6605
6672
|
if (value.width !== void 0 && !isPositiveInt(value.width)) {
|
|
6606
|
-
errors.push({ fieldPath: `${
|
|
6673
|
+
errors.push({ fieldPath: `${path22}.width`, message: "Image width must be a positive integer" });
|
|
6607
6674
|
}
|
|
6608
6675
|
if (value.height !== void 0 && !isPositiveInt(value.height)) {
|
|
6609
6676
|
errors.push({
|
|
6610
|
-
fieldPath: `${
|
|
6677
|
+
fieldPath: `${path22}.height`,
|
|
6611
6678
|
message: "Image height must be a positive integer"
|
|
6612
6679
|
});
|
|
6613
6680
|
}
|
|
6614
6681
|
if (value.crop !== void 0 && !isValidImageCropValue(value.crop)) {
|
|
6615
|
-
errors.push({ fieldPath: `${
|
|
6682
|
+
errors.push({ fieldPath: `${path22}.crop`, message: "Invalid image crop rect" });
|
|
6616
6683
|
}
|
|
6617
6684
|
return errors;
|
|
6618
6685
|
}
|
|
@@ -6624,44 +6691,44 @@ function referenceValueId(value) {
|
|
|
6624
6691
|
return void 0;
|
|
6625
6692
|
}
|
|
6626
6693
|
var selectOptionValues = (field) => (field.options ?? []).map((opt) => typeof opt === "string" ? opt : opt.value);
|
|
6627
|
-
function validateScalar(field, value,
|
|
6694
|
+
function validateScalar(field, value, path22) {
|
|
6628
6695
|
switch (field.type) {
|
|
6629
6696
|
case "number":
|
|
6630
6697
|
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
6631
|
-
return [{ fieldPath:
|
|
6698
|
+
return [{ fieldPath: path22, message: "Expected a number" }];
|
|
6632
6699
|
}
|
|
6633
6700
|
return [];
|
|
6634
6701
|
case "boolean":
|
|
6635
6702
|
if (typeof value !== "boolean") {
|
|
6636
|
-
return [{ fieldPath:
|
|
6703
|
+
return [{ fieldPath: path22, message: "Expected true or false" }];
|
|
6637
6704
|
}
|
|
6638
6705
|
return [];
|
|
6639
6706
|
case "datetime":
|
|
6640
6707
|
if (typeof value !== "string" || value !== "" && Number.isNaN(Date.parse(value))) {
|
|
6641
|
-
return [{ fieldPath:
|
|
6708
|
+
return [{ fieldPath: path22, message: "Expected a valid date/time string" }];
|
|
6642
6709
|
}
|
|
6643
6710
|
return [];
|
|
6644
6711
|
case "select": {
|
|
6645
6712
|
if (typeof value !== "string") {
|
|
6646
|
-
return [{ fieldPath:
|
|
6713
|
+
return [{ fieldPath: path22, message: "Expected a selection" }];
|
|
6647
6714
|
}
|
|
6648
6715
|
const options = selectOptionValues(field);
|
|
6649
6716
|
if (value !== "" && !options.includes(value)) {
|
|
6650
|
-
return [{ fieldPath:
|
|
6717
|
+
return [{ fieldPath: path22, message: `Must be one of: ${options.join(", ")}` }];
|
|
6651
6718
|
}
|
|
6652
6719
|
return [];
|
|
6653
6720
|
}
|
|
6654
6721
|
case "reference":
|
|
6655
6722
|
if (referenceValueId(value) === void 0) {
|
|
6656
|
-
return [{ fieldPath:
|
|
6723
|
+
return [{ fieldPath: path22, message: "Expected a reference id" }];
|
|
6657
6724
|
}
|
|
6658
6725
|
return [];
|
|
6659
6726
|
case "image":
|
|
6660
|
-
return validateImageValue(field, value,
|
|
6727
|
+
return validateImageValue(field, value, path22);
|
|
6661
6728
|
default:
|
|
6662
6729
|
if (STRING_FIELD_TYPES.has(field.type)) {
|
|
6663
6730
|
if (typeof value !== "string") {
|
|
6664
|
-
return [{ fieldPath:
|
|
6731
|
+
return [{ fieldPath: path22, message: "Expected text" }];
|
|
6665
6732
|
}
|
|
6666
6733
|
}
|
|
6667
6734
|
return [];
|
|
@@ -6684,12 +6751,12 @@ function validateEntryData(fields, data, pathPrefix = "") {
|
|
|
6684
6751
|
errors.push(...validateEntryData(field.fields, data, pathPrefix));
|
|
6685
6752
|
continue;
|
|
6686
6753
|
}
|
|
6687
|
-
const
|
|
6754
|
+
const path22 = pathPrefix ? `${pathPrefix}.${field.name}` : field.name;
|
|
6688
6755
|
const value = data[field.name];
|
|
6689
6756
|
const isRequired = "required" in field && field.required === true;
|
|
6690
6757
|
if (isRequired && isEmptyForRequired(field, value)) {
|
|
6691
6758
|
const message = Array.isArray(value) ? "At least one item is required" : "This field is required";
|
|
6692
|
-
errors.push({ fieldPath:
|
|
6759
|
+
errors.push({ fieldPath: path22, message });
|
|
6693
6760
|
continue;
|
|
6694
6761
|
}
|
|
6695
6762
|
if (value === void 0 || value === null)
|
|
@@ -6697,11 +6764,11 @@ function validateEntryData(fields, data, pathPrefix = "") {
|
|
|
6697
6764
|
if (field.type === "block") {
|
|
6698
6765
|
const blockField = field;
|
|
6699
6766
|
if (!Array.isArray(value)) {
|
|
6700
|
-
errors.push({ fieldPath:
|
|
6767
|
+
errors.push({ fieldPath: path22, message: "Expected a list of blocks" });
|
|
6701
6768
|
continue;
|
|
6702
6769
|
}
|
|
6703
6770
|
value.forEach((item, index) => {
|
|
6704
|
-
const itemPath = `${
|
|
6771
|
+
const itemPath = `${path22}[${index}]`;
|
|
6705
6772
|
if (!isPlainRecord3(item)) {
|
|
6706
6773
|
errors.push({ fieldPath: itemPath, message: "Expected a block object" });
|
|
6707
6774
|
return;
|
|
@@ -6723,11 +6790,11 @@ function validateEntryData(fields, data, pathPrefix = "") {
|
|
|
6723
6790
|
const objectField = field;
|
|
6724
6791
|
if (objectField.list) {
|
|
6725
6792
|
if (!Array.isArray(value)) {
|
|
6726
|
-
errors.push({ fieldPath:
|
|
6793
|
+
errors.push({ fieldPath: path22, message: "Expected a list of items" });
|
|
6727
6794
|
continue;
|
|
6728
6795
|
}
|
|
6729
6796
|
value.forEach((item, index) => {
|
|
6730
|
-
const itemPath = `${
|
|
6797
|
+
const itemPath = `${path22}[${index}]`;
|
|
6731
6798
|
if (!isPlainRecord3(item)) {
|
|
6732
6799
|
errors.push({ fieldPath: itemPath, message: "Expected an object" });
|
|
6733
6800
|
return;
|
|
@@ -6737,27 +6804,27 @@ function validateEntryData(fields, data, pathPrefix = "") {
|
|
|
6737
6804
|
}
|
|
6738
6805
|
});
|
|
6739
6806
|
} else if (!isPlainRecord3(value)) {
|
|
6740
|
-
errors.push({ fieldPath:
|
|
6807
|
+
errors.push({ fieldPath: path22, message: "Expected an object" });
|
|
6741
6808
|
} else if (objectField.fields) {
|
|
6742
|
-
errors.push(...validateEntryData(objectField.fields, value,
|
|
6809
|
+
errors.push(...validateEntryData(objectField.fields, value, path22));
|
|
6743
6810
|
}
|
|
6744
6811
|
continue;
|
|
6745
6812
|
}
|
|
6746
6813
|
if ("list" in field && field.list) {
|
|
6747
6814
|
if (!Array.isArray(value)) {
|
|
6748
|
-
errors.push({ fieldPath:
|
|
6815
|
+
errors.push({ fieldPath: path22, message: "Expected a list of values" });
|
|
6749
6816
|
continue;
|
|
6750
6817
|
}
|
|
6751
6818
|
value.forEach((item, index) => {
|
|
6752
|
-
errors.push(...validateScalar(field, item, `${
|
|
6819
|
+
errors.push(...validateScalar(field, item, `${path22}[${index}]`));
|
|
6753
6820
|
});
|
|
6754
6821
|
continue;
|
|
6755
6822
|
}
|
|
6756
6823
|
if (Array.isArray(value) && KNOWN_FIELD_TYPES.has(field.type)) {
|
|
6757
|
-
errors.push({ fieldPath:
|
|
6824
|
+
errors.push({ fieldPath: path22, message: "Expected a single value, not a list" });
|
|
6758
6825
|
continue;
|
|
6759
6826
|
}
|
|
6760
|
-
errors.push(...validateScalar(field, value,
|
|
6827
|
+
errors.push(...validateScalar(field, value, path22));
|
|
6761
6828
|
}
|
|
6762
6829
|
return errors;
|
|
6763
6830
|
}
|
|
@@ -6808,7 +6875,7 @@ These are likely abandoned create-scaffolds (empty entries left behind when a cr
|
|
|
6808
6875
|
var GENERATED_RECORD_FILENAME = ".canopy-generated.json";
|
|
6809
6876
|
async function readGeneratedRecord(outputDir) {
|
|
6810
6877
|
try {
|
|
6811
|
-
const raw = await
|
|
6878
|
+
const raw = await fs18.readFile(path20.join(outputDir, GENERATED_RECORD_FILENAME), "utf-8");
|
|
6812
6879
|
const parsed = JSON.parse(raw);
|
|
6813
6880
|
if (typeof parsed !== "object" || parsed === null || !("files" in parsed))
|
|
6814
6881
|
return [];
|
|
@@ -6822,20 +6889,56 @@ async function readGeneratedRecord(outputDir) {
|
|
|
6822
6889
|
}
|
|
6823
6890
|
async function writeGeneratedRecord(outputDir, files) {
|
|
6824
6891
|
const record = { files: [...files].sort() };
|
|
6825
|
-
await
|
|
6826
|
-
await
|
|
6892
|
+
await fs18.mkdir(outputDir, { recursive: true });
|
|
6893
|
+
await fs18.writeFile(path20.join(outputDir, GENERATED_RECORD_FILENAME), `${JSON.stringify(record, null, 2)}
|
|
6827
6894
|
`, "utf-8");
|
|
6828
6895
|
}
|
|
6829
6896
|
async function removeEmptyDirsUpward(startDir, stopAt) {
|
|
6830
6897
|
let current = startDir;
|
|
6831
6898
|
while (current.startsWith(stopAt) && current !== stopAt) {
|
|
6832
6899
|
try {
|
|
6833
|
-
await
|
|
6900
|
+
await fs18.rmdir(current);
|
|
6834
6901
|
} catch {
|
|
6835
6902
|
return;
|
|
6836
6903
|
}
|
|
6837
|
-
current =
|
|
6904
|
+
current = path20.dirname(current);
|
|
6905
|
+
}
|
|
6906
|
+
}
|
|
6907
|
+
var SAFE_BUILD_ID = /^[A-Za-z0-9._-]{1,255}$/;
|
|
6908
|
+
var BUILD_ID_RULE = 'must be 1-255 characters of [A-Za-z0-9._-] and not "." or ".."';
|
|
6909
|
+
function isUsableBuildId(value) {
|
|
6910
|
+
return SAFE_BUILD_ID.test(value) && value !== "." && value !== "..";
|
|
6911
|
+
}
|
|
6912
|
+
function unpinnedConsequence(buildId) {
|
|
6913
|
+
return buildId ? "omitting `generated` from the manifest entirely (a build id is set, so no build clock is recorded)." : "recording the current time in `generated` instead.";
|
|
6914
|
+
}
|
|
6915
|
+
function resolveBuildStamp() {
|
|
6916
|
+
const rawBuildId = process.env.CANOPY_BUILD_ID;
|
|
6917
|
+
const trimmedBuildId = rawBuildId?.trim();
|
|
6918
|
+
let buildId;
|
|
6919
|
+
if (rawBuildId === void 0) {
|
|
6920
|
+
buildId = void 0;
|
|
6921
|
+
} else if (!trimmedBuildId) {
|
|
6922
|
+
console.warn("CanopyCMS: CANOPY_BUILD_ID is set but blank \u2014 recording no buildId in the manifest.");
|
|
6923
|
+
} else if (!isUsableBuildId(trimmedBuildId)) {
|
|
6924
|
+
console.warn(`CanopyCMS: ignoring CANOPY_BUILD_ID="${trimmedBuildId}": it ${BUILD_ID_RULE}, so that the manifest's buildId can match the build id Next stores for the same artifact.`);
|
|
6925
|
+
} else {
|
|
6926
|
+
buildId = trimmedBuildId;
|
|
6927
|
+
}
|
|
6928
|
+
const rawEpoch = process.env.SOURCE_DATE_EPOCH;
|
|
6929
|
+
const trimmedEpoch = rawEpoch?.trim();
|
|
6930
|
+
if (rawEpoch !== void 0 && !trimmedEpoch) {
|
|
6931
|
+
console.warn(`CanopyCMS: SOURCE_DATE_EPOCH is set but blank \u2014 ${unpinnedConsequence(buildId)}`);
|
|
6932
|
+
}
|
|
6933
|
+
if (!trimmedEpoch)
|
|
6934
|
+
return { buildId };
|
|
6935
|
+
const seconds = /^\d+$/.test(trimmedEpoch) ? Number(trimmedEpoch) : Number.NaN;
|
|
6936
|
+
const date = new Date(seconds * 1e3);
|
|
6937
|
+
if (Number.isNaN(date.getTime())) {
|
|
6938
|
+
console.warn(`CanopyCMS: ignoring SOURCE_DATE_EPOCH="${trimmedEpoch}" (expected decimal seconds since the Unix epoch) \u2014 ${unpinnedConsequence(buildId)}`);
|
|
6939
|
+
return { buildId };
|
|
6838
6940
|
}
|
|
6941
|
+
return { generatedAt: date.toISOString(), buildId };
|
|
6839
6942
|
}
|
|
6840
6943
|
async function generateAIContentFiles(options) {
|
|
6841
6944
|
const { config, entrySchemaRegistry, outputDir, aiConfig, _testFlatSchema } = options;
|
|
@@ -6857,11 +6960,12 @@ async function generateAIContentFiles(options) {
|
|
|
6857
6960
|
flatSchema,
|
|
6858
6961
|
contentRoot: contentRootName,
|
|
6859
6962
|
config: aiConfig,
|
|
6860
|
-
entryLinkUrl: config.entryLinkUrl
|
|
6963
|
+
entryLinkUrl: config.entryLinkUrl,
|
|
6964
|
+
...resolveBuildStamp()
|
|
6861
6965
|
});
|
|
6862
|
-
const absoluteOutputDir =
|
|
6966
|
+
const absoluteOutputDir = path20.resolve(outputDir) + path20.sep;
|
|
6863
6967
|
const resolveOutputPath = (filePath) => {
|
|
6864
|
-
const absolutePath =
|
|
6968
|
+
const absolutePath = path20.resolve(path20.join(absoluteOutputDir, filePath));
|
|
6865
6969
|
if (!absolutePath.startsWith(absoluteOutputDir)) {
|
|
6866
6970
|
throw new Error(`Path traversal detected in AI content output: ${filePath}`);
|
|
6867
6971
|
}
|
|
@@ -6873,8 +6977,8 @@ async function generateAIContentFiles(options) {
|
|
|
6873
6977
|
let fileCount = 0;
|
|
6874
6978
|
for (const [filePath, content] of result.files) {
|
|
6875
6979
|
const absolutePath = resolveOutputPath(filePath);
|
|
6876
|
-
await
|
|
6877
|
-
await
|
|
6980
|
+
await fs18.mkdir(path20.dirname(absolutePath), { recursive: true });
|
|
6981
|
+
await fs18.writeFile(absolutePath, content, "utf-8");
|
|
6878
6982
|
fileCount++;
|
|
6879
6983
|
}
|
|
6880
6984
|
const currentSet = new Set(currentFiles);
|
|
@@ -6883,13 +6987,13 @@ async function generateAIContentFiles(options) {
|
|
|
6883
6987
|
for (const filePath of stale) {
|
|
6884
6988
|
const absolutePath = resolveOutputPath(filePath);
|
|
6885
6989
|
try {
|
|
6886
|
-
await
|
|
6990
|
+
await fs18.unlink(absolutePath);
|
|
6887
6991
|
removedCount++;
|
|
6888
6992
|
} catch (err) {
|
|
6889
6993
|
if (!isNodeError(err) || err.code !== "ENOENT")
|
|
6890
6994
|
throw err;
|
|
6891
6995
|
}
|
|
6892
|
-
await removeEmptyDirsUpward(
|
|
6996
|
+
await removeEmptyDirsUpward(path20.dirname(absolutePath), absoluteOutputDir);
|
|
6893
6997
|
}
|
|
6894
6998
|
await writeGeneratedRecord(absoluteOutputDir, currentFiles);
|
|
6895
6999
|
return { fileCount, removedCount, outputDir: absoluteOutputDir };
|
|
@@ -6900,7 +7004,7 @@ var jiti = createJiti(import.meta.url);
|
|
|
6900
7004
|
async function generateAIContentCLI(options) {
|
|
6901
7005
|
const { projectDir, outputDir = "public/ai", configPath, appDir = "app" } = options;
|
|
6902
7006
|
console.log("\nCanopyCMS generate-ai-content\n");
|
|
6903
|
-
const canopyConfigPath =
|
|
7007
|
+
const canopyConfigPath = path21.join(projectDir, "canopycms.config.ts");
|
|
6904
7008
|
let canopyConfigModule;
|
|
6905
7009
|
try {
|
|
6906
7010
|
canopyConfigModule = await jiti.import(canopyConfigPath);
|
|
@@ -6911,7 +7015,7 @@ async function generateAIContentCLI(options) {
|
|
|
6911
7015
|
}
|
|
6912
7016
|
const configExport = canopyConfigModule.default ?? canopyConfigModule.config ?? canopyConfigModule;
|
|
6913
7017
|
const serverConfig = typeof configExport === "object" && configExport !== null && "server" in configExport ? configExport.server : configExport;
|
|
6914
|
-
const schemasPath =
|
|
7018
|
+
const schemasPath = path21.join(projectDir, appDir, "schemas.ts");
|
|
6915
7019
|
let entrySchemaRegistry = {};
|
|
6916
7020
|
try {
|
|
6917
7021
|
const schemasModule = await jiti.import(schemasPath);
|
|
@@ -6922,7 +7026,7 @@ async function generateAIContentCLI(options) {
|
|
|
6922
7026
|
let aiConfig;
|
|
6923
7027
|
if (configPath) {
|
|
6924
7028
|
try {
|
|
6925
|
-
const aiConfigModule = await jiti.import(
|
|
7029
|
+
const aiConfigModule = await jiti.import(path21.resolve(configPath));
|
|
6926
7030
|
aiConfig = aiConfigModule.aiContentConfig ?? aiConfigModule.default ?? aiConfigModule.config;
|
|
6927
7031
|
} catch (err) {
|
|
6928
7032
|
console.error(`Could not load AI config from ${configPath}`);
|
|
@@ -6939,7 +7043,7 @@ async function generateAIContentCLI(options) {
|
|
|
6939
7043
|
console.error("Make sure canopycms.config.ts uses defineCanopyConfig().");
|
|
6940
7044
|
process.exit(1);
|
|
6941
7045
|
}
|
|
6942
|
-
const resolvedOutput =
|
|
7046
|
+
const resolvedOutput = path21.resolve(projectDir, outputDir);
|
|
6943
7047
|
console.log(` Output: ${resolvedOutput}`);
|
|
6944
7048
|
console.log(` Mode: ${serverConfig.mode ?? "dev"}`);
|
|
6945
7049
|
const result = await generateAIContentFiles({
|