@contentrain/mcp 2.3.1 → 3.0.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.
Files changed (162) hide show
  1. package/README.md +1 -1
  2. package/dist/{annotations-D3tlsF38.mjs → annotations-C-tQVmot.mjs} +15 -1
  3. package/dist/annotations-C-tQVmot.mjs.map +1 -0
  4. package/dist/{apply-manager-SLCRLHN_.mjs → apply-manager-BybvnUVC.mjs} +28 -18
  5. package/dist/apply-manager-BybvnUVC.mjs.map +1 -0
  6. package/dist/{astro-parser-LNjEspnf.mjs → astro-parser-Cb9uuBpW.mjs} +3 -3
  7. package/dist/{astro-parser-LNjEspnf.mjs.map → astro-parser-Cb9uuBpW.mjs.map} +1 -1
  8. package/dist/{availability-sWWRz2W0.mjs → availability-Dgcmvv99.mjs} +1 -1
  9. package/dist/{availability-sWWRz2W0.mjs.map → availability-Dgcmvv99.mjs.map} +1 -1
  10. package/dist/{branch-lifecycle-BAfgSQBv.mjs → branch-lifecycle-B6PXZ2N7.mjs} +11 -61
  11. package/dist/branch-lifecycle-B6PXZ2N7.mjs.map +1 -0
  12. package/dist/{branch-lifecycle-D4W0WIs4.d.mts → branch-lifecycle-DhzHfzs3.d.mts} +1 -1
  13. package/dist/{branch-lifecycle-D4W0WIs4.d.mts.map → branch-lifecycle-DhzHfzs3.d.mts.map} +1 -1
  14. package/dist/{compiler-CNzm2Y2I.mjs → compiler-B6hNQeCC.mjs} +2 -2
  15. package/dist/{compiler-CNzm2Y2I.mjs.map → compiler-B6hNQeCC.mjs.map} +1 -1
  16. package/dist/{compiler-sfc.cjs-y0B0L3MH.mjs → compiler-sfc.cjs-8yY7b5bx.mjs} +2 -2
  17. package/dist/{compiler-sfc.cjs-y0B0L3MH.mjs.map → compiler-sfc.cjs-8yY7b5bx.mjs.map} +1 -1
  18. package/dist/{config-oxxgznz7.mjs → config-VlRKRwAS.mjs} +1 -1
  19. package/dist/{config-oxxgznz7.mjs.map → config-VlRKRwAS.mjs.map} +1 -1
  20. package/dist/{content-manager-DWo3G64y.d.mts → content-manager-Dn4tqDw1.d.mts} +10 -7
  21. package/dist/content-manager-Dn4tqDw1.d.mts.map +1 -0
  22. package/dist/{context-DjglDPvj.mjs → context-C3x6I3cE.mjs} +4 -4
  23. package/dist/{context-DjglDPvj.mjs.map → context-C3x6I3cE.mjs.map} +1 -1
  24. package/dist/core/apply-manager.d.mts +8 -0
  25. package/dist/core/apply-manager.d.mts.map +1 -1
  26. package/dist/core/apply-manager.mjs +10 -9
  27. package/dist/core/config.d.mts +1 -1
  28. package/dist/core/config.mjs +1 -1
  29. package/dist/core/content-manager.d.mts +2 -2
  30. package/dist/core/content-manager.mjs +2 -2
  31. package/dist/core/context.d.mts +1 -1
  32. package/dist/core/context.mjs +5 -5
  33. package/dist/core/contracts/index.d.mts +2 -2
  34. package/dist/core/doctor.mjs +7 -6
  35. package/dist/core/graph-builder.mjs +2 -2
  36. package/dist/core/meta-manager.d.mts +8 -9
  37. package/dist/core/meta-manager.d.mts.map +1 -1
  38. package/dist/core/meta-manager.mjs +2 -2
  39. package/dist/core/model-manager.d.mts +39 -3
  40. package/dist/core/model-manager.d.mts.map +1 -1
  41. package/dist/core/model-manager.mjs +3 -3
  42. package/dist/core/ops/index.d.mts +40 -3
  43. package/dist/core/ops/index.d.mts.map +1 -1
  44. package/dist/core/ops/index.mjs +4 -4
  45. package/dist/core/overlay-reader.d.mts +1 -1
  46. package/dist/core/overlay-reader.mjs +1 -1
  47. package/dist/core/scan-config.mjs +1 -1
  48. package/dist/core/scanner.mjs +3 -3
  49. package/dist/core/validator/index.d.mts +1 -1
  50. package/dist/core/validator/index.d.mts.map +1 -1
  51. package/dist/core/validator/index.mjs +4 -4
  52. package/dist/{detect-CdDU4Ew_.mjs → detect-B0oXbQaP.mjs} +1 -1
  53. package/dist/{detect-CdDU4Ew_.mjs.map → detect-B0oXbQaP.mjs.map} +1 -1
  54. package/dist/{doctor-BwJ_nmqS.mjs → doctor-DNPuGgcg.mjs} +28 -15
  55. package/dist/doctor-DNPuGgcg.mjs.map +1 -0
  56. package/dist/{errors-e0YdjooK.mjs → errors-CvBS12e7.mjs} +1 -1
  57. package/dist/{errors-e0YdjooK.mjs.map → errors-CvBS12e7.mjs.map} +1 -1
  58. package/dist/fs-DLbVB-Ek.mjs.map +1 -1
  59. package/dist/git/branch-lifecycle.d.mts +1 -1
  60. package/dist/git/branch-lifecycle.mjs +3 -2
  61. package/dist/git/identity.d.mts +42 -0
  62. package/dist/git/identity.d.mts.map +1 -0
  63. package/dist/git/identity.mjs +2 -0
  64. package/dist/git/transaction.d.mts +1 -1
  65. package/dist/git/transaction.d.mts.map +1 -1
  66. package/dist/git/transaction.mjs +9 -8
  67. package/dist/{graph-builder-DK4Mh8Tn.mjs → graph-builder-BEocwW13.mjs} +2 -2
  68. package/dist/{graph-builder-DK4Mh8Tn.mjs.map → graph-builder-BEocwW13.mjs.map} +1 -1
  69. package/dist/{id-DV_T9Ic8.mjs → id-BjhaIxTL.mjs} +1 -1
  70. package/dist/{id-DV_T9Ic8.mjs.map → id-BjhaIxTL.mjs.map} +1 -1
  71. package/dist/identity-D8QoGxhT.mjs +125 -0
  72. package/dist/identity-D8QoGxhT.mjs.map +1 -0
  73. package/dist/{index-w8QHThNS.d.mts → index-BWbTJlXm.d.mts} +2 -2
  74. package/dist/index.mjs +21 -20
  75. package/dist/index.mjs.map +1 -1
  76. package/dist/{local-8dJ5NIWL.mjs → local-Db5X9mjZ.mjs} +25 -13
  77. package/dist/local-Db5X9mjZ.mjs.map +1 -0
  78. package/dist/{meta-manager-CJUiTgP2.mjs → meta-manager-B_Se183X.mjs} +29 -5
  79. package/dist/meta-manager-B_Se183X.mjs.map +1 -0
  80. package/dist/{model-manager-DP2CZiMT.mjs → model-manager-DgyKMjLN.mjs} +169 -25
  81. package/dist/model-manager-DgyKMjLN.mjs.map +1 -0
  82. package/dist/{node-BxujauDw.mjs → node-DUGiIkv1.mjs} +1 -1
  83. package/dist/{node-BxujauDw.mjs.map → node-DUGiIkv1.mjs.map} +1 -1
  84. package/dist/{ops-CGKIKwDc.mjs → ops-BzbThweu.mjs} +165 -26
  85. package/dist/ops-BzbThweu.mjs.map +1 -0
  86. package/dist/{overlay-reader-DNaVsgiS.mjs → overlay-reader-CkU_pGXZ.mjs} +1 -1
  87. package/dist/{overlay-reader-DNaVsgiS.mjs.map → overlay-reader-CkU_pGXZ.mjs.map} +1 -1
  88. package/dist/{paths-enT2coeX.mjs → paths-CdPPWpbG.mjs} +1 -1
  89. package/dist/{paths-enT2coeX.mjs.map → paths-CdPPWpbG.mjs.map} +1 -1
  90. package/dist/providers/github/index.d.mts +1 -1
  91. package/dist/providers/github/index.mjs +2 -2
  92. package/dist/providers/gitlab/index.d.mts +1 -1
  93. package/dist/providers/gitlab/index.mjs +2 -2
  94. package/dist/providers/local/index.d.mts +7 -1
  95. package/dist/providers/local/index.d.mts.map +1 -1
  96. package/dist/providers/local/index.mjs +11 -10
  97. package/dist/{scan-config-BlNLRCMx.mjs → scan-config-Dx49nCqk.mjs} +1 -1
  98. package/dist/{scan-config-BlNLRCMx.mjs.map → scan-config-Dx49nCqk.mjs.map} +1 -1
  99. package/dist/{scanner-CGWhmpDz.mjs → scanner-BSvXjfkp.mjs} +6 -6
  100. package/dist/{scanner-CGWhmpDz.mjs.map → scanner-BSvXjfkp.mjs.map} +1 -1
  101. package/dist/serialization-tmgfFBOh.mjs +2 -0
  102. package/dist/server/http/index.d.mts +1 -1
  103. package/dist/server/http/index.mjs +21 -20
  104. package/dist/server/http/index.mjs.map +1 -1
  105. package/dist/{server-CUZnuHQx.mjs → server-B2ib9TkR.mjs} +313 -115
  106. package/dist/server-B2ib9TkR.mjs.map +1 -0
  107. package/dist/{server-DLauBiu2.d.mts → server-CLPNnhYL.d.mts} +2 -2
  108. package/dist/{server-DLauBiu2.d.mts.map → server-CLPNnhYL.d.mts.map} +1 -1
  109. package/dist/server.d.mts +1 -1
  110. package/dist/server.mjs +21 -20
  111. package/dist/{source-map-generator-Bvbu-kM5.mjs → source-map-generator-DhjiQdV3.mjs} +1 -1
  112. package/dist/{source-map-generator-Bvbu-kM5.mjs.map → source-map-generator-DhjiQdV3.mjs.map} +1 -1
  113. package/dist/{svelte-parser-DaFK9iY0.mjs → svelte-parser-CwySu9yn.mjs} +3 -3
  114. package/dist/{svelte-parser-DaFK9iY0.mjs.map → svelte-parser-CwySu9yn.mjs.map} +1 -1
  115. package/dist/templates/index.mjs +1 -1
  116. package/dist/{templates-D6EllixQ.mjs → templates-CAkTIRkE.mjs} +430 -109
  117. package/dist/templates-CAkTIRkE.mjs.map +1 -0
  118. package/dist/testing/memory-provider.d.mts +52 -0
  119. package/dist/testing/memory-provider.d.mts.map +1 -0
  120. package/dist/testing/memory-provider.mjs +205 -0
  121. package/dist/testing/memory-provider.mjs.map +1 -0
  122. package/dist/tools/annotations.d.mts.map +1 -1
  123. package/dist/tools/annotations.mjs +1 -1
  124. package/dist/tools/availability.mjs +1 -1
  125. package/dist/{transaction-1SPznNt3.mjs → transaction-BW9ektWu.mjs} +13 -12
  126. package/dist/transaction-BW9ektWu.mjs.map +1 -0
  127. package/dist/{tsx-parser-md1N0Niu.mjs → tsx-parser-BvsK6lyG.mjs} +1 -1
  128. package/dist/{tsx-parser-md1N0Niu.mjs.map → tsx-parser-BvsK6lyG.mjs.map} +1 -1
  129. package/dist/tsx-parser-Byc7HlJ8.mjs +2 -0
  130. package/dist/util/detect.mjs +1 -1
  131. package/dist/util/fs.d.mts +1 -1
  132. package/dist/util/fs.d.mts.map +1 -1
  133. package/dist/util/id.mjs +1 -1
  134. package/dist/{validator-ChiYk6ap.mjs → validator-0sJ9vh1j.mjs} +65 -8
  135. package/dist/validator-0sJ9vh1j.mjs.map +1 -0
  136. package/dist/{vue-parser-CHxDohd8.mjs → vue-parser-auedZiJG.mjs} +3 -3
  137. package/dist/{vue-parser-CHxDohd8.mjs.map → vue-parser-auedZiJG.mjs.map} +1 -1
  138. package/package.json +13 -5
  139. package/testing/conformance/01-new-collection-entry/expected/.contentrain/meta/blog/en.json +1 -0
  140. package/testing/conformance/01-new-collection-entry/expected/.contentrain/models/blog.json +6 -5
  141. package/testing/conformance/01-new-collection-entry/setup/.contentrain/models/blog.json +6 -5
  142. package/testing/conformance/02-new-singleton/expected/.contentrain/meta/hero/en.json +1 -0
  143. package/testing/conformance/02-new-singleton/expected/.contentrain/models/hero.json +6 -5
  144. package/testing/conformance/02-new-singleton/setup/.contentrain/models/hero.json +6 -5
  145. package/testing/conformance/03-new-dictionary/expected/.contentrain/meta/ui-strings/en.json +1 -0
  146. package/testing/conformance/03-new-dictionary/expected/.contentrain/models/ui-strings.json +4 -3
  147. package/testing/conformance/03-new-dictionary/setup/.contentrain/models/ui-strings.json +4 -3
  148. package/dist/annotations-D3tlsF38.mjs.map +0 -1
  149. package/dist/apply-manager-SLCRLHN_.mjs.map +0 -1
  150. package/dist/branch-lifecycle-BAfgSQBv.mjs.map +0 -1
  151. package/dist/content-manager-DWo3G64y.d.mts.map +0 -1
  152. package/dist/doctor-BwJ_nmqS.mjs.map +0 -1
  153. package/dist/local-8dJ5NIWL.mjs.map +0 -1
  154. package/dist/meta-manager-CJUiTgP2.mjs.map +0 -1
  155. package/dist/model-manager-DP2CZiMT.mjs.map +0 -1
  156. package/dist/ops-CGKIKwDc.mjs.map +0 -1
  157. package/dist/serialization-B1CEzR4H.mjs +0 -2
  158. package/dist/server-CUZnuHQx.mjs.map +0 -1
  159. package/dist/templates-D6EllixQ.mjs.map +0 -1
  160. package/dist/transaction-1SPznNt3.mjs.map +0 -1
  161. package/dist/tsx-parser-C3XsIrwU.mjs +0 -2
  162. package/dist/validator-ChiYk6ap.mjs.map +0 -1
@@ -0,0 +1,125 @@
1
+ import { delimiter, isAbsolute, join } from "node:path";
2
+ import { simpleGit } from "simple-git";
3
+ import { accessSync, constants } from "node:fs";
4
+ //#region src/git/identity.ts
5
+ /**
6
+ * Git identity + guard-safe `simple-git` construction for MCP write operations.
7
+ *
8
+ * simple-git >= 3.34 bundles `@simple-git/argv-parser`, whose block-unsafe
9
+ * guard rejects a `git` invocation when any of ~18 "unsafe" variables (EDITOR,
10
+ * GIT_ASKPASS, PAGER, GIT_SSH_COMMAND, GIT_PROXY_COMMAND, …) is passed
11
+ * EXPLICITLY through `.env()`. Crucially, the guard only scans the object
12
+ * handed to `.env()` — it never inspects the inherited process environment.
13
+ *
14
+ * The rule this module enforces: NEVER spread `process.env` into `.env()`.
15
+ * - Commit identity is supplied as `-c user.*` config (guard-safe: `user.name`
16
+ * / `user.email` are not on any unsafe list, and git honours them for both
17
+ * the author and the committer). See {@link authorConfig}.
18
+ * - The rare instance that genuinely needs the inherited environment — network
19
+ * push/fetch, which relies on the host's askpass/SSH/proxy setup to
20
+ * authenticate — opts out of the affected guard categories via `unsafe`
21
+ * instead of hiding the environment. See {@link NETWORK_UNSAFE}.
22
+ */
23
+ const DEFAULT_AUTHOR_NAME = "Contentrain";
24
+ const DEFAULT_AUTHOR_EMAIL = "ai@contentrain.io";
25
+ /**
26
+ * Absolute path to the `git` executable, resolved once per process.
27
+ *
28
+ * Every write here spawns git dozens of times — a `contentrain_init` is 33
29
+ * spawns — and spawning by bare name makes the OS re-walk `PATH` on each one.
30
+ * That walk is not free: a developer PATH is commonly 40-50 entries, only one
31
+ * of which holds git, and each miss costs a failed `stat`. Measured on macOS,
32
+ * a spawn by name costs ~211ms against ~16ms for the same spawn by absolute
33
+ * path, and an `init` drops from ~9.4s to ~2.2s.
34
+ *
35
+ * A malformed entry makes it far worse: a PATH containing `/usr/bin/git`
36
+ * (the binary itself, mistaken for a directory) turns every probe into an
37
+ * `ENOTDIR` that measured ~311ms on its own.
38
+ *
39
+ * This is not a test-only concern. An MCP server launched from an editor
40
+ * inherits that editor's PATH, so the shipped product pays the same cost on
41
+ * every write.
42
+ *
43
+ * Resolution is deliberately conservative: the first PATH entry that holds an
44
+ * executable `git`, which is the same one the OS would have picked. We do not
45
+ * try to bypass the macOS `xcode-select` shim — it exists to track the active
46
+ * toolchain, and second-guessing it would trade correctness for ~10ms.
47
+ */
48
+ let resolvedBinary;
49
+ function gitBinary() {
50
+ if (resolvedBinary !== void 0) return resolvedBinary;
51
+ const override = process.env["CONTENTRAIN_GIT_BINARY"];
52
+ if (override && isAbsolute(override)) {
53
+ resolvedBinary = override;
54
+ return resolvedBinary;
55
+ }
56
+ const names = process.platform === "win32" ? [
57
+ "git.exe",
58
+ "git.cmd",
59
+ "git"
60
+ ] : ["git"];
61
+ for (const dir of (process.env["PATH"] ?? "").split(delimiter)) {
62
+ if (!dir) continue;
63
+ for (const name of names) {
64
+ const candidate = join(dir, name);
65
+ try {
66
+ accessSync(candidate, constants.X_OK);
67
+ resolvedBinary = candidate;
68
+ return resolvedBinary;
69
+ } catch {}
70
+ }
71
+ }
72
+ resolvedBinary = "git";
73
+ return resolvedBinary;
74
+ }
75
+ /** Reset the cached binary. Tests only. */
76
+ function resetGitBinary() {
77
+ resolvedBinary = void 0;
78
+ }
79
+ /**
80
+ * The single constructor for every `simple-git` instance in this package.
81
+ *
82
+ * Exists so the resolved binary (above) cannot be forgotten at a call site —
83
+ * there were 41 of them, and one that spawns by bare name silently gives back
84
+ * the 13× penalty for that code path only.
85
+ */
86
+ function createGit(baseDir, options) {
87
+ const merged = {
88
+ ...options,
89
+ binary: gitBinary()
90
+ };
91
+ return baseDir === void 0 ? simpleGit(merged) : simpleGit(baseDir, merged);
92
+ }
93
+ /**
94
+ * Commit identity as `-c` config entries for `simpleGit(dir, { config })`.
95
+ * Passed as arguments (not env) so the block-unsafe guard is never triggered,
96
+ * regardless of what the host process exports. Sets author + committer alike.
97
+ */
98
+ function authorConfig() {
99
+ const name = process.env["CONTENTRAIN_AUTHOR_NAME"] ?? DEFAULT_AUTHOR_NAME;
100
+ const email = process.env["CONTENTRAIN_AUTHOR_EMAIL"] ?? DEFAULT_AUTHOR_EMAIL;
101
+ return [`user.name=${name}`, `user.email=${email}`];
102
+ }
103
+ /**
104
+ * Guard opt-outs for network `git` instances that MUST inherit the real
105
+ * environment (credential askpass helpers, SSH agent, proxy) to authenticate a
106
+ * push/fetch. Covers every guard category reachable from an inherited env var,
107
+ * so the command never trips regardless of what the host (VS Code, CI) exports
108
+ * — while still leaving arg-injection protections (custom binaries, `ext::`
109
+ * protocol, `--upload-pack`) intact.
110
+ */
111
+ const NETWORK_UNSAFE = {
112
+ allowUnsafeAskPass: true,
113
+ allowUnsafeConfigEnvCount: true,
114
+ allowUnsafeConfigPaths: true,
115
+ allowUnsafeDiffExternal: true,
116
+ allowUnsafeEditor: true,
117
+ allowUnsafeGitProxy: true,
118
+ allowUnsafePager: true,
119
+ allowUnsafeSshCommand: true,
120
+ allowUnsafeTemplateDir: true
121
+ };
122
+ //#endregion
123
+ export { resetGitBinary as a, gitBinary as i, authorConfig as n, createGit as r, NETWORK_UNSAFE as t };
124
+
125
+ //# sourceMappingURL=identity-D8QoGxhT.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"identity-D8QoGxhT.mjs","names":[],"sources":["../src/git/identity.ts"],"sourcesContent":["/**\n * Git identity + guard-safe `simple-git` construction for MCP write operations.\n *\n * simple-git >= 3.34 bundles `@simple-git/argv-parser`, whose block-unsafe\n * guard rejects a `git` invocation when any of ~18 \"unsafe\" variables (EDITOR,\n * GIT_ASKPASS, PAGER, GIT_SSH_COMMAND, GIT_PROXY_COMMAND, …) is passed\n * EXPLICITLY through `.env()`. Crucially, the guard only scans the object\n * handed to `.env()` — it never inspects the inherited process environment.\n *\n * The rule this module enforces: NEVER spread `process.env` into `.env()`.\n * - Commit identity is supplied as `-c user.*` config (guard-safe: `user.name`\n * / `user.email` are not on any unsafe list, and git honours them for both\n * the author and the committer). See {@link authorConfig}.\n * - The rare instance that genuinely needs the inherited environment — network\n * push/fetch, which relies on the host's askpass/SSH/proxy setup to\n * authenticate — opts out of the affected guard categories via `unsafe`\n * instead of hiding the environment. See {@link NETWORK_UNSAFE}.\n */\n\nimport { accessSync, constants } from 'node:fs'\nimport { delimiter, isAbsolute, join } from 'node:path'\nimport { simpleGit, type SimpleGit, type SimpleGitOptions } from 'simple-git'\n\nconst DEFAULT_AUTHOR_NAME = 'Contentrain'\nconst DEFAULT_AUTHOR_EMAIL = 'ai@contentrain.io'\n\n/**\n * Absolute path to the `git` executable, resolved once per process.\n *\n * Every write here spawns git dozens of times — a `contentrain_init` is 33\n * spawns — and spawning by bare name makes the OS re-walk `PATH` on each one.\n * That walk is not free: a developer PATH is commonly 40-50 entries, only one\n * of which holds git, and each miss costs a failed `stat`. Measured on macOS,\n * a spawn by name costs ~211ms against ~16ms for the same spawn by absolute\n * path, and an `init` drops from ~9.4s to ~2.2s.\n *\n * A malformed entry makes it far worse: a PATH containing `/usr/bin/git`\n * (the binary itself, mistaken for a directory) turns every probe into an\n * `ENOTDIR` that measured ~311ms on its own.\n *\n * This is not a test-only concern. An MCP server launched from an editor\n * inherits that editor's PATH, so the shipped product pays the same cost on\n * every write.\n *\n * Resolution is deliberately conservative: the first PATH entry that holds an\n * executable `git`, which is the same one the OS would have picked. We do not\n * try to bypass the macOS `xcode-select` shim — it exists to track the active\n * toolchain, and second-guessing it would trade correctness for ~10ms.\n */\nlet resolvedBinary: string | undefined\n\nexport function gitBinary(): string {\n if (resolvedBinary !== undefined) return resolvedBinary\n\n const override = process.env['CONTENTRAIN_GIT_BINARY']\n if (override && isAbsolute(override)) {\n resolvedBinary = override\n return resolvedBinary\n }\n\n const isWindows = process.platform === 'win32'\n const names = isWindows ? ['git.exe', 'git.cmd', 'git'] : ['git']\n for (const dir of (process.env['PATH'] ?? '').split(delimiter)) {\n if (!dir) continue\n for (const name of names) {\n const candidate = join(dir, name)\n try {\n accessSync(candidate, constants.X_OK)\n resolvedBinary = candidate\n return resolvedBinary\n } catch { /* next candidate */ }\n }\n }\n\n // Not found on PATH — hand back the bare name so the failure surfaces from\n // git itself with its usual message rather than from this resolver.\n resolvedBinary = 'git'\n return resolvedBinary\n}\n\n/** Reset the cached binary. Tests only. */\nexport function resetGitBinary(): void {\n resolvedBinary = undefined\n}\n\n/**\n * The single constructor for every `simple-git` instance in this package.\n *\n * Exists so the resolved binary (above) cannot be forgotten at a call site —\n * there were 41 of them, and one that spawns by bare name silently gives back\n * the 13× penalty for that code path only.\n */\nexport function createGit(baseDir?: string, options?: Partial<SimpleGitOptions>): SimpleGit {\n const merged = { ...options, binary: gitBinary() }\n return baseDir === undefined ? simpleGit(merged) : simpleGit(baseDir, merged)\n}\n\n/**\n * Commit identity as `-c` config entries for `simpleGit(dir, { config })`.\n * Passed as arguments (not env) so the block-unsafe guard is never triggered,\n * regardless of what the host process exports. Sets author + committer alike.\n */\nexport function authorConfig(): string[] {\n const name = process.env['CONTENTRAIN_AUTHOR_NAME'] ?? DEFAULT_AUTHOR_NAME\n const email = process.env['CONTENTRAIN_AUTHOR_EMAIL'] ?? DEFAULT_AUTHOR_EMAIL\n return [`user.name=${name}`, `user.email=${email}`]\n}\n\n/**\n * Guard opt-outs for network `git` instances that MUST inherit the real\n * environment (credential askpass helpers, SSH agent, proxy) to authenticate a\n * push/fetch. Covers every guard category reachable from an inherited env var,\n * so the command never trips regardless of what the host (VS Code, CI) exports\n * — while still leaving arg-injection protections (custom binaries, `ext::`\n * protocol, `--upload-pack`) intact.\n */\nexport const NETWORK_UNSAFE = {\n allowUnsafeAskPass: true,\n allowUnsafeConfigEnvCount: true,\n allowUnsafeConfigPaths: true,\n allowUnsafeDiffExternal: true,\n allowUnsafeEditor: true,\n allowUnsafeGitProxy: true,\n allowUnsafePager: true,\n allowUnsafeSshCommand: true,\n allowUnsafeTemplateDir: true,\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAuBA,MAAM,sBAAsB;AAC5B,MAAM,uBAAuB;;;;;;;;;;;;;;;;;;;;;;;;AAyB7B,IAAI;AAEJ,SAAgB,YAAoB;AAClC,KAAI,mBAAmB,KAAA,EAAW,QAAO;CAEzC,MAAM,WAAW,QAAQ,IAAI;AAC7B,KAAI,YAAY,WAAW,SAAS,EAAE;AACpC,mBAAiB;AACjB,SAAO;;CAIT,MAAM,QADY,QAAQ,aAAa,UACb;EAAC;EAAW;EAAW;EAAM,GAAG,CAAC,MAAM;AACjE,MAAK,MAAM,QAAQ,QAAQ,IAAI,WAAW,IAAI,MAAM,UAAU,EAAE;AAC9D,MAAI,CAAC,IAAK;AACV,OAAK,MAAM,QAAQ,OAAO;GACxB,MAAM,YAAY,KAAK,KAAK,KAAK;AACjC,OAAI;AACF,eAAW,WAAW,UAAU,KAAK;AACrC,qBAAiB;AACjB,WAAO;WACD;;;AAMZ,kBAAiB;AACjB,QAAO;;;AAIT,SAAgB,iBAAuB;AACrC,kBAAiB,KAAA;;;;;;;;;AAUnB,SAAgB,UAAU,SAAkB,SAAgD;CAC1F,MAAM,SAAS;EAAE,GAAG;EAAS,QAAQ,WAAW;EAAE;AAClD,QAAO,YAAY,KAAA,IAAY,UAAU,OAAO,GAAG,UAAU,SAAS,OAAO;;;;;;;AAQ/E,SAAgB,eAAyB;CACvC,MAAM,OAAO,QAAQ,IAAI,8BAA8B;CACvD,MAAM,QAAQ,QAAQ,IAAI,+BAA+B;AACzD,QAAO,CAAC,aAAa,QAAQ,cAAc,QAAQ;;;;;;;;;;AAWrD,MAAa,iBAAiB;CAC5B,oBAAoB;CACpB,2BAA2B;CAC3B,wBAAwB;CACxB,yBAAyB;CACzB,mBAAmB;CACnB,qBAAqB;CACrB,kBAAkB;CAClB,uBAAuB;CACvB,wBAAwB;CACzB"}
@@ -1,2 +1,2 @@
1
- import { ApplyPlanInput, Branch, Commit, CommitAuthor, FileChange, FileDiff, LOCAL_CAPABILITIES, MediaAsset, MediaIngestInput, MediaListOptions, MediaListResult, MediaProvider, MediaUpdateInput, MergeResult, ProviderCapabilities as ProviderCapabilities$1, RepoProvider, RepoReader, RepoWriter } from "@contentrain/types";
2
- export { RepoReader as _, FileChange as a, MediaAsset as c, MediaListResult as d, MediaProvider as f, RepoProvider as g, ProviderCapabilities$1 as h, CommitAuthor as i, MediaIngestInput as l, MergeResult as m, Branch as n, FileDiff as o, MediaUpdateInput as p, Commit as r, LOCAL_CAPABILITIES as s, ApplyPlanInput as t, MediaListOptions as u, RepoWriter as v };
1
+ import { ApplyPlanInput, Branch, Commit, CommitAuthor, FileChange, FileDiff, LOCAL_CAPABILITIES, MediaAsset, MediaIngestInput, MediaListOptions, MediaListResult, MediaProvider, MediaUpdateInput, MergeResult, ProviderCapabilities as ProviderCapabilities$1, RepoProvider, RepoReader, RepoWriter, WriteReadiness } from "@contentrain/types";
2
+ export { RepoReader as _, FileChange as a, MediaAsset as c, MediaListResult as d, MediaProvider as f, RepoProvider as g, ProviderCapabilities$1 as h, CommitAuthor as i, MediaIngestInput as l, MergeResult as m, Branch as n, FileDiff as o, MediaUpdateInput as p, Commit as r, LOCAL_CAPABILITIES as s, ApplyPlanInput as t, MediaListOptions as u, RepoWriter as v, WriteReadiness as y };
package/dist/index.mjs CHANGED
@@ -1,26 +1,27 @@
1
1
  #!/usr/bin/env node
2
2
  import "./contracts-DfL0BfrD.mjs";
3
- import "./ops-CGKIKwDc.mjs";
4
- import "./serialization-B1CEzR4H.mjs";
3
+ import "./ops-BzbThweu.mjs";
4
+ import "./serialization-tmgfFBOh.mjs";
5
5
  import "./fs-DLbVB-Ek.mjs";
6
- import "./meta-manager-CJUiTgP2.mjs";
7
- import "./config-oxxgznz7.mjs";
8
- import "./model-manager-DP2CZiMT.mjs";
9
- import "./context-DjglDPvj.mjs";
10
- import "./branch-lifecycle-BAfgSQBv.mjs";
11
- import "./id-DV_T9Ic8.mjs";
12
- import "./transaction-1SPznNt3.mjs";
13
- import "./local-8dJ5NIWL.mjs";
14
- import "./detect-CdDU4Ew_.mjs";
15
- import "./annotations-D3tlsF38.mjs";
16
- import { n as createServer } from "./server-CUZnuHQx.mjs";
17
- import "./validator-ChiYk6ap.mjs";
18
- import "./scan-config-BlNLRCMx.mjs";
19
- import "./graph-builder-DK4Mh8Tn.mjs";
20
- import "./scanner-CGWhmpDz.mjs";
21
- import "./tsx-parser-md1N0Niu.mjs";
22
- import "./apply-manager-SLCRLHN_.mjs";
23
- import "./doctor-BwJ_nmqS.mjs";
6
+ import "./meta-manager-B_Se183X.mjs";
7
+ import "./identity-D8QoGxhT.mjs";
8
+ import "./config-VlRKRwAS.mjs";
9
+ import "./branch-lifecycle-B6PXZ2N7.mjs";
10
+ import "./model-manager-DgyKMjLN.mjs";
11
+ import "./context-C3x6I3cE.mjs";
12
+ import "./id-BjhaIxTL.mjs";
13
+ import "./transaction-BW9ektWu.mjs";
14
+ import "./local-Db5X9mjZ.mjs";
15
+ import "./detect-B0oXbQaP.mjs";
16
+ import "./annotations-C-tQVmot.mjs";
17
+ import { n as createServer } from "./server-B2ib9TkR.mjs";
18
+ import "./validator-0sJ9vh1j.mjs";
19
+ import "./scan-config-Dx49nCqk.mjs";
20
+ import "./graph-builder-BEocwW13.mjs";
21
+ import "./scanner-BSvXjfkp.mjs";
22
+ import "./tsx-parser-BvsK6lyG.mjs";
23
+ import "./apply-manager-BybvnUVC.mjs";
24
+ import "./doctor-DNPuGgcg.mjs";
24
25
  import { resolve } from "node:path";
25
26
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
26
27
  //#region src/index.ts
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { resolve } from 'node:path'\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'\nimport { createServer } from './server.js'\n\nconst projectRoot = resolve(process.env['CONTENTRAIN_PROJECT_ROOT'] ?? process.cwd())\nconst server = createServer(projectRoot)\nconst transport = new StdioServerTransport()\n\nawait server.connect(transport)\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAMA,MAAM,SAAS,aADK,QAAQ,QAAQ,IAAI,+BAA+B,QAAQ,KAAK,CAAC,CAC7C;AACxC,MAAM,YAAY,IAAI,sBAAsB;AAE5C,MAAM,OAAO,QAAQ,UAAU"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { resolve } from 'node:path'\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'\nimport { createServer } from './server.js'\n\nconst projectRoot = resolve(process.env['CONTENTRAIN_PROJECT_ROOT'] ?? process.cwd())\nconst server = createServer(projectRoot)\nconst transport = new StdioServerTransport()\n\nawait server.connect(transport)\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AAMA,MAAM,SAAS,aADK,QAAQ,QAAQ,IAAI,+BAA+B,QAAQ,KAAK,CAAC,CAC7C;AACxC,MAAM,YAAY,IAAI,sBAAsB;AAE5C,MAAM,OAAO,QAAQ,UAAU"}
@@ -1,11 +1,11 @@
1
1
  import { t as LOCAL_CAPABILITIES } from "./contracts-DfL0BfrD.mjs";
2
- import { a as applyChangesToWorktree } from "./ops-CGKIKwDc.mjs";
3
- import { t as readConfig } from "./config-oxxgznz7.mjs";
4
- import { C as LocalReader } from "./model-manager-DP2CZiMT.mjs";
5
- import { a as deleteRemoteBranch, r as classifyMergedBranches } from "./branch-lifecycle-BAfgSQBv.mjs";
6
- import { i as mergeBranch$1, n as createTransaction } from "./transaction-1SPznNt3.mjs";
2
+ import { s as applyChangesToWorktree } from "./ops-BzbThweu.mjs";
3
+ import { r as createGit } from "./identity-D8QoGxhT.mjs";
4
+ import { t as readConfig } from "./config-VlRKRwAS.mjs";
5
+ import { a as deleteRemoteBranch, n as checkBranchHealth, r as classifyMergedBranches } from "./branch-lifecycle-B6PXZ2N7.mjs";
6
+ import { E as LocalReader } from "./model-manager-DgyKMjLN.mjs";
7
+ import { i as mergeBranch$1, n as createTransaction } from "./transaction-BW9ektWu.mjs";
7
8
  import { CONTENTRAIN_BRANCH } from "@contentrain/types";
8
- import { simpleGit } from "simple-git";
9
9
  //#region src/providers/local/branch-ops.ts
10
10
  /**
11
11
  * Branch/merge/diff helpers backed by the local simple-git worktree flow.
@@ -19,10 +19,10 @@ async function getDefaultBranch(projectRoot) {
19
19
  if (config?.repository?.default_branch) return config.repository.default_branch;
20
20
  const envBranch = process.env["CONTENTRAIN_BRANCH"];
21
21
  if (envBranch) return envBranch;
22
- return (await simpleGit(projectRoot).raw(["branch", "--show-current"])).trim() || "main";
22
+ return (await createGit(projectRoot).raw(["branch", "--show-current"])).trim() || "main";
23
23
  }
24
24
  async function listBranches(projectRoot, prefix) {
25
- const summary = await simpleGit(projectRoot).branchLocal();
25
+ const summary = await createGit(projectRoot).branchLocal();
26
26
  const names = prefix ? summary.all.filter((n) => n.startsWith(prefix)) : summary.all;
27
27
  const branches = [];
28
28
  for (const name of names) {
@@ -35,18 +35,18 @@ async function listBranches(projectRoot, prefix) {
35
35
  return branches;
36
36
  }
37
37
  async function createBranch(projectRoot, name, fromRef) {
38
- await simpleGit(projectRoot).raw([
38
+ await createGit(projectRoot).raw([
39
39
  "branch",
40
40
  name,
41
41
  fromRef
42
42
  ]);
43
43
  }
44
44
  async function deleteBranch(projectRoot, name) {
45
- await simpleGit(projectRoot).deleteLocalBranch(name, true);
45
+ await createGit(projectRoot).deleteLocalBranch(name, true);
46
46
  await deleteRemoteBranch(projectRoot, name);
47
47
  }
48
48
  async function getBranchDiff(projectRoot, branch, base) {
49
- const raw = await simpleGit(projectRoot).raw([
49
+ const raw = await createGit(projectRoot).raw([
50
50
  "diff",
51
51
  "--name-status",
52
52
  `${base}...${branch}`
@@ -124,6 +124,18 @@ var LocalProvider = class {
124
124
  fileExists(path, ref) {
125
125
  return this.reader.fileExists(path, ref);
126
126
  }
127
+ /**
128
+ * Branch pressure gate. Unmerged `cr/*` branches accumulate in a local repo
129
+ * and eventually have to stop new writes; hosted providers have no such
130
+ * limit, so they omit this method entirely.
131
+ */
132
+ async checkWriteReadiness() {
133
+ const health = await checkBranchHealth(this.projectRoot);
134
+ return health.blocked ? {
135
+ blocked: true,
136
+ message: health.message ?? "Too many active contentrain branches."
137
+ } : { blocked: false };
138
+ }
127
139
  async applyPlan(input) {
128
140
  const tx = await createTransaction(this.projectRoot, input.branch, { workflowOverride: input.workflowOverride });
129
141
  try {
@@ -135,7 +147,7 @@ var LocalProvider = class {
135
147
  return {
136
148
  sha: gitResult.commit,
137
149
  message: input.message,
138
- author: {
150
+ author: input.author ?? {
139
151
  name: process.env["CONTENTRAIN_AUTHOR_NAME"] ?? DEFAULT_AUTHOR_NAME,
140
152
  email: process.env["CONTENTRAIN_AUTHOR_EMAIL"] ?? DEFAULT_AUTHOR_EMAIL
141
153
  },
@@ -176,4 +188,4 @@ var LocalProvider = class {
176
188
  //#endregion
177
189
  export { isMerged as n, LocalProvider as t };
178
190
 
179
- //# sourceMappingURL=local-8dJ5NIWL.mjs.map
191
+ //# sourceMappingURL=local-Db5X9mjZ.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"local-Db5X9mjZ.mjs","names":["mergeBranchOp","listBranchesOp","createBranchOp","deleteBranchOp","getBranchDiffOp","mergeBranchOp","isMergedOp","getDefaultBranchOp"],"sources":["../src/providers/local/branch-ops.ts","../src/providers/local/provider.ts"],"sourcesContent":["import { CONTENTRAIN_BRANCH } from '@contentrain/types'\nimport { createGit } from '../../git/identity.js'\nimport type { Branch, FileDiff, MergeResult } from '../../core/contracts/index.js'\nimport { readConfig } from '../../core/config.js'\nimport { classifyMergedBranches, deleteRemoteBranch } from '../../git/branch-lifecycle.js'\nimport { mergeBranch as mergeBranchOp } from '../../git/transaction.js'\n\n/**\n * Branch/merge/diff helpers backed by the local simple-git worktree flow.\n *\n * Pure functions composed into `LocalProvider` — mirroring the shape of\n * `providers/github/branch-ops.ts` so the two providers share the same\n * surface at the `RepoProvider` boundary.\n */\n\nexport async function getDefaultBranch(projectRoot: string): Promise<string> {\n const config = await readConfig(projectRoot)\n if (config?.repository?.default_branch) return config.repository.default_branch\n const envBranch = process.env['CONTENTRAIN_BRANCH']\n if (envBranch) return envBranch\n const git = createGit(projectRoot)\n const current = (await git.raw(['branch', '--show-current'])).trim()\n return current || 'main'\n}\n\nexport async function listBranches(\n projectRoot: string,\n prefix?: string,\n): Promise<Branch[]> {\n const git = createGit(projectRoot)\n const summary = await git.branchLocal()\n const names = prefix\n ? summary.all.filter(n => n.startsWith(prefix))\n : summary.all\n const branches: Branch[] = []\n for (const name of names) {\n const info = summary.branches[name]\n branches.push({ name, sha: info?.commit ?? '' })\n }\n return branches\n}\n\nexport async function createBranch(\n projectRoot: string,\n name: string,\n fromRef: string,\n): Promise<void> {\n const git = createGit(projectRoot)\n await git.raw(['branch', name, fromRef])\n}\n\nexport async function deleteBranch(\n projectRoot: string,\n name: string,\n): Promise<void> {\n const git = createGit(projectRoot)\n await git.deleteLocalBranch(name, true)\n // Parity with the remote-API providers, whose deleteBranch removes the\n // remote ref: best-effort, config-gated inside the helper, never throws.\n await deleteRemoteBranch(projectRoot, name)\n}\n\nexport async function getBranchDiff(\n projectRoot: string,\n branch: string,\n base: string,\n): Promise<FileDiff[]> {\n const git = createGit(projectRoot)\n const raw = await git.raw(['diff', '--name-status', `${base}...${branch}`])\n const diffs: FileDiff[] = []\n for (const line of raw.split('\\n')) {\n if (!line.trim()) continue\n const [code, ...pathParts] = line.split('\\t')\n const path = pathParts[pathParts.length - 1]\n if (!code || !path) continue\n const status: FileDiff['status'] = code.startsWith('A')\n ? 'added'\n : code.startsWith('D')\n ? 'removed'\n : 'modified'\n diffs.push({ path, status, before: null, after: null })\n }\n return diffs\n}\n\nexport async function mergeBranch(\n projectRoot: string,\n branch: string,\n into: string,\n): Promise<MergeResult> {\n if (into !== CONTENTRAIN_BRANCH) {\n throw Object.assign(new Error(\n `LocalProvider.mergeBranch only supports merging into \"${CONTENTRAIN_BRANCH}\" (got \"${into}\"). `\n + `The local flow merges feature branches into the content-tracking branch and fast-forwards the base branch via update-ref.`,\n ), {\n code: 'UNSUPPORTED_MERGE_TARGET',\n agent_hint: `Pass \"${CONTENTRAIN_BRANCH}\" as the merge target, or use a non-local provider that supports arbitrary targets.`,\n developer_action: `Merge \"${branch}\" into \"${CONTENTRAIN_BRANCH}\" instead.`,\n })\n }\n const result = await mergeBranchOp(projectRoot, branch)\n return {\n merged: true,\n sha: result.commit,\n pullRequestUrl: null,\n sync: result.sync,\n ...(result.remote ? { remote: result.remote } : {}),\n }\n}\n\nexport async function isMerged(\n projectRoot: string,\n branch: string,\n into: string,\n): Promise<boolean> {\n try {\n const merged = await classifyMergedBranches(projectRoot, [branch], into)\n return merged.has(branch)\n } catch {\n return false\n }\n}\n","import { CONTENTRAIN_BRANCH } from '@contentrain/types'\nimport type {\n Branch,\n FileDiff,\n MergeResult,\n ProviderCapabilities,\n RepoProvider,\n WriteReadiness,\n} from '../../core/contracts/index.js'\nimport { LOCAL_CAPABILITIES } from '../../core/contracts/index.js'\nimport { applyChangesToWorktree } from '../../core/ops/index.js'\nimport { checkBranchHealth } from '../../git/branch-lifecycle.js'\nimport { createTransaction } from '../../git/transaction.js'\nimport {\n createBranch as createBranchOp,\n deleteBranch as deleteBranchOp,\n getBranchDiff as getBranchDiffOp,\n getDefaultBranch as getDefaultBranchOp,\n isMerged as isMergedOp,\n listBranches as listBranchesOp,\n mergeBranch as mergeBranchOp,\n} from './branch-ops.js'\nimport { LocalReader } from './reader.js'\nimport type { LocalApplyPlanInput, LocalApplyResult } from './types.js'\n\nconst DEFAULT_AUTHOR_NAME = 'Contentrain'\nconst DEFAULT_AUTHOR_EMAIL = 'ai@contentrain.io'\n\n/**\n * LocalProvider — the local-filesystem, worktree-backed content provider.\n *\n * Implements the full `RepoProvider` surface:\n * - Reader methods delegate to `LocalReader`.\n * - `applyPlan` wraps `createTransaction` and returns `LocalApplyResult`\n * (a superset of `Commit` carrying workflow action + selective sync).\n * - Branch ops mirror `GitHubProvider` — thin wrappers over the local\n * simple-git helpers in `./branch-ops.ts`.\n *\n * `mergeBranch` only supports merging into the singleton\n * `CONTENTRAIN_BRANCH`; the local flow advances the base branch via\n * `update-ref` in `transaction.mergeBranch`, so arbitrary merge targets\n * would bypass that invariant.\n */\nexport class LocalProvider implements RepoProvider {\n readonly capabilities: ProviderCapabilities = LOCAL_CAPABILITIES\n private readonly reader: LocalReader\n\n constructor(public readonly projectRoot: string) {\n this.reader = new LocalReader(projectRoot)\n }\n\n readFile(path: string, ref?: string): Promise<string> {\n return this.reader.readFile(path, ref)\n }\n\n listDirectory(path: string, ref?: string): Promise<string[]> {\n return this.reader.listDirectory(path, ref)\n }\n\n fileExists(path: string, ref?: string): Promise<boolean> {\n return this.reader.fileExists(path, ref)\n }\n\n /**\n * Branch pressure gate. Unmerged `cr/*` branches accumulate in a local repo\n * and eventually have to stop new writes; hosted providers have no such\n * limit, so they omit this method entirely.\n */\n async checkWriteReadiness(): Promise<WriteReadiness> {\n const health = await checkBranchHealth(this.projectRoot)\n return health.blocked\n ? { blocked: true, message: health.message ?? 'Too many active contentrain branches.' }\n : { blocked: false }\n }\n\n async applyPlan(input: LocalApplyPlanInput): Promise<LocalApplyResult> {\n const tx = await createTransaction(this.projectRoot, input.branch, {\n workflowOverride: input.workflowOverride,\n })\n try {\n await tx.write(async (wt) => {\n await applyChangesToWorktree(wt, input.changes)\n })\n await tx.commit(input.message, input.context)\n const gitResult = await tx.complete()\n return {\n sha: gitResult.commit,\n message: input.message,\n author: input.author ?? {\n name: process.env['CONTENTRAIN_AUTHOR_NAME'] ?? DEFAULT_AUTHOR_NAME,\n email: process.env['CONTENTRAIN_AUTHOR_EMAIL'] ?? DEFAULT_AUTHOR_EMAIL,\n },\n timestamp: new Date().toISOString(),\n workflowAction: gitResult.action,\n sync: gitResult.sync,\n warning: gitResult.warning,\n }\n } finally {\n await tx.cleanup()\n }\n }\n\n listBranches(prefix?: string): Promise<Branch[]> {\n return listBranchesOp(this.projectRoot, prefix)\n }\n\n async createBranch(name: string, fromRef?: string): Promise<void> {\n const resolved = fromRef ?? CONTENTRAIN_BRANCH\n await createBranchOp(this.projectRoot, name, resolved)\n }\n\n deleteBranch(name: string): Promise<void> {\n return deleteBranchOp(this.projectRoot, name)\n }\n\n getBranchDiff(branch: string, base?: string): Promise<FileDiff[]> {\n const resolved = base ?? CONTENTRAIN_BRANCH\n return getBranchDiffOp(this.projectRoot, branch, resolved)\n }\n\n mergeBranch(branch: string, into: string): Promise<MergeResult> {\n return mergeBranchOp(this.projectRoot, branch, into)\n }\n\n isMerged(branch: string, into?: string): Promise<boolean> {\n const resolved = into ?? CONTENTRAIN_BRANCH\n return isMergedOp(this.projectRoot, branch, resolved)\n }\n\n getDefaultBranch(): Promise<string> {\n return getDefaultBranchOp(this.projectRoot)\n }\n}\n"],"mappings":"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"}
@@ -20,17 +20,41 @@ async function readMeta(projectRoot, model, opts) {
20
20
  *
21
21
  * `status` is a publish decision and belongs to whoever made it — editing a
22
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.
23
+ * `draft`. `source`/`updated_by`/`updated_at` describe *this* write, so they
24
+ * are stamped every time; `approved_by`/`version` survive via the spread.
25
25
  *
26
26
  * Pass `existing: undefined` to mint meta for a genuinely new entry.
27
27
  */
28
+ /**
29
+ * The write timestamp. One function so there is one answer to "when", and so
30
+ * a test can freeze it in one place.
31
+ */
32
+ function nowIso() {
33
+ return (/* @__PURE__ */ new Date()).toISOString();
34
+ }
35
+ /**
36
+ * Meta for a deliberate status change.
37
+ *
38
+ * Distinct from {@link mergeEntryMeta}, which preserves `status` on purpose —
39
+ * editing a field must not unpublish an entry. Changing the status IS the
40
+ * operation here, so it is the one place that may overwrite it. Everything
41
+ * else about the write is stamped identically.
42
+ */
43
+ function applyStatusChange(existing, status) {
44
+ return {
45
+ ...existing,
46
+ status,
47
+ updated_by: "contentrain-mcp",
48
+ updated_at: nowIso()
49
+ };
50
+ }
28
51
  function mergeEntryMeta(existing, data) {
29
52
  const meta = {
30
53
  ...existing,
31
54
  status: existing?.status ?? "draft",
32
55
  source: "agent",
33
- updated_by: "contentrain-mcp"
56
+ updated_by: "contentrain-mcp",
57
+ updated_at: nowIso()
34
58
  };
35
59
  if (data?.["publish_at"] !== void 0) meta.publish_at = data["publish_at"];
36
60
  if (data?.["expire_at"] !== void 0) meta.expire_at = data["expire_at"];
@@ -93,6 +117,6 @@ async function deleteMeta(projectRoot, model, opts) {
93
117
  }
94
118
  }
95
119
  //#endregion
96
- export { writeMetaEntries as a, writeMeta as i, mergeEntryMeta as n, readMeta as r, deleteMeta as t };
120
+ export { writeMeta as a, readMeta as i, deleteMeta as n, writeMetaEntries as o, mergeEntryMeta as r, applyStatusChange as t };
97
121
 
98
- //# sourceMappingURL=meta-manager-CJUiTgP2.mjs.map
122
+ //# sourceMappingURL=meta-manager-B_Se183X.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"meta-manager-B_Se183X.mjs","names":[],"sources":["../src/core/meta-manager.ts"],"sourcesContent":["import type { ContentStatus, 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`/`updated_at` describe *this* write, so they\n * are stamped every time; `approved_by`/`version` survive via the spread.\n *\n * Pass `existing: undefined` to mint meta for a genuinely new entry.\n */\n/**\n * The write timestamp. One function so there is one answer to \"when\", and so\n * a test can freeze it in one place.\n */\nfunction nowIso(): string {\n return new Date().toISOString()\n}\n\n/**\n * Meta for a deliberate status change.\n *\n * Distinct from {@link mergeEntryMeta}, which preserves `status` on purpose —\n * editing a field must not unpublish an entry. Changing the status IS the\n * operation here, so it is the one place that may overwrite it. Everything\n * else about the write is stamped identically.\n */\nexport function applyStatusChange(existing: EntryMeta, status: ContentStatus): EntryMeta {\n return {\n ...existing,\n status,\n updated_by: 'contentrain-mcp',\n updated_at: nowIso(),\n }\n}\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 updated_at: nowIso(),\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;;;;;;;;;;;;;;;;AAiBrD,SAAS,SAAiB;AACxB,yBAAO,IAAI,MAAM,EAAC,aAAa;;;;;;;;;;AAWjC,SAAgB,kBAAkB,UAAqB,QAAkC;AACvF,QAAO;EACL,GAAG;EACH;EACA,YAAY;EACZ,YAAY,QAAQ;EACrB;;AAGH,SAAgB,eACd,UACA,MACW;CACX,MAAM,OAAkB;EACtB,GAAG;EACH,QAAQ,UAAU,UAAU;EAC5B,QAAQ;EACR,YAAY;EACZ,YAAY,QAAQ;EACrB;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"}