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