speclore 0.1.6 → 0.1.7
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 +378 -173
- package/dist/cli/index.js.map +1 -1
- package/dist/index.js +378 -173
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +159 -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,35 @@ 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
|
+
isEvalSupported: false
|
|
1508
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
1509
|
+
}).promise;
|
|
1510
|
+
const textParts = [];
|
|
1511
|
+
for (let i = 1; i <= doc.numPages; i++) {
|
|
1512
|
+
const page = await doc.getPage(i);
|
|
1513
|
+
const textContent = await page.getTextContent();
|
|
1514
|
+
const pageText = textContent.items.map((item) => item.str).join("");
|
|
1515
|
+
if (pageText) textParts.push(pageText);
|
|
1503
1516
|
}
|
|
1504
|
-
|
|
1505
|
-
const
|
|
1506
|
-
const data = await pdfParse(buffer);
|
|
1517
|
+
doc.destroy();
|
|
1518
|
+
const text = textParts.join("\n");
|
|
1507
1519
|
return [{
|
|
1508
1520
|
id: source.replace(/\\/g, "/").replace(/\.pdf$/i, ""),
|
|
1509
1521
|
title: source.split("/").pop()?.replace(/\.pdf$/i, "") ?? "Untitled",
|
|
1510
|
-
description:
|
|
1511
|
-
rawContent:
|
|
1522
|
+
description: text,
|
|
1523
|
+
rawContent: text,
|
|
1512
1524
|
confidence: 0.75
|
|
1513
1525
|
}];
|
|
1514
1526
|
}
|
|
@@ -2104,13 +2116,13 @@ var init_image_reader = __esm({
|
|
|
2104
2116
|
return /\.(png|jpe?g|webp)$/i.test(source);
|
|
2105
2117
|
}
|
|
2106
2118
|
async read(source) {
|
|
2107
|
-
const { existsSync:
|
|
2108
|
-
if (!
|
|
2119
|
+
const { existsSync: existsSync31, readFileSync: readFileSync24 } = await import("fs");
|
|
2120
|
+
if (!existsSync31(source)) {
|
|
2109
2121
|
throw new Error(`Image file not found: ${source}`);
|
|
2110
2122
|
}
|
|
2111
2123
|
const ext = source.split(".").pop()?.toLowerCase() ?? "png";
|
|
2112
2124
|
const mimeType = ext === "jpg" ? "image/jpeg" : `image/${ext}`;
|
|
2113
|
-
const buffer =
|
|
2125
|
+
const buffer = readFileSync24(source);
|
|
2114
2126
|
let text;
|
|
2115
2127
|
try {
|
|
2116
2128
|
const { createProvider: createProvider2 } = await Promise.resolve().then(() => (init_provider(), provider_exports));
|
|
@@ -2139,7 +2151,7 @@ var init_image_reader = __esm({
|
|
|
2139
2151
|
});
|
|
2140
2152
|
|
|
2141
2153
|
// src/plugins/builtin/cursor-writer.ts
|
|
2142
|
-
import { writeFileSync as writeFileSync7, mkdirSync as
|
|
2154
|
+
import { writeFileSync as writeFileSync7, mkdirSync as mkdirSync7, existsSync as existsSync11, rmSync } from "fs";
|
|
2143
2155
|
import { join as join14 } from "path";
|
|
2144
2156
|
var CursorWriter;
|
|
2145
2157
|
var init_cursor_writer = __esm({
|
|
@@ -2155,7 +2167,7 @@ var init_cursor_writer = __esm({
|
|
|
2155
2167
|
write(constraints) {
|
|
2156
2168
|
this.projectRoot = constraints.projectRoot;
|
|
2157
2169
|
const rulesDir = join14(constraints.projectRoot, ".cursor", "rules");
|
|
2158
|
-
|
|
2170
|
+
mkdirSync7(rulesDir, { recursive: true });
|
|
2159
2171
|
const frontmatter = [
|
|
2160
2172
|
"---",
|
|
2161
2173
|
"description: SpecLore coding constraints \u2014 auto-generated",
|
|
@@ -2234,7 +2246,7 @@ var init_cursor_writer = __esm({
|
|
|
2234
2246
|
});
|
|
2235
2247
|
|
|
2236
2248
|
// src/plugins/builtin/claude-writer.ts
|
|
2237
|
-
import { writeFileSync as writeFileSync8, mkdirSync as
|
|
2249
|
+
import { writeFileSync as writeFileSync8, mkdirSync as mkdirSync8, existsSync as existsSync12, rmSync as rmSync2 } from "fs";
|
|
2238
2250
|
import { join as join15 } from "path";
|
|
2239
2251
|
var ClaudeWriter;
|
|
2240
2252
|
var init_claude_writer = __esm({
|
|
@@ -2250,7 +2262,7 @@ var init_claude_writer = __esm({
|
|
|
2250
2262
|
write(constraints) {
|
|
2251
2263
|
this.projectRoot = constraints.projectRoot;
|
|
2252
2264
|
const rulesDir = join15(constraints.projectRoot, ".claude", "rules");
|
|
2253
|
-
|
|
2265
|
+
mkdirSync8(rulesDir, { recursive: true });
|
|
2254
2266
|
const content = this.buildMarkdown(constraints);
|
|
2255
2267
|
writeFileSync8(join15(rulesDir, "speclore.md"), content, "utf-8");
|
|
2256
2268
|
return Promise.resolve();
|
|
@@ -2316,7 +2328,7 @@ var init_claude_writer = __esm({
|
|
|
2316
2328
|
});
|
|
2317
2329
|
|
|
2318
2330
|
// src/plugins/builtin/qoder-writer.ts
|
|
2319
|
-
import { writeFileSync as writeFileSync9, mkdirSync as
|
|
2331
|
+
import { writeFileSync as writeFileSync9, mkdirSync as mkdirSync9, existsSync as existsSync13, rmSync as rmSync3 } from "fs";
|
|
2320
2332
|
import { join as join16 } from "path";
|
|
2321
2333
|
var QoderWriter;
|
|
2322
2334
|
var init_qoder_writer = __esm({
|
|
@@ -2332,7 +2344,7 @@ var init_qoder_writer = __esm({
|
|
|
2332
2344
|
write(constraints) {
|
|
2333
2345
|
this.projectRoot = constraints.projectRoot;
|
|
2334
2346
|
const rulesDir = join16(constraints.projectRoot, ".qoder", "rules");
|
|
2335
|
-
|
|
2347
|
+
mkdirSync9(rulesDir, { recursive: true });
|
|
2336
2348
|
const content = this.buildMarkdown(constraints);
|
|
2337
2349
|
writeFileSync9(join16(rulesDir, "speclore.md"), content, "utf-8");
|
|
2338
2350
|
return Promise.resolve();
|
|
@@ -2765,11 +2777,12 @@ var init_markdown_reader = __esm({
|
|
|
2765
2777
|
|
|
2766
2778
|
// src/core/requirement-reader/docx-reader.ts
|
|
2767
2779
|
import { basename as basename3, extname as extname3 } from "path";
|
|
2780
|
+
import { readFileSync as readFileSync10 } from "fs";
|
|
2768
2781
|
import mammoth from "mammoth";
|
|
2769
|
-
async function
|
|
2770
|
-
const result = await mammoth.extractRawText({
|
|
2782
|
+
async function parseDocxBuffer(buffer, idHint = "document") {
|
|
2783
|
+
const result = await mammoth.extractRawText({ buffer });
|
|
2771
2784
|
const content = result.value;
|
|
2772
|
-
const id =
|
|
2785
|
+
const id = idHint.toLowerCase().replace(/[^a-z0-9\u4e00-\u9fff-]/g, "-").replace(/-+/g, "-");
|
|
2773
2786
|
const lines = content.split("\n").filter((l) => l.trim());
|
|
2774
2787
|
const title = lines[0]?.trim() ?? id;
|
|
2775
2788
|
return {
|
|
@@ -2780,6 +2793,11 @@ async function readDocxFile(filePath) {
|
|
|
2780
2793
|
confidence: 0.9
|
|
2781
2794
|
};
|
|
2782
2795
|
}
|
|
2796
|
+
async function readDocxFile(filePath) {
|
|
2797
|
+
const buffer = readFileSync10(filePath);
|
|
2798
|
+
const name = basename3(filePath, extname3(filePath));
|
|
2799
|
+
return parseDocxBuffer(buffer, name);
|
|
2800
|
+
}
|
|
2783
2801
|
var init_docx_reader2 = __esm({
|
|
2784
2802
|
"src/core/requirement-reader/docx-reader.ts"() {
|
|
2785
2803
|
"use strict";
|
|
@@ -2788,17 +2806,16 @@ var init_docx_reader2 = __esm({
|
|
|
2788
2806
|
|
|
2789
2807
|
// src/core/requirement-reader/xlsx-reader.ts
|
|
2790
2808
|
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, "-");
|
|
2809
|
+
import { readFile as readFile2, read as read2, utils as utils2 } from "xlsx";
|
|
2810
|
+
function parseWorkbook2(workbook, idHint) {
|
|
2811
|
+
const id = idHint.toLowerCase().replace(/[^a-z0-9\u4e00-\u9fff-]/g, "-").replace(/-+/g, "-");
|
|
2795
2812
|
const firstSheetName = workbook.SheetNames[0];
|
|
2796
2813
|
if (!firstSheetName) {
|
|
2797
|
-
return Promise.reject(new Error(
|
|
2814
|
+
return Promise.reject(new Error("No sheets found in workbook"));
|
|
2798
2815
|
}
|
|
2799
2816
|
const sheet = workbook.Sheets[firstSheetName];
|
|
2800
2817
|
if (!sheet) {
|
|
2801
|
-
return Promise.reject(new Error(`Sheet "${firstSheetName}" not found
|
|
2818
|
+
return Promise.reject(new Error(`Sheet "${firstSheetName}" not found`));
|
|
2802
2819
|
}
|
|
2803
2820
|
const title = firstSheetName;
|
|
2804
2821
|
const rows = utils2.sheet_to_json(sheet, { header: 1, defval: "" });
|
|
@@ -2834,6 +2851,11 @@ function readXlsxFile(filePath) {
|
|
|
2834
2851
|
confidence: 0.85
|
|
2835
2852
|
});
|
|
2836
2853
|
}
|
|
2854
|
+
function readXlsxFile(filePath) {
|
|
2855
|
+
const workbook = readFile2(filePath);
|
|
2856
|
+
const idHint = basename4(filePath, extname4(filePath));
|
|
2857
|
+
return parseWorkbook2(workbook, idHint);
|
|
2858
|
+
}
|
|
2837
2859
|
var init_xlsx_reader2 = __esm({
|
|
2838
2860
|
"src/core/requirement-reader/xlsx-reader.ts"() {
|
|
2839
2861
|
"use strict";
|
|
@@ -2844,21 +2866,42 @@ var init_xlsx_reader2 = __esm({
|
|
|
2844
2866
|
// src/core/requirement-reader/pdf-reader.ts
|
|
2845
2867
|
import { basename as basename5, extname as extname5 } from "path";
|
|
2846
2868
|
import { readFile as readFile3 } from "fs/promises";
|
|
2847
|
-
async function
|
|
2848
|
-
const
|
|
2849
|
-
const
|
|
2850
|
-
const
|
|
2851
|
-
|
|
2852
|
-
|
|
2869
|
+
async function extractPdfText(dataBuffer) {
|
|
2870
|
+
const pdfjsLib = await import("pdfjs-dist/legacy/build/pdf.mjs");
|
|
2871
|
+
const uint8 = new Uint8Array(dataBuffer);
|
|
2872
|
+
const doc = await pdfjsLib.getDocument({
|
|
2873
|
+
data: uint8,
|
|
2874
|
+
useWorkerFetch: false,
|
|
2875
|
+
isEvalSupported: false
|
|
2876
|
+
}).promise;
|
|
2877
|
+
const textParts = [];
|
|
2878
|
+
for (let i = 1; i <= doc.numPages; i++) {
|
|
2879
|
+
const page = await doc.getPage(i);
|
|
2880
|
+
const textContent = await page.getTextContent();
|
|
2881
|
+
const pageText = textContent.items.map((item) => item.str).join("");
|
|
2882
|
+
if (pageText) textParts.push(pageText);
|
|
2883
|
+
}
|
|
2884
|
+
doc.destroy();
|
|
2885
|
+
return textParts.join("\n");
|
|
2886
|
+
}
|
|
2887
|
+
async function parsePdfBuffer(dataBuffer, idHint = "document") {
|
|
2888
|
+
const text = await extractPdfText(dataBuffer);
|
|
2889
|
+
const id = idHint.toLowerCase().replace(/[^a-z0-9\u4e00-\u9fff-]/g, "-").replace(/-+/g, "-");
|
|
2890
|
+
const lines = text.split("\n").filter((l) => l.trim());
|
|
2853
2891
|
const title = lines[0]?.trim() ?? id;
|
|
2854
2892
|
return {
|
|
2855
2893
|
id,
|
|
2856
2894
|
title,
|
|
2857
|
-
description:
|
|
2858
|
-
rawContent:
|
|
2895
|
+
description: text,
|
|
2896
|
+
rawContent: text,
|
|
2859
2897
|
confidence: 0.8
|
|
2860
2898
|
};
|
|
2861
2899
|
}
|
|
2900
|
+
async function readPdfFile(filePath) {
|
|
2901
|
+
const dataBuffer = await readFile3(filePath);
|
|
2902
|
+
const name = basename5(filePath, extname5(filePath));
|
|
2903
|
+
return parsePdfBuffer(dataBuffer, name);
|
|
2904
|
+
}
|
|
2862
2905
|
var init_pdf_reader2 = __esm({
|
|
2863
2906
|
"src/core/requirement-reader/pdf-reader.ts"() {
|
|
2864
2907
|
"use strict";
|
|
@@ -2867,14 +2910,14 @@ var init_pdf_reader2 = __esm({
|
|
|
2867
2910
|
|
|
2868
2911
|
// src/core/requirement-reader/image-reader.ts
|
|
2869
2912
|
import { basename as basename6, extname as extname6 } from "path";
|
|
2870
|
-
import { readFileSync as
|
|
2871
|
-
async function readImageFile(filePath) {
|
|
2913
|
+
import { readFileSync as readFileSync11 } from "fs";
|
|
2914
|
+
async function readImageFile(filePath, providerOverride) {
|
|
2872
2915
|
const id = basename6(filePath, extname6(filePath)).toLowerCase().replace(/[^a-z0-9\u4e00-\u9fff-]/g, "-").replace(/-+/g, "-");
|
|
2873
2916
|
logger.info(`Reading image via AI Vision: ${filePath}`);
|
|
2874
2917
|
const ext = extname6(filePath).toLowerCase();
|
|
2875
2918
|
const mimeType = MIME_MAP[ext] ?? "image/png";
|
|
2876
|
-
const buffer =
|
|
2877
|
-
const provider = await createProvider();
|
|
2919
|
+
const buffer = readFileSync11(filePath);
|
|
2920
|
+
const provider = providerOverride ?? await createProvider();
|
|
2878
2921
|
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
2922
|
if (!provider.generateWithImage) {
|
|
2880
2923
|
throw new Error(`AI provider '${provider.name}' does not support image/vision input. Use a vision-capable model.`);
|
|
@@ -3319,11 +3362,11 @@ var init_prompt_builder = __esm({
|
|
|
3319
3362
|
});
|
|
3320
3363
|
|
|
3321
3364
|
// src/core/feature-generator/generator.ts
|
|
3322
|
-
import { writeFileSync as writeFileSync10, mkdirSync as
|
|
3365
|
+
import { writeFileSync as writeFileSync10, mkdirSync as mkdirSync10, existsSync as existsSync15 } from "fs";
|
|
3323
3366
|
import { join as join17, dirname as dirname2 } from "path";
|
|
3324
3367
|
import { Parser as Parser2, GherkinClassicTokenMatcher as GherkinClassicTokenMatcher2, AstBuilder as AstBuilder2 } from "@cucumber/gherkin";
|
|
3325
3368
|
import { IdGenerator as IdGenerator2 } from "@cucumber/messages";
|
|
3326
|
-
async function generateFeature(requirement, context, config, projectRoot2) {
|
|
3369
|
+
async function generateFeature(requirement, context, config, projectRoot2, providerOverride) {
|
|
3327
3370
|
logger.info(`Generating feature for: ${requirement.title}`);
|
|
3328
3371
|
const registry = getRegistry();
|
|
3329
3372
|
await registry.invokeLifecycle("beforeSpec", requirement);
|
|
@@ -3331,7 +3374,7 @@ async function generateFeature(requirement, context, config, projectRoot2) {
|
|
|
3331
3374
|
const aiConfig = config.ai;
|
|
3332
3375
|
const fallbackConfigs = aiConfig?.fallbackProviders ?? [];
|
|
3333
3376
|
const providerConfigs = aiConfig ? [aiConfig, ...fallbackConfigs] : [];
|
|
3334
|
-
const provider = providerConfigs.length > 1 ? await createProviderChain(providerConfigs) : await createProvider(aiConfig);
|
|
3377
|
+
const provider = providerOverride ?? (providerConfigs.length > 1 ? await createProviderChain(providerConfigs) : await createProvider(aiConfig));
|
|
3335
3378
|
if (!provider.isAvailable()) {
|
|
3336
3379
|
throw new Error("AI provider not available. Set API key in environment or config.yaml.");
|
|
3337
3380
|
}
|
|
@@ -3388,7 +3431,7 @@ Please fix these issues and regenerate the complete Feature file.`;
|
|
|
3388
3431
|
const moduleDir = inferModule(requirement, context);
|
|
3389
3432
|
const filePath = join17(outputDir, moduleDir, `${requirement.id}.feature`);
|
|
3390
3433
|
if (!existsSync15(dirname2(filePath))) {
|
|
3391
|
-
|
|
3434
|
+
mkdirSync10(dirname2(filePath), { recursive: true });
|
|
3392
3435
|
}
|
|
3393
3436
|
writeFileSync10(filePath, featureContent, "utf-8");
|
|
3394
3437
|
logger.info(`Feature written: ${toPosixPath(filePath)}`);
|
|
@@ -3554,9 +3597,9 @@ function detectAITools2(projectRoot2) {
|
|
|
3554
3597
|
tools.push("cursor");
|
|
3555
3598
|
logger.debug("Detected Cursor (.cursor/)");
|
|
3556
3599
|
}
|
|
3557
|
-
if (existsSync16(join19(projectRoot2, ".claude")) || existsSync16(join19(projectRoot2, "CLAUDE.md"))
|
|
3600
|
+
if (existsSync16(join19(projectRoot2, ".claude")) || existsSync16(join19(projectRoot2, "CLAUDE.md"))) {
|
|
3558
3601
|
tools.push("claude");
|
|
3559
|
-
logger.debug("Detected Claude Code (.claude/ or CLAUDE.md
|
|
3602
|
+
logger.debug("Detected Claude Code (.claude/ or CLAUDE.md)");
|
|
3560
3603
|
}
|
|
3561
3604
|
if (existsSync16(join19(projectRoot2, ".qoder"))) {
|
|
3562
3605
|
tools.push("qoder");
|
|
@@ -3573,7 +3616,7 @@ var init_ai_tool_detector = __esm({
|
|
|
3573
3616
|
});
|
|
3574
3617
|
|
|
3575
3618
|
// src/core/constraint-coder/constraint-writer.ts
|
|
3576
|
-
import { writeFileSync as writeFileSync11, mkdirSync as
|
|
3619
|
+
import { writeFileSync as writeFileSync11, mkdirSync as mkdirSync11, existsSync as existsSync17 } from "fs";
|
|
3577
3620
|
import { join as join20 } from "path";
|
|
3578
3621
|
function writeConstraints(projectRoot2, tools, content) {
|
|
3579
3622
|
const writtenFiles = [];
|
|
@@ -3647,7 +3690,7 @@ function buildConstraintText(content) {
|
|
|
3647
3690
|
}
|
|
3648
3691
|
function writeCursorRule2(projectRoot2, text) {
|
|
3649
3692
|
const dir = join20(projectRoot2, ".cursor", "rules");
|
|
3650
|
-
if (!existsSync17(dir))
|
|
3693
|
+
if (!existsSync17(dir)) mkdirSync11(dir, { recursive: true });
|
|
3651
3694
|
const filePath = join20(dir, "speclore.mdc");
|
|
3652
3695
|
const frontmatter = [
|
|
3653
3696
|
"---",
|
|
@@ -3662,14 +3705,14 @@ function writeCursorRule2(projectRoot2, text) {
|
|
|
3662
3705
|
}
|
|
3663
3706
|
function writeClaudeRule2(projectRoot2, text) {
|
|
3664
3707
|
const dir = join20(projectRoot2, ".claude", "rules");
|
|
3665
|
-
if (!existsSync17(dir))
|
|
3708
|
+
if (!existsSync17(dir)) mkdirSync11(dir, { recursive: true });
|
|
3666
3709
|
const filePath = join20(dir, "speclore.md");
|
|
3667
3710
|
writeFileSync11(filePath, text, "utf-8");
|
|
3668
3711
|
return filePath;
|
|
3669
3712
|
}
|
|
3670
3713
|
function writeQoderRule2(projectRoot2, text) {
|
|
3671
3714
|
const dir = join20(projectRoot2, ".qoder", "rules");
|
|
3672
|
-
if (!existsSync17(dir))
|
|
3715
|
+
if (!existsSync17(dir)) mkdirSync11(dir, { recursive: true });
|
|
3673
3716
|
const filePath = join20(dir, "speclore.md");
|
|
3674
3717
|
writeFileSync11(filePath, text, "utf-8");
|
|
3675
3718
|
return filePath;
|
|
@@ -3749,14 +3792,14 @@ var init_constraint_coder = __esm({
|
|
|
3749
3792
|
init_logger();
|
|
3750
3793
|
init_ai_tool_detector();
|
|
3751
3794
|
init_constraint_writer();
|
|
3752
|
-
MAPPING_INSTRUCTIONS =
|
|
3753
|
-
|
|
3754
|
-
|
|
3795
|
+
MAPPING_INSTRUCTIONS = `Generate a mapping file for each test file at .speclore/mappings/{module}/{feature-name}.json.
|
|
3796
|
+
Format: { "feature": "specs/...", "scenarios": { "Scenario name": { "testFile": "...", "testMethod": "..." } } }
|
|
3797
|
+
Keep mapping files in sync whenever tests are modified.`;
|
|
3755
3798
|
}
|
|
3756
3799
|
});
|
|
3757
3800
|
|
|
3758
3801
|
// src/core/test-scaffolder/framework-detector.ts
|
|
3759
|
-
import { readFileSync as
|
|
3802
|
+
import { readFileSync as readFileSync12, existsSync as existsSync18 } from "fs";
|
|
3760
3803
|
import { join as join21 } from "path";
|
|
3761
3804
|
function detectTestFramework(projectRoot2) {
|
|
3762
3805
|
const pkgPath = join21(projectRoot2, "package.json");
|
|
@@ -3764,7 +3807,7 @@ function detectTestFramework(projectRoot2) {
|
|
|
3764
3807
|
return "vitest";
|
|
3765
3808
|
}
|
|
3766
3809
|
try {
|
|
3767
|
-
const pkg = JSON.parse(
|
|
3810
|
+
const pkg = JSON.parse(readFileSync12(pkgPath, "utf-8"));
|
|
3768
3811
|
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
3769
3812
|
if ("vitest" in deps) return "vitest";
|
|
3770
3813
|
if ("jest" in deps || "@jest/core" in deps) return "jest";
|
|
@@ -3780,7 +3823,7 @@ var init_framework_detector = __esm({
|
|
|
3780
3823
|
});
|
|
3781
3824
|
|
|
3782
3825
|
// src/core/test-scaffolder/index.ts
|
|
3783
|
-
import { readFileSync as
|
|
3826
|
+
import { readFileSync as readFileSync13, writeFileSync as writeFileSync12, existsSync as existsSync19, mkdirSync as mkdirSync12 } from "fs";
|
|
3784
3827
|
import { join as join22, relative as relative3, dirname as dirname3, basename as basename8 } from "path";
|
|
3785
3828
|
function generateTestScaffolding(projectRoot2, features, config) {
|
|
3786
3829
|
const framework = detectTestFramework(projectRoot2);
|
|
@@ -3803,7 +3846,7 @@ function generateTestScaffolding(projectRoot2, features, config) {
|
|
|
3803
3846
|
const content = generateTestFileContent(feature.featureName, scenarios, framework);
|
|
3804
3847
|
const dir = dirname3(absTestPath);
|
|
3805
3848
|
if (!existsSync19(dir)) {
|
|
3806
|
-
|
|
3849
|
+
mkdirSync12(dir, { recursive: true });
|
|
3807
3850
|
}
|
|
3808
3851
|
writeFileSync12(absTestPath, content, "utf-8");
|
|
3809
3852
|
results.push({ testFile: testFilePath, framework, scenarios: scenarios.length });
|
|
@@ -3876,7 +3919,7 @@ function generateTestFileContent(featureName, scenarios, framework) {
|
|
|
3876
3919
|
return lines.join("\n");
|
|
3877
3920
|
}
|
|
3878
3921
|
function appendMissingScenarios(testFilePath, scenarios, _framework) {
|
|
3879
|
-
const content =
|
|
3922
|
+
const content = readFileSync13(testFilePath, "utf-8");
|
|
3880
3923
|
const missing = [];
|
|
3881
3924
|
for (const scenario of scenarios) {
|
|
3882
3925
|
if (!content.includes(`Scenario: ${scenario.name}`)) {
|
|
@@ -3917,7 +3960,7 @@ var init_test_scaffolder = __esm({
|
|
|
3917
3960
|
});
|
|
3918
3961
|
|
|
3919
3962
|
// src/core/verifier/mapping-resolver.ts
|
|
3920
|
-
import { readFileSync as
|
|
3963
|
+
import { readFileSync as readFileSync15, existsSync as existsSync21, readdirSync as readdirSync2 } from "fs";
|
|
3921
3964
|
import { join as join24 } from "path";
|
|
3922
3965
|
function resolveMappings(projectRoot2, features, testOutput) {
|
|
3923
3966
|
const results = [];
|
|
@@ -3947,7 +3990,7 @@ function resolveFromMappingFile(projectRoot2, _feature, scenario) {
|
|
|
3947
3990
|
const files = findMappingFiles(mappingsDir);
|
|
3948
3991
|
for (const file of files) {
|
|
3949
3992
|
try {
|
|
3950
|
-
const content =
|
|
3993
|
+
const content = readFileSync15(file, "utf-8");
|
|
3951
3994
|
const mapping = JSON.parse(content);
|
|
3952
3995
|
if (mapping.scenarios && scenario.name in mapping.scenarios) {
|
|
3953
3996
|
const entry = mapping.scenarios[scenario.name];
|
|
@@ -3977,7 +4020,7 @@ function resolveFromTag(projectRoot2, _feature, scenario) {
|
|
|
3977
4020
|
const files = findTestFiles(testsDir);
|
|
3978
4021
|
for (const file of files) {
|
|
3979
4022
|
try {
|
|
3980
|
-
const content =
|
|
4023
|
+
const content = readFileSync15(file, "utf-8");
|
|
3981
4024
|
const tagRegex = /@speclore-scenario:\s*(.+)/g;
|
|
3982
4025
|
let match;
|
|
3983
4026
|
while ((match = tagRegex.exec(content)) !== null) {
|
|
@@ -4157,13 +4200,13 @@ var report_generator_exports = {};
|
|
|
4157
4200
|
__export(report_generator_exports, {
|
|
4158
4201
|
generateReport: () => generateReport
|
|
4159
4202
|
});
|
|
4160
|
-
import { writeFileSync as writeFileSync13, mkdirSync as
|
|
4203
|
+
import { writeFileSync as writeFileSync13, mkdirSync as mkdirSync13, existsSync as existsSync22, readFileSync as readFileSync16 } from "fs";
|
|
4161
4204
|
import { join as join25, dirname as dirname4 } from "path";
|
|
4162
4205
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
4163
4206
|
function generateReport(report, projectRoot2, config) {
|
|
4164
4207
|
const reportsDir = join25(projectRoot2, ".speclore", "reports");
|
|
4165
4208
|
if (!existsSync22(reportsDir)) {
|
|
4166
|
-
|
|
4209
|
+
mkdirSync13(reportsDir, { recursive: true });
|
|
4167
4210
|
}
|
|
4168
4211
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
4169
4212
|
const writtenFiles = [];
|
|
@@ -4220,7 +4263,7 @@ function renderHtmlReport(report) {
|
|
|
4220
4263
|
const templatePath = join25(dirname4(fileURLToPath2(import.meta.url)), "..", "..", "cli", "templates", "report.html");
|
|
4221
4264
|
let template;
|
|
4222
4265
|
try {
|
|
4223
|
-
template =
|
|
4266
|
+
template = readFileSync16(templatePath, "utf-8");
|
|
4224
4267
|
} catch {
|
|
4225
4268
|
template = getInlineTemplate();
|
|
4226
4269
|
}
|
|
@@ -4299,7 +4342,7 @@ var init_verifier = __esm({
|
|
|
4299
4342
|
});
|
|
4300
4343
|
|
|
4301
4344
|
// src/core/analyzer/rdg-builder.ts
|
|
4302
|
-
import { readFileSync as
|
|
4345
|
+
import { readFileSync as readFileSync17, existsSync as existsSync23, readdirSync as readdirSync3 } from "fs";
|
|
4303
4346
|
import { join as join26 } from "path";
|
|
4304
4347
|
var init_rdg_builder = __esm({
|
|
4305
4348
|
"src/core/analyzer/rdg-builder.ts"() {
|
|
@@ -4329,6 +4372,9 @@ import { execFileSync as execFileSync3 } from "child_process";
|
|
|
4329
4372
|
import { globSync as globSync5 } from "glob";
|
|
4330
4373
|
function analyzeImpact(projectRoot2, context, config) {
|
|
4331
4374
|
const changedFiles = getChangedFiles(projectRoot2);
|
|
4375
|
+
return analyzeImpactWithChanges(changedFiles, context, config, projectRoot2);
|
|
4376
|
+
}
|
|
4377
|
+
function analyzeImpactWithChanges(changedFiles, context, config, projectRoot2) {
|
|
4332
4378
|
const affectedModules = determineAffectedModules(changedFiles, context);
|
|
4333
4379
|
const affectedFeatures = determineAffectedFeatures(affectedModules, projectRoot2, config.spec.outputDir);
|
|
4334
4380
|
logger.info(
|
|
@@ -4404,11 +4450,11 @@ var init_analyzer = __esm({
|
|
|
4404
4450
|
});
|
|
4405
4451
|
|
|
4406
4452
|
// src/infra/file-lock.ts
|
|
4407
|
-
import { readFileSync as
|
|
4408
|
-
import { join as
|
|
4453
|
+
import { readFileSync as readFileSync21, writeFileSync as writeFileSync16, unlinkSync, existsSync as existsSync28, statSync as statSync3 } from "fs";
|
|
4454
|
+
import { join as join31 } from "path";
|
|
4409
4455
|
function acquireLock(specLoreDir) {
|
|
4410
|
-
const lockPath =
|
|
4411
|
-
if (
|
|
4456
|
+
const lockPath = join31(specLoreDir, LOCK_FILENAME);
|
|
4457
|
+
if (existsSync28(lockPath)) {
|
|
4412
4458
|
const existing = readLockInfo(lockPath);
|
|
4413
4459
|
if (existing && !isLockExpired(existing)) {
|
|
4414
4460
|
logger.debug(`Lock already held by PID ${existing.pid}, not expired`);
|
|
@@ -4422,7 +4468,7 @@ function acquireLock(specLoreDir) {
|
|
|
4422
4468
|
timestamp: Date.now()
|
|
4423
4469
|
};
|
|
4424
4470
|
try {
|
|
4425
|
-
|
|
4471
|
+
writeFileSync16(lockPath, JSON.stringify(lockInfo), { flag: "wx", encoding: "utf-8" });
|
|
4426
4472
|
} catch {
|
|
4427
4473
|
logger.debug("Lock write failed \u2014 another process may have acquired it");
|
|
4428
4474
|
return false;
|
|
@@ -4431,8 +4477,8 @@ function acquireLock(specLoreDir) {
|
|
|
4431
4477
|
return true;
|
|
4432
4478
|
}
|
|
4433
4479
|
function releaseLock(specLoreDir) {
|
|
4434
|
-
const lockPath =
|
|
4435
|
-
if (!
|
|
4480
|
+
const lockPath = join31(specLoreDir, LOCK_FILENAME);
|
|
4481
|
+
if (!existsSync28(lockPath)) {
|
|
4436
4482
|
return;
|
|
4437
4483
|
}
|
|
4438
4484
|
const existing = readLockInfo(lockPath);
|
|
@@ -4443,7 +4489,7 @@ function releaseLock(specLoreDir) {
|
|
|
4443
4489
|
}
|
|
4444
4490
|
function readLockInfo(lockPath) {
|
|
4445
4491
|
try {
|
|
4446
|
-
const content =
|
|
4492
|
+
const content = readFileSync21(lockPath, "utf-8");
|
|
4447
4493
|
const parsed = JSON.parse(content);
|
|
4448
4494
|
if (typeof parsed.pid === "number" && typeof parsed.timestamp === "number") {
|
|
4449
4495
|
return parsed;
|
|
@@ -4513,8 +4559,8 @@ var init_schemas = __esm({
|
|
|
4513
4559
|
});
|
|
4514
4560
|
|
|
4515
4561
|
// src/mcp/tools.ts
|
|
4516
|
-
import { join as
|
|
4517
|
-
import { readFileSync as
|
|
4562
|
+
import { join as join32 } from "path";
|
|
4563
|
+
import { readFileSync as readFileSync22, existsSync as existsSync29, mkdirSync as mkdirSync14, writeFileSync as writeFileSync17 } from "fs";
|
|
4518
4564
|
import { globSync as globSync7 } from "glob";
|
|
4519
4565
|
import { toJSONSchema } from "zod";
|
|
4520
4566
|
async function executeSpecTool(args, projectRoot2) {
|
|
@@ -4525,7 +4571,7 @@ async function executeSpecTool(args, projectRoot2) {
|
|
|
4525
4571
|
const config = loadConfig(projectRoot2);
|
|
4526
4572
|
logger.info(`Reading requirement from: ${args.source.slice(0, 80)}...`);
|
|
4527
4573
|
const req = await readRequirement(args.source);
|
|
4528
|
-
const specLoreDir =
|
|
4574
|
+
const specLoreDir = join32(projectRoot2, ".speclore");
|
|
4529
4575
|
const context = loadContext(specLoreDir) ?? buildContext(projectRoot2, config);
|
|
4530
4576
|
const feature = await generateFeature(req, context, config, projectRoot2);
|
|
4531
4577
|
const createdFiles = [feature.path];
|
|
@@ -4557,7 +4603,7 @@ async function executeCodeTool(args, projectRoot2) {
|
|
|
4557
4603
|
}
|
|
4558
4604
|
ensureProjectReady(projectRoot2);
|
|
4559
4605
|
const config = loadConfig(projectRoot2);
|
|
4560
|
-
const specLoreDir =
|
|
4606
|
+
const specLoreDir = join32(projectRoot2, ".speclore");
|
|
4561
4607
|
const context = loadContext(specLoreDir) ?? buildContext(projectRoot2, config);
|
|
4562
4608
|
const featureFiles = resolveFeatureFiles(args.features, projectRoot2, config);
|
|
4563
4609
|
if (featureFiles.length === 0) {
|
|
@@ -4610,7 +4656,7 @@ async function executeCodeTool(args, projectRoot2) {
|
|
|
4610
4656
|
async function executeVerifyTool(args, projectRoot2) {
|
|
4611
4657
|
ensureProjectReady(projectRoot2);
|
|
4612
4658
|
const config = loadConfig(projectRoot2);
|
|
4613
|
-
const specLoreDir =
|
|
4659
|
+
const specLoreDir = join32(projectRoot2, ".speclore");
|
|
4614
4660
|
const context = loadContext(specLoreDir) ?? buildContext(projectRoot2, config);
|
|
4615
4661
|
const featureFiles = resolveFeatureFiles(args.features, projectRoot2, config);
|
|
4616
4662
|
const stateManager = new StateManager(projectRoot2);
|
|
@@ -4672,22 +4718,22 @@ async function executeVerifyTool(args, projectRoot2) {
|
|
|
4672
4718
|
};
|
|
4673
4719
|
}
|
|
4674
4720
|
function ensureProjectReady(projectRoot2) {
|
|
4675
|
-
const specLoreDir =
|
|
4676
|
-
const configPath =
|
|
4721
|
+
const specLoreDir = join32(projectRoot2, ".speclore");
|
|
4722
|
+
const configPath = join32(specLoreDir, "config.yaml");
|
|
4677
4723
|
let configCreated = false;
|
|
4678
|
-
if (!
|
|
4679
|
-
|
|
4724
|
+
if (!existsSync29(specLoreDir)) {
|
|
4725
|
+
mkdirSync14(specLoreDir, { recursive: true });
|
|
4680
4726
|
}
|
|
4681
|
-
if (!
|
|
4727
|
+
if (!existsSync29(configPath)) {
|
|
4682
4728
|
const projectName = projectRoot2.split(/[/\\]/).pop() ?? "my-project";
|
|
4683
|
-
|
|
4729
|
+
writeFileSync17(configPath, generateDefaultConfigYaml(projectName), "utf-8");
|
|
4684
4730
|
configCreated = true;
|
|
4685
4731
|
logger.info(`Auto-created default config: ${configPath}`);
|
|
4686
4732
|
}
|
|
4687
4733
|
const stateManager = new StateManager(projectRoot2);
|
|
4688
4734
|
stateManager.ensureInitialized();
|
|
4689
4735
|
const config = loadConfig(projectRoot2);
|
|
4690
|
-
const specsDir =
|
|
4736
|
+
const specsDir = join32(projectRoot2, config.spec.outputDir);
|
|
4691
4737
|
const migrated = stateManager.migrateFeatures(specsDir);
|
|
4692
4738
|
if (migrated > 0) {
|
|
4693
4739
|
logger.info(`Migrated ${migrated} existing .feature file(s) into state tracking.`);
|
|
@@ -4724,14 +4770,14 @@ function resolveNextStep(currentState, _summary) {
|
|
|
4724
4770
|
}
|
|
4725
4771
|
}
|
|
4726
4772
|
function resolveFeatureFiles(patterns, projectRoot2, config) {
|
|
4727
|
-
const specsDir =
|
|
4773
|
+
const specsDir = join32(projectRoot2, config.spec.outputDir);
|
|
4728
4774
|
const searchPatterns = patterns && patterns.length > 0 ? patterns : [`${specsDir}/**/*.feature`];
|
|
4729
4775
|
const files = [];
|
|
4730
4776
|
for (const pattern of searchPatterns) {
|
|
4731
4777
|
const matches = globSync7(pattern, { cwd: projectRoot2, absolute: true });
|
|
4732
4778
|
for (const filePath of matches) {
|
|
4733
|
-
if (
|
|
4734
|
-
const content =
|
|
4779
|
+
if (existsSync29(filePath)) {
|
|
4780
|
+
const content = readFileSync22(filePath, "utf-8");
|
|
4735
4781
|
files.push(parseFeatureFile(filePath, content));
|
|
4736
4782
|
}
|
|
4737
4783
|
}
|
|
@@ -4839,8 +4885,8 @@ var init_tools = __esm({
|
|
|
4839
4885
|
});
|
|
4840
4886
|
|
|
4841
4887
|
// src/mcp/status.ts
|
|
4842
|
-
import { readFileSync as
|
|
4843
|
-
import { join as
|
|
4888
|
+
import { readFileSync as readFileSync23, existsSync as existsSync30, mkdirSync as mkdirSync15, writeFileSync as writeFileSync18 } from "fs";
|
|
4889
|
+
import { join as join33 } from "path";
|
|
4844
4890
|
import { globSync as globSync8 } from "glob";
|
|
4845
4891
|
function executeStatusTool(args, projectRoot2) {
|
|
4846
4892
|
const { configCreated } = ensureProjectReadyForStatus(projectRoot2);
|
|
@@ -4856,9 +4902,9 @@ function executeStatusTool(args, projectRoot2) {
|
|
|
4856
4902
|
continue;
|
|
4857
4903
|
}
|
|
4858
4904
|
let scenarioCount = 0;
|
|
4859
|
-
if (
|
|
4905
|
+
if (existsSync30(path)) {
|
|
4860
4906
|
try {
|
|
4861
|
-
const content =
|
|
4907
|
+
const content = readFileSync23(path, "utf-8");
|
|
4862
4908
|
const matches = content.match(/Scenario(?: Outline)?:/g);
|
|
4863
4909
|
scenarioCount = matches?.length ?? 0;
|
|
4864
4910
|
} catch {
|
|
@@ -4873,7 +4919,7 @@ function executeStatusTool(args, projectRoot2) {
|
|
|
4873
4919
|
lastVerify: entry.lastVerify ? { passed: entry.lastVerify.passed, failed: entry.lastVerify.failed, timestamp: entry.lastVerify.timestamp } : void 0
|
|
4874
4920
|
});
|
|
4875
4921
|
}
|
|
4876
|
-
const specsDir =
|
|
4922
|
+
const specsDir = join33(projectRoot2, config.spec.outputDir);
|
|
4877
4923
|
const allFeatureFiles = globSync8(`${specsDir}/**/*.feature`, { cwd: projectRoot2, absolute: true });
|
|
4878
4924
|
const trackedPaths = new Set(featureEntries.map((f) => f.path));
|
|
4879
4925
|
for (const filePath of allFeatureFiles) {
|
|
@@ -4881,7 +4927,7 @@ function executeStatusTool(args, projectRoot2) {
|
|
|
4881
4927
|
if (args.feature && !filePath.includes(args.feature)) continue;
|
|
4882
4928
|
let scenarioCount = 0;
|
|
4883
4929
|
try {
|
|
4884
|
-
const content =
|
|
4930
|
+
const content = readFileSync23(filePath, "utf-8");
|
|
4885
4931
|
const matches = content.match(/Scenario(?: Outline)?:/g);
|
|
4886
4932
|
scenarioCount = matches?.length ?? 0;
|
|
4887
4933
|
} catch {
|
|
@@ -4938,22 +4984,22 @@ function buildRecommendedActions(features, summary, testCommand) {
|
|
|
4938
4984
|
return actions;
|
|
4939
4985
|
}
|
|
4940
4986
|
function ensureProjectReadyForStatus(projectRoot2) {
|
|
4941
|
-
const specLoreDir =
|
|
4942
|
-
const configPath =
|
|
4987
|
+
const specLoreDir = join33(projectRoot2, ".speclore");
|
|
4988
|
+
const configPath = join33(specLoreDir, "config.yaml");
|
|
4943
4989
|
let configCreated = false;
|
|
4944
|
-
if (!
|
|
4945
|
-
|
|
4990
|
+
if (!existsSync30(specLoreDir)) {
|
|
4991
|
+
mkdirSync15(specLoreDir, { recursive: true });
|
|
4946
4992
|
}
|
|
4947
|
-
if (!
|
|
4993
|
+
if (!existsSync30(configPath)) {
|
|
4948
4994
|
const projectName = projectRoot2.split(/[/\\]/).pop() ?? "my-project";
|
|
4949
|
-
|
|
4995
|
+
writeFileSync18(configPath, generateDefaultConfigYaml(projectName), "utf-8");
|
|
4950
4996
|
configCreated = true;
|
|
4951
4997
|
logger.info(`Auto-created default config: ${configPath}`);
|
|
4952
4998
|
}
|
|
4953
4999
|
const stateManager = new StateManager(projectRoot2);
|
|
4954
5000
|
stateManager.ensureInitialized();
|
|
4955
5001
|
const config = loadConfig(projectRoot2);
|
|
4956
|
-
const specsDir =
|
|
5002
|
+
const specsDir = join33(projectRoot2, config.spec.outputDir);
|
|
4957
5003
|
const migrated = stateManager.migrateFeatures(specsDir);
|
|
4958
5004
|
if (migrated > 0) {
|
|
4959
5005
|
logger.info(`Migrated ${migrated} existing .feature file(s) into state tracking.`);
|
|
@@ -4977,7 +5023,7 @@ __export(server_exports, {
|
|
|
4977
5023
|
});
|
|
4978
5024
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4979
5025
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4980
|
-
import { mkdirSync as
|
|
5026
|
+
import { mkdirSync as mkdirSync16 } from "fs";
|
|
4981
5027
|
function getProjectRoot() {
|
|
4982
5028
|
return projectRoot;
|
|
4983
5029
|
}
|
|
@@ -4985,7 +5031,7 @@ async function startMcpServer() {
|
|
|
4985
5031
|
projectRoot = process.env.SPECLORE_PROJECT_ROOT ?? process.cwd();
|
|
4986
5032
|
const specLoreDir = `${projectRoot}/.speclore`;
|
|
4987
5033
|
try {
|
|
4988
|
-
|
|
5034
|
+
mkdirSync16(specLoreDir, { recursive: true });
|
|
4989
5035
|
const locked = acquireLock(specLoreDir);
|
|
4990
5036
|
if (!locked) {
|
|
4991
5037
|
logger.warn("Could not acquire lock \u2014 another SpecLore instance may be running.");
|
|
@@ -5098,11 +5144,11 @@ import { Command } from "commander";
|
|
|
5098
5144
|
init_logger();
|
|
5099
5145
|
init_config2();
|
|
5100
5146
|
init_detector();
|
|
5101
|
-
import { existsSync as existsSync5, mkdirSync as
|
|
5147
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
5102
5148
|
import { join as join7 } from "path";
|
|
5103
5149
|
|
|
5104
5150
|
// src/setup/config-writer.ts
|
|
5105
|
-
import { writeFileSync as writeFileSync2, existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
|
|
5151
|
+
import { writeFileSync as writeFileSync2, existsSync as existsSync4, readFileSync as readFileSync4, mkdirSync as mkdirSync2 } from "fs";
|
|
5106
5152
|
import { join as join5 } from "path";
|
|
5107
5153
|
|
|
5108
5154
|
// src/infra/install-detector.ts
|
|
@@ -5153,13 +5199,42 @@ function detectCloneInstall(projectRoot2) {
|
|
|
5153
5199
|
|
|
5154
5200
|
// src/setup/config-writer.ts
|
|
5155
5201
|
init_logger();
|
|
5156
|
-
function writeMcpConfig(projectRoot2, _globalMode) {
|
|
5202
|
+
function writeMcpConfig(projectRoot2, tools, _globalMode) {
|
|
5203
|
+
const installInfo = detectInstallMethod(projectRoot2);
|
|
5204
|
+
const serverCommand = getServerCommand(installInfo);
|
|
5205
|
+
const detectedToolNames = tools.map((t) => t.tool);
|
|
5206
|
+
if (detectedToolNames.includes("cursor")) {
|
|
5207
|
+
writeCursorMcp(projectRoot2, serverCommand);
|
|
5208
|
+
}
|
|
5209
|
+
if (detectedToolNames.includes("claude")) {
|
|
5210
|
+
writeClaudeMcp(projectRoot2, serverCommand);
|
|
5211
|
+
}
|
|
5212
|
+
if (detectedToolNames.includes("qoder")) {
|
|
5213
|
+
writeQoderMcp(projectRoot2, serverCommand);
|
|
5214
|
+
}
|
|
5215
|
+
if (detectedToolNames.length > 0) {
|
|
5216
|
+
logger.info(`MCP configuration written for: ${detectedToolNames.join(", ")}`);
|
|
5217
|
+
}
|
|
5218
|
+
}
|
|
5219
|
+
function writeMcpForClient(projectRoot2, client) {
|
|
5157
5220
|
const installInfo = detectInstallMethod(projectRoot2);
|
|
5158
5221
|
const serverCommand = getServerCommand(installInfo);
|
|
5159
|
-
|
|
5160
|
-
|
|
5161
|
-
|
|
5162
|
-
|
|
5222
|
+
switch (client) {
|
|
5223
|
+
case "cursor":
|
|
5224
|
+
mkdirSync2(join5(projectRoot2, ".cursor"), { recursive: true });
|
|
5225
|
+
writeCursorMcp(projectRoot2, serverCommand);
|
|
5226
|
+
break;
|
|
5227
|
+
case "claude":
|
|
5228
|
+
if (!existsSync4(join5(projectRoot2, ".claude"))) {
|
|
5229
|
+
mkdirSync2(join5(projectRoot2, ".claude"), { recursive: true });
|
|
5230
|
+
}
|
|
5231
|
+
writeClaudeMcp(projectRoot2, serverCommand);
|
|
5232
|
+
break;
|
|
5233
|
+
case "qoder":
|
|
5234
|
+
mkdirSync2(join5(projectRoot2, ".qoder"), { recursive: true });
|
|
5235
|
+
writeQoderMcp(projectRoot2, serverCommand);
|
|
5236
|
+
break;
|
|
5237
|
+
}
|
|
5163
5238
|
}
|
|
5164
5239
|
function getServerCommand(installInfo) {
|
|
5165
5240
|
if (installInfo.mode === "npm") {
|
|
@@ -5183,6 +5258,9 @@ function writeCursorMcp(projectRoot2, serverCmd) {
|
|
|
5183
5258
|
logger.info(` Cursor: ${mcpPath}`);
|
|
5184
5259
|
}
|
|
5185
5260
|
function writeClaudeMcp(projectRoot2, serverCmd) {
|
|
5261
|
+
const hasClaudeDir = existsSync4(join5(projectRoot2, ".claude"));
|
|
5262
|
+
const hasClaudeMd = existsSync4(join5(projectRoot2, "CLAUDE.md"));
|
|
5263
|
+
if (!hasClaudeDir && !hasClaudeMd) return;
|
|
5186
5264
|
const mcpPath = join5(projectRoot2, ".mcp.json");
|
|
5187
5265
|
const existing = readExistingJson(mcpPath);
|
|
5188
5266
|
if (!existing.mcpServers) existing.mcpServers = {};
|
|
@@ -5221,7 +5299,7 @@ function readExistingJson(filePath) {
|
|
|
5221
5299
|
|
|
5222
5300
|
// src/setup/rule-writer.ts
|
|
5223
5301
|
init_logger();
|
|
5224
|
-
import { writeFileSync as writeFileSync3, mkdirSync as
|
|
5302
|
+
import { writeFileSync as writeFileSync3, mkdirSync as mkdirSync3 } from "fs";
|
|
5225
5303
|
import { join as join6 } from "path";
|
|
5226
5304
|
function writeRuleFiles(projectRoot2, tools) {
|
|
5227
5305
|
for (const tool of tools) {
|
|
@@ -5240,7 +5318,7 @@ function writeRuleFiles(projectRoot2, tools) {
|
|
|
5240
5318
|
}
|
|
5241
5319
|
function writeCursorRule(projectRoot2) {
|
|
5242
5320
|
const rulesDir = join6(projectRoot2, ".cursor", "rules");
|
|
5243
|
-
|
|
5321
|
+
mkdirSync3(rulesDir, { recursive: true });
|
|
5244
5322
|
const content = `---
|
|
5245
5323
|
description: SpecLore \u2014 AI coding constraints (setup placeholder)
|
|
5246
5324
|
globs:
|
|
@@ -5262,7 +5340,7 @@ SpecLore is configured for this project. When the \`speclore.spec\` MCP tool is
|
|
|
5262
5340
|
}
|
|
5263
5341
|
function writeClaudeRule(projectRoot2) {
|
|
5264
5342
|
const rulesDir = join6(projectRoot2, ".claude", "rules");
|
|
5265
|
-
|
|
5343
|
+
mkdirSync3(rulesDir, { recursive: true });
|
|
5266
5344
|
const content = `# SpecLore
|
|
5267
5345
|
|
|
5268
5346
|
SpecLore is configured for this project. When the \`speclore.spec\` MCP tool is available:
|
|
@@ -5278,7 +5356,7 @@ SpecLore is configured for this project. When the \`speclore.spec\` MCP tool is
|
|
|
5278
5356
|
}
|
|
5279
5357
|
function writeQoderRule(projectRoot2) {
|
|
5280
5358
|
const rulesDir = join6(projectRoot2, ".qoder", "rules");
|
|
5281
|
-
|
|
5359
|
+
mkdirSync3(rulesDir, { recursive: true });
|
|
5282
5360
|
const content = `# SpecLore
|
|
5283
5361
|
|
|
5284
5362
|
SpecLore is configured for this project. When the \`speclore.spec\` MCP tool is available:
|
|
@@ -5302,13 +5380,20 @@ function registerSetupCommand(program) {
|
|
|
5302
5380
|
logger.info("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
5303
5381
|
const tools = detectAITools(projectRoot2);
|
|
5304
5382
|
if (tools.length === 0) {
|
|
5305
|
-
logger.warn("No supported AI tools detected.
|
|
5383
|
+
logger.warn("No supported AI tools detected.");
|
|
5384
|
+
logger.info("");
|
|
5385
|
+
logger.info(" To enable MCP integration, open your AI client in this project first,");
|
|
5386
|
+
logger.info(" then re-run setup. Or use manual configuration:");
|
|
5387
|
+
logger.info(" speclore mcp add cursor \u2014 configure for Cursor");
|
|
5388
|
+
logger.info(" speclore mcp add claude \u2014 configure for Claude Code");
|
|
5389
|
+
logger.info(" speclore mcp add qoder \u2014 configure for Qoder");
|
|
5390
|
+
logger.info("");
|
|
5306
5391
|
} else {
|
|
5307
5392
|
logger.info(`Detected AI tools: ${tools.map((t) => t.tool).join(", ")}`);
|
|
5308
5393
|
}
|
|
5309
5394
|
const specLoreDir = opts.global ? join7(process.env.HOME ?? process.env.USERPROFILE ?? "~", ".speclore") : join7(projectRoot2, ".speclore");
|
|
5310
5395
|
if (!existsSync5(specLoreDir)) {
|
|
5311
|
-
|
|
5396
|
+
mkdirSync4(specLoreDir, { recursive: true });
|
|
5312
5397
|
}
|
|
5313
5398
|
const configPath = join7(specLoreDir, "config.yaml");
|
|
5314
5399
|
if (!existsSync5(configPath)) {
|
|
@@ -5316,12 +5401,23 @@ function registerSetupCommand(program) {
|
|
|
5316
5401
|
writeFileSync4(configPath, generateDefaultConfigYaml(projectName), "utf-8");
|
|
5317
5402
|
logger.info(`Created config: ${configPath}`);
|
|
5318
5403
|
}
|
|
5319
|
-
writeMcpConfig(projectRoot2, opts.global ?? false);
|
|
5404
|
+
writeMcpConfig(projectRoot2, tools, opts.global ?? false);
|
|
5320
5405
|
writeRuleFiles(projectRoot2, tools);
|
|
5321
5406
|
logger.info("");
|
|
5322
|
-
logger.info("Setup complete!
|
|
5323
|
-
logger.info("
|
|
5324
|
-
logger.info("
|
|
5407
|
+
logger.info("Setup complete! Next steps:");
|
|
5408
|
+
logger.info("");
|
|
5409
|
+
logger.info(" Option A \u2014 CLI workflow (terminal users):");
|
|
5410
|
+
logger.info(' speclore spec "your requirement" \u2192 generate .feature');
|
|
5411
|
+
logger.info(" speclore code \u2192 generate constraints + tests");
|
|
5412
|
+
logger.info(" speclore verify \u2192 run acceptance");
|
|
5413
|
+
logger.info("");
|
|
5414
|
+
logger.info(" Option B \u2014 AI client workflow (recommended):");
|
|
5415
|
+
logger.info(" Open Cursor / Qoder / Claude Code and start chatting.");
|
|
5416
|
+
logger.info(" MCP is already configured \u2014 AI handles the full pipeline.");
|
|
5417
|
+
logger.info("");
|
|
5418
|
+
logger.info(" Optional \u2014 Pre-scan project context:");
|
|
5419
|
+
logger.info(" speclore init \u2192 scan modules/entities/APIs for better AI context");
|
|
5420
|
+
logger.info(" (Not required \u2014 context is auto-built on first spec/code call)");
|
|
5325
5421
|
});
|
|
5326
5422
|
}
|
|
5327
5423
|
|
|
@@ -5331,7 +5427,7 @@ init_logger();
|
|
|
5331
5427
|
init_context_engine();
|
|
5332
5428
|
import { join as join11 } from "path";
|
|
5333
5429
|
function registerInitCommand(program) {
|
|
5334
|
-
program.command("init").description("
|
|
5430
|
+
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
5431
|
if (opts.verbose) logger.setLevel("debug");
|
|
5336
5432
|
const projectRoot2 = process.cwd();
|
|
5337
5433
|
logger.info("SpecLore Init");
|
|
@@ -5395,7 +5491,7 @@ init_test_scaffolder();
|
|
|
5395
5491
|
init_context_engine();
|
|
5396
5492
|
init_state_manager();
|
|
5397
5493
|
import { join as join23 } from "path";
|
|
5398
|
-
import { readFileSync as
|
|
5494
|
+
import { readFileSync as readFileSync14, existsSync as existsSync20 } from "fs";
|
|
5399
5495
|
import { globSync as globSync4 } from "glob";
|
|
5400
5496
|
function registerCodeCommand(program) {
|
|
5401
5497
|
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 +5547,7 @@ function resolveFeatures(patterns, projectRoot2, config) {
|
|
|
5451
5547
|
const matches = globSync4(pattern, { cwd: projectRoot2, absolute: true });
|
|
5452
5548
|
for (const filePath of matches) {
|
|
5453
5549
|
if (existsSync20(filePath)) {
|
|
5454
|
-
const content =
|
|
5550
|
+
const content = readFileSync14(filePath, "utf-8");
|
|
5455
5551
|
const featureMatch = content.match(/Feature:\s*(.+)/);
|
|
5456
5552
|
files.push({
|
|
5457
5553
|
path: filePath,
|
|
@@ -5475,7 +5571,7 @@ init_context_engine();
|
|
|
5475
5571
|
init_analyzer();
|
|
5476
5572
|
init_state_manager();
|
|
5477
5573
|
import { join as join27 } from "path";
|
|
5478
|
-
import { readFileSync as
|
|
5574
|
+
import { readFileSync as readFileSync18, existsSync as existsSync24 } from "fs";
|
|
5479
5575
|
import { globSync as globSync6 } from "glob";
|
|
5480
5576
|
function registerVerifyCommand(program) {
|
|
5481
5577
|
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 +5658,7 @@ function resolveFeatures2(patterns, projectRoot2, config) {
|
|
|
5562
5658
|
const matches = globSync6(pattern, { cwd: projectRoot2, absolute: true });
|
|
5563
5659
|
for (const filePath of matches) {
|
|
5564
5660
|
if (existsSync24(filePath)) {
|
|
5565
|
-
const content =
|
|
5661
|
+
const content = readFileSync18(filePath, "utf-8");
|
|
5566
5662
|
const featureMatch = content.match(/Feature:\s*(.+)/);
|
|
5567
5663
|
files.push({
|
|
5568
5664
|
path: filePath,
|
|
@@ -5583,7 +5679,7 @@ init_logger();
|
|
|
5583
5679
|
|
|
5584
5680
|
// src/setup/cleanup.ts
|
|
5585
5681
|
init_logger();
|
|
5586
|
-
import { rmSync as rmSync4, existsSync as existsSync25, readFileSync as
|
|
5682
|
+
import { rmSync as rmSync4, existsSync as existsSync25, readFileSync as readFileSync19, writeFileSync as writeFileSync14 } from "fs";
|
|
5587
5683
|
import { join as join28 } from "path";
|
|
5588
5684
|
function runTeardown(projectRoot2, globalMode) {
|
|
5589
5685
|
if (globalMode) {
|
|
@@ -5612,7 +5708,7 @@ function runTeardown(projectRoot2, globalMode) {
|
|
|
5612
5708
|
function removeMcpEntry(mcpPath, serverName) {
|
|
5613
5709
|
if (!existsSync25(mcpPath)) return;
|
|
5614
5710
|
try {
|
|
5615
|
-
const config = JSON.parse(
|
|
5711
|
+
const config = JSON.parse(readFileSync19(mcpPath, "utf-8"));
|
|
5616
5712
|
if (config.mcpServers?.[serverName]) {
|
|
5617
5713
|
delete config.mcpServers[serverName];
|
|
5618
5714
|
writeFileSync14(mcpPath, JSON.stringify(config, null, 2), "utf-8");
|
|
@@ -5708,6 +5804,114 @@ function registerMigrateCommand(program) {
|
|
|
5708
5804
|
});
|
|
5709
5805
|
}
|
|
5710
5806
|
|
|
5807
|
+
// src/cli/commands/mcp-config.ts
|
|
5808
|
+
init_logger();
|
|
5809
|
+
import { existsSync as existsSync27, readFileSync as readFileSync20, writeFileSync as writeFileSync15, rmSync as rmSync5 } from "fs";
|
|
5810
|
+
import { join as join30 } from "path";
|
|
5811
|
+
var VALID_CLIENTS = ["cursor", "claude", "qoder"];
|
|
5812
|
+
function getMcpPath(projectRoot2, client) {
|
|
5813
|
+
switch (client) {
|
|
5814
|
+
case "cursor":
|
|
5815
|
+
return join30(projectRoot2, ".cursor", "mcp.json");
|
|
5816
|
+
case "claude":
|
|
5817
|
+
return join30(projectRoot2, ".mcp.json");
|
|
5818
|
+
case "qoder":
|
|
5819
|
+
return join30(projectRoot2, ".qoder", "mcp.json");
|
|
5820
|
+
}
|
|
5821
|
+
}
|
|
5822
|
+
function clientLabel(client) {
|
|
5823
|
+
switch (client) {
|
|
5824
|
+
case "cursor":
|
|
5825
|
+
return "Cursor";
|
|
5826
|
+
case "claude":
|
|
5827
|
+
return "Claude Code";
|
|
5828
|
+
case "qoder":
|
|
5829
|
+
return "Qoder";
|
|
5830
|
+
}
|
|
5831
|
+
}
|
|
5832
|
+
function registerMcpConfigCommands(program) {
|
|
5833
|
+
const mcpCmd = program.commands.find((c) => c.name() === "mcp");
|
|
5834
|
+
if (!mcpCmd) return;
|
|
5835
|
+
mcpCmd.command("add <client>").description("Manually write SpecLore MCP config for a specific AI client (cursor | claude | qoder)").action((clientArg) => {
|
|
5836
|
+
const projectRoot2 = process.cwd();
|
|
5837
|
+
const client = validateClient(clientArg);
|
|
5838
|
+
if (!client) return;
|
|
5839
|
+
logger.info(`Configuring MCP for ${clientLabel(client)}...`);
|
|
5840
|
+
writeMcpForClient(projectRoot2, client);
|
|
5841
|
+
logger.info(`\u2713 ${clientLabel(client)} MCP configured at ${getMcpPath(projectRoot2, client)}`);
|
|
5842
|
+
});
|
|
5843
|
+
mcpCmd.command("remove <client>").description("Remove SpecLore MCP config from a specific AI client (cursor | claude | qoder)").action((clientArg) => {
|
|
5844
|
+
const projectRoot2 = process.cwd();
|
|
5845
|
+
const client = validateClient(clientArg);
|
|
5846
|
+
if (!client) return;
|
|
5847
|
+
const mcpPath = getMcpPath(projectRoot2, client);
|
|
5848
|
+
if (!existsSync27(mcpPath)) {
|
|
5849
|
+
logger.info(`${clientLabel(client)}: no MCP config file found at ${mcpPath}`);
|
|
5850
|
+
return;
|
|
5851
|
+
}
|
|
5852
|
+
const removed = removeMcpServerEntry(mcpPath, "speclore");
|
|
5853
|
+
if (removed) {
|
|
5854
|
+
logger.info(`\u2713 Removed speclore from ${clientLabel(client)}: ${mcpPath}`);
|
|
5855
|
+
} else {
|
|
5856
|
+
logger.info(`${clientLabel(client)}: speclore entry not found in ${mcpPath}`);
|
|
5857
|
+
}
|
|
5858
|
+
});
|
|
5859
|
+
mcpCmd.command("list").description("Show current SpecLore MCP configuration status for all clients").action(() => {
|
|
5860
|
+
const projectRoot2 = process.cwd();
|
|
5861
|
+
logger.info("SpecLore MCP Configuration Status");
|
|
5862
|
+
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");
|
|
5863
|
+
let anyFound = false;
|
|
5864
|
+
for (const client of VALID_CLIENTS) {
|
|
5865
|
+
const mcpPath = getMcpPath(projectRoot2, client);
|
|
5866
|
+
if (!existsSync27(mcpPath)) {
|
|
5867
|
+
logger.info(` \u25CB ${clientLabel(client)}: no config file (${mcpPath})`);
|
|
5868
|
+
continue;
|
|
5869
|
+
}
|
|
5870
|
+
try {
|
|
5871
|
+
const config = JSON.parse(readFileSync20(mcpPath, "utf-8"));
|
|
5872
|
+
if (config.mcpServers?.["speclore"]) {
|
|
5873
|
+
logger.info(` \u2713 ${clientLabel(client)}: configured \u2192 ${mcpPath}`);
|
|
5874
|
+
anyFound = true;
|
|
5875
|
+
} else {
|
|
5876
|
+
logger.info(` \u25CB ${clientLabel(client)}: config file exists but speclore not registered \u2192 ${mcpPath}`);
|
|
5877
|
+
}
|
|
5878
|
+
} catch {
|
|
5879
|
+
logger.info(` \u2717 ${clientLabel(client)}: failed to parse ${mcpPath}`);
|
|
5880
|
+
}
|
|
5881
|
+
}
|
|
5882
|
+
if (!anyFound) {
|
|
5883
|
+
logger.info("");
|
|
5884
|
+
logger.info(" No SpecLore MCP configurations found.");
|
|
5885
|
+
logger.info(" Run `speclore setup` or `speclore mcp add <client>` to configure.");
|
|
5886
|
+
}
|
|
5887
|
+
});
|
|
5888
|
+
}
|
|
5889
|
+
function validateClient(arg) {
|
|
5890
|
+
const normalised = arg.toLowerCase().trim();
|
|
5891
|
+
if (!VALID_CLIENTS.includes(normalised)) {
|
|
5892
|
+
logger.error(`Unknown client "${arg}". Supported: ${VALID_CLIENTS.join(", ")}`);
|
|
5893
|
+
return null;
|
|
5894
|
+
}
|
|
5895
|
+
return normalised;
|
|
5896
|
+
}
|
|
5897
|
+
function removeMcpServerEntry(mcpPath, serverName) {
|
|
5898
|
+
if (!existsSync27(mcpPath)) return false;
|
|
5899
|
+
try {
|
|
5900
|
+
const config = JSON.parse(readFileSync20(mcpPath, "utf-8"));
|
|
5901
|
+
if (!config.mcpServers?.[serverName]) return false;
|
|
5902
|
+
delete config.mcpServers[serverName];
|
|
5903
|
+
const remainingServers = Object.keys(config.mcpServers ?? {});
|
|
5904
|
+
if (remainingServers.length === 0) {
|
|
5905
|
+
rmSync5(mcpPath);
|
|
5906
|
+
} else {
|
|
5907
|
+
writeFileSync15(mcpPath, JSON.stringify(config, null, 2), "utf-8");
|
|
5908
|
+
}
|
|
5909
|
+
return true;
|
|
5910
|
+
} catch {
|
|
5911
|
+
return false;
|
|
5912
|
+
}
|
|
5913
|
+
}
|
|
5914
|
+
|
|
5711
5915
|
// src/cli/index.ts
|
|
5712
5916
|
init_version();
|
|
5713
5917
|
function createProgram() {
|
|
@@ -5727,6 +5931,7 @@ function createProgram() {
|
|
|
5727
5931
|
const { startMcpServer: startMcpServer2 } = await Promise.resolve().then(() => (init_server(), server_exports));
|
|
5728
5932
|
await startMcpServer2();
|
|
5729
5933
|
});
|
|
5934
|
+
registerMcpConfigCommands(program);
|
|
5730
5935
|
program.argument("[text...]", "Quick requirement text \u2192 .feature").action(async (text) => {
|
|
5731
5936
|
if (text.length > 0) {
|
|
5732
5937
|
const { loadConfig: loadConfig2 } = await Promise.resolve().then(() => (init_config2(), config_exports));
|