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.
Files changed (77) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +168 -0
  3. package/dist/context-An3xrvcP.mjs +50 -0
  4. package/dist/diff-otz0kGqT.mjs +116 -0
  5. package/dist/doctor-DyKjAIKH.mjs +187 -0
  6. package/dist/generate-DDEDRLdy.mjs +76 -0
  7. package/dist/index.mjs +21 -0
  8. package/dist/init-D5L_7bKE.mjs +415 -0
  9. package/dist/serve-CaCZMeHF.mjs +111 -0
  10. package/dist/serve-ui/assets/AgentPromptGroup.vue_vue_type_script_setup_true_lang-Dy-5bpEY.js +16 -0
  11. package/dist/serve-ui/assets/AppLayout-CJFq2ApF.js +16 -0
  12. package/dist/serve-ui/assets/BranchDetailPage-DzSu3JlE.js +17 -0
  13. package/dist/serve-ui/assets/BranchesPage-ZSG-l44J.js +6 -0
  14. package/dist/serve-ui/assets/CollapsibleTrigger.vue_vue_type_script_setup_true_lang-C4oejwcu.js +1 -0
  15. package/dist/serve-ui/assets/Collection-zzx9pNd2.js +6 -0
  16. package/dist/serve-ui/assets/ConfigProvider-BGjDLViI.js +1 -0
  17. package/dist/serve-ui/assets/ContentListPage-DTFaQB8N.js +51 -0
  18. package/dist/serve-ui/assets/ContentListPage-vXxAeSg8.css +1 -0
  19. package/dist/serve-ui/assets/ContentPage-4sQsAlY2.js +1 -0
  20. package/dist/serve-ui/assets/DashboardPage-CZDlnvAE.js +21 -0
  21. package/dist/serve-ui/assets/DialogTitle.vue_vue_type_script_setup_true_lang-CBu_dmA6.js +1 -0
  22. package/dist/serve-ui/assets/Input.vue_vue_type_script_setup_true_lang-otPltlZG.js +6 -0
  23. package/dist/serve-ui/assets/ModelDetailPage-BMquKCtH.js +49 -0
  24. package/dist/serve-ui/assets/ModelsPage-C_K-aWXf.js +1 -0
  25. package/dist/serve-ui/assets/NormalizePage-CHkT6yLV.js +11 -0
  26. package/dist/serve-ui/assets/PageHeader.vue_vue_type_script_setup_true_lang-C7aWxNSw.js +11 -0
  27. package/dist/serve-ui/assets/Presence-cbB6AqkQ.js +3 -0
  28. package/dist/serve-ui/assets/Separator.vue_vue_type_script_setup_true_lang-fePFl24S.js +1 -0
  29. package/dist/serve-ui/assets/StudioHint.vue_vue_type_script_setup_true_lang-B45TG2r1.js +6 -0
  30. package/dist/serve-ui/assets/TrustBadge.vue_vue_type_script_setup_true_lang-CgoQGS6k.js +11 -0
  31. package/dist/serve-ui/assets/ValidatePage-BW8DO6WC.js +21 -0
  32. package/dist/serve-ui/assets/arrow-down-DqOfGL7C.js +6 -0
  33. package/dist/serve-ui/assets/arrow-left-BRvgeveW.js +6 -0
  34. package/dist/serve-ui/assets/book-marked-D8StxRZP.js +6 -0
  35. package/dist/serve-ui/assets/book-open-BrGAe3tk.js +6 -0
  36. package/dist/serve-ui/assets/box-7olVZr5D.js +6 -0
  37. package/dist/serve-ui/assets/chevron-down-B9YdCtsj.js +6 -0
  38. package/dist/serve-ui/assets/chevron-right-DFKsmZVd.js +6 -0
  39. package/dist/serve-ui/assets/content-BQE3NEm5.js +1 -0
  40. package/dist/serve-ui/assets/database-BGEFn4Oq.js +6 -0
  41. package/dist/serve-ui/assets/file-code-BGiS4dun.js +6 -0
  42. package/dist/serve-ui/assets/file-text-BDHXQ95W.js +6 -0
  43. package/dist/serve-ui/assets/getActiveElement-FUKBWQwh.js +1 -0
  44. package/dist/serve-ui/assets/git-branch-DdE7bk9-.js +6 -0
  45. package/dist/serve-ui/assets/git-merge-iStuJS69.js +6 -0
  46. package/dist/serve-ui/assets/hash-B-ibLpM1.js +6 -0
  47. package/dist/serve-ui/assets/index-C-RjMzFm.css +1 -0
  48. package/dist/serve-ui/assets/index-CoUshYEB.js +80 -0
  49. package/dist/serve-ui/assets/index-WHgvdYOs.js +1 -0
  50. package/dist/serve-ui/assets/languages-BTcd2Rl1.js +6 -0
  51. package/dist/serve-ui/assets/model-empty-state-B6AM4bil.js +1 -0
  52. package/dist/serve-ui/assets/plus-D8Nd3YnG.js +11 -0
  53. package/dist/serve-ui/assets/project-B1qSm0Fz.js +1 -0
  54. package/dist/serve-ui/assets/scan-search-DGPL_VJu.js +6 -0
  55. package/dist/serve-ui/assets/settings-DL-0RaxD.js +6 -0
  56. package/dist/serve-ui/assets/shield-check-CUL7KkSA.js +6 -0
  57. package/dist/serve-ui/assets/trash-2-83QhL6-T.js +11 -0
  58. package/dist/serve-ui/assets/useDirection-Cd3C7VdT.js +1 -0
  59. package/dist/serve-ui/assets/useFormatters-DgWFpsAv.js +1 -0
  60. package/dist/serve-ui/assets/useId-C0uR6mdj.js +1 -0
  61. package/dist/serve-ui/assets/useNonce-DTGvzOJz.js +1 -0
  62. package/dist/serve-ui/assets/useWatch-COpVIQvm.js +1 -0
  63. package/dist/serve-ui/favicon.svg +19 -0
  64. package/dist/serve-ui/for-developers-1.svg +65 -0
  65. package/dist/serve-ui/icon-color.svg +18 -0
  66. package/dist/serve-ui/index.html +17 -0
  67. package/dist/serve-ui/logo/color-icon-black-text.svg +65 -0
  68. package/dist/serve-ui/logo/color-icon-white-text.svg +61 -0
  69. package/dist/serve-ui/merge-1.svg +131 -0
  70. package/dist/serve-ui/merge-2.svg +161 -0
  71. package/dist/serve-ui/model-empty-state.svg +12 -0
  72. package/dist/serve-ui/select-data-type.svg +23 -0
  73. package/dist/server-DW0KXB0T.mjs +607 -0
  74. package/dist/status-kF5miVty.mjs +122 -0
  75. package/dist/ui-B5mXontH.mjs +33 -0
  76. package/dist/validate-DnVamp3p.mjs +155 -0
  77. 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 {};