@contentrain/mcp 3.0.3 → 3.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -3
- package/dist/{annotations-C-tQVmot.mjs → annotations-CXHPnS-M.mjs} +8 -1
- package/dist/annotations-CXHPnS-M.mjs.map +1 -0
- package/dist/{apply-manager-BybvnUVC.mjs → apply-manager-DsNq7Zhm.mjs} +7 -7
- package/dist/{apply-manager-BybvnUVC.mjs.map → apply-manager-DsNq7Zhm.mjs.map} +1 -1
- package/dist/{astro-parser-Cb9uuBpW.mjs → astro-parser-dhFgc2wA.mjs} +3 -3
- package/dist/{astro-parser-Cb9uuBpW.mjs.map → astro-parser-dhFgc2wA.mjs.map} +1 -1
- package/dist/{availability-Dgcmvv99.mjs → availability-DYHNoP0_.mjs} +5 -1
- package/dist/availability-DYHNoP0_.mjs.map +1 -0
- package/dist/{branch-lifecycle-DhzHfzs3.d.mts → branch-lifecycle-DGlgjnC7.d.mts} +32 -2
- package/dist/branch-lifecycle-DGlgjnC7.d.mts.map +1 -0
- package/dist/{branch-lifecycle-B6PXZ2N7.mjs → branch-lifecycle-DUOoyNWn.mjs} +32 -4
- package/dist/branch-lifecycle-DUOoyNWn.mjs.map +1 -0
- package/dist/{compiler-B6hNQeCC.mjs → compiler-CR9F0wZZ.mjs} +2 -2
- package/dist/{compiler-B6hNQeCC.mjs.map → compiler-CR9F0wZZ.mjs.map} +1 -1
- package/dist/{compiler-sfc.cjs-8yY7b5bx.mjs → compiler-sfc.cjs-Cpexs92i.mjs} +2 -2
- package/dist/{compiler-sfc.cjs-8yY7b5bx.mjs.map → compiler-sfc.cjs-Cpexs92i.mjs.map} +1 -1
- package/dist/{config-VlRKRwAS.mjs → config-B8a4JBT1.mjs} +2 -2
- package/dist/{config-VlRKRwAS.mjs.map → config-B8a4JBT1.mjs.map} +1 -1
- package/dist/{content-manager-Dn4tqDw1.d.mts → content-manager-Bx94DXSF.d.mts} +2 -2
- package/dist/{content-manager-Dn4tqDw1.d.mts.map → content-manager-Bx94DXSF.d.mts.map} +1 -1
- package/dist/{context-C3x6I3cE.mjs → context-CxpTATh4.mjs} +7 -7
- package/dist/context-CxpTATh4.mjs.map +1 -0
- package/dist/core/apply-manager.mjs +10 -11
- package/dist/core/config.d.mts +1 -1
- package/dist/core/config.mjs +2 -2
- package/dist/core/content-manager.d.mts +1 -1
- package/dist/core/content-manager.mjs +3 -3
- package/dist/core/context.d.mts +1 -1
- package/dist/core/context.mjs +5 -6
- package/dist/core/contracts/index.d.mts +1 -1
- package/dist/core/doctor.mjs +8 -8
- package/dist/core/graph-builder.mjs +3 -3
- package/dist/core/meta-manager.mjs +2 -2
- package/dist/core/model-manager.d.mts +1 -1
- package/dist/core/model-manager.mjs +3 -3
- package/dist/core/ops/index.d.mts +17 -29
- package/dist/core/ops/index.d.mts.map +1 -1
- package/dist/core/ops/index.mjs +9 -6
- package/dist/core/overlay-reader.d.mts +1 -1
- package/dist/core/overlay-reader.mjs +1 -1
- package/dist/core/scan-config.mjs +2 -2
- package/dist/core/scanner.mjs +4 -4
- package/dist/core/validator/index.d.mts +1 -1
- package/dist/core/validator/index.mjs +5 -5
- package/dist/{detect-B0oXbQaP.mjs → detect-CI32ynpq.mjs} +2 -2
- package/dist/{detect-B0oXbQaP.mjs.map → detect-CI32ynpq.mjs.map} +1 -1
- package/dist/{doctor-DNPuGgcg.mjs → doctor-B7G0oFxO.mjs} +7 -7
- package/dist/{doctor-DNPuGgcg.mjs.map → doctor-B7G0oFxO.mjs.map} +1 -1
- package/dist/errors-BmLE-JSC.mjs +55 -0
- package/dist/errors-BmLE-JSC.mjs.map +1 -0
- package/dist/{errors-CvBS12e7.mjs → errors-DLkAWnzQ.mjs} +1 -1
- package/dist/{errors-CvBS12e7.mjs.map → errors-DLkAWnzQ.mjs.map} +1 -1
- package/dist/{fs-DLbVB-Ek.mjs → fs-DJv5G8uL.mjs} +1 -1
- package/dist/{fs-DLbVB-Ek.mjs.map → fs-DJv5G8uL.mjs.map} +1 -1
- package/dist/git/branch-lifecycle.d.mts +2 -2
- package/dist/git/branch-lifecycle.mjs +5 -5
- package/dist/git/errors.d.mts +30 -0
- package/dist/git/errors.d.mts.map +1 -0
- package/dist/git/errors.mjs +2 -0
- package/dist/git/identity.mjs +1 -1
- package/dist/git/reconcile.d.mts +49 -0
- package/dist/git/reconcile.d.mts.map +1 -0
- package/dist/git/reconcile.mjs +13 -0
- package/dist/git/transaction.d.mts +60 -15
- package/dist/git/transaction.d.mts.map +1 -1
- package/dist/git/transaction.mjs +10 -11
- package/dist/{graph-builder-BEocwW13.mjs → graph-builder-C9NS7y-a.mjs} +3 -3
- package/dist/{graph-builder-BEocwW13.mjs.map → graph-builder-C9NS7y-a.mjs.map} +1 -1
- package/dist/{id-BjhaIxTL.mjs → id-CjGMPP1k.mjs} +2 -2
- package/dist/{id-BjhaIxTL.mjs.map → id-CjGMPP1k.mjs.map} +1 -1
- package/dist/{identity-D8QoGxhT.mjs → identity-CxAB9Qb8.mjs} +1 -1
- package/dist/{identity-D8QoGxhT.mjs.map → identity-CxAB9Qb8.mjs.map} +1 -1
- package/dist/index-_HJVkDJa.d.mts +85 -0
- package/dist/index-_HJVkDJa.d.mts.map +1 -0
- package/dist/index.mjs +24 -22
- package/dist/index.mjs.map +1 -1
- package/dist/{local-Db5X9mjZ.mjs → local-bnn6yrG7.mjs} +83 -8
- package/dist/local-bnn6yrG7.mjs.map +1 -0
- package/dist/{meta-manager-B_Se183X.mjs → meta-manager-g3shoZES.mjs} +2 -2
- package/dist/{meta-manager-B_Se183X.mjs.map → meta-manager-g3shoZES.mjs.map} +1 -1
- package/dist/{model-manager-DgyKMjLN.mjs → model-manager-DqwL8ptm.mjs} +66 -5
- package/dist/model-manager-DqwL8ptm.mjs.map +1 -0
- package/dist/{node-DUGiIkv1.mjs → node-CpLm3qr6.mjs} +1 -1
- package/dist/{node-DUGiIkv1.mjs.map → node-CpLm3qr6.mjs.map} +1 -1
- package/dist/{ops-BzbThweu.mjs → ops-BfrdZ6GK.mjs} +24 -34
- package/dist/ops-BfrdZ6GK.mjs.map +1 -0
- package/dist/{overlay-reader-CkU_pGXZ.mjs → overlay-reader-Bx1YxGXq.mjs} +1 -1
- package/dist/{overlay-reader-CkU_pGXZ.mjs.map → overlay-reader-Bx1YxGXq.mjs.map} +1 -1
- package/dist/{paths-CdPPWpbG.mjs → paths-C6_C1gLp.mjs} +1 -1
- package/dist/{paths-CdPPWpbG.mjs.map → paths-C6_C1gLp.mjs.map} +1 -1
- package/dist/providers/github/index.d.mts +10 -1
- package/dist/providers/github/index.d.mts.map +1 -1
- package/dist/providers/github/index.mjs +104 -4
- package/dist/providers/github/index.mjs.map +1 -1
- package/dist/providers/gitlab/index.d.mts +8 -1
- package/dist/providers/gitlab/index.d.mts.map +1 -1
- package/dist/providers/gitlab/index.mjs +15 -2
- package/dist/providers/gitlab/index.mjs.map +1 -1
- package/dist/providers/local/index.d.mts +26 -2
- package/dist/providers/local/index.d.mts.map +1 -1
- package/dist/providers/local/index.mjs +13 -12
- package/dist/reconcile-D5FTVW5z.mjs +1704 -0
- package/dist/reconcile-D5FTVW5z.mjs.map +1 -0
- package/dist/reconcile-M7KBZtdn.mjs +249 -0
- package/dist/reconcile-M7KBZtdn.mjs.map +1 -0
- package/dist/{scan-config-Dx49nCqk.mjs → scan-config-C5zzygeH.mjs} +2 -2
- package/dist/{scan-config-Dx49nCqk.mjs.map → scan-config-C5zzygeH.mjs.map} +1 -1
- package/dist/{scanner-BSvXjfkp.mjs → scanner-CEiT8Tgx.mjs} +7 -7
- package/dist/{scanner-BSvXjfkp.mjs.map → scanner-CEiT8Tgx.mjs.map} +1 -1
- package/dist/server/http/index.d.mts +1 -1
- package/dist/server/http/index.mjs +24 -22
- package/dist/server/http/index.mjs.map +1 -1
- package/dist/{server-B7QA0Q1M.mjs → server-B6abaEU2.mjs} +343 -227
- package/dist/server-B6abaEU2.mjs.map +1 -0
- package/dist/{server-CLPNnhYL.d.mts → server-SDsD0XGM.d.mts} +2 -2
- package/dist/{server-CLPNnhYL.d.mts.map → server-SDsD0XGM.d.mts.map} +1 -1
- package/dist/server.d.mts +1 -1
- package/dist/server.mjs +24 -22
- package/dist/{source-map-generator-DhjiQdV3.mjs → source-map-generator-Clb2Eqse.mjs} +1 -1
- package/dist/{source-map-generator-DhjiQdV3.mjs.map → source-map-generator-Clb2Eqse.mjs.map} +1 -1
- package/dist/{svelte-parser-CwySu9yn.mjs → svelte-parser-CBN7Jxb7.mjs} +3 -3
- package/dist/{svelte-parser-CwySu9yn.mjs.map → svelte-parser-CBN7Jxb7.mjs.map} +1 -1
- package/dist/templates/index.mjs +1 -1
- package/dist/{templates-CAkTIRkE.mjs → templates-BZbohJc8.mjs} +1 -1
- package/dist/{templates-CAkTIRkE.mjs.map → templates-BZbohJc8.mjs.map} +1 -1
- package/dist/testing/memory-provider.d.mts +26 -1
- package/dist/testing/memory-provider.d.mts.map +1 -1
- package/dist/testing/memory-provider.mjs +62 -2
- package/dist/testing/memory-provider.mjs.map +1 -1
- package/dist/tools/annotations.d.mts.map +1 -1
- package/dist/tools/annotations.mjs +1 -1
- package/dist/tools/availability.d.mts.map +1 -1
- package/dist/tools/availability.mjs +1 -1
- package/dist/{transaction-BW9ektWu.mjs → transaction-BxDiiakv.mjs} +128 -64
- package/dist/transaction-BxDiiakv.mjs.map +1 -0
- package/dist/tsx-parser-DOQpJVgK.mjs +2 -0
- package/dist/{tsx-parser-BvsK6lyG.mjs → tsx-parser-go5V9zoc.mjs} +1 -1
- package/dist/{tsx-parser-BvsK6lyG.mjs.map → tsx-parser-go5V9zoc.mjs.map} +1 -1
- package/dist/util/detect.mjs +2 -2
- package/dist/util/fs.mjs +1 -1
- package/dist/util/id.mjs +1 -1
- package/dist/{validator-0sJ9vh1j.mjs → validator-B2GhPM3L.mjs} +5 -6
- package/dist/{validator-0sJ9vh1j.mjs.map → validator-B2GhPM3L.mjs.map} +1 -1
- package/dist/{vue-parser-auedZiJG.mjs → vue-parser-5Mp8w-mb.mjs} +3 -3
- package/dist/{vue-parser-auedZiJG.mjs.map → vue-parser-5Mp8w-mb.mjs.map} +1 -1
- package/package.json +13 -5
- package/dist/annotations-C-tQVmot.mjs.map +0 -1
- package/dist/availability-Dgcmvv99.mjs.map +0 -1
- package/dist/branch-lifecycle-B6PXZ2N7.mjs.map +0 -1
- package/dist/branch-lifecycle-DhzHfzs3.d.mts.map +0 -1
- package/dist/context-C3x6I3cE.mjs.map +0 -1
- package/dist/local-Db5X9mjZ.mjs.map +0 -1
- package/dist/model-manager-DgyKMjLN.mjs.map +0 -1
- package/dist/ops-BzbThweu.mjs.map +0 -1
- package/dist/paths-CmVw5Cw2.mjs +0 -65
- package/dist/paths-CmVw5Cw2.mjs.map +0 -1
- package/dist/serialization-tmgfFBOh.mjs +0 -2
- package/dist/server-B7QA0Q1M.mjs.map +0 -1
- package/dist/transaction-BW9ektWu.mjs.map +0 -1
- package/dist/tsx-parser-Byc7HlJ8.mjs +0 -2
- /package/dist/{index-BWbTJlXm.d.mts → index-ByrVsRJE.d.mts} +0 -0
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { t as readConfig } from "./config-
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { t as branchTimestamp } from "./id-
|
|
1
|
+
import { r as writeContext } from "./context-CxpTATh4.mjs";
|
|
2
|
+
import { t as readConfig } from "./config-B8a4JBT1.mjs";
|
|
3
|
+
import { n as authorConfig, r as createGit } from "./identity-CxAB9Qb8.mjs";
|
|
4
|
+
import { l as networkGit, o as deleteRemoteBranch } from "./branch-lifecycle-DUOoyNWn.mjs";
|
|
5
|
+
import { t as branchTimestamp } from "./id-CjGMPP1k.mjs";
|
|
6
6
|
import { join } from "node:path";
|
|
7
|
+
import { tmpdir } from "node:os";
|
|
8
|
+
import { randomUUID } from "node:crypto";
|
|
7
9
|
import { CONTENTRAIN_BRANCH } from "@contentrain/types";
|
|
8
10
|
import { rm } from "node:fs/promises";
|
|
9
11
|
import "simple-git";
|
|
10
|
-
import { tmpdir } from "node:os";
|
|
11
|
-
import { randomUUID } from "node:crypto";
|
|
12
12
|
//#region src/providers/local/migration.ts
|
|
13
13
|
/**
|
|
14
14
|
* Migration: the first MCP release used `contentrain/*` feature branches.
|
|
@@ -64,6 +64,12 @@ async function migrateLegacyBranches(git, baseBranch) {
|
|
|
64
64
|
}
|
|
65
65
|
//#endregion
|
|
66
66
|
//#region src/git/transaction.ts
|
|
67
|
+
/**
|
|
68
|
+
* Block-timeout for fetch/push inside transactions. These calls inherit the
|
|
69
|
+
* host's credential setup (see networkGit); without a timeout a hung SSH
|
|
70
|
+
* passphrase prompt would hang the MCP call with it.
|
|
71
|
+
*/
|
|
72
|
+
const NETWORK_TIMEOUT_MS = 1e4;
|
|
67
73
|
async function ensureContentBranch(projectRoot) {
|
|
68
74
|
const git = createGit(projectRoot);
|
|
69
75
|
const config = await readConfig(projectRoot);
|
|
@@ -196,7 +202,10 @@ async function createTransaction(projectRoot, branchName, options) {
|
|
|
196
202
|
developer_action: `git checkout ${baseBranch}`
|
|
197
203
|
});
|
|
198
204
|
await ensureContentBranch(projectRoot);
|
|
199
|
-
if (hasRemote)
|
|
205
|
+
if (hasRemote) {
|
|
206
|
+
const net = networkGit(projectRoot, NETWORK_TIMEOUT_MS);
|
|
207
|
+
await Promise.all([net.fetch(remoteName, baseBranch).catch(() => {}), net.fetch(remoteName, CONTENTRAIN_BRANCH).catch(() => {})]);
|
|
208
|
+
}
|
|
200
209
|
const worktreePath = join(tmpdir(), `cr-${randomUUID()}`);
|
|
201
210
|
const branch = branchName;
|
|
202
211
|
await git.raw([
|
|
@@ -252,7 +261,7 @@ async function createTransaction(projectRoot, branchName, options) {
|
|
|
252
261
|
pendingReview = true;
|
|
253
262
|
let warning;
|
|
254
263
|
if (hasRemote) try {
|
|
255
|
-
await
|
|
264
|
+
await networkGit(projectRoot, NETWORK_TIMEOUT_MS).push(remoteName, branch);
|
|
256
265
|
} catch (error) {
|
|
257
266
|
warning = `Changes are committed locally on "${branch}", but pushing to "${remoteName}" failed: ${(error instanceof Error ? error.message : String(error)).trim()}. The branch is intact — run contentrain_submit once you have push access, or merge it locally with contentrain_merge.`;
|
|
258
267
|
}
|
|
@@ -283,43 +292,41 @@ async function createTransaction(projectRoot, branchName, options) {
|
|
|
283
292
|
git.status()
|
|
284
293
|
]);
|
|
285
294
|
const dirtyFilesBeforeUpdate = new Set(statusBeforeUpdate.files.map((f) => f.path));
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
try {
|
|
300
|
-
await git.raw(["reset", "HEAD"]);
|
|
301
|
-
} catch {}
|
|
302
|
-
}
|
|
303
|
-
const sync = await selectiveSync(projectRoot, worktreePath, contentrainTip, previousBaseRef, dirtyFilesBeforeUpdate);
|
|
304
|
-
if (hasRemote) {
|
|
295
|
+
let sync = {
|
|
296
|
+
synced: [],
|
|
297
|
+
skipped: []
|
|
298
|
+
};
|
|
299
|
+
let baseAdvance;
|
|
300
|
+
let divergedWarning;
|
|
301
|
+
if (await isAncestor(git, previousBaseRef, contentrainTip)) {
|
|
302
|
+
baseAdvance = "advanced";
|
|
303
|
+
await git.raw([
|
|
304
|
+
"update-ref",
|
|
305
|
+
`refs/heads/${baseBranch}`,
|
|
306
|
+
contentrainTip
|
|
307
|
+
]);
|
|
305
308
|
try {
|
|
306
|
-
await git.
|
|
309
|
+
await git.raw(["read-tree", "HEAD"]);
|
|
307
310
|
} catch {
|
|
308
311
|
try {
|
|
309
|
-
await
|
|
310
|
-
await wtGit.merge([`${remoteName}/${CONTENTRAIN_BRANCH}`, "--no-edit"]);
|
|
311
|
-
await git.push(remoteName, CONTENTRAIN_BRANCH);
|
|
312
|
+
await git.raw(["reset", "HEAD"]);
|
|
312
313
|
} catch {}
|
|
313
314
|
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
315
|
+
sync = await selectiveSync(projectRoot, worktreePath, contentrainTip, previousBaseRef, dirtyFilesBeforeUpdate);
|
|
316
|
+
} else {
|
|
317
|
+
baseAdvance = "blocked_diverged";
|
|
318
|
+
divergedWarning = `Content is committed and merged into "${CONTENTRAIN_BRANCH}", but "${baseBranch}" has commits that are not in "${CONTENTRAIN_BRANCH}" — the branches have diverged, so "${baseBranch}" was not advanced. Run contentrain_reconcile (CLI: npx contentrain reconcile) to merge them and restore the fast-forward advance.`;
|
|
317
319
|
}
|
|
320
|
+
let remotePush = "no-remote";
|
|
321
|
+
if (hasRemote) remotePush = await pushContentBranches(projectRoot, worktreePath, wtGit, remoteName, baseBranch, baseAdvance === "advanced");
|
|
322
|
+
const warnings = [divergedWarning, sync.warning].filter(Boolean);
|
|
318
323
|
return {
|
|
319
324
|
action: "auto-merged",
|
|
320
325
|
commit: commitHash,
|
|
321
326
|
sync,
|
|
322
|
-
|
|
327
|
+
base_advance: baseAdvance,
|
|
328
|
+
remote_push: remotePush,
|
|
329
|
+
...warnings.length > 0 ? { warning: warnings.join(" ") } : {}
|
|
323
330
|
};
|
|
324
331
|
},
|
|
325
332
|
async cleanup() {
|
|
@@ -347,6 +354,10 @@ async function mergeBranch(projectRoot, branchName) {
|
|
|
347
354
|
} catch {
|
|
348
355
|
hasRemote = false;
|
|
349
356
|
}
|
|
357
|
+
if (hasRemote) {
|
|
358
|
+
const net = networkGit(projectRoot, NETWORK_TIMEOUT_MS);
|
|
359
|
+
await Promise.all([net.fetch(remoteName, baseBranch).catch(() => {}), net.fetch(remoteName, CONTENTRAIN_BRANCH).catch(() => {})]);
|
|
360
|
+
}
|
|
350
361
|
const worktreePath = join(tmpdir(), `cr-merge-${randomUUID()}`);
|
|
351
362
|
await git.raw([
|
|
352
363
|
"worktree",
|
|
@@ -356,6 +367,27 @@ async function mergeBranch(projectRoot, branchName) {
|
|
|
356
367
|
]);
|
|
357
368
|
const wtGit = createGit(worktreePath, { config: authorConfig() });
|
|
358
369
|
try {
|
|
370
|
+
try {
|
|
371
|
+
await wtGit.merge([baseBranch, "--no-edit"]);
|
|
372
|
+
} catch {
|
|
373
|
+
try {
|
|
374
|
+
await wtGit.merge(["--abort"]);
|
|
375
|
+
} catch {}
|
|
376
|
+
if (hasRemote) try {
|
|
377
|
+
await wtGit.merge([`${remoteName}/${baseBranch}`, "--no-edit"]);
|
|
378
|
+
} catch {
|
|
379
|
+
try {
|
|
380
|
+
await wtGit.merge(["--abort"]);
|
|
381
|
+
} catch {}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
if (hasRemote) try {
|
|
385
|
+
await wtGit.merge([`${remoteName}/${CONTENTRAIN_BRANCH}`, "--no-edit"]);
|
|
386
|
+
} catch {
|
|
387
|
+
try {
|
|
388
|
+
await wtGit.merge(["--abort"]);
|
|
389
|
+
} catch {}
|
|
390
|
+
}
|
|
359
391
|
try {
|
|
360
392
|
await wtGit.merge([branchName, "--no-edit"]);
|
|
361
393
|
} catch {
|
|
@@ -378,45 +410,44 @@ async function mergeBranch(projectRoot, branchName) {
|
|
|
378
410
|
git.status()
|
|
379
411
|
]);
|
|
380
412
|
const dirtyFilesBeforeUpdate = new Set(statusBeforeUpdate.files.map((f) => f.path));
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
try {
|
|
395
|
-
await git.raw(["reset", "HEAD"]);
|
|
396
|
-
} catch {}
|
|
397
|
-
}
|
|
398
|
-
const sync = await selectiveSync(projectRoot, worktreePath, contentrainTip, previousBaseRef, dirtyFilesBeforeUpdate);
|
|
399
|
-
if (hasRemote) {
|
|
413
|
+
let sync = {
|
|
414
|
+
synced: [],
|
|
415
|
+
skipped: []
|
|
416
|
+
};
|
|
417
|
+
let baseAdvance;
|
|
418
|
+
let divergedWarning;
|
|
419
|
+
if (await isAncestor(git, previousBaseRef, contentrainTip)) {
|
|
420
|
+
baseAdvance = "advanced";
|
|
421
|
+
await git.raw([
|
|
422
|
+
"update-ref",
|
|
423
|
+
`refs/heads/${baseBranch}`,
|
|
424
|
+
contentrainTip
|
|
425
|
+
]);
|
|
400
426
|
try {
|
|
401
|
-
await git.
|
|
427
|
+
await git.raw(["read-tree", "HEAD"]);
|
|
402
428
|
} catch {
|
|
403
429
|
try {
|
|
404
|
-
await
|
|
405
|
-
await wtGit.merge([`${remoteName}/${CONTENTRAIN_BRANCH}`, "--no-edit"]);
|
|
406
|
-
await git.push(remoteName, CONTENTRAIN_BRANCH);
|
|
430
|
+
await git.raw(["reset", "HEAD"]);
|
|
407
431
|
} catch {}
|
|
408
432
|
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
433
|
+
sync = await selectiveSync(projectRoot, worktreePath, contentrainTip, previousBaseRef, dirtyFilesBeforeUpdate);
|
|
434
|
+
} else {
|
|
435
|
+
baseAdvance = "blocked_diverged";
|
|
436
|
+
divergedWarning = `Branch "${branchName}" is merged into "${CONTENTRAIN_BRANCH}", but "${baseBranch}" has commits that are not in "${CONTENTRAIN_BRANCH}" — the branches have diverged, so "${baseBranch}" was not advanced. Run contentrain_reconcile (CLI: npx contentrain reconcile) to merge them and restore the fast-forward advance.`;
|
|
412
437
|
}
|
|
438
|
+
let remotePush = "no-remote";
|
|
439
|
+
if (hasRemote) remotePush = await pushContentBranches(projectRoot, worktreePath, wtGit, remoteName, baseBranch, baseAdvance === "advanced");
|
|
413
440
|
await safeDeleteBranch(git, branchName);
|
|
414
441
|
let remote;
|
|
415
442
|
if (hasRemote) remote = await deleteRemoteBranch(projectRoot, branchName, { config });
|
|
443
|
+
const warnings = [divergedWarning, sync.warning].filter(Boolean);
|
|
416
444
|
return {
|
|
417
445
|
action: "merged",
|
|
418
446
|
commit: contentrainTip,
|
|
419
447
|
sync,
|
|
448
|
+
base_advance: baseAdvance,
|
|
449
|
+
remote_push: remotePush,
|
|
450
|
+
...warnings.length > 0 ? { warning: warnings.join(" ") } : {},
|
|
420
451
|
...remote ? { remote } : {}
|
|
421
452
|
};
|
|
422
453
|
} finally {
|
|
@@ -460,6 +491,39 @@ async function isAncestor(git, ancestor, descendant) {
|
|
|
460
491
|
}
|
|
461
492
|
}
|
|
462
493
|
/**
|
|
494
|
+
* Push the contentrain branch (with one fetch-merge-retry) and, when the base
|
|
495
|
+
* advanced, the base branch. Returns the push outcome for contentrain; the
|
|
496
|
+
* base push is best-effort and never demotes the outcome, because the
|
|
497
|
+
* contentrain branch is the content SSOT — it is what the CDN reads.
|
|
498
|
+
*
|
|
499
|
+
* The retry absorbs a concurrent remote advance (another writer pushed
|
|
500
|
+
* contentrain first): fetch, merge the remote tip into the local worktree's
|
|
501
|
+
* contentrain, push again. A conflicting retry-merge is aborted and reported
|
|
502
|
+
* as `rejected` — that is a real divergence, not a race.
|
|
503
|
+
*/
|
|
504
|
+
async function pushContentBranches(projectRoot, worktreePath, wtGit, remoteName, baseBranch, baseAdvanced) {
|
|
505
|
+
const net = networkGit(projectRoot, NETWORK_TIMEOUT_MS);
|
|
506
|
+
let outcome = "pushed";
|
|
507
|
+
try {
|
|
508
|
+
await net.push(remoteName, CONTENTRAIN_BRANCH);
|
|
509
|
+
} catch {
|
|
510
|
+
try {
|
|
511
|
+
await networkGit(worktreePath, NETWORK_TIMEOUT_MS).fetch(remoteName, CONTENTRAIN_BRANCH);
|
|
512
|
+
await wtGit.merge([`${remoteName}/${CONTENTRAIN_BRANCH}`, "--no-edit"]);
|
|
513
|
+
await net.push(remoteName, CONTENTRAIN_BRANCH);
|
|
514
|
+
} catch {
|
|
515
|
+
try {
|
|
516
|
+
await wtGit.merge(["--abort"]);
|
|
517
|
+
} catch {}
|
|
518
|
+
outcome = "rejected";
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
if (baseAdvanced) try {
|
|
522
|
+
await net.push(remoteName, baseBranch);
|
|
523
|
+
} catch {}
|
|
524
|
+
return outcome;
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
463
527
|
* Force-delete a local branch, swallowing all errors. Never deletes the
|
|
464
528
|
* singleton `contentrain` branch. Used to prune feature branches after they
|
|
465
529
|
* are merged (auto-merge / contentrain_merge) or when a transaction fails
|
|
@@ -490,6 +554,6 @@ async function regenerateContextOnContentrain(wtGit, worktreePath, contextUpdate
|
|
|
490
554
|
} catch {}
|
|
491
555
|
}
|
|
492
556
|
//#endregion
|
|
493
|
-
export {
|
|
557
|
+
export { isAncestor as a, selectiveSync as c, ensureContentBranch as i, buildBranchName as n, mergeBranch as o, createTransaction as r, pushContentBranches as s, NETWORK_TIMEOUT_MS as t };
|
|
494
558
|
|
|
495
|
-
//# sourceMappingURL=transaction-
|
|
559
|
+
//# sourceMappingURL=transaction-BxDiiakv.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transaction-BxDiiakv.mjs","names":["removeFile"],"sources":["../src/providers/local/migration.ts","../src/git/transaction.ts"],"sourcesContent":["import type { SimpleGit } from 'simple-git'\n\n/**\n * Migration: the first MCP release used `contentrain/*` feature branches.\n * Once we introduced the singleton `contentrain` branch (tracking the\n * committed content state), those old feature branches became a ref-\n * namespace conflict — git cannot hold both `contentrain` (a leaf ref)\n * and `contentrain/foo` (implying a directory) simultaneously.\n *\n * `migrateLegacyBranches` removes the old-prefix branches so the\n * singleton `contentrain` ref can be created. It is idempotent and\n * safe to call before every `ensureContentBranch` run.\n *\n * Strategy:\n * 1. Delete merged `contentrain/*` branches first (`-d`). Their commits\n * are already on the base branch via the old auto-merge flow.\n * 2. Force-delete whatever remains (`-D`). Any unmerged leftover is\n * from an abandoned or partially-committed legacy branch — content\n * on `main` always wins, and the singleton `contentrain` branch is\n * about to be created from `main`/`baseBranch` anyway.\n *\n * Returns the number of branches that were deleted. Callers may log it;\n * the git transaction layer does not need the count for correctness.\n */\nexport async function migrateLegacyBranches(\n git: SimpleGit,\n baseBranch: string,\n): Promise<number> {\n const branches = await git.branchLocal()\n const oldPrefixBranches = branches.all.filter(b => b.startsWith('contentrain/'))\n if (oldPrefixBranches.length === 0) return 0\n\n let deleted = 0\n\n // 1) Delete merged legacy branches first — the safe path.\n let mergedLegacy: string[] = []\n try {\n const mergedOutput = await git.raw(['branch', '--merged', baseBranch])\n mergedLegacy = mergedOutput.split('\\n')\n .map(b => b.trim().replace(/^\\*\\s*/, ''))\n .filter(b => b.startsWith('contentrain/'))\n } catch {\n // `branch --merged` fails before baseBranch exists — fall through.\n }\n\n for (const b of mergedLegacy) {\n try {\n await git.raw(['branch', '-d', b])\n deleted++\n } catch {\n // Branch may be protected or already gone — safe to skip.\n }\n }\n\n // 2) Force-delete any unmerged legacy branches still present.\n const remaining = (await git.branchLocal()).all.filter(b => b.startsWith('contentrain/'))\n for (const b of remaining) {\n try {\n await git.raw(['branch', '-D', b])\n deleted++\n } catch {\n // Skip — best-effort cleanup.\n }\n }\n\n return deleted\n}\n","import { type SimpleGit } from 'simple-git'\nimport { createGit } from '../git/identity.js'\nimport { join } from 'node:path'\nimport { rm as removeFile } from 'node:fs/promises'\nimport { tmpdir } from 'node:os'\nimport { randomUUID } from 'node:crypto'\nimport { readConfig } from '../core/config.js'\nimport { writeContext } from '../core/context.js'\nimport { deleteRemoteBranch, networkGit, type RemoteDeleteResult } from './branch-lifecycle.js'\nimport { authorConfig } from './identity.js'\nimport { branchTimestamp } from '../util/id.js'\nimport { migrateLegacyBranches } from '../providers/local/migration.js'\nimport type { BaseAdvance, RemotePush, SyncResult, WorkflowMode } from '@contentrain/types'\nimport { CONTENTRAIN_BRANCH } from '@contentrain/types'\n\n/**\n * Block-timeout for fetch/push inside transactions. These calls inherit the\n * host's credential setup (see networkGit); without a timeout a hung SSH\n * passphrase prompt would hang the MCP call with it.\n */\nexport const NETWORK_TIMEOUT_MS = 10_000\n\nexport interface ContextUpdate {\n tool: string\n model: string\n locale?: string\n entries?: string[]\n}\n\n/**\n * Result of completing a write transaction.\n *\n * A diverged base branch is NOT an error: the content is committed and merged\n * into the contentrain branch either way — only the base fast-forward is\n * pending. `base_advance: 'blocked_diverged'` reports that truthfully instead\n * of failing a write that in fact landed.\n */\nexport interface CompleteResult {\n action: 'auto-merged' | 'pending-review'\n commit: string\n sync?: SyncResult\n warning?: string\n /** Present on `auto-merged`: did the base branch advance to the contentrain tip? */\n base_advance?: BaseAdvance\n /** Present on `auto-merged`: outcome of pushing the contentrain branch. */\n remote_push?: RemotePush\n}\n\n/** Result of landing a feature branch via {@link mergeBranch}. */\nexport interface MergeBranchResult {\n action: 'merged'\n commit: string\n sync: SyncResult\n base_advance: BaseAdvance\n remote_push: RemotePush\n warning?: string\n remote?: RemoteDeleteResult\n}\n\nexport interface GitTransaction {\n worktree: string\n branch: string\n write(callback: (worktreePath: string) => Promise<void>): Promise<void>\n commit(message: string, contextUpdate?: ContextUpdate): Promise<string>\n complete(): Promise<CompleteResult>\n cleanup(): Promise<void>\n}\n\nexport async function ensureContentBranch(projectRoot: string): Promise<void> {\n const git = createGit(projectRoot)\n const config = await readConfig(projectRoot)\n\n // Check if contentrain branch exists locally\n const branches = await git.branchLocal()\n if (branches.all.includes(CONTENTRAIN_BRANCH)) return\n\n // Detect base branch\n const baseBranch = config?.repository?.default_branch\n || (await git.raw(['branch', '--show-current'])).trim()\n || 'main'\n\n // Clean up legacy `contentrain/*` feature branches so the singleton\n // `contentrain` ref can be created. Idempotent — safe to call even\n // when no legacy branches exist.\n await migrateLegacyBranches(git, baseBranch)\n\n // Create contentrain branch from base\n await git.branch([CONTENTRAIN_BRANCH, baseBranch])\n\n // Push to remote if exists\n const remoteName = process.env['CONTENTRAIN_REMOTE'] ?? 'origin'\n try {\n const remotes = await git.getRemotes()\n if (remotes.some(r => r.name === remoteName)) {\n await git.push(['-u', remoteName, CONTENTRAIN_BRANCH])\n }\n } catch {\n // Remote push is best-effort\n }\n}\n\nexport async function selectiveSync(\n projectRoot: string,\n _worktreePath: string,\n contentrainTip: string,\n _previousBaseRef?: string,\n dirtyFilesBeforeUpdate?: Set<string>,\n): Promise<SyncResult> {\n const git = createGit(projectRoot)\n const synced: string[] = []\n const skipped: string[] = []\n\n // Use git plumbing to find ALL files that differ between old and new commits.\n // diff-tree is fast and ignores working tree / index state entirely.\n // Not limited to .contentrain/ — some ops also modify .gitignore, etc.\n const compareRef = _previousBaseRef ?? contentrainTip\n let changedFiles: string[] = []\n try {\n const diffOutput = await git.raw([\n 'diff-tree', '--name-only', '-r', '--no-commit-id',\n compareRef, contentrainTip,\n ])\n changedFiles = diffOutput.split('\\n').filter(f => f.trim().length > 0)\n } catch {\n // Fallback: list .contentrain/ files from the contentrainTip commit\n try {\n const lsOutput = await git.raw(['ls-tree', '-r', '--name-only', contentrainTip, '.contentrain/'])\n changedFiles = lsOutput.split('\\n').filter(f => f.trim().length > 0)\n } catch {\n return { synced, skipped }\n }\n }\n\n if (changedFiles.length === 0) return { synced, skipped }\n\n // Use pre-captured dirty files (before update-ref) to avoid false positives.\n // After update-ref, files appear as \"modified\" in status even though the developer\n // didn't touch them. We use the pre-update state to know what was truly dirty.\n const dirtyFiles = dirtyFilesBeforeUpdate ?? new Set<string>()\n\n // Which changed files still exist in contentrainTip (HEAD after update-ref)?\n // ONE `ls-tree` over the changed paths lists exactly the survivors, instead\n // of a `cat-file -e` spawn per file. Falls back to per-file probing if\n // ls-tree fails so behavior is preserved on any edge.\n const filesInTip = new Set<string>()\n try {\n const lsOutput = await git.raw(['ls-tree', '-r', '--name-only', contentrainTip, '--', ...changedFiles])\n for (const f of lsOutput.split('\\n')) {\n const trimmed = f.trim()\n if (trimmed) filesInTip.add(trimmed)\n }\n } catch {\n for (const file of changedFiles) {\n try {\n await git.raw(['cat-file', '-e', `${contentrainTip}:${file}`])\n filesInTip.add(file)\n } catch {\n // File does not exist in tip (was deleted)\n }\n }\n }\n\n // Partition: dirty developer files are skipped; survivors get checked out\n // from HEAD; the rest were deleted in the new HEAD and are removed on disk.\n const toCheckout: string[] = []\n const toRemove: string[] = []\n for (const file of changedFiles) {\n if (dirtyFiles.has(file)) skipped.push(file)\n else if (filesInTip.has(file)) toCheckout.push(file)\n else toRemove.push(file)\n }\n\n // ONE `git checkout HEAD -- f1 f2 …` restores every clean survivor at once.\n // On failure, fall back to per-file so a single unresolvable path still\n // yields precise skip accounting (dirty files were already excluded).\n if (toCheckout.length > 0) {\n try {\n await git.checkout(['HEAD', '--', ...toCheckout])\n synced.push(...toCheckout)\n } catch {\n for (const file of toCheckout) {\n try {\n await git.checkout(['HEAD', '--', file])\n synced.push(file)\n } catch {\n skipped.push(file)\n }\n }\n }\n }\n\n // Deletions are working-tree fs removals — no git spawn, safe to parallelize.\n await Promise.all(toRemove.map(async (file) => {\n try {\n await removeFile(join(projectRoot, file), { force: true })\n synced.push(file)\n } catch {\n skipped.push(file)\n }\n }))\n\n const warning = skipped.length > 0\n ? `${skipped.length} file(s) skipped due to local changes: ${skipped.join(', ')}. Commit your changes, then run: git checkout HEAD -- ${skipped.join(' ')}`\n : undefined\n\n return { synced, skipped, warning }\n}\n\nexport async function createTransaction(\n projectRoot: string,\n branchName: string,\n options?: { workflowOverride?: WorkflowMode },\n): Promise<GitTransaction> {\n const git = createGit(projectRoot)\n const config = await readConfig(projectRoot)\n const workflow = options?.workflowOverride ?? config?.workflow ?? 'auto-merge'\n\n const remoteName = process.env['CONTENTRAIN_REMOTE'] ?? 'origin'\n\n // Detect base branch + current branch + remote in ONE batch\n // (reduces subprocess spawns from 4 to 2)\n let baseBranch = process.env['CONTENTRAIN_BRANCH'] ?? config?.repository?.default_branch ?? ''\n let currentBranch = ''\n let hasRemote = false\n\n const [branchResult, remotes] = await Promise.all([\n git.raw(['branch', '--show-current']).catch(() => ''),\n git.getRemotes().catch(() => []),\n ])\n currentBranch = branchResult.trim()\n if (!baseBranch) baseBranch = currentBranch || 'main'\n hasRemote = (remotes as { name: string }[]).some(r => r.name === remoteName)\n\n // Check if developer is on contentrain branch\n if (currentBranch === CONTENTRAIN_BRANCH) {\n throw Object.assign(new Error(\n `The '${CONTENTRAIN_BRANCH}' branch is checked out in your working directory. `\n + `Contentrain manages this branch automatically. `\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 (e.g., main or a feature branch), then retry the operation.',\n developer_action: `git checkout ${baseBranch}`,\n })\n }\n\n // Ensure contentrain branch exists (with migration for old contentrain/* branches)\n await ensureContentBranch(projectRoot)\n\n // Fetch latest from remote (parallel fetch for both branches). Hardened:\n // no interactive credential prompt, block-timeout on hangs.\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 worktreePath = join(tmpdir(), `cr-${randomUUID()}`)\n const branch = branchName\n\n // Create worktree on contentrain branch\n await git.raw(['worktree', 'add', worktreePath, CONTENTRAIN_BRANCH])\n\n // Commit identity comes from `-c user.*` config (see authorConfig) — passed\n // as args, never via `.env()`, so simple-git's block-unsafe guard is never\n // triggered by an inherited EDITOR/GIT_ASKPASS/etc.\n const wtGit = createGit(worktreePath, { config: authorConfig() })\n\n // Sync contentrain with base branch (bring main changes into contentrain)\n try {\n await wtGit.merge([baseBranch, '--no-edit'])\n } catch {\n try { await wtGit.merge(['--abort']) } catch { /* not in merge state */ }\n if (hasRemote) {\n try {\n await wtGit.merge([`${remoteName}/${baseBranch}`, '--no-edit'])\n } catch {\n try { await wtGit.merge(['--abort']) } catch { /* ignore */ }\n }\n }\n }\n\n // Sync with remote contentrain if exists\n if (hasRemote) {\n try {\n await wtGit.merge([`${remoteName}/${CONTENTRAIN_BRANCH}`, '--no-edit'])\n } catch {\n try { await wtGit.merge(['--abort']) } catch { /* ignore */ }\n }\n }\n\n // Create feature branch from contentrain\n await wtGit.checkout(['-b', branch])\n\n let commitHash = ''\n let pendingReview = false\n // Set once the feature branch has been merged into the contentrain branch,\n // which is what makes deleting it in cleanup() safe — the work is reachable\n // from contentrain at that point.\n let mergedIntoContentrain = false\n let savedContextUpdate: ContextUpdate | undefined\n\n return {\n worktree: worktreePath,\n branch,\n\n async write(callback) {\n await callback(worktreePath)\n },\n\n async commit(message, contextUpdate?) {\n // context.json is intentionally NOT committed on the feature branch — it\n // is regenerated on the contentrain branch after the merge (see\n // complete()). Committing it per-branch caused cross-branch merge\n // conflicts on a single mutable file. `--no-verify` keeps the repo's\n // commit-msg / pre-commit hooks (commitlint, lefthook, husky) from\n // rejecting these machine-generated infra commits.\n savedContextUpdate = contextUpdate\n await wtGit.add('.')\n const result = await wtGit.commit(message, { '--allow-empty': null, '--no-verify': null })\n commitHash = result.commit || ''\n return commitHash\n },\n\n async complete() {\n if (workflow === 'review') {\n // Mark the branch as surviving BEFORE attempting the push. The commit\n // already exists at this point, so a push failure (no write access,\n // offline, expired credentials) must not make cleanup() treat this as\n // a failed transaction and delete the branch — that silently discards\n // committed work. Pushing is a convenience here; contentrain_submit is\n // the documented way to publish a review branch.\n pendingReview = true\n\n let warning: string | undefined\n if (hasRemote) {\n try {\n await networkGit(projectRoot, NETWORK_TIMEOUT_MS).push(remoteName, branch)\n } catch (error) {\n const detail = error instanceof Error ? error.message : String(error)\n warning = `Changes are committed locally on \"${branch}\", but pushing to \"${remoteName}\" failed: ${detail.trim()}. `\n + `The branch is intact — run contentrain_submit once you have push access, or merge it locally with contentrain_merge.`\n }\n }\n\n return { action: 'pending-review', commit: commitHash, warning }\n }\n\n // auto-merge: merge feature branch into contentrain, then advance base\n\n // Switch to contentrain branch in worktree\n await wtGit.checkout(CONTENTRAIN_BRANCH)\n\n // Merge feature branch into contentrain\n try {\n await wtGit.merge([branch, '--no-edit'])\n } catch {\n try {\n await wtGit.merge(['--abort'])\n } catch { /* not in merge state */ }\n throw Object.assign(new Error(\n `Merge conflict when merging branch \"${branch}\" into \"${CONTENTRAIN_BRANCH}\". `\n + `The branch still exists with your changes intact. `\n + `Resolve the conflict manually, or delete the branch and retry.`,\n ), {\n code: 'CONTENT_BRANCH_MERGE_CONFLICT',\n agent_hint: 'The feature branch could not be merged into the contentrain branch. Ask the developer to resolve the conflict.',\n developer_action: `git checkout ${CONTENTRAIN_BRANCH} && git merge ${branch}`,\n })\n }\n // The commit is now reachable from contentrain, so cleanup() may prune\n // the feature branch. Anything that fails after this point still leaves\n // the work safe.\n mergedIntoContentrain = true\n\n // Regenerate context.json on the contentrain branch (post-merge,\n // single-threaded) and fold it into the tip before advancing the base.\n if (savedContextUpdate) {\n await regenerateContextOnContentrain(wtGit, worktreePath, savedContextUpdate)\n }\n\n // Get contentrain tip + old base ref + dirty files in parallel\n const [contentrainTip, 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 // Fast-forward check: baseBranch must be an ancestor of contentrainTip.\n // `rev-list --count` instead of `merge-base --is-ancestor`: the latter\n // signals via exit code with empty stderr, which simple-git reports as\n // success — the check would silently pass on divergence.\n //\n // Divergence is NOT an error here. The content is already committed and\n // merged into contentrain (the CDN's source of truth) — only the base\n // fast-forward is impossible. Throwing at this point used to report a\n // failure for a write that had in fact landed, while cleanup() deleted\n // the cr/* branch. Report the truth instead.\n let sync: SyncResult = { synced: [], skipped: [] }\n let baseAdvance: BaseAdvance\n let divergedWarning: string | undefined\n if (await isAncestor(git, previousBaseRef, contentrainTip)) {\n baseAdvance = 'advanced'\n\n // Advance base branch to contentrain tip via update-ref\n await git.raw(['update-ref', `refs/heads/${baseBranch}`, contentrainTip])\n\n // Refresh index to match new HEAD.\n // update-ref moves the branch pointer but leaves the index stale.\n // read-tree updates the index to match HEAD without touching the working tree.\n try {\n await git.raw(['read-tree', 'HEAD'])\n } catch {\n // fallback: try reset for older git versions\n try { await git.raw(['reset', 'HEAD']) } catch { /* ignore */ }\n }\n\n // Selective sync: copy .contentrain/ files to developer's working tree.\n // Only after an advance — when the base did not move, the developer's\n // HEAD did not change, and syncing files from the contentrain tip\n // would desync their working tree from their own HEAD.\n sync = await selectiveSync(projectRoot, worktreePath, contentrainTip, previousBaseRef, dirtyFilesBeforeUpdate)\n } else {\n baseAdvance = 'blocked_diverged'\n divergedWarning = `Content is committed and merged into \"${CONTENTRAIN_BRANCH}\", but \"${baseBranch}\" has commits that are not in \"${CONTENTRAIN_BRANCH}\" — the branches have diverged, so \"${baseBranch}\" was not advanced. Run contentrain_reconcile (CLI: npx contentrain reconcile) to merge them and restore the fast-forward advance.`\n }\n\n // Push contentrain (with retry) and, when advanced, the base branch.\n let remotePush: RemotePush = 'no-remote'\n if (hasRemote) {\n remotePush = await pushContentBranches(projectRoot, worktreePath, wtGit, remoteName, baseBranch, baseAdvance === 'advanced')\n }\n\n const warnings = [divergedWarning, sync.warning].filter(Boolean)\n return {\n action: 'auto-merged' as const,\n commit: commitHash,\n sync,\n base_advance: baseAdvance,\n remote_push: remotePush,\n ...(warnings.length > 0 ? { warning: warnings.join(' ') } : {}),\n }\n },\n\n async cleanup() {\n try {\n await git.raw(['worktree', 'remove', worktreePath, '--force'])\n } catch {\n // worktree may already be cleaned up\n }\n // Prune the feature branch only when doing so cannot lose work:\n // - pendingReview: must survive for a later contentrain_submit/merge\n // - commitHash === '': nothing was ever committed, safe to drop\n // - mergedIntoContentrain: the work is reachable from contentrain\n // Anything else means the branch holds a commit that is reachable from\n // nowhere else, and deleting it would silently discard the user's work\n // while the caller reports success. Keeping it may leave a stray cr/*\n // ref after a failure — that is recoverable; a deleted commit is not.\n const holdsUnreachableWork = commitHash !== '' && !mergedIntoContentrain\n if (!pendingReview && !holdsUnreachableWork) {\n await safeDeleteBranch(git, branch)\n }\n },\n }\n}\n\nexport async function mergeBranch(\n projectRoot: string,\n branchName: string,\n): Promise<MergeBranchResult> {\n const git = createGit(projectRoot)\n const config = await readConfig(projectRoot)\n const remoteName = process.env['CONTENTRAIN_REMOTE'] ?? 'origin'\n\n // Detect base branch\n const baseBranch = process.env['CONTENTRAIN_BRANCH']\n ?? config?.repository?.default_branch\n ?? ((await git.raw(['branch', '--show-current'])).trim() || 'main')\n\n // Ensure contentrain branch exists\n await ensureContentBranch(projectRoot)\n\n // Check remote\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\n // Fetch latest from remote before merging — an approve against stale\n // remote-tracking refs is how an already-reconciled divergence keeps\n // reporting itself. Hardened: no credential prompt, block-timeout.\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 // Create temp worktree on contentrain branch\n const worktreePath = join(tmpdir(), `cr-merge-${randomUUID()}`)\n await git.raw(['worktree', 'add', worktreePath, CONTENTRAIN_BRANCH])\n\n // Commit identity from `-c user.*` config (see authorConfig) — guard-safe,\n // no `.env()` spread.\n const wtGit = createGit(worktreePath, { config: authorConfig() })\n\n try {\n // Sync contentrain with the base branch (and remotes) BEFORE landing the\n // feature branch — the counterpart of the pre-fork sync in\n // createTransaction. Without it, one base commit that is not in\n // contentrain makes every merge report a diverged base forever. A\n // conflicting sync is aborted silently; the fast-forward check below then\n // reports the divergence truthfully instead of this call guessing at it.\n try {\n await wtGit.merge([baseBranch, '--no-edit'])\n } catch {\n try { await wtGit.merge(['--abort']) } catch { /* not in merge state */ }\n if (hasRemote) {\n try {\n await wtGit.merge([`${remoteName}/${baseBranch}`, '--no-edit'])\n } catch {\n try { await wtGit.merge(['--abort']) } catch { /* ignore */ }\n }\n }\n }\n if (hasRemote) {\n try {\n await wtGit.merge([`${remoteName}/${CONTENTRAIN_BRANCH}`, '--no-edit'])\n } catch {\n try { await wtGit.merge(['--abort']) } catch { /* ignore */ }\n }\n }\n\n // Merge the feature branch into contentrain\n try {\n await wtGit.merge([branchName, '--no-edit'])\n } catch {\n try { await wtGit.merge(['--abort']) } catch { /* not in merge state */ }\n throw Object.assign(new Error(\n `Merge conflict when merging branch \"${branchName}\" into \"${CONTENTRAIN_BRANCH}\". `\n + `The branch still exists with your changes intact. `\n + `Resolve the conflict manually, or delete the branch and retry.`,\n ), {\n code: 'CONTENT_BRANCH_MERGE_CONFLICT',\n agent_hint: 'The feature branch could not be merged into the contentrain branch. Ask the developer to resolve the conflict.',\n developer_action: `git checkout ${CONTENTRAIN_BRANCH} && git merge ${branchName}`,\n })\n }\n\n // Regenerate context.json on contentrain post-merge (deterministic,\n // single-threaded) so review-mode branches — which carry no context.json —\n // still produce up-to-date stats once landed.\n await regenerateContextOnContentrain(wtGit, worktreePath, { tool: 'contentrain_merge', model: '*' })\n\n // Get contentrain tip + old base ref + dirty files in parallel\n const [contentrainTip, 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 // Fast-forward check: baseBranch must be an ancestor of contentrainTip.\n // (See complete() — merge-base --is-ancestor is unusable via simple-git.)\n // Divergence is NOT an error: the feature branch is already merged into\n // contentrain; only the base advance is pending until the branches are\n // reconciled. Throwing here used to strand the cr/* branch as a phantom\n // \"pending review\" while its content was already on contentrain.\n let sync: SyncResult = { synced: [], skipped: [] }\n let baseAdvance: BaseAdvance\n let divergedWarning: string | undefined\n if (await isAncestor(git, previousBaseRef, contentrainTip)) {\n baseAdvance = 'advanced'\n\n // Advance base branch to contentrain tip via update-ref\n await git.raw(['update-ref', `refs/heads/${baseBranch}`, contentrainTip])\n\n // Refresh index to match new HEAD\n try {\n await git.raw(['read-tree', 'HEAD'])\n } catch {\n try { await git.raw(['reset', 'HEAD']) } catch { /* ignore */ }\n }\n\n // Selective sync: copy .contentrain/ files to developer's working tree.\n // Only after an advance — see complete() for why.\n sync = await selectiveSync(projectRoot, worktreePath, contentrainTip, previousBaseRef, dirtyFilesBeforeUpdate)\n } else {\n baseAdvance = 'blocked_diverged'\n divergedWarning = `Branch \"${branchName}\" is merged into \"${CONTENTRAIN_BRANCH}\", but \"${baseBranch}\" has commits that are not in \"${CONTENTRAIN_BRANCH}\" — the branches have diverged, so \"${baseBranch}\" was not advanced. Run contentrain_reconcile (CLI: npx contentrain reconcile) to merge them and restore the fast-forward advance.`\n }\n\n // Push contentrain (with retry) and, when advanced, the base branch.\n let remotePush: RemotePush = 'no-remote'\n if (hasRemote) {\n remotePush = await pushContentBranches(projectRoot, worktreePath, wtGit, remoteName, baseBranch, baseAdvance === 'advanced')\n }\n\n // Prune the now-merged feature branch so merged cr/* refs don't accumulate.\n await safeDeleteBranch(git, branchName)\n\n // Delete the remote copy too (review-mode branches were pushed on save).\n // Best-effort and config-gated inside the helper: a failure surfaces as\n // `remote.warning`, never as a failed merge.\n let remote: RemoteDeleteResult | undefined\n if (hasRemote) {\n remote = await deleteRemoteBranch(projectRoot, branchName, { config })\n }\n\n const warnings = [divergedWarning, sync.warning].filter(Boolean)\n return {\n action: 'merged' as const,\n commit: contentrainTip,\n sync,\n base_advance: baseAdvance,\n remote_push: remotePush,\n ...(warnings.length > 0 ? { warning: warnings.join(' ') } : {}),\n ...(remote ? { remote } : {}),\n }\n } finally {\n // Cleanup worktree\n try {\n await git.raw(['worktree', 'remove', worktreePath, '--force'])\n } catch {\n // worktree may already be cleaned up\n }\n }\n}\n\nexport function buildBranchName(scope: string, target: string, locale?: string): string {\n const ts = branchTimestamp()\n const parts = ['cr', scope, target]\n if (locale) parts.push(locale)\n parts.push(ts)\n return parts.join('/')\n}\n\n/**\n * True when `ancestor` is an ancestor of (or equal to) `descendant`.\n * Implemented with `rev-list --count` because `merge-base --is-ancestor`\n * signals via exit code with empty stderr — simple-git reports that as\n * success, so it cannot express a negative verdict.\n */\nexport async function isAncestor(git: SimpleGit, ancestor: string, descendant: string): Promise<boolean> {\n try {\n const count = Number((await git.raw(['rev-list', '--count', ancestor, `^${descendant}`])).trim())\n return count === 0\n } catch {\n return false\n }\n}\n\n/**\n * Push the contentrain branch (with one fetch-merge-retry) and, when the base\n * advanced, the base branch. Returns the push outcome for contentrain; the\n * base push is best-effort and never demotes the outcome, because the\n * contentrain branch is the content SSOT — it is what the CDN reads.\n *\n * The retry absorbs a concurrent remote advance (another writer pushed\n * contentrain first): fetch, merge the remote tip into the local worktree's\n * contentrain, push again. A conflicting retry-merge is aborted and reported\n * as `rejected` — that is a real divergence, not a race.\n */\nexport async function pushContentBranches(\n projectRoot: string,\n worktreePath: string,\n wtGit: SimpleGit,\n remoteName: string,\n baseBranch: string,\n baseAdvanced: boolean,\n): Promise<RemotePush> {\n const net = networkGit(projectRoot, NETWORK_TIMEOUT_MS)\n let outcome: RemotePush = 'pushed'\n try {\n await net.push(remoteName, CONTENTRAIN_BRANCH)\n } catch {\n try {\n await networkGit(worktreePath, NETWORK_TIMEOUT_MS).fetch(remoteName, CONTENTRAIN_BRANCH)\n await wtGit.merge([`${remoteName}/${CONTENTRAIN_BRANCH}`, '--no-edit'])\n await net.push(remoteName, CONTENTRAIN_BRANCH)\n } catch {\n try { await wtGit.merge(['--abort']) } catch { /* not in merge state */ }\n outcome = 'rejected'\n }\n }\n if (baseAdvanced) {\n try {\n await net.push(remoteName, baseBranch)\n } catch {\n // Best-effort: a failed base push alone does not demote the outcome.\n }\n }\n return outcome\n}\n\n/**\n * Force-delete a local branch, swallowing all errors. Never deletes the\n * singleton `contentrain` branch. Used to prune feature branches after they\n * are merged (auto-merge / contentrain_merge) or when a transaction fails\n * before completing — so failed/merged `cr/*` refs do not accumulate.\n */\nasync function safeDeleteBranch(git: SimpleGit, branch: string): Promise<void> {\n if (!branch || branch === CONTENTRAIN_BRANCH) return\n try {\n await git.raw(['branch', '-D', branch])\n } catch {\n // Branch may not exist, be checked out, or already be deleted — ignore.\n }\n}\n\n/**\n * Regenerate `.contentrain/context.json` deterministically inside a worktree\n * that is currently on the `contentrain` branch, then commit it (hooks\n * bypassed). Called AFTER a feature branch is merged so context.json is only\n * ever written on `contentrain`, single-threaded — eliminating the per-branch\n * merge conflicts that came from committing it on every feature branch.\n */\nasync function regenerateContextOnContentrain(\n wtGit: SimpleGit,\n worktreePath: string,\n contextUpdate: ContextUpdate,\n): Promise<void> {\n await writeContext(worktreePath, contextUpdate)\n await wtGit.add('.contentrain/context.json')\n try {\n await wtGit.commit('[contentrain] context: update', { '--no-verify': null })\n } catch {\n // Nothing staged (context.json unchanged) — fine.\n }\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tsx-parser-BvsK6lyG.mjs","names":[],"sources":["../src/core/ast-scanner/tsx-parser.ts"],"sourcesContent":["import ts from 'typescript'\nimport type { ExtractedString, StructuralContext } from './types.js'\n\n// ─── Constants ───\n\nconst SURROUNDING_MAX = 120\n\n// ─── Public API ───\n\n/**\n * Parse a TSX/JSX/TS/JS file and extract all string literals with structural context.\n *\n * Uses TypeScript's syntax-only parser (no type checking, no tsconfig needed).\n * Walks the AST and classifies each string by its parent chain.\n */\nexport function parseTsx(content: string, fileName: string): ExtractedString[] {\n const scriptKind = getScriptKind(fileName)\n const sourceFile = ts.createSourceFile(\n fileName,\n content,\n ts.ScriptTarget.Latest,\n /* setParentNodes */ true,\n scriptKind,\n )\n\n const results: ExtractedString[] = []\n const lines = content.split('\\n')\n\n visit(sourceFile, sourceFile, lines, results)\n\n return results\n}\n\n// ─── Script kind detection ───\n\nfunction getScriptKind(fileName: string): ts.ScriptKind {\n const lower = fileName.toLowerCase()\n if (lower.endsWith('.tsx')) return ts.ScriptKind.TSX\n if (lower.endsWith('.jsx')) return ts.ScriptKind.JSX\n if (lower.endsWith('.ts')) return ts.ScriptKind.TS\n return ts.ScriptKind.JS\n}\n\n// ─── AST Walker ───\n\nfunction visit(\n node: ts.Node,\n sourceFile: ts.SourceFile,\n lines: string[],\n results: ExtractedString[],\n): void {\n // Handle JsxText nodes\n if (ts.isJsxText(node)) {\n const text = node.text.trim()\n // Skip whitespace-only or empty JsxText\n if (text.length > 0) {\n const { line: lineIdx, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile))\n const parent = getJsxParentTag(node)\n results.push({\n value: text,\n line: lineIdx + 1,\n column: character + 1,\n context: 'jsx_text',\n scope: 'script',\n parent,\n surrounding: buildSurrounding(lines, lineIdx),\n })\n }\n return\n }\n\n // Handle string literals\n if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {\n const value = node.text\n // Skip empty strings\n if (value.length === 0) return\n\n const { line: lineIdx, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile))\n const classification = classifyByParentChain(node, sourceFile)\n\n results.push({\n value,\n line: lineIdx + 1,\n column: character + 1,\n context: classification.context,\n scope: 'script',\n parent: classification.parent,\n parentProperty: classification.parentProperty,\n surrounding: buildSurrounding(lines, lineIdx),\n })\n return\n }\n\n // Handle template literals with expressions — extract static head/spans\n if (ts.isTemplateExpression(node)) {\n extractTemplateParts(node, sourceFile, lines, results)\n return\n }\n\n ts.forEachChild(node, (child) => {\n visit(child, sourceFile, lines, results)\n })\n}\n\n// ─── Template literal parts extraction ───\n\nfunction extractTemplateParts(\n node: ts.TemplateExpression,\n sourceFile: ts.SourceFile,\n lines: string[],\n results: ExtractedString[],\n): void {\n const classification = classifyByParentChain(node, sourceFile)\n\n // Head: text before first ${...}\n const headText = node.head.text\n if (headText.length > 0) {\n const { line: lineIdx, character } = sourceFile.getLineAndCharacterOfPosition(node.head.getStart(sourceFile))\n results.push({\n value: headText,\n line: lineIdx + 1,\n column: character + 1,\n context: classification.context,\n scope: 'script',\n parent: classification.parent,\n parentProperty: classification.parentProperty,\n surrounding: buildSurrounding(lines, lineIdx),\n })\n }\n\n // Template spans: text after each ${...} expression\n for (const span of node.templateSpans) {\n const spanText = span.literal.text\n if (spanText.length > 0) {\n const { line: lineIdx, character } = sourceFile.getLineAndCharacterOfPosition(span.literal.getStart(sourceFile))\n results.push({\n value: spanText,\n line: lineIdx + 1,\n column: character + 1,\n context: classification.context,\n scope: 'script',\n parent: classification.parent,\n parentProperty: classification.parentProperty,\n surrounding: buildSurrounding(lines, lineIdx),\n })\n }\n\n // Visit the expression inside ${...} for nested strings\n visit(span.expression, sourceFile, lines, results)\n }\n}\n\n// ─── Parent chain classification ───\n\ninterface Classification {\n context: StructuralContext\n parent: string\n parentProperty?: string\n}\n\nfunction classifyByParentChain(node: ts.Node, sourceFile: ts.SourceFile): Classification {\n let current = node.parent\n\n while (current) {\n // Import / Export declaration → import_path\n if (ts.isImportDeclaration(current) || ts.isExportDeclaration(current)) {\n return { context: 'import_path', parent: 'import' }\n }\n\n // Import specifier module path (dynamic import)\n if (ts.isCallExpression(current) && current.expression.kind === ts.SyntaxKind.ImportKeyword) {\n return { context: 'import_path', parent: 'import' }\n }\n\n // require() calls\n if (\n ts.isCallExpression(current)\n && ts.isIdentifier(current.expression)\n && current.expression.text === 'require'\n ) {\n return { context: 'import_path', parent: 'require' }\n }\n\n // JSX attribute → jsx_attribute or css_class\n if (ts.isJsxAttribute(current)) {\n const attrName = ts.isIdentifier(current.name) ? current.name.text : current.name.getText(sourceFile)\n // className / class / style → CSS, not content\n if (attrName === 'className' || attrName === 'class' || attrName === 'style') {\n return { context: 'css_class', parent: attrName }\n }\n return { context: 'jsx_attribute', parent: attrName, parentProperty: attrName }\n }\n\n // Variable declaration → variable_assignment\n if (ts.isVariableDeclaration(current)) {\n const varName = ts.isIdentifier(current.name) ? current.name.text : current.name.getText(sourceFile)\n return { context: 'variable_assignment', parent: varName }\n }\n\n // Property assignment → object_property\n if (ts.isPropertyAssignment(current)) {\n const key = ts.isIdentifier(current.name)\n ? current.name.text\n : ts.isStringLiteral(current.name)\n ? current.name.text\n : current.name.getText(sourceFile)\n return { context: 'object_property', parent: key, parentProperty: key }\n }\n\n // Enum member → enum_value\n if (ts.isEnumMember(current)) {\n const enumName = ts.isIdentifier(current.name) ? current.name.text : current.name.getText(sourceFile)\n return { context: 'enum_value', parent: enumName }\n }\n\n // Call expression → function_argument or css_utility_call or console_call or test_assertion\n if (ts.isCallExpression(current)) {\n const callee = getCalleeName(current.expression, sourceFile)\n // CSS utility functions\n if (['cn', 'clsx', 'classNames', 'twMerge', 'twJoin', 'cva', 'cx'].includes(callee)) {\n return { context: 'css_utility_call', parent: callee }\n }\n // Console calls\n if (callee.startsWith('console.')) {\n return { context: 'console_call', parent: callee }\n }\n // Test assertions\n if (['describe', 'it', 'test', 'expect', 'beforeEach', 'afterEach', 'beforeAll', 'afterAll'].includes(callee)) {\n return { context: 'test_assertion', parent: callee }\n }\n return { context: 'function_argument', parent: callee }\n }\n\n // Array literal → array_element\n if (ts.isArrayLiteralExpression(current)) {\n return { context: 'array_element', parent: 'array' }\n }\n\n // Case clause → switch_case\n if (ts.isCaseClause(current)) {\n return { context: 'switch_case', parent: 'case' }\n }\n\n // Type contexts → type_annotation\n if (\n ts.isTypeAliasDeclaration(current)\n || ts.isTypeReferenceNode(current)\n || ts.isInterfaceDeclaration(current)\n || ts.isTypeLiteralNode(current)\n || ts.isLiteralTypeNode(current)\n || ts.isUnionTypeNode(current)\n || ts.isIntersectionTypeNode(current)\n ) {\n return { context: 'type_annotation', parent: 'type' }\n }\n\n // Property declaration with type context (e.g., `as const` typed properties)\n // Check if we're inside a type annotation specifically\n if (ts.isPropertySignature(current)) {\n return { context: 'type_annotation', parent: 'type' }\n }\n\n current = current.parent\n }\n\n return { context: 'other', parent: '' }\n}\n\n// ─── Callee name extraction ───\n\nfunction getCalleeName(expr: ts.Expression, sourceFile: ts.SourceFile): string {\n if (ts.isIdentifier(expr)) {\n return expr.text\n }\n if (ts.isPropertyAccessExpression(expr)) {\n // e.g., console.log → \"console.log\"\n const obj = getCalleeName(expr.expression, sourceFile)\n return obj ? `${obj}.${expr.name.text}` : expr.name.text\n }\n return expr.getText(sourceFile)\n}\n\n// ─── JSX parent tag extraction ───\n\nfunction getJsxParentTag(node: ts.Node): string {\n let current = node.parent\n\n while (current) {\n if (ts.isJsxElement(current)) {\n const tagName = current.openingElement.tagName.getText()\n return tagName\n }\n if (ts.isJsxFragment(current)) {\n return 'Fragment'\n }\n current = current.parent\n }\n\n return ''\n}\n\n// ─── Surrounding code builder ───\n\nfunction buildSurrounding(lines: string[], lineIdx: number): string {\n const start = Math.max(0, lineIdx - 1)\n const end = Math.min(lines.length - 1, lineIdx + 1)\n\n const parts: string[] = []\n for (let i = start; i <= end; i++) {\n const line = lines[i]\n if (line !== undefined) {\n parts.push(line)\n }\n }\n\n const joined = parts.join('\\n')\n if (joined.length > SURROUNDING_MAX) {\n return joined.slice(0, SURROUNDING_MAX)\n }\n return joined\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"tsx-parser-go5V9zoc.mjs","names":[],"sources":["../src/core/ast-scanner/tsx-parser.ts"],"sourcesContent":["import ts from 'typescript'\nimport type { ExtractedString, StructuralContext } from './types.js'\n\n// ─── Constants ───\n\nconst SURROUNDING_MAX = 120\n\n// ─── Public API ───\n\n/**\n * Parse a TSX/JSX/TS/JS file and extract all string literals with structural context.\n *\n * Uses TypeScript's syntax-only parser (no type checking, no tsconfig needed).\n * Walks the AST and classifies each string by its parent chain.\n */\nexport function parseTsx(content: string, fileName: string): ExtractedString[] {\n const scriptKind = getScriptKind(fileName)\n const sourceFile = ts.createSourceFile(\n fileName,\n content,\n ts.ScriptTarget.Latest,\n /* setParentNodes */ true,\n scriptKind,\n )\n\n const results: ExtractedString[] = []\n const lines = content.split('\\n')\n\n visit(sourceFile, sourceFile, lines, results)\n\n return results\n}\n\n// ─── Script kind detection ───\n\nfunction getScriptKind(fileName: string): ts.ScriptKind {\n const lower = fileName.toLowerCase()\n if (lower.endsWith('.tsx')) return ts.ScriptKind.TSX\n if (lower.endsWith('.jsx')) return ts.ScriptKind.JSX\n if (lower.endsWith('.ts')) return ts.ScriptKind.TS\n return ts.ScriptKind.JS\n}\n\n// ─── AST Walker ───\n\nfunction visit(\n node: ts.Node,\n sourceFile: ts.SourceFile,\n lines: string[],\n results: ExtractedString[],\n): void {\n // Handle JsxText nodes\n if (ts.isJsxText(node)) {\n const text = node.text.trim()\n // Skip whitespace-only or empty JsxText\n if (text.length > 0) {\n const { line: lineIdx, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile))\n const parent = getJsxParentTag(node)\n results.push({\n value: text,\n line: lineIdx + 1,\n column: character + 1,\n context: 'jsx_text',\n scope: 'script',\n parent,\n surrounding: buildSurrounding(lines, lineIdx),\n })\n }\n return\n }\n\n // Handle string literals\n if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {\n const value = node.text\n // Skip empty strings\n if (value.length === 0) return\n\n const { line: lineIdx, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile))\n const classification = classifyByParentChain(node, sourceFile)\n\n results.push({\n value,\n line: lineIdx + 1,\n column: character + 1,\n context: classification.context,\n scope: 'script',\n parent: classification.parent,\n parentProperty: classification.parentProperty,\n surrounding: buildSurrounding(lines, lineIdx),\n })\n return\n }\n\n // Handle template literals with expressions — extract static head/spans\n if (ts.isTemplateExpression(node)) {\n extractTemplateParts(node, sourceFile, lines, results)\n return\n }\n\n ts.forEachChild(node, (child) => {\n visit(child, sourceFile, lines, results)\n })\n}\n\n// ─── Template literal parts extraction ───\n\nfunction extractTemplateParts(\n node: ts.TemplateExpression,\n sourceFile: ts.SourceFile,\n lines: string[],\n results: ExtractedString[],\n): void {\n const classification = classifyByParentChain(node, sourceFile)\n\n // Head: text before first ${...}\n const headText = node.head.text\n if (headText.length > 0) {\n const { line: lineIdx, character } = sourceFile.getLineAndCharacterOfPosition(node.head.getStart(sourceFile))\n results.push({\n value: headText,\n line: lineIdx + 1,\n column: character + 1,\n context: classification.context,\n scope: 'script',\n parent: classification.parent,\n parentProperty: classification.parentProperty,\n surrounding: buildSurrounding(lines, lineIdx),\n })\n }\n\n // Template spans: text after each ${...} expression\n for (const span of node.templateSpans) {\n const spanText = span.literal.text\n if (spanText.length > 0) {\n const { line: lineIdx, character } = sourceFile.getLineAndCharacterOfPosition(span.literal.getStart(sourceFile))\n results.push({\n value: spanText,\n line: lineIdx + 1,\n column: character + 1,\n context: classification.context,\n scope: 'script',\n parent: classification.parent,\n parentProperty: classification.parentProperty,\n surrounding: buildSurrounding(lines, lineIdx),\n })\n }\n\n // Visit the expression inside ${...} for nested strings\n visit(span.expression, sourceFile, lines, results)\n }\n}\n\n// ─── Parent chain classification ───\n\ninterface Classification {\n context: StructuralContext\n parent: string\n parentProperty?: string\n}\n\nfunction classifyByParentChain(node: ts.Node, sourceFile: ts.SourceFile): Classification {\n let current = node.parent\n\n while (current) {\n // Import / Export declaration → import_path\n if (ts.isImportDeclaration(current) || ts.isExportDeclaration(current)) {\n return { context: 'import_path', parent: 'import' }\n }\n\n // Import specifier module path (dynamic import)\n if (ts.isCallExpression(current) && current.expression.kind === ts.SyntaxKind.ImportKeyword) {\n return { context: 'import_path', parent: 'import' }\n }\n\n // require() calls\n if (\n ts.isCallExpression(current)\n && ts.isIdentifier(current.expression)\n && current.expression.text === 'require'\n ) {\n return { context: 'import_path', parent: 'require' }\n }\n\n // JSX attribute → jsx_attribute or css_class\n if (ts.isJsxAttribute(current)) {\n const attrName = ts.isIdentifier(current.name) ? current.name.text : current.name.getText(sourceFile)\n // className / class / style → CSS, not content\n if (attrName === 'className' || attrName === 'class' || attrName === 'style') {\n return { context: 'css_class', parent: attrName }\n }\n return { context: 'jsx_attribute', parent: attrName, parentProperty: attrName }\n }\n\n // Variable declaration → variable_assignment\n if (ts.isVariableDeclaration(current)) {\n const varName = ts.isIdentifier(current.name) ? current.name.text : current.name.getText(sourceFile)\n return { context: 'variable_assignment', parent: varName }\n }\n\n // Property assignment → object_property\n if (ts.isPropertyAssignment(current)) {\n const key = ts.isIdentifier(current.name)\n ? current.name.text\n : ts.isStringLiteral(current.name)\n ? current.name.text\n : current.name.getText(sourceFile)\n return { context: 'object_property', parent: key, parentProperty: key }\n }\n\n // Enum member → enum_value\n if (ts.isEnumMember(current)) {\n const enumName = ts.isIdentifier(current.name) ? current.name.text : current.name.getText(sourceFile)\n return { context: 'enum_value', parent: enumName }\n }\n\n // Call expression → function_argument or css_utility_call or console_call or test_assertion\n if (ts.isCallExpression(current)) {\n const callee = getCalleeName(current.expression, sourceFile)\n // CSS utility functions\n if (['cn', 'clsx', 'classNames', 'twMerge', 'twJoin', 'cva', 'cx'].includes(callee)) {\n return { context: 'css_utility_call', parent: callee }\n }\n // Console calls\n if (callee.startsWith('console.')) {\n return { context: 'console_call', parent: callee }\n }\n // Test assertions\n if (['describe', 'it', 'test', 'expect', 'beforeEach', 'afterEach', 'beforeAll', 'afterAll'].includes(callee)) {\n return { context: 'test_assertion', parent: callee }\n }\n return { context: 'function_argument', parent: callee }\n }\n\n // Array literal → array_element\n if (ts.isArrayLiteralExpression(current)) {\n return { context: 'array_element', parent: 'array' }\n }\n\n // Case clause → switch_case\n if (ts.isCaseClause(current)) {\n return { context: 'switch_case', parent: 'case' }\n }\n\n // Type contexts → type_annotation\n if (\n ts.isTypeAliasDeclaration(current)\n || ts.isTypeReferenceNode(current)\n || ts.isInterfaceDeclaration(current)\n || ts.isTypeLiteralNode(current)\n || ts.isLiteralTypeNode(current)\n || ts.isUnionTypeNode(current)\n || ts.isIntersectionTypeNode(current)\n ) {\n return { context: 'type_annotation', parent: 'type' }\n }\n\n // Property declaration with type context (e.g., `as const` typed properties)\n // Check if we're inside a type annotation specifically\n if (ts.isPropertySignature(current)) {\n return { context: 'type_annotation', parent: 'type' }\n }\n\n current = current.parent\n }\n\n return { context: 'other', parent: '' }\n}\n\n// ─── Callee name extraction ───\n\nfunction getCalleeName(expr: ts.Expression, sourceFile: ts.SourceFile): string {\n if (ts.isIdentifier(expr)) {\n return expr.text\n }\n if (ts.isPropertyAccessExpression(expr)) {\n // e.g., console.log → \"console.log\"\n const obj = getCalleeName(expr.expression, sourceFile)\n return obj ? `${obj}.${expr.name.text}` : expr.name.text\n }\n return expr.getText(sourceFile)\n}\n\n// ─── JSX parent tag extraction ───\n\nfunction getJsxParentTag(node: ts.Node): string {\n let current = node.parent\n\n while (current) {\n if (ts.isJsxElement(current)) {\n const tagName = current.openingElement.tagName.getText()\n return tagName\n }\n if (ts.isJsxFragment(current)) {\n return 'Fragment'\n }\n current = current.parent\n }\n\n return ''\n}\n\n// ─── Surrounding code builder ───\n\nfunction buildSurrounding(lines: string[], lineIdx: number): string {\n const start = Math.max(0, lineIdx - 1)\n const end = Math.min(lines.length - 1, lineIdx + 1)\n\n const parts: string[] = []\n for (let i = start; i <= end; i++) {\n const line = lines[i]\n if (line !== undefined) {\n parts.push(line)\n }\n }\n\n const joined = parts.join('\\n')\n if (joined.length > SURROUNDING_MAX) {\n return joined.slice(0, SURROUNDING_MAX)\n }\n return joined\n}\n"],"mappings":"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"}
|
package/dist/util/detect.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import "../fs-
|
|
2
|
-
import { n as detectStackInfo, r as inferPlatform, t as detectStack } from "../detect-
|
|
1
|
+
import "../fs-DJv5G8uL.mjs";
|
|
2
|
+
import { n as detectStackInfo, r as inferPlatform, t as detectStack } from "../detect-CI32ynpq.mjs";
|
|
3
3
|
export { detectStack, detectStackInfo, inferPlatform };
|
package/dist/util/fs.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as readJson, c as writeText, i as readDir, n as ensureDir, o as readText, r as pathExists, s as writeJson, t as contentrainDir } from "../fs-
|
|
1
|
+
import { a as readJson, c as writeText, i as readDir, n as ensureDir, o as readText, r as pathExists, s as writeJson, t as contentrainDir } from "../fs-DJv5G8uL.mjs";
|
|
2
2
|
export { contentrainDir, ensureDir, pathExists, readDir, readJson, readText, writeJson, writeText };
|
package/dist/util/id.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as generateEntryId, t as branchTimestamp } from "../id-
|
|
1
|
+
import { n as generateEntryId, t as branchTimestamp } from "../id-CjGMPP1k.mjs";
|
|
2
2
|
export { branchTimestamp, generateEntryId };
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { c as writeText, s as writeJson } from "./fs-
|
|
3
|
-
import { a as writeMeta, r as mergeEntryMeta } from "./meta-manager-
|
|
4
|
-
import { t as readConfig } from "./config-
|
|
5
|
-
import { E as LocalReader, c as listModels, h as parseFrontmatter, l as readModel, s as inferTitleField, u as titleFieldIssues, y as resolveLocaleStrategy } from "./model-manager-DgyKMjLN.mjs";
|
|
1
|
+
import { A as metaFilePath, D as contentDirPath, E as LocalReader, O as contentFilePath, c as listModels, h as parseFrontmatter, k as documentFilePath, l as readModel, s as inferTitleField, u as titleFieldIssues, y as resolveLocaleStrategy } from "./model-manager-DqwL8ptm.mjs";
|
|
2
|
+
import { c as writeText, s as writeJson } from "./fs-DJv5G8uL.mjs";
|
|
3
|
+
import { a as writeMeta, r as mergeEntryMeta } from "./meta-manager-g3shoZES.mjs";
|
|
4
|
+
import { t as readConfig } from "./config-B8a4JBT1.mjs";
|
|
6
5
|
import { join } from "node:path";
|
|
7
6
|
import { MODEL_FIELD_ORDER, detectSecrets, validateFieldValue } from "@contentrain/types";
|
|
8
7
|
import { rm } from "node:fs/promises";
|
|
@@ -1047,4 +1046,4 @@ async function validateProject(input, options) {
|
|
|
1047
1046
|
//#endregion
|
|
1048
1047
|
export { validateContent as i, validateScheduleFields as n, checkRelationIntegrity as r, validateProject as t };
|
|
1049
1048
|
|
|
1050
|
-
//# sourceMappingURL=validator-
|
|
1049
|
+
//# sourceMappingURL=validator-B2GhPM3L.mjs.map
|