speclore 0.1.4 → 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 +416 -201
- package/dist/cli/index.js.map +1 -1
- package/dist/index.js +416 -201
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +197 -141
- package/dist/mcp/server.js.map +1 -1
- package/package.json +14 -6
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
|
}
|
|
@@ -1413,6 +1408,13 @@ function formatCellValue(value) {
|
|
|
1413
1408
|
if (typeof value === "string") return value;
|
|
1414
1409
|
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
1415
1410
|
if (value instanceof Date) return value.toISOString();
|
|
1411
|
+
if (typeof value === "object" && value !== null && "v" in value) {
|
|
1412
|
+
const cell = value;
|
|
1413
|
+
if (cell.w != null) return cell.w;
|
|
1414
|
+
if (cell.v == null) return "";
|
|
1415
|
+
if (typeof cell.v === "string" || typeof cell.v === "number" || typeof cell.v === "boolean") return String(cell.v);
|
|
1416
|
+
return JSON.stringify(cell.v);
|
|
1417
|
+
}
|
|
1416
1418
|
return "";
|
|
1417
1419
|
}
|
|
1418
1420
|
var init_excel_utils = __esm({
|
|
@@ -1422,7 +1424,43 @@ var init_excel_utils = __esm({
|
|
|
1422
1424
|
});
|
|
1423
1425
|
|
|
1424
1426
|
// src/plugins/builtin/xlsx-reader.ts
|
|
1425
|
-
import
|
|
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
|
+
}
|
|
1426
1464
|
var XlsxReader;
|
|
1427
1465
|
var init_xlsx_reader = __esm({
|
|
1428
1466
|
"src/plugins/builtin/xlsx-reader.ts"() {
|
|
@@ -1434,46 +1472,9 @@ var init_xlsx_reader = __esm({
|
|
|
1434
1472
|
canRead(source) {
|
|
1435
1473
|
return /\.xlsx$/i.test(source) || /\.xls$/i.test(source);
|
|
1436
1474
|
}
|
|
1437
|
-
|
|
1438
|
-
const workbook =
|
|
1439
|
-
|
|
1440
|
-
const requirements = [];
|
|
1441
|
-
for (const sheet of workbook.worksheets) {
|
|
1442
|
-
const sheetName = sheet.name;
|
|
1443
|
-
const headers = [];
|
|
1444
|
-
const rows = [];
|
|
1445
|
-
sheet.eachRow((row, rowNumber) => {
|
|
1446
|
-
if (rowNumber === 1) {
|
|
1447
|
-
row.eachCell((cell, colNumber) => {
|
|
1448
|
-
headers[colNumber - 1] = formatCellValue(cell.value);
|
|
1449
|
-
});
|
|
1450
|
-
} else {
|
|
1451
|
-
const obj = {};
|
|
1452
|
-
row.eachCell((cell, colNumber) => {
|
|
1453
|
-
const key = headers[colNumber - 1] ?? `Col${colNumber}`;
|
|
1454
|
-
obj[key] = formatCellValue(cell.value);
|
|
1455
|
-
});
|
|
1456
|
-
rows.push(obj);
|
|
1457
|
-
}
|
|
1458
|
-
});
|
|
1459
|
-
for (let i = 0; i < rows.length; i++) {
|
|
1460
|
-
const row = rows[i];
|
|
1461
|
-
const title = row["Title"] ?? row["title"] ?? row["Name"] ?? row["name"] ?? row["ID"] ?? `Row ${i + 1}`;
|
|
1462
|
-
const description = row["Description"] ?? row["description"] ?? row["Desc"] ?? "";
|
|
1463
|
-
const ac = row["Acceptance Criteria"] ?? row["acceptance"] ?? row["AC"] ?? "";
|
|
1464
|
-
if (description || title) {
|
|
1465
|
-
requirements.push({
|
|
1466
|
-
id: `${sheetName}/${i + 1}`,
|
|
1467
|
-
title: String(title),
|
|
1468
|
-
description: String(description),
|
|
1469
|
-
acceptanceCriteria: ac ? String(ac).split("\n").filter(Boolean) : void 0,
|
|
1470
|
-
rawContent: JSON.stringify(row),
|
|
1471
|
-
confidence: 0.7
|
|
1472
|
-
});
|
|
1473
|
-
}
|
|
1474
|
-
}
|
|
1475
|
-
}
|
|
1476
|
-
return requirements;
|
|
1475
|
+
read(source) {
|
|
1476
|
+
const workbook = readFile(source);
|
|
1477
|
+
return Promise.resolve(parseWorkbook(workbook));
|
|
1477
1478
|
}
|
|
1478
1479
|
};
|
|
1479
1480
|
}
|
|
@@ -1491,21 +1492,35 @@ var init_pdf_reader = __esm({
|
|
|
1491
1492
|
return /\.pdf$/i.test(source);
|
|
1492
1493
|
}
|
|
1493
1494
|
async read(source) {
|
|
1494
|
-
let
|
|
1495
|
+
let pdfjsLib;
|
|
1495
1496
|
try {
|
|
1496
|
-
|
|
1497
|
-
pdfParse = mod.default ?? mod;
|
|
1497
|
+
pdfjsLib = await import("pdfjs-dist/legacy/build/pdf.mjs");
|
|
1498
1498
|
} catch {
|
|
1499
|
-
throw new Error("
|
|
1499
|
+
throw new Error("pdfjs-dist package is required for PDF support. Install: npm i pdfjs-dist");
|
|
1500
1500
|
}
|
|
1501
|
-
const { readFileSync:
|
|
1502
|
-
const buffer =
|
|
1503
|
-
const
|
|
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);
|
|
1516
|
+
}
|
|
1517
|
+
doc.destroy();
|
|
1518
|
+
const text = textParts.join("\n");
|
|
1504
1519
|
return [{
|
|
1505
1520
|
id: source.replace(/\\/g, "/").replace(/\.pdf$/i, ""),
|
|
1506
1521
|
title: source.split("/").pop()?.replace(/\.pdf$/i, "") ?? "Untitled",
|
|
1507
|
-
description:
|
|
1508
|
-
rawContent:
|
|
1522
|
+
description: text,
|
|
1523
|
+
rawContent: text,
|
|
1509
1524
|
confidence: 0.75
|
|
1510
1525
|
}];
|
|
1511
1526
|
}
|
|
@@ -2101,13 +2116,13 @@ var init_image_reader = __esm({
|
|
|
2101
2116
|
return /\.(png|jpe?g|webp)$/i.test(source);
|
|
2102
2117
|
}
|
|
2103
2118
|
async read(source) {
|
|
2104
|
-
const { existsSync:
|
|
2105
|
-
if (!
|
|
2119
|
+
const { existsSync: existsSync31, readFileSync: readFileSync24 } = await import("fs");
|
|
2120
|
+
if (!existsSync31(source)) {
|
|
2106
2121
|
throw new Error(`Image file not found: ${source}`);
|
|
2107
2122
|
}
|
|
2108
2123
|
const ext = source.split(".").pop()?.toLowerCase() ?? "png";
|
|
2109
2124
|
const mimeType = ext === "jpg" ? "image/jpeg" : `image/${ext}`;
|
|
2110
|
-
const buffer =
|
|
2125
|
+
const buffer = readFileSync24(source);
|
|
2111
2126
|
let text;
|
|
2112
2127
|
try {
|
|
2113
2128
|
const { createProvider: createProvider2 } = await Promise.resolve().then(() => (init_provider(), provider_exports));
|
|
@@ -2136,7 +2151,7 @@ var init_image_reader = __esm({
|
|
|
2136
2151
|
});
|
|
2137
2152
|
|
|
2138
2153
|
// src/plugins/builtin/cursor-writer.ts
|
|
2139
|
-
import { writeFileSync as writeFileSync7, mkdirSync as
|
|
2154
|
+
import { writeFileSync as writeFileSync7, mkdirSync as mkdirSync7, existsSync as existsSync11, rmSync } from "fs";
|
|
2140
2155
|
import { join as join14 } from "path";
|
|
2141
2156
|
var CursorWriter;
|
|
2142
2157
|
var init_cursor_writer = __esm({
|
|
@@ -2152,7 +2167,7 @@ var init_cursor_writer = __esm({
|
|
|
2152
2167
|
write(constraints) {
|
|
2153
2168
|
this.projectRoot = constraints.projectRoot;
|
|
2154
2169
|
const rulesDir = join14(constraints.projectRoot, ".cursor", "rules");
|
|
2155
|
-
|
|
2170
|
+
mkdirSync7(rulesDir, { recursive: true });
|
|
2156
2171
|
const frontmatter = [
|
|
2157
2172
|
"---",
|
|
2158
2173
|
"description: SpecLore coding constraints \u2014 auto-generated",
|
|
@@ -2231,7 +2246,7 @@ var init_cursor_writer = __esm({
|
|
|
2231
2246
|
});
|
|
2232
2247
|
|
|
2233
2248
|
// src/plugins/builtin/claude-writer.ts
|
|
2234
|
-
import { writeFileSync as writeFileSync8, mkdirSync as
|
|
2249
|
+
import { writeFileSync as writeFileSync8, mkdirSync as mkdirSync8, existsSync as existsSync12, rmSync as rmSync2 } from "fs";
|
|
2235
2250
|
import { join as join15 } from "path";
|
|
2236
2251
|
var ClaudeWriter;
|
|
2237
2252
|
var init_claude_writer = __esm({
|
|
@@ -2247,7 +2262,7 @@ var init_claude_writer = __esm({
|
|
|
2247
2262
|
write(constraints) {
|
|
2248
2263
|
this.projectRoot = constraints.projectRoot;
|
|
2249
2264
|
const rulesDir = join15(constraints.projectRoot, ".claude", "rules");
|
|
2250
|
-
|
|
2265
|
+
mkdirSync8(rulesDir, { recursive: true });
|
|
2251
2266
|
const content = this.buildMarkdown(constraints);
|
|
2252
2267
|
writeFileSync8(join15(rulesDir, "speclore.md"), content, "utf-8");
|
|
2253
2268
|
return Promise.resolve();
|
|
@@ -2313,7 +2328,7 @@ var init_claude_writer = __esm({
|
|
|
2313
2328
|
});
|
|
2314
2329
|
|
|
2315
2330
|
// src/plugins/builtin/qoder-writer.ts
|
|
2316
|
-
import { writeFileSync as writeFileSync9, mkdirSync as
|
|
2331
|
+
import { writeFileSync as writeFileSync9, mkdirSync as mkdirSync9, existsSync as existsSync13, rmSync as rmSync3 } from "fs";
|
|
2317
2332
|
import { join as join16 } from "path";
|
|
2318
2333
|
var QoderWriter;
|
|
2319
2334
|
var init_qoder_writer = __esm({
|
|
@@ -2329,7 +2344,7 @@ var init_qoder_writer = __esm({
|
|
|
2329
2344
|
write(constraints) {
|
|
2330
2345
|
this.projectRoot = constraints.projectRoot;
|
|
2331
2346
|
const rulesDir = join16(constraints.projectRoot, ".qoder", "rules");
|
|
2332
|
-
|
|
2347
|
+
mkdirSync9(rulesDir, { recursive: true });
|
|
2333
2348
|
const content = this.buildMarkdown(constraints);
|
|
2334
2349
|
writeFileSync9(join16(rulesDir, "speclore.md"), content, "utf-8");
|
|
2335
2350
|
return Promise.resolve();
|
|
@@ -2762,11 +2777,12 @@ var init_markdown_reader = __esm({
|
|
|
2762
2777
|
|
|
2763
2778
|
// src/core/requirement-reader/docx-reader.ts
|
|
2764
2779
|
import { basename as basename3, extname as extname3 } from "path";
|
|
2780
|
+
import { readFileSync as readFileSync10 } from "fs";
|
|
2765
2781
|
import mammoth from "mammoth";
|
|
2766
|
-
async function
|
|
2767
|
-
const result = await mammoth.extractRawText({
|
|
2782
|
+
async function parseDocxBuffer(buffer, idHint = "document") {
|
|
2783
|
+
const result = await mammoth.extractRawText({ buffer });
|
|
2768
2784
|
const content = result.value;
|
|
2769
|
-
const id =
|
|
2785
|
+
const id = idHint.toLowerCase().replace(/[^a-z0-9\u4e00-\u9fff-]/g, "-").replace(/-+/g, "-");
|
|
2770
2786
|
const lines = content.split("\n").filter((l) => l.trim());
|
|
2771
2787
|
const title = lines[0]?.trim() ?? id;
|
|
2772
2788
|
return {
|
|
@@ -2777,6 +2793,11 @@ async function readDocxFile(filePath) {
|
|
|
2777
2793
|
confidence: 0.9
|
|
2778
2794
|
};
|
|
2779
2795
|
}
|
|
2796
|
+
async function readDocxFile(filePath) {
|
|
2797
|
+
const buffer = readFileSync10(filePath);
|
|
2798
|
+
const name = basename3(filePath, extname3(filePath));
|
|
2799
|
+
return parseDocxBuffer(buffer, name);
|
|
2800
|
+
}
|
|
2780
2801
|
var init_docx_reader2 = __esm({
|
|
2781
2802
|
"src/core/requirement-reader/docx-reader.ts"() {
|
|
2782
2803
|
"use strict";
|
|
@@ -2785,44 +2806,55 @@ var init_docx_reader2 = __esm({
|
|
|
2785
2806
|
|
|
2786
2807
|
// src/core/requirement-reader/xlsx-reader.ts
|
|
2787
2808
|
import { basename as basename4, extname as extname4 } from "path";
|
|
2788
|
-
import
|
|
2789
|
-
|
|
2790
|
-
const
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
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, "-");
|
|
2812
|
+
const firstSheetName = workbook.SheetNames[0];
|
|
2813
|
+
if (!firstSheetName) {
|
|
2814
|
+
return Promise.reject(new Error("No sheets found in workbook"));
|
|
2815
|
+
}
|
|
2816
|
+
const sheet = workbook.Sheets[firstSheetName];
|
|
2794
2817
|
if (!sheet) {
|
|
2795
|
-
|
|
2796
|
-
}
|
|
2797
|
-
const title =
|
|
2798
|
-
const
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2818
|
+
return Promise.reject(new Error(`Sheet "${firstSheetName}" not found`));
|
|
2819
|
+
}
|
|
2820
|
+
const title = firstSheetName;
|
|
2821
|
+
const rows = utils2.sheet_to_json(sheet, { header: 1, defval: "" });
|
|
2822
|
+
if (rows.length === 0) {
|
|
2823
|
+
return Promise.resolve({
|
|
2824
|
+
id,
|
|
2825
|
+
title,
|
|
2826
|
+
description: "",
|
|
2827
|
+
rawContent: "",
|
|
2828
|
+
confidence: 0.85
|
|
2829
|
+
});
|
|
2830
|
+
}
|
|
2831
|
+
const headers = rows[0].map((cell) => formatCellValue(cell));
|
|
2832
|
+
const dataRows = rows.slice(1);
|
|
2833
|
+
const objects = dataRows.map((row) => {
|
|
2834
|
+
const obj = {};
|
|
2835
|
+
row.forEach((cell, colNumber) => {
|
|
2836
|
+
const key = headers[colNumber] ?? `Col${colNumber + 1}`;
|
|
2837
|
+
obj[key] = formatCellValue(cell);
|
|
2838
|
+
});
|
|
2839
|
+
return obj;
|
|
2813
2840
|
});
|
|
2814
|
-
const textRows =
|
|
2841
|
+
const textRows = objects.map((row) => {
|
|
2815
2842
|
const cells = Object.entries(row).map(([key, value]) => `${key}: ${value}`).join(" | ");
|
|
2816
2843
|
return cells;
|
|
2817
2844
|
});
|
|
2818
2845
|
const content = textRows.join("\n");
|
|
2819
|
-
return {
|
|
2846
|
+
return Promise.resolve({
|
|
2820
2847
|
id,
|
|
2821
2848
|
title,
|
|
2822
2849
|
description: content,
|
|
2823
2850
|
rawContent: content,
|
|
2824
2851
|
confidence: 0.85
|
|
2825
|
-
};
|
|
2852
|
+
});
|
|
2853
|
+
}
|
|
2854
|
+
function readXlsxFile(filePath) {
|
|
2855
|
+
const workbook = readFile2(filePath);
|
|
2856
|
+
const idHint = basename4(filePath, extname4(filePath));
|
|
2857
|
+
return parseWorkbook2(workbook, idHint);
|
|
2826
2858
|
}
|
|
2827
2859
|
var init_xlsx_reader2 = __esm({
|
|
2828
2860
|
"src/core/requirement-reader/xlsx-reader.ts"() {
|
|
@@ -2833,22 +2865,43 @@ var init_xlsx_reader2 = __esm({
|
|
|
2833
2865
|
|
|
2834
2866
|
// src/core/requirement-reader/pdf-reader.ts
|
|
2835
2867
|
import { basename as basename5, extname as extname5 } from "path";
|
|
2836
|
-
import { readFile } from "fs/promises";
|
|
2837
|
-
async function
|
|
2838
|
-
const
|
|
2839
|
-
const
|
|
2840
|
-
const
|
|
2841
|
-
|
|
2842
|
-
|
|
2868
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
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());
|
|
2843
2891
|
const title = lines[0]?.trim() ?? id;
|
|
2844
2892
|
return {
|
|
2845
2893
|
id,
|
|
2846
2894
|
title,
|
|
2847
|
-
description:
|
|
2848
|
-
rawContent:
|
|
2895
|
+
description: text,
|
|
2896
|
+
rawContent: text,
|
|
2849
2897
|
confidence: 0.8
|
|
2850
2898
|
};
|
|
2851
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
|
+
}
|
|
2852
2905
|
var init_pdf_reader2 = __esm({
|
|
2853
2906
|
"src/core/requirement-reader/pdf-reader.ts"() {
|
|
2854
2907
|
"use strict";
|
|
@@ -2857,14 +2910,14 @@ var init_pdf_reader2 = __esm({
|
|
|
2857
2910
|
|
|
2858
2911
|
// src/core/requirement-reader/image-reader.ts
|
|
2859
2912
|
import { basename as basename6, extname as extname6 } from "path";
|
|
2860
|
-
import { readFileSync as
|
|
2861
|
-
async function readImageFile(filePath) {
|
|
2913
|
+
import { readFileSync as readFileSync11 } from "fs";
|
|
2914
|
+
async function readImageFile(filePath, providerOverride) {
|
|
2862
2915
|
const id = basename6(filePath, extname6(filePath)).toLowerCase().replace(/[^a-z0-9\u4e00-\u9fff-]/g, "-").replace(/-+/g, "-");
|
|
2863
2916
|
logger.info(`Reading image via AI Vision: ${filePath}`);
|
|
2864
2917
|
const ext = extname6(filePath).toLowerCase();
|
|
2865
2918
|
const mimeType = MIME_MAP[ext] ?? "image/png";
|
|
2866
|
-
const buffer =
|
|
2867
|
-
const provider = await createProvider();
|
|
2919
|
+
const buffer = readFileSync11(filePath);
|
|
2920
|
+
const provider = providerOverride ?? await createProvider();
|
|
2868
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.`;
|
|
2869
2922
|
if (!provider.generateWithImage) {
|
|
2870
2923
|
throw new Error(`AI provider '${provider.name}' does not support image/vision input. Use a vision-capable model.`);
|
|
@@ -3309,11 +3362,11 @@ var init_prompt_builder = __esm({
|
|
|
3309
3362
|
});
|
|
3310
3363
|
|
|
3311
3364
|
// src/core/feature-generator/generator.ts
|
|
3312
|
-
import { writeFileSync as writeFileSync10, mkdirSync as
|
|
3365
|
+
import { writeFileSync as writeFileSync10, mkdirSync as mkdirSync10, existsSync as existsSync15 } from "fs";
|
|
3313
3366
|
import { join as join17, dirname as dirname2 } from "path";
|
|
3314
3367
|
import { Parser as Parser2, GherkinClassicTokenMatcher as GherkinClassicTokenMatcher2, AstBuilder as AstBuilder2 } from "@cucumber/gherkin";
|
|
3315
3368
|
import { IdGenerator as IdGenerator2 } from "@cucumber/messages";
|
|
3316
|
-
async function generateFeature(requirement, context, config, projectRoot2) {
|
|
3369
|
+
async function generateFeature(requirement, context, config, projectRoot2, providerOverride) {
|
|
3317
3370
|
logger.info(`Generating feature for: ${requirement.title}`);
|
|
3318
3371
|
const registry = getRegistry();
|
|
3319
3372
|
await registry.invokeLifecycle("beforeSpec", requirement);
|
|
@@ -3321,7 +3374,7 @@ async function generateFeature(requirement, context, config, projectRoot2) {
|
|
|
3321
3374
|
const aiConfig = config.ai;
|
|
3322
3375
|
const fallbackConfigs = aiConfig?.fallbackProviders ?? [];
|
|
3323
3376
|
const providerConfigs = aiConfig ? [aiConfig, ...fallbackConfigs] : [];
|
|
3324
|
-
const provider = providerConfigs.length > 1 ? await createProviderChain(providerConfigs) : await createProvider(aiConfig);
|
|
3377
|
+
const provider = providerOverride ?? (providerConfigs.length > 1 ? await createProviderChain(providerConfigs) : await createProvider(aiConfig));
|
|
3325
3378
|
if (!provider.isAvailable()) {
|
|
3326
3379
|
throw new Error("AI provider not available. Set API key in environment or config.yaml.");
|
|
3327
3380
|
}
|
|
@@ -3378,7 +3431,7 @@ Please fix these issues and regenerate the complete Feature file.`;
|
|
|
3378
3431
|
const moduleDir = inferModule(requirement, context);
|
|
3379
3432
|
const filePath = join17(outputDir, moduleDir, `${requirement.id}.feature`);
|
|
3380
3433
|
if (!existsSync15(dirname2(filePath))) {
|
|
3381
|
-
|
|
3434
|
+
mkdirSync10(dirname2(filePath), { recursive: true });
|
|
3382
3435
|
}
|
|
3383
3436
|
writeFileSync10(filePath, featureContent, "utf-8");
|
|
3384
3437
|
logger.info(`Feature written: ${toPosixPath(filePath)}`);
|
|
@@ -3544,9 +3597,9 @@ function detectAITools2(projectRoot2) {
|
|
|
3544
3597
|
tools.push("cursor");
|
|
3545
3598
|
logger.debug("Detected Cursor (.cursor/)");
|
|
3546
3599
|
}
|
|
3547
|
-
if (existsSync16(join19(projectRoot2, ".claude")) || existsSync16(join19(projectRoot2, "CLAUDE.md"))
|
|
3600
|
+
if (existsSync16(join19(projectRoot2, ".claude")) || existsSync16(join19(projectRoot2, "CLAUDE.md"))) {
|
|
3548
3601
|
tools.push("claude");
|
|
3549
|
-
logger.debug("Detected Claude Code (.claude/ or CLAUDE.md
|
|
3602
|
+
logger.debug("Detected Claude Code (.claude/ or CLAUDE.md)");
|
|
3550
3603
|
}
|
|
3551
3604
|
if (existsSync16(join19(projectRoot2, ".qoder"))) {
|
|
3552
3605
|
tools.push("qoder");
|
|
@@ -3563,7 +3616,7 @@ var init_ai_tool_detector = __esm({
|
|
|
3563
3616
|
});
|
|
3564
3617
|
|
|
3565
3618
|
// src/core/constraint-coder/constraint-writer.ts
|
|
3566
|
-
import { writeFileSync as writeFileSync11, mkdirSync as
|
|
3619
|
+
import { writeFileSync as writeFileSync11, mkdirSync as mkdirSync11, existsSync as existsSync17 } from "fs";
|
|
3567
3620
|
import { join as join20 } from "path";
|
|
3568
3621
|
function writeConstraints(projectRoot2, tools, content) {
|
|
3569
3622
|
const writtenFiles = [];
|
|
@@ -3637,7 +3690,7 @@ function buildConstraintText(content) {
|
|
|
3637
3690
|
}
|
|
3638
3691
|
function writeCursorRule2(projectRoot2, text) {
|
|
3639
3692
|
const dir = join20(projectRoot2, ".cursor", "rules");
|
|
3640
|
-
if (!existsSync17(dir))
|
|
3693
|
+
if (!existsSync17(dir)) mkdirSync11(dir, { recursive: true });
|
|
3641
3694
|
const filePath = join20(dir, "speclore.mdc");
|
|
3642
3695
|
const frontmatter = [
|
|
3643
3696
|
"---",
|
|
@@ -3652,14 +3705,14 @@ function writeCursorRule2(projectRoot2, text) {
|
|
|
3652
3705
|
}
|
|
3653
3706
|
function writeClaudeRule2(projectRoot2, text) {
|
|
3654
3707
|
const dir = join20(projectRoot2, ".claude", "rules");
|
|
3655
|
-
if (!existsSync17(dir))
|
|
3708
|
+
if (!existsSync17(dir)) mkdirSync11(dir, { recursive: true });
|
|
3656
3709
|
const filePath = join20(dir, "speclore.md");
|
|
3657
3710
|
writeFileSync11(filePath, text, "utf-8");
|
|
3658
3711
|
return filePath;
|
|
3659
3712
|
}
|
|
3660
3713
|
function writeQoderRule2(projectRoot2, text) {
|
|
3661
3714
|
const dir = join20(projectRoot2, ".qoder", "rules");
|
|
3662
|
-
if (!existsSync17(dir))
|
|
3715
|
+
if (!existsSync17(dir)) mkdirSync11(dir, { recursive: true });
|
|
3663
3716
|
const filePath = join20(dir, "speclore.md");
|
|
3664
3717
|
writeFileSync11(filePath, text, "utf-8");
|
|
3665
3718
|
return filePath;
|
|
@@ -3739,14 +3792,14 @@ var init_constraint_coder = __esm({
|
|
|
3739
3792
|
init_logger();
|
|
3740
3793
|
init_ai_tool_detector();
|
|
3741
3794
|
init_constraint_writer();
|
|
3742
|
-
MAPPING_INSTRUCTIONS =
|
|
3743
|
-
|
|
3744
|
-
|
|
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.`;
|
|
3745
3798
|
}
|
|
3746
3799
|
});
|
|
3747
3800
|
|
|
3748
3801
|
// src/core/test-scaffolder/framework-detector.ts
|
|
3749
|
-
import { readFileSync as
|
|
3802
|
+
import { readFileSync as readFileSync12, existsSync as existsSync18 } from "fs";
|
|
3750
3803
|
import { join as join21 } from "path";
|
|
3751
3804
|
function detectTestFramework(projectRoot2) {
|
|
3752
3805
|
const pkgPath = join21(projectRoot2, "package.json");
|
|
@@ -3754,7 +3807,7 @@ function detectTestFramework(projectRoot2) {
|
|
|
3754
3807
|
return "vitest";
|
|
3755
3808
|
}
|
|
3756
3809
|
try {
|
|
3757
|
-
const pkg = JSON.parse(
|
|
3810
|
+
const pkg = JSON.parse(readFileSync12(pkgPath, "utf-8"));
|
|
3758
3811
|
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
3759
3812
|
if ("vitest" in deps) return "vitest";
|
|
3760
3813
|
if ("jest" in deps || "@jest/core" in deps) return "jest";
|
|
@@ -3770,7 +3823,7 @@ var init_framework_detector = __esm({
|
|
|
3770
3823
|
});
|
|
3771
3824
|
|
|
3772
3825
|
// src/core/test-scaffolder/index.ts
|
|
3773
|
-
import { readFileSync as
|
|
3826
|
+
import { readFileSync as readFileSync13, writeFileSync as writeFileSync12, existsSync as existsSync19, mkdirSync as mkdirSync12 } from "fs";
|
|
3774
3827
|
import { join as join22, relative as relative3, dirname as dirname3, basename as basename8 } from "path";
|
|
3775
3828
|
function generateTestScaffolding(projectRoot2, features, config) {
|
|
3776
3829
|
const framework = detectTestFramework(projectRoot2);
|
|
@@ -3793,7 +3846,7 @@ function generateTestScaffolding(projectRoot2, features, config) {
|
|
|
3793
3846
|
const content = generateTestFileContent(feature.featureName, scenarios, framework);
|
|
3794
3847
|
const dir = dirname3(absTestPath);
|
|
3795
3848
|
if (!existsSync19(dir)) {
|
|
3796
|
-
|
|
3849
|
+
mkdirSync12(dir, { recursive: true });
|
|
3797
3850
|
}
|
|
3798
3851
|
writeFileSync12(absTestPath, content, "utf-8");
|
|
3799
3852
|
results.push({ testFile: testFilePath, framework, scenarios: scenarios.length });
|
|
@@ -3866,7 +3919,7 @@ function generateTestFileContent(featureName, scenarios, framework) {
|
|
|
3866
3919
|
return lines.join("\n");
|
|
3867
3920
|
}
|
|
3868
3921
|
function appendMissingScenarios(testFilePath, scenarios, _framework) {
|
|
3869
|
-
const content =
|
|
3922
|
+
const content = readFileSync13(testFilePath, "utf-8");
|
|
3870
3923
|
const missing = [];
|
|
3871
3924
|
for (const scenario of scenarios) {
|
|
3872
3925
|
if (!content.includes(`Scenario: ${scenario.name}`)) {
|
|
@@ -3907,7 +3960,7 @@ var init_test_scaffolder = __esm({
|
|
|
3907
3960
|
});
|
|
3908
3961
|
|
|
3909
3962
|
// src/core/verifier/mapping-resolver.ts
|
|
3910
|
-
import { readFileSync as
|
|
3963
|
+
import { readFileSync as readFileSync15, existsSync as existsSync21, readdirSync as readdirSync2 } from "fs";
|
|
3911
3964
|
import { join as join24 } from "path";
|
|
3912
3965
|
function resolveMappings(projectRoot2, features, testOutput) {
|
|
3913
3966
|
const results = [];
|
|
@@ -3937,7 +3990,7 @@ function resolveFromMappingFile(projectRoot2, _feature, scenario) {
|
|
|
3937
3990
|
const files = findMappingFiles(mappingsDir);
|
|
3938
3991
|
for (const file of files) {
|
|
3939
3992
|
try {
|
|
3940
|
-
const content =
|
|
3993
|
+
const content = readFileSync15(file, "utf-8");
|
|
3941
3994
|
const mapping = JSON.parse(content);
|
|
3942
3995
|
if (mapping.scenarios && scenario.name in mapping.scenarios) {
|
|
3943
3996
|
const entry = mapping.scenarios[scenario.name];
|
|
@@ -3967,7 +4020,7 @@ function resolveFromTag(projectRoot2, _feature, scenario) {
|
|
|
3967
4020
|
const files = findTestFiles(testsDir);
|
|
3968
4021
|
for (const file of files) {
|
|
3969
4022
|
try {
|
|
3970
|
-
const content =
|
|
4023
|
+
const content = readFileSync15(file, "utf-8");
|
|
3971
4024
|
const tagRegex = /@speclore-scenario:\s*(.+)/g;
|
|
3972
4025
|
let match;
|
|
3973
4026
|
while ((match = tagRegex.exec(content)) !== null) {
|
|
@@ -4147,13 +4200,13 @@ var report_generator_exports = {};
|
|
|
4147
4200
|
__export(report_generator_exports, {
|
|
4148
4201
|
generateReport: () => generateReport
|
|
4149
4202
|
});
|
|
4150
|
-
import { writeFileSync as writeFileSync13, mkdirSync as
|
|
4203
|
+
import { writeFileSync as writeFileSync13, mkdirSync as mkdirSync13, existsSync as existsSync22, readFileSync as readFileSync16 } from "fs";
|
|
4151
4204
|
import { join as join25, dirname as dirname4 } from "path";
|
|
4152
4205
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
4153
4206
|
function generateReport(report, projectRoot2, config) {
|
|
4154
4207
|
const reportsDir = join25(projectRoot2, ".speclore", "reports");
|
|
4155
4208
|
if (!existsSync22(reportsDir)) {
|
|
4156
|
-
|
|
4209
|
+
mkdirSync13(reportsDir, { recursive: true });
|
|
4157
4210
|
}
|
|
4158
4211
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
4159
4212
|
const writtenFiles = [];
|
|
@@ -4210,7 +4263,7 @@ function renderHtmlReport(report) {
|
|
|
4210
4263
|
const templatePath = join25(dirname4(fileURLToPath2(import.meta.url)), "..", "..", "cli", "templates", "report.html");
|
|
4211
4264
|
let template;
|
|
4212
4265
|
try {
|
|
4213
|
-
template =
|
|
4266
|
+
template = readFileSync16(templatePath, "utf-8");
|
|
4214
4267
|
} catch {
|
|
4215
4268
|
template = getInlineTemplate();
|
|
4216
4269
|
}
|
|
@@ -4289,7 +4342,7 @@ var init_verifier = __esm({
|
|
|
4289
4342
|
});
|
|
4290
4343
|
|
|
4291
4344
|
// src/core/analyzer/rdg-builder.ts
|
|
4292
|
-
import { readFileSync as
|
|
4345
|
+
import { readFileSync as readFileSync17, existsSync as existsSync23, readdirSync as readdirSync3 } from "fs";
|
|
4293
4346
|
import { join as join26 } from "path";
|
|
4294
4347
|
var init_rdg_builder = __esm({
|
|
4295
4348
|
"src/core/analyzer/rdg-builder.ts"() {
|
|
@@ -4319,6 +4372,9 @@ import { execFileSync as execFileSync3 } from "child_process";
|
|
|
4319
4372
|
import { globSync as globSync5 } from "glob";
|
|
4320
4373
|
function analyzeImpact(projectRoot2, context, config) {
|
|
4321
4374
|
const changedFiles = getChangedFiles(projectRoot2);
|
|
4375
|
+
return analyzeImpactWithChanges(changedFiles, context, config, projectRoot2);
|
|
4376
|
+
}
|
|
4377
|
+
function analyzeImpactWithChanges(changedFiles, context, config, projectRoot2) {
|
|
4322
4378
|
const affectedModules = determineAffectedModules(changedFiles, context);
|
|
4323
4379
|
const affectedFeatures = determineAffectedFeatures(affectedModules, projectRoot2, config.spec.outputDir);
|
|
4324
4380
|
logger.info(
|
|
@@ -4394,11 +4450,11 @@ var init_analyzer = __esm({
|
|
|
4394
4450
|
});
|
|
4395
4451
|
|
|
4396
4452
|
// src/infra/file-lock.ts
|
|
4397
|
-
import { readFileSync as
|
|
4398
|
-
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";
|
|
4399
4455
|
function acquireLock(specLoreDir) {
|
|
4400
|
-
const lockPath =
|
|
4401
|
-
if (
|
|
4456
|
+
const lockPath = join31(specLoreDir, LOCK_FILENAME);
|
|
4457
|
+
if (existsSync28(lockPath)) {
|
|
4402
4458
|
const existing = readLockInfo(lockPath);
|
|
4403
4459
|
if (existing && !isLockExpired(existing)) {
|
|
4404
4460
|
logger.debug(`Lock already held by PID ${existing.pid}, not expired`);
|
|
@@ -4412,7 +4468,7 @@ function acquireLock(specLoreDir) {
|
|
|
4412
4468
|
timestamp: Date.now()
|
|
4413
4469
|
};
|
|
4414
4470
|
try {
|
|
4415
|
-
|
|
4471
|
+
writeFileSync16(lockPath, JSON.stringify(lockInfo), { flag: "wx", encoding: "utf-8" });
|
|
4416
4472
|
} catch {
|
|
4417
4473
|
logger.debug("Lock write failed \u2014 another process may have acquired it");
|
|
4418
4474
|
return false;
|
|
@@ -4421,8 +4477,8 @@ function acquireLock(specLoreDir) {
|
|
|
4421
4477
|
return true;
|
|
4422
4478
|
}
|
|
4423
4479
|
function releaseLock(specLoreDir) {
|
|
4424
|
-
const lockPath =
|
|
4425
|
-
if (!
|
|
4480
|
+
const lockPath = join31(specLoreDir, LOCK_FILENAME);
|
|
4481
|
+
if (!existsSync28(lockPath)) {
|
|
4426
4482
|
return;
|
|
4427
4483
|
}
|
|
4428
4484
|
const existing = readLockInfo(lockPath);
|
|
@@ -4433,7 +4489,7 @@ function releaseLock(specLoreDir) {
|
|
|
4433
4489
|
}
|
|
4434
4490
|
function readLockInfo(lockPath) {
|
|
4435
4491
|
try {
|
|
4436
|
-
const content =
|
|
4492
|
+
const content = readFileSync21(lockPath, "utf-8");
|
|
4437
4493
|
const parsed = JSON.parse(content);
|
|
4438
4494
|
if (typeof parsed.pid === "number" && typeof parsed.timestamp === "number") {
|
|
4439
4495
|
return parsed;
|
|
@@ -4503,8 +4559,8 @@ var init_schemas = __esm({
|
|
|
4503
4559
|
});
|
|
4504
4560
|
|
|
4505
4561
|
// src/mcp/tools.ts
|
|
4506
|
-
import { join as
|
|
4507
|
-
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";
|
|
4508
4564
|
import { globSync as globSync7 } from "glob";
|
|
4509
4565
|
import { toJSONSchema } from "zod";
|
|
4510
4566
|
async function executeSpecTool(args, projectRoot2) {
|
|
@@ -4515,7 +4571,7 @@ async function executeSpecTool(args, projectRoot2) {
|
|
|
4515
4571
|
const config = loadConfig(projectRoot2);
|
|
4516
4572
|
logger.info(`Reading requirement from: ${args.source.slice(0, 80)}...`);
|
|
4517
4573
|
const req = await readRequirement(args.source);
|
|
4518
|
-
const specLoreDir =
|
|
4574
|
+
const specLoreDir = join32(projectRoot2, ".speclore");
|
|
4519
4575
|
const context = loadContext(specLoreDir) ?? buildContext(projectRoot2, config);
|
|
4520
4576
|
const feature = await generateFeature(req, context, config, projectRoot2);
|
|
4521
4577
|
const createdFiles = [feature.path];
|
|
@@ -4547,7 +4603,7 @@ async function executeCodeTool(args, projectRoot2) {
|
|
|
4547
4603
|
}
|
|
4548
4604
|
ensureProjectReady(projectRoot2);
|
|
4549
4605
|
const config = loadConfig(projectRoot2);
|
|
4550
|
-
const specLoreDir =
|
|
4606
|
+
const specLoreDir = join32(projectRoot2, ".speclore");
|
|
4551
4607
|
const context = loadContext(specLoreDir) ?? buildContext(projectRoot2, config);
|
|
4552
4608
|
const featureFiles = resolveFeatureFiles(args.features, projectRoot2, config);
|
|
4553
4609
|
if (featureFiles.length === 0) {
|
|
@@ -4600,7 +4656,7 @@ async function executeCodeTool(args, projectRoot2) {
|
|
|
4600
4656
|
async function executeVerifyTool(args, projectRoot2) {
|
|
4601
4657
|
ensureProjectReady(projectRoot2);
|
|
4602
4658
|
const config = loadConfig(projectRoot2);
|
|
4603
|
-
const specLoreDir =
|
|
4659
|
+
const specLoreDir = join32(projectRoot2, ".speclore");
|
|
4604
4660
|
const context = loadContext(specLoreDir) ?? buildContext(projectRoot2, config);
|
|
4605
4661
|
const featureFiles = resolveFeatureFiles(args.features, projectRoot2, config);
|
|
4606
4662
|
const stateManager = new StateManager(projectRoot2);
|
|
@@ -4662,22 +4718,22 @@ async function executeVerifyTool(args, projectRoot2) {
|
|
|
4662
4718
|
};
|
|
4663
4719
|
}
|
|
4664
4720
|
function ensureProjectReady(projectRoot2) {
|
|
4665
|
-
const specLoreDir =
|
|
4666
|
-
const configPath =
|
|
4721
|
+
const specLoreDir = join32(projectRoot2, ".speclore");
|
|
4722
|
+
const configPath = join32(specLoreDir, "config.yaml");
|
|
4667
4723
|
let configCreated = false;
|
|
4668
|
-
if (!
|
|
4669
|
-
|
|
4724
|
+
if (!existsSync29(specLoreDir)) {
|
|
4725
|
+
mkdirSync14(specLoreDir, { recursive: true });
|
|
4670
4726
|
}
|
|
4671
|
-
if (!
|
|
4727
|
+
if (!existsSync29(configPath)) {
|
|
4672
4728
|
const projectName = projectRoot2.split(/[/\\]/).pop() ?? "my-project";
|
|
4673
|
-
|
|
4729
|
+
writeFileSync17(configPath, generateDefaultConfigYaml(projectName), "utf-8");
|
|
4674
4730
|
configCreated = true;
|
|
4675
4731
|
logger.info(`Auto-created default config: ${configPath}`);
|
|
4676
4732
|
}
|
|
4677
4733
|
const stateManager = new StateManager(projectRoot2);
|
|
4678
4734
|
stateManager.ensureInitialized();
|
|
4679
4735
|
const config = loadConfig(projectRoot2);
|
|
4680
|
-
const specsDir =
|
|
4736
|
+
const specsDir = join32(projectRoot2, config.spec.outputDir);
|
|
4681
4737
|
const migrated = stateManager.migrateFeatures(specsDir);
|
|
4682
4738
|
if (migrated > 0) {
|
|
4683
4739
|
logger.info(`Migrated ${migrated} existing .feature file(s) into state tracking.`);
|
|
@@ -4714,14 +4770,14 @@ function resolveNextStep(currentState, _summary) {
|
|
|
4714
4770
|
}
|
|
4715
4771
|
}
|
|
4716
4772
|
function resolveFeatureFiles(patterns, projectRoot2, config) {
|
|
4717
|
-
const specsDir =
|
|
4773
|
+
const specsDir = join32(projectRoot2, config.spec.outputDir);
|
|
4718
4774
|
const searchPatterns = patterns && patterns.length > 0 ? patterns : [`${specsDir}/**/*.feature`];
|
|
4719
4775
|
const files = [];
|
|
4720
4776
|
for (const pattern of searchPatterns) {
|
|
4721
4777
|
const matches = globSync7(pattern, { cwd: projectRoot2, absolute: true });
|
|
4722
4778
|
for (const filePath of matches) {
|
|
4723
|
-
if (
|
|
4724
|
-
const content =
|
|
4779
|
+
if (existsSync29(filePath)) {
|
|
4780
|
+
const content = readFileSync22(filePath, "utf-8");
|
|
4725
4781
|
files.push(parseFeatureFile(filePath, content));
|
|
4726
4782
|
}
|
|
4727
4783
|
}
|
|
@@ -4829,8 +4885,8 @@ var init_tools = __esm({
|
|
|
4829
4885
|
});
|
|
4830
4886
|
|
|
4831
4887
|
// src/mcp/status.ts
|
|
4832
|
-
import { readFileSync as
|
|
4833
|
-
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";
|
|
4834
4890
|
import { globSync as globSync8 } from "glob";
|
|
4835
4891
|
function executeStatusTool(args, projectRoot2) {
|
|
4836
4892
|
const { configCreated } = ensureProjectReadyForStatus(projectRoot2);
|
|
@@ -4846,9 +4902,9 @@ function executeStatusTool(args, projectRoot2) {
|
|
|
4846
4902
|
continue;
|
|
4847
4903
|
}
|
|
4848
4904
|
let scenarioCount = 0;
|
|
4849
|
-
if (
|
|
4905
|
+
if (existsSync30(path)) {
|
|
4850
4906
|
try {
|
|
4851
|
-
const content =
|
|
4907
|
+
const content = readFileSync23(path, "utf-8");
|
|
4852
4908
|
const matches = content.match(/Scenario(?: Outline)?:/g);
|
|
4853
4909
|
scenarioCount = matches?.length ?? 0;
|
|
4854
4910
|
} catch {
|
|
@@ -4863,7 +4919,7 @@ function executeStatusTool(args, projectRoot2) {
|
|
|
4863
4919
|
lastVerify: entry.lastVerify ? { passed: entry.lastVerify.passed, failed: entry.lastVerify.failed, timestamp: entry.lastVerify.timestamp } : void 0
|
|
4864
4920
|
});
|
|
4865
4921
|
}
|
|
4866
|
-
const specsDir =
|
|
4922
|
+
const specsDir = join33(projectRoot2, config.spec.outputDir);
|
|
4867
4923
|
const allFeatureFiles = globSync8(`${specsDir}/**/*.feature`, { cwd: projectRoot2, absolute: true });
|
|
4868
4924
|
const trackedPaths = new Set(featureEntries.map((f) => f.path));
|
|
4869
4925
|
for (const filePath of allFeatureFiles) {
|
|
@@ -4871,7 +4927,7 @@ function executeStatusTool(args, projectRoot2) {
|
|
|
4871
4927
|
if (args.feature && !filePath.includes(args.feature)) continue;
|
|
4872
4928
|
let scenarioCount = 0;
|
|
4873
4929
|
try {
|
|
4874
|
-
const content =
|
|
4930
|
+
const content = readFileSync23(filePath, "utf-8");
|
|
4875
4931
|
const matches = content.match(/Scenario(?: Outline)?:/g);
|
|
4876
4932
|
scenarioCount = matches?.length ?? 0;
|
|
4877
4933
|
} catch {
|
|
@@ -4928,22 +4984,22 @@ function buildRecommendedActions(features, summary, testCommand) {
|
|
|
4928
4984
|
return actions;
|
|
4929
4985
|
}
|
|
4930
4986
|
function ensureProjectReadyForStatus(projectRoot2) {
|
|
4931
|
-
const specLoreDir =
|
|
4932
|
-
const configPath =
|
|
4987
|
+
const specLoreDir = join33(projectRoot2, ".speclore");
|
|
4988
|
+
const configPath = join33(specLoreDir, "config.yaml");
|
|
4933
4989
|
let configCreated = false;
|
|
4934
|
-
if (!
|
|
4935
|
-
|
|
4990
|
+
if (!existsSync30(specLoreDir)) {
|
|
4991
|
+
mkdirSync15(specLoreDir, { recursive: true });
|
|
4936
4992
|
}
|
|
4937
|
-
if (!
|
|
4993
|
+
if (!existsSync30(configPath)) {
|
|
4938
4994
|
const projectName = projectRoot2.split(/[/\\]/).pop() ?? "my-project";
|
|
4939
|
-
|
|
4995
|
+
writeFileSync18(configPath, generateDefaultConfigYaml(projectName), "utf-8");
|
|
4940
4996
|
configCreated = true;
|
|
4941
4997
|
logger.info(`Auto-created default config: ${configPath}`);
|
|
4942
4998
|
}
|
|
4943
4999
|
const stateManager = new StateManager(projectRoot2);
|
|
4944
5000
|
stateManager.ensureInitialized();
|
|
4945
5001
|
const config = loadConfig(projectRoot2);
|
|
4946
|
-
const specsDir =
|
|
5002
|
+
const specsDir = join33(projectRoot2, config.spec.outputDir);
|
|
4947
5003
|
const migrated = stateManager.migrateFeatures(specsDir);
|
|
4948
5004
|
if (migrated > 0) {
|
|
4949
5005
|
logger.info(`Migrated ${migrated} existing .feature file(s) into state tracking.`);
|
|
@@ -4967,7 +5023,7 @@ __export(server_exports, {
|
|
|
4967
5023
|
});
|
|
4968
5024
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4969
5025
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4970
|
-
import { mkdirSync as
|
|
5026
|
+
import { mkdirSync as mkdirSync16 } from "fs";
|
|
4971
5027
|
function getProjectRoot() {
|
|
4972
5028
|
return projectRoot;
|
|
4973
5029
|
}
|
|
@@ -4975,7 +5031,7 @@ async function startMcpServer() {
|
|
|
4975
5031
|
projectRoot = process.env.SPECLORE_PROJECT_ROOT ?? process.cwd();
|
|
4976
5032
|
const specLoreDir = `${projectRoot}/.speclore`;
|
|
4977
5033
|
try {
|
|
4978
|
-
|
|
5034
|
+
mkdirSync16(specLoreDir, { recursive: true });
|
|
4979
5035
|
const locked = acquireLock(specLoreDir);
|
|
4980
5036
|
if (!locked) {
|
|
4981
5037
|
logger.warn("Could not acquire lock \u2014 another SpecLore instance may be running.");
|
|
@@ -5088,11 +5144,11 @@ import { Command } from "commander";
|
|
|
5088
5144
|
init_logger();
|
|
5089
5145
|
init_config2();
|
|
5090
5146
|
init_detector();
|
|
5091
|
-
import { existsSync as existsSync5, mkdirSync as
|
|
5147
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
5092
5148
|
import { join as join7 } from "path";
|
|
5093
5149
|
|
|
5094
5150
|
// src/setup/config-writer.ts
|
|
5095
|
-
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";
|
|
5096
5152
|
import { join as join5 } from "path";
|
|
5097
5153
|
|
|
5098
5154
|
// src/infra/install-detector.ts
|
|
@@ -5143,13 +5199,42 @@ function detectCloneInstall(projectRoot2) {
|
|
|
5143
5199
|
|
|
5144
5200
|
// src/setup/config-writer.ts
|
|
5145
5201
|
init_logger();
|
|
5146
|
-
function writeMcpConfig(projectRoot2, _globalMode) {
|
|
5202
|
+
function writeMcpConfig(projectRoot2, tools, _globalMode) {
|
|
5147
5203
|
const installInfo = detectInstallMethod(projectRoot2);
|
|
5148
5204
|
const serverCommand = getServerCommand(installInfo);
|
|
5149
|
-
|
|
5150
|
-
|
|
5151
|
-
|
|
5152
|
-
|
|
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) {
|
|
5220
|
+
const installInfo = detectInstallMethod(projectRoot2);
|
|
5221
|
+
const serverCommand = getServerCommand(installInfo);
|
|
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
|
+
}
|
|
5153
5238
|
}
|
|
5154
5239
|
function getServerCommand(installInfo) {
|
|
5155
5240
|
if (installInfo.mode === "npm") {
|
|
@@ -5173,6 +5258,9 @@ function writeCursorMcp(projectRoot2, serverCmd) {
|
|
|
5173
5258
|
logger.info(` Cursor: ${mcpPath}`);
|
|
5174
5259
|
}
|
|
5175
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;
|
|
5176
5264
|
const mcpPath = join5(projectRoot2, ".mcp.json");
|
|
5177
5265
|
const existing = readExistingJson(mcpPath);
|
|
5178
5266
|
if (!existing.mcpServers) existing.mcpServers = {};
|
|
@@ -5211,7 +5299,7 @@ function readExistingJson(filePath) {
|
|
|
5211
5299
|
|
|
5212
5300
|
// src/setup/rule-writer.ts
|
|
5213
5301
|
init_logger();
|
|
5214
|
-
import { writeFileSync as writeFileSync3, mkdirSync as
|
|
5302
|
+
import { writeFileSync as writeFileSync3, mkdirSync as mkdirSync3 } from "fs";
|
|
5215
5303
|
import { join as join6 } from "path";
|
|
5216
5304
|
function writeRuleFiles(projectRoot2, tools) {
|
|
5217
5305
|
for (const tool of tools) {
|
|
@@ -5230,7 +5318,7 @@ function writeRuleFiles(projectRoot2, tools) {
|
|
|
5230
5318
|
}
|
|
5231
5319
|
function writeCursorRule(projectRoot2) {
|
|
5232
5320
|
const rulesDir = join6(projectRoot2, ".cursor", "rules");
|
|
5233
|
-
|
|
5321
|
+
mkdirSync3(rulesDir, { recursive: true });
|
|
5234
5322
|
const content = `---
|
|
5235
5323
|
description: SpecLore \u2014 AI coding constraints (setup placeholder)
|
|
5236
5324
|
globs:
|
|
@@ -5252,7 +5340,7 @@ SpecLore is configured for this project. When the \`speclore.spec\` MCP tool is
|
|
|
5252
5340
|
}
|
|
5253
5341
|
function writeClaudeRule(projectRoot2) {
|
|
5254
5342
|
const rulesDir = join6(projectRoot2, ".claude", "rules");
|
|
5255
|
-
|
|
5343
|
+
mkdirSync3(rulesDir, { recursive: true });
|
|
5256
5344
|
const content = `# SpecLore
|
|
5257
5345
|
|
|
5258
5346
|
SpecLore is configured for this project. When the \`speclore.spec\` MCP tool is available:
|
|
@@ -5268,7 +5356,7 @@ SpecLore is configured for this project. When the \`speclore.spec\` MCP tool is
|
|
|
5268
5356
|
}
|
|
5269
5357
|
function writeQoderRule(projectRoot2) {
|
|
5270
5358
|
const rulesDir = join6(projectRoot2, ".qoder", "rules");
|
|
5271
|
-
|
|
5359
|
+
mkdirSync3(rulesDir, { recursive: true });
|
|
5272
5360
|
const content = `# SpecLore
|
|
5273
5361
|
|
|
5274
5362
|
SpecLore is configured for this project. When the \`speclore.spec\` MCP tool is available:
|
|
@@ -5292,13 +5380,20 @@ function registerSetupCommand(program) {
|
|
|
5292
5380
|
logger.info("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
5293
5381
|
const tools = detectAITools(projectRoot2);
|
|
5294
5382
|
if (tools.length === 0) {
|
|
5295
|
-
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("");
|
|
5296
5391
|
} else {
|
|
5297
5392
|
logger.info(`Detected AI tools: ${tools.map((t) => t.tool).join(", ")}`);
|
|
5298
5393
|
}
|
|
5299
5394
|
const specLoreDir = opts.global ? join7(process.env.HOME ?? process.env.USERPROFILE ?? "~", ".speclore") : join7(projectRoot2, ".speclore");
|
|
5300
5395
|
if (!existsSync5(specLoreDir)) {
|
|
5301
|
-
|
|
5396
|
+
mkdirSync4(specLoreDir, { recursive: true });
|
|
5302
5397
|
}
|
|
5303
5398
|
const configPath = join7(specLoreDir, "config.yaml");
|
|
5304
5399
|
if (!existsSync5(configPath)) {
|
|
@@ -5306,12 +5401,23 @@ function registerSetupCommand(program) {
|
|
|
5306
5401
|
writeFileSync4(configPath, generateDefaultConfigYaml(projectName), "utf-8");
|
|
5307
5402
|
logger.info(`Created config: ${configPath}`);
|
|
5308
5403
|
}
|
|
5309
|
-
writeMcpConfig(projectRoot2, opts.global ?? false);
|
|
5404
|
+
writeMcpConfig(projectRoot2, tools, opts.global ?? false);
|
|
5310
5405
|
writeRuleFiles(projectRoot2, tools);
|
|
5311
5406
|
logger.info("");
|
|
5312
|
-
logger.info("Setup complete!
|
|
5313
|
-
logger.info("
|
|
5314
|
-
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)");
|
|
5315
5421
|
});
|
|
5316
5422
|
}
|
|
5317
5423
|
|
|
@@ -5321,7 +5427,7 @@ init_logger();
|
|
|
5321
5427
|
init_context_engine();
|
|
5322
5428
|
import { join as join11 } from "path";
|
|
5323
5429
|
function registerInitCommand(program) {
|
|
5324
|
-
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) => {
|
|
5325
5431
|
if (opts.verbose) logger.setLevel("debug");
|
|
5326
5432
|
const projectRoot2 = process.cwd();
|
|
5327
5433
|
logger.info("SpecLore Init");
|
|
@@ -5385,7 +5491,7 @@ init_test_scaffolder();
|
|
|
5385
5491
|
init_context_engine();
|
|
5386
5492
|
init_state_manager();
|
|
5387
5493
|
import { join as join23 } from "path";
|
|
5388
|
-
import { readFileSync as
|
|
5494
|
+
import { readFileSync as readFileSync14, existsSync as existsSync20 } from "fs";
|
|
5389
5495
|
import { globSync as globSync4 } from "glob";
|
|
5390
5496
|
function registerCodeCommand(program) {
|
|
5391
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) => {
|
|
@@ -5441,7 +5547,7 @@ function resolveFeatures(patterns, projectRoot2, config) {
|
|
|
5441
5547
|
const matches = globSync4(pattern, { cwd: projectRoot2, absolute: true });
|
|
5442
5548
|
for (const filePath of matches) {
|
|
5443
5549
|
if (existsSync20(filePath)) {
|
|
5444
|
-
const content =
|
|
5550
|
+
const content = readFileSync14(filePath, "utf-8");
|
|
5445
5551
|
const featureMatch = content.match(/Feature:\s*(.+)/);
|
|
5446
5552
|
files.push({
|
|
5447
5553
|
path: filePath,
|
|
@@ -5465,7 +5571,7 @@ init_context_engine();
|
|
|
5465
5571
|
init_analyzer();
|
|
5466
5572
|
init_state_manager();
|
|
5467
5573
|
import { join as join27 } from "path";
|
|
5468
|
-
import { readFileSync as
|
|
5574
|
+
import { readFileSync as readFileSync18, existsSync as existsSync24 } from "fs";
|
|
5469
5575
|
import { globSync as globSync6 } from "glob";
|
|
5470
5576
|
function registerVerifyCommand(program) {
|
|
5471
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) => {
|
|
@@ -5552,7 +5658,7 @@ function resolveFeatures2(patterns, projectRoot2, config) {
|
|
|
5552
5658
|
const matches = globSync6(pattern, { cwd: projectRoot2, absolute: true });
|
|
5553
5659
|
for (const filePath of matches) {
|
|
5554
5660
|
if (existsSync24(filePath)) {
|
|
5555
|
-
const content =
|
|
5661
|
+
const content = readFileSync18(filePath, "utf-8");
|
|
5556
5662
|
const featureMatch = content.match(/Feature:\s*(.+)/);
|
|
5557
5663
|
files.push({
|
|
5558
5664
|
path: filePath,
|
|
@@ -5573,7 +5679,7 @@ init_logger();
|
|
|
5573
5679
|
|
|
5574
5680
|
// src/setup/cleanup.ts
|
|
5575
5681
|
init_logger();
|
|
5576
|
-
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";
|
|
5577
5683
|
import { join as join28 } from "path";
|
|
5578
5684
|
function runTeardown(projectRoot2, globalMode) {
|
|
5579
5685
|
if (globalMode) {
|
|
@@ -5602,7 +5708,7 @@ function runTeardown(projectRoot2, globalMode) {
|
|
|
5602
5708
|
function removeMcpEntry(mcpPath, serverName) {
|
|
5603
5709
|
if (!existsSync25(mcpPath)) return;
|
|
5604
5710
|
try {
|
|
5605
|
-
const config = JSON.parse(
|
|
5711
|
+
const config = JSON.parse(readFileSync19(mcpPath, "utf-8"));
|
|
5606
5712
|
if (config.mcpServers?.[serverName]) {
|
|
5607
5713
|
delete config.mcpServers[serverName];
|
|
5608
5714
|
writeFileSync14(mcpPath, JSON.stringify(config, null, 2), "utf-8");
|
|
@@ -5698,6 +5804,114 @@ function registerMigrateCommand(program) {
|
|
|
5698
5804
|
});
|
|
5699
5805
|
}
|
|
5700
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
|
+
|
|
5701
5915
|
// src/cli/index.ts
|
|
5702
5916
|
init_version();
|
|
5703
5917
|
function createProgram() {
|
|
@@ -5717,6 +5931,7 @@ function createProgram() {
|
|
|
5717
5931
|
const { startMcpServer: startMcpServer2 } = await Promise.resolve().then(() => (init_server(), server_exports));
|
|
5718
5932
|
await startMcpServer2();
|
|
5719
5933
|
});
|
|
5934
|
+
registerMcpConfigCommands(program);
|
|
5720
5935
|
program.argument("[text...]", "Quick requirement text \u2192 .feature").action(async (text) => {
|
|
5721
5936
|
if (text.length > 0) {
|
|
5722
5937
|
const { loadConfig: loadConfig2 } = await Promise.resolve().then(() => (init_config2(), config_exports));
|