speclore 0.1.6 → 0.1.8
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.en.md +116 -65
- package/README.md +113 -62
- package/dist/cli/index.js +374 -173
- package/dist/cli/index.js.map +1 -1
- package/dist/index.js +374 -173
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +155 -113
- package/dist/mcp/server.js.map +1 -1
- package/package.json +12 -4
package/dist/index.js
CHANGED
|
@@ -1,12 +1,7 @@
|
|
|
1
1
|
var __defProp = Object.defineProperty;
|
|
2
2
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
-
var __esm = (fn, res
|
|
4
|
-
|
|
5
|
-
try {
|
|
6
|
-
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
7
|
-
} catch (e) {
|
|
8
|
-
throw err = [e], e;
|
|
9
|
-
}
|
|
3
|
+
var __esm = (fn, res) => function __init() {
|
|
4
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
10
5
|
};
|
|
11
6
|
var __export = (target, all) => {
|
|
12
7
|
for (var name in all)
|
|
@@ -323,7 +318,7 @@ function detectAITools(projectRoot2) {
|
|
|
323
318
|
detected: existsSync2(join2(projectRoot2, ".cursor")),
|
|
324
319
|
configFiles: cursorConfigs
|
|
325
320
|
});
|
|
326
|
-
const claudeFiles = [".claude/", "
|
|
321
|
+
const claudeFiles = [".claude/", "CLAUDE.md"];
|
|
327
322
|
const claudeConfigs = claudeFiles.filter((f) => existsSync2(join2(projectRoot2, f)));
|
|
328
323
|
tools.push({
|
|
329
324
|
tool: "claude",
|
|
@@ -759,7 +754,7 @@ var init_graph_builder = __esm({
|
|
|
759
754
|
});
|
|
760
755
|
|
|
761
756
|
// src/core/context-engine/context-writer.ts
|
|
762
|
-
import { readFileSync as readFileSync6, writeFileSync as writeFileSync5, existsSync as existsSync7, statSync as statSync2, mkdirSync as
|
|
757
|
+
import { readFileSync as readFileSync6, writeFileSync as writeFileSync5, existsSync as existsSync7, statSync as statSync2, mkdirSync as mkdirSync5 } from "fs";
|
|
763
758
|
import { join as join10 } from "path";
|
|
764
759
|
import { execFileSync } from "child_process";
|
|
765
760
|
function buildContext(projectRoot2, config) {
|
|
@@ -830,7 +825,7 @@ function loadContext(specLoreDir) {
|
|
|
830
825
|
}
|
|
831
826
|
function writeContextFile(specLoreDir, context) {
|
|
832
827
|
if (!existsSync7(specLoreDir)) {
|
|
833
|
-
|
|
828
|
+
mkdirSync5(specLoreDir, { recursive: true });
|
|
834
829
|
}
|
|
835
830
|
const contextPath = join10(specLoreDir, CONTEXT_FILENAME);
|
|
836
831
|
writeFileSync5(contextPath, JSON.stringify(context, null, 2), "utf-8");
|
|
@@ -1021,7 +1016,7 @@ var init_cost_tracker = __esm({
|
|
|
1021
1016
|
});
|
|
1022
1017
|
|
|
1023
1018
|
// src/core/state-manager/index.ts
|
|
1024
|
-
import { readFileSync as readFileSync7, writeFileSync as writeFileSync6, existsSync as existsSync8, mkdirSync as
|
|
1019
|
+
import { readFileSync as readFileSync7, writeFileSync as writeFileSync6, existsSync as existsSync8, mkdirSync as mkdirSync6 } from "fs";
|
|
1025
1020
|
import { join as join12 } from "path";
|
|
1026
1021
|
import yaml2 from "js-yaml";
|
|
1027
1022
|
import { globSync as globSync2 } from "glob";
|
|
@@ -1070,7 +1065,7 @@ var init_state_manager = __esm({
|
|
|
1070
1065
|
save(state) {
|
|
1071
1066
|
const dir = join12(this.projectRoot, ".speclore");
|
|
1072
1067
|
if (!existsSync8(dir)) {
|
|
1073
|
-
|
|
1068
|
+
mkdirSync6(dir, { recursive: true });
|
|
1074
1069
|
}
|
|
1075
1070
|
writeFileSync6(this.statePath, yaml2.dump(state, { lineWidth: 120 }), "utf-8");
|
|
1076
1071
|
}
|
|
@@ -1427,7 +1422,43 @@ var init_excel_utils = __esm({
|
|
|
1427
1422
|
});
|
|
1428
1423
|
|
|
1429
1424
|
// src/plugins/builtin/xlsx-reader.ts
|
|
1430
|
-
import { readFile, utils } from "xlsx";
|
|
1425
|
+
import { readFile, read, utils } from "xlsx";
|
|
1426
|
+
function parseWorkbook(workbook) {
|
|
1427
|
+
const requirements = [];
|
|
1428
|
+
for (const sheetName of workbook.SheetNames) {
|
|
1429
|
+
const sheet = workbook.Sheets[sheetName];
|
|
1430
|
+
if (!sheet) continue;
|
|
1431
|
+
const allRows = utils.sheet_to_json(sheet, { header: 1, defval: "" });
|
|
1432
|
+
if (allRows.length === 0) continue;
|
|
1433
|
+
const headers = allRows[0].map((cell) => formatCellValue(cell));
|
|
1434
|
+
const dataRows = allRows.slice(1);
|
|
1435
|
+
const rows = dataRows.map((row) => {
|
|
1436
|
+
const obj = {};
|
|
1437
|
+
row.forEach((cell, colNumber) => {
|
|
1438
|
+
const key = headers[colNumber] ?? `Col${colNumber + 1}`;
|
|
1439
|
+
obj[key] = formatCellValue(cell);
|
|
1440
|
+
});
|
|
1441
|
+
return obj;
|
|
1442
|
+
});
|
|
1443
|
+
for (let i = 0; i < rows.length; i++) {
|
|
1444
|
+
const row = rows[i];
|
|
1445
|
+
const title = row["Title"] ?? row["title"] ?? row["Name"] ?? row["name"] ?? row["ID"] ?? `Row ${i + 1}`;
|
|
1446
|
+
const description = row["Description"] ?? row["description"] ?? row["Desc"] ?? "";
|
|
1447
|
+
const ac = row["Acceptance Criteria"] ?? row["acceptance"] ?? row["AC"] ?? "";
|
|
1448
|
+
if (description || title) {
|
|
1449
|
+
requirements.push({
|
|
1450
|
+
id: `${sheetName}/${i + 1}`,
|
|
1451
|
+
title: String(title),
|
|
1452
|
+
description: String(description),
|
|
1453
|
+
acceptanceCriteria: ac ? String(ac).split("\n").filter(Boolean) : void 0,
|
|
1454
|
+
rawContent: JSON.stringify(row),
|
|
1455
|
+
confidence: 0.7
|
|
1456
|
+
});
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
return requirements;
|
|
1461
|
+
}
|
|
1431
1462
|
var XlsxReader;
|
|
1432
1463
|
var init_xlsx_reader = __esm({
|
|
1433
1464
|
"src/plugins/builtin/xlsx-reader.ts"() {
|
|
@@ -1441,40 +1472,7 @@ var init_xlsx_reader = __esm({
|
|
|
1441
1472
|
}
|
|
1442
1473
|
read(source) {
|
|
1443
1474
|
const workbook = readFile(source);
|
|
1444
|
-
|
|
1445
|
-
for (const sheetName of workbook.SheetNames) {
|
|
1446
|
-
const sheet = workbook.Sheets[sheetName];
|
|
1447
|
-
if (!sheet) continue;
|
|
1448
|
-
const allRows = utils.sheet_to_json(sheet, { header: 1, defval: "" });
|
|
1449
|
-
if (allRows.length === 0) continue;
|
|
1450
|
-
const headers = allRows[0].map((cell) => formatCellValue(cell));
|
|
1451
|
-
const dataRows = allRows.slice(1);
|
|
1452
|
-
const rows = dataRows.map((row) => {
|
|
1453
|
-
const obj = {};
|
|
1454
|
-
row.forEach((cell, colNumber) => {
|
|
1455
|
-
const key = headers[colNumber] ?? `Col${colNumber + 1}`;
|
|
1456
|
-
obj[key] = formatCellValue(cell);
|
|
1457
|
-
});
|
|
1458
|
-
return obj;
|
|
1459
|
-
});
|
|
1460
|
-
for (let i = 0; i < rows.length; i++) {
|
|
1461
|
-
const row = rows[i];
|
|
1462
|
-
const title = row["Title"] ?? row["title"] ?? row["Name"] ?? row["name"] ?? row["ID"] ?? `Row ${i + 1}`;
|
|
1463
|
-
const description = row["Description"] ?? row["description"] ?? row["Desc"] ?? "";
|
|
1464
|
-
const ac = row["Acceptance Criteria"] ?? row["acceptance"] ?? row["AC"] ?? "";
|
|
1465
|
-
if (description || title) {
|
|
1466
|
-
requirements.push({
|
|
1467
|
-
id: `${sheetName}/${i + 1}`,
|
|
1468
|
-
title: String(title),
|
|
1469
|
-
description: String(description),
|
|
1470
|
-
acceptanceCriteria: ac ? String(ac).split("\n").filter(Boolean) : void 0,
|
|
1471
|
-
rawContent: JSON.stringify(row),
|
|
1472
|
-
confidence: 0.7
|
|
1473
|
-
});
|
|
1474
|
-
}
|
|
1475
|
-
}
|
|
1476
|
-
}
|
|
1477
|
-
return Promise.resolve(requirements);
|
|
1475
|
+
return Promise.resolve(parseWorkbook(workbook));
|
|
1478
1476
|
}
|
|
1479
1477
|
};
|
|
1480
1478
|
}
|
|
@@ -1492,21 +1490,33 @@ var init_pdf_reader = __esm({
|
|
|
1492
1490
|
return /\.pdf$/i.test(source);
|
|
1493
1491
|
}
|
|
1494
1492
|
async read(source) {
|
|
1495
|
-
let
|
|
1493
|
+
let pdfjsLib;
|
|
1496
1494
|
try {
|
|
1497
|
-
|
|
1498
|
-
pdfParse = mod.default ?? mod;
|
|
1495
|
+
pdfjsLib = await import("pdfjs-dist/legacy/build/pdf.mjs");
|
|
1499
1496
|
} catch {
|
|
1500
|
-
throw new Error("
|
|
1497
|
+
throw new Error("pdfjs-dist package is required for PDF support. Install: npm i pdfjs-dist");
|
|
1498
|
+
}
|
|
1499
|
+
const { readFileSync: readFileSync24 } = await import("fs");
|
|
1500
|
+
const buffer = readFileSync24(source);
|
|
1501
|
+
const uint8 = new Uint8Array(buffer);
|
|
1502
|
+
const doc = await pdfjsLib.getDocument({
|
|
1503
|
+
data: uint8,
|
|
1504
|
+
useWorkerFetch: false
|
|
1505
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
1506
|
+
}).promise;
|
|
1507
|
+
const textParts = [];
|
|
1508
|
+
for (let i = 1; i <= doc.numPages; i++) {
|
|
1509
|
+
const page = await doc.getPage(i);
|
|
1510
|
+
const textContent = await page.getTextContent();
|
|
1511
|
+
const pageText = textContent.items.map((item) => item.str).join("");
|
|
1512
|
+
if (pageText) textParts.push(pageText);
|
|
1501
1513
|
}
|
|
1502
|
-
const
|
|
1503
|
-
const buffer = readFileSync22(source);
|
|
1504
|
-
const data = await pdfParse(buffer);
|
|
1514
|
+
const text = textParts.join("\n");
|
|
1505
1515
|
return [{
|
|
1506
1516
|
id: source.replace(/\\/g, "/").replace(/\.pdf$/i, ""),
|
|
1507
1517
|
title: source.split("/").pop()?.replace(/\.pdf$/i, "") ?? "Untitled",
|
|
1508
|
-
description:
|
|
1509
|
-
rawContent:
|
|
1518
|
+
description: text,
|
|
1519
|
+
rawContent: text,
|
|
1510
1520
|
confidence: 0.75
|
|
1511
1521
|
}];
|
|
1512
1522
|
}
|
|
@@ -2102,13 +2112,13 @@ var init_image_reader = __esm({
|
|
|
2102
2112
|
return /\.(png|jpe?g|webp)$/i.test(source);
|
|
2103
2113
|
}
|
|
2104
2114
|
async read(source) {
|
|
2105
|
-
const { existsSync:
|
|
2106
|
-
if (!
|
|
2115
|
+
const { existsSync: existsSync31, readFileSync: readFileSync24 } = await import("fs");
|
|
2116
|
+
if (!existsSync31(source)) {
|
|
2107
2117
|
throw new Error(`Image file not found: ${source}`);
|
|
2108
2118
|
}
|
|
2109
2119
|
const ext = source.split(".").pop()?.toLowerCase() ?? "png";
|
|
2110
2120
|
const mimeType = ext === "jpg" ? "image/jpeg" : `image/${ext}`;
|
|
2111
|
-
const buffer =
|
|
2121
|
+
const buffer = readFileSync24(source);
|
|
2112
2122
|
let text;
|
|
2113
2123
|
try {
|
|
2114
2124
|
const { createProvider: createProvider2 } = await Promise.resolve().then(() => (init_provider(), provider_exports));
|
|
@@ -2137,7 +2147,7 @@ var init_image_reader = __esm({
|
|
|
2137
2147
|
});
|
|
2138
2148
|
|
|
2139
2149
|
// src/plugins/builtin/cursor-writer.ts
|
|
2140
|
-
import { writeFileSync as writeFileSync7, mkdirSync as
|
|
2150
|
+
import { writeFileSync as writeFileSync7, mkdirSync as mkdirSync7, existsSync as existsSync11, rmSync } from "fs";
|
|
2141
2151
|
import { join as join14 } from "path";
|
|
2142
2152
|
var CursorWriter;
|
|
2143
2153
|
var init_cursor_writer = __esm({
|
|
@@ -2153,7 +2163,7 @@ var init_cursor_writer = __esm({
|
|
|
2153
2163
|
write(constraints) {
|
|
2154
2164
|
this.projectRoot = constraints.projectRoot;
|
|
2155
2165
|
const rulesDir = join14(constraints.projectRoot, ".cursor", "rules");
|
|
2156
|
-
|
|
2166
|
+
mkdirSync7(rulesDir, { recursive: true });
|
|
2157
2167
|
const frontmatter = [
|
|
2158
2168
|
"---",
|
|
2159
2169
|
"description: SpecLore coding constraints \u2014 auto-generated",
|
|
@@ -2232,7 +2242,7 @@ var init_cursor_writer = __esm({
|
|
|
2232
2242
|
});
|
|
2233
2243
|
|
|
2234
2244
|
// src/plugins/builtin/claude-writer.ts
|
|
2235
|
-
import { writeFileSync as writeFileSync8, mkdirSync as
|
|
2245
|
+
import { writeFileSync as writeFileSync8, mkdirSync as mkdirSync8, existsSync as existsSync12, rmSync as rmSync2 } from "fs";
|
|
2236
2246
|
import { join as join15 } from "path";
|
|
2237
2247
|
var ClaudeWriter;
|
|
2238
2248
|
var init_claude_writer = __esm({
|
|
@@ -2248,7 +2258,7 @@ var init_claude_writer = __esm({
|
|
|
2248
2258
|
write(constraints) {
|
|
2249
2259
|
this.projectRoot = constraints.projectRoot;
|
|
2250
2260
|
const rulesDir = join15(constraints.projectRoot, ".claude", "rules");
|
|
2251
|
-
|
|
2261
|
+
mkdirSync8(rulesDir, { recursive: true });
|
|
2252
2262
|
const content = this.buildMarkdown(constraints);
|
|
2253
2263
|
writeFileSync8(join15(rulesDir, "speclore.md"), content, "utf-8");
|
|
2254
2264
|
return Promise.resolve();
|
|
@@ -2314,7 +2324,7 @@ var init_claude_writer = __esm({
|
|
|
2314
2324
|
});
|
|
2315
2325
|
|
|
2316
2326
|
// src/plugins/builtin/qoder-writer.ts
|
|
2317
|
-
import { writeFileSync as writeFileSync9, mkdirSync as
|
|
2327
|
+
import { writeFileSync as writeFileSync9, mkdirSync as mkdirSync9, existsSync as existsSync13, rmSync as rmSync3 } from "fs";
|
|
2318
2328
|
import { join as join16 } from "path";
|
|
2319
2329
|
var QoderWriter;
|
|
2320
2330
|
var init_qoder_writer = __esm({
|
|
@@ -2330,7 +2340,7 @@ var init_qoder_writer = __esm({
|
|
|
2330
2340
|
write(constraints) {
|
|
2331
2341
|
this.projectRoot = constraints.projectRoot;
|
|
2332
2342
|
const rulesDir = join16(constraints.projectRoot, ".qoder", "rules");
|
|
2333
|
-
|
|
2343
|
+
mkdirSync9(rulesDir, { recursive: true });
|
|
2334
2344
|
const content = this.buildMarkdown(constraints);
|
|
2335
2345
|
writeFileSync9(join16(rulesDir, "speclore.md"), content, "utf-8");
|
|
2336
2346
|
return Promise.resolve();
|
|
@@ -2763,11 +2773,12 @@ var init_markdown_reader = __esm({
|
|
|
2763
2773
|
|
|
2764
2774
|
// src/core/requirement-reader/docx-reader.ts
|
|
2765
2775
|
import { basename as basename3, extname as extname3 } from "path";
|
|
2776
|
+
import { readFileSync as readFileSync10 } from "fs";
|
|
2766
2777
|
import mammoth from "mammoth";
|
|
2767
|
-
async function
|
|
2768
|
-
const result = await mammoth.extractRawText({
|
|
2778
|
+
async function parseDocxBuffer(buffer, idHint = "document") {
|
|
2779
|
+
const result = await mammoth.extractRawText({ buffer });
|
|
2769
2780
|
const content = result.value;
|
|
2770
|
-
const id =
|
|
2781
|
+
const id = idHint.toLowerCase().replace(/[^a-z0-9\u4e00-\u9fff-]/g, "-").replace(/-+/g, "-");
|
|
2771
2782
|
const lines = content.split("\n").filter((l) => l.trim());
|
|
2772
2783
|
const title = lines[0]?.trim() ?? id;
|
|
2773
2784
|
return {
|
|
@@ -2778,6 +2789,11 @@ async function readDocxFile(filePath) {
|
|
|
2778
2789
|
confidence: 0.9
|
|
2779
2790
|
};
|
|
2780
2791
|
}
|
|
2792
|
+
async function readDocxFile(filePath) {
|
|
2793
|
+
const buffer = readFileSync10(filePath);
|
|
2794
|
+
const name = basename3(filePath, extname3(filePath));
|
|
2795
|
+
return parseDocxBuffer(buffer, name);
|
|
2796
|
+
}
|
|
2781
2797
|
var init_docx_reader2 = __esm({
|
|
2782
2798
|
"src/core/requirement-reader/docx-reader.ts"() {
|
|
2783
2799
|
"use strict";
|
|
@@ -2786,17 +2802,16 @@ var init_docx_reader2 = __esm({
|
|
|
2786
2802
|
|
|
2787
2803
|
// src/core/requirement-reader/xlsx-reader.ts
|
|
2788
2804
|
import { basename as basename4, extname as extname4 } from "path";
|
|
2789
|
-
import { readFile as readFile2, utils as utils2 } from "xlsx";
|
|
2790
|
-
function
|
|
2791
|
-
const
|
|
2792
|
-
const id = basename4(filePath, extname4(filePath)).toLowerCase().replace(/[^a-z0-9\u4e00-\u9fff-]/g, "-").replace(/-+/g, "-");
|
|
2805
|
+
import { readFile as readFile2, read as read2, utils as utils2 } from "xlsx";
|
|
2806
|
+
function parseWorkbook2(workbook, idHint) {
|
|
2807
|
+
const id = idHint.toLowerCase().replace(/[^a-z0-9\u4e00-\u9fff-]/g, "-").replace(/-+/g, "-");
|
|
2793
2808
|
const firstSheetName = workbook.SheetNames[0];
|
|
2794
2809
|
if (!firstSheetName) {
|
|
2795
|
-
return Promise.reject(new Error(
|
|
2810
|
+
return Promise.reject(new Error("No sheets found in workbook"));
|
|
2796
2811
|
}
|
|
2797
2812
|
const sheet = workbook.Sheets[firstSheetName];
|
|
2798
2813
|
if (!sheet) {
|
|
2799
|
-
return Promise.reject(new Error(`Sheet "${firstSheetName}" not found
|
|
2814
|
+
return Promise.reject(new Error(`Sheet "${firstSheetName}" not found`));
|
|
2800
2815
|
}
|
|
2801
2816
|
const title = firstSheetName;
|
|
2802
2817
|
const rows = utils2.sheet_to_json(sheet, { header: 1, defval: "" });
|
|
@@ -2832,6 +2847,11 @@ function readXlsxFile(filePath) {
|
|
|
2832
2847
|
confidence: 0.85
|
|
2833
2848
|
});
|
|
2834
2849
|
}
|
|
2850
|
+
function readXlsxFile(filePath) {
|
|
2851
|
+
const workbook = readFile2(filePath);
|
|
2852
|
+
const idHint = basename4(filePath, extname4(filePath));
|
|
2853
|
+
return parseWorkbook2(workbook, idHint);
|
|
2854
|
+
}
|
|
2835
2855
|
var init_xlsx_reader2 = __esm({
|
|
2836
2856
|
"src/core/requirement-reader/xlsx-reader.ts"() {
|
|
2837
2857
|
"use strict";
|
|
@@ -2842,21 +2862,40 @@ var init_xlsx_reader2 = __esm({
|
|
|
2842
2862
|
// src/core/requirement-reader/pdf-reader.ts
|
|
2843
2863
|
import { basename as basename5, extname as extname5 } from "path";
|
|
2844
2864
|
import { readFile as readFile3 } from "fs/promises";
|
|
2845
|
-
async function
|
|
2846
|
-
const
|
|
2847
|
-
const
|
|
2848
|
-
const
|
|
2849
|
-
|
|
2850
|
-
|
|
2865
|
+
async function extractPdfText(dataBuffer) {
|
|
2866
|
+
const pdfjsLib = await import("pdfjs-dist/legacy/build/pdf.mjs");
|
|
2867
|
+
const uint8 = new Uint8Array(dataBuffer);
|
|
2868
|
+
const doc = await pdfjsLib.getDocument({
|
|
2869
|
+
data: uint8,
|
|
2870
|
+
useWorkerFetch: false
|
|
2871
|
+
}).promise;
|
|
2872
|
+
const textParts = [];
|
|
2873
|
+
for (let i = 1; i <= doc.numPages; i++) {
|
|
2874
|
+
const page = await doc.getPage(i);
|
|
2875
|
+
const textContent = await page.getTextContent();
|
|
2876
|
+
const pageText = textContent.items.map((item) => item.str).join("");
|
|
2877
|
+
if (pageText) textParts.push(pageText);
|
|
2878
|
+
}
|
|
2879
|
+
return textParts.join("\n");
|
|
2880
|
+
}
|
|
2881
|
+
async function parsePdfBuffer(dataBuffer, idHint = "document") {
|
|
2882
|
+
const text = await extractPdfText(dataBuffer);
|
|
2883
|
+
const id = idHint.toLowerCase().replace(/[^a-z0-9\u4e00-\u9fff-]/g, "-").replace(/-+/g, "-");
|
|
2884
|
+
const lines = text.split("\n").filter((l) => l.trim());
|
|
2851
2885
|
const title = lines[0]?.trim() ?? id;
|
|
2852
2886
|
return {
|
|
2853
2887
|
id,
|
|
2854
2888
|
title,
|
|
2855
|
-
description:
|
|
2856
|
-
rawContent:
|
|
2889
|
+
description: text,
|
|
2890
|
+
rawContent: text,
|
|
2857
2891
|
confidence: 0.8
|
|
2858
2892
|
};
|
|
2859
2893
|
}
|
|
2894
|
+
async function readPdfFile(filePath) {
|
|
2895
|
+
const dataBuffer = await readFile3(filePath);
|
|
2896
|
+
const name = basename5(filePath, extname5(filePath));
|
|
2897
|
+
return parsePdfBuffer(dataBuffer, name);
|
|
2898
|
+
}
|
|
2860
2899
|
var init_pdf_reader2 = __esm({
|
|
2861
2900
|
"src/core/requirement-reader/pdf-reader.ts"() {
|
|
2862
2901
|
"use strict";
|
|
@@ -2865,14 +2904,14 @@ var init_pdf_reader2 = __esm({
|
|
|
2865
2904
|
|
|
2866
2905
|
// src/core/requirement-reader/image-reader.ts
|
|
2867
2906
|
import { basename as basename6, extname as extname6 } from "path";
|
|
2868
|
-
import { readFileSync as
|
|
2869
|
-
async function readImageFile(filePath) {
|
|
2907
|
+
import { readFileSync as readFileSync11 } from "fs";
|
|
2908
|
+
async function readImageFile(filePath, providerOverride) {
|
|
2870
2909
|
const id = basename6(filePath, extname6(filePath)).toLowerCase().replace(/[^a-z0-9\u4e00-\u9fff-]/g, "-").replace(/-+/g, "-");
|
|
2871
2910
|
logger.info(`Reading image via AI Vision: ${filePath}`);
|
|
2872
2911
|
const ext = extname6(filePath).toLowerCase();
|
|
2873
2912
|
const mimeType = MIME_MAP[ext] ?? "image/png";
|
|
2874
|
-
const buffer =
|
|
2875
|
-
const provider = await createProvider();
|
|
2913
|
+
const buffer = readFileSync11(filePath);
|
|
2914
|
+
const provider = providerOverride ?? await createProvider();
|
|
2876
2915
|
const prompt = `Please extract all text content from this image. Return the text as-is, preserving structure and formatting. If the image contains a table, convert it to a structured text format.`;
|
|
2877
2916
|
if (!provider.generateWithImage) {
|
|
2878
2917
|
throw new Error(`AI provider '${provider.name}' does not support image/vision input. Use a vision-capable model.`);
|
|
@@ -3317,11 +3356,11 @@ var init_prompt_builder = __esm({
|
|
|
3317
3356
|
});
|
|
3318
3357
|
|
|
3319
3358
|
// src/core/feature-generator/generator.ts
|
|
3320
|
-
import { writeFileSync as writeFileSync10, mkdirSync as
|
|
3359
|
+
import { writeFileSync as writeFileSync10, mkdirSync as mkdirSync10, existsSync as existsSync15 } from "fs";
|
|
3321
3360
|
import { join as join17, dirname as dirname2 } from "path";
|
|
3322
3361
|
import { Parser as Parser2, GherkinClassicTokenMatcher as GherkinClassicTokenMatcher2, AstBuilder as AstBuilder2 } from "@cucumber/gherkin";
|
|
3323
3362
|
import { IdGenerator as IdGenerator2 } from "@cucumber/messages";
|
|
3324
|
-
async function generateFeature(requirement, context, config, projectRoot2) {
|
|
3363
|
+
async function generateFeature(requirement, context, config, projectRoot2, providerOverride) {
|
|
3325
3364
|
logger.info(`Generating feature for: ${requirement.title}`);
|
|
3326
3365
|
const registry = getRegistry();
|
|
3327
3366
|
await registry.invokeLifecycle("beforeSpec", requirement);
|
|
@@ -3329,7 +3368,7 @@ async function generateFeature(requirement, context, config, projectRoot2) {
|
|
|
3329
3368
|
const aiConfig = config.ai;
|
|
3330
3369
|
const fallbackConfigs = aiConfig?.fallbackProviders ?? [];
|
|
3331
3370
|
const providerConfigs = aiConfig ? [aiConfig, ...fallbackConfigs] : [];
|
|
3332
|
-
const provider = providerConfigs.length > 1 ? await createProviderChain(providerConfigs) : await createProvider(aiConfig);
|
|
3371
|
+
const provider = providerOverride ?? (providerConfigs.length > 1 ? await createProviderChain(providerConfigs) : await createProvider(aiConfig));
|
|
3333
3372
|
if (!provider.isAvailable()) {
|
|
3334
3373
|
throw new Error("AI provider not available. Set API key in environment or config.yaml.");
|
|
3335
3374
|
}
|
|
@@ -3386,7 +3425,7 @@ Please fix these issues and regenerate the complete Feature file.`;
|
|
|
3386
3425
|
const moduleDir = inferModule(requirement, context);
|
|
3387
3426
|
const filePath = join17(outputDir, moduleDir, `${requirement.id}.feature`);
|
|
3388
3427
|
if (!existsSync15(dirname2(filePath))) {
|
|
3389
|
-
|
|
3428
|
+
mkdirSync10(dirname2(filePath), { recursive: true });
|
|
3390
3429
|
}
|
|
3391
3430
|
writeFileSync10(filePath, featureContent, "utf-8");
|
|
3392
3431
|
logger.info(`Feature written: ${toPosixPath(filePath)}`);
|
|
@@ -3552,9 +3591,9 @@ function detectAITools2(projectRoot2) {
|
|
|
3552
3591
|
tools.push("cursor");
|
|
3553
3592
|
logger.debug("Detected Cursor (.cursor/)");
|
|
3554
3593
|
}
|
|
3555
|
-
if (existsSync16(join19(projectRoot2, ".claude")) || existsSync16(join19(projectRoot2, "CLAUDE.md"))
|
|
3594
|
+
if (existsSync16(join19(projectRoot2, ".claude")) || existsSync16(join19(projectRoot2, "CLAUDE.md"))) {
|
|
3556
3595
|
tools.push("claude");
|
|
3557
|
-
logger.debug("Detected Claude Code (.claude/ or CLAUDE.md
|
|
3596
|
+
logger.debug("Detected Claude Code (.claude/ or CLAUDE.md)");
|
|
3558
3597
|
}
|
|
3559
3598
|
if (existsSync16(join19(projectRoot2, ".qoder"))) {
|
|
3560
3599
|
tools.push("qoder");
|
|
@@ -3571,7 +3610,7 @@ var init_ai_tool_detector = __esm({
|
|
|
3571
3610
|
});
|
|
3572
3611
|
|
|
3573
3612
|
// src/core/constraint-coder/constraint-writer.ts
|
|
3574
|
-
import { writeFileSync as writeFileSync11, mkdirSync as
|
|
3613
|
+
import { writeFileSync as writeFileSync11, mkdirSync as mkdirSync11, existsSync as existsSync17 } from "fs";
|
|
3575
3614
|
import { join as join20 } from "path";
|
|
3576
3615
|
function writeConstraints(projectRoot2, tools, content) {
|
|
3577
3616
|
const writtenFiles = [];
|
|
@@ -3645,7 +3684,7 @@ function buildConstraintText(content) {
|
|
|
3645
3684
|
}
|
|
3646
3685
|
function writeCursorRule2(projectRoot2, text) {
|
|
3647
3686
|
const dir = join20(projectRoot2, ".cursor", "rules");
|
|
3648
|
-
if (!existsSync17(dir))
|
|
3687
|
+
if (!existsSync17(dir)) mkdirSync11(dir, { recursive: true });
|
|
3649
3688
|
const filePath = join20(dir, "speclore.mdc");
|
|
3650
3689
|
const frontmatter = [
|
|
3651
3690
|
"---",
|
|
@@ -3660,14 +3699,14 @@ function writeCursorRule2(projectRoot2, text) {
|
|
|
3660
3699
|
}
|
|
3661
3700
|
function writeClaudeRule2(projectRoot2, text) {
|
|
3662
3701
|
const dir = join20(projectRoot2, ".claude", "rules");
|
|
3663
|
-
if (!existsSync17(dir))
|
|
3702
|
+
if (!existsSync17(dir)) mkdirSync11(dir, { recursive: true });
|
|
3664
3703
|
const filePath = join20(dir, "speclore.md");
|
|
3665
3704
|
writeFileSync11(filePath, text, "utf-8");
|
|
3666
3705
|
return filePath;
|
|
3667
3706
|
}
|
|
3668
3707
|
function writeQoderRule2(projectRoot2, text) {
|
|
3669
3708
|
const dir = join20(projectRoot2, ".qoder", "rules");
|
|
3670
|
-
if (!existsSync17(dir))
|
|
3709
|
+
if (!existsSync17(dir)) mkdirSync11(dir, { recursive: true });
|
|
3671
3710
|
const filePath = join20(dir, "speclore.md");
|
|
3672
3711
|
writeFileSync11(filePath, text, "utf-8");
|
|
3673
3712
|
return filePath;
|
|
@@ -3747,14 +3786,14 @@ var init_constraint_coder = __esm({
|
|
|
3747
3786
|
init_logger();
|
|
3748
3787
|
init_ai_tool_detector();
|
|
3749
3788
|
init_constraint_writer();
|
|
3750
|
-
MAPPING_INSTRUCTIONS =
|
|
3751
|
-
|
|
3752
|
-
|
|
3789
|
+
MAPPING_INSTRUCTIONS = `Generate a mapping file for each test file at .speclore/mappings/{module}/{feature-name}.json.
|
|
3790
|
+
Format: { "feature": "specs/...", "scenarios": { "Scenario name": { "testFile": "...", "testMethod": "..." } } }
|
|
3791
|
+
Keep mapping files in sync whenever tests are modified.`;
|
|
3753
3792
|
}
|
|
3754
3793
|
});
|
|
3755
3794
|
|
|
3756
3795
|
// src/core/test-scaffolder/framework-detector.ts
|
|
3757
|
-
import { readFileSync as
|
|
3796
|
+
import { readFileSync as readFileSync12, existsSync as existsSync18 } from "fs";
|
|
3758
3797
|
import { join as join21 } from "path";
|
|
3759
3798
|
function detectTestFramework(projectRoot2) {
|
|
3760
3799
|
const pkgPath = join21(projectRoot2, "package.json");
|
|
@@ -3762,7 +3801,7 @@ function detectTestFramework(projectRoot2) {
|
|
|
3762
3801
|
return "vitest";
|
|
3763
3802
|
}
|
|
3764
3803
|
try {
|
|
3765
|
-
const pkg = JSON.parse(
|
|
3804
|
+
const pkg = JSON.parse(readFileSync12(pkgPath, "utf-8"));
|
|
3766
3805
|
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
3767
3806
|
if ("vitest" in deps) return "vitest";
|
|
3768
3807
|
if ("jest" in deps || "@jest/core" in deps) return "jest";
|
|
@@ -3778,7 +3817,7 @@ var init_framework_detector = __esm({
|
|
|
3778
3817
|
});
|
|
3779
3818
|
|
|
3780
3819
|
// src/core/test-scaffolder/index.ts
|
|
3781
|
-
import { readFileSync as
|
|
3820
|
+
import { readFileSync as readFileSync13, writeFileSync as writeFileSync12, existsSync as existsSync19, mkdirSync as mkdirSync12 } from "fs";
|
|
3782
3821
|
import { join as join22, relative as relative3, dirname as dirname3, basename as basename8 } from "path";
|
|
3783
3822
|
function generateTestScaffolding(projectRoot2, features, config) {
|
|
3784
3823
|
const framework = detectTestFramework(projectRoot2);
|
|
@@ -3801,7 +3840,7 @@ function generateTestScaffolding(projectRoot2, features, config) {
|
|
|
3801
3840
|
const content = generateTestFileContent(feature.featureName, scenarios, framework);
|
|
3802
3841
|
const dir = dirname3(absTestPath);
|
|
3803
3842
|
if (!existsSync19(dir)) {
|
|
3804
|
-
|
|
3843
|
+
mkdirSync12(dir, { recursive: true });
|
|
3805
3844
|
}
|
|
3806
3845
|
writeFileSync12(absTestPath, content, "utf-8");
|
|
3807
3846
|
results.push({ testFile: testFilePath, framework, scenarios: scenarios.length });
|
|
@@ -3874,7 +3913,7 @@ function generateTestFileContent(featureName, scenarios, framework) {
|
|
|
3874
3913
|
return lines.join("\n");
|
|
3875
3914
|
}
|
|
3876
3915
|
function appendMissingScenarios(testFilePath, scenarios, _framework) {
|
|
3877
|
-
const content =
|
|
3916
|
+
const content = readFileSync13(testFilePath, "utf-8");
|
|
3878
3917
|
const missing = [];
|
|
3879
3918
|
for (const scenario of scenarios) {
|
|
3880
3919
|
if (!content.includes(`Scenario: ${scenario.name}`)) {
|
|
@@ -3915,7 +3954,7 @@ var init_test_scaffolder = __esm({
|
|
|
3915
3954
|
});
|
|
3916
3955
|
|
|
3917
3956
|
// src/core/verifier/mapping-resolver.ts
|
|
3918
|
-
import { readFileSync as
|
|
3957
|
+
import { readFileSync as readFileSync15, existsSync as existsSync21, readdirSync as readdirSync2 } from "fs";
|
|
3919
3958
|
import { join as join24 } from "path";
|
|
3920
3959
|
function resolveMappings(projectRoot2, features, testOutput) {
|
|
3921
3960
|
const results = [];
|
|
@@ -3945,7 +3984,7 @@ function resolveFromMappingFile(projectRoot2, _feature, scenario) {
|
|
|
3945
3984
|
const files = findMappingFiles(mappingsDir);
|
|
3946
3985
|
for (const file of files) {
|
|
3947
3986
|
try {
|
|
3948
|
-
const content =
|
|
3987
|
+
const content = readFileSync15(file, "utf-8");
|
|
3949
3988
|
const mapping = JSON.parse(content);
|
|
3950
3989
|
if (mapping.scenarios && scenario.name in mapping.scenarios) {
|
|
3951
3990
|
const entry = mapping.scenarios[scenario.name];
|
|
@@ -3975,7 +4014,7 @@ function resolveFromTag(projectRoot2, _feature, scenario) {
|
|
|
3975
4014
|
const files = findTestFiles(testsDir);
|
|
3976
4015
|
for (const file of files) {
|
|
3977
4016
|
try {
|
|
3978
|
-
const content =
|
|
4017
|
+
const content = readFileSync15(file, "utf-8");
|
|
3979
4018
|
const tagRegex = /@speclore-scenario:\s*(.+)/g;
|
|
3980
4019
|
let match;
|
|
3981
4020
|
while ((match = tagRegex.exec(content)) !== null) {
|
|
@@ -4155,13 +4194,13 @@ var report_generator_exports = {};
|
|
|
4155
4194
|
__export(report_generator_exports, {
|
|
4156
4195
|
generateReport: () => generateReport
|
|
4157
4196
|
});
|
|
4158
|
-
import { writeFileSync as writeFileSync13, mkdirSync as
|
|
4197
|
+
import { writeFileSync as writeFileSync13, mkdirSync as mkdirSync13, existsSync as existsSync22, readFileSync as readFileSync16 } from "fs";
|
|
4159
4198
|
import { join as join25, dirname as dirname4 } from "path";
|
|
4160
4199
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
4161
4200
|
function generateReport(report, projectRoot2, config) {
|
|
4162
4201
|
const reportsDir = join25(projectRoot2, ".speclore", "reports");
|
|
4163
4202
|
if (!existsSync22(reportsDir)) {
|
|
4164
|
-
|
|
4203
|
+
mkdirSync13(reportsDir, { recursive: true });
|
|
4165
4204
|
}
|
|
4166
4205
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
4167
4206
|
const writtenFiles = [];
|
|
@@ -4218,7 +4257,7 @@ function renderHtmlReport(report) {
|
|
|
4218
4257
|
const templatePath = join25(dirname4(fileURLToPath2(import.meta.url)), "..", "..", "cli", "templates", "report.html");
|
|
4219
4258
|
let template;
|
|
4220
4259
|
try {
|
|
4221
|
-
template =
|
|
4260
|
+
template = readFileSync16(templatePath, "utf-8");
|
|
4222
4261
|
} catch {
|
|
4223
4262
|
template = getInlineTemplate();
|
|
4224
4263
|
}
|
|
@@ -4297,7 +4336,7 @@ var init_verifier = __esm({
|
|
|
4297
4336
|
});
|
|
4298
4337
|
|
|
4299
4338
|
// src/core/analyzer/rdg-builder.ts
|
|
4300
|
-
import { readFileSync as
|
|
4339
|
+
import { readFileSync as readFileSync17, existsSync as existsSync23, readdirSync as readdirSync3 } from "fs";
|
|
4301
4340
|
import { join as join26 } from "path";
|
|
4302
4341
|
var init_rdg_builder = __esm({
|
|
4303
4342
|
"src/core/analyzer/rdg-builder.ts"() {
|
|
@@ -4327,6 +4366,9 @@ import { execFileSync as execFileSync3 } from "child_process";
|
|
|
4327
4366
|
import { globSync as globSync5 } from "glob";
|
|
4328
4367
|
function analyzeImpact(projectRoot2, context, config) {
|
|
4329
4368
|
const changedFiles = getChangedFiles(projectRoot2);
|
|
4369
|
+
return analyzeImpactWithChanges(changedFiles, context, config, projectRoot2);
|
|
4370
|
+
}
|
|
4371
|
+
function analyzeImpactWithChanges(changedFiles, context, config, projectRoot2) {
|
|
4330
4372
|
const affectedModules = determineAffectedModules(changedFiles, context);
|
|
4331
4373
|
const affectedFeatures = determineAffectedFeatures(affectedModules, projectRoot2, config.spec.outputDir);
|
|
4332
4374
|
logger.info(
|
|
@@ -4402,11 +4444,11 @@ var init_analyzer = __esm({
|
|
|
4402
4444
|
});
|
|
4403
4445
|
|
|
4404
4446
|
// src/infra/file-lock.ts
|
|
4405
|
-
import { readFileSync as
|
|
4406
|
-
import { join as
|
|
4447
|
+
import { readFileSync as readFileSync21, writeFileSync as writeFileSync16, unlinkSync, existsSync as existsSync28, statSync as statSync3 } from "fs";
|
|
4448
|
+
import { join as join31 } from "path";
|
|
4407
4449
|
function acquireLock(specLoreDir) {
|
|
4408
|
-
const lockPath =
|
|
4409
|
-
if (
|
|
4450
|
+
const lockPath = join31(specLoreDir, LOCK_FILENAME);
|
|
4451
|
+
if (existsSync28(lockPath)) {
|
|
4410
4452
|
const existing = readLockInfo(lockPath);
|
|
4411
4453
|
if (existing && !isLockExpired(existing)) {
|
|
4412
4454
|
logger.debug(`Lock already held by PID ${existing.pid}, not expired`);
|
|
@@ -4420,7 +4462,7 @@ function acquireLock(specLoreDir) {
|
|
|
4420
4462
|
timestamp: Date.now()
|
|
4421
4463
|
};
|
|
4422
4464
|
try {
|
|
4423
|
-
|
|
4465
|
+
writeFileSync16(lockPath, JSON.stringify(lockInfo), { flag: "wx", encoding: "utf-8" });
|
|
4424
4466
|
} catch {
|
|
4425
4467
|
logger.debug("Lock write failed \u2014 another process may have acquired it");
|
|
4426
4468
|
return false;
|
|
@@ -4429,8 +4471,8 @@ function acquireLock(specLoreDir) {
|
|
|
4429
4471
|
return true;
|
|
4430
4472
|
}
|
|
4431
4473
|
function releaseLock(specLoreDir) {
|
|
4432
|
-
const lockPath =
|
|
4433
|
-
if (!
|
|
4474
|
+
const lockPath = join31(specLoreDir, LOCK_FILENAME);
|
|
4475
|
+
if (!existsSync28(lockPath)) {
|
|
4434
4476
|
return;
|
|
4435
4477
|
}
|
|
4436
4478
|
const existing = readLockInfo(lockPath);
|
|
@@ -4441,7 +4483,7 @@ function releaseLock(specLoreDir) {
|
|
|
4441
4483
|
}
|
|
4442
4484
|
function readLockInfo(lockPath) {
|
|
4443
4485
|
try {
|
|
4444
|
-
const content =
|
|
4486
|
+
const content = readFileSync21(lockPath, "utf-8");
|
|
4445
4487
|
const parsed = JSON.parse(content);
|
|
4446
4488
|
if (typeof parsed.pid === "number" && typeof parsed.timestamp === "number") {
|
|
4447
4489
|
return parsed;
|
|
@@ -4511,8 +4553,8 @@ var init_schemas = __esm({
|
|
|
4511
4553
|
});
|
|
4512
4554
|
|
|
4513
4555
|
// src/mcp/tools.ts
|
|
4514
|
-
import { join as
|
|
4515
|
-
import { readFileSync as
|
|
4556
|
+
import { join as join32 } from "path";
|
|
4557
|
+
import { readFileSync as readFileSync22, existsSync as existsSync29, mkdirSync as mkdirSync14, writeFileSync as writeFileSync17 } from "fs";
|
|
4516
4558
|
import { globSync as globSync7 } from "glob";
|
|
4517
4559
|
import { toJSONSchema } from "zod";
|
|
4518
4560
|
async function executeSpecTool(args, projectRoot2) {
|
|
@@ -4523,7 +4565,7 @@ async function executeSpecTool(args, projectRoot2) {
|
|
|
4523
4565
|
const config = loadConfig(projectRoot2);
|
|
4524
4566
|
logger.info(`Reading requirement from: ${args.source.slice(0, 80)}...`);
|
|
4525
4567
|
const req = await readRequirement(args.source);
|
|
4526
|
-
const specLoreDir =
|
|
4568
|
+
const specLoreDir = join32(projectRoot2, ".speclore");
|
|
4527
4569
|
const context = loadContext(specLoreDir) ?? buildContext(projectRoot2, config);
|
|
4528
4570
|
const feature = await generateFeature(req, context, config, projectRoot2);
|
|
4529
4571
|
const createdFiles = [feature.path];
|
|
@@ -4555,7 +4597,7 @@ async function executeCodeTool(args, projectRoot2) {
|
|
|
4555
4597
|
}
|
|
4556
4598
|
ensureProjectReady(projectRoot2);
|
|
4557
4599
|
const config = loadConfig(projectRoot2);
|
|
4558
|
-
const specLoreDir =
|
|
4600
|
+
const specLoreDir = join32(projectRoot2, ".speclore");
|
|
4559
4601
|
const context = loadContext(specLoreDir) ?? buildContext(projectRoot2, config);
|
|
4560
4602
|
const featureFiles = resolveFeatureFiles(args.features, projectRoot2, config);
|
|
4561
4603
|
if (featureFiles.length === 0) {
|
|
@@ -4608,7 +4650,7 @@ async function executeCodeTool(args, projectRoot2) {
|
|
|
4608
4650
|
async function executeVerifyTool(args, projectRoot2) {
|
|
4609
4651
|
ensureProjectReady(projectRoot2);
|
|
4610
4652
|
const config = loadConfig(projectRoot2);
|
|
4611
|
-
const specLoreDir =
|
|
4653
|
+
const specLoreDir = join32(projectRoot2, ".speclore");
|
|
4612
4654
|
const context = loadContext(specLoreDir) ?? buildContext(projectRoot2, config);
|
|
4613
4655
|
const featureFiles = resolveFeatureFiles(args.features, projectRoot2, config);
|
|
4614
4656
|
const stateManager = new StateManager(projectRoot2);
|
|
@@ -4670,22 +4712,22 @@ async function executeVerifyTool(args, projectRoot2) {
|
|
|
4670
4712
|
};
|
|
4671
4713
|
}
|
|
4672
4714
|
function ensureProjectReady(projectRoot2) {
|
|
4673
|
-
const specLoreDir =
|
|
4674
|
-
const configPath =
|
|
4715
|
+
const specLoreDir = join32(projectRoot2, ".speclore");
|
|
4716
|
+
const configPath = join32(specLoreDir, "config.yaml");
|
|
4675
4717
|
let configCreated = false;
|
|
4676
|
-
if (!
|
|
4677
|
-
|
|
4718
|
+
if (!existsSync29(specLoreDir)) {
|
|
4719
|
+
mkdirSync14(specLoreDir, { recursive: true });
|
|
4678
4720
|
}
|
|
4679
|
-
if (!
|
|
4721
|
+
if (!existsSync29(configPath)) {
|
|
4680
4722
|
const projectName = projectRoot2.split(/[/\\]/).pop() ?? "my-project";
|
|
4681
|
-
|
|
4723
|
+
writeFileSync17(configPath, generateDefaultConfigYaml(projectName), "utf-8");
|
|
4682
4724
|
configCreated = true;
|
|
4683
4725
|
logger.info(`Auto-created default config: ${configPath}`);
|
|
4684
4726
|
}
|
|
4685
4727
|
const stateManager = new StateManager(projectRoot2);
|
|
4686
4728
|
stateManager.ensureInitialized();
|
|
4687
4729
|
const config = loadConfig(projectRoot2);
|
|
4688
|
-
const specsDir =
|
|
4730
|
+
const specsDir = join32(projectRoot2, config.spec.outputDir);
|
|
4689
4731
|
const migrated = stateManager.migrateFeatures(specsDir);
|
|
4690
4732
|
if (migrated > 0) {
|
|
4691
4733
|
logger.info(`Migrated ${migrated} existing .feature file(s) into state tracking.`);
|
|
@@ -4722,14 +4764,14 @@ function resolveNextStep(currentState, _summary) {
|
|
|
4722
4764
|
}
|
|
4723
4765
|
}
|
|
4724
4766
|
function resolveFeatureFiles(patterns, projectRoot2, config) {
|
|
4725
|
-
const specsDir =
|
|
4767
|
+
const specsDir = join32(projectRoot2, config.spec.outputDir);
|
|
4726
4768
|
const searchPatterns = patterns && patterns.length > 0 ? patterns : [`${specsDir}/**/*.feature`];
|
|
4727
4769
|
const files = [];
|
|
4728
4770
|
for (const pattern of searchPatterns) {
|
|
4729
4771
|
const matches = globSync7(pattern, { cwd: projectRoot2, absolute: true });
|
|
4730
4772
|
for (const filePath of matches) {
|
|
4731
|
-
if (
|
|
4732
|
-
const content =
|
|
4773
|
+
if (existsSync29(filePath)) {
|
|
4774
|
+
const content = readFileSync22(filePath, "utf-8");
|
|
4733
4775
|
files.push(parseFeatureFile(filePath, content));
|
|
4734
4776
|
}
|
|
4735
4777
|
}
|
|
@@ -4837,8 +4879,8 @@ var init_tools = __esm({
|
|
|
4837
4879
|
});
|
|
4838
4880
|
|
|
4839
4881
|
// src/mcp/status.ts
|
|
4840
|
-
import { readFileSync as
|
|
4841
|
-
import { join as
|
|
4882
|
+
import { readFileSync as readFileSync23, existsSync as existsSync30, mkdirSync as mkdirSync15, writeFileSync as writeFileSync18 } from "fs";
|
|
4883
|
+
import { join as join33 } from "path";
|
|
4842
4884
|
import { globSync as globSync8 } from "glob";
|
|
4843
4885
|
function executeStatusTool(args, projectRoot2) {
|
|
4844
4886
|
const { configCreated } = ensureProjectReadyForStatus(projectRoot2);
|
|
@@ -4854,9 +4896,9 @@ function executeStatusTool(args, projectRoot2) {
|
|
|
4854
4896
|
continue;
|
|
4855
4897
|
}
|
|
4856
4898
|
let scenarioCount = 0;
|
|
4857
|
-
if (
|
|
4899
|
+
if (existsSync30(path)) {
|
|
4858
4900
|
try {
|
|
4859
|
-
const content =
|
|
4901
|
+
const content = readFileSync23(path, "utf-8");
|
|
4860
4902
|
const matches = content.match(/Scenario(?: Outline)?:/g);
|
|
4861
4903
|
scenarioCount = matches?.length ?? 0;
|
|
4862
4904
|
} catch {
|
|
@@ -4871,7 +4913,7 @@ function executeStatusTool(args, projectRoot2) {
|
|
|
4871
4913
|
lastVerify: entry.lastVerify ? { passed: entry.lastVerify.passed, failed: entry.lastVerify.failed, timestamp: entry.lastVerify.timestamp } : void 0
|
|
4872
4914
|
});
|
|
4873
4915
|
}
|
|
4874
|
-
const specsDir =
|
|
4916
|
+
const specsDir = join33(projectRoot2, config.spec.outputDir);
|
|
4875
4917
|
const allFeatureFiles = globSync8(`${specsDir}/**/*.feature`, { cwd: projectRoot2, absolute: true });
|
|
4876
4918
|
const trackedPaths = new Set(featureEntries.map((f) => f.path));
|
|
4877
4919
|
for (const filePath of allFeatureFiles) {
|
|
@@ -4879,7 +4921,7 @@ function executeStatusTool(args, projectRoot2) {
|
|
|
4879
4921
|
if (args.feature && !filePath.includes(args.feature)) continue;
|
|
4880
4922
|
let scenarioCount = 0;
|
|
4881
4923
|
try {
|
|
4882
|
-
const content =
|
|
4924
|
+
const content = readFileSync23(filePath, "utf-8");
|
|
4883
4925
|
const matches = content.match(/Scenario(?: Outline)?:/g);
|
|
4884
4926
|
scenarioCount = matches?.length ?? 0;
|
|
4885
4927
|
} catch {
|
|
@@ -4936,22 +4978,22 @@ function buildRecommendedActions(features, summary, testCommand) {
|
|
|
4936
4978
|
return actions;
|
|
4937
4979
|
}
|
|
4938
4980
|
function ensureProjectReadyForStatus(projectRoot2) {
|
|
4939
|
-
const specLoreDir =
|
|
4940
|
-
const configPath =
|
|
4981
|
+
const specLoreDir = join33(projectRoot2, ".speclore");
|
|
4982
|
+
const configPath = join33(specLoreDir, "config.yaml");
|
|
4941
4983
|
let configCreated = false;
|
|
4942
|
-
if (!
|
|
4943
|
-
|
|
4984
|
+
if (!existsSync30(specLoreDir)) {
|
|
4985
|
+
mkdirSync15(specLoreDir, { recursive: true });
|
|
4944
4986
|
}
|
|
4945
|
-
if (!
|
|
4987
|
+
if (!existsSync30(configPath)) {
|
|
4946
4988
|
const projectName = projectRoot2.split(/[/\\]/).pop() ?? "my-project";
|
|
4947
|
-
|
|
4989
|
+
writeFileSync18(configPath, generateDefaultConfigYaml(projectName), "utf-8");
|
|
4948
4990
|
configCreated = true;
|
|
4949
4991
|
logger.info(`Auto-created default config: ${configPath}`);
|
|
4950
4992
|
}
|
|
4951
4993
|
const stateManager = new StateManager(projectRoot2);
|
|
4952
4994
|
stateManager.ensureInitialized();
|
|
4953
4995
|
const config = loadConfig(projectRoot2);
|
|
4954
|
-
const specsDir =
|
|
4996
|
+
const specsDir = join33(projectRoot2, config.spec.outputDir);
|
|
4955
4997
|
const migrated = stateManager.migrateFeatures(specsDir);
|
|
4956
4998
|
if (migrated > 0) {
|
|
4957
4999
|
logger.info(`Migrated ${migrated} existing .feature file(s) into state tracking.`);
|
|
@@ -4975,7 +5017,7 @@ __export(server_exports, {
|
|
|
4975
5017
|
});
|
|
4976
5018
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4977
5019
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4978
|
-
import { mkdirSync as
|
|
5020
|
+
import { mkdirSync as mkdirSync16 } from "fs";
|
|
4979
5021
|
function getProjectRoot() {
|
|
4980
5022
|
return projectRoot;
|
|
4981
5023
|
}
|
|
@@ -4983,7 +5025,7 @@ async function startMcpServer() {
|
|
|
4983
5025
|
projectRoot = process.env.SPECLORE_PROJECT_ROOT ?? process.cwd();
|
|
4984
5026
|
const specLoreDir = `${projectRoot}/.speclore`;
|
|
4985
5027
|
try {
|
|
4986
|
-
|
|
5028
|
+
mkdirSync16(specLoreDir, { recursive: true });
|
|
4987
5029
|
const locked = acquireLock(specLoreDir);
|
|
4988
5030
|
if (!locked) {
|
|
4989
5031
|
logger.warn("Could not acquire lock \u2014 another SpecLore instance may be running.");
|
|
@@ -5096,11 +5138,11 @@ import { Command } from "commander";
|
|
|
5096
5138
|
init_logger();
|
|
5097
5139
|
init_config2();
|
|
5098
5140
|
init_detector();
|
|
5099
|
-
import { existsSync as existsSync5, mkdirSync as
|
|
5141
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
5100
5142
|
import { join as join7 } from "path";
|
|
5101
5143
|
|
|
5102
5144
|
// src/setup/config-writer.ts
|
|
5103
|
-
import { writeFileSync as writeFileSync2, existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
|
|
5145
|
+
import { writeFileSync as writeFileSync2, existsSync as existsSync4, readFileSync as readFileSync4, mkdirSync as mkdirSync2 } from "fs";
|
|
5104
5146
|
import { join as join5 } from "path";
|
|
5105
5147
|
|
|
5106
5148
|
// src/infra/install-detector.ts
|
|
@@ -5151,13 +5193,42 @@ function detectCloneInstall(projectRoot2) {
|
|
|
5151
5193
|
|
|
5152
5194
|
// src/setup/config-writer.ts
|
|
5153
5195
|
init_logger();
|
|
5154
|
-
function writeMcpConfig(projectRoot2, _globalMode) {
|
|
5196
|
+
function writeMcpConfig(projectRoot2, tools, _globalMode) {
|
|
5197
|
+
const installInfo = detectInstallMethod(projectRoot2);
|
|
5198
|
+
const serverCommand = getServerCommand(installInfo);
|
|
5199
|
+
const detectedToolNames = tools.map((t) => t.tool);
|
|
5200
|
+
if (detectedToolNames.includes("cursor")) {
|
|
5201
|
+
writeCursorMcp(projectRoot2, serverCommand);
|
|
5202
|
+
}
|
|
5203
|
+
if (detectedToolNames.includes("claude")) {
|
|
5204
|
+
writeClaudeMcp(projectRoot2, serverCommand);
|
|
5205
|
+
}
|
|
5206
|
+
if (detectedToolNames.includes("qoder")) {
|
|
5207
|
+
writeQoderMcp(projectRoot2, serverCommand);
|
|
5208
|
+
}
|
|
5209
|
+
if (detectedToolNames.length > 0) {
|
|
5210
|
+
logger.info(`MCP configuration written for: ${detectedToolNames.join(", ")}`);
|
|
5211
|
+
}
|
|
5212
|
+
}
|
|
5213
|
+
function writeMcpForClient(projectRoot2, client) {
|
|
5155
5214
|
const installInfo = detectInstallMethod(projectRoot2);
|
|
5156
5215
|
const serverCommand = getServerCommand(installInfo);
|
|
5157
|
-
|
|
5158
|
-
|
|
5159
|
-
|
|
5160
|
-
|
|
5216
|
+
switch (client) {
|
|
5217
|
+
case "cursor":
|
|
5218
|
+
mkdirSync2(join5(projectRoot2, ".cursor"), { recursive: true });
|
|
5219
|
+
writeCursorMcp(projectRoot2, serverCommand);
|
|
5220
|
+
break;
|
|
5221
|
+
case "claude":
|
|
5222
|
+
if (!existsSync4(join5(projectRoot2, ".claude"))) {
|
|
5223
|
+
mkdirSync2(join5(projectRoot2, ".claude"), { recursive: true });
|
|
5224
|
+
}
|
|
5225
|
+
writeClaudeMcp(projectRoot2, serverCommand);
|
|
5226
|
+
break;
|
|
5227
|
+
case "qoder":
|
|
5228
|
+
mkdirSync2(join5(projectRoot2, ".qoder"), { recursive: true });
|
|
5229
|
+
writeQoderMcp(projectRoot2, serverCommand);
|
|
5230
|
+
break;
|
|
5231
|
+
}
|
|
5161
5232
|
}
|
|
5162
5233
|
function getServerCommand(installInfo) {
|
|
5163
5234
|
if (installInfo.mode === "npm") {
|
|
@@ -5181,6 +5252,9 @@ function writeCursorMcp(projectRoot2, serverCmd) {
|
|
|
5181
5252
|
logger.info(` Cursor: ${mcpPath}`);
|
|
5182
5253
|
}
|
|
5183
5254
|
function writeClaudeMcp(projectRoot2, serverCmd) {
|
|
5255
|
+
const hasClaudeDir = existsSync4(join5(projectRoot2, ".claude"));
|
|
5256
|
+
const hasClaudeMd = existsSync4(join5(projectRoot2, "CLAUDE.md"));
|
|
5257
|
+
if (!hasClaudeDir && !hasClaudeMd) return;
|
|
5184
5258
|
const mcpPath = join5(projectRoot2, ".mcp.json");
|
|
5185
5259
|
const existing = readExistingJson(mcpPath);
|
|
5186
5260
|
if (!existing.mcpServers) existing.mcpServers = {};
|
|
@@ -5219,7 +5293,7 @@ function readExistingJson(filePath) {
|
|
|
5219
5293
|
|
|
5220
5294
|
// src/setup/rule-writer.ts
|
|
5221
5295
|
init_logger();
|
|
5222
|
-
import { writeFileSync as writeFileSync3, mkdirSync as
|
|
5296
|
+
import { writeFileSync as writeFileSync3, mkdirSync as mkdirSync3 } from "fs";
|
|
5223
5297
|
import { join as join6 } from "path";
|
|
5224
5298
|
function writeRuleFiles(projectRoot2, tools) {
|
|
5225
5299
|
for (const tool of tools) {
|
|
@@ -5238,7 +5312,7 @@ function writeRuleFiles(projectRoot2, tools) {
|
|
|
5238
5312
|
}
|
|
5239
5313
|
function writeCursorRule(projectRoot2) {
|
|
5240
5314
|
const rulesDir = join6(projectRoot2, ".cursor", "rules");
|
|
5241
|
-
|
|
5315
|
+
mkdirSync3(rulesDir, { recursive: true });
|
|
5242
5316
|
const content = `---
|
|
5243
5317
|
description: SpecLore \u2014 AI coding constraints (setup placeholder)
|
|
5244
5318
|
globs:
|
|
@@ -5260,7 +5334,7 @@ SpecLore is configured for this project. When the \`speclore.spec\` MCP tool is
|
|
|
5260
5334
|
}
|
|
5261
5335
|
function writeClaudeRule(projectRoot2) {
|
|
5262
5336
|
const rulesDir = join6(projectRoot2, ".claude", "rules");
|
|
5263
|
-
|
|
5337
|
+
mkdirSync3(rulesDir, { recursive: true });
|
|
5264
5338
|
const content = `# SpecLore
|
|
5265
5339
|
|
|
5266
5340
|
SpecLore is configured for this project. When the \`speclore.spec\` MCP tool is available:
|
|
@@ -5276,7 +5350,7 @@ SpecLore is configured for this project. When the \`speclore.spec\` MCP tool is
|
|
|
5276
5350
|
}
|
|
5277
5351
|
function writeQoderRule(projectRoot2) {
|
|
5278
5352
|
const rulesDir = join6(projectRoot2, ".qoder", "rules");
|
|
5279
|
-
|
|
5353
|
+
mkdirSync3(rulesDir, { recursive: true });
|
|
5280
5354
|
const content = `# SpecLore
|
|
5281
5355
|
|
|
5282
5356
|
SpecLore is configured for this project. When the \`speclore.spec\` MCP tool is available:
|
|
@@ -5300,13 +5374,20 @@ function registerSetupCommand(program) {
|
|
|
5300
5374
|
logger.info("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
5301
5375
|
const tools = detectAITools(projectRoot2);
|
|
5302
5376
|
if (tools.length === 0) {
|
|
5303
|
-
logger.warn("No supported AI tools detected.
|
|
5377
|
+
logger.warn("No supported AI tools detected.");
|
|
5378
|
+
logger.info("");
|
|
5379
|
+
logger.info(" To enable MCP integration, open your AI client in this project first,");
|
|
5380
|
+
logger.info(" then re-run setup. Or use manual configuration:");
|
|
5381
|
+
logger.info(" speclore mcp add cursor \u2014 configure for Cursor");
|
|
5382
|
+
logger.info(" speclore mcp add claude \u2014 configure for Claude Code");
|
|
5383
|
+
logger.info(" speclore mcp add qoder \u2014 configure for Qoder");
|
|
5384
|
+
logger.info("");
|
|
5304
5385
|
} else {
|
|
5305
5386
|
logger.info(`Detected AI tools: ${tools.map((t) => t.tool).join(", ")}`);
|
|
5306
5387
|
}
|
|
5307
5388
|
const specLoreDir = opts.global ? join7(process.env.HOME ?? process.env.USERPROFILE ?? "~", ".speclore") : join7(projectRoot2, ".speclore");
|
|
5308
5389
|
if (!existsSync5(specLoreDir)) {
|
|
5309
|
-
|
|
5390
|
+
mkdirSync4(specLoreDir, { recursive: true });
|
|
5310
5391
|
}
|
|
5311
5392
|
const configPath = join7(specLoreDir, "config.yaml");
|
|
5312
5393
|
if (!existsSync5(configPath)) {
|
|
@@ -5314,12 +5395,23 @@ function registerSetupCommand(program) {
|
|
|
5314
5395
|
writeFileSync4(configPath, generateDefaultConfigYaml(projectName), "utf-8");
|
|
5315
5396
|
logger.info(`Created config: ${configPath}`);
|
|
5316
5397
|
}
|
|
5317
|
-
writeMcpConfig(projectRoot2, opts.global ?? false);
|
|
5398
|
+
writeMcpConfig(projectRoot2, tools, opts.global ?? false);
|
|
5318
5399
|
writeRuleFiles(projectRoot2, tools);
|
|
5319
5400
|
logger.info("");
|
|
5320
|
-
logger.info("Setup complete!
|
|
5321
|
-
logger.info("
|
|
5322
|
-
logger.info("
|
|
5401
|
+
logger.info("Setup complete! Next steps:");
|
|
5402
|
+
logger.info("");
|
|
5403
|
+
logger.info(" Option A \u2014 CLI workflow (terminal users):");
|
|
5404
|
+
logger.info(' speclore spec "your requirement" \u2192 generate .feature');
|
|
5405
|
+
logger.info(" speclore code \u2192 generate constraints + tests");
|
|
5406
|
+
logger.info(" speclore verify \u2192 run acceptance");
|
|
5407
|
+
logger.info("");
|
|
5408
|
+
logger.info(" Option B \u2014 AI client workflow (recommended):");
|
|
5409
|
+
logger.info(" Open Cursor / Qoder / Claude Code and start chatting.");
|
|
5410
|
+
logger.info(" MCP is already configured \u2014 AI handles the full pipeline.");
|
|
5411
|
+
logger.info("");
|
|
5412
|
+
logger.info(" Optional \u2014 Pre-scan project context:");
|
|
5413
|
+
logger.info(" speclore init \u2192 scan modules/entities/APIs for better AI context");
|
|
5414
|
+
logger.info(" (Not required \u2014 context is auto-built on first spec/code call)");
|
|
5323
5415
|
});
|
|
5324
5416
|
}
|
|
5325
5417
|
|
|
@@ -5329,7 +5421,7 @@ init_logger();
|
|
|
5329
5421
|
init_context_engine();
|
|
5330
5422
|
import { join as join11 } from "path";
|
|
5331
5423
|
function registerInitCommand(program) {
|
|
5332
|
-
program.command("init").description("
|
|
5424
|
+
program.command("init").description("Scan project structure and generate context.json (optional \u2014 auto-runs on first spec/code call)").option("-v, --verbose", "Enable debug logging").action((opts) => {
|
|
5333
5425
|
if (opts.verbose) logger.setLevel("debug");
|
|
5334
5426
|
const projectRoot2 = process.cwd();
|
|
5335
5427
|
logger.info("SpecLore Init");
|
|
@@ -5393,7 +5485,7 @@ init_test_scaffolder();
|
|
|
5393
5485
|
init_context_engine();
|
|
5394
5486
|
init_state_manager();
|
|
5395
5487
|
import { join as join23 } from "path";
|
|
5396
|
-
import { readFileSync as
|
|
5488
|
+
import { readFileSync as readFileSync14, existsSync as existsSync20 } from "fs";
|
|
5397
5489
|
import { globSync as globSync4 } from "glob";
|
|
5398
5490
|
function registerCodeCommand(program) {
|
|
5399
5491
|
program.command("code").description("Generate AI coding constraint files from .feature specs").argument("[features...]", "Feature file paths or glob patterns (all if omitted)").option("-v, --verbose", "Enable debug logging").action(async (features, opts) => {
|
|
@@ -5449,7 +5541,7 @@ function resolveFeatures(patterns, projectRoot2, config) {
|
|
|
5449
5541
|
const matches = globSync4(pattern, { cwd: projectRoot2, absolute: true });
|
|
5450
5542
|
for (const filePath of matches) {
|
|
5451
5543
|
if (existsSync20(filePath)) {
|
|
5452
|
-
const content =
|
|
5544
|
+
const content = readFileSync14(filePath, "utf-8");
|
|
5453
5545
|
const featureMatch = content.match(/Feature:\s*(.+)/);
|
|
5454
5546
|
files.push({
|
|
5455
5547
|
path: filePath,
|
|
@@ -5473,7 +5565,7 @@ init_context_engine();
|
|
|
5473
5565
|
init_analyzer();
|
|
5474
5566
|
init_state_manager();
|
|
5475
5567
|
import { join as join27 } from "path";
|
|
5476
|
-
import { readFileSync as
|
|
5568
|
+
import { readFileSync as readFileSync18, existsSync as existsSync24 } from "fs";
|
|
5477
5569
|
import { globSync as globSync6 } from "glob";
|
|
5478
5570
|
function registerVerifyCommand(program) {
|
|
5479
5571
|
program.command("verify").description("Run tests and map results to .feature scenarios").argument("[features...]", "Feature file paths or glob patterns (all if omitted)").option("--impact", "Enable change impact analysis").option("--watch", "Watch for .feature file changes and re-run").option("--timeout <minutes>", "Watch timeout in minutes (default: 30)", "30").option("-v, --verbose", "Enable debug logging").action(async (features, opts) => {
|
|
@@ -5560,7 +5652,7 @@ function resolveFeatures2(patterns, projectRoot2, config) {
|
|
|
5560
5652
|
const matches = globSync6(pattern, { cwd: projectRoot2, absolute: true });
|
|
5561
5653
|
for (const filePath of matches) {
|
|
5562
5654
|
if (existsSync24(filePath)) {
|
|
5563
|
-
const content =
|
|
5655
|
+
const content = readFileSync18(filePath, "utf-8");
|
|
5564
5656
|
const featureMatch = content.match(/Feature:\s*(.+)/);
|
|
5565
5657
|
files.push({
|
|
5566
5658
|
path: filePath,
|
|
@@ -5581,7 +5673,7 @@ init_logger();
|
|
|
5581
5673
|
|
|
5582
5674
|
// src/setup/cleanup.ts
|
|
5583
5675
|
init_logger();
|
|
5584
|
-
import { rmSync as rmSync4, existsSync as existsSync25, readFileSync as
|
|
5676
|
+
import { rmSync as rmSync4, existsSync as existsSync25, readFileSync as readFileSync19, writeFileSync as writeFileSync14 } from "fs";
|
|
5585
5677
|
import { join as join28 } from "path";
|
|
5586
5678
|
function runTeardown(projectRoot2, globalMode) {
|
|
5587
5679
|
if (globalMode) {
|
|
@@ -5610,7 +5702,7 @@ function runTeardown(projectRoot2, globalMode) {
|
|
|
5610
5702
|
function removeMcpEntry(mcpPath, serverName) {
|
|
5611
5703
|
if (!existsSync25(mcpPath)) return;
|
|
5612
5704
|
try {
|
|
5613
|
-
const config = JSON.parse(
|
|
5705
|
+
const config = JSON.parse(readFileSync19(mcpPath, "utf-8"));
|
|
5614
5706
|
if (config.mcpServers?.[serverName]) {
|
|
5615
5707
|
delete config.mcpServers[serverName];
|
|
5616
5708
|
writeFileSync14(mcpPath, JSON.stringify(config, null, 2), "utf-8");
|
|
@@ -5706,6 +5798,114 @@ function registerMigrateCommand(program) {
|
|
|
5706
5798
|
});
|
|
5707
5799
|
}
|
|
5708
5800
|
|
|
5801
|
+
// src/cli/commands/mcp-config.ts
|
|
5802
|
+
init_logger();
|
|
5803
|
+
import { existsSync as existsSync27, readFileSync as readFileSync20, writeFileSync as writeFileSync15, rmSync as rmSync5 } from "fs";
|
|
5804
|
+
import { join as join30 } from "path";
|
|
5805
|
+
var VALID_CLIENTS = ["cursor", "claude", "qoder"];
|
|
5806
|
+
function getMcpPath(projectRoot2, client) {
|
|
5807
|
+
switch (client) {
|
|
5808
|
+
case "cursor":
|
|
5809
|
+
return join30(projectRoot2, ".cursor", "mcp.json");
|
|
5810
|
+
case "claude":
|
|
5811
|
+
return join30(projectRoot2, ".mcp.json");
|
|
5812
|
+
case "qoder":
|
|
5813
|
+
return join30(projectRoot2, ".qoder", "mcp.json");
|
|
5814
|
+
}
|
|
5815
|
+
}
|
|
5816
|
+
function clientLabel(client) {
|
|
5817
|
+
switch (client) {
|
|
5818
|
+
case "cursor":
|
|
5819
|
+
return "Cursor";
|
|
5820
|
+
case "claude":
|
|
5821
|
+
return "Claude Code";
|
|
5822
|
+
case "qoder":
|
|
5823
|
+
return "Qoder";
|
|
5824
|
+
}
|
|
5825
|
+
}
|
|
5826
|
+
function registerMcpConfigCommands(program) {
|
|
5827
|
+
const mcpCmd = program.commands.find((c) => c.name() === "mcp");
|
|
5828
|
+
if (!mcpCmd) return;
|
|
5829
|
+
mcpCmd.command("add <client>").description("Manually write SpecLore MCP config for a specific AI client (cursor | claude | qoder)").action((clientArg) => {
|
|
5830
|
+
const projectRoot2 = process.cwd();
|
|
5831
|
+
const client = validateClient(clientArg);
|
|
5832
|
+
if (!client) return;
|
|
5833
|
+
logger.info(`Configuring MCP for ${clientLabel(client)}...`);
|
|
5834
|
+
writeMcpForClient(projectRoot2, client);
|
|
5835
|
+
logger.info(`\u2713 ${clientLabel(client)} MCP configured at ${getMcpPath(projectRoot2, client)}`);
|
|
5836
|
+
});
|
|
5837
|
+
mcpCmd.command("remove <client>").description("Remove SpecLore MCP config from a specific AI client (cursor | claude | qoder)").action((clientArg) => {
|
|
5838
|
+
const projectRoot2 = process.cwd();
|
|
5839
|
+
const client = validateClient(clientArg);
|
|
5840
|
+
if (!client) return;
|
|
5841
|
+
const mcpPath = getMcpPath(projectRoot2, client);
|
|
5842
|
+
if (!existsSync27(mcpPath)) {
|
|
5843
|
+
logger.info(`${clientLabel(client)}: no MCP config file found at ${mcpPath}`);
|
|
5844
|
+
return;
|
|
5845
|
+
}
|
|
5846
|
+
const removed = removeMcpServerEntry(mcpPath, "speclore");
|
|
5847
|
+
if (removed) {
|
|
5848
|
+
logger.info(`\u2713 Removed speclore from ${clientLabel(client)}: ${mcpPath}`);
|
|
5849
|
+
} else {
|
|
5850
|
+
logger.info(`${clientLabel(client)}: speclore entry not found in ${mcpPath}`);
|
|
5851
|
+
}
|
|
5852
|
+
});
|
|
5853
|
+
mcpCmd.command("list").description("Show current SpecLore MCP configuration status for all clients").action(() => {
|
|
5854
|
+
const projectRoot2 = process.cwd();
|
|
5855
|
+
logger.info("SpecLore MCP Configuration Status");
|
|
5856
|
+
logger.info("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
5857
|
+
let anyFound = false;
|
|
5858
|
+
for (const client of VALID_CLIENTS) {
|
|
5859
|
+
const mcpPath = getMcpPath(projectRoot2, client);
|
|
5860
|
+
if (!existsSync27(mcpPath)) {
|
|
5861
|
+
logger.info(` \u25CB ${clientLabel(client)}: no config file (${mcpPath})`);
|
|
5862
|
+
continue;
|
|
5863
|
+
}
|
|
5864
|
+
try {
|
|
5865
|
+
const config = JSON.parse(readFileSync20(mcpPath, "utf-8"));
|
|
5866
|
+
if (config.mcpServers?.["speclore"]) {
|
|
5867
|
+
logger.info(` \u2713 ${clientLabel(client)}: configured \u2192 ${mcpPath}`);
|
|
5868
|
+
anyFound = true;
|
|
5869
|
+
} else {
|
|
5870
|
+
logger.info(` \u25CB ${clientLabel(client)}: config file exists but speclore not registered \u2192 ${mcpPath}`);
|
|
5871
|
+
}
|
|
5872
|
+
} catch {
|
|
5873
|
+
logger.info(` \u2717 ${clientLabel(client)}: failed to parse ${mcpPath}`);
|
|
5874
|
+
}
|
|
5875
|
+
}
|
|
5876
|
+
if (!anyFound) {
|
|
5877
|
+
logger.info("");
|
|
5878
|
+
logger.info(" No SpecLore MCP configurations found.");
|
|
5879
|
+
logger.info(" Run `speclore setup` or `speclore mcp add <client>` to configure.");
|
|
5880
|
+
}
|
|
5881
|
+
});
|
|
5882
|
+
}
|
|
5883
|
+
function validateClient(arg) {
|
|
5884
|
+
const normalised = arg.toLowerCase().trim();
|
|
5885
|
+
if (!VALID_CLIENTS.includes(normalised)) {
|
|
5886
|
+
logger.error(`Unknown client "${arg}". Supported: ${VALID_CLIENTS.join(", ")}`);
|
|
5887
|
+
return null;
|
|
5888
|
+
}
|
|
5889
|
+
return normalised;
|
|
5890
|
+
}
|
|
5891
|
+
function removeMcpServerEntry(mcpPath, serverName) {
|
|
5892
|
+
if (!existsSync27(mcpPath)) return false;
|
|
5893
|
+
try {
|
|
5894
|
+
const config = JSON.parse(readFileSync20(mcpPath, "utf-8"));
|
|
5895
|
+
if (!config.mcpServers?.[serverName]) return false;
|
|
5896
|
+
delete config.mcpServers[serverName];
|
|
5897
|
+
const remainingServers = Object.keys(config.mcpServers ?? {});
|
|
5898
|
+
if (remainingServers.length === 0) {
|
|
5899
|
+
rmSync5(mcpPath);
|
|
5900
|
+
} else {
|
|
5901
|
+
writeFileSync15(mcpPath, JSON.stringify(config, null, 2), "utf-8");
|
|
5902
|
+
}
|
|
5903
|
+
return true;
|
|
5904
|
+
} catch {
|
|
5905
|
+
return false;
|
|
5906
|
+
}
|
|
5907
|
+
}
|
|
5908
|
+
|
|
5709
5909
|
// src/cli/index.ts
|
|
5710
5910
|
init_version();
|
|
5711
5911
|
function createProgram() {
|
|
@@ -5725,6 +5925,7 @@ function createProgram() {
|
|
|
5725
5925
|
const { startMcpServer: startMcpServer2 } = await Promise.resolve().then(() => (init_server(), server_exports));
|
|
5726
5926
|
await startMcpServer2();
|
|
5727
5927
|
});
|
|
5928
|
+
registerMcpConfigCommands(program);
|
|
5728
5929
|
program.argument("[text...]", "Quick requirement text \u2192 .feature").action(async (text) => {
|
|
5729
5930
|
if (text.length > 0) {
|
|
5730
5931
|
const { loadConfig: loadConfig2 } = await Promise.resolve().then(() => (init_config2(), config_exports));
|