contentrain 0.3.1 → 0.3.2

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 CHANGED
@@ -135,7 +135,7 @@ contentrain generate --watch
135
135
 
136
136
  ## 👀 Review Workflow
137
137
 
138
- Most write operations create `contentrain/*` branches in review mode.
138
+ Most write operations create feature branches from the dedicated `contentrain` branch. In review mode, these branches are pushed to remote for team review. In auto-merge mode, they are merged into the `contentrain` branch and baseBranch is advanced via update-ref.
139
139
 
140
140
  Use:
141
141
 
@@ -146,7 +146,7 @@ contentrain diff
146
146
 
147
147
  to understand:
148
148
 
149
- - how many active review branches exist
149
+ - how many active review branches exist on the `contentrain` branch
150
150
  - whether branch health is blocking new writes
151
151
  - what changed before merging or deleting a branch
152
152
 
@@ -4,6 +4,10 @@ import { defineCommand } from "citty";
4
4
  import { confirm, intro, isCancel, log, outro, select } from "@clack/prompts";
5
5
  import { simpleGit } from "simple-git";
6
6
  import { readConfig } from "@contentrain/mcp/core/config";
7
+ import { CONTENTRAIN_BRANCH } from "@contentrain/types";
8
+ import { tmpdir } from "node:os";
9
+ import { randomUUID } from "node:crypto";
10
+ import { join } from "node:path";
7
11
  //#region src/commands/diff.ts
8
12
  var diff_default = defineCommand({
9
13
  meta: {
@@ -19,17 +23,16 @@ var diff_default = defineCommand({
19
23
  const projectRoot = await resolveProjectRoot(args.root);
20
24
  const git = simpleGit(projectRoot);
21
25
  intro(pc.bold("contentrain diff"));
22
- const branches = await git.branch(["--list", "contentrain/*"]);
23
- if (branches.all.length === 0) {
26
+ const featureBranches = (await git.branch(["--list", "cr/*"])).all.filter((b) => b !== CONTENTRAIN_BRANCH);
27
+ if (featureBranches.length === 0) {
24
28
  log.message("No pending contentrain branches.");
25
29
  outro("");
26
30
  return;
27
31
  }
28
- log.info(pc.bold(`Pending branches (${branches.all.length})`));
29
- const config = await readConfig(projectRoot);
30
- const baseBranch = process.env["CONTENTRAIN_BRANCH"] ?? config?.repository?.default_branch ?? ((await git.raw(["branch", "--show-current"])).trim() || "main");
32
+ log.info(pc.bold(`Pending branches (${featureBranches.length})`));
33
+ const baseBranch = (await readConfig(projectRoot))?.repository?.default_branch ?? ((await git.raw(["branch", "--show-current"])).trim() || "main");
31
34
  const branchInfos = [];
32
- for (const branch of branches.all) try {
35
+ for (const branch of featureBranches) try {
33
36
  const diffStat = await git.diffSummary([`${baseBranch}...${branch}`]);
34
37
  branchInfos.push({
35
38
  name: branch,
@@ -92,13 +95,42 @@ var diff_default = defineCommand({
92
95
  }
93
96
  if (action === "merge") {
94
97
  const confirmMerge = await confirm({ message: `Merge ${selectedBranch} into ${baseBranch}?` });
95
- if (!isCancel(confirmMerge) && confirmMerge) try {
96
- await git.checkout(baseBranch);
97
- await git.merge([selectedBranch, "--no-edit"]);
98
- await git.deleteLocalBranch(selectedBranch, true);
99
- log.success(`Merged and deleted ${selectedBranch}`);
100
- } catch (error) {
101
- log.error(`Merge failed: ${error instanceof Error ? error.message : String(error)}`);
98
+ if (!isCancel(confirmMerge) && confirmMerge) {
99
+ const mergePath = join(tmpdir(), `cr-merge-${randomUUID()}`);
100
+ try {
101
+ if (!(await git.branchLocal()).all.includes(CONTENTRAIN_BRANCH)) await git.branch([CONTENTRAIN_BRANCH, baseBranch]);
102
+ await git.raw([
103
+ "worktree",
104
+ "add",
105
+ mergePath,
106
+ CONTENTRAIN_BRANCH
107
+ ]);
108
+ const mergeGit = simpleGit(mergePath);
109
+ await mergeGit.merge([baseBranch, "--no-edit"]).catch(() => {});
110
+ await mergeGit.merge([selectedBranch, "--no-edit"]);
111
+ const tip = (await mergeGit.raw(["rev-parse", "HEAD"])).trim();
112
+ await git.raw([
113
+ "update-ref",
114
+ `refs/heads/${baseBranch}`,
115
+ tip
116
+ ]);
117
+ if ((await git.raw(["branch", "--show-current"])).trim() === baseBranch) await git.checkout([
118
+ tip,
119
+ "--",
120
+ ".contentrain/"
121
+ ]);
122
+ await git.deleteLocalBranch(selectedBranch, true);
123
+ log.success(`Merged and deleted ${selectedBranch}`);
124
+ } catch (error) {
125
+ log.error(`Merge failed: ${error instanceof Error ? error.message : String(error)}`);
126
+ } finally {
127
+ await git.raw([
128
+ "worktree",
129
+ "remove",
130
+ mergePath,
131
+ "--force"
132
+ ]).catch(() => {});
133
+ }
102
134
  }
103
135
  } else if (action === "delete") {
104
136
  const confirmDelete = await confirm({ message: `Delete ${selectedBranch}? This cannot be undone.` });
package/dist/index.mjs CHANGED
@@ -5,17 +5,17 @@ import { defineCommand, runMain } from "citty";
5
5
  runMain(defineCommand({
6
6
  meta: {
7
7
  name: "contentrain",
8
- version: "0.3.1",
8
+ version: "0.3.2",
9
9
  description: "Contentrain CLI — AI content governance infrastructure"
10
10
  },
11
11
  subCommands: {
12
12
  init: () => import("./init-BARgfYUV.mjs").then((m) => m.default),
13
- status: () => import("./status-kF5miVty.mjs").then((m) => m.default),
13
+ status: () => import("./status-Bi9HYgcA.mjs").then((m) => m.default),
14
14
  doctor: () => import("./doctor-DyKjAIKH.mjs").then((m) => m.default),
15
15
  validate: () => import("./validate-DnVamp3p.mjs").then((m) => m.default),
16
- serve: () => import("./serve-CaCZMeHF.mjs").then((m) => m.default),
16
+ serve: () => import("./serve-DL4LhWxl.mjs").then((m) => m.default),
17
17
  generate: () => import("./generate-DDEDRLdy.mjs").then((m) => m.default),
18
- diff: () => import("./diff-otz0kGqT.mjs").then((m) => m.default)
18
+ diff: () => import("./diff-Cq6iSnaD.mjs").then((m) => m.default)
19
19
  }
20
20
  }));
21
21
  //#endregion
@@ -66,7 +66,7 @@ var serve_default = defineCommand({
66
66
  consola.warn("Serve UI not found. Running in API-only mode.");
67
67
  consola.info("UI will be available after building: cd packages/cli/src/serve-ui && pnpm build");
68
68
  }
69
- const { createServeApp } = await import("./server-DW0KXB0T.mjs");
69
+ const { createServeApp } = await import("./server-WLDOAZa4.mjs");
70
70
  const { app, handleUpgrade } = await createServeApp({
71
71
  projectRoot,
72
72
  port,
@@ -1,4 +1,7 @@
1
1
  import { simpleGit } from "simple-git";
2
+ import { CONTENTRAIN_BRANCH } from "@contentrain/types";
3
+ import { tmpdir } from "node:os";
4
+ import { randomUUID } from "node:crypto";
2
5
  import { join } from "node:path";
3
6
  import { readFile, unlink, writeFile } from "node:fs/promises";
4
7
  import { existsSync, readFileSync } from "node:fs";
@@ -202,18 +205,19 @@ async function createServeApp(options) {
202
205
  }));
203
206
  router.add("/api/branches", defineEventHandler(async () => {
204
207
  const branches = await simpleGit(projectRoot).branchLocal();
205
- const crBranches = branches.all.filter((b) => b.startsWith("contentrain/")).map((b) => ({
208
+ const crBranches = branches.all.filter((b) => b.startsWith("cr/")).map((b) => ({
206
209
  name: b,
207
- current: b === branches.current
210
+ current: b === branches.current,
211
+ system: b === CONTENTRAIN_BRANCH
208
212
  }));
209
213
  return {
210
214
  branches: crBranches,
211
- total: crBranches.length
215
+ total: crBranches.filter((b) => !b.system).length
212
216
  };
213
217
  }));
214
218
  router.add("/api/branches/diff", defineEventHandler(async (event) => {
215
219
  const branchName = getQuery(event)["name"];
216
- if (!branchName?.startsWith("contentrain/")) throw createError({
220
+ if (!branchName?.startsWith("cr/")) throw createError({
217
221
  statusCode: 400,
218
222
  message: "Invalid branch name"
219
223
  });
@@ -232,29 +236,53 @@ async function createServeApp(options) {
232
236
  message: "Method not allowed"
233
237
  });
234
238
  const branchName = (await readBody(event)).branch;
235
- if (!branchName?.startsWith("contentrain/")) throw createError({
239
+ if (!branchName?.startsWith("cr/")) throw createError({
236
240
  statusCode: 400,
237
241
  message: "Invalid branch name"
238
242
  });
239
243
  const git = simpleGit(projectRoot);
240
244
  const baseBranch = getDefaultBranch();
241
- await git.checkout(baseBranch);
242
- await git.merge([
243
- branchName,
244
- "--no-edit",
245
- "-X",
246
- "theirs"
245
+ if (!(await git.branchLocal()).all.includes(CONTENTRAIN_BRANCH)) await git.branch([CONTENTRAIN_BRANCH, baseBranch]);
246
+ const mergePath = join(tmpdir(), `cr-merge-${randomUUID()}`);
247
+ await git.raw([
248
+ "worktree",
249
+ "add",
250
+ mergePath,
251
+ CONTENTRAIN_BRANCH
247
252
  ]);
248
- await git.deleteLocalBranch(branchName, true);
249
- broadcast({
250
- type: "branch:merged",
251
- branch: branchName
252
- });
253
- return {
254
- status: "merged",
255
- branch: branchName,
256
- into: baseBranch
257
- };
253
+ const mergeGit = simpleGit(mergePath);
254
+ try {
255
+ await mergeGit.merge([baseBranch, "--no-edit"]).catch(() => {});
256
+ await mergeGit.merge([branchName, "--no-edit"]);
257
+ const tip = (await mergeGit.raw(["rev-parse", "HEAD"])).trim();
258
+ await git.raw([
259
+ "update-ref",
260
+ `refs/heads/${baseBranch}`,
261
+ tip
262
+ ]);
263
+ if ((await git.raw(["branch", "--show-current"])).trim() === baseBranch) await git.checkout([
264
+ tip,
265
+ "--",
266
+ ".contentrain/"
267
+ ]);
268
+ await git.deleteLocalBranch(branchName, true);
269
+ broadcast({
270
+ type: "branch:merged",
271
+ branch: branchName
272
+ });
273
+ return {
274
+ status: "merged",
275
+ branch: branchName,
276
+ into: baseBranch
277
+ };
278
+ } finally {
279
+ await git.raw([
280
+ "worktree",
281
+ "remove",
282
+ mergePath,
283
+ "--force"
284
+ ]).catch(() => {});
285
+ }
258
286
  }));
259
287
  router.add("/api/branches/reject", defineEventHandler(async (event) => {
260
288
  if (event.method !== "POST") throw createError({
@@ -262,7 +290,7 @@ async function createServeApp(options) {
262
290
  message: "Method not allowed"
263
291
  });
264
292
  const branchName = (await readBody(event)).branch;
265
- if (!branchName?.startsWith("contentrain/")) throw createError({
293
+ if (!branchName?.startsWith("cr/")) throw createError({
266
294
  statusCode: 400,
267
295
  message: "Invalid branch name"
268
296
  });
@@ -417,7 +445,7 @@ async function createServeApp(options) {
417
445
  const op = JSON.parse(raw)?.lastOperation;
418
446
  if (op?.tool === "contentrain_scan" || op?.tool === "contentrain_apply") lastNormalize = op;
419
447
  } catch {}
420
- const branches = (await simpleGit(projectRoot).branchLocal()).all.filter((b) => b.startsWith("contentrain/normalize/")).map((b) => ({ name: b }));
448
+ const branches = (await simpleGit(projectRoot).branchLocal()).all.filter((b) => b.startsWith("cr/normalize/")).map((b) => ({ name: b }));
421
449
  return {
422
450
  lastOperation: lastNormalize,
423
451
  pendingBranches: branches
@@ -511,23 +539,53 @@ async function createServeApp(options) {
511
539
  message: "Method not allowed"
512
540
  });
513
541
  const branchName = (await readBody(event)).branch;
514
- if (!branchName?.startsWith("contentrain/")) throw createError({
542
+ if (!branchName?.startsWith("cr/")) throw createError({
515
543
  statusCode: 400,
516
544
  message: "Invalid branch name"
517
545
  });
518
546
  const git = simpleGit(projectRoot);
519
- const currentBranch = (await git.branchLocal()).current;
520
- await git.merge([branchName]);
521
- await git.deleteLocalBranch(branchName, true);
522
- broadcast({
523
- type: "branch:merged",
524
- branch: branchName
525
- });
526
- return {
527
- status: "merged",
528
- branch: branchName,
529
- into: currentBranch
530
- };
547
+ const baseBranch = getDefaultBranch();
548
+ if (!(await git.branchLocal()).all.includes(CONTENTRAIN_BRANCH)) await git.branch([CONTENTRAIN_BRANCH, baseBranch]);
549
+ const mergePath = join(tmpdir(), `cr-merge-${randomUUID()}`);
550
+ await git.raw([
551
+ "worktree",
552
+ "add",
553
+ mergePath,
554
+ CONTENTRAIN_BRANCH
555
+ ]);
556
+ const mergeGit = simpleGit(mergePath);
557
+ try {
558
+ await mergeGit.merge([baseBranch, "--no-edit"]).catch(() => {});
559
+ await mergeGit.merge([branchName, "--no-edit"]);
560
+ const tip = (await mergeGit.raw(["rev-parse", "HEAD"])).trim();
561
+ await git.raw([
562
+ "update-ref",
563
+ `refs/heads/${baseBranch}`,
564
+ tip
565
+ ]);
566
+ if ((await git.raw(["branch", "--show-current"])).trim() === baseBranch) await git.checkout([
567
+ tip,
568
+ "--",
569
+ ".contentrain/"
570
+ ]);
571
+ await git.deleteLocalBranch(branchName, true);
572
+ broadcast({
573
+ type: "branch:merged",
574
+ branch: branchName
575
+ });
576
+ return {
577
+ status: "merged",
578
+ branch: branchName,
579
+ into: baseBranch
580
+ };
581
+ } finally {
582
+ await git.raw([
583
+ "worktree",
584
+ "remove",
585
+ mergePath,
586
+ "--force"
587
+ ]).catch(() => {});
588
+ }
531
589
  }));
532
590
  router.add("/api/normalize/sources", defineEventHandler(async () => {
533
591
  const sourcesPath = join(crDir, "normalize-sources.json");
@@ -3,6 +3,7 @@ import { i as pc, n as formatPercent, r as formatTable, t as formatCount } from
3
3
  import { defineCommand } from "citty";
4
4
  import { intro, log, outro } from "@clack/prompts";
5
5
  import { simpleGit } from "simple-git";
6
+ import { CONTENTRAIN_BRANCH } from "@contentrain/types";
6
7
  import { countEntries, readModel } from "@contentrain/mcp/core/model-manager";
7
8
  import { validateProject } from "@contentrain/mcp/core/validator";
8
9
  //#region src/commands/status.ts
@@ -43,7 +44,22 @@ var status_default = defineCommand({
43
44
  };
44
45
  } catch {}
45
46
  try {
46
- jsonResult["pending_branches"] = (await simpleGit(projectRoot).branch(["--list", "contentrain/*"])).all;
47
+ const git = simpleGit(projectRoot);
48
+ const branches = await git.branch(["--list", "cr/*"]);
49
+ const allLocal = await git.branchLocal();
50
+ jsonResult["pending_branches"] = branches.all.filter((b) => b !== CONTENTRAIN_BRANCH);
51
+ const contentBranchExists = allLocal.all.includes(CONTENTRAIN_BRANCH);
52
+ const contentBranchInfo = { exists: contentBranchExists };
53
+ if (contentBranchExists) try {
54
+ const baseBranch = ctx.config?.repository?.default_branch ?? "main";
55
+ const aheadRaw = await git.raw([
56
+ "rev-list",
57
+ "--count",
58
+ `${baseBranch}..${CONTENTRAIN_BRANCH}`
59
+ ]);
60
+ contentBranchInfo["ahead"] = Number.parseInt(aheadRaw.trim(), 10);
61
+ } catch {}
62
+ jsonResult["content_branch"] = contentBranchInfo;
47
63
  } catch {}
48
64
  }
49
65
  process.stdout.write(JSON.stringify(jsonResult, null, 2));
@@ -91,9 +107,26 @@ var status_default = defineCommand({
91
107
  ], rows));
92
108
  } else log.message(" No models yet. Run `contentrain init` with a template or create models.");
93
109
  try {
94
- const branches = await simpleGit(projectRoot).branch(["--list", "contentrain/*"]);
95
- if (branches.all.length > 0) {
96
- const count = branches.all.length;
110
+ const git = simpleGit(projectRoot);
111
+ if ((await git.branchLocal()).all.includes(CONTENTRAIN_BRANCH)) try {
112
+ const baseBranch = ctx.config?.repository?.default_branch ?? "main";
113
+ const aheadRaw = await git.raw([
114
+ "rev-list",
115
+ "--count",
116
+ `${baseBranch}..${CONTENTRAIN_BRANCH}`
117
+ ]);
118
+ const ahead = Number.parseInt(aheadRaw.trim(), 10);
119
+ if (ahead > 0) {
120
+ log.info(pc.bold(`\nContent branch`));
121
+ log.message(` ${pc.cyan(CONTENTRAIN_BRANCH)} is ${pc.yellow(String(ahead))} commit(s) ahead of ${baseBranch}`);
122
+ } else {
123
+ log.info(pc.bold(`\nContent branch`));
124
+ log.message(` ${pc.cyan(CONTENTRAIN_BRANCH)} is in sync with ${baseBranch}`);
125
+ }
126
+ } catch {}
127
+ const featureBranches = (await git.branch(["--list", "cr/*"])).all.filter((b) => b !== CONTENTRAIN_BRANCH);
128
+ if (featureBranches.length > 0) {
129
+ const count = featureBranches.length;
97
130
  if (count >= 80) {
98
131
  log.error(pc.bold(`\nBLOCKED: ${count} active contentrain branches (limit: 80)`));
99
132
  log.message(` New writes are blocked. Merge or delete old branches with ${pc.cyan("contentrain diff")}.`);
@@ -101,7 +134,7 @@ var status_default = defineCommand({
101
134
  log.warning(pc.bold(`\nWARNING: ${count} active contentrain branches (limit: 50)`));
102
135
  log.message(` Consider merging or deleting old branches with ${pc.cyan("contentrain diff")}.`);
103
136
  } else log.info(pc.bold(`\nPending branches (${count})`));
104
- for (const branch of branches.all) log.message(` ${pc.yellow("●")} ${branch}`);
137
+ for (const branch of featureBranches) log.message(` ${pc.yellow("●")} ${branch}`);
105
138
  log.message(` Run ${pc.cyan("contentrain diff")} to review.`);
106
139
  }
107
140
  } catch {}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "contentrain",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "license": "MIT",
5
5
  "description": "CLI for Contentrain — AI content governance infrastructure",
6
6
  "type": "module",
@@ -46,10 +46,10 @@
46
46
  "picocolors": "^1.1.0",
47
47
  "simple-git": "^3.27.0",
48
48
  "ws": "^8.18.0",
49
- "@contentrain/rules": "0.3.0",
50
- "@contentrain/mcp": "1.0.7",
51
- "@contentrain/query": "5.1.0",
52
- "@contentrain/types": "0.2.0"
49
+ "@contentrain/rules": "0.3.1",
50
+ "@contentrain/mcp": "1.1.0",
51
+ "@contentrain/query": "5.1.1",
52
+ "@contentrain/types": "0.3.0"
53
53
  },
54
54
  "devDependencies": {
55
55
  "@types/ws": "^8.5.0",