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