@contentrain/mcp 1.2.0 → 1.3.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 +93 -25
- package/dist/annotations-Clhbw2lh.mjs +126 -0
- package/dist/annotations-Clhbw2lh.mjs.map +1 -0
- package/dist/{apply-manager-BkPMfpsN.mjs → apply-manager-D5ym5Jba.mjs} +7 -7
- package/dist/{apply-manager-BkPMfpsN.mjs.map → apply-manager-D5ym5Jba.mjs.map} +1 -1
- package/dist/{astro-parser-BJ-54JGe.mjs → astro-parser-B-nLl0zE.mjs} +3 -3
- package/dist/{astro-parser-BJ-54JGe.mjs.map → astro-parser-B-nLl0zE.mjs.map} +1 -1
- package/dist/{branch-lifecycle-wGJg25LT.mjs → branch-lifecycle-CFVpTpNT.mjs} +33 -3
- package/dist/{branch-lifecycle-wGJg25LT.mjs.map → branch-lifecycle-CFVpTpNT.mjs.map} +1 -1
- package/dist/{compiler-rIC4ETHv.mjs → compiler-k4yttlzM.mjs} +2 -2
- package/dist/{compiler-rIC4ETHv.mjs.map → compiler-k4yttlzM.mjs.map} +1 -1
- package/dist/{compiler-sfc.cjs-BbD04rn_.mjs → compiler-sfc.cjs-D0d_OicF.mjs} +2 -2
- package/dist/{compiler-sfc.cjs-BbD04rn_.mjs.map → compiler-sfc.cjs-D0d_OicF.mjs.map} +1 -1
- package/dist/config-BIl5Ybj0.mjs +40 -0
- package/dist/config-BIl5Ybj0.mjs.map +1 -0
- package/dist/content-manager-DqDLSNQ-.d.mts +63 -0
- package/dist/content-manager-DqDLSNQ-.d.mts.map +1 -0
- package/dist/context-J0OU9Nfp.mjs +102 -0
- package/dist/context-J0OU9Nfp.mjs.map +1 -0
- package/dist/contracts-DfL0BfrD.mjs +2 -0
- package/dist/core/apply-manager.mjs +10 -10
- package/dist/core/config.d.mts +13 -0
- package/dist/core/config.d.mts.map +1 -1
- package/dist/core/config.mjs +2 -3
- package/dist/core/content-manager.d.mts +2 -45
- package/dist/core/content-manager.mjs +3 -4
- package/dist/core/context.d.mts +32 -2
- package/dist/core/context.d.mts.map +1 -1
- package/dist/core/context.mjs +7 -7
- package/dist/core/contracts/index.d.mts +2 -0
- package/dist/core/contracts/index.mjs +2 -0
- package/dist/core/doctor.d.mts +76 -0
- package/dist/core/doctor.d.mts.map +1 -0
- package/dist/core/doctor.mjs +8 -0
- package/dist/core/graph-builder.mjs +3 -4
- package/dist/core/meta-manager.mjs +2 -3
- package/dist/core/model-manager.d.mts +34 -1
- package/dist/core/model-manager.d.mts.map +1 -1
- package/dist/core/model-manager.mjs +3 -4
- package/dist/core/ops/index.d.mts +137 -0
- package/dist/core/ops/index.d.mts.map +1 -0
- package/dist/core/ops/index.mjs +4 -0
- package/dist/core/overlay-reader.d.mts +42 -0
- package/dist/core/overlay-reader.d.mts.map +1 -0
- package/dist/core/overlay-reader.mjs +2 -0
- package/dist/core/scan-config.mjs +2 -3
- package/dist/core/scanner.mjs +4 -5
- package/dist/core/validator/index.d.mts +129 -0
- package/dist/core/validator/index.d.mts.map +1 -0
- package/dist/core/validator/index.mjs +6 -0
- package/dist/{detect-DinOQAmK.mjs → detect-DL9mFmaS.mjs} +2 -2
- package/dist/{detect-DinOQAmK.mjs.map → detect-DL9mFmaS.mjs.map} +1 -1
- package/dist/doctor-CgBoT6h1.mjs +338 -0
- package/dist/doctor-CgBoT6h1.mjs.map +1 -0
- package/dist/{fs-BnIQ4dKj.mjs → fs-DLbVB-Ek.mjs} +2 -2
- package/dist/fs-DLbVB-Ek.mjs.map +1 -0
- package/dist/git/branch-lifecycle.d.mts +30 -1
- package/dist/git/branch-lifecycle.d.mts.map +1 -1
- package/dist/git/branch-lifecycle.mjs +4 -5
- package/dist/git/transaction.d.mts +2 -2
- package/dist/git/transaction.d.mts.map +1 -1
- package/dist/git/transaction.mjs +8 -8
- package/dist/{graph-builder-tM7zj-SX.mjs → graph-builder-DXv2JEp-.mjs} +3 -3
- package/dist/{graph-builder-tM7zj-SX.mjs.map → graph-builder-DXv2JEp-.mjs.map} +1 -1
- package/dist/{id-DV_T9Ic8.mjs → id-CIO9s0x_.mjs} +1 -1
- package/dist/{id-DV_T9Ic8.mjs.map → id-CIO9s0x_.mjs.map} +1 -1
- package/dist/index-D0wQJSgs.d.mts +2 -0
- package/dist/index.mjs +22 -17
- package/dist/index.mjs.map +1 -1
- package/dist/local-DvP4Y4T-.mjs +182 -0
- package/dist/local-DvP4Y4T-.mjs.map +1 -0
- package/dist/{meta-manager--0IwAPCl.mjs → meta-manager-C900rEyG.mjs} +2 -2
- package/dist/{meta-manager--0IwAPCl.mjs.map → meta-manager-C900rEyG.mjs.map} +1 -1
- package/dist/{model-manager-DW_fOGRM.mjs → model-manager-BhLsUgaB.mjs} +241 -49
- package/dist/model-manager-BhLsUgaB.mjs.map +1 -0
- package/dist/{node-Cbq5eR5i.mjs → node-DN9hZeil.mjs} +1 -1
- package/dist/{node-Cbq5eR5i.mjs.map → node-DN9hZeil.mjs.map} +1 -1
- package/dist/ops-DjEWi5Wa.mjs +486 -0
- package/dist/ops-DjEWi5Wa.mjs.map +1 -0
- package/dist/overlay-reader-DOdyDRIv.mjs +74 -0
- package/dist/overlay-reader-DOdyDRIv.mjs.map +1 -0
- package/dist/paths-C3xNyc7z.mjs +52 -0
- package/dist/paths-C3xNyc7z.mjs.map +1 -0
- package/dist/paths-CYl5vCBD.mjs +54 -0
- package/dist/paths-CYl5vCBD.mjs.map +1 -0
- package/dist/providers/github/index.d.mts +144 -0
- package/dist/providers/github/index.d.mts.map +1 -0
- package/dist/providers/github/index.mjs +414 -0
- package/dist/providers/github/index.mjs.map +1 -0
- package/dist/providers/gitlab/index.d.mts +158 -0
- package/dist/providers/gitlab/index.d.mts.map +1 -0
- package/dist/providers/gitlab/index.mjs +361 -0
- package/dist/providers/gitlab/index.mjs.map +1 -0
- package/dist/providers/local/index.d.mts +106 -0
- package/dist/providers/local/index.d.mts.map +1 -0
- package/dist/providers/local/index.mjs +12 -0
- package/dist/{scan-config-QgnJ1-52.mjs → scan-config-jC6XmiqF.mjs} +2 -2
- package/dist/{scan-config-QgnJ1-52.mjs.map → scan-config-jC6XmiqF.mjs.map} +1 -1
- package/dist/{scanner-CQOxARUU.mjs → scanner-DKMwc03k.mjs} +26 -8
- package/dist/scanner-DKMwc03k.mjs.map +1 -0
- package/dist/serialization-B1CEzR4H.mjs +2 -0
- package/dist/server/http/index.d.mts +65 -0
- package/dist/server/http/index.d.mts.map +1 -0
- package/dist/server/http/index.mjs +137 -0
- package/dist/server/http/index.mjs.map +1 -0
- package/dist/{server-DfWRpzdP.mjs → server-B1OJrUQ9.mjs} +486 -355
- package/dist/server-B1OJrUQ9.mjs.map +1 -0
- package/dist/server-WqGx6ait.d.mts +50 -0
- package/dist/server-WqGx6ait.d.mts.map +1 -0
- package/dist/server.d.mts +2 -7
- package/dist/server.mjs +22 -17
- package/dist/{source-map-generator-DpO-ejrt.mjs → source-map-generator-BWDva49I.mjs} +1 -1
- package/dist/{source-map-generator-DpO-ejrt.mjs.map → source-map-generator-BWDva49I.mjs.map} +1 -1
- package/dist/{svelte-parser-1bBcMx3T.mjs → svelte-parser-Dr9mmZuk.mjs} +3 -3
- package/dist/{svelte-parser-1bBcMx3T.mjs.map → svelte-parser-Dr9mmZuk.mjs.map} +1 -1
- package/dist/templates/index.mjs +1 -1
- package/dist/{templates-bRPzYq4M.mjs → templates-VrLVqfe5.mjs} +1 -1
- package/dist/{templates-bRPzYq4M.mjs.map → templates-VrLVqfe5.mjs.map} +1 -1
- package/dist/tools/annotations.d.mts +24 -0
- package/dist/tools/annotations.d.mts.map +1 -0
- package/dist/tools/annotations.mjs +2 -0
- package/dist/{transaction-C_Pvux78.mjs → transaction-DgkNgWSI.mjs} +61 -32
- package/dist/transaction-DgkNgWSI.mjs.map +1 -0
- package/dist/tsx-parser-BOMZE_yQ.mjs +2 -0
- package/dist/{tsx-parser-BWiMGAIR.mjs → tsx-parser-CQc6nr-o.mjs} +1 -1
- package/dist/{tsx-parser-BWiMGAIR.mjs.map → tsx-parser-CQc6nr-o.mjs.map} +1 -1
- package/dist/util/detect.mjs +2 -3
- package/dist/util/fs.mjs +1 -2
- package/dist/util/id.mjs +1 -1
- package/dist/validator-B0nNRFeU.mjs +906 -0
- package/dist/validator-B0nNRFeU.mjs.map +1 -0
- package/dist/{vue-parser-DTPTN3Yb.mjs → vue-parser-COgjArqv.mjs} +3 -3
- package/dist/{vue-parser-DTPTN3Yb.mjs.map → vue-parser-COgjArqv.mjs.map} +1 -1
- package/package.json +75 -21
- package/dist/config-B5kc2oeV.mjs +0 -27
- package/dist/config-B5kc2oeV.mjs.map +0 -1
- package/dist/context-DIDt_TxX.mjs +0 -46
- package/dist/context-DIDt_TxX.mjs.map +0 -1
- package/dist/core/content-manager.d.mts.map +0 -1
- package/dist/core/validator.d.mts +0 -23
- package/dist/core/validator.d.mts.map +0 -1
- package/dist/core/validator.mjs +0 -7
- package/dist/fs-BnIQ4dKj.mjs.map +0 -1
- package/dist/model-manager-DW_fOGRM.mjs.map +0 -1
- package/dist/scanner-CQOxARUU.mjs.map +0 -1
- package/dist/serializer-CnHTtmc1.mjs +0 -2
- package/dist/server-DfWRpzdP.mjs.map +0 -1
- package/dist/server.d.mts.map +0 -1
- package/dist/transaction-C_Pvux78.mjs.map +0 -1
- package/dist/tsx-parser-BFLv292Y.mjs +0 -2
- package/dist/util/serializer.d.mts +0 -2
- package/dist/util/serializer.mjs +0 -2
- package/dist/validator-SQYUV2JP.mjs +0 -581
- package/dist/validator-SQYUV2JP.mjs.map +0 -1
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
//#region src/providers/shared/errors.ts
|
|
2
|
+
/**
|
|
3
|
+
* Unified "is this a 404?" helper for API-backed providers.
|
|
4
|
+
*
|
|
5
|
+
* Two common error shapes in the provider SDKs we use:
|
|
6
|
+
*
|
|
7
|
+
* - **Octokit** (`@octokit/rest`) — rejects with an `Error` that has a
|
|
8
|
+
* top-level `.status` number set to the HTTP status code.
|
|
9
|
+
* - **Gitbeaker** (`@gitbeaker/rest`) — rejects with a plain `Error` whose
|
|
10
|
+
* `.cause` includes `{ response: { status } }`.
|
|
11
|
+
*
|
|
12
|
+
* Both forms converge on `404` meaning "resource missing", so we check
|
|
13
|
+
* both shapes strictly. We deliberately do NOT fall back to substring
|
|
14
|
+
* matching on the error message — that leniency can silently mask other
|
|
15
|
+
* 404-like errors (forbidden repo, deleted project, rate limits) and
|
|
16
|
+
* produce the wrong answer. If either SDK ever stops populating the
|
|
17
|
+
* status field on a legitimate 404, the regression will surface in tests
|
|
18
|
+
* rather than being papered over at the reader layer.
|
|
19
|
+
*/
|
|
20
|
+
function isNotFoundError(error) {
|
|
21
|
+
if (typeof error !== "object" || error === null) return false;
|
|
22
|
+
if (error.status === 404) return true;
|
|
23
|
+
return error.cause?.response?.status === 404;
|
|
24
|
+
}
|
|
25
|
+
//#endregion
|
|
26
|
+
//#region src/providers/shared/paths.ts
|
|
27
|
+
/**
|
|
28
|
+
* Normalise an optional contentRoot — strip leading/trailing slashes,
|
|
29
|
+
* treat `''`, `/` and `undefined` as "no prefix". Used by API-backed
|
|
30
|
+
* providers (GitHub, GitLab, future Bitbucket) to anchor content-relative
|
|
31
|
+
* paths against a repo subdirectory when Contentrain lives under a
|
|
32
|
+
* monorepo path like `apps/web/.contentrain/`.
|
|
33
|
+
*/
|
|
34
|
+
function normaliseContentRoot(raw) {
|
|
35
|
+
if (!raw || raw === "/" || raw === "") return "";
|
|
36
|
+
return raw.replace(/^\/+|\/+$/g, "");
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Resolve a content-root-relative path to a repo-relative path. The result
|
|
40
|
+
* always uses forward slashes and has no leading slash — the form every
|
|
41
|
+
* REST git API consumes for `file_path` / `path` query parameters and the
|
|
42
|
+
* Git Data API tree entries.
|
|
43
|
+
*/
|
|
44
|
+
function resolveRepoPath(contentRoot, relativePath) {
|
|
45
|
+
const prefix = normaliseContentRoot(contentRoot);
|
|
46
|
+
const cleanPath = relativePath.replace(/^\/+/, "");
|
|
47
|
+
return prefix ? `${prefix}/${cleanPath}` : cleanPath;
|
|
48
|
+
}
|
|
49
|
+
//#endregion
|
|
50
|
+
export { isNotFoundError as n, resolveRepoPath as t };
|
|
51
|
+
|
|
52
|
+
//# sourceMappingURL=paths-C3xNyc7z.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"paths-C3xNyc7z.mjs","names":[],"sources":["../src/providers/shared/errors.ts","../src/providers/shared/paths.ts"],"sourcesContent":["/**\n * Unified \"is this a 404?\" helper for API-backed providers.\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 * Both forms converge on `404` meaning \"resource missing\", so we check\n * both shapes strictly. We deliberately do NOT fall back to substring\n * matching on the error message — that leniency can silently mask other\n * 404-like errors (forbidden repo, deleted project, rate limits) and\n * produce the wrong answer. If either SDK ever stops populating the\n * status field on a legitimate 404, the regression will surface in tests\n * rather than being papered over at the reader layer.\n */\nexport function isNotFoundError(error: unknown): boolean {\n if (typeof error !== 'object' || error === null) return false\n const direct = (error as { status?: number }).status\n if (direct === 404) return true\n const nested = (error as { cause?: { response?: { status?: number } } }).cause?.response?.status\n return nested === 404\n}\n","/**\n * Normalise an optional contentRoot — strip leading/trailing slashes,\n * treat `''`, `/` and `undefined` as \"no prefix\". Used by API-backed\n * providers (GitHub, GitLab, future Bitbucket) to anchor content-relative\n * paths against a repo subdirectory when Contentrain lives under a\n * monorepo path like `apps/web/.contentrain/`.\n */\nexport function normaliseContentRoot(raw?: string): string {\n if (!raw || raw === '/' || raw === '') return ''\n return raw.replace(/^\\/+|\\/+$/g, '')\n}\n\n/**\n * Resolve a content-root-relative path to a repo-relative path. The result\n * always uses forward slashes and has no leading slash — the form every\n * REST git API consumes for `file_path` / `path` query parameters and the\n * Git Data API tree entries.\n */\nexport function resolveRepoPath(contentRoot: string | undefined, relativePath: string): string {\n const prefix = normaliseContentRoot(contentRoot)\n const cleanPath = relativePath.replace(/^\\/+/, '')\n return prefix ? `${prefix}/${cleanPath}` : cleanPath\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAkBA,SAAgB,gBAAgB,OAAyB;AACvD,KAAI,OAAO,UAAU,YAAY,UAAU,KAAM,QAAO;AAExD,KADgB,MAA8B,WAC/B,IAAK,QAAO;AAE3B,QADgB,MAAyD,OAAO,UAAU,WACxE;;;;;;;;;;;AChBpB,SAAgB,qBAAqB,KAAsB;AACzD,KAAI,CAAC,OAAO,QAAQ,OAAO,QAAQ,GAAI,QAAO;AAC9C,QAAO,IAAI,QAAQ,cAAc,GAAG;;;;;;;;AAStC,SAAgB,gBAAgB,aAAiC,cAA8B;CAC7F,MAAM,SAAS,qBAAqB,YAAY;CAChD,MAAM,YAAY,aAAa,QAAQ,QAAQ,GAAG;AAClD,QAAO,SAAS,GAAG,OAAO,GAAG,cAAc"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
//#region src/core/ops/paths.ts
|
|
2
|
+
/**
|
|
3
|
+
* Content-root-relative path helpers for plan-API consumers.
|
|
4
|
+
*
|
|
5
|
+
* These mirror the logic inside `core/content-manager.ts:resolveJsonFilePath`
|
|
6
|
+
* but return forward-slash relative strings suitable for `FileChange.path`.
|
|
7
|
+
* Providers are responsible for resolving these against their backing store
|
|
8
|
+
* (worktree on disk for LocalProvider, git tree path for GitHubProvider, ...).
|
|
9
|
+
*
|
|
10
|
+
* Kept inline to `core/ops/` for now; when Faz 2 finishes migrating every op
|
|
11
|
+
* the legacy absolute-path helpers in `core/content-manager.ts` and
|
|
12
|
+
* `core/meta-manager.ts` will call into these (or be replaced entirely).
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Content-root-relative content directory for a model (forward-slash paths).
|
|
16
|
+
* Mirrors `resolveContentDir` with an empty `projectRoot`, so reader-backed
|
|
17
|
+
* callers and plan-API consumers share the same path resolution.
|
|
18
|
+
*/
|
|
19
|
+
function contentDirPath(model) {
|
|
20
|
+
if (model.content_path) return model.content_path;
|
|
21
|
+
return `.contentrain/content/${model.domain}/${model.id}`;
|
|
22
|
+
}
|
|
23
|
+
function contentDir(model) {
|
|
24
|
+
return contentDirPath(model);
|
|
25
|
+
}
|
|
26
|
+
function contentFilePath(model, locale) {
|
|
27
|
+
const dir = contentDir(model);
|
|
28
|
+
if (!model.i18n) return `${dir}/data.json`;
|
|
29
|
+
switch (model.locale_strategy ?? "file") {
|
|
30
|
+
case "suffix": return `${dir}/${model.id}.${locale}.json`;
|
|
31
|
+
case "directory": return `${dir}/${locale}/${model.id}.json`;
|
|
32
|
+
case "none": return `${dir}/${model.id}.json`;
|
|
33
|
+
default: return `${dir}/${locale}.json`;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function documentFilePath(model, locale, slug) {
|
|
37
|
+
const dir = contentDir(model);
|
|
38
|
+
if (!model.i18n) return `${dir}/${slug}.md`;
|
|
39
|
+
switch (model.locale_strategy ?? "file") {
|
|
40
|
+
case "suffix": return `${dir}/${slug}.${locale}.md`;
|
|
41
|
+
case "directory": return `${dir}/${locale}/${slug}.md`;
|
|
42
|
+
case "none": return `${dir}/${slug}.md`;
|
|
43
|
+
default: return `${dir}/${slug}/${locale}.md`;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function metaFilePath(model, locale, slug) {
|
|
47
|
+
const base = `.contentrain/meta/${model.id}`;
|
|
48
|
+
if (model.kind === "document" && slug) return `${base}/${slug}/${locale}.json`;
|
|
49
|
+
return `${base}/${locale}.json`;
|
|
50
|
+
}
|
|
51
|
+
//#endregion
|
|
52
|
+
export { metaFilePath as i, contentFilePath as n, documentFilePath as r, contentDirPath as t };
|
|
53
|
+
|
|
54
|
+
//# sourceMappingURL=paths-CYl5vCBD.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"paths-CYl5vCBD.mjs","names":[],"sources":["../src/core/ops/paths.ts"],"sourcesContent":["import type { ModelDefinition } from '@contentrain/types'\n\n/**\n * Content-root-relative path helpers for plan-API consumers.\n *\n * These mirror the logic inside `core/content-manager.ts:resolveJsonFilePath`\n * but return forward-slash relative strings suitable for `FileChange.path`.\n * Providers are responsible for resolving these against their backing store\n * (worktree on disk for LocalProvider, git tree path for GitHubProvider, ...).\n *\n * Kept inline to `core/ops/` for now; when Faz 2 finishes migrating every op\n * the legacy absolute-path helpers in `core/content-manager.ts` and\n * `core/meta-manager.ts` will call into these (or be replaced entirely).\n */\n\n/**\n * Content-root-relative content directory for a model (forward-slash paths).\n * Mirrors `resolveContentDir` with an empty `projectRoot`, so reader-backed\n * callers and plan-API consumers share the same path resolution.\n */\nexport function contentDirPath(\n model: Pick<ModelDefinition, 'domain' | 'id' | 'content_path'>,\n): string {\n if (model.content_path) return model.content_path\n return `.contentrain/content/${model.domain}/${model.id}`\n}\n\nfunction contentDir(model: Pick<ModelDefinition, 'domain' | 'id' | 'content_path'>): string {\n return contentDirPath(model)\n}\n\nexport function contentFilePath(\n model: Pick<ModelDefinition, 'id' | 'kind' | 'domain' | 'i18n' | 'content_path' | 'locale_strategy'>,\n locale: string,\n): string {\n const dir = contentDir(model)\n if (!model.i18n) return `${dir}/data.json`\n const strategy = model.locale_strategy ?? 'file'\n switch (strategy) {\n case 'suffix': return `${dir}/${model.id}.${locale}.json`\n case 'directory': return `${dir}/${locale}/${model.id}.json`\n case 'none': return `${dir}/${model.id}.json`\n default: return `${dir}/${locale}.json`\n }\n}\n\nexport function documentFilePath(\n model: Pick<ModelDefinition, 'id' | 'domain' | 'i18n' | 'content_path' | 'locale_strategy'>,\n locale: string,\n slug: string,\n): string {\n const dir = contentDir(model)\n if (!model.i18n) return `${dir}/${slug}.md`\n const strategy = model.locale_strategy ?? 'file'\n switch (strategy) {\n case 'suffix': return `${dir}/${slug}.${locale}.md`\n case 'directory': return `${dir}/${locale}/${slug}.md`\n case 'none': return `${dir}/${slug}.md`\n default: return `${dir}/${slug}/${locale}.md`\n }\n}\n\nexport function metaFilePath(\n model: Pick<ModelDefinition, 'id' | 'kind'>,\n locale: string,\n slug?: string,\n): string {\n const base = `.contentrain/meta/${model.id}`\n if (model.kind === 'document' && slug) {\n return `${base}/${slug}/${locale}.json`\n }\n return `${base}/${locale}.json`\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAoBA,SAAgB,eACd,OACQ;AACR,KAAI,MAAM,aAAc,QAAO,MAAM;AACrC,QAAO,wBAAwB,MAAM,OAAO,GAAG,MAAM;;AAGvD,SAAS,WAAW,OAAwE;AAC1F,QAAO,eAAe,MAAM;;AAG9B,SAAgB,gBACd,OACA,QACQ;CACR,MAAM,MAAM,WAAW,MAAM;AAC7B,KAAI,CAAC,MAAM,KAAM,QAAO,GAAG,IAAI;AAE/B,SADiB,MAAM,mBAAmB,QAC1C;EACE,KAAK,SAAU,QAAO,GAAG,IAAI,GAAG,MAAM,GAAG,GAAG,OAAO;EACnD,KAAK,YAAa,QAAO,GAAG,IAAI,GAAG,OAAO,GAAG,MAAM,GAAG;EACtD,KAAK,OAAQ,QAAO,GAAG,IAAI,GAAG,MAAM,GAAG;EACvC,QAAS,QAAO,GAAG,IAAI,GAAG,OAAO;;;AAIrC,SAAgB,iBACd,OACA,QACA,MACQ;CACR,MAAM,MAAM,WAAW,MAAM;AAC7B,KAAI,CAAC,MAAM,KAAM,QAAO,GAAG,IAAI,GAAG,KAAK;AAEvC,SADiB,MAAM,mBAAmB,QAC1C;EACE,KAAK,SAAU,QAAO,GAAG,IAAI,GAAG,KAAK,GAAG,OAAO;EAC/C,KAAK,YAAa,QAAO,GAAG,IAAI,GAAG,OAAO,GAAG,KAAK;EAClD,KAAK,OAAQ,QAAO,GAAG,IAAI,GAAG,KAAK;EACnC,QAAS,QAAO,GAAG,IAAI,GAAG,KAAK,GAAG,OAAO;;;AAI7C,SAAgB,aACd,OACA,QACA,MACQ;CACR,MAAM,OAAO,qBAAqB,MAAM;AACxC,KAAI,MAAM,SAAS,cAAc,KAC/B,QAAO,GAAG,KAAK,GAAG,KAAK,GAAG,OAAO;AAEnC,QAAO,GAAG,KAAK,GAAG,OAAO"}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { c as MergeResult, d as RepoReader, i as CommitAuthor, l as ProviderCapabilities, n as Branch, o as FileDiff, r as Commit, t as ApplyPlanInput, u as RepoProvider } from "../../index-D0wQJSgs.mjs";
|
|
2
|
+
import { Octokit } from "@octokit/rest";
|
|
3
|
+
|
|
4
|
+
//#region src/providers/github/capabilities.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Capability set for GitHubProvider.
|
|
7
|
+
*
|
|
8
|
+
* GitHub over the Git Data API has no working tree, so local worktree
|
|
9
|
+
* features, source-file access and AST scans are unavailable. Push /
|
|
10
|
+
* PR operations are free because every commit goes straight to the
|
|
11
|
+
* remote. Branch protection detection uses the Repos API.
|
|
12
|
+
*
|
|
13
|
+
* Tools that require `astScan`, `sourceRead` or `sourceWrite` must
|
|
14
|
+
* gracefully reject (with `capability_required`) when running against
|
|
15
|
+
* a GitHubProvider — this is the mechanism behind phase 6's normalize
|
|
16
|
+
* capability-gate.
|
|
17
|
+
*/
|
|
18
|
+
declare const GITHUB_CAPABILITIES: ProviderCapabilities;
|
|
19
|
+
//#endregion
|
|
20
|
+
//#region src/providers/github/client.d.ts
|
|
21
|
+
/**
|
|
22
|
+
* GitHubClient — a typed alias for an `@octokit/rest` instance.
|
|
23
|
+
*
|
|
24
|
+
* We rely on the peer-installed `@octokit/rest` for its types only; the
|
|
25
|
+
* provider code never imports Octokit eagerly. Consumers install it
|
|
26
|
+
* themselves via the optional peer dependency; the factory in
|
|
27
|
+
* `factory.ts` `await import()`s Octokit at runtime so the rest of MCP
|
|
28
|
+
* stays usable without it.
|
|
29
|
+
*/
|
|
30
|
+
type GitHubClient = Octokit;
|
|
31
|
+
//#endregion
|
|
32
|
+
//#region src/providers/github/types.d.ts
|
|
33
|
+
/**
|
|
34
|
+
* A reference to a GitHub repository.
|
|
35
|
+
*
|
|
36
|
+
* `contentRoot` is the repo-relative directory prefix where Contentrain
|
|
37
|
+
* content lives. For a flat content repo it stays `''`; for a monorepo
|
|
38
|
+
* where Contentrain is embedded (e.g. `apps/web/.contentrain/`) it holds
|
|
39
|
+
* that prefix. All reader/writer paths are joined against it.
|
|
40
|
+
*/
|
|
41
|
+
interface RepoRef {
|
|
42
|
+
owner: string;
|
|
43
|
+
name: string;
|
|
44
|
+
contentRoot?: string;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Authentication options for the GitHub provider.
|
|
48
|
+
*
|
|
49
|
+
* - `pat` — a personal access token or fine-grained PAT. Simplest for
|
|
50
|
+
* self-hosted MCP or CI runners. Phase 5.1 ships with this mode only.
|
|
51
|
+
* - `app` — GitHub App installation auth (JWT + installation token
|
|
52
|
+
* exchange). Planned for Phase 5.2; see `.internal/refactor/
|
|
53
|
+
* 02-studio-handoff.md` S6 for how Studio's hosted MCP plugs in.
|
|
54
|
+
*/
|
|
55
|
+
type GitHubAuth = {
|
|
56
|
+
type: 'pat';
|
|
57
|
+
token: string;
|
|
58
|
+
} | {
|
|
59
|
+
type: 'app';
|
|
60
|
+
appId: number;
|
|
61
|
+
privateKey: string;
|
|
62
|
+
installationId: number;
|
|
63
|
+
};
|
|
64
|
+
/** Default author used when a call does not provide one. */
|
|
65
|
+
declare const DEFAULT_GITHUB_AUTHOR: CommitAuthor;
|
|
66
|
+
//#endregion
|
|
67
|
+
//#region src/providers/github/provider.d.ts
|
|
68
|
+
/**
|
|
69
|
+
* GitHubProvider — `RepoProvider` backed by the Octokit-driven GitHub
|
|
70
|
+
* REST + Git Data APIs.
|
|
71
|
+
*
|
|
72
|
+
* The provider is transport-agnostic; it only talks to an `Octokit`
|
|
73
|
+
* instance passed into the constructor. The `createGitHubProvider`
|
|
74
|
+
* helper in `factory.ts` wraps the dynamic import so consumers never
|
|
75
|
+
* have to touch Octokit directly.
|
|
76
|
+
*/
|
|
77
|
+
declare class GitHubProvider implements RepoProvider {
|
|
78
|
+
private readonly client;
|
|
79
|
+
readonly repo: RepoRef;
|
|
80
|
+
readonly capabilities: ProviderCapabilities;
|
|
81
|
+
private readonly reader;
|
|
82
|
+
constructor(client: GitHubClient, repo: RepoRef);
|
|
83
|
+
readFile(path: string, ref?: string): Promise<string>;
|
|
84
|
+
listDirectory(path: string, ref?: string): Promise<string[]>;
|
|
85
|
+
fileExists(path: string, ref?: string): Promise<boolean>;
|
|
86
|
+
applyPlan(input: ApplyPlanInput): Promise<Commit>;
|
|
87
|
+
listBranches(prefix?: string): Promise<Branch[]>;
|
|
88
|
+
createBranch(name: string, fromRef?: string): Promise<void>;
|
|
89
|
+
deleteBranch(name: string): Promise<void>;
|
|
90
|
+
getBranchDiff(branch: string, base?: string): Promise<FileDiff[]>;
|
|
91
|
+
mergeBranch(branch: string, into: string): Promise<MergeResult>;
|
|
92
|
+
isMerged(branch: string, into?: string): Promise<boolean>;
|
|
93
|
+
getDefaultBranch(): Promise<string>;
|
|
94
|
+
}
|
|
95
|
+
//#endregion
|
|
96
|
+
//#region src/providers/github/factory.d.ts
|
|
97
|
+
/**
|
|
98
|
+
* Create an Octokit-backed `GitHubClient` from an auth configuration.
|
|
99
|
+
*
|
|
100
|
+
* The `@octokit/rest` module is imported dynamically so it stays a pure
|
|
101
|
+
* optional peer dependency — self-hosted MCP on stdio can run without
|
|
102
|
+
* it. If the module is not installed, the import throws with a helpful
|
|
103
|
+
* hint pointing the operator at the peer dependency.
|
|
104
|
+
*
|
|
105
|
+
* Phase 5.1 ships with PAT auth only. App-based auth (JWT + installation
|
|
106
|
+
* token) lands in phase 5.2 together with the HTTP transport.
|
|
107
|
+
*/
|
|
108
|
+
declare function createGitHubClient(auth: GitHubAuth): Promise<GitHubClient>;
|
|
109
|
+
/**
|
|
110
|
+
* Factory for the full provider — instantiates an Octokit client and
|
|
111
|
+
* wraps it in a `GitHubProvider`. Consumers who already hold an Octokit
|
|
112
|
+
* instance (HTTP server injecting shared clients, tests, etc.) should
|
|
113
|
+
* instantiate `GitHubProvider` directly instead.
|
|
114
|
+
*/
|
|
115
|
+
declare function createGitHubProvider(opts: {
|
|
116
|
+
auth: GitHubAuth;
|
|
117
|
+
repo: RepoRef;
|
|
118
|
+
}): Promise<GitHubProvider>;
|
|
119
|
+
//#endregion
|
|
120
|
+
//#region src/providers/github/reader.d.ts
|
|
121
|
+
/**
|
|
122
|
+
* GitHubReader — `RepoReader` backed by the GitHub Repos + Git Data APIs.
|
|
123
|
+
*
|
|
124
|
+
* Reads pass through `repos.getContent`; directories return a list of
|
|
125
|
+
* names, files return decoded UTF-8 text. Files larger than ~1 MB are
|
|
126
|
+
* fetched by blob SHA through `git.getBlob` because `getContent` omits
|
|
127
|
+
* the body in that case.
|
|
128
|
+
*
|
|
129
|
+
* `ref` is forwarded verbatim and may be a branch name, tag name or
|
|
130
|
+
* commit SHA. When omitted, GitHub resolves to the repository's default
|
|
131
|
+
* branch — which is usually wrong for Contentrain flows, so callers
|
|
132
|
+
* should always pass the explicit `contentrain` tracking branch.
|
|
133
|
+
*/
|
|
134
|
+
declare class GitHubReader implements RepoReader {
|
|
135
|
+
private readonly client;
|
|
136
|
+
private readonly repo;
|
|
137
|
+
constructor(client: GitHubClient, repo: RepoRef);
|
|
138
|
+
readFile(path: string, ref?: string): Promise<string>;
|
|
139
|
+
listDirectory(path: string, ref?: string): Promise<string[]>;
|
|
140
|
+
fileExists(path: string, ref?: string): Promise<boolean>;
|
|
141
|
+
}
|
|
142
|
+
//#endregion
|
|
143
|
+
export { DEFAULT_GITHUB_AUTHOR, GITHUB_CAPABILITIES, type GitHubAuth, type GitHubClient, GitHubProvider, GitHubReader, type RepoRef, createGitHubClient, createGitHubProvider };
|
|
144
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../../src/providers/github/capabilities.ts","../../../src/providers/github/client.ts","../../../src/providers/github/types.ts","../../../src/providers/github/provider.ts","../../../src/providers/github/factory.ts","../../../src/providers/github/reader.ts"],"mappings":";;;;;;;AAeA;;;;;;;;ACJA;;cDIa,mBAAA,EAAqB,oBAAA;;;;;;AAAlC;;;;;;KCJY,YAAA,GAAe,OAAA;;;;;;ADI3B;;;;;UELiB,OAAA;EACf,KAAA;EACA,IAAA;EACA,WAAA;AAAA;;;;;;;AAHF;;;KAeY,UAAA;EACN,IAAA;EAAa,KAAA;AAAA;EAEb,IAAA;EACA,KAAA;EACA,UAAA;EACA,cAAA;AAAA;;cAIO,qBAAA,EAAuB,YAAA;;;;AFpBpC;;;;;;;;cGkBa,cAAA,YAA0B,YAAA;EAAA,iBAKlB,MAAA;EAAA,SACD,IAAA,EAAM,OAAA;EAAA,SALf,YAAA,EAAc,oBAAA;EAAA,iBACN,MAAA;cAGE,MAAA,EAAQ,YAAA,EACT,IAAA,EAAM,OAAA;EAKxB,QAAA,CAAS,IAAA,UAAc,GAAA,YAAe,OAAA;EAGtC,aAAA,CAAc,IAAA,UAAc,GAAA,YAAe,OAAA;EAG3C,UAAA,CAAW,IAAA,UAAc,GAAA,YAAe,OAAA;EAIxC,SAAA,CAAU,KAAA,EAAO,cAAA,GAAiB,OAAA,CAAQ,MAAA;EAI1C,YAAA,CAAa,MAAA,YAAkB,OAAA,CAAQ,MAAA;EAGvC,YAAA,CAAa,IAAA,UAAc,OAAA,YAAmB,OAAA;EAI9C,YAAA,CAAa,IAAA,WAAe,OAAA;EAG5B,aAAA,CAAc,MAAA,UAAgB,IAAA,YAAgB,OAAA,CAAQ,QAAA;EAItD,WAAA,CAAY,MAAA,UAAgB,IAAA,WAAe,OAAA,CAAQ,WAAA;EAGnD,QAAA,CAAS,MAAA,UAAgB,IAAA,YAAgB,OAAA;EAIzC,gBAAA,CAAA,GAAoB,OAAA;AAAA;;;;AHhEtB;;;;;;;;ACJA;;iBGIsB,kBAAA,CAAmB,IAAA,EAAM,UAAA,GAAa,OAAA,CAAQ,YAAA;;;;;;AFLpE;iBEiCsB,oBAAA,CAAqB,IAAA;EAAQ,IAAA,EAAM,UAAA;EAAY,IAAA,EAAM,OAAA;AAAA,IAAY,OAAA,CAAQ,cAAA;;;;AJ5B/F;;;;;;;;ACJA;;;;cIOa,YAAA,YAAwB,UAAA;EAAA,iBAEhB,MAAA;EAAA,iBACA,IAAA;cADA,MAAA,EAAQ,YAAA,EACR,IAAA,EAAM,OAAA;EAGnB,QAAA,CAAS,IAAA,UAAc,GAAA,YAAe,OAAA;EAmCtC,aAAA,CAAc,IAAA,UAAc,GAAA,YAAe,OAAA;EAkB3C,UAAA,CAAW,IAAA,UAAc,GAAA,YAAe,OAAA;AAAA"}
|
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
import { n as isNotFoundError, t as resolveRepoPath } from "../../paths-C3xNyc7z.mjs";
|
|
2
|
+
import { CONTENTRAIN_BRANCH } from "@contentrain/types";
|
|
3
|
+
//#region src/providers/github/capabilities.ts
|
|
4
|
+
/**
|
|
5
|
+
* Capability set for GitHubProvider.
|
|
6
|
+
*
|
|
7
|
+
* GitHub over the Git Data API has no working tree, so local worktree
|
|
8
|
+
* features, source-file access and AST scans are unavailable. Push /
|
|
9
|
+
* PR operations are free because every commit goes straight to the
|
|
10
|
+
* remote. Branch protection detection uses the Repos API.
|
|
11
|
+
*
|
|
12
|
+
* Tools that require `astScan`, `sourceRead` or `sourceWrite` must
|
|
13
|
+
* gracefully reject (with `capability_required`) when running against
|
|
14
|
+
* a GitHubProvider — this is the mechanism behind phase 6's normalize
|
|
15
|
+
* capability-gate.
|
|
16
|
+
*/
|
|
17
|
+
const GITHUB_CAPABILITIES = {
|
|
18
|
+
localWorktree: false,
|
|
19
|
+
sourceRead: false,
|
|
20
|
+
sourceWrite: false,
|
|
21
|
+
pushRemote: true,
|
|
22
|
+
branchProtection: true,
|
|
23
|
+
pullRequestFallback: true,
|
|
24
|
+
astScan: false
|
|
25
|
+
};
|
|
26
|
+
//#endregion
|
|
27
|
+
//#region src/providers/github/apply-plan.ts
|
|
28
|
+
/**
|
|
29
|
+
* Apply a plan to a GitHub repository as a single atomic commit via the
|
|
30
|
+
* Git Data API. High-level flow:
|
|
31
|
+
*
|
|
32
|
+
* 1. Resolve the base commit SHA — either the current HEAD of the target
|
|
33
|
+
* branch, or the HEAD of `input.base` (or the repo's default branch)
|
|
34
|
+
* when the target branch does not yet exist.
|
|
35
|
+
* 2. Read the base tree SHA from that commit.
|
|
36
|
+
* 3. Walk `input.changes` in parallel — create a blob per non-null
|
|
37
|
+
* content, emit a tree entry with `sha: null` for each deletion.
|
|
38
|
+
* 4. Create a new tree layered on top of the base tree with the collected
|
|
39
|
+
* entries.
|
|
40
|
+
* 5. Create the commit (tree, parents, author).
|
|
41
|
+
* 6. Update an existing branch ref or create a new one pointing at the
|
|
42
|
+
* commit.
|
|
43
|
+
*
|
|
44
|
+
* No working tree, no transaction — the commit is durable as soon as the
|
|
45
|
+
* final ref update returns.
|
|
46
|
+
*/
|
|
47
|
+
async function applyPlanToGitHub(client, repo, input) {
|
|
48
|
+
const { baseSha, branchExists } = await resolveBaseSha(client, repo, input.branch, input.base);
|
|
49
|
+
const baseTreeSha = (await client.rest.git.getCommit({
|
|
50
|
+
owner: repo.owner,
|
|
51
|
+
repo: repo.name,
|
|
52
|
+
commit_sha: baseSha
|
|
53
|
+
})).data.tree.sha;
|
|
54
|
+
const treeEntries = await Promise.all(input.changes.map((change) => buildTreeEntry(client, repo, change)));
|
|
55
|
+
const tree = await client.rest.git.createTree({
|
|
56
|
+
owner: repo.owner,
|
|
57
|
+
repo: repo.name,
|
|
58
|
+
base_tree: baseTreeSha,
|
|
59
|
+
tree: treeEntries
|
|
60
|
+
});
|
|
61
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
62
|
+
const commit = await client.rest.git.createCommit({
|
|
63
|
+
owner: repo.owner,
|
|
64
|
+
repo: repo.name,
|
|
65
|
+
message: input.message,
|
|
66
|
+
tree: tree.data.sha,
|
|
67
|
+
parents: [baseSha],
|
|
68
|
+
author: {
|
|
69
|
+
name: input.author.name,
|
|
70
|
+
email: input.author.email,
|
|
71
|
+
date: timestamp
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
if (branchExists) await client.rest.git.updateRef({
|
|
75
|
+
owner: repo.owner,
|
|
76
|
+
repo: repo.name,
|
|
77
|
+
ref: `heads/${input.branch}`,
|
|
78
|
+
sha: commit.data.sha
|
|
79
|
+
});
|
|
80
|
+
else await client.rest.git.createRef({
|
|
81
|
+
owner: repo.owner,
|
|
82
|
+
repo: repo.name,
|
|
83
|
+
ref: `refs/heads/${input.branch}`,
|
|
84
|
+
sha: commit.data.sha
|
|
85
|
+
});
|
|
86
|
+
return {
|
|
87
|
+
sha: commit.data.sha,
|
|
88
|
+
message: commit.data.message,
|
|
89
|
+
author: {
|
|
90
|
+
name: commit.data.author?.name ?? input.author.name,
|
|
91
|
+
email: commit.data.author?.email ?? input.author.email
|
|
92
|
+
},
|
|
93
|
+
timestamp: commit.data.author?.date ?? timestamp
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
async function buildTreeEntry(client, repo, change) {
|
|
97
|
+
const path = resolveRepoPath(repo.contentRoot, change.path);
|
|
98
|
+
if (change.content === null) return {
|
|
99
|
+
path,
|
|
100
|
+
mode: "100644",
|
|
101
|
+
type: "blob",
|
|
102
|
+
sha: null
|
|
103
|
+
};
|
|
104
|
+
return {
|
|
105
|
+
path,
|
|
106
|
+
mode: "100644",
|
|
107
|
+
type: "blob",
|
|
108
|
+
sha: (await client.rest.git.createBlob({
|
|
109
|
+
owner: repo.owner,
|
|
110
|
+
repo: repo.name,
|
|
111
|
+
content: change.content,
|
|
112
|
+
encoding: "utf-8"
|
|
113
|
+
})).data.sha
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
async function resolveBaseSha(client, repo, branch, base) {
|
|
117
|
+
try {
|
|
118
|
+
return {
|
|
119
|
+
baseSha: (await client.rest.git.getRef({
|
|
120
|
+
owner: repo.owner,
|
|
121
|
+
repo: repo.name,
|
|
122
|
+
ref: `heads/${branch}`
|
|
123
|
+
})).data.object.sha,
|
|
124
|
+
branchExists: true
|
|
125
|
+
};
|
|
126
|
+
} catch (error) {
|
|
127
|
+
if (!isNotFoundError(error)) throw error;
|
|
128
|
+
}
|
|
129
|
+
const baseRefName = base ?? CONTENTRAIN_BRANCH;
|
|
130
|
+
return {
|
|
131
|
+
baseSha: (await client.rest.git.getRef({
|
|
132
|
+
owner: repo.owner,
|
|
133
|
+
repo: repo.name,
|
|
134
|
+
ref: `heads/${baseRefName}`
|
|
135
|
+
})).data.object.sha,
|
|
136
|
+
branchExists: false
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
//#endregion
|
|
140
|
+
//#region src/providers/github/branch-ops.ts
|
|
141
|
+
/**
|
|
142
|
+
* Branch/merge/diff helpers backed by the Repos and Git Data APIs.
|
|
143
|
+
*
|
|
144
|
+
* Pure functions so they can be composed into `GitHubProvider` or used
|
|
145
|
+
* standalone. All throw on non-404 errors; 404s collapse to empty-ish
|
|
146
|
+
* results where that matches the `RepoProvider` contract (e.g. a
|
|
147
|
+
* missing branch prefix yields `[]` rather than raising).
|
|
148
|
+
*/
|
|
149
|
+
async function getDefaultBranch(client, repo) {
|
|
150
|
+
return (await client.rest.repos.get({
|
|
151
|
+
owner: repo.owner,
|
|
152
|
+
repo: repo.name
|
|
153
|
+
})).data.default_branch;
|
|
154
|
+
}
|
|
155
|
+
async function listBranches(client, repo, prefix) {
|
|
156
|
+
const branches = [];
|
|
157
|
+
const iterator = client.paginate.iterator(client.rest.repos.listBranches, {
|
|
158
|
+
owner: repo.owner,
|
|
159
|
+
repo: repo.name,
|
|
160
|
+
per_page: 100
|
|
161
|
+
});
|
|
162
|
+
for await (const page of iterator) for (const b of page.data) {
|
|
163
|
+
if (prefix && !b.name.startsWith(prefix)) continue;
|
|
164
|
+
branches.push({
|
|
165
|
+
name: b.name,
|
|
166
|
+
sha: b.commit.sha,
|
|
167
|
+
protected: b.protected
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
return branches;
|
|
171
|
+
}
|
|
172
|
+
async function createBranch(client, repo, name, fromRef) {
|
|
173
|
+
const base = await client.rest.git.getRef({
|
|
174
|
+
owner: repo.owner,
|
|
175
|
+
repo: repo.name,
|
|
176
|
+
ref: `heads/${fromRef}`
|
|
177
|
+
});
|
|
178
|
+
await client.rest.git.createRef({
|
|
179
|
+
owner: repo.owner,
|
|
180
|
+
repo: repo.name,
|
|
181
|
+
ref: `refs/heads/${name}`,
|
|
182
|
+
sha: base.data.object.sha
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
async function deleteBranch(client, repo, name) {
|
|
186
|
+
await client.rest.git.deleteRef({
|
|
187
|
+
owner: repo.owner,
|
|
188
|
+
repo: repo.name,
|
|
189
|
+
ref: `heads/${name}`
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
async function getBranchDiff(client, repo, branch, base) {
|
|
193
|
+
return ((await client.rest.repos.compareCommits({
|
|
194
|
+
owner: repo.owner,
|
|
195
|
+
repo: repo.name,
|
|
196
|
+
base,
|
|
197
|
+
head: branch
|
|
198
|
+
})).data.files ?? []).map((f) => ({
|
|
199
|
+
path: f.filename,
|
|
200
|
+
status: normaliseStatus(f.status),
|
|
201
|
+
before: null,
|
|
202
|
+
after: null
|
|
203
|
+
}));
|
|
204
|
+
}
|
|
205
|
+
async function mergeBranch(client, repo, branch, into) {
|
|
206
|
+
try {
|
|
207
|
+
return {
|
|
208
|
+
merged: true,
|
|
209
|
+
sha: (await client.rest.repos.merge({
|
|
210
|
+
owner: repo.owner,
|
|
211
|
+
repo: repo.name,
|
|
212
|
+
base: into,
|
|
213
|
+
head: branch
|
|
214
|
+
})).data.sha,
|
|
215
|
+
pullRequestUrl: null
|
|
216
|
+
};
|
|
217
|
+
} catch (error) {
|
|
218
|
+
if (isNotModified(error)) return {
|
|
219
|
+
merged: true,
|
|
220
|
+
sha: null,
|
|
221
|
+
pullRequestUrl: null
|
|
222
|
+
};
|
|
223
|
+
throw error;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
async function isMerged(client, repo, branch, into) {
|
|
227
|
+
return (await client.rest.repos.compareCommits({
|
|
228
|
+
owner: repo.owner,
|
|
229
|
+
repo: repo.name,
|
|
230
|
+
base: into,
|
|
231
|
+
head: branch
|
|
232
|
+
})).data.ahead_by === 0;
|
|
233
|
+
}
|
|
234
|
+
function normaliseStatus(status) {
|
|
235
|
+
if (status === "added") return "added";
|
|
236
|
+
if (status === "removed") return "removed";
|
|
237
|
+
return "modified";
|
|
238
|
+
}
|
|
239
|
+
function isNotModified(error) {
|
|
240
|
+
return typeof error === "object" && error !== null && error.status === 204;
|
|
241
|
+
}
|
|
242
|
+
//#endregion
|
|
243
|
+
//#region src/providers/github/reader.ts
|
|
244
|
+
/**
|
|
245
|
+
* GitHubReader — `RepoReader` backed by the GitHub Repos + Git Data APIs.
|
|
246
|
+
*
|
|
247
|
+
* Reads pass through `repos.getContent`; directories return a list of
|
|
248
|
+
* names, files return decoded UTF-8 text. Files larger than ~1 MB are
|
|
249
|
+
* fetched by blob SHA through `git.getBlob` because `getContent` omits
|
|
250
|
+
* the body in that case.
|
|
251
|
+
*
|
|
252
|
+
* `ref` is forwarded verbatim and may be a branch name, tag name or
|
|
253
|
+
* commit SHA. When omitted, GitHub resolves to the repository's default
|
|
254
|
+
* branch — which is usually wrong for Contentrain flows, so callers
|
|
255
|
+
* should always pass the explicit `contentrain` tracking branch.
|
|
256
|
+
*/
|
|
257
|
+
var GitHubReader = class {
|
|
258
|
+
constructor(client, repo) {
|
|
259
|
+
this.client = client;
|
|
260
|
+
this.repo = repo;
|
|
261
|
+
}
|
|
262
|
+
async readFile(path, ref) {
|
|
263
|
+
const repoPath = resolveRepoPath(this.repo.contentRoot, path);
|
|
264
|
+
const data = (await this.client.rest.repos.getContent({
|
|
265
|
+
owner: this.repo.owner,
|
|
266
|
+
repo: this.repo.name,
|
|
267
|
+
path: repoPath,
|
|
268
|
+
ref
|
|
269
|
+
})).data;
|
|
270
|
+
if (Array.isArray(data)) throw new Error(`GitHubReader: path "${path}" is a directory, not a file`);
|
|
271
|
+
if (data.type !== "file") throw new Error(`GitHubReader: path "${path}" is a ${data.type}, not a file`);
|
|
272
|
+
if (data.content === "" && data.size > 0) {
|
|
273
|
+
const blob = await this.client.rest.git.getBlob({
|
|
274
|
+
owner: this.repo.owner,
|
|
275
|
+
repo: this.repo.name,
|
|
276
|
+
file_sha: data.sha
|
|
277
|
+
});
|
|
278
|
+
if (blob.data.encoding !== "base64") throw new Error(`GitHubReader: unexpected blob encoding "${blob.data.encoding}" for ${path}`);
|
|
279
|
+
return Buffer.from(blob.data.content, "base64").toString("utf-8");
|
|
280
|
+
}
|
|
281
|
+
if (data.encoding !== "base64") throw new Error(`GitHubReader: unexpected encoding "${data.encoding}" for ${path}`);
|
|
282
|
+
return Buffer.from(data.content, "base64").toString("utf-8");
|
|
283
|
+
}
|
|
284
|
+
async listDirectory(path, ref) {
|
|
285
|
+
const repoPath = resolveRepoPath(this.repo.contentRoot, path);
|
|
286
|
+
try {
|
|
287
|
+
const data = (await this.client.rest.repos.getContent({
|
|
288
|
+
owner: this.repo.owner,
|
|
289
|
+
repo: this.repo.name,
|
|
290
|
+
path: repoPath,
|
|
291
|
+
ref
|
|
292
|
+
})).data;
|
|
293
|
+
if (!Array.isArray(data)) return [];
|
|
294
|
+
return data.map((entry) => entry.name);
|
|
295
|
+
} catch (error) {
|
|
296
|
+
if (isNotFoundError(error)) return [];
|
|
297
|
+
throw error;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
async fileExists(path, ref) {
|
|
301
|
+
const repoPath = resolveRepoPath(this.repo.contentRoot, path);
|
|
302
|
+
try {
|
|
303
|
+
await this.client.rest.repos.getContent({
|
|
304
|
+
owner: this.repo.owner,
|
|
305
|
+
repo: this.repo.name,
|
|
306
|
+
path: repoPath,
|
|
307
|
+
ref
|
|
308
|
+
});
|
|
309
|
+
return true;
|
|
310
|
+
} catch (error) {
|
|
311
|
+
if (isNotFoundError(error)) return false;
|
|
312
|
+
throw error;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
//#endregion
|
|
317
|
+
//#region src/providers/github/provider.ts
|
|
318
|
+
/**
|
|
319
|
+
* GitHubProvider — `RepoProvider` backed by the Octokit-driven GitHub
|
|
320
|
+
* REST + Git Data APIs.
|
|
321
|
+
*
|
|
322
|
+
* The provider is transport-agnostic; it only talks to an `Octokit`
|
|
323
|
+
* instance passed into the constructor. The `createGitHubProvider`
|
|
324
|
+
* helper in `factory.ts` wraps the dynamic import so consumers never
|
|
325
|
+
* have to touch Octokit directly.
|
|
326
|
+
*/
|
|
327
|
+
var GitHubProvider = class {
|
|
328
|
+
capabilities = GITHUB_CAPABILITIES;
|
|
329
|
+
reader;
|
|
330
|
+
constructor(client, repo) {
|
|
331
|
+
this.client = client;
|
|
332
|
+
this.repo = repo;
|
|
333
|
+
this.reader = new GitHubReader(client, repo);
|
|
334
|
+
}
|
|
335
|
+
readFile(path, ref) {
|
|
336
|
+
return this.reader.readFile(path, ref);
|
|
337
|
+
}
|
|
338
|
+
listDirectory(path, ref) {
|
|
339
|
+
return this.reader.listDirectory(path, ref);
|
|
340
|
+
}
|
|
341
|
+
fileExists(path, ref) {
|
|
342
|
+
return this.reader.fileExists(path, ref);
|
|
343
|
+
}
|
|
344
|
+
applyPlan(input) {
|
|
345
|
+
return applyPlanToGitHub(this.client, this.repo, input);
|
|
346
|
+
}
|
|
347
|
+
listBranches(prefix) {
|
|
348
|
+
return listBranches(this.client, this.repo, prefix);
|
|
349
|
+
}
|
|
350
|
+
createBranch(name, fromRef) {
|
|
351
|
+
const resolved = fromRef ?? "main";
|
|
352
|
+
return createBranch(this.client, this.repo, name, resolved);
|
|
353
|
+
}
|
|
354
|
+
deleteBranch(name) {
|
|
355
|
+
return deleteBranch(this.client, this.repo, name);
|
|
356
|
+
}
|
|
357
|
+
getBranchDiff(branch, base) {
|
|
358
|
+
const resolved = base ?? "main";
|
|
359
|
+
return getBranchDiff(this.client, this.repo, branch, resolved);
|
|
360
|
+
}
|
|
361
|
+
mergeBranch(branch, into) {
|
|
362
|
+
return mergeBranch(this.client, this.repo, branch, into);
|
|
363
|
+
}
|
|
364
|
+
isMerged(branch, into) {
|
|
365
|
+
const resolved = into ?? "main";
|
|
366
|
+
return isMerged(this.client, this.repo, branch, resolved);
|
|
367
|
+
}
|
|
368
|
+
getDefaultBranch() {
|
|
369
|
+
return getDefaultBranch(this.client, this.repo);
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
//#endregion
|
|
373
|
+
//#region src/providers/github/factory.ts
|
|
374
|
+
/**
|
|
375
|
+
* Create an Octokit-backed `GitHubClient` from an auth configuration.
|
|
376
|
+
*
|
|
377
|
+
* The `@octokit/rest` module is imported dynamically so it stays a pure
|
|
378
|
+
* optional peer dependency — self-hosted MCP on stdio can run without
|
|
379
|
+
* it. If the module is not installed, the import throws with a helpful
|
|
380
|
+
* hint pointing the operator at the peer dependency.
|
|
381
|
+
*
|
|
382
|
+
* Phase 5.1 ships with PAT auth only. App-based auth (JWT + installation
|
|
383
|
+
* token) lands in phase 5.2 together with the HTTP transport.
|
|
384
|
+
*/
|
|
385
|
+
async function createGitHubClient(auth) {
|
|
386
|
+
if (auth.type !== "pat") throw new Error(`GitHub auth type "${auth.type}" is not yet supported. Phase 5.1 ships with "pat" only; "app" installation auth arrives in phase 5.2.`);
|
|
387
|
+
let OctokitCtor;
|
|
388
|
+
try {
|
|
389
|
+
({Octokit: OctokitCtor} = await import("@octokit/rest"));
|
|
390
|
+
} catch (error) {
|
|
391
|
+
throw new Error("@octokit/rest is required for the GitHubProvider but could not be loaded. Install it as a peer dependency: pnpm add @octokit/rest.", { cause: error });
|
|
392
|
+
}
|
|
393
|
+
return new OctokitCtor({ auth: auth.token });
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Factory for the full provider — instantiates an Octokit client and
|
|
397
|
+
* wraps it in a `GitHubProvider`. Consumers who already hold an Octokit
|
|
398
|
+
* instance (HTTP server injecting shared clients, tests, etc.) should
|
|
399
|
+
* instantiate `GitHubProvider` directly instead.
|
|
400
|
+
*/
|
|
401
|
+
async function createGitHubProvider(opts) {
|
|
402
|
+
return new GitHubProvider(await createGitHubClient(opts.auth), opts.repo);
|
|
403
|
+
}
|
|
404
|
+
//#endregion
|
|
405
|
+
//#region src/providers/github/types.ts
|
|
406
|
+
/** Default author used when a call does not provide one. */
|
|
407
|
+
const DEFAULT_GITHUB_AUTHOR = {
|
|
408
|
+
name: "Contentrain",
|
|
409
|
+
email: "mcp@contentrain.io"
|
|
410
|
+
};
|
|
411
|
+
//#endregion
|
|
412
|
+
export { DEFAULT_GITHUB_AUTHOR, GITHUB_CAPABILITIES, GitHubProvider, GitHubReader, createGitHubClient, createGitHubProvider };
|
|
413
|
+
|
|
414
|
+
//# sourceMappingURL=index.mjs.map
|