@contentrain/mcp 3.1.8 → 3.2.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/core/ops/index.mjs +1 -1
- package/dist/git/reconcile.mjs +2 -2
- package/dist/index-_HJVkDJa.d.mts.map +1 -1
- package/dist/index.mjs +4 -4
- package/dist/{local-BVww7ArO.mjs → local-DbPlR0Br.mjs} +2 -2
- package/dist/{local-BVww7ArO.mjs.map → local-DbPlR0Br.mjs.map} +1 -1
- package/dist/providers/local/index.mjs +2 -2
- package/dist/{reconcile-MiURoMUx.mjs → reconcile-EANDIp_T.mjs} +2 -2
- package/dist/{reconcile-MiURoMUx.mjs.map → reconcile-EANDIp_T.mjs.map} +1 -1
- package/dist/{reconcile-B8qhwneb.mjs → reconcile-TyPQKjme.mjs} +24 -1
- package/dist/{reconcile-B8qhwneb.mjs.map → reconcile-TyPQKjme.mjs.map} +1 -1
- package/dist/server/http/index.mjs +4 -4
- package/dist/{server-CXfGWDd4.mjs → server-ChpsLHlF.mjs} +4 -4
- package/dist/{server-CXfGWDd4.mjs.map → server-ChpsLHlF.mjs.map} +1 -1
- package/dist/server.mjs +4 -4
- package/package.json +2 -2
package/dist/core/ops/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as applyChangesToWorktree, t as planReconcile } from "../../reconcile-
|
|
1
|
+
import { n as applyChangesToWorktree, t as planReconcile } from "../../reconcile-TyPQKjme.mjs";
|
|
2
2
|
import "../../context-BHfO4WBv.mjs";
|
|
3
3
|
import { A as contentDirPath, M as documentFilePath, N as metaFilePath, j as contentFilePath } from "../../model-manager-CfwJWvz7.mjs";
|
|
4
4
|
import { a as planModelDelete, i as planModelSave, n as planVocabularySave, o as planContentSave, r as bindRef, s as planContentDelete, t as planVocabularyDelete } from "../../ops-B2P11b8v.mjs";
|
package/dist/git/reconcile.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import "../reconcile-
|
|
1
|
+
import "../reconcile-TyPQKjme.mjs";
|
|
2
2
|
import "../context-BHfO4WBv.mjs";
|
|
3
3
|
import "../model-manager-CfwJWvz7.mjs";
|
|
4
4
|
import "../fs-DJv5G8uL.mjs";
|
|
@@ -9,5 +9,5 @@ import "../identity-CxAB9Qb8.mjs";
|
|
|
9
9
|
import "../branch-lifecycle-DUOoyNWn.mjs";
|
|
10
10
|
import "../id-CjGMPP1k.mjs";
|
|
11
11
|
import "../transaction-BR2jg4uj.mjs";
|
|
12
|
-
import { t as reconcileBranches } from "../reconcile-
|
|
12
|
+
import { t as reconcileBranches } from "../reconcile-EANDIp_T.mjs";
|
|
13
13
|
export { reconcileBranches };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index-_HJVkDJa.d.mts","names":[],"sources":["../src/core/ops/types.ts","../src/core/ops/reconcile/types.ts","../src/core/ops/reconcile/plan.ts"],"mappings":";;;;;;;AAUA;;;;;UAAiB,MAAA;EAEN;EAAT,OAAA,EAAS,UAAA;EAED;EAAR,MAAA,EAAQ,OAAA;EAEE;EAAV,UAAA;AAAA;AAAA,UAGe,sBAAA;EACf,MAAA;EACA,EAAA;EACA,IAAA;EACA,MAAA;EACA,UAAA;AAAA;AAAA,KAGU,eAAA,GAAkB,MAAA,CAAO,sBAAA;;;AAjBrC;;;;;;;;AAAA,UCGiB,cAAA;EACf,IAAA,EAAM,UAAA;EACN,IAAA,EAAM,UAAA;EACN,MAAA,EAAQ,UAAA;EDGO;ECDf,WAAA,GAAc,kBAAA;;EAEd,MAAA,GAAS,aAAA;AAAA;AAAA,UAGM,eAAA;EDDf;ECGA,YAAA;EDDA;ECGA,kBAAA;EACA,oBAAA;EDDU;ECGV,oBAAA;;EAEA,WAAA;AAAA;;;;AAnBF;;;UA4BiB,aAAA,SAAsB,MAAA,CAAO,eAAA;EAC5C,SAAA,EAAW,YAAA;AAAA;;;;;;ADhCb;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"index-_HJVkDJa.d.mts","names":[],"sources":["../src/core/ops/types.ts","../src/core/ops/reconcile/types.ts","../src/core/ops/reconcile/plan.ts"],"mappings":";;;;;;;AAUA;;;;;UAAiB,MAAA;EAEN;EAAT,OAAA,EAAS,UAAA;EAED;EAAR,MAAA,EAAQ,OAAA;EAEE;EAAV,UAAA;AAAA;AAAA,UAGe,sBAAA;EACf,MAAA;EACA,EAAA;EACA,IAAA;EACA,MAAA;EACA,UAAA;AAAA;AAAA,KAGU,eAAA,GAAkB,MAAA,CAAO,sBAAA;;;AAjBrC;;;;;;;;AAAA,UCGiB,cAAA;EACf,IAAA,EAAM,UAAA;EACN,IAAA,EAAM,UAAA;EACN,MAAA,EAAQ,UAAA;EDGO;ECDf,WAAA,GAAc,kBAAA;;EAEd,MAAA,GAAS,aAAA;AAAA;AAAA,UAGM,eAAA;EDDf;ECGA,YAAA;EDDA;ECGA,kBAAA;EACA,oBAAA;EDDU;ECGV,oBAAA;;EAEA,WAAA;AAAA;;;;AAnBF;;;UA4BiB,aAAA,SAAsB,MAAA,CAAO,eAAA;EAC5C,SAAA,EAAW,YAAA;AAAA;;;;;;ADhCb;;;;;;;;;iBEkDsB,aAAA,CAAc,KAAA,EAAO,cAAA,GAAiB,OAAA,CAAQ,aAAA"}
|
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import "./contracts-DfL0BfrD.mjs";
|
|
3
|
-
import "./reconcile-
|
|
3
|
+
import "./reconcile-TyPQKjme.mjs";
|
|
4
4
|
import "./context-BHfO4WBv.mjs";
|
|
5
5
|
import "./model-manager-CfwJWvz7.mjs";
|
|
6
6
|
import "./ops-B2P11b8v.mjs";
|
|
@@ -12,12 +12,12 @@ import "./identity-CxAB9Qb8.mjs";
|
|
|
12
12
|
import "./branch-lifecycle-DUOoyNWn.mjs";
|
|
13
13
|
import "./id-CjGMPP1k.mjs";
|
|
14
14
|
import "./transaction-BR2jg4uj.mjs";
|
|
15
|
-
import "./local-
|
|
15
|
+
import "./local-DbPlR0Br.mjs";
|
|
16
16
|
import "./detect-CI32ynpq.mjs";
|
|
17
17
|
import "./annotations-CXHPnS-M.mjs";
|
|
18
|
-
import { n as createServer } from "./server-
|
|
18
|
+
import { n as createServer } from "./server-ChpsLHlF.mjs";
|
|
19
19
|
import "./errors-BmLE-JSC.mjs";
|
|
20
|
-
import "./reconcile-
|
|
20
|
+
import "./reconcile-EANDIp_T.mjs";
|
|
21
21
|
import "./scan-config-C5zzygeH.mjs";
|
|
22
22
|
import "./graph-builder-C9NS7y-a.mjs";
|
|
23
23
|
import "./scanner-CEiT8Tgx.mjs";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as LOCAL_CAPABILITIES } from "./contracts-DfL0BfrD.mjs";
|
|
2
|
-
import { n as applyChangesToWorktree } from "./reconcile-
|
|
2
|
+
import { n as applyChangesToWorktree } from "./reconcile-TyPQKjme.mjs";
|
|
3
3
|
import { k as LocalReader } from "./model-manager-CfwJWvz7.mjs";
|
|
4
4
|
import { t as readConfig } from "./config-B8a4JBT1.mjs";
|
|
5
5
|
import { r as createGit } from "./identity-CxAB9Qb8.mjs";
|
|
@@ -263,4 +263,4 @@ var LocalProvider = class {
|
|
|
263
263
|
//#endregion
|
|
264
264
|
export { isMerged as n, LocalProvider as t };
|
|
265
265
|
|
|
266
|
-
//# sourceMappingURL=local-
|
|
266
|
+
//# sourceMappingURL=local-DbPlR0Br.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"local-BVww7ArO.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 { join } from 'node:path'\nimport { tmpdir } from 'node:os'\nimport { randomUUID } from 'node:crypto'\nimport { CONTENTRAIN_BRANCH } from '@contentrain/types'\nimport type {\n Branch,\n Commit,\n CommitAuthor,\n FileChange,\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 { createGit } from '../../git/identity.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 base_advance: gitResult.base_advance,\n remote_push: gitResult.remote_push,\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 async getMergeBase(refA: string, refB: string): Promise<string | null> {\n const git = createGit(this.projectRoot)\n try {\n return (await git.raw(['merge-base', refA, refB])).trim() || null\n } catch {\n return null\n }\n }\n\n /**\n * Two-parent merge commit in a temp worktree. The real `git merge`\n * resolves everything outside the plan; the plan's changes then\n * overwrite every content-owned path (planner output is authoritative,\n * never git's textual auto-merge). Leftover unmerged paths abort — they\n * are outside the content planner's scope by construction.\n */\n async createMergeCommit(input: {\n branch: string\n ours: string\n theirs: string\n changes: FileChange[]\n message: string\n author: CommitAuthor\n }): Promise<Commit> {\n const git = createGit(this.projectRoot)\n const tip = (await git.raw(['rev-parse', input.branch])).trim()\n if (tip !== input.ours) {\n throw Object.assign(new Error(\n `createMergeCommit: \"${input.branch}\" is at ${tip.slice(0, 8)}, not the expected ${input.ours.slice(0, 8)} — the branch moved since planning.`,\n ), {\n code: 'RECONCILE_STALE_OURS',\n agent_hint: 'Re-run the reconcile plan against the current tip.',\n })\n }\n\n const worktreePath = join(tmpdir(), `cr-merge-commit-${randomUUID()}`)\n await git.raw(['worktree', 'add', worktreePath, input.branch])\n const wtGit = createGit(worktreePath, {\n config: [\n `user.name=${input.author.name}`,\n `user.email=${input.author.email}`,\n ],\n })\n try {\n try {\n await wtGit.merge([input.theirs, '--no-commit', '--no-ff'])\n } catch {\n // Conflicts are expected — the plan overwrites content paths next.\n }\n await applyChangesToWorktree(worktreePath, input.changes)\n await wtGit.raw(['add', '-A'])\n const unmerged = (await wtGit.raw(['ls-files', '-u'])).trim()\n if (unmerged) {\n const paths = [...new Set(unmerged.split('\\n').map(l => l.split('\\t')[1]).filter(Boolean))]\n try { await wtGit.merge(['--abort']) } catch { /* not in merge state */ }\n throw Object.assign(new Error(\n `createMergeCommit: ${paths.length} non-content file(s) conflict: ${paths.join(', ')}.`,\n ), { code: 'RECONCILE_SOURCE_CONFLICT' })\n }\n await wtGit.commit(input.message, { '--no-verify': null })\n const sha = (await wtGit.raw(['rev-parse', 'HEAD'])).trim()\n return {\n sha,\n message: input.message,\n author: input.author,\n timestamp: new Date().toISOString(),\n }\n } finally {\n try {\n await git.raw(['worktree', 'remove', worktreePath, '--force'])\n } catch {\n // worktree may already be cleaned up\n }\n }\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"local-DbPlR0Br.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 { join } from 'node:path'\nimport { tmpdir } from 'node:os'\nimport { randomUUID } from 'node:crypto'\nimport { CONTENTRAIN_BRANCH } from '@contentrain/types'\nimport type {\n Branch,\n Commit,\n CommitAuthor,\n FileChange,\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 { createGit } from '../../git/identity.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 base_advance: gitResult.base_advance,\n remote_push: gitResult.remote_push,\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 async getMergeBase(refA: string, refB: string): Promise<string | null> {\n const git = createGit(this.projectRoot)\n try {\n return (await git.raw(['merge-base', refA, refB])).trim() || null\n } catch {\n return null\n }\n }\n\n /**\n * Two-parent merge commit in a temp worktree. The real `git merge`\n * resolves everything outside the plan; the plan's changes then\n * overwrite every content-owned path (planner output is authoritative,\n * never git's textual auto-merge). Leftover unmerged paths abort — they\n * are outside the content planner's scope by construction.\n */\n async createMergeCommit(input: {\n branch: string\n ours: string\n theirs: string\n changes: FileChange[]\n message: string\n author: CommitAuthor\n }): Promise<Commit> {\n const git = createGit(this.projectRoot)\n const tip = (await git.raw(['rev-parse', input.branch])).trim()\n if (tip !== input.ours) {\n throw Object.assign(new Error(\n `createMergeCommit: \"${input.branch}\" is at ${tip.slice(0, 8)}, not the expected ${input.ours.slice(0, 8)} — the branch moved since planning.`,\n ), {\n code: 'RECONCILE_STALE_OURS',\n agent_hint: 'Re-run the reconcile plan against the current tip.',\n })\n }\n\n const worktreePath = join(tmpdir(), `cr-merge-commit-${randomUUID()}`)\n await git.raw(['worktree', 'add', worktreePath, input.branch])\n const wtGit = createGit(worktreePath, {\n config: [\n `user.name=${input.author.name}`,\n `user.email=${input.author.email}`,\n ],\n })\n try {\n try {\n await wtGit.merge([input.theirs, '--no-commit', '--no-ff'])\n } catch {\n // Conflicts are expected — the plan overwrites content paths next.\n }\n await applyChangesToWorktree(worktreePath, input.changes)\n await wtGit.raw(['add', '-A'])\n const unmerged = (await wtGit.raw(['ls-files', '-u'])).trim()\n if (unmerged) {\n const paths = [...new Set(unmerged.split('\\n').map(l => l.split('\\t')[1]).filter(Boolean))]\n try { await wtGit.merge(['--abort']) } catch { /* not in merge state */ }\n throw Object.assign(new Error(\n `createMergeCommit: ${paths.length} non-content file(s) conflict: ${paths.join(', ')}.`,\n ), { code: 'RECONCILE_SOURCE_CONFLICT' })\n }\n await wtGit.commit(input.message, { '--no-verify': null })\n const sha = (await wtGit.raw(['rev-parse', 'HEAD'])).trim()\n return {\n sha,\n message: input.message,\n author: input.author,\n timestamp: new Date().toISOString(),\n }\n } finally {\n try {\n await git.raw(['worktree', 'remove', worktreePath, '--force'])\n } catch {\n // worktree may already be cleaned up\n }\n }\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import "../../contracts-DfL0BfrD.mjs";
|
|
2
|
-
import "../../reconcile-
|
|
2
|
+
import "../../reconcile-TyPQKjme.mjs";
|
|
3
3
|
import "../../context-BHfO4WBv.mjs";
|
|
4
4
|
import { k as LocalReader } from "../../model-manager-CfwJWvz7.mjs";
|
|
5
5
|
import "../../ops-B2P11b8v.mjs";
|
|
@@ -11,5 +11,5 @@ import "../../identity-CxAB9Qb8.mjs";
|
|
|
11
11
|
import "../../branch-lifecycle-DUOoyNWn.mjs";
|
|
12
12
|
import "../../id-CjGMPP1k.mjs";
|
|
13
13
|
import "../../transaction-BR2jg4uj.mjs";
|
|
14
|
-
import { t as LocalProvider } from "../../local-
|
|
14
|
+
import { t as LocalProvider } from "../../local-DbPlR0Br.mjs";
|
|
15
15
|
export { LocalProvider, LocalReader };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as applyChangesToWorktree, t as planReconcile } from "./reconcile-
|
|
1
|
+
import { n as applyChangesToWorktree, t as planReconcile } from "./reconcile-TyPQKjme.mjs";
|
|
2
2
|
import { t as readConfig } from "./config-B8a4JBT1.mjs";
|
|
3
3
|
import { n as authorConfig, r as createGit } from "./identity-CxAB9Qb8.mjs";
|
|
4
4
|
import { l as networkGit } from "./branch-lifecycle-DUOoyNWn.mjs";
|
|
@@ -246,4 +246,4 @@ async function reconcileBranches(projectRoot, options = {}) {
|
|
|
246
246
|
//#endregion
|
|
247
247
|
export { reconcileBranches as t };
|
|
248
248
|
|
|
249
|
-
//# sourceMappingURL=reconcile-
|
|
249
|
+
//# sourceMappingURL=reconcile-EANDIp_T.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reconcile-MiURoMUx.mjs","names":[],"sources":["../src/git/ref-reader.ts","../src/git/reconcile.ts"],"sourcesContent":["import { type SimpleGit } from 'simple-git'\nimport { createGit } from './identity.js'\nimport type { RepoReader } from '../core/contracts/index.js'\n\n/**\n * GitRefReader — `RepoReader` over an arbitrary local git ref, without a\n * checkout. `git show <ref>:<path>` reads blobs, `ls-tree` lists a tree\n * level, `cat-file -e` answers existence. This is what lets a reconcile\n * planner read three trees (merge-base, contentrain, base branch) side by\n * side while the developer's working tree stays untouched.\n *\n * The ref is fixed at construction; the per-call `ref?` parameter of the\n * `RepoReader` contract is ignored — a bound reader answering for a\n * different ref than it was built for would be a lie. Bind another instance\n * instead.\n */\nexport class GitRefReader implements RepoReader {\n private readonly git: SimpleGit\n\n constructor(\n public readonly projectRoot: string,\n public readonly ref: string,\n ) {\n this.git = createGit(projectRoot)\n }\n\n async readFile(path: string, _ref?: string): Promise<string> {\n return this.git.show([`${this.ref}:${normalize(path)}`])\n }\n\n async listDirectory(path: string, _ref?: string): Promise<string[]> {\n try {\n // Trailing slash = \"list the tree at this path\", bare names via\n // --name-only; ls-tree is non-recursive by default, matching the\n // contract's \"does not recurse\".\n const dir = normalize(path)\n const args = dir && dir !== '.'\n ? ['ls-tree', '--name-only', this.ref, `${dir}/`]\n : ['ls-tree', '--name-only', this.ref]\n const raw = await this.git.raw(args)\n return raw\n .split('\\n')\n .map(line => line.trim())\n .filter(Boolean)\n .map(entry => entry.slice(entry.lastIndexOf('/') + 1))\n } catch {\n return []\n }\n }\n\n async fileExists(path: string, _ref?: string): Promise<boolean> {\n try {\n await this.git.raw(['cat-file', '-e', `${this.ref}:${normalize(path)}`])\n return true\n } catch {\n return false\n }\n }\n}\n\n/** Strip a trailing slash — `<ref>:dir/` and `<ref>:dir` name different things to git. */\nfunction normalize(path: string): string {\n return path.endsWith('/') ? path.slice(0, -1) : path\n}\n","import { join } from 'node:path'\nimport { tmpdir } from 'node:os'\nimport { randomUUID } from 'node:crypto'\nimport { CONTENTRAIN_BRANCH } from '@contentrain/types'\nimport type { BaseAdvance, ConflictResolution, ContextSource, RemotePush, SyncResult } from '@contentrain/types'\nimport { createGit, authorConfig } from './identity.js'\nimport { networkGit } from './branch-lifecycle.js'\nimport { GitRefReader } from './ref-reader.js'\nimport {\n NETWORK_TIMEOUT_MS,\n ensureContentBranch,\n isAncestor,\n pushContentBranches,\n selectiveSync,\n} from './transaction.js'\nimport { readConfig } from '../core/config.js'\nimport { planReconcile } from '../core/ops/reconcile/index.js'\nimport type { ReconcilePlan } from '../core/ops/reconcile/index.js'\nimport { applyChangesToWorktree } from '../core/ops/apply-to-worktree.js'\n\nexport interface ReconcileBranchesOptions {\n /** Preview only — plan, never touch a ref. Default true. */\n dryRun?: boolean\n /** Decisions for previously reported conflicts, matched by id. */\n resolutions?: ConflictResolution[]\n /** Stamped into the regenerated context.json. */\n source?: ContextSource\n}\n\nexport interface ReconcileBranchesResult {\n /**\n * `in_sync` — nothing to reconcile (base already contained in contentrain\n * and no content differences; a base merely behind was fast-forwarded).\n * `preview` — dry-run: the plan says what WOULD happen.\n * `conflicts` — unresolved conflicts remain; nothing was written.\n * `reconciled` — the merge commit landed and the base branch advanced.\n */\n action: 'in_sync' | 'preview' | 'conflicts' | 'reconciled'\n plan: ReconcilePlan\n base: string\n merge_base: string | null\n commit?: string\n base_advance?: BaseAdvance\n sync?: SyncResult\n remote_push?: RemotePush\n}\n\n/**\n * Content-aware reconcile of a diverged `contentrain` ↔ base pair, local\n * executor. Plans with three `GitRefReader`s (no checkout, developer tree\n * untouched), then — when clean or fully resolved — performs a REAL\n * `git merge --no-commit` in a temp worktree and writes the ENTIRE plan\n * output over it before committing: for content-owned paths the planner is\n * authoritative, never git's textual auto-merge (its line-level result can\n * be non-canonical or wrong without ever surfacing as a conflict). Git\n * still resolves everything the planner does not own — source files, lock\n * files — exactly as a manual merge would. The commit joins both parents,\n * so afterwards the base branch is an ancestor again and the ordinary\n * fast-forward advance works.\n *\n * Reconcile never runs inside a write path: it is an explicit, separate\n * step (tool, CLI, or Studio background job).\n */\nexport async function reconcileBranches(\n projectRoot: string,\n options: ReconcileBranchesOptions = {},\n): Promise<ReconcileBranchesResult> {\n const dryRun = options.dryRun ?? true\n const git = createGit(projectRoot)\n const config = await readConfig(projectRoot)\n const remoteName = process.env['CONTENTRAIN_REMOTE'] ?? 'origin'\n\n const baseBranch = process.env['CONTENTRAIN_BRANCH']\n ?? config?.repository?.default_branch\n ?? ((await git.raw(['branch', '--show-current'])).trim() || 'main')\n\n const currentBranch = (await git.raw(['branch', '--show-current']).catch(() => '')).trim()\n if (currentBranch === CONTENTRAIN_BRANCH) {\n throw Object.assign(new Error(\n `The '${CONTENTRAIN_BRANCH}' branch is checked out in your working directory. `\n + `Switch to your working branch and retry.`,\n ), {\n code: 'CONTENT_BRANCH_CHECKED_OUT',\n agent_hint: 'Ask the developer to switch to their working branch, then retry.',\n developer_action: `git checkout ${baseBranch}`,\n })\n }\n\n await ensureContentBranch(projectRoot)\n\n let hasRemote = false\n try {\n const remotes = await git.getRemotes()\n hasRemote = remotes.some(r => r.name === remoteName)\n } catch {\n hasRemote = false\n }\n if (hasRemote) {\n const net = networkGit(projectRoot, NETWORK_TIMEOUT_MS)\n await Promise.all([\n net.fetch(remoteName, baseBranch).catch(() => {}),\n net.fetch(remoteName, CONTENTRAIN_BRANCH).catch(() => {}),\n ])\n }\n\n const [contentrainTip, baseTip] = await Promise.all([\n git.raw(['rev-parse', CONTENTRAIN_BRANCH]).then(s => s.trim()),\n git.raw(['rev-parse', baseBranch]).then(s => s.trim()),\n ])\n\n let mergeBase: string | null = null\n try {\n mergeBase = (await git.raw(['merge-base', CONTENTRAIN_BRANCH, baseBranch])).trim() || null\n } catch {\n mergeBase = null\n }\n if (!mergeBase) {\n throw Object.assign(new Error(\n `\"${CONTENTRAIN_BRANCH}\" and \"${baseBranch}\" share no history — reconcile needs a common ancestor.`,\n ), {\n code: 'RECONCILE_NO_MERGE_BASE',\n agent_hint: 'The branches are unrelated. This needs a human decision about which history is canonical.',\n })\n }\n\n const plan = await planReconcile({\n base: new GitRefReader(projectRoot, mergeBase),\n ours: new GitRefReader(projectRoot, CONTENTRAIN_BRANCH),\n theirs: new GitRefReader(projectRoot, baseBranch),\n resolutions: options.resolutions,\n source: options.source ?? 'mcp-local',\n })\n\n // Base already contained in contentrain: there is no divergence. A base\n // merely BEHIND is fast-forwarded on the spot (that is what the ordinary\n // write path would do next anyway).\n if (await isAncestor(git, baseTip, contentrainTip)) {\n if (!dryRun && baseTip !== contentrainTip) {\n await git.raw(['update-ref', `refs/heads/${baseBranch}`, contentrainTip])\n try {\n await git.raw(['read-tree', 'HEAD'])\n } catch {\n try { await git.raw(['reset', 'HEAD']) } catch { /* ignore */ }\n }\n return { action: 'reconciled', plan, base: baseBranch, merge_base: mergeBase, commit: contentrainTip, base_advance: 'advanced' }\n }\n return { action: 'in_sync', plan, base: baseBranch, merge_base: mergeBase }\n }\n\n if (dryRun) {\n return { action: 'preview', plan, base: baseBranch, merge_base: mergeBase }\n }\n if (plan.conflicts.length > 0) {\n return { action: 'conflicts', plan, base: baseBranch, merge_base: mergeBase }\n }\n\n // Execute: real merge in a temp worktree, planner output written on top.\n const worktreePath = join(tmpdir(), `cr-reconcile-${randomUUID()}`)\n await git.raw(['worktree', 'add', worktreePath, CONTENTRAIN_BRANCH])\n const wtGit = createGit(worktreePath, { config: authorConfig() })\n\n try {\n try {\n await wtGit.merge([baseBranch, '--no-commit', '--no-ff'])\n } catch {\n // Conflicts inside the worktree are expected — the planner's output\n // overwrites every content-owned path next. Anything left unmerged\n // after that is genuinely outside the planner's scope.\n }\n\n await applyChangesToWorktree(worktreePath, plan.changes)\n await wtGit.raw(['add', '-A'])\n\n const unmerged = (await wtGit.raw(['ls-files', '-u'])).trim()\n if (unmerged) {\n const paths = [...new Set(unmerged.split('\\n').map(l => l.split('\\t')[1]).filter(Boolean))]\n try { await wtGit.merge(['--abort']) } catch { /* not in merge state */ }\n throw Object.assign(new Error(\n `Reconcile stopped: ${paths.length} non-content file(s) conflict between \"${CONTENTRAIN_BRANCH}\" and \"${baseBranch}\": ${paths.join(', ')}. `\n + `These are outside the content planner's scope and need a manual merge.`,\n ), {\n code: 'RECONCILE_SOURCE_CONFLICT',\n agent_hint: 'Non-content files conflict. Ask the developer to merge the branches manually for those paths.',\n developer_action: `git checkout ${CONTENTRAIN_BRANCH} && git merge ${baseBranch}`,\n })\n }\n\n await wtGit.commit(\n `[contentrain] reconcile: merge ${baseBranch} into ${CONTENTRAIN_BRANCH}`,\n { '--no-verify': null },\n )\n\n const [newTip, previousBaseRef, statusBeforeUpdate] = await Promise.all([\n wtGit.raw(['rev-parse', 'HEAD']).then(s => s.trim()),\n git.raw(['rev-parse', baseBranch]).then(s => s.trim()),\n git.status(),\n ])\n const dirtyFilesBeforeUpdate = new Set(statusBeforeUpdate.files.map(f => f.path))\n\n // The merge commit has baseBranch as a parent, so this is a true\n // fast-forward again — the whole point of the exercise.\n await git.raw(['update-ref', `refs/heads/${baseBranch}`, newTip])\n try {\n await git.raw(['read-tree', 'HEAD'])\n } catch {\n try { await git.raw(['reset', 'HEAD']) } catch { /* ignore */ }\n }\n const sync = await selectiveSync(projectRoot, worktreePath, newTip, previousBaseRef, dirtyFilesBeforeUpdate)\n\n let remotePush: RemotePush = 'no-remote'\n if (hasRemote) {\n remotePush = await pushContentBranches(projectRoot, worktreePath, wtGit, remoteName, baseBranch, true)\n }\n\n return {\n action: 'reconciled',\n plan,\n base: baseBranch,\n merge_base: mergeBase,\n commit: newTip,\n base_advance: 'advanced',\n sync,\n remote_push: remotePush,\n }\n } finally {\n try {\n await git.raw(['worktree', 'remove', worktreePath, '--force'])\n } catch {\n // worktree may already be cleaned up\n }\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"reconcile-EANDIp_T.mjs","names":[],"sources":["../src/git/ref-reader.ts","../src/git/reconcile.ts"],"sourcesContent":["import { type SimpleGit } from 'simple-git'\nimport { createGit } from './identity.js'\nimport type { RepoReader } from '../core/contracts/index.js'\n\n/**\n * GitRefReader — `RepoReader` over an arbitrary local git ref, without a\n * checkout. `git show <ref>:<path>` reads blobs, `ls-tree` lists a tree\n * level, `cat-file -e` answers existence. This is what lets a reconcile\n * planner read three trees (merge-base, contentrain, base branch) side by\n * side while the developer's working tree stays untouched.\n *\n * The ref is fixed at construction; the per-call `ref?` parameter of the\n * `RepoReader` contract is ignored — a bound reader answering for a\n * different ref than it was built for would be a lie. Bind another instance\n * instead.\n */\nexport class GitRefReader implements RepoReader {\n private readonly git: SimpleGit\n\n constructor(\n public readonly projectRoot: string,\n public readonly ref: string,\n ) {\n this.git = createGit(projectRoot)\n }\n\n async readFile(path: string, _ref?: string): Promise<string> {\n return this.git.show([`${this.ref}:${normalize(path)}`])\n }\n\n async listDirectory(path: string, _ref?: string): Promise<string[]> {\n try {\n // Trailing slash = \"list the tree at this path\", bare names via\n // --name-only; ls-tree is non-recursive by default, matching the\n // contract's \"does not recurse\".\n const dir = normalize(path)\n const args = dir && dir !== '.'\n ? ['ls-tree', '--name-only', this.ref, `${dir}/`]\n : ['ls-tree', '--name-only', this.ref]\n const raw = await this.git.raw(args)\n return raw\n .split('\\n')\n .map(line => line.trim())\n .filter(Boolean)\n .map(entry => entry.slice(entry.lastIndexOf('/') + 1))\n } catch {\n return []\n }\n }\n\n async fileExists(path: string, _ref?: string): Promise<boolean> {\n try {\n await this.git.raw(['cat-file', '-e', `${this.ref}:${normalize(path)}`])\n return true\n } catch {\n return false\n }\n }\n}\n\n/** Strip a trailing slash — `<ref>:dir/` and `<ref>:dir` name different things to git. */\nfunction normalize(path: string): string {\n return path.endsWith('/') ? path.slice(0, -1) : path\n}\n","import { join } from 'node:path'\nimport { tmpdir } from 'node:os'\nimport { randomUUID } from 'node:crypto'\nimport { CONTENTRAIN_BRANCH } from '@contentrain/types'\nimport type { BaseAdvance, ConflictResolution, ContextSource, RemotePush, SyncResult } from '@contentrain/types'\nimport { createGit, authorConfig } from './identity.js'\nimport { networkGit } from './branch-lifecycle.js'\nimport { GitRefReader } from './ref-reader.js'\nimport {\n NETWORK_TIMEOUT_MS,\n ensureContentBranch,\n isAncestor,\n pushContentBranches,\n selectiveSync,\n} from './transaction.js'\nimport { readConfig } from '../core/config.js'\nimport { planReconcile } from '../core/ops/reconcile/index.js'\nimport type { ReconcilePlan } from '../core/ops/reconcile/index.js'\nimport { applyChangesToWorktree } from '../core/ops/apply-to-worktree.js'\n\nexport interface ReconcileBranchesOptions {\n /** Preview only — plan, never touch a ref. Default true. */\n dryRun?: boolean\n /** Decisions for previously reported conflicts, matched by id. */\n resolutions?: ConflictResolution[]\n /** Stamped into the regenerated context.json. */\n source?: ContextSource\n}\n\nexport interface ReconcileBranchesResult {\n /**\n * `in_sync` — nothing to reconcile (base already contained in contentrain\n * and no content differences; a base merely behind was fast-forwarded).\n * `preview` — dry-run: the plan says what WOULD happen.\n * `conflicts` — unresolved conflicts remain; nothing was written.\n * `reconciled` — the merge commit landed and the base branch advanced.\n */\n action: 'in_sync' | 'preview' | 'conflicts' | 'reconciled'\n plan: ReconcilePlan\n base: string\n merge_base: string | null\n commit?: string\n base_advance?: BaseAdvance\n sync?: SyncResult\n remote_push?: RemotePush\n}\n\n/**\n * Content-aware reconcile of a diverged `contentrain` ↔ base pair, local\n * executor. Plans with three `GitRefReader`s (no checkout, developer tree\n * untouched), then — when clean or fully resolved — performs a REAL\n * `git merge --no-commit` in a temp worktree and writes the ENTIRE plan\n * output over it before committing: for content-owned paths the planner is\n * authoritative, never git's textual auto-merge (its line-level result can\n * be non-canonical or wrong without ever surfacing as a conflict). Git\n * still resolves everything the planner does not own — source files, lock\n * files — exactly as a manual merge would. The commit joins both parents,\n * so afterwards the base branch is an ancestor again and the ordinary\n * fast-forward advance works.\n *\n * Reconcile never runs inside a write path: it is an explicit, separate\n * step (tool, CLI, or Studio background job).\n */\nexport async function reconcileBranches(\n projectRoot: string,\n options: ReconcileBranchesOptions = {},\n): Promise<ReconcileBranchesResult> {\n const dryRun = options.dryRun ?? true\n const git = createGit(projectRoot)\n const config = await readConfig(projectRoot)\n const remoteName = process.env['CONTENTRAIN_REMOTE'] ?? 'origin'\n\n const baseBranch = process.env['CONTENTRAIN_BRANCH']\n ?? config?.repository?.default_branch\n ?? ((await git.raw(['branch', '--show-current'])).trim() || 'main')\n\n const currentBranch = (await git.raw(['branch', '--show-current']).catch(() => '')).trim()\n if (currentBranch === CONTENTRAIN_BRANCH) {\n throw Object.assign(new Error(\n `The '${CONTENTRAIN_BRANCH}' branch is checked out in your working directory. `\n + `Switch to your working branch and retry.`,\n ), {\n code: 'CONTENT_BRANCH_CHECKED_OUT',\n agent_hint: 'Ask the developer to switch to their working branch, then retry.',\n developer_action: `git checkout ${baseBranch}`,\n })\n }\n\n await ensureContentBranch(projectRoot)\n\n let hasRemote = false\n try {\n const remotes = await git.getRemotes()\n hasRemote = remotes.some(r => r.name === remoteName)\n } catch {\n hasRemote = false\n }\n if (hasRemote) {\n const net = networkGit(projectRoot, NETWORK_TIMEOUT_MS)\n await Promise.all([\n net.fetch(remoteName, baseBranch).catch(() => {}),\n net.fetch(remoteName, CONTENTRAIN_BRANCH).catch(() => {}),\n ])\n }\n\n const [contentrainTip, baseTip] = await Promise.all([\n git.raw(['rev-parse', CONTENTRAIN_BRANCH]).then(s => s.trim()),\n git.raw(['rev-parse', baseBranch]).then(s => s.trim()),\n ])\n\n let mergeBase: string | null = null\n try {\n mergeBase = (await git.raw(['merge-base', CONTENTRAIN_BRANCH, baseBranch])).trim() || null\n } catch {\n mergeBase = null\n }\n if (!mergeBase) {\n throw Object.assign(new Error(\n `\"${CONTENTRAIN_BRANCH}\" and \"${baseBranch}\" share no history — reconcile needs a common ancestor.`,\n ), {\n code: 'RECONCILE_NO_MERGE_BASE',\n agent_hint: 'The branches are unrelated. This needs a human decision about which history is canonical.',\n })\n }\n\n const plan = await planReconcile({\n base: new GitRefReader(projectRoot, mergeBase),\n ours: new GitRefReader(projectRoot, CONTENTRAIN_BRANCH),\n theirs: new GitRefReader(projectRoot, baseBranch),\n resolutions: options.resolutions,\n source: options.source ?? 'mcp-local',\n })\n\n // Base already contained in contentrain: there is no divergence. A base\n // merely BEHIND is fast-forwarded on the spot (that is what the ordinary\n // write path would do next anyway).\n if (await isAncestor(git, baseTip, contentrainTip)) {\n if (!dryRun && baseTip !== contentrainTip) {\n await git.raw(['update-ref', `refs/heads/${baseBranch}`, contentrainTip])\n try {\n await git.raw(['read-tree', 'HEAD'])\n } catch {\n try { await git.raw(['reset', 'HEAD']) } catch { /* ignore */ }\n }\n return { action: 'reconciled', plan, base: baseBranch, merge_base: mergeBase, commit: contentrainTip, base_advance: 'advanced' }\n }\n return { action: 'in_sync', plan, base: baseBranch, merge_base: mergeBase }\n }\n\n if (dryRun) {\n return { action: 'preview', plan, base: baseBranch, merge_base: mergeBase }\n }\n if (plan.conflicts.length > 0) {\n return { action: 'conflicts', plan, base: baseBranch, merge_base: mergeBase }\n }\n\n // Execute: real merge in a temp worktree, planner output written on top.\n const worktreePath = join(tmpdir(), `cr-reconcile-${randomUUID()}`)\n await git.raw(['worktree', 'add', worktreePath, CONTENTRAIN_BRANCH])\n const wtGit = createGit(worktreePath, { config: authorConfig() })\n\n try {\n try {\n await wtGit.merge([baseBranch, '--no-commit', '--no-ff'])\n } catch {\n // Conflicts inside the worktree are expected — the planner's output\n // overwrites every content-owned path next. Anything left unmerged\n // after that is genuinely outside the planner's scope.\n }\n\n await applyChangesToWorktree(worktreePath, plan.changes)\n await wtGit.raw(['add', '-A'])\n\n const unmerged = (await wtGit.raw(['ls-files', '-u'])).trim()\n if (unmerged) {\n const paths = [...new Set(unmerged.split('\\n').map(l => l.split('\\t')[1]).filter(Boolean))]\n try { await wtGit.merge(['--abort']) } catch { /* not in merge state */ }\n throw Object.assign(new Error(\n `Reconcile stopped: ${paths.length} non-content file(s) conflict between \"${CONTENTRAIN_BRANCH}\" and \"${baseBranch}\": ${paths.join(', ')}. `\n + `These are outside the content planner's scope and need a manual merge.`,\n ), {\n code: 'RECONCILE_SOURCE_CONFLICT',\n agent_hint: 'Non-content files conflict. Ask the developer to merge the branches manually for those paths.',\n developer_action: `git checkout ${CONTENTRAIN_BRANCH} && git merge ${baseBranch}`,\n })\n }\n\n await wtGit.commit(\n `[contentrain] reconcile: merge ${baseBranch} into ${CONTENTRAIN_BRANCH}`,\n { '--no-verify': null },\n )\n\n const [newTip, previousBaseRef, statusBeforeUpdate] = await Promise.all([\n wtGit.raw(['rev-parse', 'HEAD']).then(s => s.trim()),\n git.raw(['rev-parse', baseBranch]).then(s => s.trim()),\n git.status(),\n ])\n const dirtyFilesBeforeUpdate = new Set(statusBeforeUpdate.files.map(f => f.path))\n\n // The merge commit has baseBranch as a parent, so this is a true\n // fast-forward again — the whole point of the exercise.\n await git.raw(['update-ref', `refs/heads/${baseBranch}`, newTip])\n try {\n await git.raw(['read-tree', 'HEAD'])\n } catch {\n try { await git.raw(['reset', 'HEAD']) } catch { /* ignore */ }\n }\n const sync = await selectiveSync(projectRoot, worktreePath, newTip, previousBaseRef, dirtyFilesBeforeUpdate)\n\n let remotePush: RemotePush = 'no-remote'\n if (hasRemote) {\n remotePush = await pushContentBranches(projectRoot, worktreePath, wtGit, remoteName, baseBranch, true)\n }\n\n return {\n action: 'reconciled',\n plan,\n base: baseBranch,\n merge_base: mergeBase,\n commit: newTip,\n base_advance: 'advanced',\n sync,\n remote_push: remotePush,\n }\n } finally {\n try {\n await git.raw(['worktree', 'remove', worktreePath, '--force'])\n } catch {\n // worktree may already be cleaned up\n }\n }\n}\n"],"mappings":"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"}
|
|
@@ -1214,6 +1214,7 @@ async function listTextFiles(reader, root, depth = 0) {
|
|
|
1214
1214
|
const CONTEXT_PATH = ".contentrain/context.json";
|
|
1215
1215
|
const VOCABULARY_PATH = ".contentrain/vocabulary.json";
|
|
1216
1216
|
const SOURCES_PATH = ".contentrain/normalize-sources.json";
|
|
1217
|
+
const CONFIG_PATH = ".contentrain/config.json";
|
|
1217
1218
|
const MAX_VALIDATION_ADVISORIES = 15;
|
|
1218
1219
|
/**
|
|
1219
1220
|
* Content-aware three-way merge of `.contentrain/**` (plus `content_path`
|
|
@@ -1405,6 +1406,7 @@ async function planReconcile(input) {
|
|
|
1405
1406
|
}));
|
|
1406
1407
|
claimed.add(VOCABULARY_PATH);
|
|
1407
1408
|
claimed.add(SOURCES_PATH);
|
|
1409
|
+
claimed.add(CONFIG_PATH);
|
|
1408
1410
|
{
|
|
1409
1411
|
const [base, ours, theirs] = await Promise.all(SIDES.map((side) => readJsonOrNull(readers[side], VOCABULARY_PATH)));
|
|
1410
1412
|
if (base !== null || ours !== null || theirs !== null) {
|
|
@@ -1447,6 +1449,27 @@ async function planReconcile(input) {
|
|
|
1447
1449
|
});
|
|
1448
1450
|
}
|
|
1449
1451
|
}
|
|
1452
|
+
{
|
|
1453
|
+
const [base, ours, theirs] = await Promise.all(SIDES.map((side) => readJsonOrNull(readers[side], CONFIG_PATH)));
|
|
1454
|
+
if (base !== null || ours !== null || theirs !== null) {
|
|
1455
|
+
const merge = mergeKeyedJson({
|
|
1456
|
+
base: base ?? void 0,
|
|
1457
|
+
ours: ours ?? void 0,
|
|
1458
|
+
theirs: theirs ?? void 0
|
|
1459
|
+
}, {
|
|
1460
|
+
outPath: CONFIG_PATH,
|
|
1461
|
+
kind: "file",
|
|
1462
|
+
resolutions
|
|
1463
|
+
});
|
|
1464
|
+
conflicts.push(...merge.conflicts);
|
|
1465
|
+
stats = addStats(stats, merge.stats);
|
|
1466
|
+
outputs.push({
|
|
1467
|
+
path: CONFIG_PATH,
|
|
1468
|
+
format: "json",
|
|
1469
|
+
value: merge.merged ?? null
|
|
1470
|
+
});
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1450
1473
|
const roots = new Set([".contentrain"]);
|
|
1451
1474
|
for (const state of modelStates) for (const side of SIDES) {
|
|
1452
1475
|
const contentPath = state.three[side]?.content_path;
|
|
@@ -1701,4 +1724,4 @@ function parseJson(raw) {
|
|
|
1701
1724
|
//#endregion
|
|
1702
1725
|
export { applyChangesToWorktree as n, planReconcile as t };
|
|
1703
1726
|
|
|
1704
|
-
//# sourceMappingURL=reconcile-
|
|
1727
|
+
//# sourceMappingURL=reconcile-TyPQKjme.mjs.map
|