@contentrain/mcp 1.6.0 → 1.8.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 +8 -7
- package/dist/{apply-manager-BJ3nKEls.mjs → apply-manager-BRmB27H5.mjs} +5 -5
- package/dist/{apply-manager-BJ3nKEls.mjs.map → apply-manager-BRmB27H5.mjs.map} +1 -1
- package/dist/branch-lifecycle-7dQoF22r.d.mts +141 -0
- package/dist/branch-lifecycle-7dQoF22r.d.mts.map +1 -0
- package/dist/branch-lifecycle-D7tXOE4f.mjs +444 -0
- package/dist/branch-lifecycle-D7tXOE4f.mjs.map +1 -0
- package/dist/{config-Lw7vpK9n.mjs → config-DlFcTxFG.mjs} +3 -2
- package/dist/config-DlFcTxFG.mjs.map +1 -0
- package/dist/{context-DS40WOdw.mjs → context-CYN__o3Q.mjs} +24 -11
- package/dist/context-CYN__o3Q.mjs.map +1 -0
- package/dist/core/apply-manager.mjs +6 -6
- package/dist/core/config.d.mts.map +1 -1
- package/dist/core/config.mjs +1 -1
- package/dist/core/context.d.mts +20 -2
- package/dist/core/context.d.mts.map +1 -1
- package/dist/core/context.mjs +2 -2
- package/dist/core/doctor.mjs +3 -3
- package/dist/core/validator/index.mjs +2 -2
- package/dist/{detect-DL9mFmaS.mjs → detect-eWS6lvoH.mjs} +1 -1
- package/dist/{detect-DL9mFmaS.mjs.map → detect-eWS6lvoH.mjs.map} +1 -1
- package/dist/{doctor-Dxu_SzZy.mjs → doctor-qiUKCy9b.mjs} +34 -15
- package/dist/doctor-qiUKCy9b.mjs.map +1 -0
- package/dist/git/branch-lifecycle.d.mts +2 -57
- package/dist/git/branch-lifecycle.mjs +3 -3
- package/dist/git/transaction.d.mts +2 -0
- package/dist/git/transaction.d.mts.map +1 -1
- package/dist/git/transaction.mjs +5 -4
- package/dist/{id-CIO9s0x_.mjs → id-DV_T9Ic8.mjs} +1 -1
- package/dist/{id-CIO9s0x_.mjs.map → id-DV_T9Ic8.mjs.map} +1 -1
- package/dist/index.mjs +11 -11
- package/dist/{local-D56D9e5T.mjs → local-BZEUWBoC.mjs} +8 -11
- package/dist/local-BZEUWBoC.mjs.map +1 -0
- package/dist/providers/github/index.d.mts +23 -2
- package/dist/providers/github/index.d.mts.map +1 -1
- package/dist/providers/github/index.mjs +135 -51
- package/dist/providers/github/index.mjs.map +1 -1
- package/dist/providers/gitlab/index.d.mts +3 -1
- package/dist/providers/gitlab/index.d.mts.map +1 -1
- package/dist/providers/gitlab/index.mjs +46 -5
- package/dist/providers/gitlab/index.mjs.map +1 -1
- package/dist/providers/local/index.mjs +6 -5
- package/dist/server/http/index.d.mts +1 -1
- package/dist/server/http/index.mjs +11 -11
- package/dist/{server-CufhOunD.mjs → server-CXZFrEyu.mjs} +56 -22
- package/dist/server-CXZFrEyu.mjs.map +1 -0
- package/dist/{server-WqGx6ait.d.mts → server-DZBcZWhE.d.mts} +1 -1
- package/dist/{server-WqGx6ait.d.mts.map → server-DZBcZWhE.d.mts.map} +1 -1
- package/dist/server.d.mts +1 -1
- package/dist/server.mjs +11 -11
- package/dist/{transaction-LiTjan9Q.mjs → transaction-qliKvBRL.mjs} +104 -60
- package/dist/transaction-qliKvBRL.mjs.map +1 -0
- package/dist/util/detect.mjs +1 -1
- package/dist/util/id.mjs +1 -1
- package/dist/{validator-Bo06QPVB.mjs → validator-Z3DGNWQ6.mjs} +2 -2
- package/dist/{validator-Bo06QPVB.mjs.map → validator-Z3DGNWQ6.mjs.map} +1 -1
- package/package.json +2 -2
- package/dist/branch-lifecycle-DjX3MXNG.mjs +0 -149
- package/dist/branch-lifecycle-DjX3MXNG.mjs.map +0 -1
- package/dist/config-Lw7vpK9n.mjs.map +0 -1
- package/dist/context-DS40WOdw.mjs.map +0 -1
- package/dist/doctor-Dxu_SzZy.mjs.map +0 -1
- package/dist/git/branch-lifecycle.d.mts.map +0 -1
- package/dist/local-D56D9e5T.mjs.map +0 -1
- package/dist/server-CufhOunD.mjs.map +0 -1
- package/dist/transaction-LiTjan9Q.mjs.map +0 -1
package/dist/core/context.d.mts
CHANGED
|
@@ -2,6 +2,16 @@ import { a as FileChange, d as RepoReader } from "../index-D0wQJSgs.mjs";
|
|
|
2
2
|
import { ContextJson, ContextSource } from "@contentrain/types";
|
|
3
3
|
|
|
4
4
|
//#region src/core/context.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Pre-computed stats a caller can pass to {@link buildContextChange} to skip
|
|
7
|
+
* the model/entry scan. `entries` is nullable — a `null` is dropped from the
|
|
8
|
+
* emitted context.json by {@link canonicalStringify}, exactly as a failed
|
|
9
|
+
* scan is today.
|
|
10
|
+
*/
|
|
11
|
+
interface ContextStats {
|
|
12
|
+
models: number;
|
|
13
|
+
entries: number | null;
|
|
14
|
+
}
|
|
5
15
|
/**
|
|
6
16
|
* Read the committed `.contentrain/context.json` payload written by the
|
|
7
17
|
* most recent content or model operation. Returns `null` when the file
|
|
@@ -31,13 +41,21 @@ declare function writeContext(projectRoot: string, operation: {
|
|
|
31
41
|
* read surface. GitHubProvider pays an extra round trip per model; the
|
|
32
42
|
* payoff is a context.json that matches what the local write path emits,
|
|
33
43
|
* so cross-provider merges stay deterministic.
|
|
44
|
+
*
|
|
45
|
+
* A caller that already knows the model/entry counts (e.g. Studio deriving
|
|
46
|
+
* them from its own index) can pass `opts.stats` to skip that O(models·
|
|
47
|
+
* locales) scan entirely — only `readConfig` remains, for the locale list
|
|
48
|
+
* and the `lastOperation.locale` default. The emitted context.json is
|
|
49
|
+
* byte-identical to the scanned variant for the same logical state.
|
|
34
50
|
*/
|
|
35
51
|
declare function buildContextChange(reader: RepoReader, operation: {
|
|
36
52
|
tool: string;
|
|
37
53
|
model: string;
|
|
38
54
|
locale?: string;
|
|
39
55
|
entries?: string[];
|
|
40
|
-
}, source?: ContextSource
|
|
56
|
+
}, source?: ContextSource, opts?: {
|
|
57
|
+
stats?: ContextStats;
|
|
58
|
+
}): Promise<FileChange>;
|
|
41
59
|
//#endregion
|
|
42
|
-
export { buildContextChange, readContext, writeContext };
|
|
60
|
+
export { ContextStats, buildContextChange, readContext, writeContext };
|
|
43
61
|
//# sourceMappingURL=context.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"context.d.mts","names":[],"sources":["../../src/core/context.ts"],"mappings":";;;;;;
|
|
1
|
+
{"version":3,"file":"context.d.mts","names":[],"sources":["../../src/core/context.ts"],"mappings":";;;;;;AAgBA;;;;UAAiB,YAAA;EACf,MAAA;EACA,OAAA;AAAA;;;;;;;AAcF;;;;iBADgB,WAAA,CAAY,WAAA,WAAsB,OAAA,CAAQ,WAAA;AAAA,iBAC1C,WAAA,CAAY,MAAA,EAAQ,UAAA,GAAa,OAAA,CAAQ,WAAA;AAAA,iBAiCnC,YAAA,CACpB,WAAA,UACA,SAAA;EAAa,IAAA;EAAc,KAAA;EAAe,MAAA;EAAiB,OAAA;AAAA,IAC1D,OAAA;;;AAHH;;;;;;;;;;;;;AAiDA;;;iBAAsB,kBAAA,CACpB,MAAA,EAAQ,UAAA,EACR,SAAA;EAAa,IAAA;EAAc,KAAA;EAAe,MAAA;EAAiB,OAAA;AAAA,GAC3D,MAAA,GAAS,aAAA,EACT,IAAA;EAAS,KAAA,GAAQ,YAAA;AAAA,IAChB,OAAA,CAAQ,UAAA"}
|
package/dist/core/context.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import "../serialization-B1CEzR4H.mjs";
|
|
2
2
|
import "../fs-DLbVB-Ek.mjs";
|
|
3
|
-
import "../config-
|
|
3
|
+
import "../config-DlFcTxFG.mjs";
|
|
4
4
|
import "../model-manager-BhLsUgaB.mjs";
|
|
5
5
|
import "../meta-manager-C900rEyG.mjs";
|
|
6
|
-
import { n as readContext, r as writeContext, t as buildContextChange } from "../context-
|
|
6
|
+
import { n as readContext, r as writeContext, t as buildContextChange } from "../context-CYN__o3Q.mjs";
|
|
7
7
|
export { buildContextChange, readContext, writeContext };
|
package/dist/core/doctor.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import "../fs-DLbVB-Ek.mjs";
|
|
2
|
-
import "../config-
|
|
2
|
+
import "../config-DlFcTxFG.mjs";
|
|
3
3
|
import "../model-manager-BhLsUgaB.mjs";
|
|
4
4
|
import "../meta-manager-C900rEyG.mjs";
|
|
5
|
-
import "../branch-lifecycle-
|
|
5
|
+
import "../branch-lifecycle-D7tXOE4f.mjs";
|
|
6
6
|
import "../scan-config-jC6XmiqF.mjs";
|
|
7
|
-
import { t as runDoctor } from "../doctor-
|
|
7
|
+
import { t as runDoctor } from "../doctor-qiUKCy9b.mjs";
|
|
8
8
|
export { runDoctor };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import "../../fs-DLbVB-Ek.mjs";
|
|
2
|
-
import "../../config-
|
|
2
|
+
import "../../config-DlFcTxFG.mjs";
|
|
3
3
|
import "../../model-manager-BhLsUgaB.mjs";
|
|
4
4
|
import "../../meta-manager-C900rEyG.mjs";
|
|
5
|
-
import { i as validateContent, n as validateScheduleFields, r as checkRelationIntegrity, t as validateProject } from "../../validator-
|
|
5
|
+
import { i as validateContent, n as validateScheduleFields, r as checkRelationIntegrity, t as validateProject } from "../../validator-Z3DGNWQ6.mjs";
|
|
6
6
|
export { checkRelationIntegrity, validateContent, validateProject, validateScheduleFields };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"detect-DL9mFmaS.mjs","names":[],"sources":["../src/util/detect.ts"],"sourcesContent":["import type { StackType, Platform } from '@contentrain/types'\nimport { join, dirname } from 'node:path'\nimport { readJson, pathExists } from './fs.js'\n\ninterface PackageJson {\n dependencies?: Record<string, string>\n devDependencies?: Record<string, string>\n workspaces?: string[] | { packages: string[] }\n}\n\nexport interface StackInfo {\n stack: StackType\n platform: Platform\n name: string\n description: string\n monorepo: boolean\n monorepoTool?: string\n features: string[]\n}\n\nconst STACK_META: Record<StackType, { name: string; description: string }> = {\n // Meta-frameworks\n nuxt: { name: 'Nuxt', description: 'Vue meta-framework with SSR/SSG' },\n next: { name: 'Next.js', description: 'React framework with SSR/SSG' },\n astro: { name: 'Astro', description: 'Content-first web framework' },\n sveltekit: { name: 'SvelteKit', description: 'Svelte meta-framework' },\n remix: { name: 'Remix', description: 'Full-stack React framework' },\n analog: { name: 'Analog', description: 'Angular meta-framework' },\n // Plain frameworks\n vue: { name: 'Vue', description: 'Progressive JavaScript framework' },\n react: { name: 'React', description: 'UI library for web and native' },\n svelte: { name: 'Svelte', description: 'Compile-time UI framework' },\n solid: { name: 'Solid', description: 'Reactive UI library' },\n angular: { name: 'Angular', description: 'Platform for web applications' },\n // Mobile\n 'react-native': { name: 'React Native', description: 'Cross-platform mobile framework' },\n expo: { name: 'Expo', description: 'React Native toolchain' },\n flutter: { name: 'Flutter', description: 'Cross-platform UI toolkit (Dart)' },\n // Backend\n node: { name: 'Node.js', description: 'Server-side JavaScript runtime' },\n express: { name: 'Express', description: 'Minimal Node.js web framework' },\n fastify: { name: 'Fastify', description: 'Fast Node.js web framework' },\n nestjs: { name: 'NestJS', description: 'Progressive Node.js framework' },\n django: { name: 'Django', description: 'Python web framework' },\n rails: { name: 'Rails', description: 'Ruby web framework' },\n laravel: { name: 'Laravel', description: 'PHP web framework' },\n go: { name: 'Go', description: 'Go programming language' },\n rust: { name: 'Rust', description: 'Rust programming language' },\n dotnet: { name: '.NET', description: 'Microsoft .NET platform' },\n // Static\n hugo: { name: 'Hugo', description: 'Go-based static site generator' },\n jekyll: { name: 'Jekyll', description: 'Ruby-based static site generator' },\n eleventy: { name: 'Eleventy', description: 'JavaScript static site generator' },\n // Desktop\n electron: { name: 'Electron', description: 'Cross-platform desktop apps' },\n tauri: { name: 'Tauri', description: 'Lightweight desktop apps (Rust)' },\n // Catch-all\n other: { name: 'Other', description: 'Unknown or unsupported framework' },\n}\n\nexport function inferPlatform(stack: StackType): Platform {\n switch (stack) {\n case 'nuxt': case 'next': case 'astro': case 'sveltekit': case 'remix': case 'analog':\n case 'vue': case 'react': case 'svelte': case 'solid': case 'angular':\n return 'web'\n case 'react-native': case 'expo': case 'flutter':\n return 'mobile'\n case 'node': case 'express': case 'fastify': case 'nestjs':\n case 'django': case 'rails': case 'laravel': case 'go': case 'rust': case 'dotnet':\n return 'api'\n case 'electron': case 'tauri':\n return 'desktop'\n case 'hugo': case 'jekyll': case 'eleventy':\n return 'static'\n default:\n return 'other'\n }\n}\n\n/** Backward-compatible: returns just the StackType string */\nexport async function detectStack(projectRoot: string): Promise<StackType> {\n const info = await detectStackInfo(projectRoot)\n return info.stack\n}\n\n/** Rich detection: stack + platform + monorepo + features */\nexport async function detectStackInfo(projectRoot: string): Promise<StackInfo> {\n let stack = await detectFromDeps(projectRoot)\n\n // Walk up for monorepo root if local didn't match\n if (stack === 'other') {\n let dir = dirname(projectRoot)\n for (let i = 0; i < 5; i++) {\n const pkg = await readJson<PackageJson>(join(dir, 'package.json'))\n if (pkg?.workspaces) {\n const rootStack = await detectFromDeps(dir)\n if (rootStack !== 'other') {\n stack = rootStack\n break\n }\n }\n const parent = dirname(dir)\n if (parent === dir) break\n dir = parent\n }\n }\n\n // Non-JS detection if still 'other'\n if (stack === 'other') {\n stack = await detectNonJs(projectRoot)\n }\n\n const mono = await detectMonorepo(projectRoot)\n const features = await detectFeatures(projectRoot)\n const meta = STACK_META[stack]\n\n return {\n stack,\n platform: inferPlatform(stack),\n name: meta.name,\n description: meta.description,\n monorepo: mono.isMonorepo,\n monorepoTool: mono.tool,\n features,\n }\n}\n\nasync function detectFromDeps(dir: string): Promise<StackType> {\n const pkg = await readJson<PackageJson>(join(dir, 'package.json'))\n if (!pkg) return 'other'\n\n const allDeps = { ...pkg.dependencies, ...pkg.devDependencies }\n\n // Meta-frameworks (check BEFORE plain frameworks)\n if ('nuxt' in allDeps) return 'nuxt'\n if ('next' in allDeps) return 'next'\n if ('astro' in allDeps) return 'astro'\n if ('@sveltejs/kit' in allDeps) return 'sveltekit'\n if ('@remix-run/node' in allDeps || 'remix' in allDeps) return 'remix'\n if ('@analogjs/platform' in allDeps) return 'analog'\n\n // Mobile (check before plain react)\n if ('expo' in allDeps) return 'expo'\n if ('react-native' in allDeps) return 'react-native'\n\n // Plain frameworks\n if ('vue' in allDeps) return 'vue'\n if ('svelte' in allDeps) return 'svelte'\n if ('solid-js' in allDeps) return 'solid'\n if ('@angular/core' in allDeps) return 'angular'\n if ('react' in allDeps) return 'react'\n\n // Desktop\n if ('electron' in allDeps) return 'electron'\n if ('@tauri-apps/api' in allDeps) return 'tauri'\n\n // Backend (Node.js)\n if ('@nestjs/core' in allDeps) return 'nestjs'\n if ('fastify' in allDeps) return 'fastify'\n if ('express' in allDeps) return 'express'\n\n // Static site generators\n if ('@11ty/eleventy' in allDeps) return 'eleventy'\n\n // Generic Node.js (has package.json but no framework)\n if ('node' in (pkg.dependencies ?? {})) return 'node'\n\n return 'other'\n}\n\nasync function detectNonJs(projectRoot: string): Promise<StackType> {\n // Go\n if (await pathExists(join(projectRoot, 'go.mod'))) return 'go'\n\n // Rust\n if (await pathExists(join(projectRoot, 'Cargo.toml'))) return 'rust'\n\n // Python\n for (const f of ['requirements.txt', 'pyproject.toml', 'setup.py']) {\n if (await pathExists(join(projectRoot, f))) {\n // Check for Django\n try {\n const { readText } = await import('./fs.js')\n const content = await readText(join(projectRoot, f))\n if (content?.includes('django') || content?.includes('Django')) return 'django'\n } catch { /* ignore */ }\n return 'other' // Generic Python — no specific stack type yet\n }\n }\n\n // Ruby\n if (await pathExists(join(projectRoot, 'Gemfile'))) {\n try {\n const { readText } = await import('./fs.js')\n const content = await readText(join(projectRoot, 'Gemfile'))\n if (content?.includes('rails')) return 'rails'\n } catch { /* ignore */ }\n return 'other'\n }\n\n // .NET\n const dotnetFiles = ['*.csproj', '*.sln']\n for (const pattern of dotnetFiles) {\n if (pattern === '*.csproj' && await pathExists(join(projectRoot, 'Program.cs'))) return 'dotnet'\n if (pattern === '*.sln' && await pathExists(join(projectRoot, 'Program.cs'))) return 'dotnet'\n }\n\n // PHP / Laravel\n if (await pathExists(join(projectRoot, 'composer.json'))) {\n try {\n const composer = await readJson<Record<string, unknown>>(join(projectRoot, 'composer.json'))\n const require = composer?.require as Record<string, string> | undefined\n if (require?.['laravel/framework']) return 'laravel'\n } catch { /* ignore */ }\n return 'other'\n }\n\n // Flutter (Dart)\n if (await pathExists(join(projectRoot, 'pubspec.yaml'))) return 'flutter'\n\n // Hugo\n if (await pathExists(join(projectRoot, 'hugo.toml'))) return 'hugo'\n if (await pathExists(join(projectRoot, 'config.toml'))) {\n try {\n const { readText } = await import('./fs.js')\n const content = await readText(join(projectRoot, 'config.toml'))\n if (content?.includes('baseURL') || content?.includes('hugo')) return 'hugo'\n } catch { /* ignore */ }\n }\n\n // Jekyll\n if (await pathExists(join(projectRoot, '_config.yml'))) return 'jekyll'\n\n return 'other'\n}\n\nasync function detectMonorepo(projectRoot: string): Promise<{ isMonorepo: boolean; tool?: string }> {\n if (await pathExists(join(projectRoot, 'pnpm-workspace.yaml'))) return { isMonorepo: true, tool: 'pnpm workspaces' }\n if (await pathExists(join(projectRoot, 'lerna.json'))) return { isMonorepo: true, tool: 'Lerna' }\n if (await pathExists(join(projectRoot, 'nx.json'))) return { isMonorepo: true, tool: 'Nx' }\n if (await pathExists(join(projectRoot, 'turbo.json'))) return { isMonorepo: true, tool: 'Turborepo' }\n\n const pkg = await readJson<PackageJson>(join(projectRoot, 'package.json'))\n if (pkg?.workspaces) return { isMonorepo: true, tool: 'npm/yarn workspaces' }\n\n // Walk up\n let dir = dirname(projectRoot)\n for (let i = 0; i < 5; i++) {\n if (await pathExists(join(dir, 'pnpm-workspace.yaml'))) return { isMonorepo: true, tool: 'pnpm workspaces' }\n if (await pathExists(join(dir, 'lerna.json'))) return { isMonorepo: true, tool: 'Lerna' }\n if (await pathExists(join(dir, 'nx.json'))) return { isMonorepo: true, tool: 'Nx' }\n if (await pathExists(join(dir, 'turbo.json'))) return { isMonorepo: true, tool: 'Turborepo' }\n const parentPkg = await readJson<PackageJson>(join(dir, 'package.json'))\n if (parentPkg?.workspaces) return { isMonorepo: true, tool: 'npm/yarn workspaces' }\n const parent = dirname(dir)\n if (parent === dir) break\n dir = parent\n }\n\n return { isMonorepo: false }\n}\n\nasync function detectFeatures(projectRoot: string): Promise<string[]> {\n const features: string[] = []\n const allDeps = await collectAllDeps(projectRoot)\n\n if ('typescript' in allDeps || await pathExists(join(projectRoot, 'tsconfig.json'))) features.push('TypeScript')\n\n const i18nLibs = ['vue-i18n', '@nuxtjs/i18n', 'next-intl', 'react-intl', 'i18next', 'react-i18next']\n for (const lib of i18nLibs) {\n if (lib in allDeps) { features.push(`i18n (${lib})`); break }\n }\n\n if ('tailwindcss' in allDeps) features.push('Tailwind CSS')\n else if ('sass' in allDeps || 'scss' in allDeps) features.push('Sass/SCSS')\n\n if ('vitest' in allDeps) features.push('Vitest')\n else if ('jest' in allDeps) features.push('Jest')\n\n if ('pinia' in allDeps) features.push('Pinia')\n else if ('zustand' in allDeps) features.push('Zustand')\n else if ('@reduxjs/toolkit' in allDeps || 'redux' in allDeps) features.push('Redux')\n\n return features\n}\n\nasync function collectAllDeps(projectRoot: string): Promise<Record<string, string>> {\n const pkg = await readJson<PackageJson>(join(projectRoot, 'package.json'))\n const allDeps: Record<string, string> = { ...pkg?.dependencies, ...pkg?.devDependencies }\n\n let dir = dirname(projectRoot)\n for (let i = 0; i < 5; i++) {\n const parentPkg = await readJson<PackageJson>(join(dir, 'package.json'))\n if (parentPkg?.workspaces) {\n Object.assign(allDeps, parentPkg.dependencies, parentPkg.devDependencies)\n break\n }\n const parent = dirname(dir)\n if (parent === dir) break\n dir = parent\n }\n\n return allDeps\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"detect-eWS6lvoH.mjs","names":[],"sources":["../src/util/detect.ts"],"sourcesContent":["import type { StackType, Platform } from '@contentrain/types'\nimport { join, dirname } from 'node:path'\nimport { readJson, pathExists } from './fs.js'\n\ninterface PackageJson {\n dependencies?: Record<string, string>\n devDependencies?: Record<string, string>\n workspaces?: string[] | { packages: string[] }\n}\n\nexport interface StackInfo {\n stack: StackType\n platform: Platform\n name: string\n description: string\n monorepo: boolean\n monorepoTool?: string\n features: string[]\n}\n\nconst STACK_META: Record<StackType, { name: string; description: string }> = {\n // Meta-frameworks\n nuxt: { name: 'Nuxt', description: 'Vue meta-framework with SSR/SSG' },\n next: { name: 'Next.js', description: 'React framework with SSR/SSG' },\n astro: { name: 'Astro', description: 'Content-first web framework' },\n sveltekit: { name: 'SvelteKit', description: 'Svelte meta-framework' },\n remix: { name: 'Remix', description: 'Full-stack React framework' },\n analog: { name: 'Analog', description: 'Angular meta-framework' },\n // Plain frameworks\n vue: { name: 'Vue', description: 'Progressive JavaScript framework' },\n react: { name: 'React', description: 'UI library for web and native' },\n svelte: { name: 'Svelte', description: 'Compile-time UI framework' },\n solid: { name: 'Solid', description: 'Reactive UI library' },\n angular: { name: 'Angular', description: 'Platform for web applications' },\n // Mobile\n 'react-native': { name: 'React Native', description: 'Cross-platform mobile framework' },\n expo: { name: 'Expo', description: 'React Native toolchain' },\n flutter: { name: 'Flutter', description: 'Cross-platform UI toolkit (Dart)' },\n // Backend\n node: { name: 'Node.js', description: 'Server-side JavaScript runtime' },\n express: { name: 'Express', description: 'Minimal Node.js web framework' },\n fastify: { name: 'Fastify', description: 'Fast Node.js web framework' },\n nestjs: { name: 'NestJS', description: 'Progressive Node.js framework' },\n django: { name: 'Django', description: 'Python web framework' },\n rails: { name: 'Rails', description: 'Ruby web framework' },\n laravel: { name: 'Laravel', description: 'PHP web framework' },\n go: { name: 'Go', description: 'Go programming language' },\n rust: { name: 'Rust', description: 'Rust programming language' },\n dotnet: { name: '.NET', description: 'Microsoft .NET platform' },\n // Static\n hugo: { name: 'Hugo', description: 'Go-based static site generator' },\n jekyll: { name: 'Jekyll', description: 'Ruby-based static site generator' },\n eleventy: { name: 'Eleventy', description: 'JavaScript static site generator' },\n // Desktop\n electron: { name: 'Electron', description: 'Cross-platform desktop apps' },\n tauri: { name: 'Tauri', description: 'Lightweight desktop apps (Rust)' },\n // Catch-all\n other: { name: 'Other', description: 'Unknown or unsupported framework' },\n}\n\nexport function inferPlatform(stack: StackType): Platform {\n switch (stack) {\n case 'nuxt': case 'next': case 'astro': case 'sveltekit': case 'remix': case 'analog':\n case 'vue': case 'react': case 'svelte': case 'solid': case 'angular':\n return 'web'\n case 'react-native': case 'expo': case 'flutter':\n return 'mobile'\n case 'node': case 'express': case 'fastify': case 'nestjs':\n case 'django': case 'rails': case 'laravel': case 'go': case 'rust': case 'dotnet':\n return 'api'\n case 'electron': case 'tauri':\n return 'desktop'\n case 'hugo': case 'jekyll': case 'eleventy':\n return 'static'\n default:\n return 'other'\n }\n}\n\n/** Backward-compatible: returns just the StackType string */\nexport async function detectStack(projectRoot: string): Promise<StackType> {\n const info = await detectStackInfo(projectRoot)\n return info.stack\n}\n\n/** Rich detection: stack + platform + monorepo + features */\nexport async function detectStackInfo(projectRoot: string): Promise<StackInfo> {\n let stack = await detectFromDeps(projectRoot)\n\n // Walk up for monorepo root if local didn't match\n if (stack === 'other') {\n let dir = dirname(projectRoot)\n for (let i = 0; i < 5; i++) {\n const pkg = await readJson<PackageJson>(join(dir, 'package.json'))\n if (pkg?.workspaces) {\n const rootStack = await detectFromDeps(dir)\n if (rootStack !== 'other') {\n stack = rootStack\n break\n }\n }\n const parent = dirname(dir)\n if (parent === dir) break\n dir = parent\n }\n }\n\n // Non-JS detection if still 'other'\n if (stack === 'other') {\n stack = await detectNonJs(projectRoot)\n }\n\n const mono = await detectMonorepo(projectRoot)\n const features = await detectFeatures(projectRoot)\n const meta = STACK_META[stack]\n\n return {\n stack,\n platform: inferPlatform(stack),\n name: meta.name,\n description: meta.description,\n monorepo: mono.isMonorepo,\n monorepoTool: mono.tool,\n features,\n }\n}\n\nasync function detectFromDeps(dir: string): Promise<StackType> {\n const pkg = await readJson<PackageJson>(join(dir, 'package.json'))\n if (!pkg) return 'other'\n\n const allDeps = { ...pkg.dependencies, ...pkg.devDependencies }\n\n // Meta-frameworks (check BEFORE plain frameworks)\n if ('nuxt' in allDeps) return 'nuxt'\n if ('next' in allDeps) return 'next'\n if ('astro' in allDeps) return 'astro'\n if ('@sveltejs/kit' in allDeps) return 'sveltekit'\n if ('@remix-run/node' in allDeps || 'remix' in allDeps) return 'remix'\n if ('@analogjs/platform' in allDeps) return 'analog'\n\n // Mobile (check before plain react)\n if ('expo' in allDeps) return 'expo'\n if ('react-native' in allDeps) return 'react-native'\n\n // Plain frameworks\n if ('vue' in allDeps) return 'vue'\n if ('svelte' in allDeps) return 'svelte'\n if ('solid-js' in allDeps) return 'solid'\n if ('@angular/core' in allDeps) return 'angular'\n if ('react' in allDeps) return 'react'\n\n // Desktop\n if ('electron' in allDeps) return 'electron'\n if ('@tauri-apps/api' in allDeps) return 'tauri'\n\n // Backend (Node.js)\n if ('@nestjs/core' in allDeps) return 'nestjs'\n if ('fastify' in allDeps) return 'fastify'\n if ('express' in allDeps) return 'express'\n\n // Static site generators\n if ('@11ty/eleventy' in allDeps) return 'eleventy'\n\n // Generic Node.js (has package.json but no framework)\n if ('node' in (pkg.dependencies ?? {})) return 'node'\n\n return 'other'\n}\n\nasync function detectNonJs(projectRoot: string): Promise<StackType> {\n // Go\n if (await pathExists(join(projectRoot, 'go.mod'))) return 'go'\n\n // Rust\n if (await pathExists(join(projectRoot, 'Cargo.toml'))) return 'rust'\n\n // Python\n for (const f of ['requirements.txt', 'pyproject.toml', 'setup.py']) {\n if (await pathExists(join(projectRoot, f))) {\n // Check for Django\n try {\n const { readText } = await import('./fs.js')\n const content = await readText(join(projectRoot, f))\n if (content?.includes('django') || content?.includes('Django')) return 'django'\n } catch { /* ignore */ }\n return 'other' // Generic Python — no specific stack type yet\n }\n }\n\n // Ruby\n if (await pathExists(join(projectRoot, 'Gemfile'))) {\n try {\n const { readText } = await import('./fs.js')\n const content = await readText(join(projectRoot, 'Gemfile'))\n if (content?.includes('rails')) return 'rails'\n } catch { /* ignore */ }\n return 'other'\n }\n\n // .NET\n const dotnetFiles = ['*.csproj', '*.sln']\n for (const pattern of dotnetFiles) {\n if (pattern === '*.csproj' && await pathExists(join(projectRoot, 'Program.cs'))) return 'dotnet'\n if (pattern === '*.sln' && await pathExists(join(projectRoot, 'Program.cs'))) return 'dotnet'\n }\n\n // PHP / Laravel\n if (await pathExists(join(projectRoot, 'composer.json'))) {\n try {\n const composer = await readJson<Record<string, unknown>>(join(projectRoot, 'composer.json'))\n const require = composer?.require as Record<string, string> | undefined\n if (require?.['laravel/framework']) return 'laravel'\n } catch { /* ignore */ }\n return 'other'\n }\n\n // Flutter (Dart)\n if (await pathExists(join(projectRoot, 'pubspec.yaml'))) return 'flutter'\n\n // Hugo\n if (await pathExists(join(projectRoot, 'hugo.toml'))) return 'hugo'\n if (await pathExists(join(projectRoot, 'config.toml'))) {\n try {\n const { readText } = await import('./fs.js')\n const content = await readText(join(projectRoot, 'config.toml'))\n if (content?.includes('baseURL') || content?.includes('hugo')) return 'hugo'\n } catch { /* ignore */ }\n }\n\n // Jekyll\n if (await pathExists(join(projectRoot, '_config.yml'))) return 'jekyll'\n\n return 'other'\n}\n\nasync function detectMonorepo(projectRoot: string): Promise<{ isMonorepo: boolean; tool?: string }> {\n if (await pathExists(join(projectRoot, 'pnpm-workspace.yaml'))) return { isMonorepo: true, tool: 'pnpm workspaces' }\n if (await pathExists(join(projectRoot, 'lerna.json'))) return { isMonorepo: true, tool: 'Lerna' }\n if (await pathExists(join(projectRoot, 'nx.json'))) return { isMonorepo: true, tool: 'Nx' }\n if (await pathExists(join(projectRoot, 'turbo.json'))) return { isMonorepo: true, tool: 'Turborepo' }\n\n const pkg = await readJson<PackageJson>(join(projectRoot, 'package.json'))\n if (pkg?.workspaces) return { isMonorepo: true, tool: 'npm/yarn workspaces' }\n\n // Walk up\n let dir = dirname(projectRoot)\n for (let i = 0; i < 5; i++) {\n if (await pathExists(join(dir, 'pnpm-workspace.yaml'))) return { isMonorepo: true, tool: 'pnpm workspaces' }\n if (await pathExists(join(dir, 'lerna.json'))) return { isMonorepo: true, tool: 'Lerna' }\n if (await pathExists(join(dir, 'nx.json'))) return { isMonorepo: true, tool: 'Nx' }\n if (await pathExists(join(dir, 'turbo.json'))) return { isMonorepo: true, tool: 'Turborepo' }\n const parentPkg = await readJson<PackageJson>(join(dir, 'package.json'))\n if (parentPkg?.workspaces) return { isMonorepo: true, tool: 'npm/yarn workspaces' }\n const parent = dirname(dir)\n if (parent === dir) break\n dir = parent\n }\n\n return { isMonorepo: false }\n}\n\nasync function detectFeatures(projectRoot: string): Promise<string[]> {\n const features: string[] = []\n const allDeps = await collectAllDeps(projectRoot)\n\n if ('typescript' in allDeps || await pathExists(join(projectRoot, 'tsconfig.json'))) features.push('TypeScript')\n\n const i18nLibs = ['vue-i18n', '@nuxtjs/i18n', 'next-intl', 'react-intl', 'i18next', 'react-i18next']\n for (const lib of i18nLibs) {\n if (lib in allDeps) { features.push(`i18n (${lib})`); break }\n }\n\n if ('tailwindcss' in allDeps) features.push('Tailwind CSS')\n else if ('sass' in allDeps || 'scss' in allDeps) features.push('Sass/SCSS')\n\n if ('vitest' in allDeps) features.push('Vitest')\n else if ('jest' in allDeps) features.push('Jest')\n\n if ('pinia' in allDeps) features.push('Pinia')\n else if ('zustand' in allDeps) features.push('Zustand')\n else if ('@reduxjs/toolkit' in allDeps || 'redux' in allDeps) features.push('Redux')\n\n return features\n}\n\nasync function collectAllDeps(projectRoot: string): Promise<Record<string, string>> {\n const pkg = await readJson<PackageJson>(join(projectRoot, 'package.json'))\n const allDeps: Record<string, string> = { ...pkg?.dependencies, ...pkg?.devDependencies }\n\n let dir = dirname(projectRoot)\n for (let i = 0; i < 5; i++) {\n const parentPkg = await readJson<PackageJson>(join(dir, 'package.json'))\n if (parentPkg?.workspaces) {\n Object.assign(allDeps, parentPkg.dependencies, parentPkg.devDependencies)\n break\n }\n const parent = dirname(dir)\n if (parent === dir) break\n dir = parent\n }\n\n return allDeps\n}\n"],"mappings":"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"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { a as readJson, i as readDir, o as readText, r as pathExists, t as contentrainDir } from "./fs-DLbVB-Ek.mjs";
|
|
2
|
-
import { t as readConfig } from "./config-
|
|
2
|
+
import { t as readConfig } from "./config-DlFcTxFG.mjs";
|
|
3
3
|
import { g as resolveLocaleStrategy, h as resolveJsonFilePath, m as resolveContentDir, o as listModels, s as readModel } from "./model-manager-BhLsUgaB.mjs";
|
|
4
|
-
import { n as checkBranchHealth } from "./branch-lifecycle-
|
|
4
|
+
import { n as checkBranchHealth, s as listRemoteCrBranches } from "./branch-lifecycle-D7tXOE4f.mjs";
|
|
5
5
|
import { i as autoDetectSourceDirs, o as discoverFiles } from "./scan-config-jC6XmiqF.mjs";
|
|
6
6
|
import { join } from "node:path";
|
|
7
7
|
import { stat } from "node:fs/promises";
|
|
@@ -92,20 +92,39 @@ async function runDoctor(projectRoot, options = {}) {
|
|
|
92
92
|
severity: orphans.length === 0 ? void 0 : "warning"
|
|
93
93
|
});
|
|
94
94
|
}
|
|
95
|
-
if (hasGit)
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
95
|
+
if (hasGit) {
|
|
96
|
+
try {
|
|
97
|
+
const health = await checkBranchHealth(projectRoot);
|
|
98
|
+
checks.push({
|
|
99
|
+
name: "Pending branches",
|
|
100
|
+
pass: !health.blocked && !health.warning,
|
|
101
|
+
detail: health.message ?? (health.unmerged === 0 ? "None" : `${health.unmerged} active cr/* branch(es)`),
|
|
102
|
+
severity: health.blocked ? "error" : health.warning ? "warning" : void 0
|
|
103
|
+
});
|
|
104
|
+
} catch {
|
|
105
|
+
checks.push({
|
|
106
|
+
name: "Pending branches",
|
|
107
|
+
pass: true,
|
|
108
|
+
detail: "Could not check"
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
const remoteList = await listRemoteCrBranches(projectRoot, { timeoutMs: 5e3 });
|
|
112
|
+
if (remoteList) if (remoteList.error) checks.push({
|
|
113
|
+
name: "Remote branches",
|
|
106
114
|
pass: true,
|
|
107
|
-
detail:
|
|
115
|
+
detail: `Could not check ${remoteList.remote} (offline?)`,
|
|
116
|
+
severity: "info"
|
|
108
117
|
});
|
|
118
|
+
else {
|
|
119
|
+
const count = remoteList.branches.length;
|
|
120
|
+
const warn = count >= (config?.branchWarnLimit ?? 50);
|
|
121
|
+
checks.push({
|
|
122
|
+
name: "Remote branches",
|
|
123
|
+
pass: !warn,
|
|
124
|
+
detail: count === 0 ? `None on ${remoteList.remote}` : `${count} cr/* branch(es) on ${remoteList.remote}${warn ? " — run `contentrain prune` to remove merged leftovers" : ""}`,
|
|
125
|
+
severity: warn ? "warning" : void 0
|
|
126
|
+
});
|
|
127
|
+
}
|
|
109
128
|
}
|
|
110
129
|
const clientDir = join(crDir, "client");
|
|
111
130
|
const modelsDir = join(crDir, "models");
|
|
@@ -335,4 +354,4 @@ async function analyzeMissingLocaleKeys(projectRoot, config) {
|
|
|
335
354
|
//#endregion
|
|
336
355
|
export { runDoctor as t };
|
|
337
356
|
|
|
338
|
-
//# sourceMappingURL=doctor-
|
|
357
|
+
//# sourceMappingURL=doctor-qiUKCy9b.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"doctor-qiUKCy9b.mjs","names":[],"sources":["../src/core/doctor.ts"],"sourcesContent":["import { join } from 'node:path'\nimport { stat } from 'node:fs/promises'\nimport { simpleGit } from 'simple-git'\nimport type { ContentrainConfig, ModelDefinition } from '@contentrain/types'\nimport { readConfig } from './config.js'\nimport { listModels, readModel } from './model-manager.js'\nimport { resolveContentDir, resolveJsonFilePath, resolveLocaleStrategy } from './content-manager.js'\nimport { autoDetectSourceDirs, discoverFiles } from './scan-config.js'\nimport { checkBranchHealth, listRemoteCrBranches } from '../git/branch-lifecycle.js'\nimport { contentrainDir, pathExists, readDir, readJson, readText } from '../util/fs.js'\n\n/**\n * Doctor — project health report.\n *\n * The public entry point is `runDoctor(projectRoot, { usage? })`. It is\n * inherently local-filesystem work (Node version, git install, file\n * mtimes, orphan directory detection), so the MCP tool surface gates\n * it behind the `localWorktree` capability — same pattern as\n * `contentrain_setup` and normalize.\n *\n * The report is structured JSON so three consumers can share it:\n *\n * - The `contentrain doctor` CLI command pretty-prints the checks.\n * - The Serve UI `/api/doctor` route returns the report to the\n * Dashboard's Doctor panel.\n * - Automation (CI, Studio) gets a deterministic JSON shape it can\n * assert against.\n *\n * Usage analysis (`--usage`) is a heavier, opt-in branch — it scans\n * every source file in the repo for content-key references. Kept\n * behind the flag so the default doctor run stays fast.\n */\n\nexport type CheckSeverity = 'error' | 'warning' | 'info'\n\nexport interface DoctorCheck {\n name: string\n pass: boolean\n detail: string\n /**\n * `error` — default for failing checks. Blocks a clean bill of health.\n * `warning` — failing-but-not-blocking (e.g. pending branches above\n * threshold, stale SDK client).\n * `info` — passed check; pure informational.\n */\n severity?: CheckSeverity\n}\n\nexport interface UnusedKeyEntry {\n model: string\n kind: string\n key: string\n locale: string\n}\n\nexport interface DuplicateValueEntry {\n model: string\n locale: string\n value: string\n keys: string[]\n}\n\nexport interface MissingLocaleEntry {\n model: string\n key: string\n missingIn: string\n}\n\nexport interface DoctorUsageAnalysis {\n unusedKeys: UnusedKeyEntry[]\n duplicateValues: DuplicateValueEntry[]\n missingLocaleKeys: MissingLocaleEntry[]\n}\n\nexport interface DoctorReport {\n checks: DoctorCheck[]\n summary: {\n total: number\n passed: number\n failed: number\n warnings: number\n }\n /** Present only when `options.usage === true`. */\n usage?: DoctorUsageAnalysis\n}\n\nexport interface RunDoctorOptions {\n /** Run heavier `--usage` analysis (unused keys, duplicates, locale gaps). */\n usage?: boolean\n}\n\nexport async function runDoctor(\n projectRoot: string,\n options: RunDoctorOptions = {},\n): Promise<DoctorReport> {\n const checks: DoctorCheck[] = []\n\n // ─── 1. Git installed ───\n try {\n const git = simpleGit(projectRoot)\n const version = await git.version()\n checks.push({\n name: 'Git',\n pass: true,\n detail: `v${version.major}.${version.minor}.${version.patch}`,\n })\n } catch {\n checks.push({ name: 'Git', pass: false, detail: 'Not installed or not in PATH', severity: 'error' })\n }\n\n // ─── 2. Git repo initialized ───\n const hasGit = await pathExists(join(projectRoot, '.git'))\n checks.push({\n name: 'Git repository',\n pass: hasGit,\n detail: hasGit ? projectRoot : 'No .git directory found',\n severity: hasGit ? undefined : 'error',\n })\n\n // ─── 3. Node version ───\n const nodeVersion = process.versions.node\n const [major] = nodeVersion.split('.').map(Number)\n const nodePass = (major ?? 0) >= 22\n checks.push({\n name: 'Node.js',\n pass: nodePass,\n detail: `v${nodeVersion}${nodePass ? '' : ' (requires ≥22)'}`,\n severity: nodePass ? undefined : 'error',\n })\n\n // ─── 4. .contentrain/ structure ───\n const crDir = contentrainDir(projectRoot)\n const hasCrDir = await pathExists(crDir)\n const hasConfig = await pathExists(join(crDir, 'config.json'))\n const hasModels = await pathExists(join(crDir, 'models'))\n const hasContent = await pathExists(join(crDir, 'content'))\n const structurePass = hasCrDir && hasConfig && hasModels && hasContent\n\n checks.push({\n name: '.contentrain/ structure',\n pass: structurePass,\n detail: !hasCrDir\n ? 'Not initialized — run `contentrain init`'\n : [\n hasConfig ? null : 'missing config.json',\n hasModels ? null : 'missing models/',\n hasContent ? null : 'missing content/',\n ].filter(Boolean).join(', ') || 'OK',\n severity: structurePass ? undefined : 'error',\n })\n\n // ─── 5. Config parseable ───\n let config: ContentrainConfig | null = null\n if (hasConfig) {\n config = await readConfig(projectRoot)\n checks.push({\n name: 'Config',\n pass: config !== null,\n detail: config\n ? `stack: ${config.stack}, locales: ${config.locales.supported.join(', ')}`\n : 'Failed to parse config.json',\n severity: config ? undefined : 'error',\n })\n }\n\n // ─── 6. Models all parseable ───\n if (hasCrDir) {\n try {\n const models = await listModels(projectRoot)\n const parseResults = await Promise.all(models.map(m => readModel(projectRoot, m.id)))\n const allParseable = parseResults.every(r => r !== null)\n checks.push({\n name: 'Models',\n pass: allParseable,\n detail: `${models.length} model(s)${allParseable ? ', all valid' : ', some failed to parse'}`,\n severity: allParseable ? undefined : 'error',\n })\n } catch {\n checks.push({ name: 'Models', pass: false, detail: 'Failed to read models', severity: 'error' })\n }\n }\n\n // ─── 7. Orphan content ───\n if (hasCrDir) {\n const orphans = await findOrphanContent(projectRoot)\n checks.push({\n name: 'Orphan content',\n pass: orphans.length === 0,\n detail: orphans.length === 0 ? 'None' : `Found: ${orphans.join(', ')}`,\n severity: orphans.length === 0 ? undefined : 'warning',\n })\n }\n\n // ─── 8. Stale contentrain branches ───\n if (hasGit) {\n try {\n const health = await checkBranchHealth(projectRoot)\n checks.push({\n name: 'Pending branches',\n pass: !health.blocked && !health.warning,\n detail: health.message\n ?? (health.unmerged === 0 ? 'None' : `${health.unmerged} active cr/* branch(es)`),\n severity: health.blocked ? 'error' : health.warning ? 'warning' : undefined,\n })\n } catch {\n checks.push({ name: 'Pending branches', pass: true, detail: 'Could not check' })\n }\n\n // ─── 8b. Remote cr/* branches ───\n // Authoritative ls-remote count (local branch pressure cannot see the\n // remote pile). Best-effort: skipped entirely without a remote, and an\n // unreachable remote is informational — doctor never fails offline.\n const remoteList = await listRemoteCrBranches(projectRoot, { timeoutMs: 5000 })\n if (remoteList) {\n if (remoteList.error) {\n checks.push({\n name: 'Remote branches',\n pass: true,\n detail: `Could not check ${remoteList.remote} (offline?)`,\n severity: 'info',\n })\n } else {\n const count = remoteList.branches.length\n const warnLimit = config?.branchWarnLimit ?? 50\n const warn = count >= warnLimit\n checks.push({\n name: 'Remote branches',\n pass: !warn,\n detail: count === 0\n ? `None on ${remoteList.remote}`\n : `${count} cr/* branch(es) on ${remoteList.remote}${warn ? ' — run `contentrain prune` to remove merged leftovers' : ''}`,\n severity: warn ? 'warning' : undefined,\n })\n }\n }\n }\n\n // ─── 9. SDK client freshness ───\n const clientDir = join(crDir, 'client')\n const modelsDir = join(crDir, 'models')\n if (await pathExists(clientDir) && await pathExists(modelsDir)) {\n try {\n const [clientStat, modelsStat] = await Promise.all([stat(clientDir), stat(modelsDir)])\n const fresh = clientStat.mtimeMs >= modelsStat.mtimeMs\n checks.push({\n name: 'SDK client',\n pass: fresh,\n detail: fresh ? 'Up to date' : 'Stale — run `contentrain generate`',\n severity: fresh ? undefined : 'warning',\n })\n } catch {\n checks.push({ name: 'SDK client', pass: true, detail: 'Could not check' })\n }\n }\n\n // ─── 10–12. Usage analysis (optional) ───\n let usage: DoctorUsageAnalysis | undefined\n if (options.usage && hasCrDir && config) {\n const [unusedKeys, duplicateValues, missingLocaleKeys] = await Promise.all([\n analyzeUnusedKeys(projectRoot, config),\n analyzeDuplicateValues(projectRoot, config),\n analyzeMissingLocaleKeys(projectRoot, config),\n ])\n usage = { unusedKeys, duplicateValues, missingLocaleKeys }\n\n checks.push({\n name: 'Unused content keys',\n pass: unusedKeys.length === 0,\n detail: unusedKeys.length === 0\n ? 'All keys referenced in source'\n : `${unusedKeys.length} key(s) not referenced in source code`,\n severity: unusedKeys.length === 0 ? undefined : 'warning',\n })\n\n checks.push({\n name: 'Duplicate dictionary values',\n pass: duplicateValues.length === 0,\n detail: duplicateValues.length === 0\n ? 'No duplicate values'\n : `${duplicateValues.length} value(s) mapped to multiple keys`,\n severity: duplicateValues.length === 0 ? undefined : 'warning',\n })\n\n checks.push({\n name: 'Locale key coverage',\n pass: missingLocaleKeys.length === 0,\n detail: missingLocaleKeys.length === 0\n ? 'All locales have matching keys'\n : `${missingLocaleKeys.length} key(s) missing in some locales`,\n severity: missingLocaleKeys.length === 0 ? undefined : 'warning',\n })\n }\n\n const passed = checks.filter(c => c.pass).length\n const failed = checks.length - passed\n const warnings = checks.filter(c => !c.pass && c.severity === 'warning').length\n\n const report: DoctorReport = {\n checks,\n summary: { total: checks.length, passed, failed, warnings },\n }\n if (usage) report.usage = usage\n return report\n}\n\nasync function findOrphanContent(projectRoot: string): Promise<string[]> {\n const crDir = contentrainDir(projectRoot)\n const models = await listModels(projectRoot)\n const orphans: string[] = []\n\n const knownContentDirs = new Set<string>()\n for (const m of models) {\n const full = await readModel(projectRoot, m.id)\n const modelForPath = full\n ? {\n ...full,\n content_path: full.content_path ?? (m as { content_path?: string }).content_path,\n }\n : {\n id: m.id,\n name: m.id,\n kind: m.kind,\n domain: m.domain,\n i18n: m.i18n,\n fields: {},\n content_path: (m as { content_path?: string }).content_path,\n }\n knownContentDirs.add(resolveContentDir(projectRoot, modelForPath))\n }\n\n const contentDir = join(crDir, 'content')\n if (await pathExists(contentDir)) {\n const domains = await readDir(contentDir)\n for (const domain of domains) {\n const domainDir = join(contentDir, domain)\n const entries = await readDir(domainDir)\n for (const entry of entries) {\n if (entry === '.gitkeep') continue\n const entryDir = join(domainDir, entry)\n if (!knownContentDirs.has(entryDir)) {\n orphans.push(`${domain}/${entry}`)\n }\n }\n }\n }\n\n for (const dir of knownContentDirs) {\n if (dir.startsWith(contentDir)) continue\n if (!await pathExists(dir)) {\n orphans.push(`(missing custom path) ${dir}`)\n continue\n }\n await readDir(dir)\n }\n\n return orphans\n}\n\nasync function analyzeUnusedKeys(\n projectRoot: string,\n config: ContentrainConfig,\n): Promise<UnusedKeyEntry[]> {\n const sourceDirs = await autoDetectSourceDirs(projectRoot)\n const files = await discoverFiles(projectRoot, { paths: sourceDirs })\n if (files.length === 0) return []\n\n const chunks = await Promise.all(\n files.map(async (relPath) => {\n const content = await readText(join(projectRoot, relPath))\n return content ?? ''\n }),\n )\n const allSource = chunks.join('\\n')\n\n const models = await listModels(projectRoot)\n const defaultLocale = config.locales.default\n const unused: UnusedKeyEntry[] = []\n\n for (const m of models) {\n const fullModel = await readModel(projectRoot, m.id)\n if (!fullModel) continue\n\n const keys = await extractContentKeys(projectRoot, fullModel, defaultLocale)\n for (const key of keys) {\n if (!allSource.includes(key)) {\n unused.push({ model: m.id, kind: m.kind, key, locale: defaultLocale })\n }\n }\n }\n\n return unused\n}\n\nasync function extractContentKeys(\n projectRoot: string,\n model: ModelDefinition,\n locale: string,\n): Promise<string[]> {\n const cDir = resolveContentDir(projectRoot, model)\n if (!await pathExists(cDir)) return []\n\n switch (model.kind) {\n case 'dictionary': {\n const filePath = resolveJsonFilePath(cDir, model, locale)\n const data = await readJson<Record<string, string>>(filePath)\n return data ? Object.keys(data) : []\n }\n case 'collection': {\n const filePath = resolveJsonFilePath(cDir, model, locale)\n const data = await readJson<Record<string, Record<string, unknown>>>(filePath)\n return data ? Object.keys(data) : []\n }\n case 'document': {\n const strategy = resolveLocaleStrategy(model)\n const slugs: string[] = []\n if (!model.i18n) {\n const files = await readDir(cDir)\n for (const f of files) if (f.endsWith('.md')) slugs.push(f.replace('.md', ''))\n } else if (strategy === 'file') {\n const dirs = await readDir(cDir)\n for (const d of dirs) if (!d.startsWith('.')) slugs.push(d)\n } else if (strategy === 'suffix') {\n const files = await readDir(cDir)\n const suffix = `.${locale}.md`\n for (const f of files) if (f.endsWith(suffix)) slugs.push(f.slice(0, -suffix.length))\n } else if (strategy === 'directory') {\n const localeDir = join(cDir, locale)\n if (await pathExists(localeDir)) {\n const files = await readDir(localeDir)\n for (const f of files) if (f.endsWith('.md')) slugs.push(f.replace('.md', ''))\n }\n } else {\n const files = await readDir(cDir)\n for (const f of files) if (f.endsWith('.md')) slugs.push(f.replace('.md', ''))\n }\n return slugs\n }\n case 'singleton':\n return []\n default:\n return []\n }\n}\n\nasync function analyzeDuplicateValues(\n projectRoot: string,\n config: ContentrainConfig,\n): Promise<DuplicateValueEntry[]> {\n const models = await listModels(projectRoot)\n const result: DuplicateValueEntry[] = []\n\n for (const m of models) {\n if (m.kind !== 'dictionary') continue\n const fullModel = await readModel(projectRoot, m.id)\n if (!fullModel) continue\n\n const cDir = resolveContentDir(projectRoot, fullModel)\n for (const locale of config.locales.supported) {\n const filePath = resolveJsonFilePath(cDir, fullModel, locale)\n const data = await readJson<Record<string, string>>(filePath)\n if (!data) continue\n\n const valueToKeys = new Map<string, string[]>()\n for (const [key, value] of Object.entries(data)) {\n const arr = valueToKeys.get(value)\n if (arr) arr.push(key)\n else valueToKeys.set(value, [key])\n }\n\n for (const [value, keys] of valueToKeys) {\n if (keys.length > 1) {\n result.push({ model: m.id, locale, value, keys })\n }\n }\n }\n }\n\n return result\n}\n\nasync function analyzeMissingLocaleKeys(\n projectRoot: string,\n config: ContentrainConfig,\n): Promise<MissingLocaleEntry[]> {\n if (config.locales.supported.length < 2) return []\n\n const models = await listModels(projectRoot)\n const result: MissingLocaleEntry[] = []\n const defaultLocale = config.locales.default\n const otherLocales = config.locales.supported.filter(l => l !== defaultLocale)\n\n for (const m of models) {\n if (m.kind !== 'dictionary' && m.kind !== 'collection') continue\n if (!m.i18n) continue\n\n const fullModel = await readModel(projectRoot, m.id)\n if (!fullModel) continue\n\n const cDir = resolveContentDir(projectRoot, fullModel)\n const defaultPath = resolveJsonFilePath(cDir, fullModel, defaultLocale)\n const defaultData = await readJson<Record<string, unknown>>(defaultPath)\n if (!defaultData) continue\n const defaultKeys = new Set(Object.keys(defaultData))\n\n for (const locale of otherLocales) {\n const localePath = resolveJsonFilePath(cDir, fullModel, locale)\n const localeData = await readJson<Record<string, unknown>>(localePath)\n const localeKeys = localeData ? new Set(Object.keys(localeData)) : new Set<string>()\n\n for (const key of defaultKeys) {\n if (!localeKeys.has(key)) {\n result.push({ model: m.id, key, missingIn: locale })\n }\n }\n }\n }\n\n return result\n}\n"],"mappings":"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"}
|
|
@@ -1,57 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
deleted: number;
|
|
4
|
-
remaining: number;
|
|
5
|
-
deletedBranches: string[];
|
|
6
|
-
}
|
|
7
|
-
interface BranchHealthCheck {
|
|
8
|
-
total: number;
|
|
9
|
-
merged: number;
|
|
10
|
-
unmerged: number;
|
|
11
|
-
warning: boolean;
|
|
12
|
-
blocked: boolean;
|
|
13
|
-
message?: string;
|
|
14
|
-
}
|
|
15
|
-
/**
|
|
16
|
-
* Lists all local contentrain/* branches, deletes those already merged
|
|
17
|
-
* into the base branch, and returns the count of remaining unmerged ones.
|
|
18
|
-
*/
|
|
19
|
-
declare function cleanupMergedBranches(projectRoot: string): Promise<CleanupResult>;
|
|
20
|
-
/**
|
|
21
|
-
* Check branch health: count contentrain/* branches and return warning/blocked status.
|
|
22
|
-
* - 50+ branches: warning
|
|
23
|
-
* - 80+ branches: blocked
|
|
24
|
-
*/
|
|
25
|
-
declare function checkBranchHealth(projectRoot: string): Promise<BranchHealthCheck>;
|
|
26
|
-
interface BranchDiffResult {
|
|
27
|
-
/** The feature branch the diff was computed from. */
|
|
28
|
-
branch: string;
|
|
29
|
-
/** The base ref the diff was computed against. Defaults to the `contentrain` branch. */
|
|
30
|
-
base: string;
|
|
31
|
-
/** `git diff --stat` output — human-readable summary. */
|
|
32
|
-
stat: string;
|
|
33
|
-
/** Raw unified diff. */
|
|
34
|
-
patch: string;
|
|
35
|
-
/** Number of files touched in the diff. */
|
|
36
|
-
filesChanged: number;
|
|
37
|
-
}
|
|
38
|
-
/**
|
|
39
|
-
* Compute the diff between a feature branch and its base.
|
|
40
|
-
*
|
|
41
|
-
* Defaults `base` to `CONTENTRAIN_BRANCH` — the singleton content-
|
|
42
|
-
* tracking branch every feature branch forks from. Passing the repo's
|
|
43
|
-
* default branch (e.g. `main`) is almost always a bug: when
|
|
44
|
-
* `contentrain` is ahead of `main`, the diff picks up unrelated
|
|
45
|
-
* historical content changes that the feature branch did not produce.
|
|
46
|
-
*
|
|
47
|
-
* Used by `contentrain serve` (branch detail view), the `contentrain
|
|
48
|
-
* diff` CLI command, and any Studio-side driver that needs to preview
|
|
49
|
-
* a feature branch before approving it.
|
|
50
|
-
*/
|
|
51
|
-
declare function branchDiff(projectRoot: string, opts: {
|
|
52
|
-
branch: string;
|
|
53
|
-
base?: string;
|
|
54
|
-
}): Promise<BranchDiffResult>;
|
|
55
|
-
//#endregion
|
|
56
|
-
export { BranchDiffResult, BranchHealthCheck, CleanupResult, branchDiff, checkBranchHealth, cleanupMergedBranches };
|
|
57
|
-
//# sourceMappingURL=branch-lifecycle.d.mts.map
|
|
1
|
+
import { a as RemoteDeleteResult, c as checkBranchHealth, d as deleteRemoteBranch, f as isRefMerged, i as RemoteBranchList, l as classifyMergedBranches, m as pruneMergedRemoteBranches, n as BranchHealthCheck, o as RemotePruneResult, p as listRemoteCrBranches, r as CleanupResult, s as branchDiff, t as BranchDiffResult, u as cleanupMergedBranches } from "../branch-lifecycle-7dQoF22r.mjs";
|
|
2
|
+
export { BranchDiffResult, BranchHealthCheck, CleanupResult, RemoteBranchList, RemoteDeleteResult, RemotePruneResult, branchDiff, checkBranchHealth, classifyMergedBranches, cleanupMergedBranches, deleteRemoteBranch, isRefMerged, listRemoteCrBranches, pruneMergedRemoteBranches };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import "../fs-DLbVB-Ek.mjs";
|
|
2
|
-
import "../config-
|
|
3
|
-
import { n as checkBranchHealth, r as
|
|
4
|
-
export { branchDiff, checkBranchHealth, cleanupMergedBranches };
|
|
2
|
+
import "../config-DlFcTxFG.mjs";
|
|
3
|
+
import { a as deleteRemoteBranch, c as pruneMergedRemoteBranches, i as cleanupMergedBranches, n as checkBranchHealth, o as isRefMerged, r as classifyMergedBranches, s as listRemoteCrBranches, t as branchDiff } from "../branch-lifecycle-D7tXOE4f.mjs";
|
|
4
|
+
export { branchDiff, checkBranchHealth, classifyMergedBranches, cleanupMergedBranches, deleteRemoteBranch, isRefMerged, listRemoteCrBranches, pruneMergedRemoteBranches };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { a as RemoteDeleteResult } from "../branch-lifecycle-7dQoF22r.mjs";
|
|
1
2
|
import { SyncResult, WorkflowMode } from "@contentrain/types";
|
|
2
3
|
|
|
3
4
|
//#region src/git/transaction.d.ts
|
|
@@ -28,6 +29,7 @@ declare function mergeBranch(projectRoot: string, branchName: string): Promise<{
|
|
|
28
29
|
action: 'merged';
|
|
29
30
|
commit: string;
|
|
30
31
|
sync: SyncResult;
|
|
32
|
+
remote?: RemoteDeleteResult;
|
|
31
33
|
}>;
|
|
32
34
|
declare function buildBranchName(scope: string, target: string, locale?: string): string;
|
|
33
35
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transaction.d.mts","names":[],"sources":["../../src/git/transaction.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"transaction.d.mts","names":[],"sources":["../../src/git/transaction.ts"],"mappings":";;;;UAaiB,aAAA;EACf,IAAA;EACA,KAAA;EACA,MAAA;EACA,OAAA;AAAA;AAAA,UAGe,cAAA;EACf,QAAA;EACA,MAAA;EACA,KAAA,CAAM,QAAA,GAAW,YAAA,aAAyB,OAAA,SAAgB,OAAA;EAC1D,MAAA,CAAO,OAAA,UAAiB,aAAA,GAAgB,aAAA,GAAgB,OAAA;EACxD,QAAA,IAAY,OAAA;IAAU,MAAA;IAA0C,MAAA;IAAgB,IAAA,GAAO,UAAA;IAAY,OAAA;EAAA;EACnG,OAAA,IAAW,OAAA;AAAA;AAAA,iBAGS,mBAAA,CAAoB,WAAA,WAAsB,OAAA;AAAA,iBA+J1C,iBAAA,CACpB,WAAA,UACA,UAAA,UACA,OAAA;EAAY,gBAAA,GAAmB,YAAA;AAAA,IAC9B,OAAA,CAAQ,cAAA;AAAA,iBA2OW,WAAA,CACpB,WAAA,UACA,UAAA,WACC,OAAA;EAAU,MAAA;EAAkB,MAAA;EAAgB,IAAA,EAAM,UAAA;EAAY,MAAA,GAAS,kBAAA;AAAA;AAAA,iBAqI1D,eAAA,CAAgB,KAAA,UAAe,MAAA,UAAgB,MAAA"}
|
package/dist/git/transaction.mjs
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import "../serialization-B1CEzR4H.mjs";
|
|
2
2
|
import "../fs-DLbVB-Ek.mjs";
|
|
3
|
-
import "../config-
|
|
3
|
+
import "../config-DlFcTxFG.mjs";
|
|
4
4
|
import "../model-manager-BhLsUgaB.mjs";
|
|
5
5
|
import "../meta-manager-C900rEyG.mjs";
|
|
6
|
-
import "../context-
|
|
7
|
-
import "../
|
|
8
|
-
import
|
|
6
|
+
import "../context-CYN__o3Q.mjs";
|
|
7
|
+
import "../branch-lifecycle-D7tXOE4f.mjs";
|
|
8
|
+
import "../id-DV_T9Ic8.mjs";
|
|
9
|
+
import { i as mergeBranch, n as createTransaction, r as ensureContentBranch, t as buildBranchName } from "../transaction-qliKvBRL.mjs";
|
|
9
10
|
export { buildBranchName, createTransaction, ensureContentBranch, mergeBranch };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"id-
|
|
1
|
+
{"version":3,"file":"id-DV_T9Ic8.mjs","names":[],"sources":["../src/util/id.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto'\n\nexport function branchTimestamp(): string {\n const ts = Math.floor(Date.now() / 1000).toString()\n const suffix = randomUUID().slice(0, 4)\n return `${ts}-${suffix}`\n}\n\n// generateEntryId is now exported from @contentrain/types\nexport { generateEntryId } from '@contentrain/types'\n"],"mappings":";;;AAEA,SAAgB,kBAA0B;AAGxC,QAAO,GAFI,KAAK,MAAM,KAAK,KAAK,GAAG,IAAK,CAAC,UAAU,CAEtC,GADE,YAAY,CAAC,MAAM,GAAG,EAAE"}
|
package/dist/index.mjs
CHANGED
|
@@ -3,24 +3,24 @@ import "./contracts-DfL0BfrD.mjs";
|
|
|
3
3
|
import "./ops-Dh2T3u53.mjs";
|
|
4
4
|
import "./serialization-B1CEzR4H.mjs";
|
|
5
5
|
import "./fs-DLbVB-Ek.mjs";
|
|
6
|
-
import "./config-
|
|
6
|
+
import "./config-DlFcTxFG.mjs";
|
|
7
7
|
import "./model-manager-BhLsUgaB.mjs";
|
|
8
8
|
import "./meta-manager-C900rEyG.mjs";
|
|
9
|
-
import "./context-
|
|
10
|
-
import "./
|
|
11
|
-
import "./
|
|
12
|
-
import "./
|
|
13
|
-
import "./
|
|
14
|
-
import "./
|
|
9
|
+
import "./context-CYN__o3Q.mjs";
|
|
10
|
+
import "./branch-lifecycle-D7tXOE4f.mjs";
|
|
11
|
+
import "./id-DV_T9Ic8.mjs";
|
|
12
|
+
import "./transaction-qliKvBRL.mjs";
|
|
13
|
+
import "./local-BZEUWBoC.mjs";
|
|
14
|
+
import "./detect-eWS6lvoH.mjs";
|
|
15
15
|
import "./annotations-BFJbfSKe.mjs";
|
|
16
|
-
import { t as createServer } from "./server-
|
|
17
|
-
import "./validator-
|
|
16
|
+
import { t as createServer } from "./server-CXZFrEyu.mjs";
|
|
17
|
+
import "./validator-Z3DGNWQ6.mjs";
|
|
18
18
|
import "./scan-config-jC6XmiqF.mjs";
|
|
19
19
|
import "./graph-builder-DXv2JEp-.mjs";
|
|
20
20
|
import "./scanner-DKMwc03k.mjs";
|
|
21
21
|
import "./tsx-parser-CQc6nr-o.mjs";
|
|
22
|
-
import "./apply-manager-
|
|
23
|
-
import "./doctor-
|
|
22
|
+
import "./apply-manager-BRmB27H5.mjs";
|
|
23
|
+
import "./doctor-qiUKCy9b.mjs";
|
|
24
24
|
import { resolve } from "node:path";
|
|
25
25
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
26
26
|
//#region src/index.ts
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { t as LOCAL_CAPABILITIES } from "./contracts-DfL0BfrD.mjs";
|
|
2
2
|
import { a as applyChangesToWorktree } from "./ops-Dh2T3u53.mjs";
|
|
3
|
-
import { t as readConfig } from "./config-
|
|
3
|
+
import { t as readConfig } from "./config-DlFcTxFG.mjs";
|
|
4
4
|
import { C as LocalReader } from "./model-manager-BhLsUgaB.mjs";
|
|
5
|
-
import {
|
|
5
|
+
import { a as deleteRemoteBranch, r as classifyMergedBranches } from "./branch-lifecycle-D7tXOE4f.mjs";
|
|
6
|
+
import { i as mergeBranch$1, n as createTransaction } from "./transaction-qliKvBRL.mjs";
|
|
6
7
|
import { CONTENTRAIN_BRANCH } from "@contentrain/types";
|
|
7
8
|
import { simpleGit } from "simple-git";
|
|
8
9
|
//#region src/providers/local/branch-ops.ts
|
|
@@ -42,6 +43,7 @@ async function createBranch(projectRoot, name, fromRef) {
|
|
|
42
43
|
}
|
|
43
44
|
async function deleteBranch(projectRoot, name) {
|
|
44
45
|
await simpleGit(projectRoot).deleteLocalBranch(name, true);
|
|
46
|
+
await deleteRemoteBranch(projectRoot, name);
|
|
45
47
|
}
|
|
46
48
|
async function getBranchDiff(projectRoot, branch, base) {
|
|
47
49
|
const raw = await simpleGit(projectRoot).raw([
|
|
@@ -76,18 +78,13 @@ async function mergeBranch(projectRoot, branch, into) {
|
|
|
76
78
|
merged: true,
|
|
77
79
|
sha: result.commit,
|
|
78
80
|
pullRequestUrl: null,
|
|
79
|
-
sync: result.sync
|
|
81
|
+
sync: result.sync,
|
|
82
|
+
...result.remote ? { remote: result.remote } : {}
|
|
80
83
|
};
|
|
81
84
|
}
|
|
82
85
|
async function isMerged(projectRoot, branch, into) {
|
|
83
|
-
const git = simpleGit(projectRoot);
|
|
84
86
|
try {
|
|
85
|
-
|
|
86
|
-
"branch",
|
|
87
|
-
"--merged",
|
|
88
|
-
into
|
|
89
|
-
]);
|
|
90
|
-
return new Set(raw.split("\n").map((b) => b.replace(/^\*?\s+/, "").trim()).filter(Boolean)).has(branch);
|
|
87
|
+
return (await classifyMergedBranches(projectRoot, [branch], into)).has(branch);
|
|
91
88
|
} catch {
|
|
92
89
|
return false;
|
|
93
90
|
}
|
|
@@ -179,4 +176,4 @@ var LocalProvider = class {
|
|
|
179
176
|
//#endregion
|
|
180
177
|
export { isMerged as n, LocalProvider as t };
|
|
181
178
|
|
|
182
|
-
//# sourceMappingURL=local-
|
|
179
|
+
//# sourceMappingURL=local-BZEUWBoC.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"local-BZEUWBoC.mjs","names":["mergeBranchOp","listBranchesOp","createBranchOp","deleteBranchOp","getBranchDiffOp","mergeBranchOp","isMergedOp","getDefaultBranchOp"],"sources":["../src/providers/local/branch-ops.ts","../src/providers/local/provider.ts"],"sourcesContent":["import { simpleGit } from 'simple-git'\nimport { CONTENTRAIN_BRANCH } from '@contentrain/types'\nimport type { Branch, FileDiff, MergeResult } from '../../core/contracts/index.js'\nimport { readConfig } from '../../core/config.js'\nimport { classifyMergedBranches, deleteRemoteBranch } from '../../git/branch-lifecycle.js'\nimport { mergeBranch as mergeBranchOp } from '../../git/transaction.js'\n\n/**\n * Branch/merge/diff helpers backed by the local simple-git worktree flow.\n *\n * Pure functions composed into `LocalProvider` — mirroring the shape of\n * `providers/github/branch-ops.ts` so the two providers share the same\n * surface at the `RepoProvider` boundary.\n */\n\nexport async function getDefaultBranch(projectRoot: string): Promise<string> {\n const config = await readConfig(projectRoot)\n if (config?.repository?.default_branch) return config.repository.default_branch\n const envBranch = process.env['CONTENTRAIN_BRANCH']\n if (envBranch) return envBranch\n const git = simpleGit(projectRoot)\n const current = (await git.raw(['branch', '--show-current'])).trim()\n return current || 'main'\n}\n\nexport async function listBranches(\n projectRoot: string,\n prefix?: string,\n): Promise<Branch[]> {\n const git = simpleGit(projectRoot)\n const summary = await git.branchLocal()\n const names = prefix\n ? summary.all.filter(n => n.startsWith(prefix))\n : summary.all\n const branches: Branch[] = []\n for (const name of names) {\n const info = summary.branches[name]\n branches.push({ name, sha: info?.commit ?? '' })\n }\n return branches\n}\n\nexport async function createBranch(\n projectRoot: string,\n name: string,\n fromRef: string,\n): Promise<void> {\n const git = simpleGit(projectRoot)\n await git.raw(['branch', name, fromRef])\n}\n\nexport async function deleteBranch(\n projectRoot: string,\n name: string,\n): Promise<void> {\n const git = simpleGit(projectRoot)\n await git.deleteLocalBranch(name, true)\n // Parity with the remote-API providers, whose deleteBranch removes the\n // remote ref: best-effort, config-gated inside the helper, never throws.\n await deleteRemoteBranch(projectRoot, name)\n}\n\nexport async function getBranchDiff(\n projectRoot: string,\n branch: string,\n base: string,\n): Promise<FileDiff[]> {\n const git = simpleGit(projectRoot)\n const raw = await git.raw(['diff', '--name-status', `${base}...${branch}`])\n const diffs: FileDiff[] = []\n for (const line of raw.split('\\n')) {\n if (!line.trim()) continue\n const [code, ...pathParts] = line.split('\\t')\n const path = pathParts[pathParts.length - 1]\n if (!code || !path) continue\n const status: FileDiff['status'] = code.startsWith('A')\n ? 'added'\n : code.startsWith('D')\n ? 'removed'\n : 'modified'\n diffs.push({ path, status, before: null, after: null })\n }\n return diffs\n}\n\nexport async function mergeBranch(\n projectRoot: string,\n branch: string,\n into: string,\n): Promise<MergeResult> {\n if (into !== CONTENTRAIN_BRANCH) {\n throw Object.assign(new Error(\n `LocalProvider.mergeBranch only supports merging into \"${CONTENTRAIN_BRANCH}\" (got \"${into}\"). `\n + `The local flow merges feature branches into the content-tracking branch and fast-forwards the base branch via update-ref.`,\n ), {\n code: 'UNSUPPORTED_MERGE_TARGET',\n agent_hint: `Pass \"${CONTENTRAIN_BRANCH}\" as the merge target, or use a non-local provider that supports arbitrary targets.`,\n developer_action: `Merge \"${branch}\" into \"${CONTENTRAIN_BRANCH}\" instead.`,\n })\n }\n const result = await mergeBranchOp(projectRoot, branch)\n return {\n merged: true,\n sha: result.commit,\n pullRequestUrl: null,\n sync: result.sync,\n ...(result.remote ? { remote: result.remote } : {}),\n }\n}\n\nexport async function isMerged(\n projectRoot: string,\n branch: string,\n into: string,\n): Promise<boolean> {\n try {\n const merged = await classifyMergedBranches(projectRoot, [branch], into)\n return merged.has(branch)\n } catch {\n return false\n }\n}\n","import { CONTENTRAIN_BRANCH } from '@contentrain/types'\nimport type {\n Branch,\n FileDiff,\n MergeResult,\n ProviderCapabilities,\n RepoProvider,\n} from '../../core/contracts/index.js'\nimport { LOCAL_CAPABILITIES } from '../../core/contracts/index.js'\nimport { applyChangesToWorktree } from '../../core/ops/index.js'\nimport { createTransaction } from '../../git/transaction.js'\nimport {\n createBranch as createBranchOp,\n deleteBranch as deleteBranchOp,\n getBranchDiff as getBranchDiffOp,\n getDefaultBranch as getDefaultBranchOp,\n isMerged as isMergedOp,\n listBranches as listBranchesOp,\n mergeBranch as mergeBranchOp,\n} from './branch-ops.js'\nimport { LocalReader } from './reader.js'\nimport type { LocalApplyPlanInput, LocalApplyResult } from './types.js'\n\nconst DEFAULT_AUTHOR_NAME = 'Contentrain'\nconst DEFAULT_AUTHOR_EMAIL = 'ai@contentrain.io'\n\n/**\n * LocalProvider — the local-filesystem, worktree-backed content provider.\n *\n * Implements the full `RepoProvider` surface:\n * - Reader methods delegate to `LocalReader`.\n * - `applyPlan` wraps `createTransaction` and returns `LocalApplyResult`\n * (a superset of `Commit` carrying workflow action + selective sync).\n * - Branch ops mirror `GitHubProvider` — thin wrappers over the local\n * simple-git helpers in `./branch-ops.ts`.\n *\n * `mergeBranch` only supports merging into the singleton\n * `CONTENTRAIN_BRANCH`; the local flow advances the base branch via\n * `update-ref` in `transaction.mergeBranch`, so arbitrary merge targets\n * would bypass that invariant.\n */\nexport class LocalProvider implements RepoProvider {\n readonly capabilities: ProviderCapabilities = LOCAL_CAPABILITIES\n private readonly reader: LocalReader\n\n constructor(public readonly projectRoot: string) {\n this.reader = new LocalReader(projectRoot)\n }\n\n readFile(path: string, ref?: string): Promise<string> {\n return this.reader.readFile(path, ref)\n }\n\n listDirectory(path: string, ref?: string): Promise<string[]> {\n return this.reader.listDirectory(path, ref)\n }\n\n fileExists(path: string, ref?: string): Promise<boolean> {\n return this.reader.fileExists(path, ref)\n }\n\n async applyPlan(input: LocalApplyPlanInput): Promise<LocalApplyResult> {\n const tx = await createTransaction(this.projectRoot, input.branch, {\n workflowOverride: input.workflowOverride,\n })\n try {\n await tx.write(async (wt) => {\n await applyChangesToWorktree(wt, input.changes)\n })\n await tx.commit(input.message, input.context)\n const gitResult = await tx.complete()\n return {\n sha: gitResult.commit,\n message: input.message,\n author: {\n name: process.env['CONTENTRAIN_AUTHOR_NAME'] ?? DEFAULT_AUTHOR_NAME,\n email: process.env['CONTENTRAIN_AUTHOR_EMAIL'] ?? DEFAULT_AUTHOR_EMAIL,\n },\n timestamp: new Date().toISOString(),\n workflowAction: gitResult.action,\n sync: gitResult.sync,\n warning: gitResult.warning,\n }\n } finally {\n await tx.cleanup()\n }\n }\n\n listBranches(prefix?: string): Promise<Branch[]> {\n return listBranchesOp(this.projectRoot, prefix)\n }\n\n async createBranch(name: string, fromRef?: string): Promise<void> {\n const resolved = fromRef ?? CONTENTRAIN_BRANCH\n await createBranchOp(this.projectRoot, name, resolved)\n }\n\n deleteBranch(name: string): Promise<void> {\n return deleteBranchOp(this.projectRoot, name)\n }\n\n getBranchDiff(branch: string, base?: string): Promise<FileDiff[]> {\n const resolved = base ?? CONTENTRAIN_BRANCH\n return getBranchDiffOp(this.projectRoot, branch, resolved)\n }\n\n mergeBranch(branch: string, into: string): Promise<MergeResult> {\n return mergeBranchOp(this.projectRoot, branch, into)\n }\n\n isMerged(branch: string, into?: string): Promise<boolean> {\n const resolved = into ?? CONTENTRAIN_BRANCH\n return isMergedOp(this.projectRoot, branch, resolved)\n }\n\n getDefaultBranch(): Promise<string> {\n return getDefaultBranchOp(this.projectRoot)\n }\n}\n"],"mappings":"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"}
|
|
@@ -136,7 +136,9 @@ declare class GitHubProvider implements RepoProvider {
|
|
|
136
136
|
createBranch(name: string, fromRef?: string): Promise<void>;
|
|
137
137
|
deleteBranch(name: string): Promise<void>;
|
|
138
138
|
getBranchDiff(branch: string, base?: string): Promise<FileDiff[]>;
|
|
139
|
-
mergeBranch(branch: string, into: string
|
|
139
|
+
mergeBranch(branch: string, into: string, opts?: {
|
|
140
|
+
removeSourceBranch?: boolean;
|
|
141
|
+
}): Promise<MergeResult>;
|
|
140
142
|
isMerged(branch: string, into?: string): Promise<boolean>;
|
|
141
143
|
getDefaultBranch(): Promise<string>;
|
|
142
144
|
}
|
|
@@ -187,12 +189,31 @@ declare function createGitHubProvider(opts: {
|
|
|
187
189
|
* commit SHA. When omitted, GitHub resolves to the repository's default
|
|
188
190
|
* branch — which is usually wrong for Contentrain flows, so callers
|
|
189
191
|
* should always pass the explicit `contentrain` tracking branch.
|
|
192
|
+
*
|
|
193
|
+
* Pass `{ memoize: true }` to dedupe reads within one operation: a repeated
|
|
194
|
+
* `(path, ref)` returns the in-flight/cached promise instead of a fresh
|
|
195
|
+
* `getContent`. This is OPT-IN and only safe for a SHORT-LIVED, read-only
|
|
196
|
+
* reader — a long-lived reader that outlives a write would serve stale
|
|
197
|
+
* results, so the provider's own reader never enables it. Failed reads are
|
|
198
|
+
* evicted so a transient error is retried, not cached forever.
|
|
190
199
|
*/
|
|
191
200
|
declare class GitHubReader implements RepoReader {
|
|
192
201
|
private readonly client;
|
|
193
202
|
private readonly repo;
|
|
194
|
-
|
|
203
|
+
private readonly opts?;
|
|
204
|
+
private readonly fileMemo;
|
|
205
|
+
private readonly listMemo;
|
|
206
|
+
private readonly existsMemo;
|
|
207
|
+
constructor(client: GitHubClient, repo: RepoRef, opts?: {
|
|
208
|
+
memoize?: boolean;
|
|
209
|
+
} | undefined);
|
|
210
|
+
/**
|
|
211
|
+
* Run `fetch` through the given memo when memoization is enabled, keyed by
|
|
212
|
+
* `(ref, repoPath)`. A rejected promise is evicted so the next call retries.
|
|
213
|
+
*/
|
|
214
|
+
private memoized;
|
|
195
215
|
readFile(path: string, ref?: string): Promise<string>;
|
|
216
|
+
private readFileUncached;
|
|
196
217
|
listDirectory(path: string, ref?: string): Promise<string[]>;
|
|
197
218
|
fileExists(path: string, ref?: string): Promise<boolean>;
|
|
198
219
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../../../src/providers/github/app-auth.ts","../../../src/providers/github/capabilities.ts","../../../src/providers/github/client.ts","../../../src/providers/github/types.ts","../../../src/providers/github/provider.ts","../../../src/providers/github/factory.ts","../../../src/providers/github/reader.ts"],"mappings":";;;;;;;;AAuBA;;;;;;;;;AASA;;;;;AAkBA;;UA3BiB,aAAA;EA2BsB;EAzBrC,KAAA;EAyBsC;EAvBtC,UAAA;EAuB4E;EArB5E,cAAA;AAAA;AAAA,UAGe,uBAAA;;EAEf,KAAA;EA4C6C;EA1C7C,SAAA;AAAA;AAAA,iBAcc,UAAA,CAAW,MAAA,EAAQ,IAAA,CAAK,aAAA;;;;;;;;iBA0BlB,yBAAA,CACpB,MAAA,EAAQ,aAAA,EACR,IAAA;EAAQ,OAAA;EAAkB,SAAA,UAAmB,UAAA,CAAW,KAAA;AAAA,IACvD,OAAA,CAAQ,uBAAA;;;;;;AAxDX;;;;;;;;;AASA;cCjBa,mBAAA,EAAqB,oBAAA;;;;;;ADQlC;;;;;;KEZY,YAAA,GAAe,OAAA;;;;;;AFY3B;;;;;UGbiB,OAAA;EACf,KAAA;EACA,IAAA;EACA,WAAA;AAAA;;;;;AHqCF;;;;;KGzBY,UAAA;EACN,IAAA;EAAa,KAAA;AAAA;EAEb,IAAA;EACA,KAAA;EACA,UAAA;EACA,cAAA;AAAA;;cAIO,qBAAA,EAAuB,YAAA;;;;AHZpC;;;;;;;;cIUa,cAAA,YAA0B,YAAA;EAAA,iBAKlB,MAAA;EAAA,SACD,IAAA,EAAM,OAAA;EAAA,SALf,YAAA,EAAc,oBAAA;EAAA,iBACN,MAAA;cAGE,MAAA,EAAQ,YAAA,EACT,IAAA,EAAM,OAAA;EAKxB,QAAA,CAAS,IAAA,UAAc,GAAA,YAAe,OAAA;EAGtC,aAAA,CAAc,IAAA,UAAc,GAAA,YAAe,OAAA;EAG3C,UAAA,CAAW,IAAA,UAAc,GAAA,YAAe,OAAA;EAIxC,SAAA,CAAU,KAAA,EAAO,cAAA,GAAiB,OAAA,CAAQ,MAAA;EAI1C,YAAA,CAAa,MAAA,YAAkB,OAAA,CAAQ,MAAA;EAGvC,YAAA,CAAa,IAAA,UAAc,OAAA,YAAmB,OAAA;EAI9C,YAAA,CAAa,IAAA,WAAe,OAAA;EAG5B,aAAA,CAAc,MAAA,UAAgB,IAAA,YAAgB,OAAA,CAAQ,QAAA;EAItD,WAAA,CAAY,MAAA,UAAgB,IAAA,
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../../src/providers/github/app-auth.ts","../../../src/providers/github/capabilities.ts","../../../src/providers/github/client.ts","../../../src/providers/github/types.ts","../../../src/providers/github/provider.ts","../../../src/providers/github/factory.ts","../../../src/providers/github/reader.ts"],"mappings":";;;;;;;;AAuBA;;;;;;;;;AASA;;;;;AAkBA;;UA3BiB,aAAA;EA2BsB;EAzBrC,KAAA;EAyBsC;EAvBtC,UAAA;EAuB4E;EArB5E,cAAA;AAAA;AAAA,UAGe,uBAAA;;EAEf,KAAA;EA4C6C;EA1C7C,SAAA;AAAA;AAAA,iBAcc,UAAA,CAAW,MAAA,EAAQ,IAAA,CAAK,aAAA;;;;;;;;iBA0BlB,yBAAA,CACpB,MAAA,EAAQ,aAAA,EACR,IAAA;EAAQ,OAAA;EAAkB,SAAA,UAAmB,UAAA,CAAW,KAAA;AAAA,IACvD,OAAA,CAAQ,uBAAA;;;;;;AAxDX;;;;;;;;;AASA;cCjBa,mBAAA,EAAqB,oBAAA;;;;;;ADQlC;;;;;;KEZY,YAAA,GAAe,OAAA;;;;;;AFY3B;;;;;UGbiB,OAAA;EACf,KAAA;EACA,IAAA;EACA,WAAA;AAAA;;;;;AHqCF;;;;;KGzBY,UAAA;EACN,IAAA;EAAa,KAAA;AAAA;EAEb,IAAA;EACA,KAAA;EACA,UAAA;EACA,cAAA;AAAA;;cAIO,qBAAA,EAAuB,YAAA;;;;AHZpC;;;;;;;;cIUa,cAAA,YAA0B,YAAA;EAAA,iBAKlB,MAAA;EAAA,SACD,IAAA,EAAM,OAAA;EAAA,SALf,YAAA,EAAc,oBAAA;EAAA,iBACN,MAAA;cAGE,MAAA,EAAQ,YAAA,EACT,IAAA,EAAM,OAAA;EAKxB,QAAA,CAAS,IAAA,UAAc,GAAA,YAAe,OAAA;EAGtC,aAAA,CAAc,IAAA,UAAc,GAAA,YAAe,OAAA;EAG3C,UAAA,CAAW,IAAA,UAAc,GAAA,YAAe,OAAA;EAIxC,SAAA,CAAU,KAAA,EAAO,cAAA,GAAiB,OAAA,CAAQ,MAAA;EAI1C,YAAA,CAAa,MAAA,YAAkB,OAAA,CAAQ,MAAA;EAGvC,YAAA,CAAa,IAAA,UAAc,OAAA,YAAmB,OAAA;EAI9C,YAAA,CAAa,IAAA,WAAe,OAAA;EAG5B,aAAA,CAAc,MAAA,UAAgB,IAAA,YAAgB,OAAA,CAAQ,QAAA;EAItD,WAAA,CAAY,MAAA,UAAgB,IAAA,UAAc,IAAA;IAAS,kBAAA;EAAA,IAAiC,OAAA,CAAQ,WAAA;EAG5F,QAAA,CAAS,MAAA,UAAgB,IAAA,YAAgB,OAAA;EAIzC,gBAAA,CAAA,GAAoB,OAAA;AAAA;;;;AJxDtB;;;;;;;;;AASA;;;;;AAkBA;;;;;iBKzBsB,kBAAA,CAAmB,IAAA,EAAM,UAAA,GAAa,OAAA,CAAQ,YAAA;;;;ALmDpE;;;iBKhBsB,oBAAA,CAAqB,IAAA;EAAQ,IAAA,EAAM,UAAA;EAAY,IAAA,EAAM,OAAA;AAAA,IAAY,OAAA,CAAQ,cAAA;;;;ALrC/F;;;;;;;;;AASA;;;;;AAkBA;;;;;cMzBa,YAAA,YAAwB,UAAA;EAAA,iBAMhB,MAAA;EAAA,iBACA,IAAA;EAAA,iBACA,IAAA;EAAA,iBAPF,QAAA;EAAA,iBACA,QAAA;EAAA,iBACA,UAAA;cAGE,MAAA,EAAQ,YAAA,EACR,IAAA,EAAM,OAAA,EACN,IAAA;IAAS,OAAA;EAAA;EN8C3B;;;;EAAA,QMvCO,QAAA;EAWF,QAAA,CAAS,IAAA,UAAc,GAAA,YAAe,OAAA;EAAA,QAK9B,gBAAA;EAkCR,aAAA,CAAc,IAAA,UAAc,GAAA,YAAe,OAAA;EAoB3C,UAAA,CAAW,IAAA,UAAc,GAAA,YAAe,OAAA;AAAA"}
|