contentrain 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +168 -0
- package/dist/context-An3xrvcP.mjs +50 -0
- package/dist/diff-otz0kGqT.mjs +116 -0
- package/dist/doctor-DyKjAIKH.mjs +187 -0
- package/dist/generate-DDEDRLdy.mjs +76 -0
- package/dist/index.mjs +21 -0
- package/dist/init-D5L_7bKE.mjs +415 -0
- package/dist/serve-CaCZMeHF.mjs +111 -0
- package/dist/serve-ui/assets/AgentPromptGroup.vue_vue_type_script_setup_true_lang-Dy-5bpEY.js +16 -0
- package/dist/serve-ui/assets/AppLayout-CJFq2ApF.js +16 -0
- package/dist/serve-ui/assets/BranchDetailPage-DzSu3JlE.js +17 -0
- package/dist/serve-ui/assets/BranchesPage-ZSG-l44J.js +6 -0
- package/dist/serve-ui/assets/CollapsibleTrigger.vue_vue_type_script_setup_true_lang-C4oejwcu.js +1 -0
- package/dist/serve-ui/assets/Collection-zzx9pNd2.js +6 -0
- package/dist/serve-ui/assets/ConfigProvider-BGjDLViI.js +1 -0
- package/dist/serve-ui/assets/ContentListPage-DTFaQB8N.js +51 -0
- package/dist/serve-ui/assets/ContentListPage-vXxAeSg8.css +1 -0
- package/dist/serve-ui/assets/ContentPage-4sQsAlY2.js +1 -0
- package/dist/serve-ui/assets/DashboardPage-CZDlnvAE.js +21 -0
- package/dist/serve-ui/assets/DialogTitle.vue_vue_type_script_setup_true_lang-CBu_dmA6.js +1 -0
- package/dist/serve-ui/assets/Input.vue_vue_type_script_setup_true_lang-otPltlZG.js +6 -0
- package/dist/serve-ui/assets/ModelDetailPage-BMquKCtH.js +49 -0
- package/dist/serve-ui/assets/ModelsPage-C_K-aWXf.js +1 -0
- package/dist/serve-ui/assets/NormalizePage-CHkT6yLV.js +11 -0
- package/dist/serve-ui/assets/PageHeader.vue_vue_type_script_setup_true_lang-C7aWxNSw.js +11 -0
- package/dist/serve-ui/assets/Presence-cbB6AqkQ.js +3 -0
- package/dist/serve-ui/assets/Separator.vue_vue_type_script_setup_true_lang-fePFl24S.js +1 -0
- package/dist/serve-ui/assets/StudioHint.vue_vue_type_script_setup_true_lang-B45TG2r1.js +6 -0
- package/dist/serve-ui/assets/TrustBadge.vue_vue_type_script_setup_true_lang-CgoQGS6k.js +11 -0
- package/dist/serve-ui/assets/ValidatePage-BW8DO6WC.js +21 -0
- package/dist/serve-ui/assets/arrow-down-DqOfGL7C.js +6 -0
- package/dist/serve-ui/assets/arrow-left-BRvgeveW.js +6 -0
- package/dist/serve-ui/assets/book-marked-D8StxRZP.js +6 -0
- package/dist/serve-ui/assets/book-open-BrGAe3tk.js +6 -0
- package/dist/serve-ui/assets/box-7olVZr5D.js +6 -0
- package/dist/serve-ui/assets/chevron-down-B9YdCtsj.js +6 -0
- package/dist/serve-ui/assets/chevron-right-DFKsmZVd.js +6 -0
- package/dist/serve-ui/assets/content-BQE3NEm5.js +1 -0
- package/dist/serve-ui/assets/database-BGEFn4Oq.js +6 -0
- package/dist/serve-ui/assets/file-code-BGiS4dun.js +6 -0
- package/dist/serve-ui/assets/file-text-BDHXQ95W.js +6 -0
- package/dist/serve-ui/assets/getActiveElement-FUKBWQwh.js +1 -0
- package/dist/serve-ui/assets/git-branch-DdE7bk9-.js +6 -0
- package/dist/serve-ui/assets/git-merge-iStuJS69.js +6 -0
- package/dist/serve-ui/assets/hash-B-ibLpM1.js +6 -0
- package/dist/serve-ui/assets/index-C-RjMzFm.css +1 -0
- package/dist/serve-ui/assets/index-CoUshYEB.js +80 -0
- package/dist/serve-ui/assets/index-WHgvdYOs.js +1 -0
- package/dist/serve-ui/assets/languages-BTcd2Rl1.js +6 -0
- package/dist/serve-ui/assets/model-empty-state-B6AM4bil.js +1 -0
- package/dist/serve-ui/assets/plus-D8Nd3YnG.js +11 -0
- package/dist/serve-ui/assets/project-B1qSm0Fz.js +1 -0
- package/dist/serve-ui/assets/scan-search-DGPL_VJu.js +6 -0
- package/dist/serve-ui/assets/settings-DL-0RaxD.js +6 -0
- package/dist/serve-ui/assets/shield-check-CUL7KkSA.js +6 -0
- package/dist/serve-ui/assets/trash-2-83QhL6-T.js +11 -0
- package/dist/serve-ui/assets/useDirection-Cd3C7VdT.js +1 -0
- package/dist/serve-ui/assets/useFormatters-DgWFpsAv.js +1 -0
- package/dist/serve-ui/assets/useId-C0uR6mdj.js +1 -0
- package/dist/serve-ui/assets/useNonce-DTGvzOJz.js +1 -0
- package/dist/serve-ui/assets/useWatch-COpVIQvm.js +1 -0
- package/dist/serve-ui/favicon.svg +19 -0
- package/dist/serve-ui/for-developers-1.svg +65 -0
- package/dist/serve-ui/icon-color.svg +18 -0
- package/dist/serve-ui/index.html +17 -0
- package/dist/serve-ui/logo/color-icon-black-text.svg +65 -0
- package/dist/serve-ui/logo/color-icon-white-text.svg +61 -0
- package/dist/serve-ui/merge-1.svg +131 -0
- package/dist/serve-ui/merge-2.svg +161 -0
- package/dist/serve-ui/model-empty-state.svg +12 -0
- package/dist/serve-ui/select-data-type.svg +23 -0
- package/dist/server-DW0KXB0T.mjs +607 -0
- package/dist/status-kF5miVty.mjs +122 -0
- package/dist/ui-B5mXontH.mjs +33 -0
- package/dist/validate-DnVamp3p.mjs +155 -0
- package/package.json +66 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Contentrain
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# `contentrain`
|
|
2
|
+
|
|
3
|
+
CLI for Contentrain.
|
|
4
|
+
|
|
5
|
+
`contentrain` is the local operations surface for a Contentrain project:
|
|
6
|
+
|
|
7
|
+
- initialize `.contentrain/` in an existing repo
|
|
8
|
+
- inspect project health and validation state
|
|
9
|
+
- generate the typed `#contentrain` SDK client
|
|
10
|
+
- review pending `contentrain/*` branches
|
|
11
|
+
- run the local review UI
|
|
12
|
+
- expose the MCP server over stdio for IDE agents
|
|
13
|
+
|
|
14
|
+
This package is the human-facing companion to:
|
|
15
|
+
|
|
16
|
+
- [`@contentrain/mcp`](https://github.com/contentrain/contentrain-ai/tree/main/packages/mcp) for deterministic content operations
|
|
17
|
+
- [`@contentrain/query`](https://github.com/contentrain/contentrain-ai/tree/main/packages/sdk/js) for generated runtime queries
|
|
18
|
+
- [`@contentrain/rules`](https://github.com/contentrain/contentrain-ai/tree/main/packages/rules) and [`@contentrain/skills`](https://github.com/contentrain/contentrain-ai/tree/main/packages/skills) for agent guidance
|
|
19
|
+
|
|
20
|
+
## 🚀 Install
|
|
21
|
+
|
|
22
|
+
Use `npx`:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npx contentrain init
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Or install globally:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pnpm add -g contentrain
|
|
32
|
+
contentrain status
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Requirements:
|
|
36
|
+
|
|
37
|
+
- Node.js 22+
|
|
38
|
+
- Git available in `PATH`
|
|
39
|
+
|
|
40
|
+
## 🧰 Commands
|
|
41
|
+
|
|
42
|
+
| Command | Purpose |
|
|
43
|
+
| --- | --- |
|
|
44
|
+
| `contentrain init` | Initialize `.contentrain/`, git workflow, templates, and IDE rules |
|
|
45
|
+
| `contentrain status` | Show project overview, models, branch pressure, and validation summary |
|
|
46
|
+
| `contentrain doctor` | Check setup health, SDK freshness, orphan content, and branch limits |
|
|
47
|
+
| `contentrain validate` | Validate content against schemas, optionally create review-branch fixes |
|
|
48
|
+
| `contentrain generate` | Generate `.contentrain/client/` and `#contentrain` package imports |
|
|
49
|
+
| `contentrain diff` | Review and merge or reject pending `contentrain/*` branches |
|
|
50
|
+
| `contentrain serve` | Start the local review UI or the MCP stdio server |
|
|
51
|
+
|
|
52
|
+
## 🔄 Typical Flow
|
|
53
|
+
|
|
54
|
+
Initialize a project:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
contentrain init
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Check project state:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
contentrain status
|
|
64
|
+
contentrain doctor
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Generate the typed SDK client:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
contentrain generate
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Validate content and create review-branch fixes when possible:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
contentrain validate
|
|
77
|
+
contentrain validate --fix
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Review pending changes:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
contentrain diff
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Open the local UI:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
contentrain serve
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## 🖥 `serve` Modes
|
|
93
|
+
|
|
94
|
+
`contentrain serve` has two roles.
|
|
95
|
+
|
|
96
|
+
Start the local review UI:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
contentrain serve
|
|
100
|
+
contentrain serve --port 3333 --host localhost
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
This serves:
|
|
104
|
+
|
|
105
|
+
- REST endpoints for status, content, validation, branches, and normalize data
|
|
106
|
+
- a WebSocket stream for live updates
|
|
107
|
+
- the embedded Vue `serve-ui` app bundled with the CLI
|
|
108
|
+
|
|
109
|
+
Start the MCP server for IDE integration:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
contentrain serve --stdio
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Use stdio mode when connecting Claude Code, Cursor, Windsurf, or another MCP client to the local project.
|
|
116
|
+
|
|
117
|
+
## 📦 `generate` and `#contentrain`
|
|
118
|
+
|
|
119
|
+
`contentrain generate` writes a typed client to `.contentrain/client/` and injects `#contentrain` imports into your `package.json`.
|
|
120
|
+
|
|
121
|
+
After generation:
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
import { query, singleton, dictionary, document } from '#contentrain'
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Run with watch mode during local model/content work:
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
contentrain generate --watch
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## 👀 Review Workflow
|
|
134
|
+
|
|
135
|
+
Most write operations create `contentrain/*` branches in review mode.
|
|
136
|
+
|
|
137
|
+
Use:
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
contentrain status
|
|
141
|
+
contentrain diff
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
to understand:
|
|
145
|
+
|
|
146
|
+
- how many active review branches exist
|
|
147
|
+
- whether branch health is blocking new writes
|
|
148
|
+
- what changed before merging or deleting a branch
|
|
149
|
+
|
|
150
|
+
## 🤖 IDE Rules
|
|
151
|
+
|
|
152
|
+
`contentrain init` installs project-level AI rules automatically:
|
|
153
|
+
|
|
154
|
+
- `CLAUDE.md` for Claude Code or generic fallback
|
|
155
|
+
- `.cursorrules` for Cursor
|
|
156
|
+
- `.windsurfrules` for Windsurf
|
|
157
|
+
|
|
158
|
+
If the target file already exists, Contentrain appends its rules instead of overwriting unrelated content where possible.
|
|
159
|
+
|
|
160
|
+
## 🛠 Development
|
|
161
|
+
|
|
162
|
+
From the monorepo root:
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
pnpm --filter contentrain test -- --run
|
|
166
|
+
pnpm --filter contentrain exec tsc --noEmit
|
|
167
|
+
pnpm --filter contentrain build
|
|
168
|
+
```
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { readConfig, readVocabulary } from "@contentrain/mcp/core/config";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { readContext } from "@contentrain/mcp/core/context";
|
|
4
|
+
import { listModels } from "@contentrain/mcp/core/model-manager";
|
|
5
|
+
import { contentrainDir, pathExists } from "@contentrain/mcp/util/fs";
|
|
6
|
+
//#region src/utils/context.ts
|
|
7
|
+
async function resolveProjectRoot(argRoot) {
|
|
8
|
+
return resolve(argRoot ?? process.env["CONTENTRAIN_PROJECT_ROOT"] ?? process.cwd());
|
|
9
|
+
}
|
|
10
|
+
async function loadProjectContext(projectRoot) {
|
|
11
|
+
const crDir = contentrainDir(projectRoot);
|
|
12
|
+
if (!await pathExists(join(crDir, "config.json"))) return {
|
|
13
|
+
projectRoot,
|
|
14
|
+
crDir,
|
|
15
|
+
initialized: false,
|
|
16
|
+
config: null,
|
|
17
|
+
context: null,
|
|
18
|
+
models: [],
|
|
19
|
+
vocabulary: null
|
|
20
|
+
};
|
|
21
|
+
const config = await readConfig(projectRoot);
|
|
22
|
+
if (!config) return {
|
|
23
|
+
projectRoot,
|
|
24
|
+
crDir,
|
|
25
|
+
initialized: false,
|
|
26
|
+
config: null,
|
|
27
|
+
context: null,
|
|
28
|
+
models: [],
|
|
29
|
+
vocabulary: null
|
|
30
|
+
};
|
|
31
|
+
const [context, models, vocabulary] = await Promise.all([
|
|
32
|
+
readContext(projectRoot),
|
|
33
|
+
listModels(projectRoot),
|
|
34
|
+
readVocabulary(projectRoot)
|
|
35
|
+
]);
|
|
36
|
+
return {
|
|
37
|
+
projectRoot,
|
|
38
|
+
crDir,
|
|
39
|
+
initialized: true,
|
|
40
|
+
config,
|
|
41
|
+
context,
|
|
42
|
+
models,
|
|
43
|
+
vocabulary
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function requireInitialized(ctx) {
|
|
47
|
+
if (!ctx.initialized || !ctx.config) throw new Error("Project not initialized. Run `contentrain init` first.");
|
|
48
|
+
}
|
|
49
|
+
//#endregion
|
|
50
|
+
export { requireInitialized as n, resolveProjectRoot as r, loadProjectContext as t };
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { r as resolveProjectRoot } from "./context-An3xrvcP.mjs";
|
|
2
|
+
import { i as pc } from "./ui-B5mXontH.mjs";
|
|
3
|
+
import { defineCommand } from "citty";
|
|
4
|
+
import { confirm, intro, isCancel, log, outro, select } from "@clack/prompts";
|
|
5
|
+
import { simpleGit } from "simple-git";
|
|
6
|
+
import { readConfig } from "@contentrain/mcp/core/config";
|
|
7
|
+
//#region src/commands/diff.ts
|
|
8
|
+
var diff_default = defineCommand({
|
|
9
|
+
meta: {
|
|
10
|
+
name: "diff",
|
|
11
|
+
description: "Review pending contentrain branches"
|
|
12
|
+
},
|
|
13
|
+
args: { root: {
|
|
14
|
+
type: "string",
|
|
15
|
+
description: "Project root path",
|
|
16
|
+
required: false
|
|
17
|
+
} },
|
|
18
|
+
async run({ args }) {
|
|
19
|
+
const projectRoot = await resolveProjectRoot(args.root);
|
|
20
|
+
const git = simpleGit(projectRoot);
|
|
21
|
+
intro(pc.bold("contentrain diff"));
|
|
22
|
+
const branches = await git.branch(["--list", "contentrain/*"]);
|
|
23
|
+
if (branches.all.length === 0) {
|
|
24
|
+
log.message("No pending contentrain branches.");
|
|
25
|
+
outro("");
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
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");
|
|
31
|
+
const branchInfos = [];
|
|
32
|
+
for (const branch of branches.all) try {
|
|
33
|
+
const diffStat = await git.diffSummary([`${baseBranch}...${branch}`]);
|
|
34
|
+
branchInfos.push({
|
|
35
|
+
name: branch,
|
|
36
|
+
summary: `${diffStat.changed} file(s), +${diffStat.insertions}/-${diffStat.deletions}`,
|
|
37
|
+
files: diffStat.changed
|
|
38
|
+
});
|
|
39
|
+
} catch {
|
|
40
|
+
branchInfos.push({
|
|
41
|
+
name: branch,
|
|
42
|
+
summary: "Could not diff",
|
|
43
|
+
files: 0
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
for (const info of branchInfos) log.message(` ${pc.yellow("●")} ${pc.bold(info.name)} ${pc.dim(info.summary)}`);
|
|
47
|
+
const reviewChoice = await select({
|
|
48
|
+
message: "Select a branch to review",
|
|
49
|
+
options: [...branchInfos.map((b) => ({
|
|
50
|
+
value: b.name,
|
|
51
|
+
label: `${b.name} (${b.summary})`
|
|
52
|
+
})), {
|
|
53
|
+
value: "__skip",
|
|
54
|
+
label: "Done — exit"
|
|
55
|
+
}]
|
|
56
|
+
});
|
|
57
|
+
if (isCancel(reviewChoice) || reviewChoice === "__skip") {
|
|
58
|
+
outro("");
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const selectedBranch = reviewChoice;
|
|
62
|
+
try {
|
|
63
|
+
const diff = await git.diff([`${baseBranch}...${selectedBranch}`, "--stat"]);
|
|
64
|
+
log.info(pc.bold(`\nDiff: ${selectedBranch}`));
|
|
65
|
+
log.message(diff);
|
|
66
|
+
const fullDiff = await git.diff([`${baseBranch}...${selectedBranch}`]);
|
|
67
|
+
if (fullDiff.length < 5e3) log.message(fullDiff);
|
|
68
|
+
else log.message(pc.dim(`(${Math.round(fullDiff.length / 1024)}KB diff — too large to display inline)`));
|
|
69
|
+
} catch (error) {
|
|
70
|
+
log.error(`Could not show diff: ${error instanceof Error ? error.message : String(error)}`);
|
|
71
|
+
}
|
|
72
|
+
const action = await select({
|
|
73
|
+
message: "Action",
|
|
74
|
+
options: [
|
|
75
|
+
{
|
|
76
|
+
value: "merge",
|
|
77
|
+
label: `Merge into ${baseBranch}`
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
value: "delete",
|
|
81
|
+
label: "Delete branch (reject changes)"
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
value: "skip",
|
|
85
|
+
label: "Leave for later"
|
|
86
|
+
}
|
|
87
|
+
]
|
|
88
|
+
});
|
|
89
|
+
if (isCancel(action) || action === "skip") {
|
|
90
|
+
outro("");
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (action === "merge") {
|
|
94
|
+
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)}`);
|
|
102
|
+
}
|
|
103
|
+
} else if (action === "delete") {
|
|
104
|
+
const confirmDelete = await confirm({ message: `Delete ${selectedBranch}? This cannot be undone.` });
|
|
105
|
+
if (!isCancel(confirmDelete) && confirmDelete) try {
|
|
106
|
+
await git.deleteLocalBranch(selectedBranch, true);
|
|
107
|
+
log.success(`Deleted ${selectedBranch}`);
|
|
108
|
+
} catch (error) {
|
|
109
|
+
log.error(`Delete failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
outro("");
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
//#endregion
|
|
116
|
+
export { diff_default as default };
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { r as resolveProjectRoot } from "./context-An3xrvcP.mjs";
|
|
2
|
+
import { i as pc, o as statusIcon } from "./ui-B5mXontH.mjs";
|
|
3
|
+
import { defineCommand } from "citty";
|
|
4
|
+
import { intro, log, outro, spinner } from "@clack/prompts";
|
|
5
|
+
import { simpleGit } from "simple-git";
|
|
6
|
+
import { readConfig } from "@contentrain/mcp/core/config";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { listModels, readModel } from "@contentrain/mcp/core/model-manager";
|
|
9
|
+
import { contentrainDir, pathExists, readDir } from "@contentrain/mcp/util/fs";
|
|
10
|
+
import { stat } from "node:fs/promises";
|
|
11
|
+
import { resolveContentDir } from "@contentrain/mcp/core/content-manager";
|
|
12
|
+
//#region src/commands/doctor.ts
|
|
13
|
+
var doctor_default = defineCommand({
|
|
14
|
+
meta: {
|
|
15
|
+
name: "doctor",
|
|
16
|
+
description: "Check project health and environment"
|
|
17
|
+
},
|
|
18
|
+
args: { root: {
|
|
19
|
+
type: "string",
|
|
20
|
+
description: "Project root path",
|
|
21
|
+
required: false
|
|
22
|
+
} },
|
|
23
|
+
async run({ args }) {
|
|
24
|
+
const projectRoot = await resolveProjectRoot(args.root);
|
|
25
|
+
intro(pc.bold("contentrain doctor"));
|
|
26
|
+
const s = spinner();
|
|
27
|
+
s.start("Running health checks...");
|
|
28
|
+
const checks = [];
|
|
29
|
+
try {
|
|
30
|
+
const version = await simpleGit(projectRoot).version();
|
|
31
|
+
checks.push({
|
|
32
|
+
name: "Git",
|
|
33
|
+
pass: true,
|
|
34
|
+
detail: `v${version.major}.${version.minor}.${version.patch}`
|
|
35
|
+
});
|
|
36
|
+
} catch {
|
|
37
|
+
checks.push({
|
|
38
|
+
name: "Git",
|
|
39
|
+
pass: false,
|
|
40
|
+
detail: "Not installed or not in PATH"
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
const hasGit = await pathExists(join(projectRoot, ".git"));
|
|
44
|
+
checks.push({
|
|
45
|
+
name: "Git repository",
|
|
46
|
+
pass: hasGit,
|
|
47
|
+
detail: hasGit ? projectRoot : "No .git directory found"
|
|
48
|
+
});
|
|
49
|
+
const nodeVersion = process.versions.node;
|
|
50
|
+
const [major] = nodeVersion.split(".").map(Number);
|
|
51
|
+
checks.push({
|
|
52
|
+
name: "Node.js",
|
|
53
|
+
pass: (major ?? 0) >= 22,
|
|
54
|
+
detail: `v${nodeVersion}${(major ?? 0) < 22 ? " (requires ≥22)" : ""}`
|
|
55
|
+
});
|
|
56
|
+
const crDir = contentrainDir(projectRoot);
|
|
57
|
+
const hasCrDir = await pathExists(crDir);
|
|
58
|
+
const hasConfig = await pathExists(join(crDir, "config.json"));
|
|
59
|
+
const hasModels = await pathExists(join(crDir, "models"));
|
|
60
|
+
const hasContent = await pathExists(join(crDir, "content"));
|
|
61
|
+
checks.push({
|
|
62
|
+
name: ".contentrain/ structure",
|
|
63
|
+
pass: hasCrDir && hasConfig && hasModels && hasContent,
|
|
64
|
+
detail: !hasCrDir ? "Not initialized — run `contentrain init`" : [
|
|
65
|
+
hasConfig ? null : "missing config.json",
|
|
66
|
+
hasModels ? null : "missing models/",
|
|
67
|
+
hasContent ? null : "missing content/"
|
|
68
|
+
].filter(Boolean).join(", ") || "OK"
|
|
69
|
+
});
|
|
70
|
+
if (hasConfig) {
|
|
71
|
+
const config = await readConfig(projectRoot);
|
|
72
|
+
checks.push({
|
|
73
|
+
name: "Config",
|
|
74
|
+
pass: config !== null,
|
|
75
|
+
detail: config ? `stack: ${config.stack}, locales: ${config.locales.supported.join(", ")}` : "Failed to parse config.json"
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
if (hasCrDir) try {
|
|
79
|
+
const models = await listModels(projectRoot);
|
|
80
|
+
let allParseable = true;
|
|
81
|
+
for (const m of models) if (!await readModel(projectRoot, m.id)) allParseable = false;
|
|
82
|
+
checks.push({
|
|
83
|
+
name: "Models",
|
|
84
|
+
pass: allParseable,
|
|
85
|
+
detail: `${models.length} model(s)${allParseable ? ", all valid" : ", some failed to parse"}`
|
|
86
|
+
});
|
|
87
|
+
} catch {
|
|
88
|
+
checks.push({
|
|
89
|
+
name: "Models",
|
|
90
|
+
pass: false,
|
|
91
|
+
detail: "Failed to read models"
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
if (hasCrDir) {
|
|
95
|
+
const orphans = await findOrphanContent(projectRoot);
|
|
96
|
+
checks.push({
|
|
97
|
+
name: "Orphan content",
|
|
98
|
+
pass: orphans.length === 0,
|
|
99
|
+
detail: orphans.length === 0 ? "None" : `Found: ${orphans.join(", ")}`
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
if (hasGit) try {
|
|
103
|
+
const staleCount = (await simpleGit(projectRoot).branch(["--list", "contentrain/*"])).all.length;
|
|
104
|
+
checks.push({
|
|
105
|
+
name: "Pending branches",
|
|
106
|
+
pass: staleCount < 50,
|
|
107
|
+
detail: staleCount === 0 ? "None" : staleCount >= 80 ? `${staleCount} branches (BLOCKED — limit: 80)` : staleCount >= 50 ? `${staleCount} branches (WARNING — limit: 50)` : `${staleCount} contentrain branch(es)`
|
|
108
|
+
});
|
|
109
|
+
} catch {
|
|
110
|
+
checks.push({
|
|
111
|
+
name: "Pending branches",
|
|
112
|
+
pass: true,
|
|
113
|
+
detail: "Could not check"
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
const clientDir = join(crDir, "client");
|
|
117
|
+
const modelsDir = join(crDir, "models");
|
|
118
|
+
if (await pathExists(clientDir) && await pathExists(modelsDir)) try {
|
|
119
|
+
const clientStat = await stat(clientDir);
|
|
120
|
+
const modelsStat = await stat(modelsDir);
|
|
121
|
+
const fresh = clientStat.mtimeMs >= modelsStat.mtimeMs;
|
|
122
|
+
checks.push({
|
|
123
|
+
name: "SDK client",
|
|
124
|
+
pass: fresh,
|
|
125
|
+
detail: fresh ? "Up to date" : "Stale — run `contentrain generate`"
|
|
126
|
+
});
|
|
127
|
+
} catch {
|
|
128
|
+
checks.push({
|
|
129
|
+
name: "SDK client",
|
|
130
|
+
pass: true,
|
|
131
|
+
detail: "Could not check"
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
s.stop("Health checks complete");
|
|
135
|
+
const passed = checks.filter((c) => c.pass).length;
|
|
136
|
+
const failed = checks.filter((c) => !c.pass).length;
|
|
137
|
+
for (const check of checks) log.message(`${statusIcon(check.pass)} ${pc.bold(check.name)}: ${check.detail}`);
|
|
138
|
+
if (failed === 0) outro(pc.green(`All ${passed} checks passed!`));
|
|
139
|
+
else outro(pc.yellow(`${passed} passed, ${failed} failed`));
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
async function findOrphanContent(projectRoot) {
|
|
143
|
+
const crDir = contentrainDir(projectRoot);
|
|
144
|
+
const models = await listModels(projectRoot);
|
|
145
|
+
const orphans = [];
|
|
146
|
+
const knownContentDirs = /* @__PURE__ */ new Set();
|
|
147
|
+
for (const m of models) {
|
|
148
|
+
const full = await readModel(projectRoot, m.id);
|
|
149
|
+
const modelForPath = full ? {
|
|
150
|
+
...full,
|
|
151
|
+
content_path: full.content_path ?? m.content_path
|
|
152
|
+
} : {
|
|
153
|
+
id: m.id,
|
|
154
|
+
name: m.id,
|
|
155
|
+
kind: m.kind,
|
|
156
|
+
domain: m.domain,
|
|
157
|
+
i18n: m.i18n,
|
|
158
|
+
fields: {},
|
|
159
|
+
content_path: m.content_path
|
|
160
|
+
};
|
|
161
|
+
knownContentDirs.add(resolveContentDir(projectRoot, modelForPath));
|
|
162
|
+
}
|
|
163
|
+
const contentDir = join(crDir, "content");
|
|
164
|
+
if (await pathExists(contentDir)) {
|
|
165
|
+
const domains = await readDir(contentDir);
|
|
166
|
+
for (const domain of domains) {
|
|
167
|
+
const domainDir = join(contentDir, domain);
|
|
168
|
+
const entries = await readDir(domainDir);
|
|
169
|
+
for (const entry of entries) {
|
|
170
|
+
if (entry === ".gitkeep") continue;
|
|
171
|
+
const entryDir = join(domainDir, entry);
|
|
172
|
+
if (!knownContentDirs.has(entryDir)) orphans.push(`${domain}/${entry}`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
for (const dir of knownContentDirs) {
|
|
177
|
+
if (dir.startsWith(contentDir)) continue;
|
|
178
|
+
if (!await pathExists(dir)) {
|
|
179
|
+
orphans.push(`(missing custom path) ${dir}`);
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
await readDir(dir);
|
|
183
|
+
}
|
|
184
|
+
return orphans;
|
|
185
|
+
}
|
|
186
|
+
//#endregion
|
|
187
|
+
export { doctor_default as default };
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { n as requireInitialized, r as resolveProjectRoot, t as loadProjectContext } from "./context-An3xrvcP.mjs";
|
|
2
|
+
import { i as pc } from "./ui-B5mXontH.mjs";
|
|
3
|
+
import { defineCommand } from "citty";
|
|
4
|
+
import { intro, log, outro, spinner } from "@clack/prompts";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { contentrainDir } from "@contentrain/mcp/util/fs";
|
|
7
|
+
import { watch } from "node:fs";
|
|
8
|
+
//#region src/utils/watch.ts
|
|
9
|
+
const watchPath = watch;
|
|
10
|
+
//#endregion
|
|
11
|
+
//#region src/commands/generate.ts
|
|
12
|
+
var generate_default = defineCommand({
|
|
13
|
+
meta: {
|
|
14
|
+
name: "generate",
|
|
15
|
+
description: "Generate typed SDK client from models"
|
|
16
|
+
},
|
|
17
|
+
args: {
|
|
18
|
+
root: {
|
|
19
|
+
type: "string",
|
|
20
|
+
description: "Project root path",
|
|
21
|
+
required: false
|
|
22
|
+
},
|
|
23
|
+
watch: {
|
|
24
|
+
type: "boolean",
|
|
25
|
+
description: "Watch for changes and regenerate",
|
|
26
|
+
required: false
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
async run({ args }) {
|
|
30
|
+
const projectRoot = await resolveProjectRoot(args.root);
|
|
31
|
+
requireInitialized(await loadProjectContext(projectRoot));
|
|
32
|
+
intro(pc.bold("contentrain generate"));
|
|
33
|
+
const s = spinner();
|
|
34
|
+
s.start("Generating SDK client...");
|
|
35
|
+
try {
|
|
36
|
+
const { generate } = await import("@contentrain/query/generate");
|
|
37
|
+
const result = await generate({ projectRoot });
|
|
38
|
+
s.stop("SDK client generated");
|
|
39
|
+
log.success(`Output: ${pc.cyan(".contentrain/client/")}`);
|
|
40
|
+
log.message(` Files: ${result.generatedFiles.length}`);
|
|
41
|
+
log.message(` Types: ${result.typesCount}`);
|
|
42
|
+
log.message(` Data: ${result.dataModulesCount} modules`);
|
|
43
|
+
if (result.packageJsonUpdated) log.info(`${pc.cyan("#contentrain")} imports added to package.json`);
|
|
44
|
+
if (args.watch) {
|
|
45
|
+
log.info("Watching for changes... (Ctrl+C to stop)");
|
|
46
|
+
const crDir = contentrainDir(projectRoot);
|
|
47
|
+
const dirsToWatch = [
|
|
48
|
+
join(crDir, "models"),
|
|
49
|
+
join(crDir, "content"),
|
|
50
|
+
join(crDir, "config.json")
|
|
51
|
+
];
|
|
52
|
+
let debounce = null;
|
|
53
|
+
for (const dir of dirsToWatch) watchPath(dir, { recursive: true }, () => {
|
|
54
|
+
if (debounce) clearTimeout(debounce);
|
|
55
|
+
debounce = setTimeout(async () => {
|
|
56
|
+
log.info("Changes detected, regenerating...");
|
|
57
|
+
try {
|
|
58
|
+
const r = await generate({ projectRoot });
|
|
59
|
+
log.success(`Regenerated: ${r.generatedFiles.length} files`);
|
|
60
|
+
} catch (err) {
|
|
61
|
+
log.error(`Regeneration failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
62
|
+
}
|
|
63
|
+
}, 500);
|
|
64
|
+
});
|
|
65
|
+
await new Promise(() => {});
|
|
66
|
+
}
|
|
67
|
+
} catch (error) {
|
|
68
|
+
s.stop("Generation failed");
|
|
69
|
+
log.error(error instanceof Error ? error.message : String(error));
|
|
70
|
+
process.exitCode = 1;
|
|
71
|
+
}
|
|
72
|
+
outro("");
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
//#endregion
|
|
76
|
+
export { generate_default as default };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { defineCommand, runMain } from "citty";
|
|
3
|
+
//#region src/index.ts
|
|
4
|
+
runMain(defineCommand({
|
|
5
|
+
meta: {
|
|
6
|
+
name: "contentrain",
|
|
7
|
+
version: "0.1.0",
|
|
8
|
+
description: "Contentrain CLI — AI content governance infrastructure"
|
|
9
|
+
},
|
|
10
|
+
subCommands: {
|
|
11
|
+
init: () => import("./init-D5L_7bKE.mjs").then((m) => m.default),
|
|
12
|
+
status: () => import("./status-kF5miVty.mjs").then((m) => m.default),
|
|
13
|
+
doctor: () => import("./doctor-DyKjAIKH.mjs").then((m) => m.default),
|
|
14
|
+
validate: () => import("./validate-DnVamp3p.mjs").then((m) => m.default),
|
|
15
|
+
serve: () => import("./serve-CaCZMeHF.mjs").then((m) => m.default),
|
|
16
|
+
generate: () => import("./generate-DDEDRLdy.mjs").then((m) => m.default),
|
|
17
|
+
diff: () => import("./diff-otz0kGqT.mjs").then((m) => m.default)
|
|
18
|
+
}
|
|
19
|
+
}));
|
|
20
|
+
//#endregion
|
|
21
|
+
export {};
|