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.
- package/README.md +112 -34
- package/dist/{activity-CbkxcfDf.mjs → activity-B8j9gfsK.mjs} +3 -3
- package/dist/{branches-D2UWex1N.mjs → branches-CvpFbyrJ.mjs} +3 -3
- package/dist/{cdn-build-BzC2QPNi.mjs → cdn-build-DS9FprWU.mjs} +3 -3
- package/dist/{cdn-init-CsmqJIaQ.mjs → cdn-init-Bm4dpEJn.mjs} +3 -3
- package/dist/{connect-DJo9PSiw.mjs → connect-Bb0BrZsx.mjs} +3 -3
- package/dist/describe-DJ5H9Cxm.mjs +106 -0
- package/dist/describe-format-CUAqB0C4.mjs +50 -0
- package/dist/{diff--halNUpG.mjs → diff-ov1j9bAb.mjs} +67 -59
- package/dist/doctor-MOf_LQEL.mjs +95 -0
- package/dist/{generate-DrUojrk8.mjs → generate-ByscV5SN.mjs} +26 -9
- package/dist/index.mjs +50 -12
- package/dist/{init-CqoDCSV5.mjs → init-540qiNQJ.mjs} +6 -6
- package/dist/{login-DCXkOrQV.mjs → login-xvzL-UvR.mjs} +3 -3
- package/dist/{logout-nPt2UAeQ.mjs → logout-DzspSatR.mjs} +2 -2
- package/dist/mcp-client-DVWTex2i.mjs +44 -0
- package/dist/merge-DdPRttF-.mjs +82 -0
- package/dist/{resolve-context-Cg6JoVZt.mjs → resolve-context-hKVYPniC.mjs} +1 -1
- package/dist/scaffold-iuvrMh78.mjs +115 -0
- package/dist/{serve-CNeFIQ1i.mjs → serve-CxA2pKbp.mjs} +58 -5
- 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
- package/dist/serve-ui/assets/AppLayout-B-fbrfMk.js +21 -0
- package/dist/serve-ui/assets/BranchDetailPage-B3PL9PGp.js +17 -0
- package/dist/serve-ui/assets/{BranchesPage-CSwikNkd.js → BranchesPage-4YpE_szf.js} +2 -2
- package/dist/serve-ui/assets/CardContent.vue_vue_type_script_setup_true_lang-D1L2MnBU.js +1 -0
- package/dist/serve-ui/assets/CollapsibleTrigger.vue_vue_type_script_setup_true_lang-B4uDtllm.js +1 -0
- package/dist/serve-ui/assets/{Collection-DTB4xCCE.js → Collection-9URFko-Y.js} +2 -2
- package/dist/serve-ui/assets/ConfigProvider-CwkmRpT5.js +1 -0
- package/dist/serve-ui/assets/{ContentListPage-DF-LjAaS.js → ContentListPage-BMfSfg8x.js} +2 -2
- package/dist/serve-ui/assets/ContentPage-BXUnNJlB.js +1 -0
- package/dist/serve-ui/assets/DashboardPage-1UyYgg5J.js +21 -0
- 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
- package/dist/serve-ui/assets/DoctorPage-WnQ8xYWy.js +1 -0
- package/dist/serve-ui/assets/FormatReferencePage-DoKEaawl.js +1 -0
- 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
- package/dist/serve-ui/assets/{ModelDetailPage-_TIV9itx.js → ModelDetailPage-BysqN8IL.js} +3 -3
- package/dist/serve-ui/assets/ModelsPage-DFZrdBIi.js +1 -0
- package/dist/serve-ui/assets/{NormalizePage-Cde8vdk2.js → NormalizePage-4f5Ib_ES.js} +2 -2
- 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
- package/dist/serve-ui/assets/Presence-CA3xTrht.js +3 -0
- 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
- 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
- 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
- package/dist/serve-ui/assets/ValidatePage-BMTpV2KL.js +16 -0
- package/dist/serve-ui/assets/{arrow-down-BAJwpryD.js → arrow-down-IJu6H7Es.js} +1 -1
- package/dist/serve-ui/assets/{arrow-left-DANf_wrK.js → arrow-left-D8ZjVYAI.js} +1 -1
- package/dist/serve-ui/assets/{book-marked-DS8FXgjz.js → book-marked-BNyyZrq3.js} +1 -1
- package/dist/serve-ui/assets/{book-open-DnH5T78M.js → book-open-Dw1fQtSI.js} +1 -1
- package/dist/serve-ui/assets/{box-UJk5SvJJ.js → box-BoGMZDZR.js} +1 -1
- package/dist/serve-ui/assets/{chevron-down-C1nYx5Eu.js → chevron-down-UcXxhyyY.js} +1 -1
- package/dist/serve-ui/assets/{chevron-right-XnJVrezx.js → chevron-right-SpdM5LBc.js} +1 -1
- package/dist/serve-ui/assets/circle-alert-CEwKJpYl.js +6 -0
- package/dist/serve-ui/assets/{content-Ctqretwl.js → content-BQEzysvq.js} +1 -1
- package/dist/serve-ui/assets/{database-Wd_i2bM1.js → database-U6JU8oc9.js} +1 -1
- package/dist/serve-ui/assets/{file-code-ScWZN40B.js → file-code-_sq1QHQo.js} +1 -1
- package/dist/serve-ui/assets/{file-text-J3Wzwieh.js → file-text-BiMGJckO.js} +1 -1
- package/dist/serve-ui/assets/{git-branch-DqEFTZDt.js → git-branch-CdZMOuSP.js} +1 -1
- package/dist/serve-ui/assets/{git-merge-Bmz-MVBm.js → git-merge-D8Y-BwIh.js} +1 -1
- package/dist/serve-ui/assets/{hash-pCwLSzjB.js → hash-CrXakbCy.js} +1 -1
- package/dist/serve-ui/assets/index-DL_dw3XX.js +80 -0
- package/dist/serve-ui/assets/index-KZ2rT4mJ.css +1 -0
- package/dist/serve-ui/assets/{languages-CQqNMim-.js → languages-DvyMGPwy.js} +1 -1
- package/dist/serve-ui/assets/{plus-Cw1OnTR2.js → plus-Ddujj7Qx.js} +1 -1
- package/dist/serve-ui/assets/project-Dg2BVay_.js +1 -0
- package/dist/serve-ui/assets/{scan-search-UJ-_oiOR.js → scan-search-CsPYGmEs.js} +1 -1
- package/dist/serve-ui/assets/{settings-Dnm4yTIj.js → settings-IQvevsKM.js} +1 -1
- package/dist/serve-ui/assets/{shield-check-B67ELGUT.js → shield-check-BmSBU8Ai.js} +1 -1
- package/dist/serve-ui/assets/stethoscope-DgCa_7BL.js +6 -0
- package/dist/serve-ui/assets/{trash-2-B1QgZW1G.js → trash-2-CNTkluuz.js} +1 -1
- package/dist/serve-ui/assets/useDirection-DfPL7a9d.js +1 -0
- package/dist/serve-ui/assets/{useId-CPFWZ4Bo.js → useId-B-RTM02R.js} +1 -1
- package/dist/serve-ui/assets/{useNonce-BffIzuhE.js → useNonce-DR6M3tLq.js} +1 -1
- package/dist/serve-ui/assets/useWatch-D4Y-epQR.js +1 -0
- package/dist/serve-ui/index.html +2 -2
- package/dist/{server-DTdmutjb.mjs → server-B1PSjXbu.mjs} +313 -112
- package/dist/{setup-Y1LFQ9rZ.mjs → setup-BMqKibZt.mjs} +3 -3
- package/dist/{skills-Cp8KF1af.mjs → skills-CZ54F-97.mjs} +3 -3
- package/dist/{status-Cyv-jo1y.mjs → status-BJ6Lu3J4.mjs} +3 -3
- package/dist/{status-CoHgz6HN.mjs → status-Bmjfrk-B.mjs} +24 -11
- package/dist/studio-DPVJ-2oG.mjs +24 -0
- package/dist/{submissions-bIG-MWvv.mjs → submissions-zH_S7JIQ.mjs} +3 -3
- package/dist/{usage-B2xuc2DT.mjs → usage-z6eyT64a.mjs} +3 -3
- package/dist/{validate-B10L7RL7.mjs → validate-BG0X5g09.mjs} +102 -4
- package/dist/{webhooks-Dq3zFLP7.mjs → webhooks-CBG2Bs9P.mjs} +3 -3
- package/dist/{whoami-Ub5RznbU.mjs → whoami-CXVfvo7x.mjs} +2 -2
- package/package.json +7 -6
- package/dist/doctor-DyKjAIKH.mjs +0 -187
- package/dist/serve-ui/assets/AppLayout-C91DN08z.js +0 -21
- package/dist/serve-ui/assets/BranchDetailPage-BokOnhE7.js +0 -17
- package/dist/serve-ui/assets/CollapsibleTrigger.vue_vue_type_script_setup_true_lang-BRzXVXDG.js +0 -1
- package/dist/serve-ui/assets/ConfigProvider-Dvd7YoEV.js +0 -1
- package/dist/serve-ui/assets/ContentPage-BABg8pgn.js +0 -1
- package/dist/serve-ui/assets/DashboardPage-BFLFEJNZ.js +0 -21
- package/dist/serve-ui/assets/ModelsPage-DrQemHQ-.js +0 -1
- package/dist/serve-ui/assets/Presence-BXKCIfUk.js +0 -3
- package/dist/serve-ui/assets/ValidatePage-DRFAySNR.js +0 -21
- package/dist/serve-ui/assets/index-CG5NtNtt.css +0 -1
- package/dist/serve-ui/assets/index-De_z0714.js +0 -1
- package/dist/serve-ui/assets/index-RDdffBTh.js +0 -80
- package/dist/serve-ui/assets/project-BlZcHJw6.js +0 -1
- package/dist/serve-ui/assets/useDirection-B4S5kPsI.js +0 -1
- package/dist/serve-ui/assets/useWatch-B2FLRWr8.js +0 -1
- package/dist/studio-CJUGiIMd.mjs +0 -24
- package/dist/{client-C400qHcL.mjs → client-C9E9D4mb.mjs} +1 -1
- package/dist/{context-An3xrvcP.mjs → context-OskMGy8y.mjs} +1 -1
- package/dist/{demo-DcBt6fw8.mjs → demo-BOUcAoFZ.mjs} +3 -3
- package/dist/{ide-_C2gUPbQ.mjs → ide-DPDVYiaq.mjs} +1 -1
- package/dist/{oauth-server-Beppljfz.mjs → oauth-server-smSxDoH5.mjs} +1 -1
- /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 `
|
|
20
|
+
- review pending `cr/*` branches
|
|
21
21
|
- run the local review UI
|
|
22
|
-
- expose the MCP server over stdio
|
|
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
|
-
##
|
|
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
|
-
##
|
|
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 `
|
|
60
|
-
| `contentrain
|
|
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
|
-
##
|
|
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
|
|
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
|
-
|
|
154
|
+
Enable detailed logging:
|
|
115
155
|
|
|
116
|
-
|
|
156
|
+
```bash
|
|
157
|
+
contentrain --debug status
|
|
158
|
+
CONTENTRAIN_DEBUG=1 contentrain validate # Via environment variable
|
|
159
|
+
```
|
|
117
160
|
|
|
118
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
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
|
-
##
|
|
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
|
-
##
|
|
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
|
-
##
|
|
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-
|
|
2
|
-
import { n as resolveStudioClient } from "./client-
|
|
3
|
-
import { t as resolveStudioContext } from "./resolve-context-
|
|
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-
|
|
2
|
-
import { n as resolveStudioClient } from "./client-
|
|
3
|
-
import { t as resolveStudioContext } from "./resolve-context-
|
|
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-
|
|
2
|
-
import { n as resolveStudioClient } from "./client-
|
|
3
|
-
import { t as resolveStudioContext } from "./resolve-context-
|
|
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-
|
|
2
|
-
import { n as resolveStudioClient } from "./client-
|
|
3
|
-
import { t as resolveStudioContext } from "./resolve-context-
|
|
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-
|
|
2
|
-
import { c as saveDefaults, n as resolveStudioClient } from "./client-
|
|
3
|
-
import { n as openBrowser, t as startOAuthServer } from "./oauth-server-
|
|
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 {
|
|
2
|
-
import {
|
|
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 {
|
|
9
|
-
import {
|
|
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: {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
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
|
|
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: `${
|
|
40
|
-
files:
|
|
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
|
|
67
|
-
log.info(pc.bold(`\nDiff: ${selectedBranch}`));
|
|
68
|
-
log.message(
|
|
69
|
-
|
|
70
|
-
|
|
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 ${
|
|
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 ${
|
|
98
|
-
if (!isCancel(confirmMerge) && confirmMerge) {
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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.` });
|