contentrain 0.4.4 → 0.5.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.
Files changed (109) hide show
  1. package/README.md +112 -34
  2. package/dist/{activity-CbkxcfDf.mjs → activity-B8j9gfsK.mjs} +3 -3
  3. package/dist/{branches-D2UWex1N.mjs → branches-CvpFbyrJ.mjs} +3 -3
  4. package/dist/{cdn-build-BzC2QPNi.mjs → cdn-build-DS9FprWU.mjs} +3 -3
  5. package/dist/{cdn-init-CsmqJIaQ.mjs → cdn-init-Bm4dpEJn.mjs} +3 -3
  6. package/dist/{connect-DJo9PSiw.mjs → connect-Bb0BrZsx.mjs} +3 -3
  7. package/dist/describe-DJ5H9Cxm.mjs +106 -0
  8. package/dist/describe-format-CUAqB0C4.mjs +50 -0
  9. package/dist/{diff--halNUpG.mjs → diff-ov1j9bAb.mjs} +67 -59
  10. package/dist/doctor-MOf_LQEL.mjs +95 -0
  11. package/dist/{generate-DrUojrk8.mjs → generate-ByscV5SN.mjs} +26 -9
  12. package/dist/index.mjs +50 -12
  13. package/dist/{init-CqoDCSV5.mjs → init-540qiNQJ.mjs} +6 -6
  14. package/dist/{login-DCXkOrQV.mjs → login-xvzL-UvR.mjs} +3 -3
  15. package/dist/{logout-nPt2UAeQ.mjs → logout-DzspSatR.mjs} +2 -2
  16. package/dist/mcp-client-DVWTex2i.mjs +44 -0
  17. package/dist/merge-DdPRttF-.mjs +82 -0
  18. package/dist/{resolve-context-Cg6JoVZt.mjs → resolve-context-hKVYPniC.mjs} +1 -1
  19. package/dist/scaffold-iuvrMh78.mjs +115 -0
  20. package/dist/{serve-CNeFIQ1i.mjs → serve-CxA2pKbp.mjs} +58 -5
  21. package/dist/serve-ui/assets/{AgentPromptGroup.vue_vue_type_script_setup_true_lang-Ulb-9Tr3.js → AgentPromptGroup.vue_vue_type_script_setup_true_lang-Bftt9tx_.js} +1 -1
  22. package/dist/serve-ui/assets/AppLayout-B-fbrfMk.js +21 -0
  23. package/dist/serve-ui/assets/BranchDetailPage-B3PL9PGp.js +17 -0
  24. package/dist/serve-ui/assets/{BranchesPage-CSwikNkd.js → BranchesPage-4YpE_szf.js} +2 -2
  25. package/dist/serve-ui/assets/CardContent.vue_vue_type_script_setup_true_lang-D1L2MnBU.js +1 -0
  26. package/dist/serve-ui/assets/CollapsibleTrigger.vue_vue_type_script_setup_true_lang-B4uDtllm.js +1 -0
  27. package/dist/serve-ui/assets/{Collection-DTB4xCCE.js → Collection-9URFko-Y.js} +2 -2
  28. package/dist/serve-ui/assets/ConfigProvider-CwkmRpT5.js +1 -0
  29. package/dist/serve-ui/assets/{ContentListPage-DF-LjAaS.js → ContentListPage-BMfSfg8x.js} +2 -2
  30. package/dist/serve-ui/assets/ContentPage-BXUnNJlB.js +1 -0
  31. package/dist/serve-ui/assets/DashboardPage-1UyYgg5J.js +21 -0
  32. package/dist/serve-ui/assets/{DialogTitle.vue_vue_type_script_setup_true_lang-2sQAJn_Y.js → DialogTitle.vue_vue_type_script_setup_true_lang-M4LOzTva.js} +1 -1
  33. package/dist/serve-ui/assets/DoctorPage-WnQ8xYWy.js +1 -0
  34. package/dist/serve-ui/assets/FormatReferencePage-DoKEaawl.js +1 -0
  35. package/dist/serve-ui/assets/{Input.vue_vue_type_script_setup_true_lang-BveqfZsR.js → Input.vue_vue_type_script_setup_true_lang-tW0hgReH.js} +2 -2
  36. package/dist/serve-ui/assets/{ModelDetailPage-_TIV9itx.js → ModelDetailPage-BysqN8IL.js} +3 -3
  37. package/dist/serve-ui/assets/ModelsPage-DFZrdBIi.js +1 -0
  38. package/dist/serve-ui/assets/{NormalizePage-Cde8vdk2.js → NormalizePage-4f5Ib_ES.js} +2 -2
  39. package/dist/serve-ui/assets/{PageHeader.vue_vue_type_script_setup_true_lang-CEnjONvd.js → PageHeader.vue_vue_type_script_setup_true_lang-Bln1czg7.js} +1 -1
  40. package/dist/serve-ui/assets/Presence-CA3xTrht.js +3 -0
  41. package/dist/serve-ui/assets/{Separator.vue_vue_type_script_setup_true_lang-1pFZ2PR2.js → Separator.vue_vue_type_script_setup_true_lang-PECBq3nc.js} +1 -1
  42. package/dist/serve-ui/assets/{StudioHint.vue_vue_type_script_setup_true_lang-C6JN5tPq.js → StudioHint.vue_vue_type_script_setup_true_lang-DYTEoyjw.js} +1 -1
  43. package/dist/serve-ui/assets/{TrustBadge.vue_vue_type_script_setup_true_lang-KRiTnLVk.js → TrustBadge.vue_vue_type_script_setup_true_lang-BRXWvOYU.js} +2 -2
  44. package/dist/serve-ui/assets/ValidatePage-BMTpV2KL.js +16 -0
  45. package/dist/serve-ui/assets/{arrow-down-BAJwpryD.js → arrow-down-IJu6H7Es.js} +1 -1
  46. package/dist/serve-ui/assets/{arrow-left-DANf_wrK.js → arrow-left-D8ZjVYAI.js} +1 -1
  47. package/dist/serve-ui/assets/{book-marked-DS8FXgjz.js → book-marked-BNyyZrq3.js} +1 -1
  48. package/dist/serve-ui/assets/{book-open-DnH5T78M.js → book-open-Dw1fQtSI.js} +1 -1
  49. package/dist/serve-ui/assets/{box-UJk5SvJJ.js → box-BoGMZDZR.js} +1 -1
  50. package/dist/serve-ui/assets/{chevron-down-C1nYx5Eu.js → chevron-down-UcXxhyyY.js} +1 -1
  51. package/dist/serve-ui/assets/{chevron-right-XnJVrezx.js → chevron-right-SpdM5LBc.js} +1 -1
  52. package/dist/serve-ui/assets/circle-alert-CEwKJpYl.js +6 -0
  53. package/dist/serve-ui/assets/{content-Ctqretwl.js → content-BQEzysvq.js} +1 -1
  54. package/dist/serve-ui/assets/{database-Wd_i2bM1.js → database-U6JU8oc9.js} +1 -1
  55. package/dist/serve-ui/assets/{file-code-ScWZN40B.js → file-code-_sq1QHQo.js} +1 -1
  56. package/dist/serve-ui/assets/{file-text-J3Wzwieh.js → file-text-BiMGJckO.js} +1 -1
  57. package/dist/serve-ui/assets/{git-branch-DqEFTZDt.js → git-branch-CdZMOuSP.js} +1 -1
  58. package/dist/serve-ui/assets/{git-merge-Bmz-MVBm.js → git-merge-D8Y-BwIh.js} +1 -1
  59. package/dist/serve-ui/assets/{hash-pCwLSzjB.js → hash-CrXakbCy.js} +1 -1
  60. package/dist/serve-ui/assets/index-DL_dw3XX.js +80 -0
  61. package/dist/serve-ui/assets/index-KZ2rT4mJ.css +1 -0
  62. package/dist/serve-ui/assets/{languages-CQqNMim-.js → languages-DvyMGPwy.js} +1 -1
  63. package/dist/serve-ui/assets/{plus-Cw1OnTR2.js → plus-Ddujj7Qx.js} +1 -1
  64. package/dist/serve-ui/assets/project-Dg2BVay_.js +1 -0
  65. package/dist/serve-ui/assets/{scan-search-UJ-_oiOR.js → scan-search-CsPYGmEs.js} +1 -1
  66. package/dist/serve-ui/assets/{settings-Dnm4yTIj.js → settings-IQvevsKM.js} +1 -1
  67. package/dist/serve-ui/assets/{shield-check-B67ELGUT.js → shield-check-BmSBU8Ai.js} +1 -1
  68. package/dist/serve-ui/assets/stethoscope-DgCa_7BL.js +6 -0
  69. package/dist/serve-ui/assets/{trash-2-B1QgZW1G.js → trash-2-CNTkluuz.js} +1 -1
  70. package/dist/serve-ui/assets/useDirection-DfPL7a9d.js +1 -0
  71. package/dist/serve-ui/assets/{useId-CPFWZ4Bo.js → useId-B-RTM02R.js} +1 -1
  72. package/dist/serve-ui/assets/{useNonce-BffIzuhE.js → useNonce-DR6M3tLq.js} +1 -1
  73. package/dist/serve-ui/assets/useWatch-D4Y-epQR.js +1 -0
  74. package/dist/serve-ui/index.html +2 -2
  75. package/dist/{server-DTdmutjb.mjs → server-B1PSjXbu.mjs} +313 -112
  76. package/dist/{setup-Y1LFQ9rZ.mjs → setup-BMqKibZt.mjs} +3 -3
  77. package/dist/{skills-Cp8KF1af.mjs → skills-CZ54F-97.mjs} +3 -3
  78. package/dist/{status-Cyv-jo1y.mjs → status-BJ6Lu3J4.mjs} +3 -3
  79. package/dist/{status-CoHgz6HN.mjs → status-Bmjfrk-B.mjs} +24 -11
  80. package/dist/studio-DPVJ-2oG.mjs +24 -0
  81. package/dist/{submissions-bIG-MWvv.mjs → submissions-zH_S7JIQ.mjs} +3 -3
  82. package/dist/{usage-B2xuc2DT.mjs → usage-z6eyT64a.mjs} +3 -3
  83. package/dist/{validate-B10L7RL7.mjs → validate-BG0X5g09.mjs} +102 -4
  84. package/dist/{webhooks-Dq3zFLP7.mjs → webhooks-CBG2Bs9P.mjs} +3 -3
  85. package/dist/{whoami-Ub5RznbU.mjs → whoami-CXVfvo7x.mjs} +2 -2
  86. package/package.json +7 -6
  87. package/dist/doctor-DyKjAIKH.mjs +0 -187
  88. package/dist/serve-ui/assets/AppLayout-C91DN08z.js +0 -21
  89. package/dist/serve-ui/assets/BranchDetailPage-BokOnhE7.js +0 -17
  90. package/dist/serve-ui/assets/CollapsibleTrigger.vue_vue_type_script_setup_true_lang-BRzXVXDG.js +0 -1
  91. package/dist/serve-ui/assets/ConfigProvider-Dvd7YoEV.js +0 -1
  92. package/dist/serve-ui/assets/ContentPage-BABg8pgn.js +0 -1
  93. package/dist/serve-ui/assets/DashboardPage-BFLFEJNZ.js +0 -21
  94. package/dist/serve-ui/assets/ModelsPage-DrQemHQ-.js +0 -1
  95. package/dist/serve-ui/assets/Presence-BXKCIfUk.js +0 -3
  96. package/dist/serve-ui/assets/ValidatePage-DRFAySNR.js +0 -21
  97. package/dist/serve-ui/assets/index-CG5NtNtt.css +0 -1
  98. package/dist/serve-ui/assets/index-De_z0714.js +0 -1
  99. package/dist/serve-ui/assets/index-RDdffBTh.js +0 -80
  100. package/dist/serve-ui/assets/project-BlZcHJw6.js +0 -1
  101. package/dist/serve-ui/assets/useDirection-B4S5kPsI.js +0 -1
  102. package/dist/serve-ui/assets/useWatch-B2FLRWr8.js +0 -1
  103. package/dist/studio-CJUGiIMd.mjs +0 -24
  104. package/dist/{client-C400qHcL.mjs → client-C9E9D4mb.mjs} +1 -1
  105. package/dist/{context-An3xrvcP.mjs → context-OskMGy8y.mjs} +1 -1
  106. package/dist/{demo-DcBt6fw8.mjs → demo-BOUcAoFZ.mjs} +3 -3
  107. package/dist/{ide-_C2gUPbQ.mjs → ide-DPDVYiaq.mjs} +1 -1
  108. package/dist/{oauth-server-Beppljfz.mjs → oauth-server-smSxDoH5.mjs} +1 -1
  109. /package/dist/{ui-B5mXontH.mjs → ui-B8l_LC36.mjs} +0 -0
package/README.md CHANGED
@@ -17,9 +17,9 @@ Start here:
17
17
  - initialize `.contentrain/` in an existing repo
18
18
  - inspect project health and validation state
19
19
  - generate the typed `#contentrain` SDK client
20
- - review pending `contentrain/*` branches
20
+ - review pending `cr/*` branches
21
21
  - run the local review UI
22
- - expose the MCP server over stdio for IDE agents
22
+ - expose the MCP server over stdio (IDE agents) or HTTP (Studio, CI, remote drivers)
23
23
 
24
24
  This package is the human-facing companion to:
25
25
 
@@ -27,7 +27,7 @@ This package is the human-facing companion to:
27
27
  - [`@contentrain/query`](https://github.com/Contentrain/ai/tree/main/packages/sdk/js) for generated runtime queries
28
28
  - [`@contentrain/rules`](https://github.com/Contentrain/ai/tree/main/packages/rules) and [`@contentrain/skills`](https://github.com/Contentrain/ai/tree/main/packages/skills) for agent guidance
29
29
 
30
- ## 🚀 Install
30
+ ## Install
31
31
 
32
32
  Use `npx`:
33
33
 
@@ -47,7 +47,15 @@ Requirements:
47
47
  - Node.js 22+
48
48
  - Git available in `PATH`
49
49
 
50
- ## 🧰 Commands
50
+ ## Global Flags
51
+
52
+ | Flag | Env var | Description |
53
+ | --- | --- | --- |
54
+ | `--debug` | `CONTENTRAIN_DEBUG=1` | Verbose debug logging to stderr (works on every subcommand) |
55
+
56
+ Example: `contentrain --debug status` or `CONTENTRAIN_DEBUG=1 contentrain validate`.
57
+
58
+ ## Commands
51
59
 
52
60
  | Command | Purpose |
53
61
  | --- | --- |
@@ -56,8 +64,14 @@ Requirements:
56
64
  | `contentrain doctor` | Check setup health, SDK freshness, orphan content, and branch limits |
57
65
  | `contentrain validate` | Validate content against schemas, optionally create review-branch fixes |
58
66
  | `contentrain generate` | Generate `.contentrain/client/` and `#contentrain` package imports |
59
- | `contentrain diff` | Review and merge or reject pending `contentrain/*` branches |
60
- | `contentrain serve` | Start the local review UI or the MCP stdio server |
67
+ | `contentrain diff` | Review and merge or reject pending `cr/*` branches interactively |
68
+ | `contentrain merge <branch>` | Merge one pending `cr/*` branch non-interactively (CI/agents) |
69
+ | `contentrain describe <model>` | Inspect a model's schema, stats, and import snippet |
70
+ | `contentrain describe-format` | Print the Contentrain content-format specification |
71
+ | `contentrain scaffold --template` | Apply a template (`blog`, `landing`, `docs`, `ecommerce`, `saas`, `i18n`, `mobile`) |
72
+ | `contentrain setup <agent\|--all>` | Configure MCP server + AI rules for IDE (Claude Code, Cursor, Windsurf, VSCode, Copilot) |
73
+ | `contentrain skills` | Install, update, or list Contentrain AI skills and IDE rules |
74
+ | `contentrain serve` | Start the local review UI (REST + WS), MCP stdio server (`--stdio`), or MCP HTTP server (`--mcpHttp`) |
61
75
  | `contentrain studio connect` | Connect a repository to a Studio project |
62
76
  | `contentrain studio login` | Authenticate with Contentrain Studio |
63
77
  | `contentrain studio logout` | Log out from Studio |
@@ -71,7 +85,23 @@ Requirements:
71
85
  | `contentrain studio webhooks` | Manage webhooks |
72
86
  | `contentrain studio submissions` | Manage form submissions |
73
87
 
74
- ## 🔄 Typical Flow
88
+ ## Flag Matrix
89
+
90
+ Every read command supports `--json` for CI use; write commands surface `--watch` or `--yes` where they apply:
91
+
92
+ | Command | Notable flags |
93
+ | --- | --- |
94
+ | `status` | `--json` |
95
+ | `doctor` | `--json`, `--usage` — non-zero exit on failure |
96
+ | `validate` | `--json`, `--fix`, `--interactive`, `--watch`, `--model <id>` |
97
+ | `generate` | `--json`, `--watch` |
98
+ | `diff` | `--json` |
99
+ | `merge` | `--yes` (skip confirm) |
100
+ | `describe` | `--sample`, `--locale`, `--json` |
101
+ | `scaffold` | `--template <id>`, `--locales <csv>`, `--no-sample`, `--json` |
102
+ | `serve` | `--port`, `--host`, `--open`, `--demo`, `--stdio`, `--mcpHttp`, `--authToken` |
103
+
104
+ ## Typical Flow
75
105
 
76
106
  Initialize a project:
77
107
 
@@ -79,56 +109,72 @@ Initialize a project:
79
109
  contentrain init
80
110
  ```
81
111
 
82
- Check project state:
112
+ Check project state (with optional JSON for CI):
83
113
 
84
114
  ```bash
85
115
  contentrain status
116
+ contentrain status --json
117
+
86
118
  contentrain doctor
119
+ contentrain doctor --json
120
+ contentrain doctor --usage # Analyze content key usage
87
121
  ```
88
122
 
89
123
  Generate the typed SDK client:
90
124
 
91
125
  ```bash
92
126
  contentrain generate
127
+ contentrain generate --watch # Watch for changes
128
+ contentrain generate --json # CI-friendly JSON output
93
129
  ```
94
130
 
95
- Validate content and create review-branch fixes when possible:
131
+ Validate content and create review-branch fixes:
96
132
 
97
133
  ```bash
98
134
  contentrain validate
99
135
  contentrain validate --fix
136
+ contentrain validate --watch # Live validation in dev mode
137
+ contentrain validate --json # CI output
100
138
  ```
101
139
 
102
140
  Review pending changes:
103
141
 
104
142
  ```bash
105
143
  contentrain diff
144
+ contentrain diff --json # CI integration
106
145
  ```
107
146
 
108
147
  Open the local UI:
109
148
 
110
149
  ```bash
111
150
  contentrain serve
151
+ contentrain serve --demo # Start with a temporary demo project
112
152
  ```
113
153
 
114
- ## 🖥 `serve` Modes
154
+ Enable detailed logging:
115
155
 
116
- `contentrain serve` has two roles.
156
+ ```bash
157
+ contentrain --debug status
158
+ CONTENTRAIN_DEBUG=1 contentrain validate # Via environment variable
159
+ ```
117
160
 
118
- Start the local review UI:
161
+ ## `serve` Modes
162
+
163
+ `contentrain serve` has three roles.
164
+
165
+ ### Local review UI (default)
119
166
 
120
167
  ```bash
121
168
  contentrain serve
122
169
  contentrain serve --port 3333 --host localhost
170
+ contentrain serve --demo # Temporary project (no setup needed)
123
171
  ```
124
172
 
125
- This serves:
173
+ Serves REST endpoints for status / content / validation / branches / normalize / doctor / describe-format / preview-merge, a WebSocket stream for live updates, and the embedded Vue `serve-ui` app bundled with the CLI.
126
174
 
127
- - REST endpoints for status, content, validation, branches, and normalize data
128
- - a WebSocket stream for live updates
129
- - the embedded Vue `serve-ui` app bundled with the CLI
175
+ WebSocket event types: `connected`, `config:changed`, `context:changed`, `meta:changed`, `model:changed`, `content:changed`, `branch:created`, `branch:merged`, `branch:rejected`, `branch:merge-conflict`, `sync:warning`, `validation:updated`, `normalize:plan-updated`, `file-watch:error`.
130
176
 
131
- Start the MCP server for IDE integration:
177
+ ### MCP stdio (IDE agents)
132
178
 
133
179
  ```bash
134
180
  contentrain serve --stdio
@@ -136,7 +182,22 @@ contentrain serve --stdio
136
182
 
137
183
  Use stdio mode when connecting Claude Code, Cursor, Windsurf, or another MCP client to the local project.
138
184
 
139
- ## 📦 `generate` and `#contentrain`
185
+ ### MCP HTTP (Studio, CI, remote drivers)
186
+
187
+ ```bash
188
+ contentrain serve --mcpHttp --authToken $(openssl rand -hex 32)
189
+ contentrain serve --mcpHttp --port 3333 --host 0.0.0.0 --authToken $TOKEN
190
+ ```
191
+
192
+ Spins up a [Streamable HTTP MCP](https://modelcontextprotocol.io) server at `POST /mcp`. Bearer auth is **required** on non-localhost binds — the CLI hard-errors when no `--authToken` is set for an exposed interface (OWASP Secure-by-Default). Use HTTP mode when:
193
+
194
+ - Studio's agent drives MCP remotely
195
+ - a CI runner needs deterministic content operations
196
+ - an agent on another machine orchestrates content changes
197
+
198
+ HTTP sessions use the same `LocalProvider` backing as stdio — the transport differs, the behaviour does not. Remote git-host providers (`GitHubProvider`, `GitLabProvider`) are constructed by embedders who instantiate the MCP server programmatically; see the MCP package docs for that flow.
199
+
200
+ ## `generate` and `#contentrain`
140
201
 
141
202
  `contentrain generate` writes a typed client to `.contentrain/client/` and injects `#contentrain` imports into your `package.json`.
142
203
 
@@ -152,7 +213,30 @@ Run with watch mode during local model/content work:
152
213
  contentrain generate --watch
153
214
  ```
154
215
 
155
- ## 👀 Review Workflow
216
+ ## IDE Setup and AI Skills
217
+
218
+ Configure your IDE to use Contentrain's MCP server and AI agent rules:
219
+
220
+ ```bash
221
+ contentrain setup claude-code
222
+ contentrain setup cursor
223
+ contentrain setup vscode
224
+ contentrain setup --all # Configure all detected IDEs
225
+ ```
226
+
227
+ Install or update AI agent skills and IDE rules:
228
+
229
+ ```bash
230
+ contentrain skills
231
+ contentrain skills --update # Force update
232
+ contentrain skills --list # List installed skills
233
+ ```
234
+
235
+ This installs:
236
+ - Contentrain Agent Skills (task guidance for Claude, Cursor, etc.)
237
+ - IDE rules files (CLAUDE.md, .cursorrules, .windsurfrules)
238
+
239
+ ## Review Workflow
156
240
 
157
241
  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.
158
242
 
@@ -165,11 +249,11 @@ contentrain diff
165
249
 
166
250
  to understand:
167
251
 
168
- - how many active review branches exist on the `contentrain` branch
169
- - whether branch health is blocking new writes
252
+ - how many active `cr/*` review branches exist on the `contentrain` branch
253
+ - whether branch health is blocking new writes (warning at 50, blocked at 80)
170
254
  - what changed before merging or deleting a branch
171
255
 
172
- ## 🔗 Studio Integration
256
+ ## Studio Integration
173
257
 
174
258
  The `studio` command group connects the CLI to [Contentrain Studio](https://studio.contentrain.io) for enterprise workflows.
175
259
 
@@ -212,21 +296,11 @@ contentrain studio submissions --form contact-form
212
296
 
213
297
  Credentials are stored securely in `~/.contentrain/credentials.json` with `0o600` permissions. Use `CONTENTRAIN_STUDIO_TOKEN` environment variable for CI/CD.
214
298
 
215
- ## 🤖 IDE Rules
216
-
217
- `contentrain init` installs project-level AI rules automatically:
218
-
219
- - `CLAUDE.md` for Claude Code or generic fallback
220
- - `.cursorrules` for Cursor
221
- - `.windsurfrules` for Windsurf
222
-
223
- If the target file already exists, Contentrain appends its rules instead of overwriting unrelated content where possible.
224
-
225
- ## 📚 Documentation
299
+ ## Documentation
226
300
 
227
301
  Full documentation at **[ai.contentrain.io/packages/cli](https://ai.contentrain.io/packages/cli)**.
228
302
 
229
- ## 🛠 Development
303
+ ## Development
230
304
 
231
305
  From the monorepo root:
232
306
 
@@ -235,3 +309,7 @@ pnpm --filter contentrain test -- --run
235
309
  pnpm --filter contentrain exec tsc --noEmit
236
310
  pnpm --filter contentrain build
237
311
  ```
312
+
313
+ ## License
314
+
315
+ MIT
@@ -1,6 +1,6 @@
1
- import { i as pc } from "./ui-B5mXontH.mjs";
2
- import { n as resolveStudioClient } from "./client-C400qHcL.mjs";
3
- import { t as resolveStudioContext } from "./resolve-context-Cg6JoVZt.mjs";
1
+ import { i as pc } from "./ui-B8l_LC36.mjs";
2
+ import { n as resolveStudioClient } from "./client-C9E9D4mb.mjs";
3
+ import { t as resolveStudioContext } from "./resolve-context-hKVYPniC.mjs";
4
4
  import { defineCommand } from "citty";
5
5
  import { intro, log, outro, spinner } from "@clack/prompts";
6
6
  //#region src/studio/commands/activity.ts
@@ -1,6 +1,6 @@
1
- import { i as pc, r as formatTable, t as formatCount } from "./ui-B5mXontH.mjs";
2
- import { n as resolveStudioClient } from "./client-C400qHcL.mjs";
3
- import { t as resolveStudioContext } from "./resolve-context-Cg6JoVZt.mjs";
1
+ import { i as pc, r as formatTable, t as formatCount } from "./ui-B8l_LC36.mjs";
2
+ import { n as resolveStudioClient } from "./client-C9E9D4mb.mjs";
3
+ import { t as resolveStudioContext } from "./resolve-context-hKVYPniC.mjs";
4
4
  import { defineCommand } from "citty";
5
5
  import { confirm, intro, isCancel, log, outro, select, spinner } from "@clack/prompts";
6
6
  //#region src/studio/commands/branches.ts
@@ -1,6 +1,6 @@
1
- import { i as pc, t as formatCount } from "./ui-B5mXontH.mjs";
2
- import { n as resolveStudioClient } from "./client-C400qHcL.mjs";
3
- import { t as resolveStudioContext } from "./resolve-context-Cg6JoVZt.mjs";
1
+ import { i as pc, t as formatCount } from "./ui-B8l_LC36.mjs";
2
+ import { n as resolveStudioClient } from "./client-C9E9D4mb.mjs";
3
+ import { t as resolveStudioContext } from "./resolve-context-hKVYPniC.mjs";
4
4
  import { defineCommand } from "citty";
5
5
  import { intro, log, outro, spinner } from "@clack/prompts";
6
6
  //#region src/studio/commands/cdn-build.ts
@@ -1,6 +1,6 @@
1
- import { i as pc } from "./ui-B5mXontH.mjs";
2
- import { n as resolveStudioClient } from "./client-C400qHcL.mjs";
3
- import { t as resolveStudioContext } from "./resolve-context-Cg6JoVZt.mjs";
1
+ import { i as pc } from "./ui-B8l_LC36.mjs";
2
+ import { n as resolveStudioClient } from "./client-C9E9D4mb.mjs";
3
+ import { t as resolveStudioContext } from "./resolve-context-hKVYPniC.mjs";
4
4
  import { defineCommand } from "citty";
5
5
  import { confirm, intro, isCancel, log, outro, spinner } from "@clack/prompts";
6
6
  //#region src/studio/commands/cdn-init.ts
@@ -1,6 +1,6 @@
1
- import { i as pc } from "./ui-B5mXontH.mjs";
2
- import { c as saveDefaults, n as resolveStudioClient } from "./client-C400qHcL.mjs";
3
- import { n as openBrowser, t as startOAuthServer } from "./oauth-server-Beppljfz.mjs";
1
+ import { i as pc } from "./ui-B8l_LC36.mjs";
2
+ import { c as saveDefaults, n as resolveStudioClient } from "./client-C9E9D4mb.mjs";
3
+ import { n as openBrowser, t as startOAuthServer } from "./oauth-server-smSxDoH5.mjs";
4
4
  import { defineCommand } from "citty";
5
5
  import { confirm, intro, isCancel, log, outro, select, spinner, text } from "@clack/prompts";
6
6
  import { simpleGit } from "simple-git";
@@ -0,0 +1,106 @@
1
+ import { i as pc } from "./ui-B8l_LC36.mjs";
2
+ import { r as resolveProjectRoot } from "./context-OskMGy8y.mjs";
3
+ import { t as openMcpSession } from "./mcp-client-DVWTex2i.mjs";
4
+ import { defineCommand } from "citty";
5
+ import { intro, log, outro } from "@clack/prompts";
6
+ //#region src/commands/describe.ts
7
+ /**
8
+ * Read-only inspector for a single model, wrapping `contentrain_describe`.
9
+ *
10
+ * Useful for humans driving the CLI and for agents that want to sanity-
11
+ * check a model's fields, stats, and import snippet without committing
12
+ * anything. `--json` mirrors the MCP tool response verbatim.
13
+ */
14
+ var describe_default = defineCommand({
15
+ meta: {
16
+ name: "describe",
17
+ description: "Show the schema, stats, and import snippet for a model"
18
+ },
19
+ args: {
20
+ model: {
21
+ type: "positional",
22
+ description: "Model ID (e.g. \"blog-post\", \"hero\")",
23
+ required: true
24
+ },
25
+ root: {
26
+ type: "string",
27
+ description: "Project root path",
28
+ required: false
29
+ },
30
+ sample: {
31
+ type: "boolean",
32
+ description: "Include one sample entry",
33
+ required: false
34
+ },
35
+ locale: {
36
+ type: "string",
37
+ description: "Locale for the sample entry",
38
+ required: false
39
+ },
40
+ json: {
41
+ type: "boolean",
42
+ description: "Emit raw JSON for scripts",
43
+ required: false
44
+ }
45
+ },
46
+ async run({ args }) {
47
+ const projectRoot = await resolveProjectRoot(args.root);
48
+ const modelId = String(args.model);
49
+ const session = await openMcpSession(projectRoot);
50
+ try {
51
+ const result = await session.call("contentrain_describe", {
52
+ model: modelId,
53
+ include_sample: Boolean(args.sample),
54
+ ...args.locale ? { locale: String(args.locale) } : {}
55
+ });
56
+ if (args.json) {
57
+ process.stdout.write(JSON.stringify(result, null, 2));
58
+ return;
59
+ }
60
+ intro(pc.bold(`contentrain describe: ${modelId}`));
61
+ log.info(pc.bold("Metadata"));
62
+ log.message(` Name: ${String(result["name"] ?? "—")}`);
63
+ log.message(` Kind: ${pc.cyan(String(result["kind"] ?? "—"))}`);
64
+ log.message(` Domain: ${String(result["domain"] ?? "—")}`);
65
+ log.message(` i18n: ${result["i18n"] ? pc.green("yes") : pc.dim("no")}`);
66
+ if (result["description"]) log.message(` About: ${String(result["description"])}`);
67
+ const stats = result["stats"];
68
+ if (stats) {
69
+ log.info(pc.bold("\nStats"));
70
+ log.message(` Total entries: ${stats.total_entries ?? 0}`);
71
+ if (stats.locales) for (const [loc, n] of Object.entries(stats.locales)) log.message(` ${loc}: ${n}`);
72
+ }
73
+ const fields = result["fields"];
74
+ if (fields) {
75
+ log.info(pc.bold("\nFields"));
76
+ for (const [name, spec] of Object.entries(fields)) {
77
+ const s = spec;
78
+ const req = s.required ? pc.yellow(" *") : "";
79
+ log.message(` ${pc.bold(name)}${req} ${pc.dim(s.type ?? "")}`);
80
+ }
81
+ }
82
+ if (result["import_snippet"]) {
83
+ log.info(pc.bold("\nImport snippet"));
84
+ log.message(pc.dim(String(result["import_snippet"])));
85
+ }
86
+ const vocab = result["vocabulary_hint"];
87
+ if (vocab?.terms && Object.keys(vocab.terms).length > 0) {
88
+ log.info(pc.bold("\nVocabulary hint"));
89
+ if (vocab.note) log.message(pc.dim(` ${vocab.note}`));
90
+ for (const term of Object.keys(vocab.terms)) log.message(` • ${term}`);
91
+ }
92
+ if (result["sample"]) {
93
+ log.info(pc.bold("\nSample entry"));
94
+ log.message(pc.dim(JSON.stringify(result["sample"], null, 2)));
95
+ }
96
+ outro("");
97
+ } catch (error) {
98
+ log.error(error instanceof Error ? error.message : String(error));
99
+ process.exitCode = 1;
100
+ } finally {
101
+ await session.close();
102
+ }
103
+ }
104
+ });
105
+ //#endregion
106
+ export { describe_default as default };
@@ -0,0 +1,50 @@
1
+ import { i as pc } from "./ui-B8l_LC36.mjs";
2
+ import { r as resolveProjectRoot } from "./context-OskMGy8y.mjs";
3
+ import { t as openMcpSession } from "./mcp-client-DVWTex2i.mjs";
4
+ import { defineCommand } from "citty";
5
+ import { intro, log, outro } from "@clack/prompts";
6
+ //#region src/commands/describe-format.ts
7
+ /**
8
+ * Dumps the Contentrain content-format specification (what
9
+ * `contentrain_describe_format` returns). Mostly useful as a
10
+ * copy/paste primer for humans pairing with an agent, or for a quick
11
+ * `--json` handoff into another tool.
12
+ */
13
+ var describe_format_default = defineCommand({
14
+ meta: {
15
+ name: "describe-format",
16
+ description: "Print the Contentrain content-format specification"
17
+ },
18
+ args: {
19
+ root: {
20
+ type: "string",
21
+ description: "Project root path",
22
+ required: false
23
+ },
24
+ json: {
25
+ type: "boolean",
26
+ description: "Emit raw JSON for scripts",
27
+ required: false
28
+ }
29
+ },
30
+ async run({ args }) {
31
+ const session = await openMcpSession(await resolveProjectRoot(args.root));
32
+ try {
33
+ const result = await session.call("contentrain_describe_format", {});
34
+ if (args.json) {
35
+ process.stdout.write(JSON.stringify(result, null, 2));
36
+ return;
37
+ }
38
+ intro(pc.bold("contentrain describe-format"));
39
+ log.message(pc.dim(JSON.stringify(result, null, 2)));
40
+ outro("");
41
+ } catch (error) {
42
+ log.error(error instanceof Error ? error.message : String(error));
43
+ process.exitCode = 1;
44
+ } finally {
45
+ await session.close();
46
+ }
47
+ }
48
+ });
49
+ //#endregion
50
+ export { describe_format_default as default };
@@ -1,43 +1,78 @@
1
- import { r as resolveProjectRoot } from "./context-An3xrvcP.mjs";
2
- import { i as pc } from "./ui-B5mXontH.mjs";
1
+ import { i as pc } from "./ui-B8l_LC36.mjs";
2
+ import { r as resolveProjectRoot } from "./context-OskMGy8y.mjs";
3
3
  import { defineCommand } from "citty";
4
4
  import { confirm, intro, isCancel, log, outro, select } from "@clack/prompts";
5
5
  import { simpleGit } from "simple-git";
6
- import { readConfig } from "@contentrain/mcp/core/config";
7
6
  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
+ import { mergeBranch } from "@contentrain/mcp/git/transaction";
8
+ import { branchDiff } from "@contentrain/mcp/git/branch-lifecycle";
11
9
  //#region src/commands/diff.ts
12
10
  var diff_default = defineCommand({
13
11
  meta: {
14
12
  name: "diff",
15
13
  description: "Review pending contentrain branches"
16
14
  },
17
- args: { root: {
18
- type: "string",
19
- description: "Project root path",
20
- required: false
21
- } },
15
+ args: {
16
+ root: {
17
+ type: "string",
18
+ description: "Project root path",
19
+ required: false
20
+ },
21
+ json: {
22
+ type: "boolean",
23
+ description: "Emit pending-branches summary as JSON and exit (no interactive review)",
24
+ required: false
25
+ }
26
+ },
22
27
  async run({ args }) {
23
28
  const projectRoot = await resolveProjectRoot(args.root);
24
29
  const git = simpleGit(projectRoot);
25
- intro(pc.bold("contentrain diff"));
30
+ const useJson = Boolean(args.json);
31
+ if (!useJson) intro(pc.bold("contentrain diff"));
26
32
  const featureBranches = (await git.branch(["--list", "cr/*"])).all.filter((b) => b !== CONTENTRAIN_BRANCH);
33
+ if (useJson) {
34
+ const payload = await Promise.all(featureBranches.map(async (branch) => {
35
+ try {
36
+ const diff = await branchDiff(projectRoot, { branch });
37
+ const insertions = (diff.patch.match(/^\+(?!\+\+)/gmu) ?? []).length;
38
+ const deletions = (diff.patch.match(/^-(?!--)/gmu) ?? []).length;
39
+ return {
40
+ name: branch,
41
+ base: diff.base,
42
+ filesChanged: diff.filesChanged,
43
+ insertions,
44
+ deletions,
45
+ stat: diff.stat
46
+ };
47
+ } catch (error) {
48
+ return {
49
+ name: branch,
50
+ base: CONTENTRAIN_BRANCH,
51
+ filesChanged: 0,
52
+ insertions: 0,
53
+ deletions: 0,
54
+ error: error instanceof Error ? error.message : String(error)
55
+ };
56
+ }
57
+ }));
58
+ process.stdout.write(JSON.stringify({ branches: payload }, null, 2));
59
+ return;
60
+ }
27
61
  if (featureBranches.length === 0) {
28
62
  log.message("No pending contentrain branches.");
29
63
  outro("");
30
64
  return;
31
65
  }
32
66
  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");
34
67
  const branchInfos = [];
35
68
  for (const branch of featureBranches) try {
36
- const diffStat = await git.diffSummary([`${baseBranch}...${branch}`]);
69
+ const diff = await branchDiff(projectRoot, { branch });
70
+ const insertions = (diff.patch.match(/^\+(?!\+\+)/gmu) ?? []).length;
71
+ const deletions = (diff.patch.match(/^-(?!--)/gmu) ?? []).length;
37
72
  branchInfos.push({
38
73
  name: branch,
39
- summary: `${diffStat.changed} file(s), +${diffStat.insertions}/-${diffStat.deletions}`,
40
- files: diffStat.changed
74
+ summary: `${diff.filesChanged} file(s), +${insertions}/-${deletions}`,
75
+ files: diff.filesChanged
41
76
  });
42
77
  } catch {
43
78
  branchInfos.push({
@@ -63,12 +98,11 @@ var diff_default = defineCommand({
63
98
  }
64
99
  const selectedBranch = reviewChoice;
65
100
  try {
66
- const diff = await git.diff([`${baseBranch}...${selectedBranch}`, "--stat"]);
67
- log.info(pc.bold(`\nDiff: ${selectedBranch}`));
68
- log.message(diff);
69
- const fullDiff = await git.diff([`${baseBranch}...${selectedBranch}`]);
70
- if (fullDiff.length < 5e3) log.message(fullDiff);
71
- else log.message(pc.dim(`(${Math.round(fullDiff.length / 1024)}KB diff — too large to display inline)`));
101
+ const detail = await branchDiff(projectRoot, { branch: selectedBranch });
102
+ log.info(pc.bold(`\nDiff: ${selectedBranch} (base: ${detail.base})`));
103
+ log.message(detail.stat);
104
+ if (detail.patch.length < 5e3) log.message(detail.patch);
105
+ else log.message(pc.dim(`(${Math.round(detail.patch.length / 1024)}KB diff — too large to display inline)`));
72
106
  } catch (error) {
73
107
  log.error(`Could not show diff: ${error instanceof Error ? error.message : String(error)}`);
74
108
  }
@@ -77,7 +111,7 @@ var diff_default = defineCommand({
77
111
  options: [
78
112
  {
79
113
  value: "merge",
80
- label: `Merge into ${baseBranch}`
114
+ label: `Merge into ${CONTENTRAIN_BRANCH} + advance base`
81
115
  },
82
116
  {
83
117
  value: "delete",
@@ -94,43 +128,17 @@ var diff_default = defineCommand({
94
128
  return;
95
129
  }
96
130
  if (action === "merge") {
97
- const confirmMerge = await confirm({ message: `Merge ${selectedBranch} into ${baseBranch}?` });
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(() => {});
131
+ const confirmMerge = await confirm({ message: `Merge ${selectedBranch} into ${CONTENTRAIN_BRANCH}?` });
132
+ if (!isCancel(confirmMerge) && confirmMerge) try {
133
+ const result = await mergeBranch(projectRoot, selectedBranch);
134
+ log.success(`Merged ${selectedBranch} (commit ${result.commit.slice(0, 8)})`);
135
+ if (result.sync?.skipped?.length) {
136
+ log.warning(`${result.sync.skipped.length} file(s) skipped during sync — you have uncommitted changes:`);
137
+ for (const f of result.sync.skipped) log.message(pc.dim(` ${f}`));
138
+ log.message(pc.dim(" Review your working tree before running another merge."));
133
139
  }
140
+ } catch (error) {
141
+ log.error(`Merge failed: ${error instanceof Error ? error.message : String(error)}`);
134
142
  }
135
143
  } else if (action === "delete") {
136
144
  const confirmDelete = await confirm({ message: `Delete ${selectedBranch}? This cannot be undone.` });