@contentrain/mcp 1.10.1 → 1.11.0
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/{apply-manager-UXUTYFZn.mjs → apply-manager-tRqDkzBV.mjs} +6 -6
- package/dist/{apply-manager-UXUTYFZn.mjs.map → apply-manager-tRqDkzBV.mjs.map} +1 -1
- package/dist/{branch-lifecycle-Dd84lx37.mjs → branch-lifecycle-BAfgSQBv.mjs} +2 -2
- package/dist/{branch-lifecycle-Dd84lx37.mjs.map → branch-lifecycle-BAfgSQBv.mjs.map} +1 -1
- package/dist/{config-DlFcTxFG.mjs → config-oxxgznz7.mjs} +1 -1
- package/dist/{config-DlFcTxFG.mjs.map → config-oxxgznz7.mjs.map} +1 -1
- package/dist/{content-manager-DcmNwvmq.d.mts → content-manager-BgxF3dHu.d.mts} +3 -1
- package/dist/content-manager-BgxF3dHu.d.mts.map +1 -0
- package/dist/{context-CYN__o3Q.mjs → context-CrS-IvVm.mjs} +3 -3
- package/dist/{context-CYN__o3Q.mjs.map → context-CrS-IvVm.mjs.map} +1 -1
- package/dist/core/apply-manager.mjs +7 -7
- package/dist/core/config.mjs +1 -1
- package/dist/core/content-manager.d.mts +1 -1
- package/dist/core/content-manager.mjs +2 -2
- package/dist/core/context.mjs +4 -4
- package/dist/core/doctor.d.mts.map +1 -1
- package/dist/core/doctor.mjs +5 -5
- package/dist/core/meta-manager.d.mts +29 -1
- package/dist/core/meta-manager.d.mts.map +1 -1
- package/dist/core/meta-manager.mjs +2 -2
- package/dist/core/model-manager.mjs +2 -2
- package/dist/core/ops/index.d.mts +17 -2
- package/dist/core/ops/index.d.mts.map +1 -1
- package/dist/core/ops/index.mjs +4 -2
- package/dist/core/validator/index.d.mts.map +1 -1
- package/dist/core/validator/index.mjs +4 -4
- package/dist/{doctor-BwSJW1sS.mjs → doctor-BRPXxKhx.mjs} +37 -11
- package/dist/doctor-BRPXxKhx.mjs.map +1 -0
- package/dist/git/branch-lifecycle.mjs +2 -2
- package/dist/git/transaction.mjs +6 -6
- package/dist/index.mjs +12 -12
- package/dist/{local-BIE23GuI.mjs → local-DqmDeNxI.mjs} +6 -6
- package/dist/{local-BIE23GuI.mjs.map → local-DqmDeNxI.mjs.map} +1 -1
- package/dist/meta-manager-CJUiTgP2.mjs +98 -0
- package/dist/meta-manager-CJUiTgP2.mjs.map +1 -0
- package/dist/{model-manager-BhLsUgaB.mjs → model-manager-SiesTJrS.mjs} +48 -24
- package/dist/model-manager-SiesTJrS.mjs.map +1 -0
- package/dist/{ops-Dh2T3u53.mjs → ops-B422KP_S.mjs} +26 -23
- package/dist/ops-B422KP_S.mjs.map +1 -0
- package/dist/{paths-CYl5vCBD.mjs → paths-CmVw5Cw2.mjs} +15 -4
- package/dist/{paths-CYl5vCBD.mjs.map → paths-CmVw5Cw2.mjs.map} +1 -1
- package/dist/providers/local/index.mjs +8 -8
- package/dist/server/http/index.mjs +12 -12
- package/dist/{server-BkDuFvVt.mjs → server-DSEl2wzT.mjs} +113 -54
- package/dist/server-DSEl2wzT.mjs.map +1 -0
- package/dist/server.mjs +12 -12
- package/dist/{transaction-DhVoGigz.mjs → transaction-BHOsiEn1.mjs} +4 -4
- package/dist/{transaction-DhVoGigz.mjs.map → transaction-BHOsiEn1.mjs.map} +1 -1
- package/dist/{validator-8vaPxSba.mjs → validator-Ck0puppQ.mjs} +48 -8
- package/dist/validator-Ck0puppQ.mjs.map +1 -0
- package/package.json +1 -1
- package/dist/content-manager-DcmNwvmq.d.mts.map +0 -1
- package/dist/doctor-BwSJW1sS.mjs.map +0 -1
- package/dist/meta-manager-C900rEyG.mjs +0 -53
- package/dist/meta-manager-C900rEyG.mjs.map +0 -1
- package/dist/model-manager-BhLsUgaB.mjs.map +0 -1
- package/dist/ops-Dh2T3u53.mjs.map +0 -1
- package/dist/server-BkDuFvVt.mjs.map +0 -1
- package/dist/validator-8vaPxSba.mjs.map +0 -1
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { a as readJson, s as writeJson, t as contentrainDir } from "./fs-DLbVB-Ek.mjs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { rm } from "node:fs/promises";
|
|
4
|
+
//#region src/core/meta-manager.ts
|
|
5
|
+
/**
|
|
6
|
+
* Absolute meta path. Non-i18n models keep exactly one meta record, pinned to
|
|
7
|
+
* the default locale — see `ops/paths.ts:metaFilePath` for the full rationale.
|
|
8
|
+
*/
|
|
9
|
+
function metaPath(projectRoot, model, opts) {
|
|
10
|
+
const base = join(contentrainDir(projectRoot), "meta", model.id);
|
|
11
|
+
const effective = model.i18n ? opts.locale ?? opts.defaultLocale : opts.defaultLocale;
|
|
12
|
+
if (model.kind === "document" && opts.slug) return join(base, opts.slug, `${effective}.json`);
|
|
13
|
+
return join(base, `${effective}.json`);
|
|
14
|
+
}
|
|
15
|
+
async function readMeta(projectRoot, model, opts) {
|
|
16
|
+
return readJson(metaPath(projectRoot, model, opts));
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Build the meta for a content write, merged over the entry's existing meta.
|
|
20
|
+
*
|
|
21
|
+
* `status` is a publish decision and belongs to whoever made it — editing a
|
|
22
|
+
* field must not unpublish an entry. Only an entry with no meta yet starts at
|
|
23
|
+
* `draft`. `source`/`updated_by` describe *this* write, so they are stamped
|
|
24
|
+
* every time; `approved_by`/`version` survive via the spread.
|
|
25
|
+
*
|
|
26
|
+
* Pass `existing: undefined` to mint meta for a genuinely new entry.
|
|
27
|
+
*/
|
|
28
|
+
function mergeEntryMeta(existing, data) {
|
|
29
|
+
const meta = {
|
|
30
|
+
...existing,
|
|
31
|
+
status: existing?.status ?? "draft",
|
|
32
|
+
source: "agent",
|
|
33
|
+
updated_by: "contentrain-mcp"
|
|
34
|
+
};
|
|
35
|
+
if (data?.["publish_at"] !== void 0) meta.publish_at = data["publish_at"];
|
|
36
|
+
if (data?.["expire_at"] !== void 0) meta.expire_at = data["expire_at"];
|
|
37
|
+
return meta;
|
|
38
|
+
}
|
|
39
|
+
async function writeMeta(projectRoot, model, opts, meta) {
|
|
40
|
+
const filePath = metaPath(projectRoot, model, opts);
|
|
41
|
+
if (model.kind === "collection" && opts.entryId) {
|
|
42
|
+
const existing = await readJson(filePath) ?? {};
|
|
43
|
+
existing[opts.entryId] = meta;
|
|
44
|
+
await writeJson(filePath, existing);
|
|
45
|
+
} else await writeJson(filePath, meta);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Apply many entry-meta updates to one collection locale file in a single
|
|
49
|
+
* read-modify-write, returning the entry IDs actually written.
|
|
50
|
+
*
|
|
51
|
+
* Do NOT loop `writeMeta` over entry IDs: every call rewrites the whole shared
|
|
52
|
+
* `{locale}.json`, so concurrent calls each start from the same snapshot and
|
|
53
|
+
* all but the last-settling one are silently lost. This function exists so that
|
|
54
|
+
* race cannot be reintroduced.
|
|
55
|
+
*/
|
|
56
|
+
async function writeMetaEntries(projectRoot, model, opts, updates) {
|
|
57
|
+
const ids = Object.keys(updates);
|
|
58
|
+
if (ids.length === 0) return [];
|
|
59
|
+
const filePath = metaPath(projectRoot, model, opts);
|
|
60
|
+
await writeJson(filePath, {
|
|
61
|
+
...await readJson(filePath) ?? {},
|
|
62
|
+
...updates
|
|
63
|
+
});
|
|
64
|
+
return ids;
|
|
65
|
+
}
|
|
66
|
+
async function deleteMeta(projectRoot, model, opts) {
|
|
67
|
+
if (model.kind === "collection" && opts.entryId) {
|
|
68
|
+
const filePath = metaPath(projectRoot, model, opts);
|
|
69
|
+
const existing = await readJson(filePath);
|
|
70
|
+
if (existing && opts.entryId in existing) {
|
|
71
|
+
delete existing[opts.entryId];
|
|
72
|
+
if (Object.keys(existing).length > 0) await writeJson(filePath, existing);
|
|
73
|
+
else await rm(filePath, { force: true });
|
|
74
|
+
}
|
|
75
|
+
} else if (model.kind === "document" && opts.slug) {
|
|
76
|
+
const slugDir = join(contentrainDir(projectRoot), "meta", model.id, opts.slug);
|
|
77
|
+
if (opts.locale) await rm(join(slugDir, `${opts.locale}.json`), { force: true });
|
|
78
|
+
else try {
|
|
79
|
+
await rm(slugDir, {
|
|
80
|
+
recursive: true,
|
|
81
|
+
force: true
|
|
82
|
+
});
|
|
83
|
+
} catch {}
|
|
84
|
+
} else if (opts.locale) await rm(metaPath(projectRoot, model, opts), { force: true });
|
|
85
|
+
else {
|
|
86
|
+
const metaDir = join(contentrainDir(projectRoot), "meta", model.id);
|
|
87
|
+
try {
|
|
88
|
+
await rm(metaDir, {
|
|
89
|
+
recursive: true,
|
|
90
|
+
force: true
|
|
91
|
+
});
|
|
92
|
+
} catch {}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
//#endregion
|
|
96
|
+
export { writeMetaEntries as a, writeMeta as i, mergeEntryMeta as n, readMeta as r, deleteMeta as t };
|
|
97
|
+
|
|
98
|
+
//# sourceMappingURL=meta-manager-CJUiTgP2.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"meta-manager-CJUiTgP2.mjs","names":[],"sources":["../src/core/meta-manager.ts"],"sourcesContent":["import type { ModelDefinition, EntryMeta } from '@contentrain/types'\nimport { join } from 'node:path'\nimport { rm } from 'node:fs/promises'\nimport { contentrainDir, readJson, writeJson } from '../util/fs.js'\n\ninterface MetaOpts {\n locale?: string\n entryId?: string\n slug?: string\n /**\n * `config.locales.default`. Required: it places a non-i18n model's single\n * meta record, and stands in for an omitted locale on i18n models — the old\n * hardcoded `'en'` fallback silently wrote to the wrong file on projects\n * whose default was anything else.\n */\n defaultLocale: string\n}\n\n/**\n * Absolute meta path. Non-i18n models keep exactly one meta record, pinned to\n * the default locale — see `ops/paths.ts:metaFilePath` for the full rationale.\n */\nfunction metaPath(projectRoot: string, model: ModelDefinition, opts: MetaOpts): string {\n const base = join(contentrainDir(projectRoot), 'meta', model.id)\n const effective = model.i18n ? (opts.locale ?? opts.defaultLocale) : opts.defaultLocale\n\n if (model.kind === 'document' && opts.slug) {\n return join(base, opts.slug, `${effective}.json`)\n }\n\n return join(base, `${effective}.json`)\n}\n\nexport async function readMeta(\n projectRoot: string,\n model: ModelDefinition,\n opts: MetaOpts,\n): Promise<EntryMeta | Record<string, EntryMeta> | null> {\n return readJson(metaPath(projectRoot, model, opts))\n}\n\n/**\n * Build the meta for a content write, merged over the entry's existing meta.\n *\n * `status` is a publish decision and belongs to whoever made it — editing a\n * field must not unpublish an entry. Only an entry with no meta yet starts at\n * `draft`. `source`/`updated_by` describe *this* write, so they are stamped\n * every time; `approved_by`/`version` survive via the spread.\n *\n * Pass `existing: undefined` to mint meta for a genuinely new entry.\n */\nexport function mergeEntryMeta(\n existing: EntryMeta | undefined,\n data?: Record<string, unknown>,\n): EntryMeta {\n const meta: EntryMeta = {\n ...existing,\n status: existing?.status ?? 'draft',\n source: 'agent',\n updated_by: 'contentrain-mcp',\n }\n if (data?.['publish_at'] !== undefined) meta.publish_at = data['publish_at'] as string\n if (data?.['expire_at'] !== undefined) meta.expire_at = data['expire_at'] as string\n return meta\n}\n\nexport async function writeMeta(\n projectRoot: string,\n model: ModelDefinition,\n opts: MetaOpts,\n meta: EntryMeta,\n): Promise<void> {\n const filePath = metaPath(projectRoot, model, opts)\n\n if (model.kind === 'collection' && opts.entryId) {\n const existing = await readJson<Record<string, EntryMeta>>(filePath) ?? {}\n existing[opts.entryId] = meta\n await writeJson(filePath, existing)\n } else {\n await writeJson(filePath, meta)\n }\n}\n\n/**\n * Apply many entry-meta updates to one collection locale file in a single\n * read-modify-write, returning the entry IDs actually written.\n *\n * Do NOT loop `writeMeta` over entry IDs: every call rewrites the whole shared\n * `{locale}.json`, so concurrent calls each start from the same snapshot and\n * all but the last-settling one are silently lost. This function exists so that\n * race cannot be reintroduced.\n */\nexport async function writeMetaEntries(\n projectRoot: string,\n model: ModelDefinition,\n opts: MetaOpts,\n updates: Record<string, EntryMeta>,\n): Promise<string[]> {\n const ids = Object.keys(updates)\n if (ids.length === 0) return []\n\n const filePath = metaPath(projectRoot, model, opts)\n const existing = await readJson<Record<string, EntryMeta>>(filePath) ?? {}\n await writeJson(filePath, { ...existing, ...updates })\n return ids\n}\n\nexport async function deleteMeta(\n projectRoot: string,\n model: ModelDefinition,\n opts: MetaOpts,\n): Promise<void> {\n if (model.kind === 'collection' && opts.entryId) {\n const filePath = metaPath(projectRoot, model, opts)\n const existing = await readJson<Record<string, EntryMeta>>(filePath)\n if (existing && opts.entryId in existing) {\n delete existing[opts.entryId]\n if (Object.keys(existing).length > 0) {\n await writeJson(filePath, existing)\n } else {\n await rm(filePath, { force: true })\n }\n }\n } else if (model.kind === 'document' && opts.slug) {\n const slugDir = join(contentrainDir(projectRoot), 'meta', model.id, opts.slug)\n if (opts.locale) {\n await rm(join(slugDir, `${opts.locale}.json`), { force: true })\n } else {\n try {\n await rm(slugDir, { recursive: true, force: true })\n } catch {\n // directory may not exist\n }\n }\n } else if (opts.locale) {\n const filePath = metaPath(projectRoot, model, opts)\n await rm(filePath, { force: true })\n } else {\n // No locale specified — remove all meta for this model (non-i18n or full cleanup)\n const metaDir = join(contentrainDir(projectRoot), 'meta', model.id)\n try {\n await rm(metaDir, { recursive: true, force: true })\n } catch {\n // directory may not exist\n }\n }\n}\n"],"mappings":";;;;;;;;AAsBA,SAAS,SAAS,aAAqB,OAAwB,MAAwB;CACrF,MAAM,OAAO,KAAK,eAAe,YAAY,EAAE,QAAQ,MAAM,GAAG;CAChE,MAAM,YAAY,MAAM,OAAQ,KAAK,UAAU,KAAK,gBAAiB,KAAK;AAE1E,KAAI,MAAM,SAAS,cAAc,KAAK,KACpC,QAAO,KAAK,MAAM,KAAK,MAAM,GAAG,UAAU,OAAO;AAGnD,QAAO,KAAK,MAAM,GAAG,UAAU,OAAO;;AAGxC,eAAsB,SACpB,aACA,OACA,MACuD;AACvD,QAAO,SAAS,SAAS,aAAa,OAAO,KAAK,CAAC;;;;;;;;;;;;AAarD,SAAgB,eACd,UACA,MACW;CACX,MAAM,OAAkB;EACtB,GAAG;EACH,QAAQ,UAAU,UAAU;EAC5B,QAAQ;EACR,YAAY;EACb;AACD,KAAI,OAAO,kBAAkB,KAAA,EAAW,MAAK,aAAa,KAAK;AAC/D,KAAI,OAAO,iBAAiB,KAAA,EAAW,MAAK,YAAY,KAAK;AAC7D,QAAO;;AAGT,eAAsB,UACpB,aACA,OACA,MACA,MACe;CACf,MAAM,WAAW,SAAS,aAAa,OAAO,KAAK;AAEnD,KAAI,MAAM,SAAS,gBAAgB,KAAK,SAAS;EAC/C,MAAM,WAAW,MAAM,SAAoC,SAAS,IAAI,EAAE;AAC1E,WAAS,KAAK,WAAW;AACzB,QAAM,UAAU,UAAU,SAAS;OAEnC,OAAM,UAAU,UAAU,KAAK;;;;;;;;;;;AAanC,eAAsB,iBACpB,aACA,OACA,MACA,SACmB;CACnB,MAAM,MAAM,OAAO,KAAK,QAAQ;AAChC,KAAI,IAAI,WAAW,EAAG,QAAO,EAAE;CAE/B,MAAM,WAAW,SAAS,aAAa,OAAO,KAAK;AAEnD,OAAM,UAAU,UAAU;EAAE,GADX,MAAM,SAAoC,SAAS,IAAI,EAAE;EACjC,GAAG;EAAS,CAAC;AACtD,QAAO;;AAGT,eAAsB,WACpB,aACA,OACA,MACe;AACf,KAAI,MAAM,SAAS,gBAAgB,KAAK,SAAS;EAC/C,MAAM,WAAW,SAAS,aAAa,OAAO,KAAK;EACnD,MAAM,WAAW,MAAM,SAAoC,SAAS;AACpE,MAAI,YAAY,KAAK,WAAW,UAAU;AACxC,UAAO,SAAS,KAAK;AACrB,OAAI,OAAO,KAAK,SAAS,CAAC,SAAS,EACjC,OAAM,UAAU,UAAU,SAAS;OAEnC,OAAM,GAAG,UAAU,EAAE,OAAO,MAAM,CAAC;;YAG9B,MAAM,SAAS,cAAc,KAAK,MAAM;EACjD,MAAM,UAAU,KAAK,eAAe,YAAY,EAAE,QAAQ,MAAM,IAAI,KAAK,KAAK;AAC9E,MAAI,KAAK,OACP,OAAM,GAAG,KAAK,SAAS,GAAG,KAAK,OAAO,OAAO,EAAE,EAAE,OAAO,MAAM,CAAC;MAE/D,KAAI;AACF,SAAM,GAAG,SAAS;IAAE,WAAW;IAAM,OAAO;IAAM,CAAC;UAC7C;YAID,KAAK,OAEd,OAAM,GADW,SAAS,aAAa,OAAO,KAAK,EAChC,EAAE,OAAO,MAAM,CAAC;MAC9B;EAEL,MAAM,UAAU,KAAK,eAAe,YAAY,EAAE,QAAQ,MAAM,GAAG;AACnE,MAAI;AACF,SAAM,GAAG,SAAS;IAAE,WAAW;IAAM,OAAO;IAAM,CAAC;UAC7C"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { n as contentFilePath, r as documentFilePath, t as contentDirPath } from "./paths-
|
|
1
|
+
import { n as contentFilePath, r as documentFilePath, t as contentDirPath } from "./paths-CmVw5Cw2.mjs";
|
|
2
2
|
import { a as readJson, c as writeText, i as readDir, n as ensureDir, o as readText, s as writeJson, t as contentrainDir } from "./fs-DLbVB-Ek.mjs";
|
|
3
|
-
import {
|
|
3
|
+
import { i as writeMeta, n as mergeEntryMeta, r as readMeta, t as deleteMeta } from "./meta-manager-CJUiTgP2.mjs";
|
|
4
4
|
import { join, resolve } from "node:path";
|
|
5
5
|
import { generateEntryId, parseMarkdownFrontmatter, parseMarkdownFrontmatter as parseFrontmatter, serializeMarkdownFrontmatter, serializeMarkdownFrontmatter as serializeFrontmatter, validateEntryId as validateEntryId$1, validateLocale as validateLocale$1, validateSlug as validateSlug$1 } from "@contentrain/types";
|
|
6
6
|
import { access, readFile, readdir, rm } from "node:fs/promises";
|
|
@@ -69,16 +69,6 @@ function resolveMdFilePath(dir, model, locale, slug) {
|
|
|
69
69
|
default: return join(dir, slug, `${locale}.md`);
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
|
-
function defaultMeta(data) {
|
|
73
|
-
const meta = {
|
|
74
|
-
status: "draft",
|
|
75
|
-
source: "agent",
|
|
76
|
-
updated_by: "contentrain-mcp"
|
|
77
|
-
};
|
|
78
|
-
if (data?.["publish_at"] !== void 0) meta.publish_at = data["publish_at"];
|
|
79
|
-
if (data?.["expire_at"] !== void 0) meta.expire_at = data["expire_at"];
|
|
80
|
-
return meta;
|
|
81
|
-
}
|
|
82
72
|
async function writeContent(projectRoot, model, entries, config, vocabulary) {
|
|
83
73
|
const results = [];
|
|
84
74
|
const defaultLocale = config.locales.default;
|
|
@@ -95,14 +85,22 @@ async function writeContent(projectRoot, model, entries, config, vocabulary) {
|
|
|
95
85
|
if (slugErr) throw new Error(slugErr);
|
|
96
86
|
}
|
|
97
87
|
switch (model.kind) {
|
|
98
|
-
case "singleton":
|
|
88
|
+
case "singleton": {
|
|
99
89
|
await writeJson(resolveJsonFilePath(resolveContentDir(projectRoot, model), model, locale), entry.data);
|
|
100
|
-
await
|
|
90
|
+
const prevMeta = await readMeta(projectRoot, model, {
|
|
91
|
+
locale,
|
|
92
|
+
defaultLocale
|
|
93
|
+
});
|
|
94
|
+
await writeMeta(projectRoot, model, {
|
|
95
|
+
locale,
|
|
96
|
+
defaultLocale
|
|
97
|
+
}, mergeEntryMeta(prevMeta ?? void 0, entry.data));
|
|
101
98
|
results.push({
|
|
102
99
|
action: "updated",
|
|
103
100
|
locale
|
|
104
101
|
});
|
|
105
102
|
break;
|
|
103
|
+
}
|
|
106
104
|
case "collection": {
|
|
107
105
|
const isNew = !entry.id;
|
|
108
106
|
const id = entry.id ?? generateEntryId();
|
|
@@ -113,10 +111,15 @@ async function writeContent(projectRoot, model, entries, config, vocabulary) {
|
|
|
113
111
|
const sorted = {};
|
|
114
112
|
for (const key of Object.keys(existing).toSorted()) sorted[key] = existing[key];
|
|
115
113
|
await writeJson(filePath, sorted);
|
|
114
|
+
const prevMetaMap = await readMeta(projectRoot, model, {
|
|
115
|
+
locale,
|
|
116
|
+
defaultLocale
|
|
117
|
+
});
|
|
116
118
|
await writeMeta(projectRoot, model, {
|
|
117
119
|
locale,
|
|
118
|
-
entryId: id
|
|
119
|
-
|
|
120
|
+
entryId: id,
|
|
121
|
+
defaultLocale
|
|
122
|
+
}, mergeEntryMeta(prevMetaMap?.[id], entry.data));
|
|
120
123
|
results.push({
|
|
121
124
|
action,
|
|
122
125
|
id,
|
|
@@ -136,10 +139,16 @@ async function writeContent(projectRoot, model, entries, config, vocabulary) {
|
|
|
136
139
|
const docPath = resolveMdFilePath(resolveContentDir(projectRoot, model), model, locale, slug);
|
|
137
140
|
const action = await readText(docPath) ? "updated" : "created";
|
|
138
141
|
await writeText(docPath, serializeMarkdownFrontmatter(fmData, bodyContent));
|
|
142
|
+
const prevMeta = await readMeta(projectRoot, model, {
|
|
143
|
+
locale,
|
|
144
|
+
slug,
|
|
145
|
+
defaultLocale
|
|
146
|
+
});
|
|
139
147
|
await writeMeta(projectRoot, model, {
|
|
140
148
|
locale,
|
|
141
|
-
slug
|
|
142
|
-
|
|
149
|
+
slug,
|
|
150
|
+
defaultLocale
|
|
151
|
+
}, mergeEntryMeta(prevMeta ?? void 0, entry.data));
|
|
143
152
|
results.push({
|
|
144
153
|
action,
|
|
145
154
|
slug,
|
|
@@ -172,7 +181,14 @@ async function writeContent(projectRoot, model, entries, config, vocabulary) {
|
|
|
172
181
|
...existing,
|
|
173
182
|
...entry.data
|
|
174
183
|
});
|
|
175
|
-
await
|
|
184
|
+
const prevMeta = await readMeta(projectRoot, model, {
|
|
185
|
+
locale,
|
|
186
|
+
defaultLocale
|
|
187
|
+
});
|
|
188
|
+
await writeMeta(projectRoot, model, {
|
|
189
|
+
locale,
|
|
190
|
+
defaultLocale
|
|
191
|
+
}, mergeEntryMeta(prevMeta ?? void 0, entry.data));
|
|
176
192
|
results.push({
|
|
177
193
|
action: "updated",
|
|
178
194
|
locale,
|
|
@@ -202,7 +218,8 @@ async function deleteContent(projectRoot, model, opts) {
|
|
|
202
218
|
}
|
|
203
219
|
for (const loc of locales) await deleteMeta(projectRoot, model, {
|
|
204
220
|
locale: loc,
|
|
205
|
-
entryId: opts.id
|
|
221
|
+
entryId: opts.id,
|
|
222
|
+
defaultLocale: opts.defaultLocale
|
|
206
223
|
});
|
|
207
224
|
break;
|
|
208
225
|
}
|
|
@@ -228,7 +245,8 @@ async function deleteContent(projectRoot, model, opts) {
|
|
|
228
245
|
removed.push(`${model.content_path ?? `content/${model.domain}/${model.id}`}/${opts.slug}`);
|
|
229
246
|
await deleteMeta(projectRoot, model, {
|
|
230
247
|
slug: opts.slug,
|
|
231
|
-
locale: opts.locale
|
|
248
|
+
locale: opts.locale,
|
|
249
|
+
defaultLocale: opts.defaultLocale
|
|
232
250
|
});
|
|
233
251
|
break;
|
|
234
252
|
}
|
|
@@ -237,7 +255,10 @@ async function deleteContent(projectRoot, model, opts) {
|
|
|
237
255
|
const locale = opts.locale ?? "data";
|
|
238
256
|
await rm(resolveJsonFilePath(cDir, model, locale), { force: true });
|
|
239
257
|
removed.push(model.i18n ? `content/${model.domain}/${model.id}/${locale}.json` : `content/${model.domain}/${model.id}/data.json`);
|
|
240
|
-
await deleteMeta(projectRoot, model, {
|
|
258
|
+
await deleteMeta(projectRoot, model, {
|
|
259
|
+
locale: model.i18n ? locale : void 0,
|
|
260
|
+
defaultLocale: opts.defaultLocale
|
|
261
|
+
});
|
|
241
262
|
break;
|
|
242
263
|
}
|
|
243
264
|
case "dictionary": {
|
|
@@ -255,7 +276,10 @@ async function deleteContent(projectRoot, model, opts) {
|
|
|
255
276
|
} else {
|
|
256
277
|
await rm(filePath, { force: true });
|
|
257
278
|
removed.push(model.i18n ? `content/${model.domain}/${model.id}/${locale}.json` : `content/${model.domain}/${model.id}/data.json`);
|
|
258
|
-
await deleteMeta(projectRoot, model, {
|
|
279
|
+
await deleteMeta(projectRoot, model, {
|
|
280
|
+
locale: model.i18n ? locale : void 0,
|
|
281
|
+
defaultLocale: opts.defaultLocale
|
|
282
|
+
});
|
|
259
283
|
}
|
|
260
284
|
break;
|
|
261
285
|
}
|
|
@@ -1035,4 +1059,4 @@ function validateModelDefinition(input) {
|
|
|
1035
1059
|
//#endregion
|
|
1036
1060
|
export { LocalReader as C, writeContent as S, resolveMdFilePath as _, fieldDefZodSchema as a, validateLocale$1 as b, validateModelDefinition as c, listContent as d, parseFrontmatter as f, resolveLocaleStrategy as g, resolveJsonFilePath as h, deleteModel as i, writeModel as l, resolveContentDir as m, checkReferences as n, listModels as o, readContent as p, countEntries as r, readModel as s, FIELD_TYPE_ENUM as t, deleteContent as u, serializeFrontmatter as v, validateSlug$1 as x, validateEntryId$1 as y };
|
|
1037
1061
|
|
|
1038
|
-
//# sourceMappingURL=model-manager-
|
|
1062
|
+
//# sourceMappingURL=model-manager-SiesTJrS.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"model-manager-SiesTJrS.mjs","names":["validateLocale","validateEntryId","validateSlug","tryReadJsonViaReader"],"sources":["../src/providers/local/reader.ts","../src/core/content-manager.ts","../src/core/model-manager.ts"],"sourcesContent":["import { access, readdir, readFile } from 'node:fs/promises'\nimport { resolve } from 'node:path'\nimport type { RepoReader } from '../../core/contracts/index.js'\n\n/**\n * LocalReader — `RepoReader` backed by the local filesystem.\n *\n * Paths may be absolute or relative to `projectRoot` (`node:path/resolve`\n * handles both). The `ref` parameter is accepted for interface compatibility\n * but ignored because LocalReader always reads from the working tree.\n *\n * Phase 1: provided as plumbing; core ops still use direct `fs` calls.\n * Phase 2 routes ops through this reader (and through GitHubProvider's reader\n * in Phase 5) so the same op surface works on every backing store.\n */\nexport class LocalReader implements RepoReader {\n constructor(public readonly projectRoot: string) {}\n\n async readFile(path: string, _ref?: string): Promise<string> {\n return readFile(resolve(this.projectRoot, path), 'utf-8')\n }\n\n async listDirectory(path: string, _ref?: string): Promise<string[]> {\n try {\n return await readdir(resolve(this.projectRoot, path))\n } catch {\n return []\n }\n }\n\n async fileExists(path: string, _ref?: string): Promise<boolean> {\n try {\n await access(resolve(this.projectRoot, path))\n return true\n } catch {\n return false\n }\n }\n}\n","import type { ModelDefinition, ContentrainConfig, EntryMeta, LocaleStrategy, DocumentEntry, Vocabulary } from '@contentrain/types'\nimport { validateSlug, validateEntryId, validateLocale, generateEntryId, parseMarkdownFrontmatter, serializeMarkdownFrontmatter } from '@contentrain/types'\nimport { join } from 'node:path'\nimport { rm } from 'node:fs/promises'\nimport { contentrainDir, readDir, readJson, readText, writeJson, writeText } from '../util/fs.js'\nimport { readMeta, writeMeta, deleteMeta, mergeEntryMeta } from './meta-manager.js'\nimport { readModel } from './model-manager.js'\nimport type { RepoReader } from './contracts/index.js'\nimport { contentDirPath, contentFilePath, documentFilePath } from './ops/paths.js'\n\n// Re-export for backward compatibility (MCP internal consumers)\nexport { validateSlug, validateEntryId, validateLocale }\nexport { parseMarkdownFrontmatter as parseFrontmatter, serializeMarkdownFrontmatter as serializeFrontmatter } from '@contentrain/types'\n\n// ─── Content paths ───\n\nexport function resolveContentDir(projectRoot: string, model: ModelDefinition): string {\n if (model.content_path) {\n return join(projectRoot, model.content_path)\n }\n return join(contentrainDir(projectRoot), 'content', model.domain, model.id)\n}\n\nexport function resolveLocaleStrategy(model: ModelDefinition): LocaleStrategy {\n return model.locale_strategy ?? 'file'\n}\n\n/** Build the file path for a JSON content file (singleton/collection/dictionary) */\nexport function resolveJsonFilePath(dir: string, model: ModelDefinition, locale: string): string {\n // When i18n is disabled, always use data.json (locale parameter is ignored)\n if (!model.i18n) return join(dir, 'data.json')\n\n switch (resolveLocaleStrategy(model)) {\n case 'suffix': return join(dir, `${model.id}.${locale}.json`)\n case 'directory': return join(dir, locale, `${model.id}.json`)\n case 'none': return join(dir, `${model.id}.json`)\n case 'file':\n default: return join(dir, `${locale}.json`)\n }\n}\n\n/** Build the file path for a markdown document */\nexport function resolveMdFilePath(dir: string, model: ModelDefinition, locale: string, slug: string): string {\n // When i18n is disabled, always use {slug}.md (locale parameter is ignored)\n if (!model.i18n) return join(dir, `${slug}.md`)\n\n switch (resolveLocaleStrategy(model)) {\n case 'suffix': return join(dir, `${slug}.${locale}.md`)\n case 'directory': return join(dir, locale, `${slug}.md`)\n case 'none': return join(dir, `${slug}.md`)\n case 'file':\n default: return join(dir, slug, `${locale}.md`)\n }\n}\n\n// ─── Types ───\n\nexport interface ContentEntry {\n id?: string\n slug?: string\n locale?: string\n data: Record<string, unknown>\n}\n\nexport interface WriteResult {\n action: 'created' | 'updated'\n id?: string\n slug?: string\n locale: string\n advisories?: string[]\n}\n\nexport interface DeleteOpts {\n id?: string\n slug?: string\n locale?: string\n keys?: string[]\n /** `config.locales.default` — places a non-i18n model's single meta record. */\n defaultLocale: string\n}\n\nexport interface ListOpts {\n locale?: string\n filter?: Record<string, unknown>\n resolve?: boolean\n limit?: number\n offset?: number\n}\n\n// ─── writeContent ───\n\nexport async function writeContent(\n projectRoot: string,\n model: ModelDefinition,\n entries: ContentEntry[],\n config: ContentrainConfig,\n vocabulary?: Vocabulary | null,\n): Promise<WriteResult[]> {\n const results: WriteResult[] = []\n const defaultLocale = config.locales.default\n\n for (const entry of entries) {\n const locale = entry.locale ?? defaultLocale\n\n // Validate locale\n const localeErr = validateLocale(locale, config)\n if (localeErr) throw new Error(localeErr)\n\n // Validate entry ID if provided\n if (entry.id) {\n const idErr = validateEntryId(entry.id)\n if (idErr) throw new Error(idErr)\n }\n\n // Validate slug if provided\n if (entry.slug) {\n const slugErr = validateSlug(entry.slug)\n if (slugErr) throw new Error(slugErr)\n }\n\n switch (model.kind) {\n case 'singleton': {\n const filePath = resolveJsonFilePath(resolveContentDir(projectRoot, model), model, locale)\n await writeJson(filePath, entry.data)\n const prevMeta = await readMeta(projectRoot, model, { locale, defaultLocale }) as EntryMeta | null\n await writeMeta(projectRoot, model, { locale, defaultLocale }, mergeEntryMeta(prevMeta ?? undefined, entry.data))\n results.push({ action: 'updated', locale })\n break\n }\n\n case 'collection': {\n const isNew = !entry.id\n const id = entry.id ?? generateEntryId()\n const filePath = resolveJsonFilePath(resolveContentDir(projectRoot, model), model, locale)\n const existing = await readJson<Record<string, Record<string, unknown>>>(filePath) ?? {}\n\n const action: 'created' | 'updated' = (isNew || !(id in existing)) ? 'created' : 'updated'\n existing[id] = entry.data\n\n // Sort by entry ID for canonical output\n const sorted: Record<string, Record<string, unknown>> = {}\n for (const key of Object.keys(existing).toSorted()) {\n sorted[key] = existing[key]!\n }\n\n await writeJson(filePath, sorted)\n const prevMetaMap = await readMeta(projectRoot, model, { locale, defaultLocale }) as Record<string, EntryMeta> | null\n await writeMeta(projectRoot, model, { locale, entryId: id, defaultLocale }, mergeEntryMeta(prevMetaMap?.[id], entry.data))\n results.push({ action, id, locale })\n break\n }\n\n case 'document': {\n const slug = entry.slug ?? entry.data['slug'] as string\n if (!slug) throw new Error('Document entries require a slug')\n const slugErr = validateSlug(slug)\n if (slugErr) throw new Error(slugErr)\n\n const bodyContent = (entry.data['body'] as string) ?? ''\n const fmData = { ...entry.data }\n delete fmData['body']\n if (!fmData['slug']) fmData['slug'] = slug\n\n // Check if existing\n const docPath = resolveMdFilePath(resolveContentDir(projectRoot, model), model, locale, slug)\n const existingRaw = await readText(docPath)\n const action: 'created' | 'updated' = existingRaw ? 'updated' : 'created'\n\n const mdContent = serializeMarkdownFrontmatter(fmData, bodyContent)\n await writeText(docPath, mdContent)\n const prevMeta = await readMeta(projectRoot, model, { locale, slug, defaultLocale }) as EntryMeta | null\n await writeMeta(projectRoot, model, { locale, slug, defaultLocale }, mergeEntryMeta(prevMeta ?? undefined, entry.data))\n results.push({ action, slug, locale })\n break\n }\n\n case 'dictionary': {\n const filePath = resolveJsonFilePath(resolveContentDir(projectRoot, model), model, locale)\n const existing = await readJson<Record<string, string>>(filePath) ?? {}\n const collisions: string[] = []\n for (const key of Object.keys(entry.data as Record<string, string>)) {\n if (key in existing && existing[key] !== (entry.data as Record<string, string>)[key]) {\n collisions.push(key)\n }\n }\n if (collisions.length > 0) {\n throw new Error(\n `Dictionary \"${model.id}\" (${locale}): ${collisions.length} key collision(s) — [${collisions.join(', ')}] already exist with different values. ` +\n `Read existing keys with contentrain_content_list first, or include all keys in a single save call.`,\n )\n }\n\n // Duplicate value advisory: warn when a new key maps to a value that already exists\n const advisories: string[] = []\n const reverseMap = new Map<string, string>()\n for (const [k, v] of Object.entries(existing)) {\n reverseMap.set(v, k)\n }\n for (const [newKey, newValue] of Object.entries(entry.data as Record<string, string>)) {\n if (newKey in existing) continue\n const existingKey = reverseMap.get(newValue as string)\n if (existingKey && existingKey !== newKey) {\n advisories.push(\n `Value \"${newValue}\" already exists as key \"${existingKey}\". Consider reusing instead of creating \"${newKey}\".`,\n )\n }\n }\n\n // Vocabulary cross-reference: warn when a value matches a known vocabulary term\n if (vocabulary && Object.keys(vocabulary.terms).length > 0) {\n for (const [newKey, newValue] of Object.entries(entry.data as Record<string, string>)) {\n if (newKey in existing) continue\n for (const [, translations] of Object.entries(vocabulary.terms)) {\n if (Object.values(translations).includes(newValue as string)) {\n advisories.push(\n `Value \"${newValue}\" matches a vocabulary term. Use the canonical form for consistency.`,\n )\n break\n }\n }\n }\n }\n\n const merged = { ...existing, ...entry.data as Record<string, string> }\n await writeJson(filePath, merged)\n const prevMeta = await readMeta(projectRoot, model, { locale, defaultLocale }) as EntryMeta | null\n await writeMeta(projectRoot, model, { locale, defaultLocale }, mergeEntryMeta(prevMeta ?? undefined, entry.data))\n results.push({ action: 'updated', locale, ...(advisories.length > 0 ? { advisories } : {}) })\n break\n }\n }\n }\n\n return results\n}\n\n// ─── deleteContent ───\n\nexport async function deleteContent(\n projectRoot: string,\n model: ModelDefinition,\n opts: DeleteOpts,\n): Promise<string[]> {\n const removed: string[] = []\n const cDir = resolveContentDir(projectRoot, model)\n\n switch (model.kind) {\n case 'collection': {\n if (!opts.id) throw new Error('Collection delete requires an entry ID')\n const locales = opts.locale\n ? [opts.locale]\n : (await readDir(cDir)).filter(f => f.endsWith('.json')).map(f => f.replace('.json', '').replace(`${model.id}.`, ''))\n\n for (const loc of locales) {\n const filePath = resolveJsonFilePath(cDir, model, loc)\n const data = await readJson<Record<string, unknown>>(filePath)\n if (data && opts.id in data) {\n delete data[opts.id]\n await writeJson(filePath, data)\n removed.push(`content/${model.domain}/${model.id}/${loc}.json#${opts.id}`)\n }\n }\n\n for (const loc of locales) {\n await deleteMeta(projectRoot, model, { locale: loc, entryId: opts.id, defaultLocale: opts.defaultLocale })\n }\n break\n }\n\n case 'document': {\n if (!opts.slug) throw new Error('Document delete requires a slug')\n const slugDelErr = validateSlug(opts.slug)\n if (slugDelErr) throw new Error(slugDelErr)\n\n const strategy = resolveLocaleStrategy(model)\n if (!model.i18n) {\n // No i18n: single {slug}.md file\n await rm(join(cDir, `${opts.slug}.md`), { force: true })\n } else if (strategy === 'file') {\n // slug is a directory — remove entire slug directory\n await rm(join(cDir, opts.slug), { recursive: true, force: true })\n } else if (opts.locale) {\n // Remove specific locale file\n const filePath = resolveMdFilePath(cDir, model, opts.locale, opts.slug)\n await rm(filePath, { force: true })\n } else {\n // Remove all locale files for this slug\n const files = await readDir(strategy === 'directory' ? cDir : cDir)\n for (const f of files) {\n if (strategy === 'suffix' && f.startsWith(`${opts.slug}.`) && f.endsWith('.md')) {\n await rm(join(cDir, f), { force: true })\n } else if (strategy === 'directory') {\n // f is a locale dir — remove slug.md from it\n await rm(join(cDir, f, `${opts.slug}.md`), { force: true })\n } else if (strategy === 'none') {\n if (f === `${opts.slug}.md`) await rm(join(cDir, f), { force: true })\n }\n }\n }\n removed.push(`${model.content_path ?? `content/${model.domain}/${model.id}`}/${opts.slug}`)\n\n await deleteMeta(projectRoot, model, { slug: opts.slug, locale: opts.locale, defaultLocale: opts.defaultLocale })\n break\n }\n\n case 'singleton': {\n if (model.i18n && !opts.locale) throw new Error('Singleton delete requires a locale when i18n is enabled')\n const locale = opts.locale ?? 'data'\n const filePath = resolveJsonFilePath(cDir, model, locale)\n await rm(filePath, { force: true })\n removed.push(model.i18n\n ? `content/${model.domain}/${model.id}/${locale}.json`\n : `content/${model.domain}/${model.id}/data.json`)\n await deleteMeta(projectRoot, model, { locale: model.i18n ? locale : undefined, defaultLocale: opts.defaultLocale })\n break\n }\n\n case 'dictionary': {\n if (model.i18n && !opts.locale) throw new Error('Dictionary delete requires a locale when i18n is enabled')\n const locale = opts.locale ?? 'data'\n const filePath = resolveJsonFilePath(cDir, model, locale)\n\n if (opts.keys?.length) {\n // Delete specific keys from dictionary\n const existing = await readJson<Record<string, string>>(filePath) ?? {}\n const notFound: string[] = []\n for (const key of opts.keys) {\n if (key in existing) {\n delete existing[key]\n } else {\n notFound.push(key)\n }\n }\n if (notFound.length > 0) {\n throw new Error(`Dictionary \"${model.id}\" (${locale}): keys not found — [${notFound.join(', ')}]`)\n }\n await writeJson(filePath, existing)\n removed.push(...opts.keys.map(k => `${model.id}/${locale}:${k}`))\n } else {\n // Delete entire locale file\n await rm(filePath, { force: true })\n removed.push(model.i18n\n ? `content/${model.domain}/${model.id}/${locale}.json`\n : `content/${model.domain}/${model.id}/data.json`)\n await deleteMeta(projectRoot, model, { locale: model.i18n ? locale : undefined, defaultLocale: opts.defaultLocale })\n }\n break\n }\n }\n\n return removed\n}\n\n// ─── listContent ───\n\n/**\n * List content entries for a model. Dual signature:\n *\n * - `listContent(projectRoot, model, opts, config)` — legacy local flow,\n * uses direct filesystem reads and supports `opts.resolve` for\n * cross-model relation hydration.\n * - `listContent(reader, model, opts, config)` — reader-backed flow for\n * remote providers. Basic list works across all four model kinds.\n * `opts.resolve: true` is rejected with an error on remote readers\n * because the cross-model walk requires local filesystem access.\n *\n * The reader-based function lives in {@link listContentViaReader}; the\n * projectRoot-based entry point continues to call the legacy body to\n * preserve bit-for-bit behaviour for every existing caller.\n */\nexport function listContent(\n projectRoot: string,\n model: ModelDefinition,\n opts: ListOpts,\n config: ContentrainConfig,\n): Promise<unknown>\nexport function listContent(\n reader: RepoReader,\n model: ModelDefinition,\n opts: ListOpts,\n config: ContentrainConfig,\n): Promise<unknown>\nexport async function listContent(\n input: string | RepoReader,\n model: ModelDefinition,\n opts: ListOpts,\n config: ContentrainConfig,\n): Promise<unknown> {\n if (typeof input !== 'string') {\n return listContentViaReader(input, model, opts, config)\n }\n return listContentLocal(input, model, opts, config)\n}\n\nasync function listContentLocal(\n projectRoot: string,\n model: ModelDefinition,\n opts: ListOpts,\n config: ContentrainConfig,\n): Promise<unknown> {\n const cDir = resolveContentDir(projectRoot, model)\n const locale = opts.locale ?? config.locales.default\n\n switch (model.kind) {\n case 'singleton': {\n const data = await readJson<Record<string, unknown>>(resolveJsonFilePath(cDir, model, locale))\n return { kind: 'singleton', data: data ?? {}, locale }\n }\n\n case 'collection': {\n const data = await readJson<Record<string, Record<string, unknown>>>(resolveJsonFilePath(cDir, model, locale)) ?? {}\n let entries: Array<Record<string, unknown>> = Object.entries(data).map(([id, fields]) => {\n const entry: Record<string, unknown> = { id }\n Object.assign(entry, fields)\n return entry\n })\n\n // Filter\n if (opts.filter) {\n entries = entries.filter(entry => {\n for (const [key, value] of Object.entries(opts.filter!)) {\n if (entry[key] !== value) return false\n }\n return true\n })\n }\n\n const total = entries.length\n\n // Pagination\n const offset = opts.offset ?? 0\n const limit = opts.limit ?? entries.length\n entries = entries.slice(offset, offset + limit)\n\n // Resolve relations\n if (opts.resolve && model.fields) {\n entries = await resolveRelations(projectRoot, model, entries, locale)\n }\n\n return { kind: 'collection', data: entries, total, locale, offset, limit }\n }\n\n case 'document': {\n const entries: DocumentEntry[] = []\n const strategy = resolveLocaleStrategy(model)\n\n if (!model.i18n) {\n // No i18n: flat {slug}.md files, no locale in path\n const files = await readDir(cDir)\n for (const f of files) {\n if (!f.endsWith('.md')) continue\n const slug = f.replace('.md', '')\n const raw = await readText(join(cDir, f))\n if (!raw) continue\n const { frontmatter, body } = parseMarkdownFrontmatter(raw)\n entries.push({ slug, frontmatter, body })\n }\n } else if (strategy === 'file') {\n // Each slug is a subdirectory containing locale.md files\n const slugDirs = await readDir(cDir)\n for (const slug of slugDirs) {\n const raw = await readText(join(cDir, slug, `${locale}.md`))\n if (!raw) continue\n const { frontmatter, body } = parseMarkdownFrontmatter(raw)\n entries.push({ slug, frontmatter, body })\n }\n } else if (strategy === 'suffix') {\n // Files like {slug}.{locale}.md in flat directory\n const files = await readDir(cDir)\n const suffix = `.${locale}.md`\n for (const f of files) {\n if (!f.endsWith(suffix)) continue\n const slug = f.slice(0, -suffix.length)\n const raw = await readText(join(cDir, f))\n if (!raw) continue\n const { frontmatter, body } = parseMarkdownFrontmatter(raw)\n entries.push({ slug, frontmatter, body })\n }\n } else if (strategy === 'directory') {\n // Files in {locale}/ directory as {slug}.md\n const localeDir = join(cDir, locale)\n const files = await readDir(localeDir)\n for (const f of files) {\n if (!f.endsWith('.md')) continue\n const slug = f.replace('.md', '')\n const raw = await readText(join(localeDir, f))\n if (!raw) continue\n const { frontmatter, body } = parseMarkdownFrontmatter(raw)\n entries.push({ slug, frontmatter, body })\n }\n } else {\n // none — {slug}.md, no locale in filename\n const files = await readDir(cDir)\n for (const f of files) {\n if (!f.endsWith('.md')) continue\n const slug = f.replace('.md', '')\n const raw = await readText(join(cDir, f))\n if (!raw) continue\n const { frontmatter, body } = parseMarkdownFrontmatter(raw)\n entries.push({ slug, frontmatter, body })\n }\n }\n\n const total = entries.length\n const offset = opts.offset ?? 0\n const limit = opts.limit ?? entries.length\n const paged = entries.slice(offset, offset + limit)\n\n return { kind: 'document', data: paged, total, locale, offset, limit }\n }\n\n case 'dictionary': {\n const data = await readJson<Record<string, string>>(resolveJsonFilePath(cDir, model, locale)) ?? {}\n return { kind: 'dictionary', data, total_keys: Object.keys(data).length, locale }\n }\n }\n}\n\nasync function tryReadJsonViaReader<T>(reader: RepoReader, path: string): Promise<T | null> {\n try {\n return JSON.parse(await reader.readFile(path)) as T\n } catch {\n return null\n }\n}\n\nasync function tryReadTextViaReader(reader: RepoReader, path: string): Promise<string | null> {\n try {\n return await reader.readFile(path)\n } catch {\n return null\n }\n}\n\nasync function listContentViaReader(\n reader: RepoReader,\n model: ModelDefinition,\n opts: ListOpts,\n config: ContentrainConfig,\n): Promise<unknown> {\n if (opts.resolve) {\n throw new Error(\n 'contentrain_content_list with resolve:true requires local filesystem access. '\n + 'Use a LocalProvider (stdio or HTTP+LocalProvider) or omit resolve:true.',\n )\n }\n\n const cDir = contentDirPath(model)\n const locale = opts.locale ?? config.locales.default\n\n switch (model.kind) {\n case 'singleton': {\n const data = await tryReadJsonViaReader<Record<string, unknown>>(reader, contentFilePath(model, locale))\n return { kind: 'singleton', data: data ?? {}, locale }\n }\n\n case 'collection': {\n const data = await tryReadJsonViaReader<Record<string, Record<string, unknown>>>(\n reader,\n contentFilePath(model, locale),\n ) ?? {}\n let entries: Array<Record<string, unknown>> = Object.entries(data).map(([id, fields]) => {\n const entry: Record<string, unknown> = { id }\n Object.assign(entry, fields)\n return entry\n })\n\n if (opts.filter) {\n entries = entries.filter(entry => {\n for (const [key, value] of Object.entries(opts.filter!)) {\n if (entry[key] !== value) return false\n }\n return true\n })\n }\n\n const total = entries.length\n const offset = opts.offset ?? 0\n const limit = opts.limit ?? entries.length\n entries = entries.slice(offset, offset + limit)\n\n return { kind: 'collection', data: entries, total, locale, offset, limit }\n }\n\n case 'document': {\n const entries: DocumentEntry[] = []\n const strategy = resolveLocaleStrategy(model)\n\n const collectEntry = async (relPath: string, slug: string): Promise<void> => {\n const raw = await tryReadTextViaReader(reader, relPath)\n if (!raw) return\n const { frontmatter, body } = parseMarkdownFrontmatter(raw)\n entries.push({ slug, frontmatter, body })\n }\n\n if (!model.i18n) {\n const files = await reader.listDirectory(cDir)\n for (const f of files) {\n if (!f.endsWith('.md')) continue\n await collectEntry(documentFilePath(model, locale, f.replace(/\\.md$/u, '')), f.replace(/\\.md$/u, ''))\n }\n } else if (strategy === 'file') {\n const slugDirs = await reader.listDirectory(cDir)\n for (const slug of slugDirs) {\n await collectEntry(documentFilePath(model, locale, slug), slug)\n }\n } else if (strategy === 'suffix') {\n const files = await reader.listDirectory(cDir)\n const suffix = `.${locale}.md`\n for (const f of files) {\n if (!f.endsWith(suffix)) continue\n const slug = f.slice(0, -suffix.length)\n await collectEntry(documentFilePath(model, locale, slug), slug)\n }\n } else if (strategy === 'directory') {\n const files = await reader.listDirectory(`${cDir}/${locale}`)\n for (const f of files) {\n if (!f.endsWith('.md')) continue\n const slug = f.replace(/\\.md$/u, '')\n await collectEntry(documentFilePath(model, locale, slug), slug)\n }\n } else {\n const files = await reader.listDirectory(cDir)\n for (const f of files) {\n if (!f.endsWith('.md')) continue\n const slug = f.replace(/\\.md$/u, '')\n await collectEntry(documentFilePath(model, locale, slug), slug)\n }\n }\n\n const total = entries.length\n const offset = opts.offset ?? 0\n const limit = opts.limit ?? entries.length\n const paged = entries.slice(offset, offset + limit)\n\n return { kind: 'document', data: paged, total, locale, offset, limit }\n }\n\n case 'dictionary': {\n const data = await tryReadJsonViaReader<Record<string, string>>(reader, contentFilePath(model, locale)) ?? {}\n return { kind: 'dictionary', data, total_keys: Object.keys(data).length, locale }\n }\n }\n}\n\n// ─── readContent (for external use / relation resolving) ───\n\nexport async function readContent(\n projectRoot: string,\n model: ModelDefinition,\n opts: { locale: string; entryId?: string; slug?: string },\n): Promise<unknown> {\n const cDir = resolveContentDir(projectRoot, model)\n\n switch (model.kind) {\n case 'singleton':\n return readJson<Record<string, unknown>>(resolveJsonFilePath(cDir, model, opts.locale))\n\n case 'collection': {\n if (!opts.entryId) return null\n const data = await readJson<Record<string, Record<string, unknown>>>(resolveJsonFilePath(cDir, model, opts.locale))\n return data?.[opts.entryId] ? { id: opts.entryId, ...data[opts.entryId] } : null\n }\n\n case 'document': {\n if (!opts.slug) return null\n const raw = await readText(resolveMdFilePath(cDir, model, opts.locale, opts.slug))\n if (!raw) return null\n const { frontmatter, body } = parseMarkdownFrontmatter(raw)\n return { slug: opts.slug, ...frontmatter, body }\n }\n\n case 'dictionary':\n return readJson<Record<string, string>>(resolveJsonFilePath(cDir, model, opts.locale))\n }\n}\n\n// ─── Relation resolving ───\n\nasync function resolveRelations(\n projectRoot: string,\n model: ModelDefinition,\n entries: Array<Record<string, unknown>>,\n locale: string,\n): Promise<Array<Record<string, unknown>>> {\n if (!model.fields) return entries\n\n const relationFields: Array<{ name: string; targetModels: string[]; multi: boolean }> = []\n for (const [name, field] of Object.entries(model.fields)) {\n if (field.type === 'relation' || field.type === 'relations') {\n const targets = Array.isArray(field.model) ? field.model : field.model ? [field.model] : []\n relationFields.push({ name, targetModels: targets, multi: field.type === 'relations' })\n }\n }\n\n if (relationFields.length === 0) return entries\n\n // Load target model contents into cache (with circular reference protection)\n const targetCache: Record<string, Record<string, Record<string, unknown>>> = {}\n const visited = new Set<string>([model.id])\n\n for (const rf of relationFields) {\n for (const targetModelId of rf.targetModels) {\n if (targetCache[targetModelId] || visited.has(targetModelId)) continue\n visited.add(targetModelId)\n\n const targetModel = await readModel(projectRoot, targetModelId)\n if (!targetModel) continue\n\n if (targetModel.kind === 'collection') {\n const targetData = await readJson<Record<string, Record<string, unknown>>>(\n resolveJsonFilePath(resolveContentDir(projectRoot, targetModel), targetModel, locale),\n ) ?? {}\n targetCache[targetModelId] = targetData\n } else if (targetModel.kind === 'document') {\n // Build slug-keyed cache by listing document entries for this locale\n const docCache: Record<string, Record<string, unknown>> = {}\n const cDir = resolveContentDir(projectRoot, targetModel)\n const strategy = resolveLocaleStrategy(targetModel)\n\n if (!targetModel.i18n) {\n const files = await readDir(cDir)\n for (const f of files) {\n if (!f.endsWith('.md')) continue\n const slug = f.replace('.md', '')\n const raw = await readText(join(cDir, f))\n if (!raw) continue\n const { frontmatter, body } = parseMarkdownFrontmatter(raw)\n docCache[slug] = { slug, ...frontmatter, body }\n }\n } else if (strategy === 'file') {\n const slugDirs = await readDir(cDir)\n for (const slug of slugDirs) {\n const raw = await readText(join(cDir, slug, `${locale}.md`))\n if (!raw) continue\n const { frontmatter, body } = parseMarkdownFrontmatter(raw)\n docCache[slug] = { slug, ...frontmatter, body }\n }\n } else if (strategy === 'suffix') {\n const files = await readDir(cDir)\n const suffix = `.${locale}.md`\n for (const f of files) {\n if (!f.endsWith(suffix)) continue\n const slug = f.slice(0, -suffix.length)\n const raw = await readText(join(cDir, f))\n if (!raw) continue\n const { frontmatter, body } = parseMarkdownFrontmatter(raw)\n docCache[slug] = { slug, ...frontmatter, body }\n }\n } else if (strategy === 'directory') {\n const localeDir = join(cDir, locale)\n const files = await readDir(localeDir)\n for (const f of files) {\n if (!f.endsWith('.md')) continue\n const slug = f.replace('.md', '')\n const raw = await readText(join(localeDir, f))\n if (!raw) continue\n const { frontmatter, body } = parseMarkdownFrontmatter(raw)\n docCache[slug] = { slug, ...frontmatter, body }\n }\n } else {\n // 'none' strategy\n const files = await readDir(cDir)\n for (const f of files) {\n if (!f.endsWith('.md')) continue\n const slug = f.replace('.md', '')\n const raw = await readText(join(cDir, f))\n if (!raw) continue\n const { frontmatter, body } = parseMarkdownFrontmatter(raw)\n docCache[slug] = { slug, ...frontmatter, body }\n }\n }\n\n targetCache[targetModelId] = docCache\n }\n }\n }\n\n return entries.map(entry => {\n const resolved = { ...entry }\n for (const rf of relationFields) {\n const value = resolved[rf.name]\n if (!value) continue\n\n if (rf.multi && Array.isArray(value)) {\n resolved[rf.name] = value.map(id => {\n for (const targetModelId of rf.targetModels) {\n const cached = targetCache[targetModelId]?.[id as string]\n if (cached) return { id, ...cached }\n }\n return id\n })\n } else if (typeof value === 'string') {\n for (const targetModelId of rf.targetModels) {\n const cached = targetCache[targetModelId]?.[value]\n if (cached) {\n resolved[rf.name] = { id: value, ...cached }\n break\n }\n }\n }\n }\n return resolved\n })\n}\n","import type { ModelDefinition, ModelSummary } from '@contentrain/types'\nimport { join } from 'node:path'\nimport { rm } from 'node:fs/promises'\nimport { z } from 'zod'\nimport { contentrainDir, ensureDir, readDir, readJson, writeJson } from '../util/fs.js'\nimport type { RepoReader } from './contracts/index.js'\nimport { LocalReader } from '../providers/local/reader.js'\nimport { resolveContentDir, resolveLocaleStrategy } from './content-manager.js'\nimport { contentDirPath } from './ops/paths.js'\n\nexport type { ModelSummary } from '@contentrain/types'\n\nconst MODELS_DIR_PATH = '.contentrain/models'\n\nasync function tryReadJsonViaReader<T>(reader: RepoReader, path: string): Promise<T | null> {\n try {\n return JSON.parse(await reader.readFile(path)) as T\n } catch {\n return null\n }\n}\n\n/**\n * List every model defined under `.contentrain/models/*.json`.\n *\n * Accepts either a legacy `projectRoot` string (filesystem walk) or any\n * `RepoReader` — HTTP-hosted callers pass their `GitHubProvider` so the\n * same helper resolves model metadata over the Git Data API.\n */\nexport function listModels(projectRoot: string): Promise<ModelSummary[]>\nexport function listModels(reader: RepoReader): Promise<ModelSummary[]>\nexport async function listModels(input: string | RepoReader): Promise<ModelSummary[]> {\n let files: string[]\n let load: (file: string) => Promise<ModelDefinition | null>\n\n if (typeof input === 'string') {\n const modelsDir = join(contentrainDir(input), 'models')\n files = await readDir(modelsDir)\n load = file => readJson<ModelDefinition>(join(modelsDir, file))\n } else {\n files = await input.listDirectory(MODELS_DIR_PATH)\n load = file => tryReadJsonViaReader<ModelDefinition>(input, `${MODELS_DIR_PATH}/${file}`)\n }\n\n const jsonFiles = files.filter(f => f.endsWith('.json'))\n const models = await Promise.all(jsonFiles.map(load))\n\n return models\n .filter((m): m is ModelDefinition => m !== null && !!m.id)\n .map(model => ({\n id: model.id,\n kind: model.kind,\n domain: model.domain,\n i18n: model.i18n,\n fields: model.fields ? Object.keys(model.fields).length : 0,\n }))\n .toSorted((a, b) => a.id.localeCompare(b.id, 'en'))\n}\n\n/**\n * Read a single model definition. Same dual signature as {@link listModels}.\n */\nexport function readModel(projectRoot: string, modelId: string): Promise<ModelDefinition | null>\nexport function readModel(reader: RepoReader, modelId: string): Promise<ModelDefinition | null>\nexport async function readModel(input: string | RepoReader, modelId: string): Promise<ModelDefinition | null> {\n if (typeof input === 'string') {\n const filePath = join(contentrainDir(input), 'models', `${modelId}.json`)\n return readJson<ModelDefinition>(filePath)\n }\n return tryReadJsonViaReader<ModelDefinition>(input, `${MODELS_DIR_PATH}/${modelId}.json`)\n}\n\nasync function countDocumentFileStrategy(\n reader: RepoReader,\n contentDir: string,\n entries: string[],\n): Promise<{ total: number, locales: Record<string, number> }> {\n const locales: Record<string, number> = {}\n let total = 0\n\n const results = await Promise.all(\n entries.map(async (entry) => {\n const localeFiles = await reader.listDirectory(`${contentDir}/${entry}`)\n return localeFiles\n .map(lf => lf.replace(/\\.(json|md|mdx)$/, ''))\n .filter((locale, i) => locale !== localeFiles[i])\n }),\n )\n\n for (const entryLocales of results) {\n for (const locale of entryLocales) {\n locales[locale] = (locales[locale] ?? 0) + 1\n total++\n }\n }\n\n return { total, locales }\n}\n\nasync function countDocumentSuffixStrategy(\n _reader: RepoReader,\n _contentDir: string,\n files: string[],\n): Promise<{ total: number, locales: Record<string, number> }> {\n const locales: Record<string, number> = {}\n const slugsByLocale: Record<string, Set<string>> = {}\n\n for (const f of files) {\n if (!f.endsWith('.md')) continue\n // Pattern: {slug}.{locale}.md\n const match = f.match(/^(.+)\\.([a-z]{2}(?:-[A-Z]{2})?)\\.md$/)\n if (!match) continue\n const locale = match[2]!\n if (!slugsByLocale[locale]) slugsByLocale[locale] = new Set()\n slugsByLocale[locale].add(match[1]!)\n }\n\n let total = 0\n for (const [locale, slugs] of Object.entries(slugsByLocale)) {\n locales[locale] = slugs.size\n total += slugs.size\n }\n\n return { total, locales }\n}\n\nasync function countDocumentDirectoryStrategy(\n reader: RepoReader,\n contentDir: string,\n localeDirs: string[],\n): Promise<{ total: number, locales: Record<string, number> }> {\n const locales: Record<string, number> = {}\n let total = 0\n\n const results = await Promise.all(\n localeDirs.map(async (localeDir) => {\n const files = await reader.listDirectory(`${contentDir}/${localeDir}`)\n const mdFiles = files.filter(f => f.endsWith('.md'))\n return { locale: localeDir, count: mdFiles.length }\n }),\n )\n\n for (const { locale, count } of results) {\n locales[locale] = count\n total += count\n }\n\n return { total, locales }\n}\n\nasync function countDocumentNoneStrategy(\n reader: RepoReader,\n modelId: string,\n files: string[],\n i18n: boolean,\n): Promise<{ total: number, locales: Record<string, number> }> {\n const mdFiles = files.filter(f => f.endsWith('.md'))\n\n if (!i18n) {\n return { total: mdFiles.length, locales: { _: mdFiles.length } }\n }\n\n // With i18n + none strategy, content files have no locale info.\n // Use meta/{modelId}/{slug}/ directories to determine locale counts.\n const metaDir = `.contentrain/meta/${modelId}`\n const locales: Record<string, number> = {}\n let total = 0\n\n const slugs = mdFiles.map(f => f.replace('.md', ''))\n const results = await Promise.all(\n slugs.map(async (slug) => {\n const metaFiles = await reader.listDirectory(`${metaDir}/${slug}`)\n return metaFiles\n .filter(f => f.endsWith('.json'))\n .map(f => f.replace('.json', ''))\n }),\n )\n\n for (const slugLocales of results) {\n for (const locale of slugLocales) {\n locales[locale] = (locales[locale] ?? 0) + 1\n total++\n }\n }\n\n return { total, locales }\n}\n\nasync function countCollectionEntries(\n reader: RepoReader,\n contentDir: string,\n jsonFiles: string[],\n): Promise<{ total: number, locales: Record<string, number> }> {\n const locales: Record<string, number> = {}\n let total = 0\n\n const results = await Promise.all(\n jsonFiles.map(async (file) => {\n const locale = file.replace(/\\.json$/, '')\n const data = await tryReadJsonViaReader<Record<string, unknown>>(reader, `${contentDir}/${file}`)\n return { locale, count: data ? Object.keys(data).length : 0 }\n }),\n )\n\n for (const { locale, count } of results) {\n locales[locale] = count\n total += count\n }\n\n return { total, locales }\n}\n\n/**\n * Count content entries across locales for a model.\n *\n * Accepts either a legacy `projectRoot` string (wraps LocalReader internally)\n * or any `RepoReader` — so remote providers (GitHubProvider) feed the same\n * counting logic via the Git Data API.\n */\nexport function countEntries(\n projectRoot: string,\n model: ModelDefinition,\n): Promise<{ total: number, locales: Record<string, number> }>\nexport function countEntries(\n reader: RepoReader,\n model: ModelDefinition,\n): Promise<{ total: number, locales: Record<string, number> }>\nexport async function countEntries(\n input: string | RepoReader,\n model: ModelDefinition,\n): Promise<{ total: number, locales: Record<string, number> }> {\n const reader: RepoReader = typeof input === 'string' ? new LocalReader(input) : input\n const cDir = contentDirPath(model)\n const strategy = resolveLocaleStrategy(model)\n const files = await reader.listDirectory(cDir)\n\n if (model.kind === 'document') {\n if (!model.i18n) {\n // No i18n: flat {slug}.md files\n return countDocumentNoneStrategy(reader, model.id, files, false)\n }\n\n switch (strategy) {\n case 'file':\n return countDocumentFileStrategy(reader, cDir, files)\n case 'suffix':\n return countDocumentSuffixStrategy(reader, cDir, files)\n case 'directory':\n return countDocumentDirectoryStrategy(reader, cDir, files)\n case 'none':\n return countDocumentNoneStrategy(reader, model.id, files, true)\n }\n }\n\n if (model.kind === 'collection') {\n if (!model.i18n) {\n // Non-i18n: single data.json\n const jsonFiles = files.filter(f => f.endsWith('.json'))\n return countCollectionEntries(reader, cDir, jsonFiles)\n }\n switch (strategy) {\n case 'suffix': {\n // Files: {model}.{locale}.json\n const jsonFiles = files.filter(f => f.endsWith('.json'))\n const locales: Record<string, number> = {}\n for (const f of jsonFiles) {\n const match = f.match(/^.+\\.([a-z]{2}(?:-[A-Z]{2})?)\\.json$/)\n if (match) {\n const data = await tryReadJsonViaReader<Record<string, unknown>>(reader, `${cDir}/${f}`)\n locales[match[1]!] = data ? Object.keys(data).length : 0\n }\n }\n const total = Object.values(locales).reduce((a, b) => a + b, 0)\n return { total, locales }\n }\n case 'directory': {\n // Dirs: {locale}/... containing json files\n const locales: Record<string, number> = {}\n let total = 0\n for (const localeDir of files) {\n const subFiles = await reader.listDirectory(`${cDir}/${localeDir}`)\n const jsonFile = subFiles.find(f => f.endsWith('.json'))\n if (jsonFile) {\n const data = await tryReadJsonViaReader<Record<string, unknown>>(reader, `${cDir}/${localeDir}/${jsonFile}`)\n const count = data ? Object.keys(data).length : 0\n locales[localeDir] = count\n total += count\n }\n }\n return { total, locales }\n }\n case 'none': {\n // Single file: {model}.json — count entries inside\n const noneFile = files.find(f => f === `${model.id}.json`)\n if (!noneFile) return { total: 0, locales: {} }\n const data = await tryReadJsonViaReader<Record<string, unknown>>(reader, `${cDir}/${noneFile}`)\n const count = data ? Object.keys(data).length : 0\n return { total: count, locales: { _: count } }\n }\n default: {\n // 'file': {locale}.json\n const jsonFiles = files.filter(f => f.endsWith('.json'))\n return countCollectionEntries(reader, cDir, jsonFiles)\n }\n }\n }\n\n // singleton / dictionary — one entry per locale file\n if (!model.i18n) {\n // Non-i18n: single data.json\n const hasData = files.some(f => f === 'data.json')\n return { total: hasData ? 1 : 0, locales: {} }\n }\n\n switch (strategy) {\n case 'suffix': {\n // Files: {model}.{locale}.json\n const locales: Record<string, number> = {}\n for (const f of files) {\n const match = f.match(/^.+\\.([a-z]{2}(?:-[A-Z]{2})?)\\.json$/)\n if (match) locales[match[1]!] = 1\n }\n return { total: Object.keys(locales).length, locales }\n }\n case 'directory': {\n // Dirs: {locale}/...\n const locales: Record<string, number> = {}\n for (const localeDir of files) {\n const subFiles = await reader.listDirectory(`${cDir}/${localeDir}`)\n if (subFiles.some(f => f.endsWith('.json'))) {\n locales[localeDir] = 1\n }\n }\n return { total: Object.keys(locales).length, locales }\n }\n case 'none': {\n // Single file: {model}.json\n const hasFile = files.some(f => f === `${model.id}.json`)\n return { total: hasFile ? 1 : 0, locales: {} }\n }\n default: {\n // 'file': {locale}.json\n const jsonFiles = files.filter(f => f.endsWith('.json'))\n const locales: Record<string, number> = {}\n for (const file of jsonFiles) {\n locales[file.replace(/\\.json$/, '')] = 1\n }\n return { total: jsonFiles.length, locales }\n }\n }\n}\n\nconst MODEL_FIELD_ORDER = ['id', 'name', 'kind', 'domain', 'i18n', 'description', 'content_path', 'locale_strategy', 'fields']\n\nexport async function writeModel(projectRoot: string, model: ModelDefinition): Promise<void> {\n const filePath = join(contentrainDir(projectRoot), 'models', `${model.id}.json`)\n await writeJson(filePath, model, MODEL_FIELD_ORDER)\n\n // Ensure content and meta directories exist (respects content_path)\n await ensureDir(resolveContentDir(projectRoot, model))\n await ensureDir(join(contentrainDir(projectRoot), 'meta', model.id))\n}\n\nexport async function deleteModel(projectRoot: string, modelId: string): Promise<string[]> {\n const model = await readModel(projectRoot, modelId)\n if (!model) return []\n\n const crDir = contentrainDir(projectRoot)\n const removed: string[] = []\n\n const modelPath = join(crDir, 'models', `${modelId}.json`)\n const contentPath = resolveContentDir(projectRoot, model)\n const metaPath = join(crDir, 'meta', modelId)\n\n await rm(modelPath, { force: true })\n removed.push(`models/${modelId}.json`)\n\n try {\n await rm(contentPath, { recursive: true, force: true })\n removed.push(model.content_path ?? `content/${model.domain}/${modelId}/`)\n } catch {\n // directory may not exist\n }\n\n try {\n await rm(metaPath, { recursive: true, force: true })\n removed.push(`meta/${modelId}/`)\n } catch {\n // directory may not exist\n }\n\n return removed\n}\n\nexport interface ModelReference {\n model: string\n field: string\n type: 'relation' | 'relations'\n}\n\n/**\n * Enumerate models that reference `modelId` through relation or relations\n * fields. Used by `model_delete` to block destructive deletes when other\n * models still depend on the target.\n *\n * Accepts either a legacy `projectRoot` string (LocalReader wraps internally)\n * or any `RepoReader` — remote providers get the same pre-check.\n */\nexport function checkReferences(projectRoot: string, modelId: string): Promise<ModelReference[]>\nexport function checkReferences(reader: RepoReader, modelId: string): Promise<ModelReference[]>\nexport async function checkReferences(\n input: string | RepoReader,\n modelId: string,\n): Promise<ModelReference[]> {\n const summaries = typeof input === 'string' ? await listModels(input) : await listModels(input)\n const others = summaries.filter(s => s.id !== modelId)\n\n const models = await Promise.all(\n others.map(s => typeof input === 'string' ? readModel(input, s.id) : readModel(input, s.id)),\n )\n\n const refs: ModelReference[] = []\n for (const model of models) {\n if (!model?.fields) continue\n\n for (const [fieldName, fieldDef] of Object.entries(model.fields)) {\n if (fieldDef.type !== 'relation' && fieldDef.type !== 'relations') continue\n const targets = Array.isArray(fieldDef.model) ? fieldDef.model : [fieldDef.model]\n if (targets.includes(modelId)) {\n refs.push({ model: model.id, field: fieldName, type: fieldDef.type })\n }\n }\n }\n\n return refs\n}\n\n// ─── Shared Field Type Catalog ───\n\nexport const FIELD_TYPE_ENUM = [\n 'string', 'text', 'email', 'url', 'slug', 'color', 'phone', 'code', 'icon',\n 'markdown', 'richtext',\n 'number', 'integer', 'decimal', 'percent', 'rating',\n 'boolean', 'date', 'datetime',\n 'image', 'video', 'file',\n 'relation', 'relations',\n 'select', 'array', 'object',\n] as const\n\n/**\n * Shared Zod schema for field definitions.\n * Used by both model_save and normalize extract for full parity.\n */\nexport const fieldDefZodSchema: z.ZodType<Record<string, unknown>> = z.record(z.string(), z.object({\n type: z.enum(FIELD_TYPE_ENUM).describe('Field type from the 27-type catalog'),\n required: z.boolean().optional(),\n unique: z.boolean().optional(),\n default: z.unknown().optional(),\n min: z.number().optional(),\n max: z.number().optional(),\n pattern: z.string().optional(),\n options: z.array(z.string()).optional(),\n model: z.union([z.string(), z.array(z.string())]).optional(),\n items: z.union([z.string(), z.lazy(() => z.record(z.string(), z.unknown()))]).optional(),\n fields: z.lazy(() => z.record(z.string(), z.unknown())).optional(),\n accept: z.string().optional(),\n maxSize: z.number().optional(),\n description: z.string().optional(),\n}).refine(\n (f) => {\n if ((f.type === 'relation' || f.type === 'relations') && !f.model) return false\n if (f.type === 'select' && (!f.options || f.options.length === 0)) return false\n return true\n },\n { message: 'relation/relations requires \"model\", select requires non-empty \"options\"' },\n))\n\n// ─── Model Validation (shared between model_save and normalize extract) ───\n\nconst VALID_FIELD_TYPES = new Set<string>(FIELD_TYPE_ENUM)\n\n/**\n * Validate a model definition before writing.\n * Returns array of error messages (empty = valid).\n * Used by both model_save tool and normalize extract.\n */\nexport function validateModelDefinition(input: { id: string; kind: string; fields?: Record<string, unknown> }): string[] {\n const errors: string[] = []\n\n // ID format: kebab-case\n if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(input.id)) {\n errors.push(`Invalid model ID \"${input.id}\": must be kebab-case`)\n }\n\n // Dictionary models cannot have fields\n if (input.kind === 'dictionary' && input.fields && Object.keys(input.fields).length > 0) {\n errors.push('Dictionary models cannot have fields. Dictionaries store flat key-value pairs.')\n }\n\n // Fields validation\n if (input.fields) {\n for (const [fieldName, fieldDef] of Object.entries(input.fields)) {\n const def = fieldDef as { type?: string; model?: unknown; options?: unknown }\n\n // Field name format\n if (!/^[a-z][a-z0-9_]*$/.test(fieldName)) {\n errors.push(`Field \"${fieldName}\": invalid name — must be snake_case starting with letter`)\n }\n\n // Type check\n if (!def.type || !VALID_FIELD_TYPES.has(def.type)) {\n errors.push(`Field \"${fieldName}\": invalid type \"${def.type}\"`)\n }\n\n // Relation requires model\n if ((def.type === 'relation' || def.type === 'relations') && !def.model) {\n errors.push(`Field \"${fieldName}\": ${def.type} type requires \"model\" property`)\n }\n\n // Select requires options\n if (def.type === 'select' && (!def.options || !Array.isArray(def.options) || def.options.length === 0)) {\n errors.push(`Field \"${fieldName}\": select type requires non-empty \"options\" array`)\n }\n }\n }\n\n return errors\n}\n"],"mappings":"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"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { n as canonicalStringify$1, t as serializeMarkdownFrontmatter$1 } from "./serialization-B1CEzR4H.mjs";
|
|
2
|
-
import { i as metaFilePath, n as contentFilePath, r as documentFilePath } from "./paths-
|
|
2
|
+
import { i as metaFilePath, n as contentFilePath, r as documentFilePath } from "./paths-CmVw5Cw2.mjs";
|
|
3
|
+
import { n as mergeEntryMeta } from "./meta-manager-CJUiTgP2.mjs";
|
|
3
4
|
import { dirname, join } from "node:path";
|
|
4
5
|
import { generateEntryId, validateEntryId, validateLocale, validateSlug } from "@contentrain/types";
|
|
5
6
|
import { mkdir, rm, writeFile } from "node:fs/promises";
|
|
@@ -82,7 +83,7 @@ async function planContentDelete(reader, input) {
|
|
|
82
83
|
removed.push(`content/${model.domain}/${model.id}/${loc}.json#${input.id}`);
|
|
83
84
|
}
|
|
84
85
|
for (const loc of locales) {
|
|
85
|
-
const mPath = metaFilePath(model, loc);
|
|
86
|
+
const mPath = metaFilePath(model, loc, input.defaultLocale);
|
|
86
87
|
const meta = await tryReadJson(reader, mPath);
|
|
87
88
|
if (!meta || !(input.id in meta)) continue;
|
|
88
89
|
delete meta[input.id];
|
|
@@ -107,7 +108,7 @@ async function planContentDelete(reader, input) {
|
|
|
107
108
|
});
|
|
108
109
|
removed.push(model.i18n ? `content/${model.domain}/${model.id}/${locale}.json` : `content/${model.domain}/${model.id}/data.json`);
|
|
109
110
|
if (model.i18n) changes.push({
|
|
110
|
-
path: metaFilePath(model, locale),
|
|
111
|
+
path: metaFilePath(model, locale, input.defaultLocale),
|
|
111
112
|
content: null
|
|
112
113
|
});
|
|
113
114
|
else await pushAllMetaForModel(reader, model.id, changes);
|
|
@@ -135,7 +136,7 @@ async function planContentDelete(reader, input) {
|
|
|
135
136
|
});
|
|
136
137
|
removed.push(model.i18n ? `content/${model.domain}/${model.id}/${locale}.json` : `content/${model.domain}/${model.id}/data.json`);
|
|
137
138
|
if (model.i18n) changes.push({
|
|
138
|
-
path: metaFilePath(model, locale),
|
|
139
|
+
path: metaFilePath(model, locale, input.defaultLocale),
|
|
139
140
|
content: null
|
|
140
141
|
});
|
|
141
142
|
else await pushAllMetaForModel(reader, model.id, changes);
|
|
@@ -324,6 +325,18 @@ async function planContentSave(reader, input) {
|
|
|
324
325
|
return {};
|
|
325
326
|
}
|
|
326
327
|
}
|
|
328
|
+
/** Absent file and absent entry both mean "no prior meta" — hence undefined, not `{}`. */
|
|
329
|
+
async function readJsonOrUndefined(path) {
|
|
330
|
+
try {
|
|
331
|
+
return JSON.parse(await reader.readFile(path));
|
|
332
|
+
} catch {
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
/** Prior meta for a whole-file (non-collection) meta path, honouring the accumulator. */
|
|
337
|
+
async function priorMeta(mPath) {
|
|
338
|
+
return metaByPath.get(mPath) ?? await readJsonOrUndefined(mPath);
|
|
339
|
+
}
|
|
327
340
|
const defaultLocale = config.locales.default;
|
|
328
341
|
for (const entry of entries) {
|
|
329
342
|
const locale = entry.locale ?? defaultLocale;
|
|
@@ -340,9 +353,9 @@ async function planContentSave(reader, input) {
|
|
|
340
353
|
switch (model.kind) {
|
|
341
354
|
case "singleton": {
|
|
342
355
|
const cPath = contentFilePath(model, locale);
|
|
343
|
-
const mPath = metaFilePath(model, locale);
|
|
356
|
+
const mPath = metaFilePath(model, locale, defaultLocale);
|
|
344
357
|
contentByPath.set(cPath, normalizeMedia(entry.data));
|
|
345
|
-
metaByPath.set(mPath,
|
|
358
|
+
metaByPath.set(mPath, mergeEntryMeta(await priorMeta(mPath), entry.data));
|
|
346
359
|
result.push({
|
|
347
360
|
action: "updated",
|
|
348
361
|
locale
|
|
@@ -353,7 +366,7 @@ async function planContentSave(reader, input) {
|
|
|
353
366
|
const isNew = !entry.id;
|
|
354
367
|
const id = entry.id ?? generateEntryId();
|
|
355
368
|
const cPath = contentFilePath(model, locale);
|
|
356
|
-
const mPath = metaFilePath(model, locale);
|
|
369
|
+
const mPath = metaFilePath(model, locale, defaultLocale);
|
|
357
370
|
const existing = contentByPath.get(cPath) ?? await readJsonOrEmpty(cPath);
|
|
358
371
|
const action = isNew || !(id in existing) ? "created" : "updated";
|
|
359
372
|
existing[id] = normalizeMedia(entry.data);
|
|
@@ -361,7 +374,7 @@ async function planContentSave(reader, input) {
|
|
|
361
374
|
for (const key of Object.keys(existing).toSorted()) sorted[key] = existing[key];
|
|
362
375
|
contentByPath.set(cPath, sorted);
|
|
363
376
|
const existingMeta = metaByPath.get(mPath) ?? await readJsonOrEmpty(mPath);
|
|
364
|
-
existingMeta[id] =
|
|
377
|
+
existingMeta[id] = mergeEntryMeta(existingMeta[id], entry.data);
|
|
365
378
|
metaByPath.set(mPath, existingMeta);
|
|
366
379
|
result.push({
|
|
367
380
|
action,
|
|
@@ -372,7 +385,7 @@ async function planContentSave(reader, input) {
|
|
|
372
385
|
}
|
|
373
386
|
case "dictionary": {
|
|
374
387
|
const cPath = contentFilePath(model, locale);
|
|
375
|
-
const mPath = metaFilePath(model, locale);
|
|
388
|
+
const mPath = metaFilePath(model, locale, defaultLocale);
|
|
376
389
|
const existing = contentByPath.get(cPath) ?? await readJsonOrEmpty(cPath);
|
|
377
390
|
const newData = entry.data;
|
|
378
391
|
const collisions = Object.keys(newData).filter((k) => k in existing && existing[k] !== newData[k]);
|
|
@@ -397,7 +410,7 @@ async function planContentSave(reader, input) {
|
|
|
397
410
|
...existing,
|
|
398
411
|
...newData
|
|
399
412
|
});
|
|
400
|
-
metaByPath.set(mPath,
|
|
413
|
+
metaByPath.set(mPath, mergeEntryMeta(await priorMeta(mPath), entry.data));
|
|
401
414
|
result.push({
|
|
402
415
|
action: "updated",
|
|
403
416
|
locale,
|
|
@@ -415,7 +428,7 @@ async function planContentSave(reader, input) {
|
|
|
415
428
|
delete fmData["body"];
|
|
416
429
|
if (!fmData["slug"]) fmData["slug"] = slug;
|
|
417
430
|
const dPath = documentFilePath(model, locale, slug);
|
|
418
|
-
const mPath = metaFilePath(model, locale, slug);
|
|
431
|
+
const mPath = metaFilePath(model, locale, defaultLocale, slug);
|
|
419
432
|
let existingRaw = null;
|
|
420
433
|
try {
|
|
421
434
|
existingRaw = await reader.readFile(dPath);
|
|
@@ -423,7 +436,7 @@ async function planContentSave(reader, input) {
|
|
|
423
436
|
const action = existingRaw ? "updated" : "created";
|
|
424
437
|
const normalizedBody = mediaBaseUrl ? rewriteMarkdownMedia(bodyContent, mediaBaseUrl) : bodyContent;
|
|
425
438
|
markdownChanges.set(dPath, serializeMarkdownFrontmatter$1(normalizeMedia(fmData), normalizedBody));
|
|
426
|
-
metaByPath.set(mPath,
|
|
439
|
+
metaByPath.set(mPath, mergeEntryMeta(await priorMeta(mPath), entry.data));
|
|
427
440
|
result.push({
|
|
428
441
|
action,
|
|
429
442
|
slug,
|
|
@@ -453,16 +466,6 @@ async function planContentSave(reader, input) {
|
|
|
453
466
|
advisories
|
|
454
467
|
};
|
|
455
468
|
}
|
|
456
|
-
function defaultMeta(data) {
|
|
457
|
-
const meta = {
|
|
458
|
-
status: "draft",
|
|
459
|
-
source: "agent",
|
|
460
|
-
updated_by: "contentrain-mcp"
|
|
461
|
-
};
|
|
462
|
-
if (data?.["publish_at"] !== void 0) meta.publish_at = data["publish_at"];
|
|
463
|
-
if (data?.["expire_at"] !== void 0) meta.expire_at = data["expire_at"];
|
|
464
|
-
return meta;
|
|
465
|
-
}
|
|
466
469
|
//#endregion
|
|
467
470
|
//#region src/core/ops/model-delete.ts
|
|
468
471
|
/**
|
|
@@ -568,4 +571,4 @@ async function planModelSave(reader, input) {
|
|
|
568
571
|
//#endregion
|
|
569
572
|
export { applyChangesToWorktree as a, planContentDelete as i, planModelDelete as n, planContentSave as r, planModelSave as t };
|
|
570
573
|
|
|
571
|
-
//# sourceMappingURL=ops-
|
|
574
|
+
//# sourceMappingURL=ops-B422KP_S.mjs.map
|