@imenam/mcp-documentor 1.0.85 → 1.0.87
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/shared/schemas/doc.schema.d.ts +2 -1
- package/dist/shared/schemas/doc.schema.js +12 -1
- 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/embedding-engine.d.ts +1 -1
- package/dist/src/core/embedding-engine.js +3 -3
- package/dist/src/core/embedding-store.d.ts +1 -1
- package/dist/src/core/embedding-store.js +4 -4
- 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/core/storage-engine.d.ts +12 -2
- package/dist/src/core/storage-engine.js +35 -15
- package/dist/src/gui-worker.js +70 -2
- package/dist/src/index.js +165 -11
- 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/tools/move-doc.js +20 -6
- 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 +2 -3
- 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,78 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
/**
|
|
4
|
+
* Machine-local state, in `<data dir>/local-state.json`. The sync repository's .gitignore excludes
|
|
5
|
+
* this file, so what it holds never leaves the machine: the code roots — where this machine keeps
|
|
6
|
+
* each project's repository — and the report of the one-time migration of the code-source
|
|
7
|
+
* environment variables.
|
|
8
|
+
*/
|
|
9
|
+
export class LocalState {
|
|
10
|
+
dataDir;
|
|
11
|
+
constructor(dataDir) {
|
|
12
|
+
this.dataDir = dataDir;
|
|
13
|
+
}
|
|
14
|
+
get filePath() {
|
|
15
|
+
return path.join(this.dataDir, "local-state.json");
|
|
16
|
+
}
|
|
17
|
+
read() {
|
|
18
|
+
if (!fs.existsSync(this.filePath))
|
|
19
|
+
return {};
|
|
20
|
+
try {
|
|
21
|
+
const parsed = JSON.parse(fs.readFileSync(this.filePath, "utf-8"));
|
|
22
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
throw new Error(`${this.filePath} is not valid JSON — fix or delete it.`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
write(state) {
|
|
29
|
+
fs.mkdirSync(this.dataDir, { recursive: true });
|
|
30
|
+
fs.writeFileSync(this.filePath, JSON.stringify(state, null, 2), "utf-8");
|
|
31
|
+
}
|
|
32
|
+
getCodeRoots() {
|
|
33
|
+
const roots = this.read().codeRoots;
|
|
34
|
+
return { default: roots?.default, byProject: { ...(roots?.byProject ?? {}) } };
|
|
35
|
+
}
|
|
36
|
+
/** The project's own root, else the machine default. */
|
|
37
|
+
getCodeRoot(project) {
|
|
38
|
+
const roots = this.getCodeRoots();
|
|
39
|
+
return roots.byProject[project] ?? roots.default;
|
|
40
|
+
}
|
|
41
|
+
/** Sets the machine default root, or removes it when `root` is undefined. */
|
|
42
|
+
setDefaultCodeRoot(root) {
|
|
43
|
+
const state = this.read();
|
|
44
|
+
const roots = { byProject: {}, ...state.codeRoots };
|
|
45
|
+
if (root)
|
|
46
|
+
roots.default = root;
|
|
47
|
+
else
|
|
48
|
+
delete roots.default;
|
|
49
|
+
state.codeRoots = roots;
|
|
50
|
+
this.write(state);
|
|
51
|
+
}
|
|
52
|
+
/** Sets a project's own root, or removes it — the project then uses the default — when undefined. */
|
|
53
|
+
setProjectCodeRoot(project, root) {
|
|
54
|
+
const state = this.read();
|
|
55
|
+
const byProject = { ...(state.codeRoots?.byProject ?? {}) };
|
|
56
|
+
if (root)
|
|
57
|
+
byProject[project] = root;
|
|
58
|
+
else
|
|
59
|
+
delete byProject[project];
|
|
60
|
+
state.codeRoots = { ...state.codeRoots, byProject };
|
|
61
|
+
this.write(state);
|
|
62
|
+
}
|
|
63
|
+
getMigrationReport() {
|
|
64
|
+
return this.read().codeSourceMigration;
|
|
65
|
+
}
|
|
66
|
+
setMigrationReport(report) {
|
|
67
|
+
const state = this.read();
|
|
68
|
+
state.codeSourceMigration = report;
|
|
69
|
+
this.write(state);
|
|
70
|
+
}
|
|
71
|
+
markMigrationReportSeen() {
|
|
72
|
+
const state = this.read();
|
|
73
|
+
if (!state.codeSourceMigration)
|
|
74
|
+
return;
|
|
75
|
+
state.codeSourceMigration.seen = true;
|
|
76
|
+
this.write(state);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { StorageEngine } from "./storage-engine.js";
|
|
2
|
+
import type { LocalState } from "./local-state.js";
|
|
3
|
+
/**
|
|
4
|
+
* Moves the code-source environment variables into the project settings and the machine-local
|
|
5
|
+
* code roots, once per machine.
|
|
6
|
+
*
|
|
7
|
+
* - MCP_DOCUMENTOR_CODE_REPO (+ MCP_DOCUMENTOR_CODE_BRANCH for specs without "#branch") becomes
|
|
8
|
+
* the code source of each project it names. A project that already has a code source — written
|
|
9
|
+
* by another machine and pulled — keeps it.
|
|
10
|
+
* - MCP_DOCUMENTOR_CODE_ROOTS becomes the code root of each project it names, on this machine.
|
|
11
|
+
* - MCP_DOCUMENTOR_CODE_ROOT seeds the default code root (see `seedDefaultCodeRoot`); it is not
|
|
12
|
+
* one of the variables to remove.
|
|
13
|
+
*
|
|
14
|
+
* The outcome is recorded as a report in the local state; the GUI shows it until acknowledged.
|
|
15
|
+
* Once recorded, the migrated variables are never read again: their presence is only logged.
|
|
16
|
+
*/
|
|
17
|
+
export declare function migrateCodeSources(storageEngine: StorageEngine, localState: LocalState): void;
|
|
@@ -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
|
+
}
|
|
@@ -58,9 +58,19 @@ export declare class StorageEngine {
|
|
|
58
58
|
*/
|
|
59
59
|
listDocsDetailed(project: string): Promise<DocListing[]>;
|
|
60
60
|
deleteDoc(id: string, project: string): Promise<boolean>;
|
|
61
|
-
|
|
61
|
+
/**
|
|
62
|
+
* Moves a doc to `newId` in `newProject`. Across projects, the moved doc's own
|
|
63
|
+
* same-project references (`doc://id`, bare related_docs) are qualified with the
|
|
64
|
+
* source project so they keep pointing at the same entries; the history records
|
|
65
|
+
* a delete in the source project and a create in the target.
|
|
66
|
+
*/
|
|
67
|
+
moveDoc(oldId: string, project: string, newId: string, newProject: string): Promise<boolean>;
|
|
62
68
|
getBundles(): ExportBundle[];
|
|
63
69
|
saveBundles(bundles: ExportBundle[]): void;
|
|
64
|
-
|
|
70
|
+
/**
|
|
71
|
+
* Rewrites every reference to `project:oldId` so it points at `newProject:newId`.
|
|
72
|
+
* In the source project, bare references become qualified when the doc left it.
|
|
73
|
+
*/
|
|
74
|
+
rewriteLinks(oldId: string, project: string, newId: string, newProject: string): Promise<string[]>;
|
|
65
75
|
}
|
|
66
76
|
export {};
|
|
@@ -227,24 +227,37 @@ export class StorageEngine {
|
|
|
227
227
|
logger.info(`Doc deleted: ${filePath}`);
|
|
228
228
|
return true;
|
|
229
229
|
}
|
|
230
|
-
|
|
230
|
+
/**
|
|
231
|
+
* Moves a doc to `newId` in `newProject`. Across projects, the moved doc's own
|
|
232
|
+
* same-project references (`doc://id`, bare related_docs) are qualified with the
|
|
233
|
+
* source project so they keep pointing at the same entries; the history records
|
|
234
|
+
* a delete in the source project and a create in the target.
|
|
235
|
+
*/
|
|
236
|
+
async moveDoc(oldId, project, newId, newProject) {
|
|
231
237
|
const projectDir = this.getProjectDir(project);
|
|
232
238
|
const oldFilePath = this.docIdToFilePath(oldId, projectDir);
|
|
233
239
|
if (!fs.existsSync(oldFilePath))
|
|
234
240
|
return false;
|
|
235
|
-
const
|
|
241
|
+
const newProjectDir = this.getProjectDir(newProject);
|
|
242
|
+
const newFilePath = this.docIdToFilePath(newId, newProjectDir);
|
|
236
243
|
if (fs.existsSync(newFilePath)) {
|
|
237
|
-
throw new Error(`A document already exists at path "${newId}".`);
|
|
244
|
+
throw new Error(`A document already exists at path "${newId}" in project "${newProject}".`);
|
|
238
245
|
}
|
|
239
246
|
const doc = await this.getDoc(oldId, project);
|
|
240
247
|
if (!doc)
|
|
241
248
|
return false;
|
|
249
|
+
const crossProject = newProject !== project;
|
|
250
|
+
if (crossProject) {
|
|
251
|
+
this.historyStore?.record(project, doc, "delete");
|
|
252
|
+
doc.content = doc.content.replace(/doc:\/\/([^):#\s]+)(?=[)#\s]|$)/g, `doc://${project}:$1`);
|
|
253
|
+
doc.related_docs = doc.related_docs.map((ref) => (ref.includes(":") ? ref : `${project}:${ref}`));
|
|
254
|
+
}
|
|
242
255
|
const newParts = newId.split("/");
|
|
243
256
|
doc.id = newId;
|
|
244
257
|
doc.folder = newParts.length > 1 ? newParts.slice(0, -1).join("/") : undefined;
|
|
245
258
|
doc.updatedAt = new Date().toISOString();
|
|
246
259
|
fs.mkdirSync(path.dirname(newFilePath), { recursive: true });
|
|
247
|
-
await this.saveDoc(doc,
|
|
260
|
+
await this.saveDoc(doc, newProject, crossProject ? "create" : "update");
|
|
248
261
|
fs.unlinkSync(oldFilePath);
|
|
249
262
|
this.cleanEmptyDirs(path.dirname(oldFilePath), projectDir);
|
|
250
263
|
logger.info(`Doc moved: ${oldFilePath} → ${newFilePath}`);
|
|
@@ -265,22 +278,29 @@ export class StorageEngine {
|
|
|
265
278
|
const p = path.join(this.getDataDir(), "export-bundles.json");
|
|
266
279
|
fs.writeFileSync(p, JSON.stringify(bundles, null, 2), "utf-8");
|
|
267
280
|
}
|
|
268
|
-
|
|
281
|
+
/**
|
|
282
|
+
* Rewrites every reference to `project:oldId` so it points at `newProject:newId`.
|
|
283
|
+
* In the source project, bare references become qualified when the doc left it.
|
|
284
|
+
*/
|
|
285
|
+
async rewriteLinks(oldId, project, newId, newProject) {
|
|
269
286
|
const config = this.getConfig();
|
|
270
287
|
const proj = project;
|
|
288
|
+
const crossProject = newProject !== proj;
|
|
289
|
+
const qualifiedTarget = `${newProject}:${newId}`;
|
|
290
|
+
const bareTarget = crossProject ? qualifiedTarget : newId;
|
|
291
|
+
const boundary = "(?=[)#\\s]|$)";
|
|
271
292
|
const updated = [];
|
|
272
|
-
// Within the
|
|
293
|
+
// Within the source project: rewrite bare refs and explicit self-cross-project refs
|
|
273
294
|
const sameProjDocs = await this.listDocs(proj);
|
|
274
295
|
for (const summary of sameProjDocs) {
|
|
275
|
-
if (summary.id === newId)
|
|
296
|
+
if (!crossProject && summary.id === newId)
|
|
276
297
|
continue;
|
|
277
298
|
const doc = await this.getDoc(summary.id, proj);
|
|
278
299
|
if (!doc)
|
|
279
300
|
continue;
|
|
280
301
|
let changed = false;
|
|
281
|
-
let newContent = doc.content.replace(new RegExp(`doc://${escapeRegex(oldId)}`, "g"), `doc://${
|
|
282
|
-
|
|
283
|
-
newContent = newContent.replace(new RegExp(`doc://${escapeRegex(proj)}:${escapeRegex(oldId)}`, "g"), `doc://${proj}:${newId}`);
|
|
302
|
+
let newContent = doc.content.replace(new RegExp(`doc://${escapeRegex(oldId)}${boundary}`, "g"), `doc://${bareTarget}`);
|
|
303
|
+
newContent = newContent.replace(new RegExp(`doc://${escapeRegex(proj)}:${escapeRegex(oldId)}${boundary}`, "g"), `doc://${qualifiedTarget}`);
|
|
284
304
|
if (newContent !== doc.content) {
|
|
285
305
|
doc.content = newContent;
|
|
286
306
|
changed = true;
|
|
@@ -288,11 +308,11 @@ export class StorageEngine {
|
|
|
288
308
|
const newRelated = doc.related_docs.map((ref) => {
|
|
289
309
|
if (ref === oldId) {
|
|
290
310
|
changed = true;
|
|
291
|
-
return
|
|
311
|
+
return bareTarget;
|
|
292
312
|
}
|
|
293
313
|
if (ref === `${proj}:${oldId}`) {
|
|
294
314
|
changed = true;
|
|
295
|
-
return
|
|
315
|
+
return qualifiedTarget;
|
|
296
316
|
}
|
|
297
317
|
return ref;
|
|
298
318
|
});
|
|
@@ -303,7 +323,7 @@ export class StorageEngine {
|
|
|
303
323
|
updated.push(summary.id);
|
|
304
324
|
}
|
|
305
325
|
}
|
|
306
|
-
// In other projects: rewrite cross-project refs pointing to
|
|
326
|
+
// In other projects: rewrite cross-project refs pointing to the source project
|
|
307
327
|
for (const otherProj of config.projects) {
|
|
308
328
|
if (otherProj === proj)
|
|
309
329
|
continue;
|
|
@@ -313,7 +333,7 @@ export class StorageEngine {
|
|
|
313
333
|
if (!doc)
|
|
314
334
|
continue;
|
|
315
335
|
let changed = false;
|
|
316
|
-
const newContent = doc.content.replace(new RegExp(`doc://${escapeRegex(proj)}:${escapeRegex(oldId)}`, "g"), `doc://${
|
|
336
|
+
const newContent = doc.content.replace(new RegExp(`doc://${escapeRegex(proj)}:${escapeRegex(oldId)}${boundary}`, "g"), `doc://${qualifiedTarget}`);
|
|
317
337
|
if (newContent !== doc.content) {
|
|
318
338
|
doc.content = newContent;
|
|
319
339
|
changed = true;
|
|
@@ -321,7 +341,7 @@ export class StorageEngine {
|
|
|
321
341
|
const newRelated = doc.related_docs.map((ref) => {
|
|
322
342
|
if (ref === `${proj}:${oldId}`) {
|
|
323
343
|
changed = true;
|
|
324
|
-
return
|
|
344
|
+
return qualifiedTarget;
|
|
325
345
|
}
|
|
326
346
|
return ref;
|
|
327
347
|
});
|