@imenam/mcp-feature-analyzer 1.0.0
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/LICENSE +15 -0
- package/README.md +295 -0
- package/dist/shared/diagram-geometry.d.ts +45 -0
- package/dist/shared/diagram-geometry.js +120 -0
- package/dist/shared/diagram-layout.d.ts +98 -0
- package/dist/shared/diagram-layout.js +1156 -0
- package/dist/shared/diagram-quality.d.ts +30 -0
- package/dist/shared/diagram-quality.js +26 -0
- package/dist/shared/labels.d.ts +31 -0
- package/dist/shared/labels.js +53 -0
- package/dist/shared/mindmap-tree.d.ts +28 -0
- package/dist/shared/mindmap-tree.js +62 -0
- package/dist/shared/prompt.d.ts +17 -0
- package/dist/shared/prompt.js +97 -0
- package/dist/shared/review-state.d.ts +10 -0
- package/dist/shared/review-state.js +17 -0
- package/dist/shared/schemas/analysis.schema.d.ts +372 -0
- package/dist/shared/schemas/analysis.schema.js +152 -0
- package/dist/shared/schemas/api.schema.d.ts +92 -0
- package/dist/shared/schemas/api.schema.js +46 -0
- package/dist/shared/schemas/diff.schema.d.ts +91 -0
- package/dist/shared/schemas/diff.schema.js +37 -0
- package/dist/shared/schemas/ipc.schema.d.ts +29 -0
- package/dist/shared/schemas/ipc.schema.js +30 -0
- package/dist/shared/severity.d.ts +11 -0
- package/dist/shared/severity.js +17 -0
- package/dist/shared/text.d.ts +12 -0
- package/dist/shared/text.js +20 -0
- package/dist/src/cli/setup-mcp.d.ts +24 -0
- package/dist/src/cli/setup-mcp.js +145 -0
- package/dist/src/core/analysis-store.d.ts +95 -0
- package/dist/src/core/analysis-store.js +387 -0
- package/dist/src/core/data-dir.d.ts +5 -0
- package/dist/src/core/data-dir.js +11 -0
- package/dist/src/core/diff-parser.d.ts +22 -0
- package/dist/src/core/diff-parser.js +225 -0
- package/dist/src/core/errors.d.ts +16 -0
- package/dist/src/core/errors.js +28 -0
- package/dist/src/core/git.d.ts +35 -0
- package/dist/src/core/git.js +257 -0
- package/dist/src/core/ids.d.ts +16 -0
- package/dist/src/core/ids.js +41 -0
- package/dist/src/core/invariants.d.ts +24 -0
- package/dist/src/core/invariants.js +157 -0
- package/dist/src/core/locations.d.ts +30 -0
- package/dist/src/core/locations.js +66 -0
- package/dist/src/core/log-dir.d.ts +6 -0
- package/dist/src/core/log-dir.js +13 -0
- package/dist/src/core/migrate.d.ts +24 -0
- package/dist/src/core/migrate.js +53 -0
- package/dist/src/core/refresh.d.ts +30 -0
- package/dist/src/core/refresh.js +113 -0
- package/dist/src/core/request-guard.d.ts +32 -0
- package/dist/src/core/request-guard.js +72 -0
- package/dist/src/core/review-actions.d.ts +27 -0
- package/dist/src/core/review-actions.js +99 -0
- package/dist/src/gui-worker.d.ts +1 -0
- package/dist/src/gui-worker.js +401 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +658 -0
- package/dist/src/tools/add-findings.d.ts +9 -0
- package/dist/src/tools/add-findings.js +106 -0
- package/dist/src/tools/create-analysis.d.ts +4 -0
- package/dist/src/tools/create-analysis.js +36 -0
- package/dist/src/tools/delete-analysis.d.ts +4 -0
- package/dist/src/tools/delete-analysis.js +11 -0
- package/dist/src/tools/delete-diagram.d.ts +4 -0
- package/dist/src/tools/delete-diagram.js +22 -0
- package/dist/src/tools/delete-findings.d.ts +7 -0
- package/dist/src/tools/delete-findings.js +27 -0
- package/dist/src/tools/format.d.ts +27 -0
- package/dist/src/tools/format.js +92 -0
- package/dist/src/tools/get-analysis.d.ts +4 -0
- package/dist/src/tools/get-analysis.js +87 -0
- package/dist/src/tools/get-diff.d.ts +4 -0
- package/dist/src/tools/get-diff.js +67 -0
- package/dist/src/tools/get-review-feedback.d.ts +4 -0
- package/dist/src/tools/get-review-feedback.js +71 -0
- package/dist/src/tools/inputs.d.ts +14 -0
- package/dist/src/tools/inputs.js +40 -0
- package/dist/src/tools/list-analyses.d.ts +8 -0
- package/dist/src/tools/list-analyses.js +71 -0
- package/dist/src/tools/refresh-analysis.d.ts +7 -0
- package/dist/src/tools/refresh-analysis.js +39 -0
- package/dist/src/tools/set-diagram.d.ts +16 -0
- package/dist/src/tools/set-diagram.js +207 -0
- package/dist/src/tools/types.d.ts +52 -0
- package/dist/src/tools/types.js +133 -0
- package/dist/src/tools/update-analysis.d.ts +4 -0
- package/dist/src/tools/update-analysis.js +121 -0
- package/dist/src/tools/update-finding.d.ts +8 -0
- package/dist/src/tools/update-finding.js +91 -0
- package/dist/src/version.d.ts +1 -0
- package/dist/src/version.js +2 -0
- package/gui/dist/assets/index-CU9pNYDp.js +72 -0
- package/gui/dist/assets/index-Du5eL78F.css +1 -0
- package/gui/dist/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- package/gui/dist/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- package/gui/dist/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- package/gui/dist/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- package/gui/dist/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- package/gui/dist/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- package/gui/dist/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- package/gui/dist/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
- package/gui/dist/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
- package/gui/dist/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
- package/gui/dist/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
- package/gui/dist/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
- package/gui/dist/index.html +13 -0
- package/package.json +56 -0
- package/scripts/generate-version.js +20 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Taille maximale, en octets, du contenu « nouveau côté » conservé dans le
|
|
4
|
+
* snapshot pour un fichier. Au-delà, `newContent` vaut null et le fichier
|
|
5
|
+
* n'accepte pas de constat ancré sur ses lignes.
|
|
6
|
+
*/
|
|
7
|
+
export declare const MAX_SNAPSHOT_FILE_BYTES: number;
|
|
8
|
+
export declare const DIFF_LINE_TYPES: readonly ["context", "add", "del"];
|
|
9
|
+
/** Ligne d'un hunk ; le texte ne contient ni préfixe de diff ni fin de ligne (`\n` ou `\r\n`). */
|
|
10
|
+
export declare const DiffLineSchema: z.ZodObject<{
|
|
11
|
+
type: z.ZodEnum<{
|
|
12
|
+
add: "add";
|
|
13
|
+
context: "context";
|
|
14
|
+
del: "del";
|
|
15
|
+
}>;
|
|
16
|
+
oldNo: z.ZodNullable<z.ZodNumber>;
|
|
17
|
+
newNo: z.ZodNullable<z.ZodNumber>;
|
|
18
|
+
text: z.ZodString;
|
|
19
|
+
}, z.core.$strip>;
|
|
20
|
+
export declare const HunkSchema: z.ZodObject<{
|
|
21
|
+
header: z.ZodString;
|
|
22
|
+
oldStart: z.ZodNumber;
|
|
23
|
+
oldLines: z.ZodNumber;
|
|
24
|
+
newStart: z.ZodNumber;
|
|
25
|
+
newLines: z.ZodNumber;
|
|
26
|
+
lines: z.ZodArray<z.ZodObject<{
|
|
27
|
+
type: z.ZodEnum<{
|
|
28
|
+
add: "add";
|
|
29
|
+
context: "context";
|
|
30
|
+
del: "del";
|
|
31
|
+
}>;
|
|
32
|
+
oldNo: z.ZodNullable<z.ZodNumber>;
|
|
33
|
+
newNo: z.ZodNullable<z.ZodNumber>;
|
|
34
|
+
text: z.ZodString;
|
|
35
|
+
}, z.core.$strip>>;
|
|
36
|
+
}, z.core.$strip>;
|
|
37
|
+
/**
|
|
38
|
+
* Diff figé d'un fichier. `newContent` est le texte du côté « nouveau » au
|
|
39
|
+
* moment du snapshot ; il vaut null pour un fichier supprimé, binaire ou plus
|
|
40
|
+
* gros que MAX_SNAPSHOT_FILE_BYTES.
|
|
41
|
+
*/
|
|
42
|
+
export declare const FileDiffSchema: z.ZodObject<{
|
|
43
|
+
path: z.ZodString;
|
|
44
|
+
hunks: z.ZodArray<z.ZodObject<{
|
|
45
|
+
header: z.ZodString;
|
|
46
|
+
oldStart: z.ZodNumber;
|
|
47
|
+
oldLines: z.ZodNumber;
|
|
48
|
+
newStart: z.ZodNumber;
|
|
49
|
+
newLines: z.ZodNumber;
|
|
50
|
+
lines: z.ZodArray<z.ZodObject<{
|
|
51
|
+
type: z.ZodEnum<{
|
|
52
|
+
add: "add";
|
|
53
|
+
context: "context";
|
|
54
|
+
del: "del";
|
|
55
|
+
}>;
|
|
56
|
+
oldNo: z.ZodNullable<z.ZodNumber>;
|
|
57
|
+
newNo: z.ZodNullable<z.ZodNumber>;
|
|
58
|
+
text: z.ZodString;
|
|
59
|
+
}, z.core.$strip>>;
|
|
60
|
+
}, z.core.$strip>>;
|
|
61
|
+
newContent: z.ZodNullable<z.ZodString>;
|
|
62
|
+
}, z.core.$strip>;
|
|
63
|
+
export declare const DiffSnapshotSchema: z.ZodObject<{
|
|
64
|
+
analysisId: z.ZodString;
|
|
65
|
+
files: z.ZodArray<z.ZodObject<{
|
|
66
|
+
path: z.ZodString;
|
|
67
|
+
hunks: z.ZodArray<z.ZodObject<{
|
|
68
|
+
header: z.ZodString;
|
|
69
|
+
oldStart: z.ZodNumber;
|
|
70
|
+
oldLines: z.ZodNumber;
|
|
71
|
+
newStart: z.ZodNumber;
|
|
72
|
+
newLines: z.ZodNumber;
|
|
73
|
+
lines: z.ZodArray<z.ZodObject<{
|
|
74
|
+
type: z.ZodEnum<{
|
|
75
|
+
add: "add";
|
|
76
|
+
context: "context";
|
|
77
|
+
del: "del";
|
|
78
|
+
}>;
|
|
79
|
+
oldNo: z.ZodNullable<z.ZodNumber>;
|
|
80
|
+
newNo: z.ZodNullable<z.ZodNumber>;
|
|
81
|
+
text: z.ZodString;
|
|
82
|
+
}, z.core.$strip>>;
|
|
83
|
+
}, z.core.$strip>>;
|
|
84
|
+
newContent: z.ZodNullable<z.ZodString>;
|
|
85
|
+
}, z.core.$strip>>;
|
|
86
|
+
}, z.core.$strip>;
|
|
87
|
+
export type DiffLineType = (typeof DIFF_LINE_TYPES)[number];
|
|
88
|
+
export type DiffLine = z.infer<typeof DiffLineSchema>;
|
|
89
|
+
export type Hunk = z.infer<typeof HunkSchema>;
|
|
90
|
+
export type FileDiff = z.infer<typeof FileDiffSchema>;
|
|
91
|
+
export type DiffSnapshot = z.infer<typeof DiffSnapshotSchema>;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Taille maximale, en octets, du contenu « nouveau côté » conservé dans le
|
|
4
|
+
* snapshot pour un fichier. Au-delà, `newContent` vaut null et le fichier
|
|
5
|
+
* n'accepte pas de constat ancré sur ses lignes.
|
|
6
|
+
*/
|
|
7
|
+
export const MAX_SNAPSHOT_FILE_BYTES = 1024 * 1024;
|
|
8
|
+
export const DIFF_LINE_TYPES = ["context", "add", "del"];
|
|
9
|
+
/** Ligne d'un hunk ; le texte ne contient ni préfixe de diff ni fin de ligne (`\n` ou `\r\n`). */
|
|
10
|
+
export const DiffLineSchema = z.object({
|
|
11
|
+
type: z.enum(DIFF_LINE_TYPES),
|
|
12
|
+
oldNo: z.number().int().positive().nullable(),
|
|
13
|
+
newNo: z.number().int().positive().nullable(),
|
|
14
|
+
text: z.string(),
|
|
15
|
+
});
|
|
16
|
+
export const HunkSchema = z.object({
|
|
17
|
+
header: z.string(),
|
|
18
|
+
oldStart: z.number().int().nonnegative(),
|
|
19
|
+
oldLines: z.number().int().nonnegative(),
|
|
20
|
+
newStart: z.number().int().nonnegative(),
|
|
21
|
+
newLines: z.number().int().nonnegative(),
|
|
22
|
+
lines: z.array(DiffLineSchema),
|
|
23
|
+
});
|
|
24
|
+
/**
|
|
25
|
+
* Diff figé d'un fichier. `newContent` est le texte du côté « nouveau » au
|
|
26
|
+
* moment du snapshot ; il vaut null pour un fichier supprimé, binaire ou plus
|
|
27
|
+
* gros que MAX_SNAPSHOT_FILE_BYTES.
|
|
28
|
+
*/
|
|
29
|
+
export const FileDiffSchema = z.object({
|
|
30
|
+
path: z.string().min(1),
|
|
31
|
+
hunks: z.array(HunkSchema),
|
|
32
|
+
newContent: z.string().nullable(),
|
|
33
|
+
});
|
|
34
|
+
export const DiffSnapshotSchema = z.object({
|
|
35
|
+
analysisId: z.string().min(1),
|
|
36
|
+
files: z.array(FileDiffSchema),
|
|
37
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Nature d'une erreur renvoyée par le maître à une requête du GUI worker, qui
|
|
4
|
+
* la traduit en code HTTP : invalid → 400, not_found → 404, conflict → 409,
|
|
5
|
+
* internal → 500.
|
|
6
|
+
*/
|
|
7
|
+
export declare const IPC_ERROR_KINDS: readonly ["invalid", "not_found", "conflict", "internal"];
|
|
8
|
+
export type IpcErrorKind = (typeof IPC_ERROR_KINDS)[number];
|
|
9
|
+
/** Message échangé entre le processus maître MCP et le GUI worker. */
|
|
10
|
+
export declare const IPCMessageSchema: z.ZodObject<{
|
|
11
|
+
type: z.ZodString;
|
|
12
|
+
correlationId: z.ZodOptional<z.ZodString>;
|
|
13
|
+
data: z.ZodOptional<z.ZodAny>;
|
|
14
|
+
error: z.ZodOptional<z.ZodString>;
|
|
15
|
+
errorKind: z.ZodOptional<z.ZodEnum<{
|
|
16
|
+
conflict: "conflict";
|
|
17
|
+
internal: "internal";
|
|
18
|
+
invalid: "invalid";
|
|
19
|
+
not_found: "not_found";
|
|
20
|
+
}>>;
|
|
21
|
+
timestamp: z.ZodString;
|
|
22
|
+
}, z.core.$strip>;
|
|
23
|
+
export type IPCMessage = z.infer<typeof IPCMessageSchema>;
|
|
24
|
+
/** Types d'IPC envoyés par le maître et relayés tels quels aux clients SSE de la GUI. */
|
|
25
|
+
export declare const PUSHED_EVENT_TYPES: readonly ["INITIAL_STATE", "ANALYSES_UPDATED", "ANALYSIS_UPDATED"];
|
|
26
|
+
export type PushedEventType = (typeof PUSHED_EVENT_TYPES)[number];
|
|
27
|
+
/** Requêtes du GUI worker au maître ; chacune reçoit une réponse de même `correlationId`. */
|
|
28
|
+
export declare const GUI_REQUEST_TYPES: readonly ["GET_CONFIG", "LIST_ANALYSES", "GET_ANALYSIS", "GET_DIFF", "SET_FILE_REVIEWED", "SET_FINDING_STATUS", "ADD_NOTE", "DELETE_NOTE", "SUBMIT_REVIEW"];
|
|
29
|
+
export type GuiRequestType = (typeof GUI_REQUEST_TYPES)[number];
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Nature d'une erreur renvoyée par le maître à une requête du GUI worker, qui
|
|
4
|
+
* la traduit en code HTTP : invalid → 400, not_found → 404, conflict → 409,
|
|
5
|
+
* internal → 500.
|
|
6
|
+
*/
|
|
7
|
+
export const IPC_ERROR_KINDS = ["invalid", "not_found", "conflict", "internal"];
|
|
8
|
+
/** Message échangé entre le processus maître MCP et le GUI worker. */
|
|
9
|
+
export const IPCMessageSchema = z.object({
|
|
10
|
+
type: z.string(),
|
|
11
|
+
correlationId: z.string().optional(),
|
|
12
|
+
data: z.any().optional(),
|
|
13
|
+
error: z.string().optional(),
|
|
14
|
+
errorKind: z.enum(IPC_ERROR_KINDS).optional(),
|
|
15
|
+
timestamp: z.string(),
|
|
16
|
+
});
|
|
17
|
+
/** Types d'IPC envoyés par le maître et relayés tels quels aux clients SSE de la GUI. */
|
|
18
|
+
export const PUSHED_EVENT_TYPES = ["INITIAL_STATE", "ANALYSES_UPDATED", "ANALYSIS_UPDATED"];
|
|
19
|
+
/** Requêtes du GUI worker au maître ; chacune reçoit une réponse de même `correlationId`. */
|
|
20
|
+
export const GUI_REQUEST_TYPES = [
|
|
21
|
+
"GET_CONFIG",
|
|
22
|
+
"LIST_ANALYSES",
|
|
23
|
+
"GET_ANALYSIS",
|
|
24
|
+
"GET_DIFF",
|
|
25
|
+
"SET_FILE_REVIEWED",
|
|
26
|
+
"SET_FINDING_STATUS",
|
|
27
|
+
"ADD_NOTE",
|
|
28
|
+
"DELETE_NOTE",
|
|
29
|
+
"SUBMIT_REVIEW",
|
|
30
|
+
];
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type Severity } from "./schemas/analysis.schema.js";
|
|
2
|
+
/** Rang d'une gravité : 0 pour la plus grave. */
|
|
3
|
+
export declare function severityRank(severity: Severity): number;
|
|
4
|
+
/** Comparateur qui trie de la plus grave à la moins grave. */
|
|
5
|
+
export declare function compareSeverity(a: Severity, b: Severity): number;
|
|
6
|
+
/** Copie triée de la plus grave à la moins grave ; à gravité égale, l'ordre d'origine est conservé. */
|
|
7
|
+
export declare function sortBySeverity<T extends {
|
|
8
|
+
severity: Severity;
|
|
9
|
+
}>(items: readonly T[]): T[];
|
|
10
|
+
/** Compteur à zéro pour chaque gravité. */
|
|
11
|
+
export declare function emptySeverityCounts(): Record<Severity, number>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { SEVERITIES } from "./schemas/analysis.schema.js";
|
|
2
|
+
/** Rang d'une gravité : 0 pour la plus grave. */
|
|
3
|
+
export function severityRank(severity) {
|
|
4
|
+
return SEVERITIES.indexOf(severity);
|
|
5
|
+
}
|
|
6
|
+
/** Comparateur qui trie de la plus grave à la moins grave. */
|
|
7
|
+
export function compareSeverity(a, b) {
|
|
8
|
+
return severityRank(a) - severityRank(b);
|
|
9
|
+
}
|
|
10
|
+
/** Copie triée de la plus grave à la moins grave ; à gravité égale, l'ordre d'origine est conservé. */
|
|
11
|
+
export function sortBySeverity(items) {
|
|
12
|
+
return [...items].sort((a, b) => compareSeverity(a.severity, b.severity));
|
|
13
|
+
}
|
|
14
|
+
/** Compteur à zéro pour chaque gravité. */
|
|
15
|
+
export function emptySeverityCounts() {
|
|
16
|
+
return { critical: 0, major: 0, minor: 0, trivial: 0 };
|
|
17
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Découpe un texte en lignes. Les fins de ligne `\n` et `\r\n` sont retirées ;
|
|
3
|
+
* une fin de ligne finale ne crée pas de ligne vide supplémentaire.
|
|
4
|
+
*/
|
|
5
|
+
export declare function splitLines(content: string): string[];
|
|
6
|
+
/** Plage de lignes : « 12 », ou « 12-14 » avec `separator` entre les bornes. */
|
|
7
|
+
export declare function lineRange(startLine: number, endLine: number, separator?: string): string;
|
|
8
|
+
/** Emplacement d'une remarque : « chemin » pour un fichier entier, « chemin:ligne » sinon. */
|
|
9
|
+
export declare function noteLocationText(location: {
|
|
10
|
+
path: string;
|
|
11
|
+
line: number | null;
|
|
12
|
+
}): string;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Découpe un texte en lignes. Les fins de ligne `\n` et `\r\n` sont retirées ;
|
|
3
|
+
* une fin de ligne finale ne crée pas de ligne vide supplémentaire.
|
|
4
|
+
*/
|
|
5
|
+
export function splitLines(content) {
|
|
6
|
+
if (content === "")
|
|
7
|
+
return [];
|
|
8
|
+
const lines = content.split("\n").map((line) => (line.endsWith("\r") ? line.slice(0, -1) : line));
|
|
9
|
+
if (lines[lines.length - 1] === "" && content.endsWith("\n"))
|
|
10
|
+
lines.pop();
|
|
11
|
+
return lines;
|
|
12
|
+
}
|
|
13
|
+
/** Plage de lignes : « 12 », ou « 12-14 » avec `separator` entre les bornes. */
|
|
14
|
+
export function lineRange(startLine, endLine, separator = "-") {
|
|
15
|
+
return startLine === endLine ? `${startLine}` : `${startLine}${separator}${endLine}`;
|
|
16
|
+
}
|
|
17
|
+
/** Emplacement d'une remarque : « chemin » pour un fichier entier, « chemin:ligne » sinon. */
|
|
18
|
+
export function noteLocationText(location) {
|
|
19
|
+
return location.line === null ? location.path : `${location.path}:${location.line}`;
|
|
20
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Comment la GUI sera exposée. `undefined` des deux côtés = rien n'a été passé
|
|
3
|
+
* en ligne de commande : on retombe sur le placeholder PROXY_URL à compléter.
|
|
4
|
+
*/
|
|
5
|
+
export interface GuiExposure {
|
|
6
|
+
proxyUrl?: string;
|
|
7
|
+
appPort?: number;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Traduit `--proxy_url=` / `--app_port=` en mode d'exposition de la GUI.
|
|
11
|
+
* Les deux ensemble sont refusés : au runtime PROXY_URL primerait toujours,
|
|
12
|
+
* donc un fichier portant les deux serait silencieusement trompeur.
|
|
13
|
+
*/
|
|
14
|
+
export declare function parseGuiExposure(argv: string[]): {
|
|
15
|
+
exposure: GuiExposure;
|
|
16
|
+
} | {
|
|
17
|
+
error: string;
|
|
18
|
+
};
|
|
19
|
+
export interface SetupMcpOptions {
|
|
20
|
+
cwd: string;
|
|
21
|
+
force: boolean;
|
|
22
|
+
exposure?: GuiExposure;
|
|
23
|
+
}
|
|
24
|
+
export declare function runSetupMcp({ cwd, force, exposure }: SetupMcpOptions): number;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
const SERVER_NAME = "mcp-feature-analyzer";
|
|
4
|
+
const PACKAGE_NAME = "@imenam/mcp-feature-analyzer";
|
|
5
|
+
/** Slashes POSIX : évite les backslashes échappés dans le JSON, et Node les accepte sous Windows. */
|
|
6
|
+
function toPosixPath(dir) {
|
|
7
|
+
return dir.replace(/\\/g, "/");
|
|
8
|
+
}
|
|
9
|
+
/** "mon-projet" -> "Mon-projet" */
|
|
10
|
+
function capitalize(value) {
|
|
11
|
+
return value.charAt(0).toUpperCase() + value.slice(1);
|
|
12
|
+
}
|
|
13
|
+
/** "mon_projet-web" -> "Mon Projet Web" */
|
|
14
|
+
function toDisplayName(folder) {
|
|
15
|
+
return folder.split(/[-_]+/).filter(Boolean).map(capitalize).join(" ");
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Valeurs volontairement vides : chaque variable est inactive tant qu'elle n'est
|
|
19
|
+
* pas remplie (PROXY_URL et APP_PORT vides = GUI désactivée), donc le fichier
|
|
20
|
+
* généré est utilisable tel quel, jamais à moitié cassé.
|
|
21
|
+
*/
|
|
22
|
+
function buildServerEntry(codeRoot, exposure) {
|
|
23
|
+
const folder = path.basename(codeRoot) || "feature-analyzer";
|
|
24
|
+
// Un port explicite désactive le placeholder proxy : les deux variables ne
|
|
25
|
+
// doivent jamais être renseignées ensemble, PROXY_URL primant au runtime.
|
|
26
|
+
const proxyUrl = exposure.appPort !== undefined ? "" : exposure.proxyUrl ?? "http://localhost:";
|
|
27
|
+
return {
|
|
28
|
+
command: "npx",
|
|
29
|
+
args: ["-y", PACKAGE_NAME],
|
|
30
|
+
env: {
|
|
31
|
+
PROXY_URL: proxyUrl,
|
|
32
|
+
APP_PORT: exposure.appPort !== undefined ? String(exposure.appPort) : "",
|
|
33
|
+
APP_GROUP: capitalize(folder),
|
|
34
|
+
APP_NAME: `Feature Analyzer ${toDisplayName(folder)}`,
|
|
35
|
+
APP_PATH: `/feature-analyzer-${folder}`,
|
|
36
|
+
MCP_FEATURE_ANALYZER_DATA_DIR: toPosixPath(path.join(codeRoot, ".feature-analyzer-data")),
|
|
37
|
+
MCP_LOG_DIR: "",
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
const GENERATED_VARS = [
|
|
42
|
+
["PROXY_URL", "URL du proxy pour exposer la GUI (compléter le port)"],
|
|
43
|
+
["APP_PORT", "port local si aucun proxy — la GUI est servie sur http://localhost:<port>"],
|
|
44
|
+
["APP_GROUP", "groupe d'affichage dans le dashboard du proxy"],
|
|
45
|
+
["APP_NAME", "nom affiché de la GUI dans le dashboard du proxy"],
|
|
46
|
+
["APP_PATH", "chemin de montage de la GUI derrière le proxy"],
|
|
47
|
+
["MCP_FEATURE_ANALYZER_DATA_DIR", "répertoire de stockage des analyses (pré-rempli)"],
|
|
48
|
+
["MCP_LOG_DIR", "répertoire des logs (défaut : <data>/logs)"],
|
|
49
|
+
];
|
|
50
|
+
/** Lit `--flag=valeur` (tirets ou underscores acceptés). */
|
|
51
|
+
function readFlag(argv, name) {
|
|
52
|
+
const prefixes = [`--${name}=`, `--${name.replace(/_/g, "-")}=`];
|
|
53
|
+
for (const arg of argv) {
|
|
54
|
+
const prefix = prefixes.find((p) => arg.startsWith(p));
|
|
55
|
+
if (prefix)
|
|
56
|
+
return arg.slice(prefix.length).trim();
|
|
57
|
+
}
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Traduit `--proxy_url=` / `--app_port=` en mode d'exposition de la GUI.
|
|
62
|
+
* Les deux ensemble sont refusés : au runtime PROXY_URL primerait toujours,
|
|
63
|
+
* donc un fichier portant les deux serait silencieusement trompeur.
|
|
64
|
+
*/
|
|
65
|
+
export function parseGuiExposure(argv) {
|
|
66
|
+
const rawProxyUrl = readFlag(argv, "proxy_url");
|
|
67
|
+
const rawAppPort = readFlag(argv, "app_port");
|
|
68
|
+
if (rawProxyUrl !== undefined && rawAppPort !== undefined) {
|
|
69
|
+
return { error: "--proxy_url et --app_port sont exclusifs : la GUI passe par le proxy, ou écoute sur un port local." };
|
|
70
|
+
}
|
|
71
|
+
if (rawProxyUrl !== undefined) {
|
|
72
|
+
if (rawProxyUrl === "")
|
|
73
|
+
return { error: "--proxy_url attend une URL, ex : --proxy_url=http://localhost:3000" };
|
|
74
|
+
let parsed;
|
|
75
|
+
try {
|
|
76
|
+
parsed = new URL(rawProxyUrl);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return { error: `--proxy_url invalide : "${rawProxyUrl}" n'est pas une URL absolue (ex : http://localhost:3000).` };
|
|
80
|
+
}
|
|
81
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
82
|
+
return { error: `--proxy_url invalide : protocole "${parsed.protocol}" non supporté (http ou https attendu).` };
|
|
83
|
+
}
|
|
84
|
+
// Une barre finale casserait la concaténation PROXY_URL + APP_PATH.
|
|
85
|
+
return { exposure: { proxyUrl: rawProxyUrl.replace(/\/+$/, "") } };
|
|
86
|
+
}
|
|
87
|
+
if (rawAppPort !== undefined) {
|
|
88
|
+
const port = Number(rawAppPort);
|
|
89
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
90
|
+
return { error: `--app_port invalide : "${rawAppPort}" n'est pas un entier entre 1 et 65535.` };
|
|
91
|
+
}
|
|
92
|
+
return { exposure: { appPort: port } };
|
|
93
|
+
}
|
|
94
|
+
return { exposure: {} };
|
|
95
|
+
}
|
|
96
|
+
export function runSetupMcp({ cwd, force, exposure = {} }) {
|
|
97
|
+
const target = path.join(cwd, ".mcp.json");
|
|
98
|
+
let config = {};
|
|
99
|
+
const exists = fs.existsSync(target);
|
|
100
|
+
if (exists) {
|
|
101
|
+
const raw = fs.readFileSync(target, "utf-8").trim();
|
|
102
|
+
if (raw !== "") {
|
|
103
|
+
try {
|
|
104
|
+
config = JSON.parse(raw);
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
console.error(`Erreur : ${target} contient du JSON invalide (${err.message}).`);
|
|
108
|
+
console.error("Corrigez le fichier ou supprimez-le, puis relancez la commande.");
|
|
109
|
+
return 1;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (typeof config !== "object" || config === null || Array.isArray(config)) {
|
|
113
|
+
console.error(`Erreur : ${target} ne contient pas un objet JSON.`);
|
|
114
|
+
return 1;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (typeof config.mcpServers !== "object" || config.mcpServers === null || Array.isArray(config.mcpServers)) {
|
|
118
|
+
config.mcpServers = {};
|
|
119
|
+
}
|
|
120
|
+
const alreadyPresent = Object.prototype.hasOwnProperty.call(config.mcpServers, SERVER_NAME);
|
|
121
|
+
if (alreadyPresent && !force) {
|
|
122
|
+
console.error(`Le serveur "${SERVER_NAME}" est déjà configuré dans ${target}.`);
|
|
123
|
+
console.error("Relancez avec --force pour écraser cette entrée (les autres serveurs sont préservés).");
|
|
124
|
+
return 1;
|
|
125
|
+
}
|
|
126
|
+
config.mcpServers[SERVER_NAME] = buildServerEntry(cwd, exposure);
|
|
127
|
+
fs.writeFileSync(target, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
128
|
+
const action = alreadyPresent ? "remplacée" : exists ? "ajoutée" : "créée";
|
|
129
|
+
console.log(`Configuration ${SERVER_NAME} ${action} dans ${target}`);
|
|
130
|
+
console.log("");
|
|
131
|
+
if (exposure.appPort !== undefined) {
|
|
132
|
+
console.log(`GUI en accès direct : http://localhost:${exposure.appPort} (sans proxy).`);
|
|
133
|
+
}
|
|
134
|
+
else if (exposure.proxyUrl !== undefined) {
|
|
135
|
+
console.log(`GUI exposée via le proxy ${exposure.proxyUrl}.`);
|
|
136
|
+
}
|
|
137
|
+
console.log('À compléter dans le champ "env" :');
|
|
138
|
+
for (const [name, description] of GENERATED_VARS) {
|
|
139
|
+
console.log(` ${name.padEnd(30)} ${description}`);
|
|
140
|
+
}
|
|
141
|
+
console.log("");
|
|
142
|
+
console.log("Une variable laissée vide est simplement inactive.");
|
|
143
|
+
console.log("PROXY_URL et APP_PORT s'excluent : PROXY_URL prime si les deux sont renseignés.");
|
|
144
|
+
return 0;
|
|
145
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { type Analysis, type AnalysisRequest, type AnalysisSummary, type Editor, type Review } from "../../shared/schemas/analysis.schema.js";
|
|
2
|
+
import { type DiffSnapshot } from "../../shared/schemas/diff.schema.js";
|
|
3
|
+
import type { ComputedSnapshot } from "./git.js";
|
|
4
|
+
import { type RefreshStats } from "./refresh.js";
|
|
5
|
+
export interface CreateAnalysisInput {
|
|
6
|
+
project: string;
|
|
7
|
+
title: string;
|
|
8
|
+
request?: AnalysisRequest | null;
|
|
9
|
+
summary?: string[];
|
|
10
|
+
}
|
|
11
|
+
export interface MutateOptions {
|
|
12
|
+
/**
|
|
13
|
+
* Révision sur laquelle la modification est basée. Quand elle est fournie et
|
|
14
|
+
* que l'analyse a avancé depuis, l'écriture est refusée.
|
|
15
|
+
*/
|
|
16
|
+
baseRevision?: number;
|
|
17
|
+
}
|
|
18
|
+
/** Résultat de `list` : les analyses lisibles et les fichiers qui ne le sont pas. */
|
|
19
|
+
export interface AnalysisListing {
|
|
20
|
+
analyses: AnalysisSummary[];
|
|
21
|
+
unreadable: Array<{
|
|
22
|
+
id: string;
|
|
23
|
+
error: string;
|
|
24
|
+
}>;
|
|
25
|
+
}
|
|
26
|
+
export interface ReplaceSnapshotResult {
|
|
27
|
+
analysis: Analysis;
|
|
28
|
+
stats: RefreshStats;
|
|
29
|
+
/** Revue en place avant le recalcul, remplacée par une revue vierge. */
|
|
30
|
+
previousReview: Review;
|
|
31
|
+
}
|
|
32
|
+
/** Revue vierge : en attente, sans décision, sélection ni prompt. */
|
|
33
|
+
export declare function pendingReview(): Review;
|
|
34
|
+
/**
|
|
35
|
+
* Persistance des analyses : `<dataDir>/analyses/<id>.json` pour l'analyse et
|
|
36
|
+
* `<dataDir>/diffs/<id>.json` pour son snapshot de diff. Plusieurs processus
|
|
37
|
+
* peuvent partager le même répertoire : chaque cycle lire-modifier-écrire est
|
|
38
|
+
* sérialisé par un verrou fichier, et le cache mémoire est invalidé dès que la
|
|
39
|
+
* signature (mtime, taille) du fichier change.
|
|
40
|
+
*/
|
|
41
|
+
export declare class AnalysisStore {
|
|
42
|
+
readonly dataDir: string;
|
|
43
|
+
private readonly analysesDir;
|
|
44
|
+
private readonly diffsDir;
|
|
45
|
+
private readonly analysisCache;
|
|
46
|
+
private readonly snapshotCache;
|
|
47
|
+
constructor(dataDir: string);
|
|
48
|
+
analysisPath(id: string): string;
|
|
49
|
+
snapshotPath(id: string): string;
|
|
50
|
+
/** Refuse un identifiant qui ne peut pas désigner une analyse (et donc un nom de fichier). */
|
|
51
|
+
private assertId;
|
|
52
|
+
private notFound;
|
|
53
|
+
/**
|
|
54
|
+
* Sérialise l'accès en écriture à une analyse entre processus : `wx` échoue si
|
|
55
|
+
* le verrou existe déjà. Non réentrant.
|
|
56
|
+
*/
|
|
57
|
+
private withLock;
|
|
58
|
+
/**
|
|
59
|
+
* Lit, migre et valide un document JSON. Un document migré est réécrit sur
|
|
60
|
+
* disque. `parse` renvoie le document typé ou lève une erreur. Renvoie null
|
|
61
|
+
* si le fichier n'existe pas.
|
|
62
|
+
*/
|
|
63
|
+
private readDocument;
|
|
64
|
+
private parseAnalysis;
|
|
65
|
+
private parseSnapshot;
|
|
66
|
+
private writeAnalysis;
|
|
67
|
+
private writeSnapshot;
|
|
68
|
+
/** Valide une analyse complète (schéma puis invariants) avant écriture. */
|
|
69
|
+
private validate;
|
|
70
|
+
has(id: string): boolean;
|
|
71
|
+
/** Résumés des analyses, de la plus récemment modifiée à la plus ancienne. */
|
|
72
|
+
list(): AnalysisListing;
|
|
73
|
+
get(id: string): Analysis;
|
|
74
|
+
getSnapshot(id: string): DiffSnapshot;
|
|
75
|
+
/** Crée une analyse à partir d'un snapshot calculé ; le snapshot est écrit avant l'analyse. */
|
|
76
|
+
create(input: CreateAnalysisInput, snapshot: ComputedSnapshot): Analysis;
|
|
77
|
+
private buildSnapshot;
|
|
78
|
+
/**
|
|
79
|
+
* Point unique de modification : applique `fn` à une copie de l'analyse,
|
|
80
|
+
* vérifie schéma et invariants, incrémente la révision puis écrit. Les champs
|
|
81
|
+
* issus du snapshot ne peuvent pas changer ici (voir `replaceSnapshot`).
|
|
82
|
+
*/
|
|
83
|
+
mutate(id: string, editor: Editor, fn: (draft: Analysis) => void, options?: MutateOptions): Analysis;
|
|
84
|
+
private assertBaseRevision;
|
|
85
|
+
/**
|
|
86
|
+
* Remplace le snapshot de diff d'une analyse par `snapshot`, recalculé avec
|
|
87
|
+
* les mêmes dépôt, mode et refs, met à jour fichiers et constats selon
|
|
88
|
+
* `applyRefresh` et remet la revue à l'état vierge, dans la même écriture.
|
|
89
|
+
*/
|
|
90
|
+
replaceSnapshot(id: string, editor: Editor, snapshot: ComputedSnapshot, options?: MutateOptions): ReplaceSnapshotResult;
|
|
91
|
+
/** Supprime l'analyse et son snapshot de diff. */
|
|
92
|
+
delete(id: string): void;
|
|
93
|
+
}
|
|
94
|
+
/** Vue légère d'une analyse. */
|
|
95
|
+
export declare function summarize(analysis: Analysis): AnalysisSummary;
|