@imenam/mcp-documentor 1.0.74 → 1.0.77
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 +271 -259
- package/dist/shared/schemas/doc.schema.d.ts +1 -4
- package/dist/shared/schemas/doc.schema.js +1 -4
- package/dist/src/cli/setup-mcp.d.ts +20 -1
- package/dist/src/cli/setup-mcp.js +65 -6
- package/dist/src/core/storage-engine.d.ts +14 -15
- package/dist/src/core/storage-engine.js +15 -51
- package/dist/src/gui-worker.js +101 -37
- package/dist/src/index.js +99 -106
- package/dist/src/tools/check-links.js +1 -1
- package/dist/src/tools/list-projects.js +7 -5
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/gui/dist/assets/{arc-BNuZ3dSL.js → arc-CsM5OJTM.js} +1 -1
- package/gui/dist/assets/{architectureDiagram-3BPJPVTR-D8g3tPQx.js → architectureDiagram-3BPJPVTR-BwIu1llT.js} +1 -1
- package/gui/dist/assets/{blockDiagram-GPEHLZMM-limhup8_.js → blockDiagram-GPEHLZMM-BVijVnPI.js} +1 -1
- package/gui/dist/assets/{c4Diagram-AAUBKEIU-Ct39JMIy.js → c4Diagram-AAUBKEIU-D-rcZUZu.js} +1 -1
- package/gui/dist/assets/channel-DKPwwuQf.js +1 -0
- package/gui/dist/assets/{chunk-2J33WTMH-BkLJTezj.js → chunk-2J33WTMH-ByfTGREe.js} +1 -1
- package/gui/dist/assets/{chunk-4BX2VUAB-CR5NbhDR.js → chunk-4BX2VUAB-B_5oCXyz.js} +1 -1
- package/gui/dist/assets/{chunk-55IACEB6-CtQYMnJS.js → chunk-55IACEB6-Bc2Kq9HD.js} +1 -1
- package/gui/dist/assets/{chunk-727SXJPM-B2q7tUn8.js → chunk-727SXJPM-CVwmKEMQ.js} +1 -1
- package/gui/dist/assets/{chunk-AQP2D5EJ-Bbcnrfrp.js → chunk-AQP2D5EJ-BkMuEFxD.js} +1 -1
- package/gui/dist/assets/{chunk-FMBD7UC4-D8gd4zRe.js → chunk-FMBD7UC4-CU7EDl9I.js} +1 -1
- package/gui/dist/assets/{chunk-ND2GUHAM-DiZKzZtG.js → chunk-ND2GUHAM-1b34Dtkn.js} +1 -1
- package/gui/dist/assets/{chunk-QZHKN3VN-CrKx-e2_.js → chunk-QZHKN3VN-DnUfMecI.js} +1 -1
- package/gui/dist/assets/classDiagram-4FO5ZUOK-CnCvVGW9.js +1 -0
- package/gui/dist/assets/classDiagram-v2-Q7XG4LA2-CnCvVGW9.js +1 -0
- package/gui/dist/assets/{cose-bilkent-S5V4N54A-BDUCVkTl.js → cose-bilkent-S5V4N54A-D9OBrxKr.js} +1 -1
- package/gui/dist/assets/{dagre-BM42HDAG-DkoYBHXK.js → dagre-BM42HDAG-zDqwL8xs.js} +1 -1
- package/gui/dist/assets/{diagram-2AECGRRQ-BT1YJHon.js → diagram-2AECGRRQ-ejbP8WIj.js} +1 -1
- package/gui/dist/assets/{diagram-5GNKFQAL-C00r6F35.js → diagram-5GNKFQAL-Cv9WmtFO.js} +1 -1
- package/gui/dist/assets/{diagram-KO2AKTUF-B4wcN5a_.js → diagram-KO2AKTUF-BI0ybJiN.js} +1 -1
- package/gui/dist/assets/{diagram-LMA3HP47-B9FqCfel.js → diagram-LMA3HP47-DlVGttLq.js} +1 -1
- package/gui/dist/assets/{diagram-OG6HWLK6-COPCDzdG.js → diagram-OG6HWLK6-DjoAH-Fq.js} +1 -1
- package/gui/dist/assets/{erDiagram-TEJ5UH35-BGQGT2wN.js → erDiagram-TEJ5UH35-BVQHVGK4.js} +1 -1
- package/gui/dist/assets/{flowDiagram-I6XJVG4X-A7jKWUpi.js → flowDiagram-I6XJVG4X-oZP2Z-6T.js} +1 -1
- package/gui/dist/assets/{ganttDiagram-6RSMTGT7-NhFkpY-x.js → ganttDiagram-6RSMTGT7-dy4QyEOP.js} +1 -1
- package/gui/dist/assets/{gitGraphDiagram-PVQCEYII-z0AqyYG4.js → gitGraphDiagram-PVQCEYII-BxgLy7Ln.js} +1 -1
- package/gui/dist/assets/{index-Dub5OLKk.js → index-Dtv0slFQ.js} +66 -66
- package/gui/dist/assets/{infoDiagram-5YYISTIA-D69UR9u9.js → infoDiagram-5YYISTIA-DNOdinpR.js} +1 -1
- package/gui/dist/assets/{ishikawaDiagram-YF4QCWOH-DSTqIoro.js → ishikawaDiagram-YF4QCWOH-C4pQ434G.js} +1 -1
- package/gui/dist/assets/{journeyDiagram-JHISSGLW-CekXdcoE.js → journeyDiagram-JHISSGLW-CBrVYW94.js} +1 -1
- package/gui/dist/assets/{kanban-definition-UN3LZRKU-CseGvFK9.js → kanban-definition-UN3LZRKU-Df-rgiY8.js} +1 -1
- package/gui/dist/assets/{linear-BtVl_9_C.js → linear-B_6fRXVt.js} +1 -1
- package/gui/dist/assets/{mindmap-definition-RKZ34NQL-XLjQIU37.js → mindmap-definition-RKZ34NQL-CRAd_VDM.js} +1 -1
- package/gui/dist/assets/{pieDiagram-4H26LBE5-BcvUzWoW.js → pieDiagram-4H26LBE5-CJkmLrDI.js} +1 -1
- package/gui/dist/assets/{quadrantDiagram-W4KKPZXB-BzoYEmNW.js → quadrantDiagram-W4KKPZXB-D-rDEcnF.js} +1 -1
- package/gui/dist/assets/{requirementDiagram-4Y6WPE33-icKC1_Nd.js → requirementDiagram-4Y6WPE33-DE8MhSMe.js} +1 -1
- package/gui/dist/assets/{sankeyDiagram-5OEKKPKP-mi3i-2dZ.js → sankeyDiagram-5OEKKPKP-BwlxtrUs.js} +1 -1
- package/gui/dist/assets/{sequenceDiagram-3UESZ5HK-XrcDkRlM.js → sequenceDiagram-3UESZ5HK-B4LjBzWg.js} +1 -1
- package/gui/dist/assets/{stateDiagram-AJRCARHV-UXaqrCNT.js → stateDiagram-AJRCARHV-Bv72afPK.js} +1 -1
- package/gui/dist/assets/stateDiagram-v2-BHNVJYJU-BKJDOkkR.js +1 -0
- package/gui/dist/assets/{timeline-definition-PNZ67QCA-yd6607p6.js → timeline-definition-PNZ67QCA-CBxgf8iW.js} +1 -1
- package/gui/dist/assets/{vennDiagram-CIIHVFJN-BsRqUcIp.js → vennDiagram-CIIHVFJN-DPSdK06q.js} +1 -1
- package/gui/dist/assets/{wardley-L42UT6IY-BmdA7yk0.js → wardley-L42UT6IY-JIAzuoPm.js} +1 -1
- package/gui/dist/assets/{wardleyDiagram-YWT4CUSO-CRdmckD8.js → wardleyDiagram-YWT4CUSO-YDfaF1wT.js} +1 -1
- package/gui/dist/assets/{xychartDiagram-2RQKCTM6-DWpNIQY-.js → xychartDiagram-2RQKCTM6-DHlPryZX.js} +1 -1
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/dist/src/tools/set-project.d.ts +0 -14
- package/dist/src/tools/set-project.js +0 -18
- package/gui/dist/assets/channel-uoEIgtym.js +0 -1
- package/gui/dist/assets/classDiagram-4FO5ZUOK-DmhYb54l.js +0 -1
- package/gui/dist/assets/classDiagram-v2-Q7XG4LA2-DmhYb54l.js +0 -1
- package/gui/dist/assets/stateDiagram-v2-BHNVJYJU-Bhdu4lv4.js +0 -1
|
@@ -4,8 +4,9 @@ const SERVER_NAME = "mcp-documentor";
|
|
|
4
4
|
const PACKAGE_NAME = "@imenam/mcp-documentor";
|
|
5
5
|
/**
|
|
6
6
|
* Valeurs volontairement vides : chaque variable est inactive tant qu'elle n'est
|
|
7
|
-
* pas remplie (PROXY_URL
|
|
8
|
-
* donc le fichier généré est utilisable tel quel, jamais à
|
|
7
|
+
* pas remplie (PROXY_URL et APP_PORT vides = GUI désactivée, clé vide = pas
|
|
8
|
+
* d'embeddings), donc le fichier généré est utilisable tel quel, jamais à
|
|
9
|
+
* moitié cassé.
|
|
9
10
|
*/
|
|
10
11
|
/** Slashes POSIX : évite les backslashes échappés dans le JSON, et Node les accepte sous Windows. */
|
|
11
12
|
function toPosixPath(dir) {
|
|
@@ -23,14 +24,18 @@ function toDisplayName(folder) {
|
|
|
23
24
|
.map(capitalize)
|
|
24
25
|
.join(" ");
|
|
25
26
|
}
|
|
26
|
-
function buildServerEntry(codeRoot) {
|
|
27
|
+
function buildServerEntry(codeRoot, exposure) {
|
|
27
28
|
// Le nom du dossier courant sert de base aux identifiants d'affichage côté proxy.
|
|
28
29
|
const folder = path.basename(codeRoot) || "documentor";
|
|
30
|
+
// Un port explicite désactive le placeholder proxy : les deux variables ne
|
|
31
|
+
// doivent jamais être renseignées ensemble, PROXY_URL primant au runtime.
|
|
32
|
+
const proxyUrl = exposure.appPort !== undefined ? "" : exposure.proxyUrl ?? "http://localhost:";
|
|
29
33
|
return {
|
|
30
34
|
command: "npx",
|
|
31
35
|
args: ["-y", PACKAGE_NAME],
|
|
32
36
|
env: {
|
|
33
|
-
PROXY_URL:
|
|
37
|
+
PROXY_URL: proxyUrl,
|
|
38
|
+
APP_PORT: exposure.appPort !== undefined ? String(exposure.appPort) : "",
|
|
34
39
|
APP_GROUP: capitalize(folder),
|
|
35
40
|
APP_NAME: `Documentor ${toDisplayName(folder)}`,
|
|
36
41
|
APP_PATH: `/documentor-${folder}`,
|
|
@@ -47,6 +52,7 @@ function buildServerEntry(codeRoot) {
|
|
|
47
52
|
}
|
|
48
53
|
const GENERATED_VARS = [
|
|
49
54
|
["PROXY_URL", "URL du proxy pour exposer la GUI (compléter le port)"],
|
|
55
|
+
["APP_PORT", "port local si aucun proxy — la GUI est servie sur http://localhost:<port>"],
|
|
50
56
|
["APP_GROUP", "groupe d'affichage dans le dashboard du proxy"],
|
|
51
57
|
["APP_NAME", "nom affiché de la GUI dans le dashboard du proxy"],
|
|
52
58
|
["APP_PATH", "chemin de montage de la GUI derrière le proxy"],
|
|
@@ -64,7 +70,53 @@ const OPTIONAL_VARS = [
|
|
|
64
70
|
["READ_ONLY", "1/true pour désactiver toutes les écritures"],
|
|
65
71
|
["SYNC_PULL_ON_START", "false pour désactiver le pull au démarrage"],
|
|
66
72
|
];
|
|
67
|
-
|
|
73
|
+
/** Lit `--flag=valeur` (tirets ou underscores acceptés). */
|
|
74
|
+
function readFlag(argv, name) {
|
|
75
|
+
const prefixes = [`--${name}=`, `--${name.replace(/_/g, "-")}=`];
|
|
76
|
+
for (const arg of argv) {
|
|
77
|
+
const prefix = prefixes.find((p) => arg.startsWith(p));
|
|
78
|
+
if (prefix)
|
|
79
|
+
return arg.slice(prefix.length).trim();
|
|
80
|
+
}
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Traduit `--proxy_url=` / `--app_port=` en mode d'exposition de la GUI.
|
|
85
|
+
* Les deux ensemble sont refusés : au runtime PROXY_URL primerait toujours,
|
|
86
|
+
* donc un fichier portant les deux serait silencieusement trompeur.
|
|
87
|
+
*/
|
|
88
|
+
export function parseGuiExposure(argv) {
|
|
89
|
+
const rawProxyUrl = readFlag(argv, "proxy_url");
|
|
90
|
+
const rawAppPort = readFlag(argv, "app_port");
|
|
91
|
+
if (rawProxyUrl !== undefined && rawAppPort !== undefined) {
|
|
92
|
+
return { error: "--proxy_url et --app_port sont exclusifs : la GUI passe par le proxy, ou écoute sur un port local." };
|
|
93
|
+
}
|
|
94
|
+
if (rawProxyUrl !== undefined) {
|
|
95
|
+
if (rawProxyUrl === "")
|
|
96
|
+
return { error: "--proxy_url attend une URL, ex : --proxy_url=http://localhost:3000" };
|
|
97
|
+
let parsed;
|
|
98
|
+
try {
|
|
99
|
+
parsed = new URL(rawProxyUrl);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return { error: `--proxy_url invalide : "${rawProxyUrl}" n'est pas une URL absolue (ex : http://localhost:3000).` };
|
|
103
|
+
}
|
|
104
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
105
|
+
return { error: `--proxy_url invalide : protocole "${parsed.protocol}" non supporté (http ou https attendu).` };
|
|
106
|
+
}
|
|
107
|
+
// Une barre finale casserait la concaténation PROXY_URL + APP_PATH.
|
|
108
|
+
return { exposure: { proxyUrl: rawProxyUrl.replace(/\/+$/, "") } };
|
|
109
|
+
}
|
|
110
|
+
if (rawAppPort !== undefined) {
|
|
111
|
+
const port = Number(rawAppPort);
|
|
112
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
113
|
+
return { error: `--app_port invalide : "${rawAppPort}" n'est pas un entier entre 1 et 65535.` };
|
|
114
|
+
}
|
|
115
|
+
return { exposure: { appPort: port } };
|
|
116
|
+
}
|
|
117
|
+
return { exposure: {} };
|
|
118
|
+
}
|
|
119
|
+
export function runSetupMcp({ cwd, force, exposure = {} }) {
|
|
68
120
|
const target = path.join(cwd, ".mcp.json");
|
|
69
121
|
let config = {};
|
|
70
122
|
const exists = fs.existsSync(target);
|
|
@@ -94,11 +146,17 @@ export function runSetupMcp({ cwd, force }) {
|
|
|
94
146
|
console.error("Relancez avec --force pour écraser cette entrée (les autres serveurs sont préservés).");
|
|
95
147
|
return 1;
|
|
96
148
|
}
|
|
97
|
-
config.mcpServers[SERVER_NAME] = buildServerEntry(cwd);
|
|
149
|
+
config.mcpServers[SERVER_NAME] = buildServerEntry(cwd, exposure);
|
|
98
150
|
fs.writeFileSync(target, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
99
151
|
const action = alreadyPresent ? "remplacée" : exists ? "ajoutée" : "créée";
|
|
100
152
|
console.log(`Configuration ${SERVER_NAME} ${action} dans ${target}`);
|
|
101
153
|
console.log("");
|
|
154
|
+
if (exposure.appPort !== undefined) {
|
|
155
|
+
console.log(`GUI en accès direct : http://localhost:${exposure.appPort} (sans proxy).`);
|
|
156
|
+
}
|
|
157
|
+
else if (exposure.proxyUrl !== undefined) {
|
|
158
|
+
console.log(`GUI exposée via le proxy ${exposure.proxyUrl}.`);
|
|
159
|
+
}
|
|
102
160
|
console.log('À compléter dans le champ "env" :');
|
|
103
161
|
for (const [name, description] of GENERATED_VARS) {
|
|
104
162
|
console.log(` ${name.padEnd(30)} ${description}`);
|
|
@@ -110,5 +168,6 @@ export function runSetupMcp({ cwd, force }) {
|
|
|
110
168
|
}
|
|
111
169
|
console.log("");
|
|
112
170
|
console.log("Une variable laissée vide est simplement inactive.");
|
|
171
|
+
console.log("PROXY_URL et APP_PORT s'excluent : PROXY_URL prime si les deux sont renseignés.");
|
|
113
172
|
return 0;
|
|
114
173
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { DocEntry, DocSummary, ExportBundle } from "../../shared/schemas/doc.schema.js";
|
|
2
2
|
import type { HistoryStore } from "./history-store.js";
|
|
3
3
|
interface GlobalConfig {
|
|
4
|
-
currentProject: string;
|
|
5
4
|
projects: string[];
|
|
6
5
|
foldersMigrated?: boolean;
|
|
7
6
|
}
|
|
@@ -22,16 +21,16 @@ export declare class StorageEngine {
|
|
|
22
21
|
constructor(rootDir: string);
|
|
23
22
|
setHistoryStore(store: HistoryStore): void;
|
|
24
23
|
getDataDir(): string;
|
|
25
|
-
|
|
24
|
+
/**
|
|
25
|
+
* There is no "current project" fallback: every caller names the project it
|
|
26
|
+
* targets. The UI's project selector is a client-side view preference only.
|
|
27
|
+
*/
|
|
28
|
+
getProjectDir(project: string): string;
|
|
26
29
|
private getConfigPath;
|
|
27
|
-
private getLocalStatePath;
|
|
28
|
-
private readLocalState;
|
|
29
|
-
private writeLocalState;
|
|
30
30
|
/**
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* files that still embed currentProject are migrated transparently.
|
|
31
|
+
* Reads config.json (versioned): the project list and migration flags.
|
|
32
|
+
* A legacy currentProject key is stripped on read — project selection is a
|
|
33
|
+
* per-client UI concern and is no longer server state.
|
|
35
34
|
*/
|
|
36
35
|
getConfig(): GlobalConfig;
|
|
37
36
|
saveConfig(config: GlobalConfig): void;
|
|
@@ -41,13 +40,13 @@ export declare class StorageEngine {
|
|
|
41
40
|
private walkMdFiles;
|
|
42
41
|
private docIdToFilePath;
|
|
43
42
|
private cleanEmptyDirs;
|
|
44
|
-
saveDoc(doc: DocEntry, project
|
|
45
|
-
getDoc(id: string, project
|
|
46
|
-
listDocs(project
|
|
47
|
-
deleteDoc(id: string, project
|
|
48
|
-
moveDoc(oldId: string, newId: string, project
|
|
43
|
+
saveDoc(doc: DocEntry, project: string, changeType?: "create" | "update" | "patch"): Promise<void>;
|
|
44
|
+
getDoc(id: string, project: string): Promise<DocEntry | null>;
|
|
45
|
+
listDocs(project: string): Promise<DocSummary[]>;
|
|
46
|
+
deleteDoc(id: string, project: string): Promise<boolean>;
|
|
47
|
+
moveDoc(oldId: string, newId: string, project: string): Promise<boolean>;
|
|
49
48
|
getBundles(): ExportBundle[];
|
|
50
49
|
saveBundles(bundles: ExportBundle[]): void;
|
|
51
|
-
rewriteLinks(oldId: string, newId: string, project
|
|
50
|
+
rewriteLinks(oldId: string, newId: string, project: string): Promise<string[]>;
|
|
52
51
|
}
|
|
53
52
|
export {};
|
|
@@ -19,39 +19,20 @@ export class StorageEngine {
|
|
|
19
19
|
const envDir = process.env.MCP_DATA_DIR;
|
|
20
20
|
return envDir && envDir.trim() !== "" ? envDir : path.join(this.rootDir, ".docs-data");
|
|
21
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* There is no "current project" fallback: every caller names the project it
|
|
24
|
+
* targets. The UI's project selector is a client-side view preference only.
|
|
25
|
+
*/
|
|
22
26
|
getProjectDir(project) {
|
|
23
|
-
|
|
24
|
-
const proj = project ?? config.currentProject;
|
|
25
|
-
return path.join(this.getDataDir(), "projects", proj);
|
|
27
|
+
return path.join(this.getDataDir(), "projects", project);
|
|
26
28
|
}
|
|
27
29
|
getConfigPath() {
|
|
28
30
|
return path.join(this.getDataDir(), "config.json");
|
|
29
31
|
}
|
|
30
|
-
getLocalStatePath() {
|
|
31
|
-
return path.join(this.getDataDir(), "local-state.json");
|
|
32
|
-
}
|
|
33
|
-
readLocalState() {
|
|
34
|
-
const p = this.getLocalStatePath();
|
|
35
|
-
if (fs.existsSync(p)) {
|
|
36
|
-
try {
|
|
37
|
-
return JSON.parse(fs.readFileSync(p, "utf-8"));
|
|
38
|
-
}
|
|
39
|
-
catch {
|
|
40
|
-
logger.warn("Failed to parse local-state.json, using defaults");
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
return {};
|
|
44
|
-
}
|
|
45
|
-
writeLocalState(state) {
|
|
46
|
-
const p = this.getLocalStatePath();
|
|
47
|
-
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
48
|
-
fs.writeFileSync(p, JSON.stringify(state, null, 2), "utf-8");
|
|
49
|
-
}
|
|
50
32
|
/**
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
* files that still embed currentProject are migrated transparently.
|
|
33
|
+
* Reads config.json (versioned): the project list and migration flags.
|
|
34
|
+
* A legacy currentProject key is stripped on read — project selection is a
|
|
35
|
+
* per-client UI concern and is no longer server state.
|
|
55
36
|
*/
|
|
56
37
|
getConfig() {
|
|
57
38
|
const configPath = this.getConfigPath();
|
|
@@ -64,35 +45,20 @@ export class StorageEngine {
|
|
|
64
45
|
logger.warn("Failed to parse config.json, using defaults");
|
|
65
46
|
}
|
|
66
47
|
}
|
|
67
|
-
|
|
68
|
-
const projects = shared.projects ?? ["default"];
|
|
69
|
-
// Migrate a legacy currentProject embedded in config.json into local-state.
|
|
70
|
-
if (shared.currentProject !== undefined && local.currentProject === undefined) {
|
|
71
|
-
local.currentProject = shared.currentProject;
|
|
72
|
-
this.writeLocalState(local);
|
|
48
|
+
if (shared.currentProject !== undefined) {
|
|
73
49
|
delete shared.currentProject;
|
|
74
50
|
fs.writeFileSync(configPath, JSON.stringify(shared, null, 2), "utf-8");
|
|
75
|
-
logger.info("
|
|
51
|
+
logger.info("Dropped obsolete currentProject from config.json");
|
|
76
52
|
}
|
|
77
53
|
return {
|
|
78
|
-
|
|
79
|
-
projects,
|
|
54
|
+
projects: shared.projects ?? ["default"],
|
|
80
55
|
foldersMigrated: shared.foldersMigrated,
|
|
81
56
|
};
|
|
82
57
|
}
|
|
83
58
|
saveConfig(config) {
|
|
84
|
-
// Shared, versioned part → config.json (currentProject is intentionally excluded).
|
|
85
59
|
const configPath = this.getConfigPath();
|
|
86
|
-
const shared = {
|
|
87
|
-
projects: config.projects,
|
|
88
|
-
foldersMigrated: config.foldersMigrated,
|
|
89
|
-
};
|
|
90
60
|
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
91
|
-
fs.writeFileSync(configPath, JSON.stringify(
|
|
92
|
-
// Machine-local part → local-state.json.
|
|
93
|
-
const local = this.readLocalState();
|
|
94
|
-
local.currentProject = config.currentProject;
|
|
95
|
-
this.writeLocalState(local);
|
|
61
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
|
|
96
62
|
}
|
|
97
63
|
/** Reads sync settings from environment variables (admin-controlled, not agent-settable). */
|
|
98
64
|
getSyncConfig() {
|
|
@@ -146,8 +112,7 @@ export class StorageEngine {
|
|
|
146
112
|
}
|
|
147
113
|
}
|
|
148
114
|
async saveDoc(doc, project, changeType = "update") {
|
|
149
|
-
const
|
|
150
|
-
const proj = project ?? config.currentProject;
|
|
115
|
+
const proj = project;
|
|
151
116
|
const projectDir = this.getProjectDir(project);
|
|
152
117
|
const targetPath = this.docIdToFilePath(doc.id, projectDir);
|
|
153
118
|
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
@@ -220,8 +185,7 @@ export class StorageEngine {
|
|
|
220
185
|
// existed in several. Doc-targeted callers must name their project and go through
|
|
221
186
|
// getDoc(id, project) — see core/project-scope.ts.
|
|
222
187
|
async deleteDoc(id, project) {
|
|
223
|
-
const
|
|
224
|
-
const proj = project ?? config.currentProject;
|
|
188
|
+
const proj = project;
|
|
225
189
|
const projectDir = this.getProjectDir(project);
|
|
226
190
|
const filePath = this.docIdToFilePath(id, projectDir);
|
|
227
191
|
if (!fs.existsSync(filePath))
|
|
@@ -277,7 +241,7 @@ export class StorageEngine {
|
|
|
277
241
|
}
|
|
278
242
|
async rewriteLinks(oldId, newId, project) {
|
|
279
243
|
const config = this.getConfig();
|
|
280
|
-
const proj = project
|
|
244
|
+
const proj = project;
|
|
281
245
|
const updated = [];
|
|
282
246
|
// Within the same project: rewrite same-project refs and explicit self-cross-project refs
|
|
283
247
|
const sameProjDocs = await this.listDocs(proj);
|
package/dist/src/gui-worker.js
CHANGED
|
@@ -32,6 +32,29 @@ const dataDir = process.env.MCP_DATA_DIR?.trim() || path.join(rootDir, ".docs-da
|
|
|
32
32
|
const staticRoot = path.join(rootDir, "gui/dist");
|
|
33
33
|
const absoluteStaticRoot = path.resolve(staticRoot);
|
|
34
34
|
const configuredAppPath = process.env.APP_PATH || "/documentor-gui";
|
|
35
|
+
/**
|
|
36
|
+
* Chemin réellement monté par le proxy. Il peut différer de `configuredAppPath`
|
|
37
|
+
* quand `APP_PATH_PREFIX` est défini : le client partagé préfixe alors le chemin
|
|
38
|
+
* naturel et renvoie le chemin final dans `result.path`. Le `<base href>` doit
|
|
39
|
+
* suivre ce chemin, sinon les assets de la SPA sont demandés au mauvais endroit.
|
|
40
|
+
*/
|
|
41
|
+
let effectiveAppPath = configuredAppPath;
|
|
42
|
+
function resolveGuiMode() {
|
|
43
|
+
const proxyUrl = process.env.PROXY_URL?.trim();
|
|
44
|
+
if (proxyUrl)
|
|
45
|
+
return { kind: "proxy", proxyUrl };
|
|
46
|
+
const rawPort = process.env.APP_PORT?.trim();
|
|
47
|
+
if (!rawPort) {
|
|
48
|
+
return { kind: "disabled", reason: "PROXY_URL et APP_PORT non définis" };
|
|
49
|
+
}
|
|
50
|
+
const port = Number(rawPort);
|
|
51
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
52
|
+
return { kind: "disabled", reason: `APP_PORT invalide (${rawPort}) — attendu : entier entre 1 et 65535` };
|
|
53
|
+
}
|
|
54
|
+
return { kind: "standalone", port };
|
|
55
|
+
}
|
|
56
|
+
const guiMode = resolveGuiMode();
|
|
57
|
+
const isStandalone = guiMode.kind === "standalone";
|
|
35
58
|
setupLogging({ processLabel: "mcp-documentor:GUI", logDir: resolveLogDir(rootDir) });
|
|
36
59
|
const logger = createLogger("mcp-documentor:GUI");
|
|
37
60
|
process.on("uncaughtException", (err) => {
|
|
@@ -208,7 +231,7 @@ app.get("/proxy/health", (c) => {
|
|
|
208
231
|
const memory = process.memoryUsage();
|
|
209
232
|
return c.json({
|
|
210
233
|
status: "ok",
|
|
211
|
-
proxy: proxyClient ? proxyClient.getStatus() :
|
|
234
|
+
proxy: proxyClient ? proxyClient.getStatus() : isStandalone ? "standalone" : "error",
|
|
212
235
|
uptime: process.uptime(),
|
|
213
236
|
memory: { rss: memory.rss, heapUsed: memory.heapUsed },
|
|
214
237
|
ipc: process.connected ? "connected" : "disconnected",
|
|
@@ -343,22 +366,29 @@ app.get("/api/projects", async (c) => {
|
|
|
343
366
|
});
|
|
344
367
|
app.get("/api/folders", async (c) => {
|
|
345
368
|
const project = c.req.query("project");
|
|
346
|
-
|
|
369
|
+
if (!project)
|
|
370
|
+
return c.json({ error: "project query param is required" }, 400);
|
|
371
|
+
const result = await ipcRequest("LIST_FOLDERS", { project });
|
|
347
372
|
if (result.error)
|
|
348
373
|
return c.json({ error: result.error }, 500);
|
|
349
374
|
return c.json(result.data);
|
|
350
375
|
});
|
|
351
|
-
|
|
376
|
+
// Creates an empty project. Selecting a project is a client-side concern:
|
|
377
|
+
// the server holds no active project.
|
|
378
|
+
app.post("/api/projects", async (c) => {
|
|
352
379
|
const { project } = await c.req.json();
|
|
353
380
|
if (!project)
|
|
354
381
|
return c.json({ error: "project is required" }, 400);
|
|
355
|
-
const result = await ipcRequest("
|
|
382
|
+
const result = await ipcRequest("CREATE_PROJECT", { project });
|
|
356
383
|
if (result.error)
|
|
357
384
|
return c.json({ error: result.error }, 500);
|
|
358
385
|
return c.json(result.data);
|
|
359
386
|
});
|
|
360
387
|
app.get("/api/docs", async (c) => {
|
|
361
|
-
const
|
|
388
|
+
const project = c.req.query("project");
|
|
389
|
+
if (!project)
|
|
390
|
+
return c.json({ error: "project query param is required" }, 400);
|
|
391
|
+
const result = await ipcRequest("LIST_DOCS", { project });
|
|
362
392
|
if (result.error)
|
|
363
393
|
return c.json({ error: result.error }, 500);
|
|
364
394
|
return c.json(result.data);
|
|
@@ -475,10 +505,13 @@ app.get("/api/history-version/*", async (c) => {
|
|
|
475
505
|
return c.json(result.data);
|
|
476
506
|
});
|
|
477
507
|
app.post("/api/search", async (c) => {
|
|
478
|
-
const { query, top_k, all_projects } = await c.req.json();
|
|
508
|
+
const { query, top_k, all_projects, project } = await c.req.json();
|
|
479
509
|
if (!query)
|
|
480
510
|
return c.json({ error: "query is required" }, 400);
|
|
481
|
-
|
|
511
|
+
if (!all_projects && !project) {
|
|
512
|
+
return c.json({ error: "project is required unless all_projects is set" }, 400);
|
|
513
|
+
}
|
|
514
|
+
const result = await ipcRequest("SEARCH_DOCS", { query, top_k: top_k || 5, all_projects: !!all_projects, project });
|
|
482
515
|
if (result.error)
|
|
483
516
|
return c.json({ error: result.error }, 500);
|
|
484
517
|
return c.json(result.data);
|
|
@@ -522,14 +555,9 @@ app.get("/api/export-zip/:id", async (c) => {
|
|
|
522
555
|
const bundle = bundles.find((b) => b.id === id);
|
|
523
556
|
if (!bundle)
|
|
524
557
|
return c.json({ error: "Bundle not found" }, 404);
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
if (fs.existsSync(configPath)) {
|
|
529
|
-
currentProject = JSON.parse(fs.readFileSync(configPath, "utf-8")).currentProject ?? "default";
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
catch { /* use default */ }
|
|
558
|
+
// Legacy bundles may hold bare doc ids; they are resolved against the project
|
|
559
|
+
// the caller is viewing. Fully qualified "project:doc-id" refs ignore it.
|
|
560
|
+
const fallbackProject = c.req.query("project");
|
|
533
561
|
const flat = c.req.query('flat') === '1';
|
|
534
562
|
const includeHeaders = c.req.query('headers') === '1';
|
|
535
563
|
const zip = new JSZip();
|
|
@@ -541,10 +569,15 @@ app.get("/api/export-zip/:id", async (c) => {
|
|
|
541
569
|
project = ref.slice(0, colonIdx);
|
|
542
570
|
docId = ref.slice(colonIdx + 1);
|
|
543
571
|
}
|
|
544
|
-
else {
|
|
545
|
-
project =
|
|
572
|
+
else if (fallbackProject) {
|
|
573
|
+
project = fallbackProject;
|
|
546
574
|
docId = ref;
|
|
547
575
|
}
|
|
576
|
+
else {
|
|
577
|
+
zip.file(`${ref}.md`, `# [Unqualified doc reference: ${ref} — no project given]
|
|
578
|
+
`);
|
|
579
|
+
continue;
|
|
580
|
+
}
|
|
548
581
|
const docFilePath = path.join(dataDir, "projects", project, ...docId.split("/")) + ".md";
|
|
549
582
|
let content;
|
|
550
583
|
if (!fs.existsSync(docFilePath)) {
|
|
@@ -704,8 +737,9 @@ app.get("/*", (c, next) => {
|
|
|
704
737
|
const indexPath = path.join(absoluteStaticRoot, "index.html");
|
|
705
738
|
if (fs.existsSync(indexPath)) {
|
|
706
739
|
// ?__pdf=1 : accès direct depuis Puppeteer — pas de préfixe proxy dans <base href>
|
|
740
|
+
// En mode standalone la GUI est déjà servie à la racine : même règle.
|
|
707
741
|
const isPdfRender = c.req.query('__pdf') === '1';
|
|
708
|
-
const baseHref = isPdfRender ? '/' : getHtmlBaseHref(
|
|
742
|
+
const baseHref = isStandalone || isPdfRender ? '/' : getHtmlBaseHref(effectiveAppPath);
|
|
709
743
|
const html = fs
|
|
710
744
|
.readFileSync(indexPath, "utf8")
|
|
711
745
|
.replace("<head>", `<head>\n <base href="${baseHref}" />`);
|
|
@@ -717,12 +751,54 @@ app.get("/*", (c, next) => {
|
|
|
717
751
|
return next();
|
|
718
752
|
}
|
|
719
753
|
});
|
|
754
|
+
/**
|
|
755
|
+
* Lance Hono sur `port` et prévient le master. `publicUrl` est l'URL que
|
|
756
|
+
* l'utilisateur doit ouvrir : via le proxy, ou directement en localhost.
|
|
757
|
+
*/
|
|
758
|
+
function listen(port, publicUrl, isProxied) {
|
|
759
|
+
logger.info(`Starting Hono server on port ${port}...`);
|
|
760
|
+
const serverInstance = serve({ fetch: app.fetch, port, hostname: "0.0.0.0" }, (info) => {
|
|
761
|
+
logger.info(`Dashboard server listening on port ${info.port}`);
|
|
762
|
+
safeSend(IPCMessageSchema.parse({
|
|
763
|
+
type: "READY",
|
|
764
|
+
data: { port: info.port, isProxied, url: publicUrl },
|
|
765
|
+
timestamp: new Date().toISOString(),
|
|
766
|
+
}));
|
|
767
|
+
});
|
|
768
|
+
serverInstance.on("error", (err) => {
|
|
769
|
+
if (err.code === "EADDRINUSE") {
|
|
770
|
+
// Sortie propre (code 0) : le master ne doit pas relancer un worker de plus.
|
|
771
|
+
logger.error(`Port ${port} already in use — another GUI instance is running.`);
|
|
772
|
+
safeSend(IPCMessageSchema.parse({
|
|
773
|
+
type: "ALREADY_RUNNING",
|
|
774
|
+
data: { url: publicUrl },
|
|
775
|
+
timestamp: new Date().toISOString(),
|
|
776
|
+
}));
|
|
777
|
+
process.exit(0);
|
|
778
|
+
}
|
|
779
|
+
logger.error(`Server error: ${err.message}`);
|
|
780
|
+
process.exit(1);
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
/** Sans proxy : la GUI écoute sur APP_PORT et est servie à la racine. */
|
|
784
|
+
function startStandalone(port) {
|
|
785
|
+
const publicUrl = `http://localhost:${port}`;
|
|
786
|
+
logger.info(`Standalone mode (no PROXY_URL): serving GUI on ${publicUrl}`);
|
|
787
|
+
localPort = port;
|
|
788
|
+
guiPublicUrl = publicUrl;
|
|
789
|
+
setupIpcErrorHandlers();
|
|
790
|
+
listen(port, publicUrl, false);
|
|
791
|
+
}
|
|
720
792
|
async function startServer() {
|
|
721
|
-
if (
|
|
722
|
-
process.stderr.write(
|
|
793
|
+
if (guiMode.kind === "disabled") {
|
|
794
|
+
process.stderr.write(`[GUI] ${guiMode.reason} — arrêt.\n`);
|
|
723
795
|
process.exit(0);
|
|
724
796
|
}
|
|
725
|
-
|
|
797
|
+
if (guiMode.kind === "standalone") {
|
|
798
|
+
startStandalone(guiMode.port);
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
const proxyUrl = guiMode.proxyUrl;
|
|
726
802
|
const appPath = configuredAppPath;
|
|
727
803
|
const appName = process.env.APP_NAME || "MCP Documentor GUI";
|
|
728
804
|
logger.info(`Starting server with proxyUrl=${proxyUrl}`);
|
|
@@ -731,8 +807,7 @@ async function startServer() {
|
|
|
731
807
|
try {
|
|
732
808
|
// APP_GROUP place la GUI dans une section repliable du proxy (facultatif).
|
|
733
809
|
const registerOptions = { path: appPath, name: appName, group: process.env.APP_GROUP };
|
|
734
|
-
|
|
735
|
-
result = await proxyClient.register(registerOptions, 0);
|
|
810
|
+
result = await proxyClient.register(registerOptions);
|
|
736
811
|
}
|
|
737
812
|
catch (e) {
|
|
738
813
|
process.stderr.write('[GUI] Enregistrement proxy échoué — arrêt.\n');
|
|
@@ -758,7 +833,8 @@ async function startServer() {
|
|
|
758
833
|
}
|
|
759
834
|
const finalPort = result.port;
|
|
760
835
|
localPort = finalPort;
|
|
761
|
-
|
|
836
|
+
effectiveAppPath = result.path ?? appPath;
|
|
837
|
+
const publicUrl = result.url || `${proxyUrl}${effectiveAppPath}`;
|
|
762
838
|
guiPublicUrl = publicUrl;
|
|
763
839
|
logger.info(`Registered with proxy. Public URL: ${publicUrl}, Local Port: ${finalPort}`);
|
|
764
840
|
const cleanupProxy = async () => {
|
|
@@ -769,19 +845,7 @@ async function startServer() {
|
|
|
769
845
|
process.on("SIGTERM", cleanupProxy);
|
|
770
846
|
process.on("SIGINT", cleanupProxy);
|
|
771
847
|
setupIpcErrorHandlers();
|
|
772
|
-
|
|
773
|
-
const serverInstance = serve({ fetch: app.fetch, port: finalPort, hostname: "0.0.0.0" }, (info) => {
|
|
774
|
-
logger.info(`Dashboard server listening on port ${info.port}`);
|
|
775
|
-
safeSend(IPCMessageSchema.parse({
|
|
776
|
-
type: "READY",
|
|
777
|
-
data: { port: info.port, isProxied: true, url: publicUrl },
|
|
778
|
-
timestamp: new Date().toISOString(),
|
|
779
|
-
}));
|
|
780
|
-
});
|
|
781
|
-
serverInstance.on("error", (err) => {
|
|
782
|
-
logger.error(`Server error: ${err.message}`);
|
|
783
|
-
process.exit(1);
|
|
784
|
-
});
|
|
848
|
+
listen(finalPort, publicUrl, true);
|
|
785
849
|
}
|
|
786
850
|
startServer().catch((err) => {
|
|
787
851
|
logger.error(`Failed to start GUI Worker: ${err.message}`);
|