@imenam/mcp-documentor 1.0.84 → 1.0.86
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 +323 -329
- package/dist/shared/read-only.js +1 -0
- package/dist/src/cli/setup-mcp.js +2 -5
- package/dist/src/core/code-source.d.ts +33 -22
- package/dist/src/core/code-source.js +92 -69
- package/dist/src/core/github-code-api.d.ts +28 -0
- package/dist/src/core/github-code-api.js +92 -31
- package/dist/src/core/local-state.d.ts +49 -0
- package/dist/src/core/local-state.js +78 -0
- package/dist/src/core/migrate-code-sources.d.ts +17 -0
- package/dist/src/core/migrate-code-sources.js +157 -0
- package/dist/src/core/path-resolver.d.ts +29 -9
- package/dist/src/core/path-resolver.js +18 -42
- package/dist/src/core/project-settings.d.ts +21 -0
- package/dist/src/core/project-settings.js +63 -0
- package/dist/src/gui-worker.js +72 -3
- package/dist/src/index.js +160 -7
- package/dist/src/tools/check-freshness.d.ts +2 -1
- package/dist/src/tools/check-freshness.js +1 -2
- package/dist/src/tools/code-sources.d.ts +102 -0
- package/dist/src/tools/code-sources.js +260 -0
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +16 -2
- package/gui/dist/assets/{arc-CdXRzMBT.js → arc-DBgCQU2O.js} +1 -1
- package/gui/dist/assets/{architectureDiagram-3BPJPVTR-CGUnwe_I.js → architectureDiagram-3BPJPVTR-BIPBAzZr.js} +1 -1
- package/gui/dist/assets/{blockDiagram-GPEHLZMM-BCI5_vQE.js → blockDiagram-GPEHLZMM-BE9WwuyF.js} +1 -1
- package/gui/dist/assets/{c4Diagram-AAUBKEIU-Bm9a6HGg.js → c4Diagram-AAUBKEIU-V1_4ktIt.js} +1 -1
- package/gui/dist/assets/channel-B_cci0fU.js +1 -0
- package/gui/dist/assets/{chunk-2J33WTMH-c7xNwExL.js → chunk-2J33WTMH-DskG-Kmf.js} +1 -1
- package/gui/dist/assets/{chunk-4BX2VUAB-CmcU4Q0o.js → chunk-4BX2VUAB-DaPhidEI.js} +1 -1
- package/gui/dist/assets/{chunk-55IACEB6-K8rRjNKN.js → chunk-55IACEB6-C9r6WDFM.js} +1 -1
- package/gui/dist/assets/{chunk-727SXJPM-DMPnL1bv.js → chunk-727SXJPM-BA-aC8US.js} +1 -1
- package/gui/dist/assets/{chunk-AQP2D5EJ-BeRIIft8.js → chunk-AQP2D5EJ-ByBp3whV.js} +1 -1
- package/gui/dist/assets/{chunk-FMBD7UC4-CN4It-fp.js → chunk-FMBD7UC4-CzqUByx7.js} +1 -1
- package/gui/dist/assets/{chunk-ND2GUHAM-Dy-ZqPua.js → chunk-ND2GUHAM-C0_B2ryQ.js} +1 -1
- package/gui/dist/assets/{chunk-QZHKN3VN-CaV4seJw.js → chunk-QZHKN3VN-B3eEwjDh.js} +1 -1
- package/gui/dist/assets/classDiagram-4FO5ZUOK-BqlcJ5BP.js +1 -0
- package/gui/dist/assets/classDiagram-v2-Q7XG4LA2-BqlcJ5BP.js +1 -0
- package/gui/dist/assets/{cose-bilkent-S5V4N54A-B1qt_j1Z.js → cose-bilkent-S5V4N54A-D_i0I2pU.js} +1 -1
- package/gui/dist/assets/{dagre-BM42HDAG-4x5JEBQG.js → dagre-BM42HDAG-DtEVPK_z.js} +1 -1
- package/gui/dist/assets/{diagram-2AECGRRQ-Q7TvjRjv.js → diagram-2AECGRRQ-BftnXa5w.js} +1 -1
- package/gui/dist/assets/{diagram-5GNKFQAL-Dguj26bf.js → diagram-5GNKFQAL-Dh9-kl_u.js} +1 -1
- package/gui/dist/assets/{diagram-KO2AKTUF-DBv6MoZF.js → diagram-KO2AKTUF-CvB0BWCa.js} +1 -1
- package/gui/dist/assets/{diagram-LMA3HP47-mfVXwL5q.js → diagram-LMA3HP47-CDy-J1uy.js} +1 -1
- package/gui/dist/assets/{diagram-OG6HWLK6-Bn3_yQWG.js → diagram-OG6HWLK6-iY9FwkIF.js} +1 -1
- package/gui/dist/assets/{erDiagram-TEJ5UH35-Bp4wnBw7.js → erDiagram-TEJ5UH35-BT2_IX8D.js} +1 -1
- package/gui/dist/assets/{flowDiagram-I6XJVG4X-CGk72E1Q.js → flowDiagram-I6XJVG4X-DaixWyMx.js} +1 -1
- package/gui/dist/assets/{ganttDiagram-6RSMTGT7-C1WQIgpu.js → ganttDiagram-6RSMTGT7-DSo3cQPH.js} +1 -1
- package/gui/dist/assets/{gitGraphDiagram-PVQCEYII-DSIs1ZqI.js → gitGraphDiagram-PVQCEYII-BfFT7Lds.js} +1 -1
- package/gui/dist/assets/index-CxCC1gxu.css +1 -0
- package/gui/dist/assets/{index-T7ZLHNgs.js → index-DPGmAc6z.js} +107 -105
- package/gui/dist/assets/{infoDiagram-5YYISTIA-DzHdfk5Y.js → infoDiagram-5YYISTIA-BxbxNxe2.js} +1 -1
- package/gui/dist/assets/{ishikawaDiagram-YF4QCWOH-DLxqTmDU.js → ishikawaDiagram-YF4QCWOH-DpopzqZr.js} +1 -1
- package/gui/dist/assets/{journeyDiagram-JHISSGLW-DYUPmZnf.js → journeyDiagram-JHISSGLW-DrOlFzIk.js} +1 -1
- package/gui/dist/assets/{kanban-definition-UN3LZRKU-f6km2MDT.js → kanban-definition-UN3LZRKU-vH81lYXy.js} +1 -1
- package/gui/dist/assets/{linear-DHhAaAXH.js → linear-BipFpFah.js} +1 -1
- package/gui/dist/assets/{mindmap-definition-RKZ34NQL-nergVxV3.js → mindmap-definition-RKZ34NQL-DsTxCLgj.js} +1 -1
- package/gui/dist/assets/{pieDiagram-4H26LBE5-D4IJAdGz.js → pieDiagram-4H26LBE5-Bbq2ZWQU.js} +1 -1
- package/gui/dist/assets/{quadrantDiagram-W4KKPZXB-B5yYdOsV.js → quadrantDiagram-W4KKPZXB-BdTSaEo4.js} +1 -1
- package/gui/dist/assets/{requirementDiagram-4Y6WPE33-BrhJp07G.js → requirementDiagram-4Y6WPE33-BndI8YZy.js} +1 -1
- package/gui/dist/assets/{sankeyDiagram-5OEKKPKP-CUBA4e7F.js → sankeyDiagram-5OEKKPKP-ofBkVITf.js} +1 -1
- package/gui/dist/assets/{sequenceDiagram-3UESZ5HK-DP0-rUie.js → sequenceDiagram-3UESZ5HK-Cjj2UrN9.js} +1 -1
- package/gui/dist/assets/{stateDiagram-AJRCARHV-_BnVcpam.js → stateDiagram-AJRCARHV-CzPrXUuz.js} +1 -1
- package/gui/dist/assets/stateDiagram-v2-BHNVJYJU-DAq4ObN9.js +1 -0
- package/gui/dist/assets/{timeline-definition-PNZ67QCA-crbYjsrq.js → timeline-definition-PNZ67QCA-Cz4X-IOO.js} +1 -1
- package/gui/dist/assets/{vennDiagram-CIIHVFJN-EOqENtE1.js → vennDiagram-CIIHVFJN-BwIgXJcZ.js} +1 -1
- package/gui/dist/assets/{wardley-L42UT6IY-B1FVq-_Y.js → wardley-L42UT6IY-keQS0aNZ.js} +1 -1
- package/gui/dist/assets/{wardleyDiagram-YWT4CUSO-D1DZwIH_.js → wardleyDiagram-YWT4CUSO-DhykSgbD.js} +1 -1
- package/gui/dist/assets/{xychartDiagram-2RQKCTM6-B6Wr8_OJ.js → xychartDiagram-2RQKCTM6-DFTqc4cj.js} +1 -1
- package/gui/dist/index.html +2 -2
- package/package.json +3 -4
- package/scripts/migrate-paths-to-relative.js +196 -192
- package/gui/dist/assets/channel-f0BgDIcS.js +0 -1
- package/gui/dist/assets/classDiagram-4FO5ZUOK-tGvt4n1B.js +0 -1
- package/gui/dist/assets/classDiagram-v2-Q7XG4LA2-tGvt4n1B.js +0 -1
- package/gui/dist/assets/index-QkCDSQ2A.css +0 -1
- package/gui/dist/assets/stateDiagram-v2-BHNVJYJU-B5Vwq56z.js +0 -1
- package/scripts/generate-version.js +0 -20
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { createLogger } from "@imenam/mcp-gui-interface";
|
|
2
|
+
import { parseRepoSpec } from "./github-code-api.js";
|
|
3
|
+
import { hasCodeSourceConfig, writeCodeSourceConfig } from "./project-settings.js";
|
|
4
|
+
const logger = createLogger("MigrateCodeSources");
|
|
5
|
+
const REPO_VAR = "MCP_DOCUMENTOR_CODE_REPO";
|
|
6
|
+
const BRANCH_VAR = "MCP_DOCUMENTOR_CODE_BRANCH";
|
|
7
|
+
const ROOTS_VAR = "MCP_DOCUMENTOR_CODE_ROOTS";
|
|
8
|
+
const ROOT_VAR = "MCP_DOCUMENTOR_CODE_ROOT";
|
|
9
|
+
const short = (name) => name.replace(/^MCP_DOCUMENTOR_/, "");
|
|
10
|
+
function readVar(name) {
|
|
11
|
+
return process.env[name]?.trim() || undefined;
|
|
12
|
+
}
|
|
13
|
+
/** Entries of a JSON map variable, keeping non-empty string values only. */
|
|
14
|
+
function readJsonMap(name, raw) {
|
|
15
|
+
let parsed;
|
|
16
|
+
try {
|
|
17
|
+
parsed = JSON.parse(raw);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
throw new Error(`${name} looks like a JSON map but could not be parsed`);
|
|
21
|
+
}
|
|
22
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
23
|
+
throw new Error(`${name} is not a JSON object`);
|
|
24
|
+
}
|
|
25
|
+
return Object.entries(parsed)
|
|
26
|
+
.filter((entry) => typeof entry[1] === "string" && entry[1].trim() !== "")
|
|
27
|
+
.map(([project, value]) => [project, value.trim()]);
|
|
28
|
+
}
|
|
29
|
+
/** The (project, spec) pairs MCP_DOCUMENTOR_CODE_REPO declares: one spec for all, or a JSON map. */
|
|
30
|
+
function readRepoSpecs(raw, projects) {
|
|
31
|
+
if (!raw.startsWith("{"))
|
|
32
|
+
return projects.map((project) => [project, raw]);
|
|
33
|
+
return readJsonMap(REPO_VAR, raw);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* MCP_DOCUMENTOR_CODE_ROOT seeds the default code root of a data directory that has none — the
|
|
37
|
+
* setup writes it with the repository's folder. Once a default root is stored, the variable is
|
|
38
|
+
* not read again: a different value is only logged.
|
|
39
|
+
*/
|
|
40
|
+
function seedDefaultCodeRoot(localState) {
|
|
41
|
+
const envRoot = readVar(ROOT_VAR);
|
|
42
|
+
if (!envRoot)
|
|
43
|
+
return undefined;
|
|
44
|
+
const stored = localState.getCodeRoots().default;
|
|
45
|
+
if (stored === undefined) {
|
|
46
|
+
localState.setDefaultCodeRoot(envRoot);
|
|
47
|
+
logger.info(`Default code root set from ${ROOT_VAR}: ${envRoot}`);
|
|
48
|
+
return { kind: "code-root", status: "imported", detail: `Default code root · ${envRoot}`, from: short(ROOT_VAR) };
|
|
49
|
+
}
|
|
50
|
+
if (stored !== envRoot) {
|
|
51
|
+
logger.info(`${ROOT_VAR} (${envRoot}) is ignored: the default code root of this machine is ${stored}.`);
|
|
52
|
+
}
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
function migrateRepos(storageEngine, projects, entries) {
|
|
56
|
+
const repoRaw = readVar(REPO_VAR);
|
|
57
|
+
if (!repoRaw)
|
|
58
|
+
return;
|
|
59
|
+
const defaultBranch = readVar(BRANCH_VAR);
|
|
60
|
+
let specs = [];
|
|
61
|
+
try {
|
|
62
|
+
specs = readRepoSpecs(repoRaw, projects);
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
entries.push({ kind: "github", status: "error", detail: `Not imported — ${err.message}`, from: short(REPO_VAR) });
|
|
66
|
+
}
|
|
67
|
+
for (const [project, spec] of specs) {
|
|
68
|
+
if (!projects.includes(project)) {
|
|
69
|
+
entries.push({ project, kind: "github", status: "skipped", detail: "Not imported — no such project", from: short(REPO_VAR) });
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
const projectDir = storageEngine.getProjectDir(project);
|
|
73
|
+
if (hasCodeSourceConfig(projectDir)) {
|
|
74
|
+
entries.push({ project, kind: "github", status: "skipped", detail: "Already configured — kept as is", from: short(REPO_VAR) });
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (spec.toLowerCase() === "local") {
|
|
78
|
+
writeCodeSourceConfig(projectDir, { kind: "local" });
|
|
79
|
+
entries.push({ project, kind: "local", status: "imported", detail: "Local working copy", from: `${short(REPO_VAR)} "local"` });
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
let config;
|
|
83
|
+
let from = short(REPO_VAR);
|
|
84
|
+
try {
|
|
85
|
+
const parsed = parseRepoSpec(spec);
|
|
86
|
+
const branch = parsed.ref ?? defaultBranch;
|
|
87
|
+
if (!parsed.ref && defaultBranch)
|
|
88
|
+
from += ` + ${short(BRANCH_VAR)}`;
|
|
89
|
+
config = { kind: "github", repo: `${parsed.owner}/${parsed.repo}`, ...(branch && { branch }) };
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
entries.push({ project, kind: "github", status: "error", detail: `Not imported — ${err.message}`, from });
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
writeCodeSourceConfig(projectDir, config);
|
|
96
|
+
entries.push({ project, kind: "github", status: "imported", detail: `${config.repo} · ${config.branch ?? "default branch"}`, from });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function migrateCodeRoots(localState, projects, entries) {
|
|
100
|
+
const rootsRaw = readVar(ROOTS_VAR);
|
|
101
|
+
if (!rootsRaw)
|
|
102
|
+
return;
|
|
103
|
+
let roots = [];
|
|
104
|
+
try {
|
|
105
|
+
roots = readJsonMap(ROOTS_VAR, rootsRaw);
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
entries.push({ kind: "code-root", status: "error", detail: `Not imported — ${err.message}`, from: short(ROOTS_VAR) });
|
|
109
|
+
}
|
|
110
|
+
const existing = localState.getCodeRoots().byProject;
|
|
111
|
+
for (const [project, root] of roots) {
|
|
112
|
+
if (!projects.includes(project)) {
|
|
113
|
+
entries.push({ project, kind: "code-root", status: "skipped", detail: "Not imported — no such project", from: short(ROOTS_VAR) });
|
|
114
|
+
}
|
|
115
|
+
else if (existing[project] !== undefined) {
|
|
116
|
+
entries.push({ project, kind: "code-root", status: "skipped", detail: "Code root already set — kept as is", from: short(ROOTS_VAR) });
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
localState.setProjectCodeRoot(project, root);
|
|
120
|
+
entries.push({ project, kind: "code-root", status: "imported", detail: `Code root · ${root}`, from: short(ROOTS_VAR) });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Moves the code-source environment variables into the project settings and the machine-local
|
|
126
|
+
* code roots, once per machine.
|
|
127
|
+
*
|
|
128
|
+
* - MCP_DOCUMENTOR_CODE_REPO (+ MCP_DOCUMENTOR_CODE_BRANCH for specs without "#branch") becomes
|
|
129
|
+
* the code source of each project it names. A project that already has a code source — written
|
|
130
|
+
* by another machine and pulled — keeps it.
|
|
131
|
+
* - MCP_DOCUMENTOR_CODE_ROOTS becomes the code root of each project it names, on this machine.
|
|
132
|
+
* - MCP_DOCUMENTOR_CODE_ROOT seeds the default code root (see `seedDefaultCodeRoot`); it is not
|
|
133
|
+
* one of the variables to remove.
|
|
134
|
+
*
|
|
135
|
+
* The outcome is recorded as a report in the local state; the GUI shows it until acknowledged.
|
|
136
|
+
* Once recorded, the migrated variables are never read again: their presence is only logged.
|
|
137
|
+
*/
|
|
138
|
+
export function migrateCodeSources(storageEngine, localState) {
|
|
139
|
+
const seeded = seedDefaultCodeRoot(localState);
|
|
140
|
+
const present = [REPO_VAR, BRANCH_VAR, ROOTS_VAR].filter((name) => readVar(name) !== undefined);
|
|
141
|
+
if (present.length === 0)
|
|
142
|
+
return;
|
|
143
|
+
if (localState.getMigrationReport()) {
|
|
144
|
+
logger.warn(`${present.join(", ")} ${present.length > 1 ? "are" : "is"} set but no longer read — code sources and code roots ` +
|
|
145
|
+
`are configured per project in the GUI. Remove ${present.length > 1 ? "them" : "it"} from the MCP configuration.`);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const projects = storageEngine.getConfig().projects;
|
|
149
|
+
const entries = [];
|
|
150
|
+
migrateRepos(storageEngine, projects, entries);
|
|
151
|
+
migrateCodeRoots(localState, projects, entries);
|
|
152
|
+
if (seeded)
|
|
153
|
+
entries.push(seeded);
|
|
154
|
+
localState.setMigrationReport({ migratedAt: new Date().toISOString(), entries, variables: present, seen: false });
|
|
155
|
+
logger.info(`Code sources migrated from ${present.join(", ")}: ${entries.filter((e) => e.status === "imported").length} imported, ` +
|
|
156
|
+
`${entries.filter((e) => e.status !== "imported").length} not imported. These variables are no longer read.`);
|
|
157
|
+
}
|
|
@@ -1,20 +1,40 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Resolves tracked-file paths between two representations:
|
|
3
|
+
*
|
|
4
|
+
* - **stored** — what we persist in a doc's `tracked_files[].path`. Portable
|
|
5
|
+
* across machines: relative to the project's "code root" and using forward
|
|
6
|
+
* slashes. Absolute paths may still appear (files located outside any code
|
|
7
|
+
* root) and are handled transparently.
|
|
8
|
+
* - **absolute** — a real filesystem path on the current machine, used for any
|
|
9
|
+
* `fs`/`git` access.
|
|
10
|
+
*
|
|
11
|
+
* The code root is machine-local — each machine clones its repos wherever it
|
|
12
|
+
* wants — so it is read from the machine-local state (a default root, and
|
|
13
|
+
* per-project roots), through the provider installed with `setCodeRootProvider`.
|
|
14
|
+
*
|
|
15
|
+
* Without a code root, paths are stored absolute and only absolute stored paths
|
|
16
|
+
* can be resolved.
|
|
17
|
+
*/
|
|
18
|
+
/** The code root that applies to a project on this machine, or undefined. */
|
|
19
|
+
export type CodeRootProvider = (project: string) => string | undefined;
|
|
20
|
+
export declare function setCodeRootProvider(value: CodeRootProvider): void;
|
|
21
|
+
/** The code root that applies to a project on this machine, or undefined if none is set. */
|
|
4
22
|
export declare function getCodeRoot(project: string): string | undefined;
|
|
5
23
|
/**
|
|
6
24
|
* Converts an input path (typically the absolute path supplied by an agent)
|
|
7
25
|
* into the portable form persisted in a doc.
|
|
8
26
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
27
|
+
* Keeps an absolute path — with a warning — when there is no code root, or when
|
|
28
|
+
* the file lives outside the root (including a different Windows drive).
|
|
11
29
|
*/
|
|
12
30
|
export declare function toStoredPath(inputPath: string, project: string): string;
|
|
13
31
|
/**
|
|
14
|
-
* Resolves a stored path
|
|
32
|
+
* Resolves a stored path against a given code root:
|
|
15
33
|
*
|
|
16
|
-
* - absolute stored path
|
|
17
|
-
* - relative stored path +
|
|
18
|
-
* - relative stored path + no root
|
|
34
|
+
* - absolute stored path → returned as-is
|
|
35
|
+
* - relative stored path + a root → joined with the root
|
|
36
|
+
* - relative stored path + no root → undefined (unverifiable here)
|
|
19
37
|
*/
|
|
38
|
+
export declare function resolveAgainstRoot(stored: string, root: string | undefined): string | undefined;
|
|
39
|
+
/** Resolves a stored path to an absolute filesystem path on this machine. */
|
|
20
40
|
export declare function toAbsolutePath(stored: string, project: string): string | undefined;
|
|
@@ -1,42 +1,15 @@
|
|
|
1
1
|
import path from "path";
|
|
2
2
|
import { createLogger } from "@imenam/mcp-gui-interface";
|
|
3
3
|
const logger = createLogger("PathResolver");
|
|
4
|
-
let
|
|
5
|
-
function
|
|
6
|
-
|
|
7
|
-
return cached;
|
|
8
|
-
const defaultRoot = process.env.MCP_DOCUMENTOR_CODE_ROOT?.trim() || undefined;
|
|
9
|
-
let byProject = {};
|
|
10
|
-
const rawMap = process.env.MCP_DOCUMENTOR_CODE_ROOTS?.trim();
|
|
11
|
-
if (rawMap) {
|
|
12
|
-
try {
|
|
13
|
-
const parsed = JSON.parse(rawMap);
|
|
14
|
-
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
15
|
-
for (const [proj, root] of Object.entries(parsed)) {
|
|
16
|
-
if (typeof root === "string" && root.trim() !== "") {
|
|
17
|
-
byProject[proj] = root.trim();
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
else {
|
|
22
|
-
logger.warn("MCP_DOCUMENTOR_CODE_ROOTS is not a JSON object, ignoring it");
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
catch {
|
|
26
|
-
logger.warn("Failed to parse MCP_DOCUMENTOR_CODE_ROOTS as JSON, ignoring it");
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
cached = { default: defaultRoot, byProject };
|
|
30
|
-
return cached;
|
|
31
|
-
}
|
|
32
|
-
/** Test-only: clears the memoised env snapshot so a changed env is re-read. */
|
|
33
|
-
export function resetCodeRootsCache() {
|
|
34
|
-
cached = undefined;
|
|
4
|
+
let provider;
|
|
5
|
+
export function setCodeRootProvider(value) {
|
|
6
|
+
provider = value;
|
|
35
7
|
}
|
|
36
|
-
/** The
|
|
8
|
+
/** The code root that applies to a project on this machine, or undefined if none is set. */
|
|
37
9
|
export function getCodeRoot(project) {
|
|
38
|
-
|
|
39
|
-
|
|
10
|
+
if (!provider)
|
|
11
|
+
throw new Error("code roots are read before setCodeRootProvider was called");
|
|
12
|
+
return provider(project);
|
|
40
13
|
}
|
|
41
14
|
function toForwardSlashes(p) {
|
|
42
15
|
return p.split(path.sep).join("/");
|
|
@@ -45,8 +18,8 @@ function toForwardSlashes(p) {
|
|
|
45
18
|
* Converts an input path (typically the absolute path supplied by an agent)
|
|
46
19
|
* into the portable form persisted in a doc.
|
|
47
20
|
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
21
|
+
* Keeps an absolute path — with a warning — when there is no code root, or when
|
|
22
|
+
* the file lives outside the root (including a different Windows drive).
|
|
50
23
|
*/
|
|
51
24
|
export function toStoredPath(inputPath, project) {
|
|
52
25
|
const abs = path.resolve(process.cwd(), inputPath);
|
|
@@ -64,19 +37,22 @@ export function toStoredPath(inputPath, project) {
|
|
|
64
37
|
return toForwardSlashes(rel);
|
|
65
38
|
}
|
|
66
39
|
/**
|
|
67
|
-
* Resolves a stored path
|
|
40
|
+
* Resolves a stored path against a given code root:
|
|
68
41
|
*
|
|
69
|
-
* - absolute stored path
|
|
70
|
-
* - relative stored path +
|
|
71
|
-
* - relative stored path + no root
|
|
42
|
+
* - absolute stored path → returned as-is
|
|
43
|
+
* - relative stored path + a root → joined with the root
|
|
44
|
+
* - relative stored path + no root → undefined (unverifiable here)
|
|
72
45
|
*/
|
|
73
|
-
export function
|
|
46
|
+
export function resolveAgainstRoot(stored, root) {
|
|
74
47
|
if (path.isAbsolute(stored)) {
|
|
75
48
|
return stored;
|
|
76
49
|
}
|
|
77
|
-
const root = getCodeRoot(project);
|
|
78
50
|
if (!root) {
|
|
79
51
|
return undefined;
|
|
80
52
|
}
|
|
81
53
|
return path.resolve(root, stored);
|
|
82
54
|
}
|
|
55
|
+
/** Resolves a stored path to an absolute filesystem path on this machine. */
|
|
56
|
+
export function toAbsolutePath(stored, project) {
|
|
57
|
+
return resolveAgainstRoot(stored, getCodeRoot(project));
|
|
58
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Where a project's source code is read from.
|
|
4
|
+
*
|
|
5
|
+
* - `local` — the working copy on this machine, resolved through the code roots.
|
|
6
|
+
* - `github` — one branch of one repository, read over the GitHub API. `branch` undefined means
|
|
7
|
+
* the repository's default branch.
|
|
8
|
+
*/
|
|
9
|
+
export declare const CodeSourceConfigSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
10
|
+
kind: z.ZodLiteral<"local">;
|
|
11
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
12
|
+
kind: z.ZodLiteral<"github">;
|
|
13
|
+
repo: z.ZodString;
|
|
14
|
+
branch: z.ZodOptional<z.ZodString>;
|
|
15
|
+
}, z.core.$strip>], "kind">;
|
|
16
|
+
export type CodeSourceConfig = z.infer<typeof CodeSourceConfigSchema>;
|
|
17
|
+
/** Whether the project has a code source written in its settings. */
|
|
18
|
+
export declare function hasCodeSourceConfig(projectDir: string): boolean;
|
|
19
|
+
/** The project's code source. Throws when the settings file exists but cannot be read. */
|
|
20
|
+
export declare function readCodeSourceConfig(projectDir: string): CodeSourceConfig;
|
|
21
|
+
export declare function writeCodeSourceConfig(projectDir: string, config: CodeSourceConfig): void;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
/**
|
|
5
|
+
* Where a project's source code is read from.
|
|
6
|
+
*
|
|
7
|
+
* - `local` — the working copy on this machine, resolved through the code roots.
|
|
8
|
+
* - `github` — one branch of one repository, read over the GitHub API. `branch` undefined means
|
|
9
|
+
* the repository's default branch.
|
|
10
|
+
*/
|
|
11
|
+
export const CodeSourceConfigSchema = z.discriminatedUnion("kind", [
|
|
12
|
+
z.object({ kind: z.literal("local") }),
|
|
13
|
+
z.object({
|
|
14
|
+
kind: z.literal("github"),
|
|
15
|
+
repo: z.string().regex(/^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/, 'expected "owner/repo"'),
|
|
16
|
+
branch: z.string().min(1).optional(),
|
|
17
|
+
}),
|
|
18
|
+
]);
|
|
19
|
+
const ProjectSettingsSchema = z.object({
|
|
20
|
+
codeSource: CodeSourceConfigSchema.optional(),
|
|
21
|
+
});
|
|
22
|
+
/**
|
|
23
|
+
* Per-project settings, in `projects/<project>/project.json`. The file lives next to the docs, so
|
|
24
|
+
* it is versioned and synchronised with them: every machine reads the same code source.
|
|
25
|
+
*/
|
|
26
|
+
function settingsPath(projectDir) {
|
|
27
|
+
return path.join(projectDir, "project.json");
|
|
28
|
+
}
|
|
29
|
+
/** A project without settings, or without a code source in them, reads its local working copy. */
|
|
30
|
+
const DEFAULT_CODE_SOURCE = { kind: "local" };
|
|
31
|
+
function readSettings(projectDir) {
|
|
32
|
+
const file = settingsPath(projectDir);
|
|
33
|
+
if (!fs.existsSync(file))
|
|
34
|
+
return {};
|
|
35
|
+
let raw;
|
|
36
|
+
try {
|
|
37
|
+
raw = JSON.parse(fs.readFileSync(file, "utf-8"));
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
throw new Error(`${file} is not valid JSON`);
|
|
41
|
+
}
|
|
42
|
+
const parsed = ProjectSettingsSchema.safeParse(raw);
|
|
43
|
+
if (!parsed.success) {
|
|
44
|
+
throw new Error(`${file} is invalid: ${parsed.error.issues.map((i) => `${i.path.join(".")} ${i.message}`).join("; ")}`);
|
|
45
|
+
}
|
|
46
|
+
return parsed.data;
|
|
47
|
+
}
|
|
48
|
+
/** Whether the project has a code source written in its settings. */
|
|
49
|
+
export function hasCodeSourceConfig(projectDir) {
|
|
50
|
+
return readSettings(projectDir).codeSource !== undefined;
|
|
51
|
+
}
|
|
52
|
+
/** The project's code source. Throws when the settings file exists but cannot be read. */
|
|
53
|
+
export function readCodeSourceConfig(projectDir) {
|
|
54
|
+
return readSettings(projectDir).codeSource ?? DEFAULT_CODE_SOURCE;
|
|
55
|
+
}
|
|
56
|
+
export function writeCodeSourceConfig(projectDir, config) {
|
|
57
|
+
const validated = CodeSourceConfigSchema.parse(config);
|
|
58
|
+
const file = settingsPath(projectDir);
|
|
59
|
+
const settings = fs.existsSync(file) ? readSettings(projectDir) : {};
|
|
60
|
+
settings.codeSource = validated;
|
|
61
|
+
fs.mkdirSync(projectDir, { recursive: true });
|
|
62
|
+
fs.writeFileSync(file, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
63
|
+
}
|
package/dist/src/gui-worker.js
CHANGED
|
@@ -2,7 +2,7 @@ import { serve } from "@hono/node-server";
|
|
|
2
2
|
import { serveStatic } from "@hono/node-server/serve-static";
|
|
3
3
|
import { Hono } from "hono";
|
|
4
4
|
import { streamSSE } from "hono/streaming";
|
|
5
|
-
import { ProxyClient } from "@imenam/mcp-gui-interface";
|
|
5
|
+
import { GUI_LISTEN_HOST, ProxyClient, honoGuiGuard } from "@imenam/mcp-gui-interface";
|
|
6
6
|
import { IPCMessageSchema } from "../shared/schemas/doc.schema.js";
|
|
7
7
|
import { isReadOnly } from "../shared/read-only.js";
|
|
8
8
|
import path from "path";
|
|
@@ -125,6 +125,7 @@ async function findSystemBrowser() {
|
|
|
125
125
|
let localPort = 0;
|
|
126
126
|
let guiPublicUrl = '';
|
|
127
127
|
const app = new Hono();
|
|
128
|
+
app.use("*", honoGuiGuard());
|
|
128
129
|
let proxyClient = null;
|
|
129
130
|
const sseControllers = new Set();
|
|
130
131
|
const pendingRequests = new Map();
|
|
@@ -196,7 +197,15 @@ function setupIpcErrorHandlers() {
|
|
|
196
197
|
});
|
|
197
198
|
return;
|
|
198
199
|
}
|
|
199
|
-
|
|
200
|
+
const BROADCAST_TYPES = [
|
|
201
|
+
"INITIAL_STATE",
|
|
202
|
+
"CONFIG_UPDATED",
|
|
203
|
+
"BUNDLES_UPDATED",
|
|
204
|
+
"PROJECTS_CHANGED",
|
|
205
|
+
"DOCS_CHANGED",
|
|
206
|
+
"CODE_SOURCES_CHANGED",
|
|
207
|
+
];
|
|
208
|
+
if (msg && BROADCAST_TYPES.includes(msg.type)) {
|
|
200
209
|
for (const controller of sseControllers) {
|
|
201
210
|
try {
|
|
202
211
|
controller.writeSSE({ data: JSON.stringify(msg), event: "message" });
|
|
@@ -216,6 +225,8 @@ const READ_ONLY_WRITE_ROUTES = [
|
|
|
216
225
|
{ method: "POST", prefix: "/api/images" },
|
|
217
226
|
{ method: "POST", prefix: "/api/configurations/apply" },
|
|
218
227
|
{ method: "POST", prefix: "/api/sync" },
|
|
228
|
+
{ method: "POST", prefix: "/api/code-sources/apply" },
|
|
229
|
+
{ method: "POST", prefix: "/api/code-roots/apply" },
|
|
219
230
|
];
|
|
220
231
|
app.use("*", async (c, next) => {
|
|
221
232
|
if (isReadOnly()) {
|
|
@@ -358,6 +369,64 @@ app.post("/api/sync", async (c) => {
|
|
|
358
369
|
return c.json({ error: result.error }, 500);
|
|
359
370
|
return c.json(result.data);
|
|
360
371
|
});
|
|
372
|
+
// --- Code sources ---
|
|
373
|
+
/** Relays one IPC request and answers with its data, or with its error and `errorStatus`. */
|
|
374
|
+
async function relay(c, type, data, timeout, errorStatus = 500) {
|
|
375
|
+
const result = await ipcRequest(type, data, timeout);
|
|
376
|
+
if (result.error)
|
|
377
|
+
return c.json({ error: result.error }, errorStatus);
|
|
378
|
+
return c.json(result.data);
|
|
379
|
+
}
|
|
380
|
+
async function readJson(c) {
|
|
381
|
+
try {
|
|
382
|
+
return await c.req.json();
|
|
383
|
+
}
|
|
384
|
+
catch {
|
|
385
|
+
return {};
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
// GitHub projects are checked against their branch: one API round trip each.
|
|
389
|
+
app.get("/api/code-sources", (c) => relay(c, "LIST_CODE_SOURCES", {}, 30000));
|
|
390
|
+
app.post("/api/code-sources/inspect", async (c) => {
|
|
391
|
+
const { repo, branch } = await readJson(c);
|
|
392
|
+
if (!repo)
|
|
393
|
+
return c.json({ error: "repo is required" }, 400);
|
|
394
|
+
return relay(c, "INSPECT_REPOSITORY", { repo, branch }, 30000);
|
|
395
|
+
});
|
|
396
|
+
// Reads every tracked file of the project at the previewed source.
|
|
397
|
+
// `codeRoot`: omitted keeps the project's root, "" means the machine default, else the project's own.
|
|
398
|
+
app.post("/api/code-sources/preview", async (c) => {
|
|
399
|
+
const { project, config, codeRoot } = await readJson(c);
|
|
400
|
+
if (!project || !config)
|
|
401
|
+
return c.json({ error: "project and config are required" }, 400);
|
|
402
|
+
return relay(c, "PREVIEW_CODE_SOURCE", { project, config, codeRoot }, 120000, 400);
|
|
403
|
+
});
|
|
404
|
+
app.post("/api/code-sources/apply", async (c) => {
|
|
405
|
+
const { project, config, codeRoot } = await readJson(c);
|
|
406
|
+
if (!project || !config)
|
|
407
|
+
return c.json({ error: "project and config are required" }, 400);
|
|
408
|
+
return relay(c, "SAVE_CODE_SOURCE", { project, config, codeRoot }, 10000, 400);
|
|
409
|
+
});
|
|
410
|
+
app.get("/api/code-roots", (c) => relay(c, "GET_CODE_ROOTS", {}, 5000));
|
|
411
|
+
app.post("/api/code-sources/search", async (c) => {
|
|
412
|
+
const { query } = await readJson(c);
|
|
413
|
+
return relay(c, "SEARCH_REPOSITORIES", { query: query ?? "" }, 15000);
|
|
414
|
+
});
|
|
415
|
+
app.post("/api/code-roots/inspect", async (c) => {
|
|
416
|
+
const { path: root } = await readJson(c);
|
|
417
|
+
return relay(c, "INSPECT_CODE_ROOT", { path: root ?? "" }, 5000);
|
|
418
|
+
});
|
|
419
|
+
// Without a project, sets the machine default root. An empty path removes the root.
|
|
420
|
+
app.post("/api/code-roots/apply", async (c) => {
|
|
421
|
+
const { path: root, project } = await readJson(c);
|
|
422
|
+
if (typeof root !== "string")
|
|
423
|
+
return c.json({ error: "path is required" }, 400);
|
|
424
|
+
return relay(c, "SET_CODE_ROOT", { path: root, project }, 5000, 400);
|
|
425
|
+
});
|
|
426
|
+
// The token is read from MCP_DOCUMENTOR_CODE_TOKEN only: the GUI shows its status, never sets it.
|
|
427
|
+
app.get("/api/github-token", (c) => relay(c, "GET_GITHUB_TOKEN", {}, 15000));
|
|
428
|
+
app.get("/api/code-source-migration", (c) => relay(c, "GET_CODE_SOURCE_MIGRATION", {}, 5000));
|
|
429
|
+
app.post("/api/code-source-migration/ack", (c) => relay(c, "ACK_CODE_SOURCE_MIGRATION", {}, 5000));
|
|
361
430
|
app.get("/api/projects", async (c) => {
|
|
362
431
|
const result = await ipcRequest("LIST_PROJECTS");
|
|
363
432
|
if (result.error)
|
|
@@ -757,7 +826,7 @@ app.get("/*", (c, next) => {
|
|
|
757
826
|
*/
|
|
758
827
|
function listen(port, publicUrl, isProxied) {
|
|
759
828
|
logger.info(`Starting Hono server on port ${port}...`);
|
|
760
|
-
const serverInstance = serve({ fetch: app.fetch, port, hostname:
|
|
829
|
+
const serverInstance = serve({ fetch: app.fetch, port, hostname: GUI_LISTEN_HOST }, (info) => {
|
|
761
830
|
logger.info(`Dashboard server listening on port ${info.port}`);
|
|
762
831
|
safeSend(IPCMessageSchema.parse({
|
|
763
832
|
type: "READY",
|