@contentrain/mcp 3.6.8 → 3.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/{annotations-CXHPnS-M.mjs → annotations-BYkjoj2d.mjs} +1 -1
- package/dist/{annotations-CXHPnS-M.mjs.map → annotations-BYkjoj2d.mjs.map} +1 -1
- package/dist/{apply-manager-llmSyK5K.mjs → apply-manager-B-VMXKiW.mjs} +3 -3
- package/dist/{apply-manager-llmSyK5K.mjs.map → apply-manager-B-VMXKiW.mjs.map} +1 -1
- package/dist/{astro-parser-dhFgc2wA.mjs → astro-parser-DsGdiUnu.mjs} +3 -3
- package/dist/{astro-parser-dhFgc2wA.mjs.map → astro-parser-DsGdiUnu.mjs.map} +1 -1
- package/dist/{availability-DYHNoP0_.mjs → availability-CO8Xfx3w.mjs} +1 -1
- package/dist/{availability-DYHNoP0_.mjs.map → availability-CO8Xfx3w.mjs.map} +1 -1
- package/dist/base-branch-Cw9DWvCC.mjs +121 -0
- package/dist/base-branch-Cw9DWvCC.mjs.map +1 -0
- package/dist/{branch-lifecycle-f4gI_eGo.mjs → branch-lifecycle-CcsSyfSw.mjs} +3 -68
- package/dist/branch-lifecycle-CcsSyfSw.mjs.map +1 -0
- package/dist/{branch-lifecycle-DGlgjnC7.d.mts → branch-lifecycle-CirYfTXu.d.mts} +1 -1
- package/dist/{branch-lifecycle-DGlgjnC7.d.mts.map → branch-lifecycle-CirYfTXu.d.mts.map} +1 -1
- package/dist/{compiler-CR9F0wZZ.mjs → compiler-CKelkozt.mjs} +2 -2
- package/dist/{compiler-CR9F0wZZ.mjs.map → compiler-CKelkozt.mjs.map} +1 -1
- package/dist/{compiler-sfc.cjs-Cpexs92i.mjs → compiler-sfc.cjs-BH5AEf2z.mjs} +2 -2
- package/dist/{compiler-sfc.cjs-Cpexs92i.mjs.map → compiler-sfc.cjs-BH5AEf2z.mjs.map} +1 -1
- package/dist/{content-manager-BqaMkMVk.d.mts → content-manager-wKsAkhly.d.mts} +2 -2
- package/dist/{content-manager-BqaMkMVk.d.mts.map → content-manager-wKsAkhly.d.mts.map} +1 -1
- package/dist/core/apply-manager.mjs +5 -4
- package/dist/core/config.d.mts +1 -1
- package/dist/core/content-manager.d.mts +1 -1
- package/dist/core/context.d.mts +1 -1
- package/dist/core/contracts/index.d.mts +1 -1
- package/dist/core/doctor.mjs +4 -3
- package/dist/core/graph-builder.mjs +2 -2
- package/dist/core/model-manager.d.mts +1 -1
- package/dist/core/ops/index.d.mts +3 -3
- package/dist/core/overlay-reader.d.mts +1 -1
- package/dist/core/scan-config.mjs +1 -1
- package/dist/core/scanner.mjs +3 -3
- package/dist/core/validator/index.d.mts +1 -1
- package/dist/{detect-DUYFv3-O.mjs → detect-B1QHYA0u.mjs} +1 -1
- package/dist/{detect-DUYFv3-O.mjs.map → detect-B1QHYA0u.mjs.map} +1 -1
- package/dist/{doctor-fIG4TtsO.mjs → doctor-Du4MVG5w.mjs} +3 -3
- package/dist/{doctor-fIG4TtsO.mjs.map → doctor-Du4MVG5w.mjs.map} +1 -1
- package/dist/{errors-DLkAWnzQ.mjs → errors-B0NcpPIp.mjs} +1 -1
- package/dist/{errors-DLkAWnzQ.mjs.map → errors-B0NcpPIp.mjs.map} +1 -1
- package/dist/{errors-BmLE-JSC.mjs → errors-De8Noahk.mjs} +2 -2
- package/dist/{errors-BmLE-JSC.mjs.map → errors-De8Noahk.mjs.map} +1 -1
- package/dist/git/base-branch.d.mts +61 -0
- package/dist/git/base-branch.d.mts.map +1 -0
- package/dist/git/base-branch.mjs +2 -0
- package/dist/git/branch-lifecycle.d.mts +1 -1
- package/dist/git/branch-lifecycle.mjs +2 -1
- package/dist/git/errors.mjs +1 -1
- package/dist/git/reconcile.d.mts +1 -1
- package/dist/git/reconcile.mjs +5 -4
- package/dist/git/transaction.d.mts +1 -1
- package/dist/git/transaction.mjs +4 -3
- package/dist/{graph-builder-CNswWNtb.mjs → graph-builder-BPhi-1KH.mjs} +2 -2
- package/dist/{graph-builder-CNswWNtb.mjs.map → graph-builder-BPhi-1KH.mjs.map} +1 -1
- package/dist/{id-CjGMPP1k.mjs → id-jrHWdtqn.mjs} +1 -1
- package/dist/{id-CjGMPP1k.mjs.map → id-jrHWdtqn.mjs.map} +1 -1
- package/dist/{index-DKZNUqpP.d.mts → index-CkhXTFdk.d.mts} +2 -2
- package/dist/{index-DKZNUqpP.d.mts.map → index-CkhXTFdk.d.mts.map} +1 -1
- package/dist/index.mjs +16 -15
- package/dist/index.mjs.map +1 -1
- package/dist/{local-DkDTrGt_.mjs → local-BySg4Ib-.mjs} +4 -3
- package/dist/{local-DkDTrGt_.mjs.map → local-BySg4Ib-.mjs.map} +1 -1
- package/dist/{node-CpLm3qr6.mjs → node-hrF4BrH2.mjs} +1 -1
- package/dist/{node-CpLm3qr6.mjs.map → node-hrF4BrH2.mjs.map} +1 -1
- package/dist/{paths-C6_C1gLp.mjs → paths-Dz5v-z00.mjs} +1 -1
- package/dist/{paths-C6_C1gLp.mjs.map → paths-Dz5v-z00.mjs.map} +1 -1
- package/dist/providers/github/index.d.mts +1 -1
- package/dist/providers/github/index.mjs +2 -2
- package/dist/providers/gitlab/index.d.mts +1 -1
- package/dist/providers/gitlab/index.mjs +2 -2
- package/dist/providers/local/index.d.mts +1 -1
- package/dist/providers/local/index.mjs +5 -4
- package/dist/{reconcile-Cq60cldB.mjs → reconcile-BWbckvbv.mjs} +4 -3
- package/dist/{reconcile-Cq60cldB.mjs.map → reconcile-BWbckvbv.mjs.map} +1 -1
- package/dist/{scan-config-DkRKu_ON.mjs → scan-config-S5fkzNhL.mjs} +1 -1
- package/dist/{scan-config-DkRKu_ON.mjs.map → scan-config-S5fkzNhL.mjs.map} +1 -1
- package/dist/{scanner-BLo3ssiF.mjs → scanner-0sc_pARB.mjs} +6 -6
- package/dist/{scanner-BLo3ssiF.mjs.map → scanner-0sc_pARB.mjs.map} +1 -1
- package/dist/server/http/index.d.mts +1 -1
- package/dist/server/http/index.mjs +16 -15
- package/dist/server/http/index.mjs.map +1 -1
- package/dist/{server-BJkE8D5N.mjs → server-BdGsBjPT.mjs} +20 -16
- package/dist/server-BdGsBjPT.mjs.map +1 -0
- package/dist/{server-CUtmhlno.d.mts → server-D1c-79hA.d.mts} +2 -2
- package/dist/{server-CUtmhlno.d.mts.map → server-D1c-79hA.d.mts.map} +1 -1
- package/dist/server.d.mts +1 -1
- package/dist/server.mjs +16 -15
- package/dist/{source-map-generator-Clb2Eqse.mjs → source-map-generator-DVDSWUZ1.mjs} +1 -1
- package/dist/{source-map-generator-Clb2Eqse.mjs.map → source-map-generator-DVDSWUZ1.mjs.map} +1 -1
- package/dist/{svelte-parser-CBN7Jxb7.mjs → svelte-parser-CVUIcakb.mjs} +3 -3
- package/dist/{svelte-parser-CBN7Jxb7.mjs.map → svelte-parser-CVUIcakb.mjs.map} +1 -1
- package/dist/templates/index.mjs +1 -1
- package/dist/{templates-BZbohJc8.mjs → templates-Dn-iZyzp.mjs} +1 -1
- package/dist/{templates-BZbohJc8.mjs.map → templates-Dn-iZyzp.mjs.map} +1 -1
- package/dist/testing/memory-provider.d.mts +1 -1
- package/dist/tools/annotations.mjs +1 -1
- package/dist/tools/availability.mjs +1 -1
- package/dist/{transaction-BWJt1KQb.mjs → transaction-DfrvGIfk.mjs} +4 -3
- package/dist/{transaction-BWJt1KQb.mjs.map → transaction-DfrvGIfk.mjs.map} +1 -1
- package/dist/{tsx-parser-go5V9zoc.mjs → tsx-parser-DgFdpsea.mjs} +1 -1
- package/dist/{tsx-parser-go5V9zoc.mjs.map → tsx-parser-DgFdpsea.mjs.map} +1 -1
- package/dist/tsx-parser-v8C-7cZv.mjs +2 -0
- package/dist/util/detect.mjs +1 -1
- package/dist/util/id.mjs +1 -1
- package/dist/{vue-parser-5Mp8w-mb.mjs → vue-parser-DvliI6UE.mjs} +3 -3
- package/dist/{vue-parser-5Mp8w-mb.mjs.map → vue-parser-DvliI6UE.mjs.map} +1 -1
- package/package.json +8 -4
- package/dist/branch-lifecycle-f4gI_eGo.mjs.map +0 -1
- package/dist/server-BJkE8D5N.mjs.map +0 -1
- package/dist/tsx-parser-DOQpJVgK.mjs +0 -2
- /package/dist/{index-A5iw3yDk.d.mts → index-PCQQBpIH.d.mts} +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { _ as RepoReader } from "./index-
|
|
1
|
+
import { _ as RepoReader } from "./index-PCQQBpIH.mjs";
|
|
2
2
|
import { ContentrainConfig, LocaleStrategy, ModelDefinition, Vocabulary, parseMarkdownFrontmatter as parseFrontmatter, serializeMarkdownFrontmatter as serializeFrontmatter, validateEntryId as validateEntryId$1, validateLocale as validateLocale$1, validateSlug as validateSlug$1 } from "@contentrain/types";
|
|
3
3
|
|
|
4
4
|
//#region src/core/content-manager.d.ts
|
|
@@ -73,4 +73,4 @@ declare function readContent(projectRoot: string, model: ModelDefinition, opts:
|
|
|
73
73
|
}): Promise<unknown>;
|
|
74
74
|
//#endregion
|
|
75
75
|
export { validateEntryId$1 as _, ListOpts as a, writeContent as b, deleteContent as c, readContent as d, resolveContentDir as f, serializeFrontmatter as g, resolveMdFilePath as h, DeleteOpts as i, listContent as l, resolveLocaleStrategy as m, ContentFileModel as n, LocaleStrategyModel as o, resolveJsonFilePath as p, ContentPathModel as r, WriteResult as s, ContentEntry as t, parseFrontmatter as u, validateLocale$1 as v, validateSlug$1 as y };
|
|
76
|
-
//# sourceMappingURL=content-manager-
|
|
76
|
+
//# sourceMappingURL=content-manager-wKsAkhly.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"content-manager-
|
|
1
|
+
{"version":3,"file":"content-manager-wKsAkhly.d.mts","names":[],"sources":["../src/core/content-manager.ts"],"mappings":";;;;KAyBY,gBAAA,GAAmB,IAAA,CAAK,eAAA;AAAA,KACxB,mBAAA,GAAsB,IAAA,CAAK,eAAA;AAAA,KAC3B,gBAAA,GAAmB,IAAA,CAAK,eAAA;AAAA,iBAEpB,iBAAA,CAAkB,WAAA,UAAqB,KAAA,EAAO,gBAAA;AAAA,iBAO9C,qBAAA,CAAsB,KAAA,EAAO,mBAAA,GAAsB,cAAA;;iBAKnD,mBAAA,CAAoB,GAAA,UAAa,KAAA,EAAO,gBAAA,EAAkB,MAAA;;iBAc1D,iBAAA,CAAkB,GAAA,UAAa,KAAA,EAAO,IAAA,CAAK,eAAA,+BAA8C,MAAA,UAAgB,IAAA;AAAA,UAexG,YAAA;EACf,EAAA;EACA,IAAA;EACA,MAAA;EACA,IAAA,EAAM,MAAA;EA7CQ;;;;;EAmDd,UAAA;EAnDqD;EAqDrD,SAAA;AAAA;AAAA,UAGe,WAAA;EACf,MAAA;EACA,EAAA;EACA,IAAA;EACA,MAAA;EACA,UAAA;AAAA;AAAA,UAGe,UAAA;EACf,EAAA;EACA,IAAA;EACA,MAAA;EACA,IAAA;EAxDsE;EA0DtE,aAAA;AAAA;AAAA,UAGe,QAAA;EACf,MAAA;EACA,MAAA,GAAS,MAAA;EACT,OAAA;EACA,KAAA;EACA,MAAA;AAAA;AAAA,iBAKoB,YAAA,CACpB,WAAA,UACA,KAAA,EAAO,eAAA,EACP,OAAA,EAAS,YAAA,IACT,MAAA,EAAQ,iBAAA,EACR,UAAA,GAAa,UAAA,UACZ,OAAA,CAAQ,WAAA;AAAA,iBAuJW,aAAA,CACpB,WAAA,UACA,KAAA,EAAO,eAAA,EACP,IAAA,EAAM,UAAA,GACL,OAAA;;;;;;;;AA3MH;;;;;;;;iBAwVgB,WAAA,CACd,WAAA,UACA,KAAA,EAAO,eAAA,EACP,IAAA,EAAM,QAAA,EACN,MAAA,EAAQ,iBAAA,GACP,OAAA;AAAA,iBACa,WAAA,CACd,MAAA,EAAQ,UAAA,EACR,KAAA,EAAO,eAAA,EACP,IAAA,EAAM,QAAA,EACN,MAAA,EAAQ,iBAAA,GACP,OAAA;AAAA,iBA0QmB,WAAA,CACpB,WAAA,UACA,KAAA,EAAO,eAAA,EACP,IAAA;EAAQ,MAAA;EAAgB,OAAA;EAAkB,IAAA;AAAA,IACzC,OAAA"}
|
|
@@ -4,8 +4,9 @@ import "../fs-BFTuXbbw.mjs";
|
|
|
4
4
|
import "../meta-manager-CSpKtjnP.mjs";
|
|
5
5
|
import "../config-BLwZtwlI.mjs";
|
|
6
6
|
import "../identity-CxAB9Qb8.mjs";
|
|
7
|
-
import "../branch-
|
|
8
|
-
import "../
|
|
9
|
-
import "../
|
|
10
|
-
import
|
|
7
|
+
import "../base-branch-Cw9DWvCC.mjs";
|
|
8
|
+
import "../branch-lifecycle-CcsSyfSw.mjs";
|
|
9
|
+
import "../id-jrHWdtqn.mjs";
|
|
10
|
+
import "../transaction-DfrvGIfk.mjs";
|
|
11
|
+
import { a as detectFileFramework, c as validatePatchPath, i as checkSyntax, n as applyExtract, o as replaceInLine, r as applyReuse, s as validateFrameworkExpression, t as PATCHABLE_EXTENSIONS } from "../apply-manager-B-VMXKiW.mjs";
|
|
11
12
|
export { PATCHABLE_EXTENSIONS, applyExtract, applyReuse, checkSyntax, detectFileFramework, replaceInLine, validateFrameworkExpression, validatePatchPath };
|
package/dist/core/config.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { _ as validateEntryId, a as ListOpts, b as writeContent, c as deleteContent, d as readContent, f as resolveContentDir, g as serializeFrontmatter, h as resolveMdFilePath, i as DeleteOpts, l as listContent, m as resolveLocaleStrategy, n as ContentFileModel, o as LocaleStrategyModel, p as resolveJsonFilePath, r as ContentPathModel, s as WriteResult, t as ContentEntry, u as parseFrontmatter, v as validateLocale, y as validateSlug } from "../content-manager-
|
|
1
|
+
import { _ as validateEntryId, a as ListOpts, b as writeContent, c as deleteContent, d as readContent, f as resolveContentDir, g as serializeFrontmatter, h as resolveMdFilePath, i as DeleteOpts, l as listContent, m as resolveLocaleStrategy, n as ContentFileModel, o as LocaleStrategyModel, p as resolveJsonFilePath, r as ContentPathModel, s as WriteResult, t as ContentEntry, u as parseFrontmatter, v as validateLocale, y as validateSlug } from "../content-manager-wKsAkhly.mjs";
|
|
2
2
|
export { ContentEntry, ContentFileModel, ContentPathModel, DeleteOpts, ListOpts, LocaleStrategyModel, WriteResult, deleteContent, listContent, parseFrontmatter, readContent, resolveContentDir, resolveJsonFilePath, resolveLocaleStrategy, resolveMdFilePath, serializeFrontmatter, validateEntryId, validateLocale, validateSlug, writeContent };
|
package/dist/core/context.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { _ as RepoReader, a as FileChange, c as MediaAsset, d as MediaListResult, f as MediaProvider, g as RepoProvider, h as ProviderCapabilities, i as CommitAuthor, l as MediaIngestInput, m as MergeResult, n as Branch, o as FileDiff, p as MediaUpdateInput, r as Commit, s as LOCAL_CAPABILITIES, t as ApplyPlanInput, u as MediaListOptions, v as RepoWriter, y as WriteReadiness } from "../../index-
|
|
1
|
+
import { _ as RepoReader, a as FileChange, c as MediaAsset, d as MediaListResult, f as MediaProvider, g as RepoProvider, h as ProviderCapabilities, i as CommitAuthor, l as MediaIngestInput, m as MergeResult, n as Branch, o as FileDiff, p as MediaUpdateInput, r as Commit, s as LOCAL_CAPABILITIES, t as ApplyPlanInput, u as MediaListOptions, v as RepoWriter, y as WriteReadiness } from "../../index-PCQQBpIH.mjs";
|
|
2
2
|
export { ApplyPlanInput, Branch, Commit, CommitAuthor, FileChange, FileDiff, LOCAL_CAPABILITIES, MediaAsset, MediaIngestInput, MediaListOptions, MediaListResult, MediaProvider, MediaUpdateInput, MergeResult, ProviderCapabilities, RepoProvider, RepoReader, RepoWriter, WriteReadiness };
|
package/dist/core/doctor.mjs
CHANGED
|
@@ -3,7 +3,8 @@ import "../fs-BFTuXbbw.mjs";
|
|
|
3
3
|
import "../meta-manager-CSpKtjnP.mjs";
|
|
4
4
|
import "../config-BLwZtwlI.mjs";
|
|
5
5
|
import "../identity-CxAB9Qb8.mjs";
|
|
6
|
-
import "../branch-
|
|
7
|
-
import "../
|
|
8
|
-
import
|
|
6
|
+
import "../base-branch-Cw9DWvCC.mjs";
|
|
7
|
+
import "../branch-lifecycle-CcsSyfSw.mjs";
|
|
8
|
+
import "../scan-config-S5fkzNhL.mjs";
|
|
9
|
+
import { t as runDoctor } from "../doctor-Du4MVG5w.mjs";
|
|
9
10
|
export { runDoctor };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { _ as RepoReader, a as FileChange } from "../../index-
|
|
2
|
-
import { t as ContentEntry } from "../../content-manager-
|
|
3
|
-
import { a as ContentSaveEntryResult, i as ReconcileResult, n as ReconcileInput, o as ContentSavePlan, r as ReconcilePlan, s as OpPlan, t as planReconcile } from "../../index-
|
|
1
|
+
import { _ as RepoReader, a as FileChange } from "../../index-PCQQBpIH.mjs";
|
|
2
|
+
import { t as ContentEntry } from "../../content-manager-wKsAkhly.mjs";
|
|
3
|
+
import { a as ContentSaveEntryResult, i as ReconcileResult, n as ReconcileInput, o as ContentSavePlan, r as ReconcilePlan, s as OpPlan, t as planReconcile } from "../../index-CkhXTFdk.mjs";
|
|
4
4
|
import { ContentrainConfig, ModelDefinition, Vocabulary } from "@contentrain/types";
|
|
5
5
|
|
|
6
6
|
//#region src/core/ops/apply-to-worktree.d.ts
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import "../fs-BFTuXbbw.mjs";
|
|
2
|
-
import { a as classifyFile, i as autoDetectSourceDirs, n as SCAN_EXTENSIONS, o as discoverFiles, r as SCAN_IGNORE_DIRS, t as MAX_SCAN_FILES } from "../scan-config-
|
|
2
|
+
import { a as classifyFile, i as autoDetectSourceDirs, n as SCAN_EXTENSIONS, o as discoverFiles, r as SCAN_IGNORE_DIRS, t as MAX_SCAN_FILES } from "../scan-config-S5fkzNhL.mjs";
|
|
3
3
|
export { MAX_SCAN_FILES, SCAN_EXTENSIONS, SCAN_IGNORE_DIRS, autoDetectSourceDirs, classifyFile, discoverFiles };
|
package/dist/core/scanner.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import "../fs-BFTuXbbw.mjs";
|
|
2
|
-
import "../scan-config-
|
|
3
|
-
import { n as scanSummary, t as scanCandidates } from "../scanner-
|
|
4
|
-
import "../tsx-parser-
|
|
2
|
+
import "../scan-config-S5fkzNhL.mjs";
|
|
3
|
+
import { n as scanSummary, t as scanCandidates } from "../scanner-0sc_pARB.mjs";
|
|
4
|
+
import "../tsx-parser-DgFdpsea.mjs";
|
|
5
5
|
export { scanCandidates, scanSummary };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { _ as RepoReader } from "../../index-
|
|
1
|
+
import { _ as RepoReader } from "../../index-PCQQBpIH.mjs";
|
|
2
2
|
import { EntryMeta, FieldDef, ModelDefinition, ValidationError, ValidationResult } from "@contentrain/types";
|
|
3
3
|
|
|
4
4
|
//#region src/core/validator/entry.d.ts
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"detect-DUYFv3-O.mjs","names":[],"sources":["../src/util/detect.ts"],"sourcesContent":["import type { StackType, Platform } from '@contentrain/types'\nimport { join, dirname } from 'node:path'\nimport { readdir } from 'node:fs/promises'\nimport { readJson, pathExists, readText } 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 // Look DOWN into the workspaces. The walk above handles \"I am inside a\n // package, find the monorepo root\"; this handles the opposite and more\n // common case — projectRoot IS the monorepo root, whose package.json holds\n // only tooling (turbo, nx) while the frameworks live in apps/* and\n // packages/*. Without this a Next.js monorepo reports `other`, and the\n // stack is what picks the replacement conventions during normalize.\n if (stack === 'other') {\n stack = await detectFromWorkspaces(projectRoot)\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\n/**\n * Ranked by how strongly the stack determines content conventions — a\n * meta-framework dictates the i18n setup and the replacement expression,\n * a plain UI library does not. Frequency would be the wrong tie-break: a\n * monorepo with one Next app and two React packages is a Next project.\n */\nconst STACK_PRIORITY: readonly StackType[] = [\n 'nuxt', 'next', 'astro', 'sveltekit', 'remix', 'analog',\n 'expo', 'react-native',\n 'vue', 'react', 'svelte', 'solid', 'angular',\n 'electron', 'tauri',\n 'nestjs', 'fastify', 'express',\n 'eleventy', 'node',\n]\n\n/** Cap the fan-out so a large monorepo cannot turn detection into a crawl. */\nconst MAX_WORKSPACE_DIRS = 50\n\n/**\n * Workspace globs from `pnpm-workspace.yaml` or the `workspaces` field.\n *\n * The pnpm file is parsed line by line rather than with a YAML library: we\n * need exactly one list of strings with a fixed shape, and a parser\n * dependency for that is not worth the install size.\n */\nasync function readWorkspacePatterns(root: string): Promise<string[]> {\n const pnpm = await readText(join(root, 'pnpm-workspace.yaml'))\n if (pnpm) {\n const patterns: string[] = []\n let inPackages = false\n for (const raw of pnpm.split('\\n')) {\n const line = raw.replace(/#.*$/, '').trimEnd()\n if (/^packages:\\s*$/.test(line)) { inPackages = true; continue }\n if (!inPackages) continue\n const item = /^\\s+-\\s*['\"]?([^'\"]+?)['\"]?\\s*$/.exec(line)\n if (item?.[1]) { patterns.push(item[1]); continue }\n if (line.trim() !== '') break // a new top-level key ends the list\n }\n if (patterns.length > 0) return patterns\n }\n\n const pkg = await readJson<PackageJson>(join(root, 'package.json'))\n const ws = pkg?.workspaces\n if (Array.isArray(ws)) return ws\n if (ws && Array.isArray(ws.packages)) return ws.packages\n return []\n}\n\n/**\n * Expand workspace patterns to directories. Only a trailing `*` segment is\n * expanded — that covers `apps/*` and `packages/*`, which is what real\n * workspace files use. Deeper globs and negations are skipped rather than\n * half-supported.\n */\nasync function listWorkspaceDirs(root: string): Promise<string[]> {\n const patterns = (await readWorkspacePatterns(root)).filter(p => !p.startsWith('!'))\n\n const expanded = await Promise.all(patterns.map(async (pattern) => {\n const star = pattern.indexOf('*')\n if (star === -1) return [join(root, pattern)]\n const base = join(root, pattern.slice(0, star).replace(/\\/+$/, ''))\n try {\n const entries = await readdir(base, { withFileTypes: true })\n return entries.filter(e => e.isDirectory()).map(e => join(base, e.name))\n } catch {\n return []\n }\n }))\n\n return expanded.flat().slice(0, MAX_WORKSPACE_DIRS)\n}\n\n/** Highest-priority stack across the workspace packages. */\nasync function detectFromWorkspaces(root: string): Promise<StackType> {\n const dirs = await listWorkspaceDirs(root)\n if (dirs.length === 0) return 'other'\n\n const found = new Set(await Promise.all(dirs.map(d => detectFromDeps(d))))\n return STACK_PRIORITY.find(s => found.has(s)) ?? '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 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 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 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 // Same blind spot as stack detection: at a monorepo root the interesting\n // dependencies (the i18n library above all) sit in the workspace packages,\n // not in the root manifest. Reporting \"no i18n\" for a project that has it\n // would send normalize down the wrong path.\n const workspaceDirs = await listWorkspaceDirs(projectRoot)\n if (workspaceDirs.length > 0) {\n const pkgs = await Promise.all(\n workspaceDirs.map(d => readJson<PackageJson>(join(d, 'package.json'))),\n )\n for (const wsPkg of pkgs) {\n if (wsPkg) Object.assign(allDeps, wsPkg.dependencies, wsPkg.devDependencies)\n }\n }\n\n return allDeps\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"detect-B1QHYA0u.mjs","names":[],"sources":["../src/util/detect.ts"],"sourcesContent":["import type { StackType, Platform } from '@contentrain/types'\nimport { join, dirname } from 'node:path'\nimport { readdir } from 'node:fs/promises'\nimport { readJson, pathExists, readText } 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 // Look DOWN into the workspaces. The walk above handles \"I am inside a\n // package, find the monorepo root\"; this handles the opposite and more\n // common case — projectRoot IS the monorepo root, whose package.json holds\n // only tooling (turbo, nx) while the frameworks live in apps/* and\n // packages/*. Without this a Next.js monorepo reports `other`, and the\n // stack is what picks the replacement conventions during normalize.\n if (stack === 'other') {\n stack = await detectFromWorkspaces(projectRoot)\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\n/**\n * Ranked by how strongly the stack determines content conventions — a\n * meta-framework dictates the i18n setup and the replacement expression,\n * a plain UI library does not. Frequency would be the wrong tie-break: a\n * monorepo with one Next app and two React packages is a Next project.\n */\nconst STACK_PRIORITY: readonly StackType[] = [\n 'nuxt', 'next', 'astro', 'sveltekit', 'remix', 'analog',\n 'expo', 'react-native',\n 'vue', 'react', 'svelte', 'solid', 'angular',\n 'electron', 'tauri',\n 'nestjs', 'fastify', 'express',\n 'eleventy', 'node',\n]\n\n/** Cap the fan-out so a large monorepo cannot turn detection into a crawl. */\nconst MAX_WORKSPACE_DIRS = 50\n\n/**\n * Workspace globs from `pnpm-workspace.yaml` or the `workspaces` field.\n *\n * The pnpm file is parsed line by line rather than with a YAML library: we\n * need exactly one list of strings with a fixed shape, and a parser\n * dependency for that is not worth the install size.\n */\nasync function readWorkspacePatterns(root: string): Promise<string[]> {\n const pnpm = await readText(join(root, 'pnpm-workspace.yaml'))\n if (pnpm) {\n const patterns: string[] = []\n let inPackages = false\n for (const raw of pnpm.split('\\n')) {\n const line = raw.replace(/#.*$/, '').trimEnd()\n if (/^packages:\\s*$/.test(line)) { inPackages = true; continue }\n if (!inPackages) continue\n const item = /^\\s+-\\s*['\"]?([^'\"]+?)['\"]?\\s*$/.exec(line)\n if (item?.[1]) { patterns.push(item[1]); continue }\n if (line.trim() !== '') break // a new top-level key ends the list\n }\n if (patterns.length > 0) return patterns\n }\n\n const pkg = await readJson<PackageJson>(join(root, 'package.json'))\n const ws = pkg?.workspaces\n if (Array.isArray(ws)) return ws\n if (ws && Array.isArray(ws.packages)) return ws.packages\n return []\n}\n\n/**\n * Expand workspace patterns to directories. Only a trailing `*` segment is\n * expanded — that covers `apps/*` and `packages/*`, which is what real\n * workspace files use. Deeper globs and negations are skipped rather than\n * half-supported.\n */\nasync function listWorkspaceDirs(root: string): Promise<string[]> {\n const patterns = (await readWorkspacePatterns(root)).filter(p => !p.startsWith('!'))\n\n const expanded = await Promise.all(patterns.map(async (pattern) => {\n const star = pattern.indexOf('*')\n if (star === -1) return [join(root, pattern)]\n const base = join(root, pattern.slice(0, star).replace(/\\/+$/, ''))\n try {\n const entries = await readdir(base, { withFileTypes: true })\n return entries.filter(e => e.isDirectory()).map(e => join(base, e.name))\n } catch {\n return []\n }\n }))\n\n return expanded.flat().slice(0, MAX_WORKSPACE_DIRS)\n}\n\n/** Highest-priority stack across the workspace packages. */\nasync function detectFromWorkspaces(root: string): Promise<StackType> {\n const dirs = await listWorkspaceDirs(root)\n if (dirs.length === 0) return 'other'\n\n const found = new Set(await Promise.all(dirs.map(d => detectFromDeps(d))))\n return STACK_PRIORITY.find(s => found.has(s)) ?? '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 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 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 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 // Same blind spot as stack detection: at a monorepo root the interesting\n // dependencies (the i18n library above all) sit in the workspace packages,\n // not in the root manifest. Reporting \"no i18n\" for a project that has it\n // would send normalize down the wrong path.\n const workspaceDirs = await listWorkspaceDirs(projectRoot)\n if (workspaceDirs.length > 0) {\n const pkgs = await Promise.all(\n workspaceDirs.map(d => readJson<PackageJson>(join(d, 'package.json'))),\n )\n for (const wsPkg of pkgs) {\n if (wsPkg) Object.assign(allDeps, wsPkg.dependencies, wsPkg.devDependencies)\n }\n }\n\n return allDeps\n}\n"],"mappings":"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"}
|
|
@@ -2,8 +2,8 @@ import { S as resolveLocaleStrategy, b as resolveContentDir, d as readModel, m a
|
|
|
2
2
|
import { a as readJson, i as readDir, o as readText, r as pathExists, t as contentrainDir } from "./fs-BFTuXbbw.mjs";
|
|
3
3
|
import { t as readConfig } from "./config-BLwZtwlI.mjs";
|
|
4
4
|
import { r as createGit } from "./identity-CxAB9Qb8.mjs";
|
|
5
|
-
import { c as listRemoteCrBranches, n as checkBranchHealth } from "./branch-lifecycle-
|
|
6
|
-
import { i as autoDetectSourceDirs, o as discoverFiles } from "./scan-config-
|
|
5
|
+
import { c as listRemoteCrBranches, n as checkBranchHealth } from "./branch-lifecycle-CcsSyfSw.mjs";
|
|
6
|
+
import { i as autoDetectSourceDirs, o as discoverFiles } from "./scan-config-S5fkzNhL.mjs";
|
|
7
7
|
import { join } from "node:path";
|
|
8
8
|
import { readdir, stat } from "node:fs/promises";
|
|
9
9
|
//#region src/core/doctor.ts
|
|
@@ -393,4 +393,4 @@ async function analyzeMissingLocaleKeys(projectRoot, config) {
|
|
|
393
393
|
//#endregion
|
|
394
394
|
export { runDoctor as t };
|
|
395
395
|
|
|
396
|
-
//# sourceMappingURL=doctor-
|
|
396
|
+
//# sourceMappingURL=doctor-Du4MVG5w.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"doctor-fIG4TtsO.mjs","names":[],"sources":["../src/core/doctor.ts"],"sourcesContent":["import { join } from 'node:path'\nimport { createGit } from '../git/identity.js'\nimport { readdir, stat } from 'node:fs/promises'\nimport type { Dirent } from 'node:fs'\nimport type { ContentrainConfig, ModelDefinition } from '@contentrain/types'\nimport { readConfig } from './config.js'\nimport { listModels, readModel, validateModelDefinition } 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 = createGit(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 parse AND satisfy the schema contract ───\n //\n // This check used to report \"all valid\" on the strength of JSON.parse alone,\n // which was a claim it had not earned. Model reads are unvalidated casts, so a\n // definition can be structurally wrong — a missing title_field, a field\n // constraint that cannot hold — and still load without a word. Doctor reports;\n // `contentrain validate --fix` is what repairs.\n if (hasCrDir) {\n try {\n // Enumerate the files, not `listModels`. `listModels` drops anything that\n // fails to parse, so routing this check through it made the \"failed to\n // parse\" branch unreachable — a corrupt model file was invisible to the\n // check whose job is to find it.\n const modelsDir = join(crDir, 'models')\n const files = (await readDir(modelsDir)).filter(f => f.endsWith('.json'))\n const parsed = await Promise.all(\n files.map(f => readJson<ModelDefinition>(join(modelsDir, f))),\n )\n const unparseable = files.filter((_, i) => parsed[i] === null)\n const invalid = parsed\n .filter((m): m is ModelDefinition => m !== null)\n .filter(m => validateModelDefinition(m).errors.length > 0)\n .map(m => m.id)\n const models = parsed.filter((m): m is ModelDefinition => m !== null)\n\n const problems = [\n ...(unparseable.length > 0 ? [`${unparseable.length} failed to parse (${unparseable.join(', ')})`] : []),\n ...(invalid.length > 0 ? [`${invalid.length} invalid (${invalid.join(', ')}) — run: contentrain validate --fix`] : []),\n ]\n checks.push({\n name: 'Models',\n pass: problems.length === 0,\n detail: `${models.length} model(s)${problems.length === 0 ? ', all valid' : `, ${problems.join('; ')}`}`,\n severity: problems.length === 0 ? 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 [clientMtime, modelsMtime] = await Promise.all([\n newestFileMtime(clientDir),\n newestFileMtime(modelsDir),\n ])\n if (clientMtime === null || modelsMtime === null) {\n checks.push({ name: 'SDK client', pass: true, detail: 'Could not check' })\n } else {\n const fresh = clientMtime >= modelsMtime\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 }\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 dictionary keys',\n pass: unusedKeys.length === 0,\n detail: unusedKeys.length === 0\n ? 'All dictionary keys referenced in source'\n : `${unusedKeys.length} dictionary 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\n/**\n * Newest mtime among the files under `dir`, recursively — null if it holds none.\n *\n * Stat the files, never the directory. A directory's mtime only moves when an\n * entry is added, removed, or renamed inside it: `generate` rewrites the client\n * files in place, so `.contentrain/client` never moves after the first run,\n * while a selective sync recreates model files via `git checkout`, which does\n * move `.contentrain/models`. Comparing the two directories therefore reported\n * \"stale\" permanently after any model save.\n */\nasync function newestFileMtime(dir: string): Promise<number | null> {\n const entries: Dirent[] = await readdir(dir, { withFileTypes: true }).catch(() => [])\n const mtimes = await Promise.all(entries.map(async (entry) => {\n const full = join(dir, entry.name)\n if (entry.isDirectory()) return newestFileMtime(full)\n return stat(full).then(s => s.mtimeMs, () => null)\n }))\n const known = mtimes.filter((m): m is number => m !== null)\n return known.length > 0 ? Math.max(...known) : null\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 // Dictionaries only.\n //\n // A dictionary key IS the reference — source code writes `t('nav.home')`, so\n // a key absent from source is genuinely dead. Collection entry IDs and\n // document slugs are not: entries are fetched by query, and a hex id like\n // `0a1b2c3d4e5f` will never appear in a source file. Checking them produced\n // a report that was mostly noise — of 134 warnings on a real project, ~120\n // were collection and document rows, and the real findings were lost among\n // them.\n const models = (await listModels(projectRoot)).filter(m => m.kind === 'dictionary')\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\n/**\n * Keys of a model that source code could plausibly reference by name.\n *\n * Only dictionaries have any: their keys are semantic addresses that appear\n * verbatim in source. Every other kind is queried, so its ids and slugs are\n * data, not identifiers, and looking for them in source finds nothing and\n * means nothing.\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
|
+
{"version":3,"file":"doctor-Du4MVG5w.mjs","names":[],"sources":["../src/core/doctor.ts"],"sourcesContent":["import { join } from 'node:path'\nimport { createGit } from '../git/identity.js'\nimport { readdir, stat } from 'node:fs/promises'\nimport type { Dirent } from 'node:fs'\nimport type { ContentrainConfig, ModelDefinition } from '@contentrain/types'\nimport { readConfig } from './config.js'\nimport { listModels, readModel, validateModelDefinition } 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 = createGit(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 parse AND satisfy the schema contract ───\n //\n // This check used to report \"all valid\" on the strength of JSON.parse alone,\n // which was a claim it had not earned. Model reads are unvalidated casts, so a\n // definition can be structurally wrong — a missing title_field, a field\n // constraint that cannot hold — and still load without a word. Doctor reports;\n // `contentrain validate --fix` is what repairs.\n if (hasCrDir) {\n try {\n // Enumerate the files, not `listModels`. `listModels` drops anything that\n // fails to parse, so routing this check through it made the \"failed to\n // parse\" branch unreachable — a corrupt model file was invisible to the\n // check whose job is to find it.\n const modelsDir = join(crDir, 'models')\n const files = (await readDir(modelsDir)).filter(f => f.endsWith('.json'))\n const parsed = await Promise.all(\n files.map(f => readJson<ModelDefinition>(join(modelsDir, f))),\n )\n const unparseable = files.filter((_, i) => parsed[i] === null)\n const invalid = parsed\n .filter((m): m is ModelDefinition => m !== null)\n .filter(m => validateModelDefinition(m).errors.length > 0)\n .map(m => m.id)\n const models = parsed.filter((m): m is ModelDefinition => m !== null)\n\n const problems = [\n ...(unparseable.length > 0 ? [`${unparseable.length} failed to parse (${unparseable.join(', ')})`] : []),\n ...(invalid.length > 0 ? [`${invalid.length} invalid (${invalid.join(', ')}) — run: contentrain validate --fix`] : []),\n ]\n checks.push({\n name: 'Models',\n pass: problems.length === 0,\n detail: `${models.length} model(s)${problems.length === 0 ? ', all valid' : `, ${problems.join('; ')}`}`,\n severity: problems.length === 0 ? 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 [clientMtime, modelsMtime] = await Promise.all([\n newestFileMtime(clientDir),\n newestFileMtime(modelsDir),\n ])\n if (clientMtime === null || modelsMtime === null) {\n checks.push({ name: 'SDK client', pass: true, detail: 'Could not check' })\n } else {\n const fresh = clientMtime >= modelsMtime\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 }\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 dictionary keys',\n pass: unusedKeys.length === 0,\n detail: unusedKeys.length === 0\n ? 'All dictionary keys referenced in source'\n : `${unusedKeys.length} dictionary 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\n/**\n * Newest mtime among the files under `dir`, recursively — null if it holds none.\n *\n * Stat the files, never the directory. A directory's mtime only moves when an\n * entry is added, removed, or renamed inside it: `generate` rewrites the client\n * files in place, so `.contentrain/client` never moves after the first run,\n * while a selective sync recreates model files via `git checkout`, which does\n * move `.contentrain/models`. Comparing the two directories therefore reported\n * \"stale\" permanently after any model save.\n */\nasync function newestFileMtime(dir: string): Promise<number | null> {\n const entries: Dirent[] = await readdir(dir, { withFileTypes: true }).catch(() => [])\n const mtimes = await Promise.all(entries.map(async (entry) => {\n const full = join(dir, entry.name)\n if (entry.isDirectory()) return newestFileMtime(full)\n return stat(full).then(s => s.mtimeMs, () => null)\n }))\n const known = mtimes.filter((m): m is number => m !== null)\n return known.length > 0 ? Math.max(...known) : null\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 // Dictionaries only.\n //\n // A dictionary key IS the reference — source code writes `t('nav.home')`, so\n // a key absent from source is genuinely dead. Collection entry IDs and\n // document slugs are not: entries are fetched by query, and a hex id like\n // `0a1b2c3d4e5f` will never appear in a source file. Checking them produced\n // a report that was mostly noise — of 134 warnings on a real project, ~120\n // were collection and document rows, and the real findings were lost among\n // them.\n const models = (await listModels(projectRoot)).filter(m => m.kind === 'dictionary')\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\n/**\n * Keys of a model that source code could plausibly reference by name.\n *\n * Only dictionaries have any: their keys are semantic addresses that appear\n * verbatim in source. Every other kind is queried, so its ids and slugs are\n * data, not identifiers, and looking for them in source finds nothing and\n * means nothing.\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 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors-
|
|
1
|
+
{"version":3,"file":"errors-B0NcpPIp.mjs","names":[],"sources":["../src/providers/shared/errors.ts"],"sourcesContent":["/**\n * Error shapes for API-backed providers, and the mapping from those shapes\n * into Contentrain's structured error envelope.\n *\n * Two common error shapes in the provider SDKs we use:\n *\n * - **Octokit** (`@octokit/rest`) — rejects with an `Error` that has a\n * top-level `.status` number set to the HTTP status code.\n * - **Gitbeaker** (`@gitbeaker/rest`) — rejects with a plain `Error` whose\n * `.cause` includes `{ response: { status } }`.\n */\n\n/**\n * HTTP status behind a provider SDK rejection, or `undefined` when the error\n * is not an HTTP failure (a local git error, a programming mistake, …).\n *\n * We deliberately do NOT fall back to substring matching on the error\n * message — that leniency can silently mask other failures (forbidden repo,\n * deleted project, rate limits) and produce the wrong answer. If either SDK\n * ever stops populating the status field, the regression surfaces in tests\n * rather than being papered over at the reader layer.\n */\nexport function extractHttpStatus(error: unknown): number | undefined {\n if (typeof error !== 'object' || error === null) return undefined\n const direct = (error as { status?: number }).status\n if (typeof direct === 'number') return direct\n const nested = (error as { cause?: { response?: { status?: number } } }).cause?.response?.status\n return typeof nested === 'number' ? nested : undefined\n}\n\n/** Unified \"is this a 404?\" helper for API-backed providers. */\nexport function isNotFoundError(error: unknown): boolean {\n return extractHttpStatus(error) === 404\n}\n\n/**\n * Provider SDK messages are written for a human reading a terminal, not for an\n * agent deciding what to do next. Octokit appends a documentation URL to most\n * messages and embeds the raw JSON response body on validation failures.\n *\n * The vendor text still carries the only specifics we have (\"Reference already\n * exists\", which field failed), so it is kept as a parenthetical detail rather\n * than discarded — but the doc links are noise for an agent, and an embedded\n * body can be arbitrarily long, so URLs are stripped and the result is capped.\n */\nfunction sanitiseDetail(error: unknown): string {\n const raw = (error as { message?: string } | undefined)?.message ?? String(error)\n const withoutUrls = raw.replace(/\\s*-?\\s*https?:\\/\\/\\S+/g, '')\n const collapsed = withoutUrls.replace(/\\s+/g, ' ').trim()\n return collapsed.length > 200 ? `${collapsed.slice(0, 197)}…` : collapsed\n}\n\n/**\n * GitHub signals rate limiting with 403 plus an exhausted remaining-quota\n * header (secondary limits use 403 too), and with 429 on some endpoints.\n * Checking the header first keeps a genuine permission 403 from being\n * reported as a rate limit.\n */\nfunction isRateLimited(error: unknown, status: number): boolean {\n if (status === 429) return true\n if (status !== 403) return false\n const headers = (error as { response?: { headers?: Record<string, unknown> } }).response?.headers\n const remaining = headers?.['x-ratelimit-remaining']\n if (remaining !== undefined) return String(remaining) === '0'\n return /\\brate limit\\b/i.test((error as { message?: string }).message ?? '')\n}\n\nexport interface ProviderErrorInfo {\n error: string\n code: string\n agent_hint: string\n developer_action?: string\n}\n\n/**\n * Map a provider SDK rejection onto the same structured envelope the local git\n * paths already produce (`code` + `agent_hint` + `developer_action`). Returns\n * `undefined` for anything that is not a recognisable provider HTTP failure,\n * so local git errors and ordinary exceptions pass through untouched.\n */\nexport function mapProviderError(error: unknown): ProviderErrorInfo | undefined {\n const status = extractHttpStatus(error)\n if (status === undefined) return undefined\n\n const detail = sanitiseDetail(error)\n const withDetail = (summary: string): string => (detail ? `${summary} (${detail})` : summary)\n\n if (isRateLimited(error, status)) {\n return {\n error: withDetail('The git provider rejected the request because the API rate limit is exhausted.'),\n code: 'PROVIDER_RATE_LIMITED',\n agent_hint: 'Do not retry immediately — the limit is time-based. Stop the current operation and report it to the developer.',\n developer_action: 'Wait for the rate-limit window to reset, or use a token with a higher quota.',\n }\n }\n\n switch (status) {\n case 401:\n return {\n error: withDetail('The git provider rejected the credentials.'),\n code: 'PROVIDER_UNAUTHORIZED',\n agent_hint: 'The token is missing, malformed, or expired. Not retryable — ask the developer to fix the credentials.',\n developer_action: 'Check the provider token configured for this Contentrain server and reissue it if it has expired.',\n }\n case 403:\n return {\n error: withDetail('The git provider denied access to this resource.'),\n code: 'PROVIDER_FORBIDDEN',\n agent_hint: 'The credentials are valid but lack permission for this operation. Not retryable — report which operation was refused.',\n developer_action: 'Grant the token write access to the repository, or check whether branch protection or SSO authorisation is blocking it.',\n }\n case 404:\n return {\n error: withDetail('The git provider could not find the requested resource.'),\n code: 'PROVIDER_NOT_FOUND',\n agent_hint: 'The repository, branch, or path does not exist — or the token cannot see it, which also returns 404. Verify the target before retrying.',\n developer_action: 'Confirm the repository and branch names in the Contentrain config, and that the token can reach a private repository.',\n }\n case 409:\n return {\n error: withDetail('The git provider reported a conflict.'),\n code: 'PROVIDER_CONFLICT',\n agent_hint: 'The branch moved between read and write. Re-read the current state and rebuild the change before retrying once.',\n developer_action: 'Usually a concurrent write to the same branch; retrying the operation normally resolves it.',\n }\n case 422:\n return {\n error: withDetail('The git provider rejected the request as invalid.'),\n code: 'PROVIDER_VALIDATION_FAILED',\n agent_hint: 'Well-formed but semantically rejected (e.g. the ref already exists, or a name is invalid). Do not retry unchanged.',\n developer_action: 'Inspect the detail above — a stale branch left over from an earlier run is the common cause.',\n }\n default:\n if (status >= 500) {\n return {\n error: withDetail('The git provider is unavailable.'),\n code: 'PROVIDER_UNAVAILABLE',\n agent_hint: 'A server-side failure on the provider. Safe to retry once after a short pause; if it persists, stop and report it.',\n developer_action: 'Check the provider status page.',\n }\n }\n return {\n error: withDetail(`The git provider returned HTTP ${status}.`),\n code: 'PROVIDER_REQUEST_FAILED',\n agent_hint: 'An unmapped provider failure. Report the status and detail to the developer rather than retrying blindly.',\n }\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAsBA,SAAgB,kBAAkB,OAAoC;AACpE,KAAI,OAAO,UAAU,YAAY,UAAU,KAAM,QAAO,KAAA;CACxD,MAAM,SAAU,MAA8B;AAC9C,KAAI,OAAO,WAAW,SAAU,QAAO;CACvC,MAAM,SAAU,MAAyD,OAAO,UAAU;AAC1F,QAAO,OAAO,WAAW,WAAW,SAAS,KAAA;;;AAI/C,SAAgB,gBAAgB,OAAyB;AACvD,QAAO,kBAAkB,MAAM,KAAK;;;;;;;;;;;;AAatC,SAAS,eAAe,OAAwB;CAG9C,MAAM,aAFO,OAA4C,WAAW,OAAO,MAAM,EACzD,QAAQ,2BAA2B,GAAG,CAChC,QAAQ,QAAQ,IAAI,CAAC,MAAM;AACzD,QAAO,UAAU,SAAS,MAAM,GAAG,UAAU,MAAM,GAAG,IAAI,CAAC,KAAK;;;;;;;;AASlE,SAAS,cAAc,OAAgB,QAAyB;AAC9D,KAAI,WAAW,IAAK,QAAO;AAC3B,KAAI,WAAW,IAAK,QAAO;CAE3B,MAAM,aADW,MAA+D,UAAU,WAC9D;AAC5B,KAAI,cAAc,KAAA,EAAW,QAAO,OAAO,UAAU,KAAK;AAC1D,QAAO,kBAAkB,KAAM,MAA+B,WAAW,GAAG;;;;;;;;AAgB9E,SAAgB,iBAAiB,OAA+C;CAC9E,MAAM,SAAS,kBAAkB,MAAM;AACvC,KAAI,WAAW,KAAA,EAAW,QAAO,KAAA;CAEjC,MAAM,SAAS,eAAe,MAAM;CACpC,MAAM,cAAc,YAA6B,SAAS,GAAG,QAAQ,IAAI,OAAO,KAAK;AAErF,KAAI,cAAc,OAAO,OAAO,CAC9B,QAAO;EACL,OAAO,WAAW,iFAAiF;EACnG,MAAM;EACN,YAAY;EACZ,kBAAkB;EACnB;AAGH,SAAQ,QAAR;EACE,KAAK,IACH,QAAO;GACL,OAAO,WAAW,6CAA6C;GAC/D,MAAM;GACN,YAAY;GACZ,kBAAkB;GACnB;EACH,KAAK,IACH,QAAO;GACL,OAAO,WAAW,mDAAmD;GACrE,MAAM;GACN,YAAY;GACZ,kBAAkB;GACnB;EACH,KAAK,IACH,QAAO;GACL,OAAO,WAAW,0DAA0D;GAC5E,MAAM;GACN,YAAY;GACZ,kBAAkB;GACnB;EACH,KAAK,IACH,QAAO;GACL,OAAO,WAAW,wCAAwC;GAC1D,MAAM;GACN,YAAY;GACZ,kBAAkB;GACnB;EACH,KAAK,IACH,QAAO;GACL,OAAO,WAAW,oDAAoD;GACtE,MAAM;GACN,YAAY;GACZ,kBAAkB;GACnB;EACH;AACE,OAAI,UAAU,IACZ,QAAO;IACL,OAAO,WAAW,mCAAmC;IACrD,MAAM;IACN,YAAY;IACZ,kBAAkB;IACnB;AAEH,UAAO;IACL,OAAO,WAAW,kCAAkC,OAAO,GAAG;IAC9D,MAAM;IACN,YAAY;IACb"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as mapProviderError } from "./errors-
|
|
1
|
+
import { n as mapProviderError } from "./errors-B0NcpPIp.mjs";
|
|
2
2
|
//#region src/git/errors.ts
|
|
3
3
|
/**
|
|
4
4
|
* Error normalization for git-backed MCP operations.
|
|
@@ -52,4 +52,4 @@ function normalizeOperationError(err, stage) {
|
|
|
52
52
|
//#endregion
|
|
53
53
|
export { stripAnsi as n, normalizeOperationError as t };
|
|
54
54
|
|
|
55
|
-
//# sourceMappingURL=errors-
|
|
55
|
+
//# sourceMappingURL=errors-De8Noahk.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors-
|
|
1
|
+
{"version":3,"file":"errors-De8Noahk.mjs","names":[],"sources":["../src/git/errors.ts"],"sourcesContent":["/**\n * Error normalization for git-backed MCP operations.\n *\n * `simple-git` surfaces a failed `git commit`/`push`/`merge` as a `GitError`\n * whose `message` is the raw subprocess stderr — which, for hook tooling\n * (commitlint, husky, lefthook), is colorized multi-line text. Returning that\n * verbatim inside a JSON error produces an unreadable ANSI-escaped blob.\n *\n * This helper strips ANSI, preserves any structured fields the thrown error\n * already carries (`code`/`agent_hint`/`developer_action`), and detects git\n * hook rejections so the agent gets an actionable, structured envelope.\n */\n\nimport { mapProviderError } from '../providers/shared/errors.js'\n\n// Build the ANSI matcher without a literal control char so oxlint's\n// no-control-regex rule stays happy.\nconst ANSI_RE = new RegExp(`${String.fromCharCode(27)}\\\\[[0-9;]*m`, 'g')\n\nexport function stripAnsi(input: string): string {\n return input.replace(ANSI_RE, '')\n}\n\nexport interface NormalizedError {\n error: string\n stage?: string\n hook?: string\n code?: string\n agent_hint?: string\n developer_action?: string\n}\n\n/**\n * Normalize any thrown error into a structured, ANSI-free envelope for MCP\n * tool responses. `stage` labels where it happened (e.g. 'content_save').\n */\nexport function normalizeOperationError(err: unknown, stage?: string): NormalizedError {\n const e = err as { message?: string, code?: string, agent_hint?: string, developer_action?: string } | undefined\n const rawMessage = e?.message ?? String(err)\n const message = stripAnsi(rawMessage).trim()\n\n const out: NormalizedError = { error: message }\n if (stage) out.stage = stage\n if (e?.code) out.code = e.code\n if (e?.agent_hint) out.agent_hint = e.agent_hint\n if (e?.developer_action) out.developer_action = e.developer_action\n\n // Provider SDK rejections (Octokit, Gitbeaker) carry an HTTP status but no\n // structured fields, so without this they reached the client as a bare\n // vendor string — a documentation URL and no indication of whether the\n // operation is retryable. Mapping here rather than at each `throw` inside\n // the providers means every provider error is covered, including ones added\n // later. An error that already carries a Contentrain code keeps it.\n if (!out.code) {\n const mapped = mapProviderError(err)\n if (mapped) {\n out.error = mapped.error\n out.code = mapped.code\n out.agent_hint = mapped.agent_hint\n if (mapped.developer_action) out.developer_action = mapped.developer_action\n return out\n }\n }\n\n // Detect a git hook rejection (only when the error isn't already a\n // structured Contentrain error with its own code).\n if (!out.code) {\n const hookName = /\\b(commit-msg|pre-commit|prepare-commit-msg|pre-push)\\b/i.exec(message)?.[1]\n const hookTool = /\\b(commitlint|husky|lefthook)\\b/i.exec(message)?.[1]\n if (hookName || hookTool) {\n out.stage = out.stage ?? 'git-commit'\n out.hook = hookName ?? hookTool\n out.agent_hint = out.agent_hint\n ?? 'A git hook rejected the Contentrain commit. Contentrain commits with --no-verify, but a wrapping CLI or server-side hook may still run. Ask the developer to allow machine-generated \"[contentrain]\" commits or to exclude cr/* branches from the hook.'\n }\n }\n\n return out\n}\n"],"mappings":";;;;;;;;;;;;;;AAiBA,MAAM,UAAU,IAAI,OAAO,GAAG,OAAO,aAAa,GAAG,CAAC,cAAc,IAAI;AAExE,SAAgB,UAAU,OAAuB;AAC/C,QAAO,MAAM,QAAQ,SAAS,GAAG;;;;;;AAgBnC,SAAgB,wBAAwB,KAAc,OAAiC;CACrF,MAAM,IAAI;CAEV,MAAM,UAAU,UADG,GAAG,WAAW,OAAO,IAAI,CACP,CAAC,MAAM;CAE5C,MAAM,MAAuB,EAAE,OAAO,SAAS;AAC/C,KAAI,MAAO,KAAI,QAAQ;AACvB,KAAI,GAAG,KAAM,KAAI,OAAO,EAAE;AAC1B,KAAI,GAAG,WAAY,KAAI,aAAa,EAAE;AACtC,KAAI,GAAG,iBAAkB,KAAI,mBAAmB,EAAE;AAQlD,KAAI,CAAC,IAAI,MAAM;EACb,MAAM,SAAS,iBAAiB,IAAI;AACpC,MAAI,QAAQ;AACV,OAAI,QAAQ,OAAO;AACnB,OAAI,OAAO,OAAO;AAClB,OAAI,aAAa,OAAO;AACxB,OAAI,OAAO,iBAAkB,KAAI,mBAAmB,OAAO;AAC3D,UAAO;;;AAMX,KAAI,CAAC,IAAI,MAAM;EACb,MAAM,WAAW,2DAA2D,KAAK,QAAQ,GAAG;EAC5F,MAAM,WAAW,mCAAmC,KAAK,QAAQ,GAAG;AACpE,MAAI,YAAY,UAAU;AACxB,OAAI,QAAQ,IAAI,SAAS;AACzB,OAAI,OAAO,YAAY;AACvB,OAAI,aAAa,IAAI,cAChB;;;AAIT,QAAO"}
|