contentrain 0.4.3 → 0.5.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 +112 -34
- package/dist/{activity-CbkxcfDf.mjs → activity-B8j9gfsK.mjs} +3 -3
- package/dist/{branches-D2UWex1N.mjs → branches-CvpFbyrJ.mjs} +3 -3
- package/dist/{cdn-build-BzC2QPNi.mjs → cdn-build-DS9FprWU.mjs} +3 -3
- package/dist/{cdn-init-CsmqJIaQ.mjs → cdn-init-Bm4dpEJn.mjs} +3 -3
- package/dist/{connect-DJo9PSiw.mjs → connect-Bb0BrZsx.mjs} +3 -3
- package/dist/describe-DJ5H9Cxm.mjs +106 -0
- package/dist/describe-format-CUAqB0C4.mjs +50 -0
- package/dist/{diff--halNUpG.mjs → diff-ov1j9bAb.mjs} +67 -59
- package/dist/doctor-MOf_LQEL.mjs +95 -0
- package/dist/{generate-DrUojrk8.mjs → generate-ByscV5SN.mjs} +26 -9
- package/dist/{ide-4ZGiAtYB.mjs → ide-DPDVYiaq.mjs} +6 -8
- package/dist/index.mjs +50 -12
- package/dist/{init-BVlisNcW.mjs → init-540qiNQJ.mjs} +6 -6
- package/dist/{login-DCXkOrQV.mjs → login-xvzL-UvR.mjs} +3 -3
- package/dist/{logout-nPt2UAeQ.mjs → logout-DzspSatR.mjs} +2 -2
- package/dist/mcp-client-DVWTex2i.mjs +44 -0
- package/dist/merge-DdPRttF-.mjs +82 -0
- package/dist/{resolve-context-Cg6JoVZt.mjs → resolve-context-hKVYPniC.mjs} +1 -1
- package/dist/scaffold-iuvrMh78.mjs +115 -0
- package/dist/{serve-CNeFIQ1i.mjs → serve-CxA2pKbp.mjs} +58 -5
- package/dist/serve-ui/assets/{AgentPromptGroup.vue_vue_type_script_setup_true_lang-Ulb-9Tr3.js → AgentPromptGroup.vue_vue_type_script_setup_true_lang-Bftt9tx_.js} +1 -1
- package/dist/serve-ui/assets/AppLayout-B-fbrfMk.js +21 -0
- package/dist/serve-ui/assets/BranchDetailPage-B3PL9PGp.js +17 -0
- package/dist/serve-ui/assets/{BranchesPage-CSwikNkd.js → BranchesPage-4YpE_szf.js} +2 -2
- package/dist/serve-ui/assets/CardContent.vue_vue_type_script_setup_true_lang-D1L2MnBU.js +1 -0
- package/dist/serve-ui/assets/CollapsibleTrigger.vue_vue_type_script_setup_true_lang-B4uDtllm.js +1 -0
- package/dist/serve-ui/assets/{Collection-DTB4xCCE.js → Collection-9URFko-Y.js} +2 -2
- package/dist/serve-ui/assets/ConfigProvider-CwkmRpT5.js +1 -0
- package/dist/serve-ui/assets/{ContentListPage-DF-LjAaS.js → ContentListPage-BMfSfg8x.js} +2 -2
- package/dist/serve-ui/assets/ContentPage-BXUnNJlB.js +1 -0
- package/dist/serve-ui/assets/DashboardPage-1UyYgg5J.js +21 -0
- package/dist/serve-ui/assets/{DialogTitle.vue_vue_type_script_setup_true_lang-2sQAJn_Y.js → DialogTitle.vue_vue_type_script_setup_true_lang-M4LOzTva.js} +1 -1
- package/dist/serve-ui/assets/DoctorPage-WnQ8xYWy.js +1 -0
- package/dist/serve-ui/assets/FormatReferencePage-DoKEaawl.js +1 -0
- package/dist/serve-ui/assets/{Input.vue_vue_type_script_setup_true_lang-BveqfZsR.js → Input.vue_vue_type_script_setup_true_lang-tW0hgReH.js} +2 -2
- package/dist/serve-ui/assets/{ModelDetailPage-_TIV9itx.js → ModelDetailPage-BysqN8IL.js} +3 -3
- package/dist/serve-ui/assets/ModelsPage-DFZrdBIi.js +1 -0
- package/dist/serve-ui/assets/{NormalizePage-Cde8vdk2.js → NormalizePage-4f5Ib_ES.js} +2 -2
- package/dist/serve-ui/assets/{PageHeader.vue_vue_type_script_setup_true_lang-CEnjONvd.js → PageHeader.vue_vue_type_script_setup_true_lang-Bln1czg7.js} +1 -1
- package/dist/serve-ui/assets/Presence-CA3xTrht.js +3 -0
- package/dist/serve-ui/assets/{Separator.vue_vue_type_script_setup_true_lang-1pFZ2PR2.js → Separator.vue_vue_type_script_setup_true_lang-PECBq3nc.js} +1 -1
- package/dist/serve-ui/assets/{StudioHint.vue_vue_type_script_setup_true_lang-C6JN5tPq.js → StudioHint.vue_vue_type_script_setup_true_lang-DYTEoyjw.js} +1 -1
- package/dist/serve-ui/assets/{TrustBadge.vue_vue_type_script_setup_true_lang-KRiTnLVk.js → TrustBadge.vue_vue_type_script_setup_true_lang-BRXWvOYU.js} +2 -2
- package/dist/serve-ui/assets/ValidatePage-BMTpV2KL.js +16 -0
- package/dist/serve-ui/assets/{arrow-down-BAJwpryD.js → arrow-down-IJu6H7Es.js} +1 -1
- package/dist/serve-ui/assets/{arrow-left-DANf_wrK.js → arrow-left-D8ZjVYAI.js} +1 -1
- package/dist/serve-ui/assets/{book-marked-DS8FXgjz.js → book-marked-BNyyZrq3.js} +1 -1
- package/dist/serve-ui/assets/{book-open-DnH5T78M.js → book-open-Dw1fQtSI.js} +1 -1
- package/dist/serve-ui/assets/{box-UJk5SvJJ.js → box-BoGMZDZR.js} +1 -1
- package/dist/serve-ui/assets/{chevron-down-C1nYx5Eu.js → chevron-down-UcXxhyyY.js} +1 -1
- package/dist/serve-ui/assets/{chevron-right-XnJVrezx.js → chevron-right-SpdM5LBc.js} +1 -1
- package/dist/serve-ui/assets/circle-alert-CEwKJpYl.js +6 -0
- package/dist/serve-ui/assets/{content-Ctqretwl.js → content-BQEzysvq.js} +1 -1
- package/dist/serve-ui/assets/{database-Wd_i2bM1.js → database-U6JU8oc9.js} +1 -1
- package/dist/serve-ui/assets/{file-code-ScWZN40B.js → file-code-_sq1QHQo.js} +1 -1
- package/dist/serve-ui/assets/{file-text-J3Wzwieh.js → file-text-BiMGJckO.js} +1 -1
- package/dist/serve-ui/assets/{git-branch-DqEFTZDt.js → git-branch-CdZMOuSP.js} +1 -1
- package/dist/serve-ui/assets/{git-merge-Bmz-MVBm.js → git-merge-D8Y-BwIh.js} +1 -1
- package/dist/serve-ui/assets/{hash-pCwLSzjB.js → hash-CrXakbCy.js} +1 -1
- package/dist/serve-ui/assets/index-DL_dw3XX.js +80 -0
- package/dist/serve-ui/assets/index-KZ2rT4mJ.css +1 -0
- package/dist/serve-ui/assets/{languages-CQqNMim-.js → languages-DvyMGPwy.js} +1 -1
- package/dist/serve-ui/assets/{plus-Cw1OnTR2.js → plus-Ddujj7Qx.js} +1 -1
- package/dist/serve-ui/assets/project-Dg2BVay_.js +1 -0
- package/dist/serve-ui/assets/{scan-search-UJ-_oiOR.js → scan-search-CsPYGmEs.js} +1 -1
- package/dist/serve-ui/assets/{settings-Dnm4yTIj.js → settings-IQvevsKM.js} +1 -1
- package/dist/serve-ui/assets/{shield-check-B67ELGUT.js → shield-check-BmSBU8Ai.js} +1 -1
- package/dist/serve-ui/assets/stethoscope-DgCa_7BL.js +6 -0
- package/dist/serve-ui/assets/{trash-2-B1QgZW1G.js → trash-2-CNTkluuz.js} +1 -1
- package/dist/serve-ui/assets/useDirection-DfPL7a9d.js +1 -0
- package/dist/serve-ui/assets/{useId-CPFWZ4Bo.js → useId-B-RTM02R.js} +1 -1
- package/dist/serve-ui/assets/{useNonce-BffIzuhE.js → useNonce-DR6M3tLq.js} +1 -1
- package/dist/serve-ui/assets/useWatch-D4Y-epQR.js +1 -0
- package/dist/serve-ui/index.html +2 -2
- package/dist/{server-DTdmutjb.mjs → server-B1PSjXbu.mjs} +313 -112
- package/dist/{setup-4mS8RuCy.mjs → setup-BMqKibZt.mjs} +3 -3
- package/dist/{skills-4nhto0AZ.mjs → skills-CZ54F-97.mjs} +3 -3
- package/dist/{status-Cyv-jo1y.mjs → status-BJ6Lu3J4.mjs} +3 -3
- package/dist/{status-CoHgz6HN.mjs → status-Bmjfrk-B.mjs} +24 -11
- package/dist/studio-DPVJ-2oG.mjs +24 -0
- package/dist/{submissions-bIG-MWvv.mjs → submissions-zH_S7JIQ.mjs} +3 -3
- package/dist/{usage-B2xuc2DT.mjs → usage-z6eyT64a.mjs} +3 -3
- package/dist/{validate-B10L7RL7.mjs → validate-BG0X5g09.mjs} +102 -4
- package/dist/{webhooks-Dq3zFLP7.mjs → webhooks-CBG2Bs9P.mjs} +3 -3
- package/dist/{whoami-Ub5RznbU.mjs → whoami-CXVfvo7x.mjs} +2 -2
- package/package.json +7 -6
- package/dist/doctor-DyKjAIKH.mjs +0 -187
- package/dist/serve-ui/assets/AppLayout-C91DN08z.js +0 -21
- package/dist/serve-ui/assets/BranchDetailPage-BokOnhE7.js +0 -17
- package/dist/serve-ui/assets/CollapsibleTrigger.vue_vue_type_script_setup_true_lang-BRzXVXDG.js +0 -1
- package/dist/serve-ui/assets/ConfigProvider-Dvd7YoEV.js +0 -1
- package/dist/serve-ui/assets/ContentPage-BABg8pgn.js +0 -1
- package/dist/serve-ui/assets/DashboardPage-BFLFEJNZ.js +0 -21
- package/dist/serve-ui/assets/ModelsPage-DrQemHQ-.js +0 -1
- package/dist/serve-ui/assets/Presence-BXKCIfUk.js +0 -3
- package/dist/serve-ui/assets/ValidatePage-DRFAySNR.js +0 -21
- package/dist/serve-ui/assets/index-CG5NtNtt.css +0 -1
- package/dist/serve-ui/assets/index-De_z0714.js +0 -1
- package/dist/serve-ui/assets/index-RDdffBTh.js +0 -80
- package/dist/serve-ui/assets/project-BlZcHJw6.js +0 -1
- package/dist/serve-ui/assets/useDirection-B4S5kPsI.js +0 -1
- package/dist/serve-ui/assets/useWatch-B2FLRWr8.js +0 -1
- package/dist/studio-CJUGiIMd.mjs +0 -24
- package/dist/{client-C400qHcL.mjs → client-C9E9D4mb.mjs} +1 -1
- package/dist/{context-An3xrvcP.mjs → context-OskMGy8y.mjs} +1 -1
- package/dist/{demo-DcBt6fw8.mjs → demo-BOUcAoFZ.mjs} +3 -3
- package/dist/{oauth-server-Beppljfz.mjs → oauth-server-smSxDoH5.mjs} +1 -1
- /package/dist/{ui-B5mXontH.mjs → ui-B8l_LC36.mjs} +0 -0
|
@@ -1,16 +1,90 @@
|
|
|
1
|
-
import { simpleGit } from "simple-git";
|
|
2
|
-
import { CONTENTRAIN_BRANCH } from "@contentrain/types";
|
|
3
|
-
import { tmpdir } from "node:os";
|
|
4
|
-
import { randomUUID } from "node:crypto";
|
|
5
1
|
import { join } from "node:path";
|
|
6
|
-
import {
|
|
7
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { readConfig } from "@contentrain/mcp/core/config";
|
|
8
3
|
import { createServer } from "@contentrain/mcp/server";
|
|
9
|
-
import { createApp, createError, createRouter, defineEventHandler, getQuery, getRouterParam, readBody, toNodeListener } from "h3";
|
|
10
|
-
import { WebSocketServer } from "ws";
|
|
11
4
|
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
|
|
12
5
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
6
|
+
import { simpleGit } from "simple-git";
|
|
7
|
+
import { CONTENTRAIN_BRANCH, LOCAL_CAPABILITIES } from "@contentrain/types";
|
|
8
|
+
import { mergeBranch } from "@contentrain/mcp/git/transaction";
|
|
9
|
+
import { branchDiff, checkBranchHealth } from "@contentrain/mcp/git/branch-lifecycle";
|
|
10
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
11
|
+
import { readFile, unlink, writeFile } from "node:fs/promises";
|
|
12
|
+
import { createApp, createError, createRouter, defineEventHandler, getQuery, getRouterParam, readBody, toNodeListener } from "h3";
|
|
13
|
+
import { WebSocketServer } from "ws";
|
|
13
14
|
import { watch as watch$1 } from "chokidar";
|
|
15
|
+
import { z } from "zod";
|
|
16
|
+
//#region src/serve/schemas.ts
|
|
17
|
+
/**
|
|
18
|
+
* Zod schemas for `contentrain serve` HTTP write routes.
|
|
19
|
+
*
|
|
20
|
+
* Each schema validates the request body BEFORE it reaches the MCP
|
|
21
|
+
* tool layer. Catches malformed payloads (wrong types, extra fields,
|
|
22
|
+
* path-traversal candidates) with structured 400 errors instead of
|
|
23
|
+
* propagating them into git operations where they would be much
|
|
24
|
+
* harder to diagnose.
|
|
25
|
+
*
|
|
26
|
+
* Schemas are intentionally conservative — they match the loosest
|
|
27
|
+
* shape the underlying MCP tool accepts. A request that passes the
|
|
28
|
+
* schema is guaranteed to at least REACH the MCP tool; whether the
|
|
29
|
+
* tool accepts it (e.g. for domain-specific validation) is a separate
|
|
30
|
+
* concern surfaced as an MCP-level error.
|
|
31
|
+
*/
|
|
32
|
+
const branchNameRegex = /^cr\/[a-z0-9-_/]+(?:\/[a-z0-9-]+)*\/\d+-[a-z0-9]+$/u;
|
|
33
|
+
/** `cr/*` feature branch name. Legacy `contentrain/*` is explicitly rejected — old branches are auto-migrated on init. */
|
|
34
|
+
const BranchNameSchema = z.string().min(1, "Branch name is required").regex(/^cr\//u, "Branch must start with \"cr/\"").refine((n) => branchNameRegex.test(n) || n.startsWith("cr/"), { message: "Invalid branch name format" });
|
|
35
|
+
/** Body for `/api/content/:modelId/:entryId/fix`. */
|
|
36
|
+
const ContentFixBodySchema = z.object({
|
|
37
|
+
locale: z.string().min(1).optional(),
|
|
38
|
+
data: z.record(z.string(), z.unknown())
|
|
39
|
+
});
|
|
40
|
+
/** Body for `/api/branches/approve` and `/api/branches/reject` and `/api/normalize/approve`. */
|
|
41
|
+
const BranchActionBodySchema = z.object({ branch: BranchNameSchema });
|
|
42
|
+
/** Body for `/api/normalize/apply` — mirrors contentrain_apply tool input, slimmed. */
|
|
43
|
+
const NormalizeApplyBodySchema = z.object({
|
|
44
|
+
mode: z.enum(["extract", "reuse"]),
|
|
45
|
+
dry_run: z.boolean().optional().default(true),
|
|
46
|
+
extractions: z.array(z.unknown()).optional(),
|
|
47
|
+
scope: z.object({
|
|
48
|
+
model: z.string().optional(),
|
|
49
|
+
domain: z.string().optional()
|
|
50
|
+
}).optional(),
|
|
51
|
+
patches: z.array(z.unknown()).optional()
|
|
52
|
+
}).passthrough();
|
|
53
|
+
/** Body for `/api/normalize/plan/approve`. */
|
|
54
|
+
const NormalizePlanApproveBodySchema = z.object({ models: z.array(z.string()).optional() });
|
|
55
|
+
/**
|
|
56
|
+
* Body for `/api/normalize/plan/reject`. Currently the route only
|
|
57
|
+
* deletes the plan file, but we validate the body shape anyway so
|
|
58
|
+
* any future caller that wants to record a rejection reason has a
|
|
59
|
+
* well-defined contract. Both an empty body and `{ reason? }` pass.
|
|
60
|
+
*/
|
|
61
|
+
const NormalizePlanRejectBodySchema = z.object({ reason: z.string().max(500).optional() }).optional();
|
|
62
|
+
/** Query params for `/api/normalize/file-context`. */
|
|
63
|
+
const FileContextQuerySchema = z.object({
|
|
64
|
+
file: z.string().min(1).refine((p) => !p.includes(".."), { message: "Path traversal segments (\"..\") are not allowed" }).refine((p) => !p.startsWith("/") && !/^[a-zA-Z]:/u.test(p), { message: "Absolute paths are not allowed" }),
|
|
65
|
+
line: z.coerce.number().int().min(1).optional(),
|
|
66
|
+
range: z.coerce.number().int().min(0).max(50).optional()
|
|
67
|
+
});
|
|
68
|
+
/**
|
|
69
|
+
* Parse a body with a schema and either return the typed value or
|
|
70
|
+
* throw an h3-compatible 400 error. Keeps route handlers readable.
|
|
71
|
+
*
|
|
72
|
+
* Generic constraint is `z.ZodTypeAny` so the inferred type propagates
|
|
73
|
+
* as `z.infer<T>` rather than the looser `T` that `z.ZodType<T>`
|
|
74
|
+
* infers to at call sites.
|
|
75
|
+
*/
|
|
76
|
+
function parseOrThrow(schema, value, kind = "body") {
|
|
77
|
+
const result = schema.safeParse(value);
|
|
78
|
+
if (!result.success) {
|
|
79
|
+
const details = result.error.issues.map((i) => `${i.path.join(".") || "<root>"}: ${i.message}`).join("; ");
|
|
80
|
+
const err = /* @__PURE__ */ new Error(`Invalid ${kind}: ${details}`);
|
|
81
|
+
err.statusCode = 400;
|
|
82
|
+
err.data = { issues: result.error.issues };
|
|
83
|
+
throw err;
|
|
84
|
+
}
|
|
85
|
+
return result.data;
|
|
86
|
+
}
|
|
87
|
+
//#endregion
|
|
14
88
|
//#region src/serve/server.ts
|
|
15
89
|
function apiError(error) {
|
|
16
90
|
if (error && typeof error === "object" && "statusCode" in error) {
|
|
@@ -36,6 +110,8 @@ async function createServeApp(options) {
|
|
|
36
110
|
} catch {}
|
|
37
111
|
return "main";
|
|
38
112
|
}
|
|
113
|
+
const syncWarnings = /* @__PURE__ */ new Map();
|
|
114
|
+
const SYNC_WARNING_TTL_MS = 3600 * 1e3;
|
|
39
115
|
const mcpServer = createServer(projectRoot);
|
|
40
116
|
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
|
41
117
|
const mcpClient = new Client({
|
|
@@ -137,21 +213,79 @@ async function createServeApp(options) {
|
|
|
137
213
|
modelId: parts[1],
|
|
138
214
|
locale: parts[2]?.replace(".json", "")
|
|
139
215
|
};
|
|
216
|
+
} else if (rel.startsWith("meta/") && rel.endsWith(".json")) {
|
|
217
|
+
const parts = rel.replace("meta/", "").split("/");
|
|
218
|
+
const locale = parts[parts.length - 1]?.replace(".json", "");
|
|
219
|
+
const modelId = parts[0];
|
|
220
|
+
const entryId = parts.length === 3 ? parts[1] : void 0;
|
|
221
|
+
event = entryId ? {
|
|
222
|
+
type: "meta:changed",
|
|
223
|
+
modelId,
|
|
224
|
+
entryId,
|
|
225
|
+
locale
|
|
226
|
+
} : {
|
|
227
|
+
type: "meta:changed",
|
|
228
|
+
modelId,
|
|
229
|
+
locale
|
|
230
|
+
};
|
|
140
231
|
} else return;
|
|
141
232
|
pendingEvents.push(event);
|
|
142
233
|
if (debounceTimer) clearTimeout(debounceTimer);
|
|
143
234
|
debounceTimer = setTimeout(flush, 300);
|
|
144
235
|
});
|
|
236
|
+
watcher.on("error", (err) => {
|
|
237
|
+
broadcast({
|
|
238
|
+
type: "file-watch:error",
|
|
239
|
+
message: err instanceof Error ? err.message : String(err),
|
|
240
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
241
|
+
});
|
|
242
|
+
});
|
|
145
243
|
}
|
|
146
244
|
router.add("/api/status", defineEventHandler(async () => {
|
|
147
245
|
return await callTool("contentrain_status");
|
|
148
246
|
}));
|
|
247
|
+
router.add("/api/capabilities", defineEventHandler(async () => {
|
|
248
|
+
const config = await readConfig(projectRoot).catch(() => null);
|
|
249
|
+
const health = await checkBranchHealth(projectRoot).catch(() => null);
|
|
250
|
+
return {
|
|
251
|
+
version: 1,
|
|
252
|
+
provider: {
|
|
253
|
+
type: "local",
|
|
254
|
+
repo: config?.repository ?? null
|
|
255
|
+
},
|
|
256
|
+
transport: "stdio",
|
|
257
|
+
capabilities: LOCAL_CAPABILITIES,
|
|
258
|
+
contentBranch: CONTENTRAIN_BRANCH,
|
|
259
|
+
defaultBranch: getDefaultBranch(),
|
|
260
|
+
branchHealth: health
|
|
261
|
+
};
|
|
262
|
+
}));
|
|
263
|
+
router.add("/api/branches/:name/sync-status", defineEventHandler(async (event) => {
|
|
264
|
+
const name = getRouterParam(event, "name");
|
|
265
|
+
if (!name) throw createError({
|
|
266
|
+
statusCode: 400,
|
|
267
|
+
message: "Missing branch name"
|
|
268
|
+
});
|
|
269
|
+
const warning = syncWarnings.get(name);
|
|
270
|
+
if (warning && Date.now() - warning.recordedAt > SYNC_WARNING_TTL_MS) {
|
|
271
|
+
syncWarnings.delete(name);
|
|
272
|
+
return { warning: null };
|
|
273
|
+
}
|
|
274
|
+
return { warning: warning ?? null };
|
|
275
|
+
}));
|
|
149
276
|
router.add("/api/describe/:modelId", defineEventHandler(async (event) => {
|
|
150
277
|
return await callTool("contentrain_describe", {
|
|
151
278
|
model: getRouterParam(event, "modelId"),
|
|
152
279
|
include_sample: true
|
|
153
280
|
});
|
|
154
281
|
}));
|
|
282
|
+
router.add("/api/describe-format", defineEventHandler(async () => {
|
|
283
|
+
return await callTool("contentrain_describe_format");
|
|
284
|
+
}));
|
|
285
|
+
router.add("/api/doctor", defineEventHandler(async (event) => {
|
|
286
|
+
const query = getQuery(event);
|
|
287
|
+
return await callTool("contentrain_doctor", { usage: query["usage"] === "true" || query["usage"] === "1" });
|
|
288
|
+
}));
|
|
155
289
|
router.add("/api/content/:modelId", defineEventHandler(async (event) => {
|
|
156
290
|
const modelId = getRouterParam(event, "modelId");
|
|
157
291
|
const query = getQuery(event);
|
|
@@ -179,7 +313,7 @@ async function createServeApp(options) {
|
|
|
179
313
|
});
|
|
180
314
|
const modelId = getRouterParam(event, "modelId");
|
|
181
315
|
const entryId = getRouterParam(event, "entryId");
|
|
182
|
-
const body = await readBody(event);
|
|
316
|
+
const body = parseOrThrow(ContentFixBodySchema, await readBody(event));
|
|
183
317
|
const result = await callTool("contentrain_content_save", {
|
|
184
318
|
model: modelId,
|
|
185
319
|
entries: [{
|
|
@@ -221,13 +355,86 @@ async function createServeApp(options) {
|
|
|
221
355
|
statusCode: 400,
|
|
222
356
|
message: "Invalid branch name"
|
|
223
357
|
});
|
|
358
|
+
const result = await branchDiff(projectRoot, { branch: branchName });
|
|
359
|
+
return {
|
|
360
|
+
branch: result.branch,
|
|
361
|
+
base: result.base,
|
|
362
|
+
stat: result.stat,
|
|
363
|
+
diff: result.patch,
|
|
364
|
+
filesChanged: result.filesChanged
|
|
365
|
+
};
|
|
366
|
+
}));
|
|
367
|
+
router.add("/api/preview/merge", defineEventHandler(async (event) => {
|
|
368
|
+
const branchName = getQuery(event)["branch"];
|
|
369
|
+
if (!branchName || !branchName.startsWith("cr/")) throw createError({
|
|
370
|
+
statusCode: 400,
|
|
371
|
+
message: "Missing or invalid `branch` query (must start with \"cr/\")"
|
|
372
|
+
});
|
|
224
373
|
const git = simpleGit(projectRoot);
|
|
225
|
-
|
|
374
|
+
if (!(await git.branchLocal()).all.includes(branchName)) throw createError({
|
|
375
|
+
statusCode: 404,
|
|
376
|
+
message: `Branch "${branchName}" does not exist locally`
|
|
377
|
+
});
|
|
378
|
+
let alreadyMerged = false;
|
|
379
|
+
let canFastForward = false;
|
|
380
|
+
try {
|
|
381
|
+
await git.raw([
|
|
382
|
+
"merge-base",
|
|
383
|
+
"--is-ancestor",
|
|
384
|
+
branchName,
|
|
385
|
+
CONTENTRAIN_BRANCH
|
|
386
|
+
]);
|
|
387
|
+
alreadyMerged = true;
|
|
388
|
+
} catch {}
|
|
389
|
+
if (!alreadyMerged) try {
|
|
390
|
+
await git.raw([
|
|
391
|
+
"merge-base",
|
|
392
|
+
"--is-ancestor",
|
|
393
|
+
CONTENTRAIN_BRANCH,
|
|
394
|
+
branchName
|
|
395
|
+
]);
|
|
396
|
+
canFastForward = true;
|
|
397
|
+
} catch {}
|
|
398
|
+
let conflicts = null;
|
|
399
|
+
try {
|
|
400
|
+
const mergeBase = (await git.raw([
|
|
401
|
+
"merge-base",
|
|
402
|
+
CONTENTRAIN_BRANCH,
|
|
403
|
+
branchName
|
|
404
|
+
])).trim();
|
|
405
|
+
if (mergeBase) {
|
|
406
|
+
const out = await git.raw([
|
|
407
|
+
"merge-tree",
|
|
408
|
+
mergeBase,
|
|
409
|
+
CONTENTRAIN_BRANCH,
|
|
410
|
+
branchName
|
|
411
|
+
]);
|
|
412
|
+
const paths = /* @__PURE__ */ new Set();
|
|
413
|
+
const conflictBlockRegex = /^(?:changed in both|added in both|removed in (?:local|remote))\b[^\n]*\n((?:[ \t][^\n]*\n)+)/gmu;
|
|
414
|
+
let match;
|
|
415
|
+
while ((match = conflictBlockRegex.exec(out)) !== null) {
|
|
416
|
+
const block = match[1] ?? "";
|
|
417
|
+
const pathMatch = /\b100644\s+[0-9a-f]{4,}\s+(\S+)/u.exec(block);
|
|
418
|
+
if (pathMatch?.[1]) paths.add(pathMatch[1]);
|
|
419
|
+
}
|
|
420
|
+
conflicts = [...paths];
|
|
421
|
+
}
|
|
422
|
+
} catch {}
|
|
423
|
+
let stat = "";
|
|
424
|
+
let filesChanged = 0;
|
|
425
|
+
if (!alreadyMerged) try {
|
|
426
|
+
const diff = await branchDiff(projectRoot, { branch: branchName });
|
|
427
|
+
stat = diff.stat;
|
|
428
|
+
filesChanged = diff.filesChanged;
|
|
429
|
+
} catch {}
|
|
226
430
|
return {
|
|
227
431
|
branch: branchName,
|
|
228
|
-
base:
|
|
229
|
-
|
|
230
|
-
|
|
432
|
+
base: CONTENTRAIN_BRANCH,
|
|
433
|
+
alreadyMerged,
|
|
434
|
+
canFastForward,
|
|
435
|
+
conflicts,
|
|
436
|
+
filesChanged,
|
|
437
|
+
stat
|
|
231
438
|
};
|
|
232
439
|
}));
|
|
233
440
|
router.add("/api/branches/approve", defineEventHandler(async (event) => {
|
|
@@ -235,37 +442,22 @@ async function createServeApp(options) {
|
|
|
235
442
|
statusCode: 405,
|
|
236
443
|
message: "Method not allowed"
|
|
237
444
|
});
|
|
238
|
-
const branchName = (await readBody(event))
|
|
239
|
-
if (!branchName?.startsWith("cr/")) throw createError({
|
|
240
|
-
statusCode: 400,
|
|
241
|
-
message: "Invalid branch name"
|
|
242
|
-
});
|
|
243
|
-
const git = simpleGit(projectRoot);
|
|
244
|
-
const baseBranch = getDefaultBranch();
|
|
245
|
-
if (!(await git.branchLocal()).all.includes(CONTENTRAIN_BRANCH)) await git.branch([CONTENTRAIN_BRANCH, baseBranch]);
|
|
246
|
-
const mergePath = join(tmpdir(), `cr-merge-${randomUUID()}`);
|
|
247
|
-
await git.raw([
|
|
248
|
-
"worktree",
|
|
249
|
-
"add",
|
|
250
|
-
mergePath,
|
|
251
|
-
CONTENTRAIN_BRANCH
|
|
252
|
-
]);
|
|
253
|
-
const mergeGit = simpleGit(mergePath);
|
|
445
|
+
const { branch: branchName } = parseOrThrow(BranchActionBodySchema, await readBody(event));
|
|
254
446
|
try {
|
|
255
|
-
await
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
447
|
+
const result = await mergeBranch(projectRoot, branchName);
|
|
448
|
+
if (result.sync?.skipped?.length) {
|
|
449
|
+
syncWarnings.set(branchName, {
|
|
450
|
+
branch: branchName,
|
|
451
|
+
skipped: result.sync.skipped,
|
|
452
|
+
synced: result.sync.synced,
|
|
453
|
+
recordedAt: Date.now()
|
|
454
|
+
});
|
|
455
|
+
broadcast({
|
|
456
|
+
type: "sync:warning",
|
|
457
|
+
branch: branchName,
|
|
458
|
+
skippedCount: result.sync.skipped.length
|
|
459
|
+
});
|
|
460
|
+
}
|
|
269
461
|
broadcast({
|
|
270
462
|
type: "branch:merged",
|
|
271
463
|
branch: branchName
|
|
@@ -273,15 +465,21 @@ async function createServeApp(options) {
|
|
|
273
465
|
return {
|
|
274
466
|
status: "merged",
|
|
275
467
|
branch: branchName,
|
|
276
|
-
|
|
468
|
+
commit: result.commit,
|
|
469
|
+
action: result.action,
|
|
470
|
+
sync: result.sync
|
|
277
471
|
};
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
"
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
472
|
+
} catch (error) {
|
|
473
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
474
|
+
broadcast({
|
|
475
|
+
type: "branch:merge-conflict",
|
|
476
|
+
branch: branchName,
|
|
477
|
+
message
|
|
478
|
+
});
|
|
479
|
+
throw createError({
|
|
480
|
+
statusCode: 409,
|
|
481
|
+
message: `Merge failed: ${message}`
|
|
482
|
+
});
|
|
285
483
|
}
|
|
286
484
|
}));
|
|
287
485
|
router.add("/api/branches/reject", defineEventHandler(async (event) => {
|
|
@@ -289,12 +487,13 @@ async function createServeApp(options) {
|
|
|
289
487
|
statusCode: 405,
|
|
290
488
|
message: "Method not allowed"
|
|
291
489
|
});
|
|
292
|
-
const branchName = (await readBody(event))
|
|
293
|
-
if (!branchName?.startsWith("cr/")) throw createError({
|
|
294
|
-
statusCode: 400,
|
|
295
|
-
message: "Invalid branch name"
|
|
296
|
-
});
|
|
490
|
+
const { branch: branchName } = parseOrThrow(BranchActionBodySchema, await readBody(event));
|
|
297
491
|
await simpleGit(projectRoot).deleteLocalBranch(branchName, true);
|
|
492
|
+
syncWarnings.delete(branchName);
|
|
493
|
+
broadcast({
|
|
494
|
+
type: "branch:rejected",
|
|
495
|
+
branch: branchName
|
|
496
|
+
});
|
|
298
497
|
return {
|
|
299
498
|
status: "deleted",
|
|
300
499
|
branch: branchName
|
|
@@ -303,7 +502,7 @@ async function createServeApp(options) {
|
|
|
303
502
|
router.add("/api/history", defineEventHandler(async (event) => {
|
|
304
503
|
const query = getQuery(event);
|
|
305
504
|
const limit = Number(query["limit"]) || 50;
|
|
306
|
-
const
|
|
505
|
+
const log = await simpleGit(projectRoot).log({
|
|
307
506
|
maxCount: limit * 2,
|
|
308
507
|
format: {
|
|
309
508
|
hash: "%h",
|
|
@@ -314,9 +513,11 @@ async function createServeApp(options) {
|
|
|
314
513
|
date: "%aI",
|
|
315
514
|
relativeDate: "%ar"
|
|
316
515
|
}
|
|
317
|
-
})
|
|
516
|
+
});
|
|
517
|
+
const mergeBranchPattern = /^Merge branch '(cr\/|contentrain\/)/u;
|
|
518
|
+
const entries = log.all.filter((c) => {
|
|
318
519
|
const msg = c.message ?? "";
|
|
319
|
-
return msg.startsWith("[contentrain]") ||
|
|
520
|
+
return msg.startsWith("[contentrain]") || mergeBranchPattern.test(msg);
|
|
320
521
|
}).slice(0, limit).map((c) => {
|
|
321
522
|
const msg = c.message ?? "";
|
|
322
523
|
let type = "other";
|
|
@@ -330,13 +531,13 @@ async function createServeApp(options) {
|
|
|
330
531
|
} else if (msg.startsWith("[contentrain] content:")) {
|
|
331
532
|
type = "content_save";
|
|
332
533
|
target = msg.replace("[contentrain] content: ", "");
|
|
333
|
-
} else if (msg.startsWith("[contentrain] deleted:")) {
|
|
534
|
+
} else if (msg.startsWith("[contentrain] deleted:") || msg.startsWith("[contentrain] delete")) {
|
|
334
535
|
type = "delete";
|
|
335
|
-
target = msg.replace(
|
|
536
|
+
target = msg.replace(/^\[contentrain\] delete(?:d)?:?\s*/u, "");
|
|
336
537
|
} else if (msg.startsWith("[contentrain] update context")) type = "context_update";
|
|
337
|
-
else if (
|
|
538
|
+
else if (mergeBranchPattern.test(msg)) {
|
|
338
539
|
type = "merge";
|
|
339
|
-
target = msg.replace(
|
|
540
|
+
target = msg.replace(/^Merge branch '/u, "").replace(/'$/u, "");
|
|
340
541
|
} else if (msg.startsWith("[contentrain]")) {
|
|
341
542
|
type = "operation";
|
|
342
543
|
target = msg.replace("[contentrain] ", "");
|
|
@@ -466,6 +667,10 @@ async function createServeApp(options) {
|
|
|
466
667
|
statusCode: 405,
|
|
467
668
|
message: "Method not allowed"
|
|
468
669
|
});
|
|
670
|
+
if (event.method === "POST") {
|
|
671
|
+
const raw = await readBody(event).catch(() => void 0);
|
|
672
|
+
if (raw !== void 0) parseOrThrow(NormalizePlanRejectBodySchema, raw);
|
|
673
|
+
}
|
|
469
674
|
const planPath = join(crDir, "normalize-plan.json");
|
|
470
675
|
if (existsSync(planPath)) await unlink(planPath);
|
|
471
676
|
broadcast({ type: "normalize:plan-updated" });
|
|
@@ -481,9 +686,9 @@ async function createServeApp(options) {
|
|
|
481
686
|
statusCode: 404,
|
|
482
687
|
message: "No normalize plan found"
|
|
483
688
|
});
|
|
484
|
-
const
|
|
485
|
-
const plan = JSON.parse(
|
|
486
|
-
const selectedModels = (await readBody(event))
|
|
689
|
+
const rawPlan = await readFile(planPath, "utf-8");
|
|
690
|
+
const plan = JSON.parse(rawPlan);
|
|
691
|
+
const selectedModels = parseOrThrow(NormalizePlanApproveBodySchema, await readBody(event)).models;
|
|
487
692
|
plan.status = "approved";
|
|
488
693
|
plan.approved_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
489
694
|
if (selectedModels) plan.approved_models = selectedModels;
|
|
@@ -523,6 +728,11 @@ async function createServeApp(options) {
|
|
|
523
728
|
extractions: applyExtractions
|
|
524
729
|
});
|
|
525
730
|
if (existsSync(planPath)) await unlink(planPath);
|
|
731
|
+
const git = result?.["git"];
|
|
732
|
+
if (git?.["branch"] && git?.["action"] === "pending-review") broadcast({
|
|
733
|
+
type: "branch:created",
|
|
734
|
+
branch: String(git["branch"])
|
|
735
|
+
});
|
|
526
736
|
broadcast({ type: "normalize:plan-updated" });
|
|
527
737
|
return result;
|
|
528
738
|
}));
|
|
@@ -531,44 +741,29 @@ async function createServeApp(options) {
|
|
|
531
741
|
statusCode: 405,
|
|
532
742
|
message: "Method not allowed"
|
|
533
743
|
});
|
|
534
|
-
return await callTool("contentrain_apply", await readBody(event));
|
|
744
|
+
return await callTool("contentrain_apply", parseOrThrow(NormalizeApplyBodySchema, await readBody(event)));
|
|
535
745
|
}));
|
|
536
746
|
router.add("/api/normalize/approve", defineEventHandler(async (event) => {
|
|
537
747
|
if (event.method !== "POST") throw createError({
|
|
538
748
|
statusCode: 405,
|
|
539
749
|
message: "Method not allowed"
|
|
540
750
|
});
|
|
541
|
-
const branchName = (await readBody(event))
|
|
542
|
-
if (!branchName?.startsWith("cr/")) throw createError({
|
|
543
|
-
statusCode: 400,
|
|
544
|
-
message: "Invalid branch name"
|
|
545
|
-
});
|
|
546
|
-
const git = simpleGit(projectRoot);
|
|
547
|
-
const baseBranch = getDefaultBranch();
|
|
548
|
-
if (!(await git.branchLocal()).all.includes(CONTENTRAIN_BRANCH)) await git.branch([CONTENTRAIN_BRANCH, baseBranch]);
|
|
549
|
-
const mergePath = join(tmpdir(), `cr-merge-${randomUUID()}`);
|
|
550
|
-
await git.raw([
|
|
551
|
-
"worktree",
|
|
552
|
-
"add",
|
|
553
|
-
mergePath,
|
|
554
|
-
CONTENTRAIN_BRANCH
|
|
555
|
-
]);
|
|
556
|
-
const mergeGit = simpleGit(mergePath);
|
|
751
|
+
const { branch: branchName } = parseOrThrow(BranchActionBodySchema, await readBody(event));
|
|
557
752
|
try {
|
|
558
|
-
await
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
753
|
+
const result = await mergeBranch(projectRoot, branchName);
|
|
754
|
+
if (result.sync?.skipped?.length) {
|
|
755
|
+
syncWarnings.set(branchName, {
|
|
756
|
+
branch: branchName,
|
|
757
|
+
skipped: result.sync.skipped,
|
|
758
|
+
synced: result.sync.synced,
|
|
759
|
+
recordedAt: Date.now()
|
|
760
|
+
});
|
|
761
|
+
broadcast({
|
|
762
|
+
type: "sync:warning",
|
|
763
|
+
branch: branchName,
|
|
764
|
+
skippedCount: result.sync.skipped.length
|
|
765
|
+
});
|
|
766
|
+
}
|
|
572
767
|
broadcast({
|
|
573
768
|
type: "branch:merged",
|
|
574
769
|
branch: branchName
|
|
@@ -576,15 +771,21 @@ async function createServeApp(options) {
|
|
|
576
771
|
return {
|
|
577
772
|
status: "merged",
|
|
578
773
|
branch: branchName,
|
|
579
|
-
|
|
774
|
+
commit: result.commit,
|
|
775
|
+
action: result.action,
|
|
776
|
+
sync: result.sync
|
|
580
777
|
};
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
"
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
778
|
+
} catch (error) {
|
|
779
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
780
|
+
broadcast({
|
|
781
|
+
type: "branch:merge-conflict",
|
|
782
|
+
branch: branchName,
|
|
783
|
+
message
|
|
784
|
+
});
|
|
785
|
+
throw createError({
|
|
786
|
+
statusCode: 409,
|
|
787
|
+
message: `Merge failed: ${message}`
|
|
788
|
+
});
|
|
588
789
|
}
|
|
589
790
|
}));
|
|
590
791
|
router.add("/api/normalize/sources", defineEventHandler(async () => {
|
|
@@ -598,15 +799,15 @@ async function createServeApp(options) {
|
|
|
598
799
|
}
|
|
599
800
|
}));
|
|
600
801
|
router.add("/api/normalize/file-context", defineEventHandler(async (event) => {
|
|
601
|
-
const
|
|
602
|
-
const
|
|
603
|
-
const
|
|
604
|
-
const
|
|
605
|
-
|
|
802
|
+
const { file: filePath, line = 1, range = 5 } = parseOrThrow(FileContextQuerySchema, getQuery(event), "query");
|
|
803
|
+
const fullPath = join(projectRoot, filePath);
|
|
804
|
+
const { resolve: resolvePath, sep } = await import("node:path");
|
|
805
|
+
const resolvedRoot = resolvePath(projectRoot);
|
|
806
|
+
const resolvedFull = resolvePath(fullPath);
|
|
807
|
+
if (!resolvedFull.startsWith(resolvedRoot + sep) && resolvedFull !== resolvedRoot) throw createError({
|
|
606
808
|
statusCode: 400,
|
|
607
|
-
message: "
|
|
809
|
+
message: "Path escapes project root"
|
|
608
810
|
});
|
|
609
|
-
const fullPath = join(projectRoot, filePath);
|
|
610
811
|
if (!existsSync(fullPath)) throw createError({
|
|
611
812
|
statusCode: 404,
|
|
612
813
|
message: "File not found"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { a as createPackageResolver, c as writeMcpConfig, n as IDE_CONFIGS, o as detectIdes, r as MCP_CONFIGS, s as installIdeRulesAndSkills } from "./ide-
|
|
1
|
+
import { i as pc } from "./ui-B8l_LC36.mjs";
|
|
2
|
+
import { r as resolveProjectRoot, t as loadProjectContext } from "./context-OskMGy8y.mjs";
|
|
3
|
+
import { a as createPackageResolver, c as writeMcpConfig, n as IDE_CONFIGS, o as detectIdes, r as MCP_CONFIGS, s as installIdeRulesAndSkills } from "./ide-DPDVYiaq.mjs";
|
|
4
4
|
import { defineCommand } from "citty";
|
|
5
5
|
import { intro, log, outro, spinner } from "@clack/prompts";
|
|
6
6
|
//#region src/commands/setup.ts
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { a as createPackageResolver, n as IDE_CONFIGS, o as detectIdes, s as installIdeRulesAndSkills, t as AGENT_SKILL_NAMES } from "./ide-
|
|
1
|
+
import { i as pc } from "./ui-B8l_LC36.mjs";
|
|
2
|
+
import { r as resolveProjectRoot } from "./context-OskMGy8y.mjs";
|
|
3
|
+
import { a as createPackageResolver, n as IDE_CONFIGS, o as detectIdes, s as installIdeRulesAndSkills, t as AGENT_SKILL_NAMES } from "./ide-DPDVYiaq.mjs";
|
|
4
4
|
import { defineCommand } from "citty";
|
|
5
5
|
import { intro, log, outro, spinner } from "@clack/prompts";
|
|
6
6
|
import { join } from "node:path";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { i as pc, r as formatTable, t as formatCount } from "./ui-
|
|
2
|
-
import { n as resolveStudioClient } from "./client-
|
|
3
|
-
import { t as resolveStudioContext } from "./resolve-context-
|
|
1
|
+
import { i as pc, r as formatTable, t as formatCount } from "./ui-B8l_LC36.mjs";
|
|
2
|
+
import { n as resolveStudioClient } from "./client-C9E9D4mb.mjs";
|
|
3
|
+
import { t as resolveStudioContext } from "./resolve-context-hKVYPniC.mjs";
|
|
4
4
|
import { defineCommand } from "citty";
|
|
5
5
|
import { intro, log, outro, spinner } from "@clack/prompts";
|
|
6
6
|
//#region src/studio/commands/status.ts
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { n as
|
|
2
|
-
import {
|
|
1
|
+
import { i as pc, n as formatPercent, r as formatTable, t as formatCount } from "./ui-B8l_LC36.mjs";
|
|
2
|
+
import { n as requireInitialized, r as resolveProjectRoot, t as loadProjectContext } from "./context-OskMGy8y.mjs";
|
|
3
3
|
import { defineCommand } from "citty";
|
|
4
4
|
import { intro, log, outro } from "@clack/prompts";
|
|
5
|
+
import { countEntries, readModel } from "@contentrain/mcp/core/model-manager";
|
|
5
6
|
import { simpleGit } from "simple-git";
|
|
6
7
|
import { CONTENTRAIN_BRANCH } from "@contentrain/types";
|
|
7
|
-
import {
|
|
8
|
+
import { checkBranchHealth } from "@contentrain/mcp/git/branch-lifecycle";
|
|
8
9
|
import { validateProject } from "@contentrain/mcp/core/validator";
|
|
9
10
|
//#region src/commands/status.ts
|
|
10
11
|
var status_default = defineCommand({
|
|
@@ -48,6 +49,17 @@ var status_default = defineCommand({
|
|
|
48
49
|
const branches = await git.branch(["--list", "cr/*"]);
|
|
49
50
|
const allLocal = await git.branchLocal();
|
|
50
51
|
jsonResult["pending_branches"] = branches.all.filter((b) => b !== CONTENTRAIN_BRANCH);
|
|
52
|
+
try {
|
|
53
|
+
const health = await checkBranchHealth(projectRoot);
|
|
54
|
+
jsonResult["branch_health"] = {
|
|
55
|
+
total: health.total,
|
|
56
|
+
merged: health.merged,
|
|
57
|
+
unmerged: health.unmerged,
|
|
58
|
+
warning: health.warning,
|
|
59
|
+
blocked: health.blocked,
|
|
60
|
+
message: health.message ?? null
|
|
61
|
+
};
|
|
62
|
+
} catch {}
|
|
51
63
|
const contentBranchExists = allLocal.all.includes(CONTENTRAIN_BRANCH);
|
|
52
64
|
const contentBranchInfo = { exists: contentBranchExists };
|
|
53
65
|
if (contentBranchExists) try {
|
|
@@ -126,14 +138,15 @@ var status_default = defineCommand({
|
|
|
126
138
|
} catch {}
|
|
127
139
|
const featureBranches = (await git.branch(["--list", "cr/*"])).all.filter((b) => b !== CONTENTRAIN_BRANCH);
|
|
128
140
|
if (featureBranches.length > 0) {
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
log.
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
log.
|
|
136
|
-
|
|
141
|
+
const health = await checkBranchHealth(projectRoot).catch(() => null);
|
|
142
|
+
const headerLabel = health ? `\nPending branches (${health.unmerged} unmerged / ${health.total} total)` : `\nPending branches (${featureBranches.length})`;
|
|
143
|
+
if (health?.blocked) {
|
|
144
|
+
log.error(pc.bold(headerLabel));
|
|
145
|
+
log.message(` ${health.message}`);
|
|
146
|
+
} else if (health?.warning) {
|
|
147
|
+
log.warning(pc.bold(headerLabel));
|
|
148
|
+
log.message(` ${health.message}`);
|
|
149
|
+
} else log.info(pc.bold(headerLabel));
|
|
137
150
|
for (const branch of featureBranches) log.message(` ${pc.yellow("●")} ${branch}`);
|
|
138
151
|
log.message(` Run ${pc.cyan("contentrain diff")} to review.`);
|
|
139
152
|
}
|