@imenam/mcp-documentor 1.0.72 → 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/conflict-inspector.d.ts +13 -0
- package/dist/src/core/conflict-inspector.js +17 -0
- package/dist/src/core/git-sync.d.ts +11 -4
- package/dist/src/core/git-sync.js +33 -22
- 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 +100 -110
- package/dist/src/tools/check-links.js +1 -1
- package/dist/src/tools/conflicts.js +50 -5
- package/dist/src/tools/list-projects.js +7 -5
- package/dist/src/tools/sync.js +14 -7
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/gui/dist/assets/{arc-KWKRz6LO.js → arc-CsM5OJTM.js} +1 -1
- package/gui/dist/assets/{architectureDiagram-3BPJPVTR-Cjfl98ZS.js → architectureDiagram-3BPJPVTR-BwIu1llT.js} +1 -1
- package/gui/dist/assets/{blockDiagram-GPEHLZMM-CtPUrnse.js → blockDiagram-GPEHLZMM-BVijVnPI.js} +1 -1
- package/gui/dist/assets/{c4Diagram-AAUBKEIU-Dh5_7IZy.js → c4Diagram-AAUBKEIU-D-rcZUZu.js} +1 -1
- package/gui/dist/assets/channel-DKPwwuQf.js +1 -0
- package/gui/dist/assets/{chunk-2J33WTMH-44T5JbXy.js → chunk-2J33WTMH-ByfTGREe.js} +1 -1
- package/gui/dist/assets/{chunk-4BX2VUAB-D1fEQ0Ng.js → chunk-4BX2VUAB-B_5oCXyz.js} +1 -1
- package/gui/dist/assets/{chunk-55IACEB6-M2lEB2Vn.js → chunk-55IACEB6-Bc2Kq9HD.js} +1 -1
- package/gui/dist/assets/{chunk-727SXJPM-B3A_Fp4f.js → chunk-727SXJPM-CVwmKEMQ.js} +1 -1
- package/gui/dist/assets/{chunk-AQP2D5EJ-CqkJXv3c.js → chunk-AQP2D5EJ-BkMuEFxD.js} +1 -1
- package/gui/dist/assets/{chunk-FMBD7UC4-Iyj_Ky8e.js → chunk-FMBD7UC4-CU7EDl9I.js} +1 -1
- package/gui/dist/assets/{chunk-ND2GUHAM-DE5R-C9U.js → chunk-ND2GUHAM-1b34Dtkn.js} +1 -1
- package/gui/dist/assets/{chunk-QZHKN3VN-BekBU-YP.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-Dw2zOlxr.js → cose-bilkent-S5V4N54A-D9OBrxKr.js} +1 -1
- package/gui/dist/assets/{dagre-BM42HDAG-ebVZNyxI.js → dagre-BM42HDAG-zDqwL8xs.js} +1 -1
- package/gui/dist/assets/{diagram-2AECGRRQ-Bh5FUKy0.js → diagram-2AECGRRQ-ejbP8WIj.js} +1 -1
- package/gui/dist/assets/{diagram-5GNKFQAL-YJJTJy-Q.js → diagram-5GNKFQAL-Cv9WmtFO.js} +1 -1
- package/gui/dist/assets/{diagram-KO2AKTUF-D161kBvB.js → diagram-KO2AKTUF-BI0ybJiN.js} +1 -1
- package/gui/dist/assets/{diagram-LMA3HP47-Drcmx9J8.js → diagram-LMA3HP47-DlVGttLq.js} +1 -1
- package/gui/dist/assets/{diagram-OG6HWLK6-DRrxnW_H.js → diagram-OG6HWLK6-DjoAH-Fq.js} +1 -1
- package/gui/dist/assets/{erDiagram-TEJ5UH35-FrH4iEa3.js → erDiagram-TEJ5UH35-BVQHVGK4.js} +1 -1
- package/gui/dist/assets/{flowDiagram-I6XJVG4X-BMxSElvH.js → flowDiagram-I6XJVG4X-oZP2Z-6T.js} +1 -1
- package/gui/dist/assets/{ganttDiagram-6RSMTGT7-H09FGGUr.js → ganttDiagram-6RSMTGT7-dy4QyEOP.js} +1 -1
- package/gui/dist/assets/{gitGraphDiagram-PVQCEYII-DLhPfXNp.js → gitGraphDiagram-PVQCEYII-BxgLy7Ln.js} +1 -1
- package/gui/dist/assets/{index-CdGsMXYw.js → index-Dtv0slFQ.js} +97 -97
- package/gui/dist/assets/index-QkCDSQ2A.css +1 -0
- package/gui/dist/assets/{infoDiagram-5YYISTIA-CV8ctX8t.js → infoDiagram-5YYISTIA-DNOdinpR.js} +1 -1
- package/gui/dist/assets/{ishikawaDiagram-YF4QCWOH-BeuHUSPO.js → ishikawaDiagram-YF4QCWOH-C4pQ434G.js} +1 -1
- package/gui/dist/assets/{journeyDiagram-JHISSGLW-CfAHwYXQ.js → journeyDiagram-JHISSGLW-CBrVYW94.js} +1 -1
- package/gui/dist/assets/{kanban-definition-UN3LZRKU-CFF4KWbn.js → kanban-definition-UN3LZRKU-Df-rgiY8.js} +1 -1
- package/gui/dist/assets/{linear-DwZpGeRp.js → linear-B_6fRXVt.js} +1 -1
- package/gui/dist/assets/{mindmap-definition-RKZ34NQL-BW0yW4ap.js → mindmap-definition-RKZ34NQL-CRAd_VDM.js} +1 -1
- package/gui/dist/assets/{pieDiagram-4H26LBE5-plBCN_c7.js → pieDiagram-4H26LBE5-CJkmLrDI.js} +1 -1
- package/gui/dist/assets/{quadrantDiagram-W4KKPZXB-Dmp9sOq_.js → quadrantDiagram-W4KKPZXB-D-rDEcnF.js} +1 -1
- package/gui/dist/assets/{requirementDiagram-4Y6WPE33-CthKC15J.js → requirementDiagram-4Y6WPE33-DE8MhSMe.js} +1 -1
- package/gui/dist/assets/{sankeyDiagram-5OEKKPKP-BCAzzfPU.js → sankeyDiagram-5OEKKPKP-BwlxtrUs.js} +1 -1
- package/gui/dist/assets/{sequenceDiagram-3UESZ5HK-DgwOe82k.js → sequenceDiagram-3UESZ5HK-B4LjBzWg.js} +1 -1
- package/gui/dist/assets/{stateDiagram-AJRCARHV-B6k-x1TC.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-CqwqJkZw.js → timeline-definition-PNZ67QCA-CBxgf8iW.js} +1 -1
- package/gui/dist/assets/{vennDiagram-CIIHVFJN-REFDrI5t.js → vennDiagram-CIIHVFJN-DPSdK06q.js} +1 -1
- package/gui/dist/assets/{wardley-L42UT6IY-53gLZWfR.js → wardley-L42UT6IY-JIAzuoPm.js} +1 -1
- package/gui/dist/assets/{wardleyDiagram-YWT4CUSO-OPfKpNx-.js → wardleyDiagram-YWT4CUSO-YDfaF1wT.js} +1 -1
- package/gui/dist/assets/{xychartDiagram-2RQKCTM6-HWGySgFg.js → xychartDiagram-2RQKCTM6-DHlPryZX.js} +1 -1
- package/gui/dist/index.html +2 -2
- package/package.json +65 -65
- package/dist/src/tools/set-project.d.ts +0 -14
- package/dist/src/tools/set-project.js +0 -18
- package/gui/dist/assets/channel-JejxY436.js +0 -1
- package/gui/dist/assets/classDiagram-4FO5ZUOK-CdAqI_QA.js +0 -1
- package/gui/dist/assets/classDiagram-v2-Q7XG4LA2-CdAqI_QA.js +0 -1
- package/gui/dist/assets/index-DAzSbGj_.css +0 -1
- package/gui/dist/assets/stateDiagram-v2-BHNVJYJU-bfw3SR0t.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
|
}
|
|
@@ -20,6 +20,19 @@ export interface ConflictHunk {
|
|
|
20
20
|
remote: string[];
|
|
21
21
|
/** Common ancestor, present only when the markers were written in diff3 style. */
|
|
22
22
|
base?: string[];
|
|
23
|
+
/**
|
|
24
|
+
* The hunk concerns frontmatter, either because it sits inside the YAML block
|
|
25
|
+
* or because the doc merge driver hoisted a frontmatter disagreement to the
|
|
26
|
+
* top of the body. Keeping both sides of one of these would duplicate a key,
|
|
27
|
+
* so that choice is refused rather than silently producing broken YAML.
|
|
28
|
+
*/
|
|
29
|
+
inFrontmatter: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* The hunk is the block the doc merge driver hoisted out of the frontmatter.
|
|
32
|
+
* Its sides are `key: value` lines that belong back in the YAML, not in the
|
|
33
|
+
* body where the markers physically sit.
|
|
34
|
+
*/
|
|
35
|
+
hoisted: boolean;
|
|
23
36
|
}
|
|
24
37
|
export interface ParsedFile {
|
|
25
38
|
lines: string[];
|
|
@@ -34,6 +34,19 @@ export function hasConflictMarkers(text) {
|
|
|
34
34
|
function sidesFor(operation, first, second) {
|
|
35
35
|
return operation === "rebase" ? { local: second, remote: first } : { local: first, remote: second };
|
|
36
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* Line range of the YAML frontmatter block, if the file opens with one.
|
|
39
|
+
* Returns null when there is none.
|
|
40
|
+
*/
|
|
41
|
+
function frontmatterRange(lines) {
|
|
42
|
+
if (lines.length === 0 || lines[0].trim() !== "---")
|
|
43
|
+
return null;
|
|
44
|
+
for (let i = 1; i < lines.length; i++) {
|
|
45
|
+
if (lines[i].trim() === "---")
|
|
46
|
+
return { start: 0, end: i };
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
37
50
|
function sectionFor(lines, hunkStart) {
|
|
38
51
|
for (let i = hunkStart - 1; i >= 0; i--) {
|
|
39
52
|
const match = lines[i].match(HEADING);
|
|
@@ -45,6 +58,7 @@ function sectionFor(lines, hunkStart) {
|
|
|
45
58
|
/** Splits a conflicted file into hunks. Handles both diff3 and plain markers. */
|
|
46
59
|
export function parseConflicts(text, operation) {
|
|
47
60
|
const lines = splitLines(text);
|
|
61
|
+
const frontmatter = frontmatterRange(lines);
|
|
48
62
|
const hunks = [];
|
|
49
63
|
for (let i = 0; i < lines.length; i++) {
|
|
50
64
|
if (!START.test(lines[i]))
|
|
@@ -56,6 +70,7 @@ export function parseConflicts(text, operation) {
|
|
|
56
70
|
let sawBase = false;
|
|
57
71
|
let closed = false;
|
|
58
72
|
let j = i + 1;
|
|
73
|
+
const label = lines[i].match(START)?.[1] ?? "";
|
|
59
74
|
for (; j < lines.length; j++) {
|
|
60
75
|
const line = lines[j];
|
|
61
76
|
if (BASE.test(line)) {
|
|
@@ -84,6 +99,8 @@ export function parseConflicts(text, operation) {
|
|
|
84
99
|
local,
|
|
85
100
|
remote,
|
|
86
101
|
...(sawBase ? { base } : {}),
|
|
102
|
+
inFrontmatter: label.includes("frontmatter") || (frontmatter !== null && i >= frontmatter.start && i <= frontmatter.end),
|
|
103
|
+
hoisted: label.includes("frontmatter"),
|
|
87
104
|
});
|
|
88
105
|
i = j;
|
|
89
106
|
}
|
|
@@ -44,13 +44,11 @@ export type PullResult = {
|
|
|
44
44
|
} | {
|
|
45
45
|
status: "updated";
|
|
46
46
|
changed: string[];
|
|
47
|
-
|
|
48
|
-
status: "diverged";
|
|
49
|
-
ahead: number;
|
|
50
|
-
behind: number;
|
|
47
|
+
autoCommitted?: boolean;
|
|
51
48
|
} | {
|
|
52
49
|
status: "conflict";
|
|
53
50
|
conflicted: string[];
|
|
51
|
+
autoCommitted?: boolean;
|
|
54
52
|
};
|
|
55
53
|
export declare class SyncEngine {
|
|
56
54
|
private storage;
|
|
@@ -88,6 +86,15 @@ export declare class SyncEngine {
|
|
|
88
86
|
*/
|
|
89
87
|
pull(): Promise<PullResult>;
|
|
90
88
|
private diffNames;
|
|
89
|
+
/**
|
|
90
|
+
* Stages and commits whatever the working tree holds. Committing is what makes
|
|
91
|
+
* local edits safe to rebase over, so both push and pull go through here
|
|
92
|
+
* rather than asking the caller to tidy up first.
|
|
93
|
+
*
|
|
94
|
+
* .gitignore (written by ensureRepo) excludes all derived and machine-local
|
|
95
|
+
* state, so `add -A` only ever stages versioned files.
|
|
96
|
+
*/
|
|
97
|
+
private commitLocalChanges;
|
|
91
98
|
/** Stages versioned files, commits if there is anything to commit, and pushes. */
|
|
92
99
|
push(message?: string): Promise<{
|
|
93
100
|
pushed: boolean;
|
|
@@ -205,20 +205,23 @@ export class SyncEngine {
|
|
|
205
205
|
}
|
|
206
206
|
// Ensure upstream tracking so status ahead/behind is meaningful.
|
|
207
207
|
await this.git.branch(["--set-upstream-to", `origin/${cfg.branch}`, cfg.branch]).catch(() => { });
|
|
208
|
-
|
|
209
|
-
if (status.behind === 0)
|
|
208
|
+
let status = await this.git.status();
|
|
209
|
+
if (status.behind === 0 && status.files.length === 0)
|
|
210
210
|
return { status: "up-to-date" };
|
|
211
|
-
// Uncommitted local
|
|
212
|
-
//
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
211
|
+
// Uncommitted local edits cannot be rebased over, and reporting the
|
|
212
|
+
// divergence instead used to strand the caller: the pull asked for a push,
|
|
213
|
+
// which the remote rejected and which asked for a pull. Committing them
|
|
214
|
+
// resolves it - the edits become a commit the rebase can replay, nothing is
|
|
215
|
+
// published, and nothing can be clobbered.
|
|
216
|
+
const autoCommitted = status.files.length > 0 ? await this.commitLocalChanges() : false;
|
|
217
|
+
if (autoCommitted)
|
|
218
|
+
status = await this.git.status();
|
|
216
219
|
const before = await this.git.revparse(["HEAD"]).catch(() => "");
|
|
217
220
|
// Pure behind → fast-forward.
|
|
218
221
|
if (status.ahead === 0) {
|
|
219
222
|
await this.git.merge(["--ff-only", `origin/${cfg.branch}`]);
|
|
220
223
|
const after = await this.git.revparse(["HEAD"]).catch(() => "");
|
|
221
|
-
return { status: "updated", changed: await this.diffNames(before, after) };
|
|
224
|
+
return { status: "updated", changed: await this.diffNames(before, after), autoCommitted };
|
|
222
225
|
}
|
|
223
226
|
// Diverged with a clean tree → replay local commits on top of the remote. A
|
|
224
227
|
// conflict stops the rebase with both versions preserved in the index, so it
|
|
@@ -229,17 +232,35 @@ export class SyncEngine {
|
|
|
229
232
|
catch {
|
|
230
233
|
const after = await this.git.status();
|
|
231
234
|
if (after.conflicted.length > 0)
|
|
232
|
-
return { status: "conflict", conflicted: after.conflicted };
|
|
235
|
+
return { status: "conflict", conflicted: after.conflicted, autoCommitted };
|
|
233
236
|
throw new Error("Rebase failed without recorded conflicts. Inspect the data directory, or run `git rebase --abort` there to get back to the previous state.");
|
|
234
237
|
}
|
|
235
238
|
const rebased = await this.git.revparse(["HEAD"]).catch(() => "");
|
|
236
|
-
return { status: "updated", changed: await this.diffNames(before, rebased) };
|
|
239
|
+
return { status: "updated", changed: await this.diffNames(before, rebased), autoCommitted };
|
|
237
240
|
}
|
|
238
241
|
async diffNames(before, after) {
|
|
239
242
|
if (!before || !after || before === after)
|
|
240
243
|
return [];
|
|
241
244
|
return (await this.git.raw(["diff", "--name-only", before, after])).split("\n").filter(Boolean);
|
|
242
245
|
}
|
|
246
|
+
/**
|
|
247
|
+
* Stages and commits whatever the working tree holds. Committing is what makes
|
|
248
|
+
* local edits safe to rebase over, so both push and pull go through here
|
|
249
|
+
* rather than asking the caller to tidy up first.
|
|
250
|
+
*
|
|
251
|
+
* .gitignore (written by ensureRepo) excludes all derived and machine-local
|
|
252
|
+
* state, so `add -A` only ever stages versioned files.
|
|
253
|
+
*/
|
|
254
|
+
async commitLocalChanges(message) {
|
|
255
|
+
await this.git.add(["-A"]);
|
|
256
|
+
const status = await this.git.status();
|
|
257
|
+
const hasWork = status.staged.length > 0 || status.created.length > 0 || status.deleted.length > 0 || status.modified.length > 0;
|
|
258
|
+
if (!hasWork)
|
|
259
|
+
return false;
|
|
260
|
+
const machine = process.env.COMPUTERNAME || process.env.HOSTNAME || "unknown-machine";
|
|
261
|
+
await this.git.commit(message?.trim() || `docs sync from ${machine} · ${new Date().toISOString()}`);
|
|
262
|
+
return true;
|
|
263
|
+
}
|
|
243
264
|
/** Stages versioned files, commits if there is anything to commit, and pushes. */
|
|
244
265
|
async push(message) {
|
|
245
266
|
const cfg = this.requireConfig();
|
|
@@ -252,24 +273,14 @@ export class SyncEngine {
|
|
|
252
273
|
throw new Error(`A ${operation === "none" ? "conflict" : operation} is still unresolved (${pending.conflicted.length} conflicted file(s)). ` +
|
|
253
274
|
"Resolve it with resolve_sync_conflict before pushing, otherwise the conflict markers would be committed as documentation content.");
|
|
254
275
|
}
|
|
255
|
-
|
|
256
|
-
// so -A safely stages only versioned files (projects/, config.json, .gitignore).
|
|
257
|
-
await this.git.add(["-A"]);
|
|
258
|
-
const status = await this.git.status();
|
|
259
|
-
let committed = false;
|
|
260
|
-
if (status.staged.length > 0 || status.created.length > 0 || status.deleted.length > 0 || status.modified.length > 0) {
|
|
261
|
-
const machine = process.env.COMPUTERNAME || process.env.HOSTNAME || "unknown-machine";
|
|
262
|
-
const msg = message?.trim() || `docs sync from ${machine} · ${new Date().toISOString()}`;
|
|
263
|
-
await this.git.commit(msg);
|
|
264
|
-
committed = true;
|
|
265
|
-
}
|
|
276
|
+
const committed = await this.commitLocalChanges(message);
|
|
266
277
|
try {
|
|
267
278
|
await this.git.push(["-u", "origin", `HEAD:${cfg.branch}`]);
|
|
268
279
|
}
|
|
269
280
|
catch (e) {
|
|
270
281
|
const msg = e instanceof Error ? e.message : String(e);
|
|
271
282
|
if (/fetch first|non-fast-forward|\[rejected\]/i.test(msg)) {
|
|
272
|
-
throw new Error("Push rejected: the remote has commits you don't have locally. Run
|
|
283
|
+
throw new Error("Push rejected: the remote has commits you don't have locally. Run sync_docs, which integrates them and pushes in one step.");
|
|
273
284
|
}
|
|
274
285
|
throw e;
|
|
275
286
|
}
|
|
@@ -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);
|