@imenam/mcp-documentor 1.0.74 → 1.0.76
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 +91 -34
- 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,22 @@ 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
|
+
function resolveGuiMode() {
|
|
36
|
+
const proxyUrl = process.env.PROXY_URL?.trim();
|
|
37
|
+
if (proxyUrl)
|
|
38
|
+
return { kind: "proxy", proxyUrl };
|
|
39
|
+
const rawPort = process.env.APP_PORT?.trim();
|
|
40
|
+
if (!rawPort) {
|
|
41
|
+
return { kind: "disabled", reason: "PROXY_URL et APP_PORT non définis" };
|
|
42
|
+
}
|
|
43
|
+
const port = Number(rawPort);
|
|
44
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
45
|
+
return { kind: "disabled", reason: `APP_PORT invalide (${rawPort}) — attendu : entier entre 1 et 65535` };
|
|
46
|
+
}
|
|
47
|
+
return { kind: "standalone", port };
|
|
48
|
+
}
|
|
49
|
+
const guiMode = resolveGuiMode();
|
|
50
|
+
const isStandalone = guiMode.kind === "standalone";
|
|
35
51
|
setupLogging({ processLabel: "mcp-documentor:GUI", logDir: resolveLogDir(rootDir) });
|
|
36
52
|
const logger = createLogger("mcp-documentor:GUI");
|
|
37
53
|
process.on("uncaughtException", (err) => {
|
|
@@ -208,7 +224,7 @@ app.get("/proxy/health", (c) => {
|
|
|
208
224
|
const memory = process.memoryUsage();
|
|
209
225
|
return c.json({
|
|
210
226
|
status: "ok",
|
|
211
|
-
proxy: proxyClient ? proxyClient.getStatus() :
|
|
227
|
+
proxy: proxyClient ? proxyClient.getStatus() : isStandalone ? "standalone" : "error",
|
|
212
228
|
uptime: process.uptime(),
|
|
213
229
|
memory: { rss: memory.rss, heapUsed: memory.heapUsed },
|
|
214
230
|
ipc: process.connected ? "connected" : "disconnected",
|
|
@@ -343,22 +359,29 @@ app.get("/api/projects", async (c) => {
|
|
|
343
359
|
});
|
|
344
360
|
app.get("/api/folders", async (c) => {
|
|
345
361
|
const project = c.req.query("project");
|
|
346
|
-
|
|
362
|
+
if (!project)
|
|
363
|
+
return c.json({ error: "project query param is required" }, 400);
|
|
364
|
+
const result = await ipcRequest("LIST_FOLDERS", { project });
|
|
347
365
|
if (result.error)
|
|
348
366
|
return c.json({ error: result.error }, 500);
|
|
349
367
|
return c.json(result.data);
|
|
350
368
|
});
|
|
351
|
-
|
|
369
|
+
// Creates an empty project. Selecting a project is a client-side concern:
|
|
370
|
+
// the server holds no active project.
|
|
371
|
+
app.post("/api/projects", async (c) => {
|
|
352
372
|
const { project } = await c.req.json();
|
|
353
373
|
if (!project)
|
|
354
374
|
return c.json({ error: "project is required" }, 400);
|
|
355
|
-
const result = await ipcRequest("
|
|
375
|
+
const result = await ipcRequest("CREATE_PROJECT", { project });
|
|
356
376
|
if (result.error)
|
|
357
377
|
return c.json({ error: result.error }, 500);
|
|
358
378
|
return c.json(result.data);
|
|
359
379
|
});
|
|
360
380
|
app.get("/api/docs", async (c) => {
|
|
361
|
-
const
|
|
381
|
+
const project = c.req.query("project");
|
|
382
|
+
if (!project)
|
|
383
|
+
return c.json({ error: "project query param is required" }, 400);
|
|
384
|
+
const result = await ipcRequest("LIST_DOCS", { project });
|
|
362
385
|
if (result.error)
|
|
363
386
|
return c.json({ error: result.error }, 500);
|
|
364
387
|
return c.json(result.data);
|
|
@@ -475,10 +498,13 @@ app.get("/api/history-version/*", async (c) => {
|
|
|
475
498
|
return c.json(result.data);
|
|
476
499
|
});
|
|
477
500
|
app.post("/api/search", async (c) => {
|
|
478
|
-
const { query, top_k, all_projects } = await c.req.json();
|
|
501
|
+
const { query, top_k, all_projects, project } = await c.req.json();
|
|
479
502
|
if (!query)
|
|
480
503
|
return c.json({ error: "query is required" }, 400);
|
|
481
|
-
|
|
504
|
+
if (!all_projects && !project) {
|
|
505
|
+
return c.json({ error: "project is required unless all_projects is set" }, 400);
|
|
506
|
+
}
|
|
507
|
+
const result = await ipcRequest("SEARCH_DOCS", { query, top_k: top_k || 5, all_projects: !!all_projects, project });
|
|
482
508
|
if (result.error)
|
|
483
509
|
return c.json({ error: result.error }, 500);
|
|
484
510
|
return c.json(result.data);
|
|
@@ -522,14 +548,9 @@ app.get("/api/export-zip/:id", async (c) => {
|
|
|
522
548
|
const bundle = bundles.find((b) => b.id === id);
|
|
523
549
|
if (!bundle)
|
|
524
550
|
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 */ }
|
|
551
|
+
// Legacy bundles may hold bare doc ids; they are resolved against the project
|
|
552
|
+
// the caller is viewing. Fully qualified "project:doc-id" refs ignore it.
|
|
553
|
+
const fallbackProject = c.req.query("project");
|
|
533
554
|
const flat = c.req.query('flat') === '1';
|
|
534
555
|
const includeHeaders = c.req.query('headers') === '1';
|
|
535
556
|
const zip = new JSZip();
|
|
@@ -541,10 +562,15 @@ app.get("/api/export-zip/:id", async (c) => {
|
|
|
541
562
|
project = ref.slice(0, colonIdx);
|
|
542
563
|
docId = ref.slice(colonIdx + 1);
|
|
543
564
|
}
|
|
544
|
-
else {
|
|
545
|
-
project =
|
|
565
|
+
else if (fallbackProject) {
|
|
566
|
+
project = fallbackProject;
|
|
546
567
|
docId = ref;
|
|
547
568
|
}
|
|
569
|
+
else {
|
|
570
|
+
zip.file(`${ref}.md`, `# [Unqualified doc reference: ${ref} — no project given]
|
|
571
|
+
`);
|
|
572
|
+
continue;
|
|
573
|
+
}
|
|
548
574
|
const docFilePath = path.join(dataDir, "projects", project, ...docId.split("/")) + ".md";
|
|
549
575
|
let content;
|
|
550
576
|
if (!fs.existsSync(docFilePath)) {
|
|
@@ -704,8 +730,9 @@ app.get("/*", (c, next) => {
|
|
|
704
730
|
const indexPath = path.join(absoluteStaticRoot, "index.html");
|
|
705
731
|
if (fs.existsSync(indexPath)) {
|
|
706
732
|
// ?__pdf=1 : accès direct depuis Puppeteer — pas de préfixe proxy dans <base href>
|
|
733
|
+
// En mode standalone la GUI est déjà servie à la racine : même règle.
|
|
707
734
|
const isPdfRender = c.req.query('__pdf') === '1';
|
|
708
|
-
const baseHref = isPdfRender ? '/' : getHtmlBaseHref(configuredAppPath);
|
|
735
|
+
const baseHref = isStandalone || isPdfRender ? '/' : getHtmlBaseHref(configuredAppPath);
|
|
709
736
|
const html = fs
|
|
710
737
|
.readFileSync(indexPath, "utf8")
|
|
711
738
|
.replace("<head>", `<head>\n <base href="${baseHref}" />`);
|
|
@@ -717,12 +744,54 @@ app.get("/*", (c, next) => {
|
|
|
717
744
|
return next();
|
|
718
745
|
}
|
|
719
746
|
});
|
|
747
|
+
/**
|
|
748
|
+
* Lance Hono sur `port` et prévient le master. `publicUrl` est l'URL que
|
|
749
|
+
* l'utilisateur doit ouvrir : via le proxy, ou directement en localhost.
|
|
750
|
+
*/
|
|
751
|
+
function listen(port, publicUrl, isProxied) {
|
|
752
|
+
logger.info(`Starting Hono server on port ${port}...`);
|
|
753
|
+
const serverInstance = serve({ fetch: app.fetch, port, hostname: "0.0.0.0" }, (info) => {
|
|
754
|
+
logger.info(`Dashboard server listening on port ${info.port}`);
|
|
755
|
+
safeSend(IPCMessageSchema.parse({
|
|
756
|
+
type: "READY",
|
|
757
|
+
data: { port: info.port, isProxied, url: publicUrl },
|
|
758
|
+
timestamp: new Date().toISOString(),
|
|
759
|
+
}));
|
|
760
|
+
});
|
|
761
|
+
serverInstance.on("error", (err) => {
|
|
762
|
+
if (err.code === "EADDRINUSE") {
|
|
763
|
+
// Sortie propre (code 0) : le master ne doit pas relancer un worker de plus.
|
|
764
|
+
logger.error(`Port ${port} already in use — another GUI instance is running.`);
|
|
765
|
+
safeSend(IPCMessageSchema.parse({
|
|
766
|
+
type: "ALREADY_RUNNING",
|
|
767
|
+
data: { url: publicUrl },
|
|
768
|
+
timestamp: new Date().toISOString(),
|
|
769
|
+
}));
|
|
770
|
+
process.exit(0);
|
|
771
|
+
}
|
|
772
|
+
logger.error(`Server error: ${err.message}`);
|
|
773
|
+
process.exit(1);
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
/** Sans proxy : la GUI écoute sur APP_PORT et est servie à la racine. */
|
|
777
|
+
function startStandalone(port) {
|
|
778
|
+
const publicUrl = `http://localhost:${port}`;
|
|
779
|
+
logger.info(`Standalone mode (no PROXY_URL): serving GUI on ${publicUrl}`);
|
|
780
|
+
localPort = port;
|
|
781
|
+
guiPublicUrl = publicUrl;
|
|
782
|
+
setupIpcErrorHandlers();
|
|
783
|
+
listen(port, publicUrl, false);
|
|
784
|
+
}
|
|
720
785
|
async function startServer() {
|
|
721
|
-
if (
|
|
722
|
-
process.stderr.write(
|
|
786
|
+
if (guiMode.kind === "disabled") {
|
|
787
|
+
process.stderr.write(`[GUI] ${guiMode.reason} — arrêt.\n`);
|
|
723
788
|
process.exit(0);
|
|
724
789
|
}
|
|
725
|
-
|
|
790
|
+
if (guiMode.kind === "standalone") {
|
|
791
|
+
startStandalone(guiMode.port);
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
const proxyUrl = guiMode.proxyUrl;
|
|
726
795
|
const appPath = configuredAppPath;
|
|
727
796
|
const appName = process.env.APP_NAME || "MCP Documentor GUI";
|
|
728
797
|
logger.info(`Starting server with proxyUrl=${proxyUrl}`);
|
|
@@ -769,19 +838,7 @@ async function startServer() {
|
|
|
769
838
|
process.on("SIGTERM", cleanupProxy);
|
|
770
839
|
process.on("SIGINT", cleanupProxy);
|
|
771
840
|
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
|
-
});
|
|
841
|
+
listen(finalPort, publicUrl, true);
|
|
785
842
|
}
|
|
786
843
|
startServer().catch((err) => {
|
|
787
844
|
logger.error(`Failed to start GUI Worker: ${err.message}`);
|