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/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
|
}
|
|
@@ -1411,6 +1406,13 @@ function formatCellValue(value) {
|
|
|
1411
1406
|
if (typeof value === "string") return value;
|
|
1412
1407
|
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
1413
1408
|
if (value instanceof Date) return value.toISOString();
|
|
1409
|
+
if (typeof value === "object" && value !== null && "v" in value) {
|
|
1410
|
+
const cell = value;
|
|
1411
|
+
if (cell.w != null) return cell.w;
|
|
1412
|
+
if (cell.v == null) return "";
|
|
1413
|
+
if (typeof cell.v === "string" || typeof cell.v === "number" || typeof cell.v === "boolean") return String(cell.v);
|
|
1414
|
+
return JSON.stringify(cell.v);
|
|
1415
|
+
}
|
|
1414
1416
|
return "";
|
|
1415
1417
|
}
|
|
1416
1418
|
var init_excel_utils = __esm({
|
|
@@ -1420,7 +1422,43 @@ var init_excel_utils = __esm({
|
|
|
1420
1422
|
});
|
|
1421
1423
|
|
|
1422
1424
|
// src/plugins/builtin/xlsx-reader.ts
|
|
1423
|
-
import
|
|
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
|
+
}
|
|
1424
1462
|
var XlsxReader;
|
|
1425
1463
|
var init_xlsx_reader = __esm({
|
|
1426
1464
|
"src/plugins/builtin/xlsx-reader.ts"() {
|
|
@@ -1432,46 +1470,9 @@ var init_xlsx_reader = __esm({
|
|
|
1432
1470
|
canRead(source) {
|
|
1433
1471
|
return /\.xlsx$/i.test(source) || /\.xls$/i.test(source);
|
|
1434
1472
|
}
|
|
1435
|
-
|
|
1436
|
-
const workbook =
|
|
1437
|
-
|
|
1438
|
-
const requirements = [];
|
|
1439
|
-
for (const sheet of workbook.worksheets) {
|
|
1440
|
-
const sheetName = sheet.name;
|
|
1441
|
-
const headers = [];
|
|
1442
|
-
const rows = [];
|
|
1443
|
-
sheet.eachRow((row, rowNumber) => {
|
|
1444
|
-
if (rowNumber === 1) {
|
|
1445
|
-
row.eachCell((cell, colNumber) => {
|
|
1446
|
-
headers[colNumber - 1] = formatCellValue(cell.value);
|
|
1447
|
-
});
|
|
1448
|
-
} else {
|
|
1449
|
-
const obj = {};
|
|
1450
|
-
row.eachCell((cell, colNumber) => {
|
|
1451
|
-
const key = headers[colNumber - 1] ?? `Col${colNumber}`;
|
|
1452
|
-
obj[key] = formatCellValue(cell.value);
|
|
1453
|
-
});
|
|
1454
|
-
rows.push(obj);
|
|
1455
|
-
}
|
|
1456
|
-
});
|
|
1457
|
-
for (let i = 0; i < rows.length; i++) {
|
|
1458
|
-
const row = rows[i];
|
|
1459
|
-
const title = row["Title"] ?? row["title"] ?? row["Name"] ?? row["name"] ?? row["ID"] ?? `Row ${i + 1}`;
|
|
1460
|
-
const description = row["Description"] ?? row["description"] ?? row["Desc"] ?? "";
|
|
1461
|
-
const ac = row["Acceptance Criteria"] ?? row["acceptance"] ?? row["AC"] ?? "";
|
|
1462
|
-
if (description || title) {
|
|
1463
|
-
requirements.push({
|
|
1464
|
-
id: `${sheetName}/${i + 1}`,
|
|
1465
|
-
title: String(title),
|
|
1466
|
-
description: String(description),
|
|
1467
|
-
acceptanceCriteria: ac ? String(ac).split("\n").filter(Boolean) : void 0,
|
|
1468
|
-
rawContent: JSON.stringify(row),
|
|
1469
|
-
confidence: 0.7
|
|
1470
|
-
});
|
|
1471
|
-
}
|
|
1472
|
-
}
|
|
1473
|
-
}
|
|
1474
|
-
return requirements;
|
|
1473
|
+
read(source) {
|
|
1474
|
+
const workbook = readFile(source);
|
|
1475
|
+
return Promise.resolve(parseWorkbook(workbook));
|
|
1475
1476
|
}
|
|
1476
1477
|
};
|
|
1477
1478
|
}
|
|
@@ -1489,21 +1490,35 @@ var init_pdf_reader = __esm({
|
|
|
1489
1490
|
return /\.pdf$/i.test(source);
|
|
1490
1491
|
}
|
|
1491
1492
|
async read(source) {
|
|
1492
|
-
let
|
|
1493
|
+
let pdfjsLib;
|
|
1493
1494
|
try {
|
|
1494
|
-
|
|
1495
|
-
pdfParse = mod.default ?? mod;
|
|
1495
|
+
pdfjsLib = await import("pdfjs-dist/legacy/build/pdf.mjs");
|
|
1496
1496
|
} catch {
|
|
1497
|
-
throw new Error("
|
|
1497
|
+
throw new Error("pdfjs-dist package is required for PDF support. Install: npm i pdfjs-dist");
|
|
1498
1498
|
}
|
|
1499
|
-
const { readFileSync:
|
|
1500
|
-
const buffer =
|
|
1501
|
-
const
|
|
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);
|
|
1514
|
+
}
|
|
1515
|
+
doc.destroy();
|
|
1516
|
+
const text = textParts.join("\n");
|
|
1502
1517
|
return [{
|
|
1503
1518
|
id: source.replace(/\\/g, "/").replace(/\.pdf$/i, ""),
|
|
1504
1519
|
title: source.split("/").pop()?.replace(/\.pdf$/i, "") ?? "Untitled",
|
|
1505
|
-
description:
|
|
1506
|
-
rawContent:
|
|
1520
|
+
description: text,
|
|
1521
|
+
rawContent: text,
|
|
1507
1522
|
confidence: 0.75
|
|
1508
1523
|
}];
|
|
1509
1524
|
}
|
|
@@ -2099,13 +2114,13 @@ var init_image_reader = __esm({
|
|
|
2099
2114
|
return /\.(png|jpe?g|webp)$/i.test(source);
|
|
2100
2115
|
}
|
|
2101
2116
|
async read(source) {
|
|
2102
|
-
const { existsSync:
|
|
2103
|
-
if (!
|
|
2117
|
+
const { existsSync: existsSync31, readFileSync: readFileSync24 } = await import("fs");
|
|
2118
|
+
if (!existsSync31(source)) {
|
|
2104
2119
|
throw new Error(`Image file not found: ${source}`);
|
|
2105
2120
|
}
|
|
2106
2121
|
const ext = source.split(".").pop()?.toLowerCase() ?? "png";
|
|
2107
2122
|
const mimeType = ext === "jpg" ? "image/jpeg" : `image/${ext}`;
|
|
2108
|
-
const buffer =
|
|
2123
|
+
const buffer = readFileSync24(source);
|
|
2109
2124
|
let text;
|
|
2110
2125
|
try {
|
|
2111
2126
|
const { createProvider: createProvider2 } = await Promise.resolve().then(() => (init_provider(), provider_exports));
|
|
@@ -2134,7 +2149,7 @@ var init_image_reader = __esm({
|
|
|
2134
2149
|
});
|
|
2135
2150
|
|
|
2136
2151
|
// src/plugins/builtin/cursor-writer.ts
|
|
2137
|
-
import { writeFileSync as writeFileSync7, mkdirSync as
|
|
2152
|
+
import { writeFileSync as writeFileSync7, mkdirSync as mkdirSync7, existsSync as existsSync11, rmSync } from "fs";
|
|
2138
2153
|
import { join as join14 } from "path";
|
|
2139
2154
|
var CursorWriter;
|
|
2140
2155
|
var init_cursor_writer = __esm({
|
|
@@ -2150,7 +2165,7 @@ var init_cursor_writer = __esm({
|
|
|
2150
2165
|
write(constraints) {
|
|
2151
2166
|
this.projectRoot = constraints.projectRoot;
|
|
2152
2167
|
const rulesDir = join14(constraints.projectRoot, ".cursor", "rules");
|
|
2153
|
-
|
|
2168
|
+
mkdirSync7(rulesDir, { recursive: true });
|
|
2154
2169
|
const frontmatter = [
|
|
2155
2170
|
"---",
|
|
2156
2171
|
"description: SpecLore coding constraints \u2014 auto-generated",
|
|
@@ -2229,7 +2244,7 @@ var init_cursor_writer = __esm({
|
|
|
2229
2244
|
});
|
|
2230
2245
|
|
|
2231
2246
|
// src/plugins/builtin/claude-writer.ts
|
|
2232
|
-
import { writeFileSync as writeFileSync8, mkdirSync as
|
|
2247
|
+
import { writeFileSync as writeFileSync8, mkdirSync as mkdirSync8, existsSync as existsSync12, rmSync as rmSync2 } from "fs";
|
|
2233
2248
|
import { join as join15 } from "path";
|
|
2234
2249
|
var ClaudeWriter;
|
|
2235
2250
|
var init_claude_writer = __esm({
|
|
@@ -2245,7 +2260,7 @@ var init_claude_writer = __esm({
|
|
|
2245
2260
|
write(constraints) {
|
|
2246
2261
|
this.projectRoot = constraints.projectRoot;
|
|
2247
2262
|
const rulesDir = join15(constraints.projectRoot, ".claude", "rules");
|
|
2248
|
-
|
|
2263
|
+
mkdirSync8(rulesDir, { recursive: true });
|
|
2249
2264
|
const content = this.buildMarkdown(constraints);
|
|
2250
2265
|
writeFileSync8(join15(rulesDir, "speclore.md"), content, "utf-8");
|
|
2251
2266
|
return Promise.resolve();
|
|
@@ -2311,7 +2326,7 @@ var init_claude_writer = __esm({
|
|
|
2311
2326
|
});
|
|
2312
2327
|
|
|
2313
2328
|
// src/plugins/builtin/qoder-writer.ts
|
|
2314
|
-
import { writeFileSync as writeFileSync9, mkdirSync as
|
|
2329
|
+
import { writeFileSync as writeFileSync9, mkdirSync as mkdirSync9, existsSync as existsSync13, rmSync as rmSync3 } from "fs";
|
|
2315
2330
|
import { join as join16 } from "path";
|
|
2316
2331
|
var QoderWriter;
|
|
2317
2332
|
var init_qoder_writer = __esm({
|
|
@@ -2327,7 +2342,7 @@ var init_qoder_writer = __esm({
|
|
|
2327
2342
|
write(constraints) {
|
|
2328
2343
|
this.projectRoot = constraints.projectRoot;
|
|
2329
2344
|
const rulesDir = join16(constraints.projectRoot, ".qoder", "rules");
|
|
2330
|
-
|
|
2345
|
+
mkdirSync9(rulesDir, { recursive: true });
|
|
2331
2346
|
const content = this.buildMarkdown(constraints);
|
|
2332
2347
|
writeFileSync9(join16(rulesDir, "speclore.md"), content, "utf-8");
|
|
2333
2348
|
return Promise.resolve();
|
|
@@ -2760,11 +2775,12 @@ var init_markdown_reader = __esm({
|
|
|
2760
2775
|
|
|
2761
2776
|
// src/core/requirement-reader/docx-reader.ts
|
|
2762
2777
|
import { basename as basename3, extname as extname3 } from "path";
|
|
2778
|
+
import { readFileSync as readFileSync10 } from "fs";
|
|
2763
2779
|
import mammoth from "mammoth";
|
|
2764
|
-
async function
|
|
2765
|
-
const result = await mammoth.extractRawText({
|
|
2780
|
+
async function parseDocxBuffer(buffer, idHint = "document") {
|
|
2781
|
+
const result = await mammoth.extractRawText({ buffer });
|
|
2766
2782
|
const content = result.value;
|
|
2767
|
-
const id =
|
|
2783
|
+
const id = idHint.toLowerCase().replace(/[^a-z0-9\u4e00-\u9fff-]/g, "-").replace(/-+/g, "-");
|
|
2768
2784
|
const lines = content.split("\n").filter((l) => l.trim());
|
|
2769
2785
|
const title = lines[0]?.trim() ?? id;
|
|
2770
2786
|
return {
|
|
@@ -2775,6 +2791,11 @@ async function readDocxFile(filePath) {
|
|
|
2775
2791
|
confidence: 0.9
|
|
2776
2792
|
};
|
|
2777
2793
|
}
|
|
2794
|
+
async function readDocxFile(filePath) {
|
|
2795
|
+
const buffer = readFileSync10(filePath);
|
|
2796
|
+
const name = basename3(filePath, extname3(filePath));
|
|
2797
|
+
return parseDocxBuffer(buffer, name);
|
|
2798
|
+
}
|
|
2778
2799
|
var init_docx_reader2 = __esm({
|
|
2779
2800
|
"src/core/requirement-reader/docx-reader.ts"() {
|
|
2780
2801
|
"use strict";
|
|
@@ -2783,44 +2804,55 @@ var init_docx_reader2 = __esm({
|
|
|
2783
2804
|
|
|
2784
2805
|
// src/core/requirement-reader/xlsx-reader.ts
|
|
2785
2806
|
import { basename as basename4, extname as extname4 } from "path";
|
|
2786
|
-
import
|
|
2787
|
-
|
|
2788
|
-
const
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
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, "-");
|
|
2810
|
+
const firstSheetName = workbook.SheetNames[0];
|
|
2811
|
+
if (!firstSheetName) {
|
|
2812
|
+
return Promise.reject(new Error("No sheets found in workbook"));
|
|
2813
|
+
}
|
|
2814
|
+
const sheet = workbook.Sheets[firstSheetName];
|
|
2792
2815
|
if (!sheet) {
|
|
2793
|
-
|
|
2794
|
-
}
|
|
2795
|
-
const title =
|
|
2796
|
-
const
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2816
|
+
return Promise.reject(new Error(`Sheet "${firstSheetName}" not found`));
|
|
2817
|
+
}
|
|
2818
|
+
const title = firstSheetName;
|
|
2819
|
+
const rows = utils2.sheet_to_json(sheet, { header: 1, defval: "" });
|
|
2820
|
+
if (rows.length === 0) {
|
|
2821
|
+
return Promise.resolve({
|
|
2822
|
+
id,
|
|
2823
|
+
title,
|
|
2824
|
+
description: "",
|
|
2825
|
+
rawContent: "",
|
|
2826
|
+
confidence: 0.85
|
|
2827
|
+
});
|
|
2828
|
+
}
|
|
2829
|
+
const headers = rows[0].map((cell) => formatCellValue(cell));
|
|
2830
|
+
const dataRows = rows.slice(1);
|
|
2831
|
+
const objects = dataRows.map((row) => {
|
|
2832
|
+
const obj = {};
|
|
2833
|
+
row.forEach((cell, colNumber) => {
|
|
2834
|
+
const key = headers[colNumber] ?? `Col${colNumber + 1}`;
|
|
2835
|
+
obj[key] = formatCellValue(cell);
|
|
2836
|
+
});
|
|
2837
|
+
return obj;
|
|
2811
2838
|
});
|
|
2812
|
-
const textRows =
|
|
2839
|
+
const textRows = objects.map((row) => {
|
|
2813
2840
|
const cells = Object.entries(row).map(([key, value]) => `${key}: ${value}`).join(" | ");
|
|
2814
2841
|
return cells;
|
|
2815
2842
|
});
|
|
2816
2843
|
const content = textRows.join("\n");
|
|
2817
|
-
return {
|
|
2844
|
+
return Promise.resolve({
|
|
2818
2845
|
id,
|
|
2819
2846
|
title,
|
|
2820
2847
|
description: content,
|
|
2821
2848
|
rawContent: content,
|
|
2822
2849
|
confidence: 0.85
|
|
2823
|
-
};
|
|
2850
|
+
});
|
|
2851
|
+
}
|
|
2852
|
+
function readXlsxFile(filePath) {
|
|
2853
|
+
const workbook = readFile2(filePath);
|
|
2854
|
+
const idHint = basename4(filePath, extname4(filePath));
|
|
2855
|
+
return parseWorkbook2(workbook, idHint);
|
|
2824
2856
|
}
|
|
2825
2857
|
var init_xlsx_reader2 = __esm({
|
|
2826
2858
|
"src/core/requirement-reader/xlsx-reader.ts"() {
|
|
@@ -2831,22 +2863,43 @@ var init_xlsx_reader2 = __esm({
|
|
|
2831
2863
|
|
|
2832
2864
|
// src/core/requirement-reader/pdf-reader.ts
|
|
2833
2865
|
import { basename as basename5, extname as extname5 } from "path";
|
|
2834
|
-
import { readFile } from "fs/promises";
|
|
2835
|
-
async function
|
|
2836
|
-
const
|
|
2837
|
-
const
|
|
2838
|
-
const
|
|
2839
|
-
|
|
2840
|
-
|
|
2866
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
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());
|
|
2841
2889
|
const title = lines[0]?.trim() ?? id;
|
|
2842
2890
|
return {
|
|
2843
2891
|
id,
|
|
2844
2892
|
title,
|
|
2845
|
-
description:
|
|
2846
|
-
rawContent:
|
|
2893
|
+
description: text,
|
|
2894
|
+
rawContent: text,
|
|
2847
2895
|
confidence: 0.8
|
|
2848
2896
|
};
|
|
2849
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
|
+
}
|
|
2850
2903
|
var init_pdf_reader2 = __esm({
|
|
2851
2904
|
"src/core/requirement-reader/pdf-reader.ts"() {
|
|
2852
2905
|
"use strict";
|
|
@@ -2855,14 +2908,14 @@ var init_pdf_reader2 = __esm({
|
|
|
2855
2908
|
|
|
2856
2909
|
// src/core/requirement-reader/image-reader.ts
|
|
2857
2910
|
import { basename as basename6, extname as extname6 } from "path";
|
|
2858
|
-
import { readFileSync as
|
|
2859
|
-
async function readImageFile(filePath) {
|
|
2911
|
+
import { readFileSync as readFileSync11 } from "fs";
|
|
2912
|
+
async function readImageFile(filePath, providerOverride) {
|
|
2860
2913
|
const id = basename6(filePath, extname6(filePath)).toLowerCase().replace(/[^a-z0-9\u4e00-\u9fff-]/g, "-").replace(/-+/g, "-");
|
|
2861
2914
|
logger.info(`Reading image via AI Vision: ${filePath}`);
|
|
2862
2915
|
const ext = extname6(filePath).toLowerCase();
|
|
2863
2916
|
const mimeType = MIME_MAP[ext] ?? "image/png";
|
|
2864
|
-
const buffer =
|
|
2865
|
-
const provider = await createProvider();
|
|
2917
|
+
const buffer = readFileSync11(filePath);
|
|
2918
|
+
const provider = providerOverride ?? await createProvider();
|
|
2866
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.`;
|
|
2867
2920
|
if (!provider.generateWithImage) {
|
|
2868
2921
|
throw new Error(`AI provider '${provider.name}' does not support image/vision input. Use a vision-capable model.`);
|
|
@@ -3307,11 +3360,11 @@ var init_prompt_builder = __esm({
|
|
|
3307
3360
|
});
|
|
3308
3361
|
|
|
3309
3362
|
// src/core/feature-generator/generator.ts
|
|
3310
|
-
import { writeFileSync as writeFileSync10, mkdirSync as
|
|
3363
|
+
import { writeFileSync as writeFileSync10, mkdirSync as mkdirSync10, existsSync as existsSync15 } from "fs";
|
|
3311
3364
|
import { join as join17, dirname as dirname2 } from "path";
|
|
3312
3365
|
import { Parser as Parser2, GherkinClassicTokenMatcher as GherkinClassicTokenMatcher2, AstBuilder as AstBuilder2 } from "@cucumber/gherkin";
|
|
3313
3366
|
import { IdGenerator as IdGenerator2 } from "@cucumber/messages";
|
|
3314
|
-
async function generateFeature(requirement, context, config, projectRoot2) {
|
|
3367
|
+
async function generateFeature(requirement, context, config, projectRoot2, providerOverride) {
|
|
3315
3368
|
logger.info(`Generating feature for: ${requirement.title}`);
|
|
3316
3369
|
const registry = getRegistry();
|
|
3317
3370
|
await registry.invokeLifecycle("beforeSpec", requirement);
|
|
@@ -3319,7 +3372,7 @@ async function generateFeature(requirement, context, config, projectRoot2) {
|
|
|
3319
3372
|
const aiConfig = config.ai;
|
|
3320
3373
|
const fallbackConfigs = aiConfig?.fallbackProviders ?? [];
|
|
3321
3374
|
const providerConfigs = aiConfig ? [aiConfig, ...fallbackConfigs] : [];
|
|
3322
|
-
const provider = providerConfigs.length > 1 ? await createProviderChain(providerConfigs) : await createProvider(aiConfig);
|
|
3375
|
+
const provider = providerOverride ?? (providerConfigs.length > 1 ? await createProviderChain(providerConfigs) : await createProvider(aiConfig));
|
|
3323
3376
|
if (!provider.isAvailable()) {
|
|
3324
3377
|
throw new Error("AI provider not available. Set API key in environment or config.yaml.");
|
|
3325
3378
|
}
|
|
@@ -3376,7 +3429,7 @@ Please fix these issues and regenerate the complete Feature file.`;
|
|
|
3376
3429
|
const moduleDir = inferModule(requirement, context);
|
|
3377
3430
|
const filePath = join17(outputDir, moduleDir, `${requirement.id}.feature`);
|
|
3378
3431
|
if (!existsSync15(dirname2(filePath))) {
|
|
3379
|
-
|
|
3432
|
+
mkdirSync10(dirname2(filePath), { recursive: true });
|
|
3380
3433
|
}
|
|
3381
3434
|
writeFileSync10(filePath, featureContent, "utf-8");
|
|
3382
3435
|
logger.info(`Feature written: ${toPosixPath(filePath)}`);
|
|
@@ -3542,9 +3595,9 @@ function detectAITools2(projectRoot2) {
|
|
|
3542
3595
|
tools.push("cursor");
|
|
3543
3596
|
logger.debug("Detected Cursor (.cursor/)");
|
|
3544
3597
|
}
|
|
3545
|
-
if (existsSync16(join19(projectRoot2, ".claude")) || existsSync16(join19(projectRoot2, "CLAUDE.md"))
|
|
3598
|
+
if (existsSync16(join19(projectRoot2, ".claude")) || existsSync16(join19(projectRoot2, "CLAUDE.md"))) {
|
|
3546
3599
|
tools.push("claude");
|
|
3547
|
-
logger.debug("Detected Claude Code (.claude/ or CLAUDE.md
|
|
3600
|
+
logger.debug("Detected Claude Code (.claude/ or CLAUDE.md)");
|
|
3548
3601
|
}
|
|
3549
3602
|
if (existsSync16(join19(projectRoot2, ".qoder"))) {
|
|
3550
3603
|
tools.push("qoder");
|
|
@@ -3561,7 +3614,7 @@ var init_ai_tool_detector = __esm({
|
|
|
3561
3614
|
});
|
|
3562
3615
|
|
|
3563
3616
|
// src/core/constraint-coder/constraint-writer.ts
|
|
3564
|
-
import { writeFileSync as writeFileSync11, mkdirSync as
|
|
3617
|
+
import { writeFileSync as writeFileSync11, mkdirSync as mkdirSync11, existsSync as existsSync17 } from "fs";
|
|
3565
3618
|
import { join as join20 } from "path";
|
|
3566
3619
|
function writeConstraints(projectRoot2, tools, content) {
|
|
3567
3620
|
const writtenFiles = [];
|
|
@@ -3635,7 +3688,7 @@ function buildConstraintText(content) {
|
|
|
3635
3688
|
}
|
|
3636
3689
|
function writeCursorRule2(projectRoot2, text) {
|
|
3637
3690
|
const dir = join20(projectRoot2, ".cursor", "rules");
|
|
3638
|
-
if (!existsSync17(dir))
|
|
3691
|
+
if (!existsSync17(dir)) mkdirSync11(dir, { recursive: true });
|
|
3639
3692
|
const filePath = join20(dir, "speclore.mdc");
|
|
3640
3693
|
const frontmatter = [
|
|
3641
3694
|
"---",
|
|
@@ -3650,14 +3703,14 @@ function writeCursorRule2(projectRoot2, text) {
|
|
|
3650
3703
|
}
|
|
3651
3704
|
function writeClaudeRule2(projectRoot2, text) {
|
|
3652
3705
|
const dir = join20(projectRoot2, ".claude", "rules");
|
|
3653
|
-
if (!existsSync17(dir))
|
|
3706
|
+
if (!existsSync17(dir)) mkdirSync11(dir, { recursive: true });
|
|
3654
3707
|
const filePath = join20(dir, "speclore.md");
|
|
3655
3708
|
writeFileSync11(filePath, text, "utf-8");
|
|
3656
3709
|
return filePath;
|
|
3657
3710
|
}
|
|
3658
3711
|
function writeQoderRule2(projectRoot2, text) {
|
|
3659
3712
|
const dir = join20(projectRoot2, ".qoder", "rules");
|
|
3660
|
-
if (!existsSync17(dir))
|
|
3713
|
+
if (!existsSync17(dir)) mkdirSync11(dir, { recursive: true });
|
|
3661
3714
|
const filePath = join20(dir, "speclore.md");
|
|
3662
3715
|
writeFileSync11(filePath, text, "utf-8");
|
|
3663
3716
|
return filePath;
|
|
@@ -3737,14 +3790,14 @@ var init_constraint_coder = __esm({
|
|
|
3737
3790
|
init_logger();
|
|
3738
3791
|
init_ai_tool_detector();
|
|
3739
3792
|
init_constraint_writer();
|
|
3740
|
-
MAPPING_INSTRUCTIONS =
|
|
3741
|
-
|
|
3742
|
-
|
|
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.`;
|
|
3743
3796
|
}
|
|
3744
3797
|
});
|
|
3745
3798
|
|
|
3746
3799
|
// src/core/test-scaffolder/framework-detector.ts
|
|
3747
|
-
import { readFileSync as
|
|
3800
|
+
import { readFileSync as readFileSync12, existsSync as existsSync18 } from "fs";
|
|
3748
3801
|
import { join as join21 } from "path";
|
|
3749
3802
|
function detectTestFramework(projectRoot2) {
|
|
3750
3803
|
const pkgPath = join21(projectRoot2, "package.json");
|
|
@@ -3752,7 +3805,7 @@ function detectTestFramework(projectRoot2) {
|
|
|
3752
3805
|
return "vitest";
|
|
3753
3806
|
}
|
|
3754
3807
|
try {
|
|
3755
|
-
const pkg = JSON.parse(
|
|
3808
|
+
const pkg = JSON.parse(readFileSync12(pkgPath, "utf-8"));
|
|
3756
3809
|
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
3757
3810
|
if ("vitest" in deps) return "vitest";
|
|
3758
3811
|
if ("jest" in deps || "@jest/core" in deps) return "jest";
|
|
@@ -3768,7 +3821,7 @@ var init_framework_detector = __esm({
|
|
|
3768
3821
|
});
|
|
3769
3822
|
|
|
3770
3823
|
// src/core/test-scaffolder/index.ts
|
|
3771
|
-
import { readFileSync as
|
|
3824
|
+
import { readFileSync as readFileSync13, writeFileSync as writeFileSync12, existsSync as existsSync19, mkdirSync as mkdirSync12 } from "fs";
|
|
3772
3825
|
import { join as join22, relative as relative3, dirname as dirname3, basename as basename8 } from "path";
|
|
3773
3826
|
function generateTestScaffolding(projectRoot2, features, config) {
|
|
3774
3827
|
const framework = detectTestFramework(projectRoot2);
|
|
@@ -3791,7 +3844,7 @@ function generateTestScaffolding(projectRoot2, features, config) {
|
|
|
3791
3844
|
const content = generateTestFileContent(feature.featureName, scenarios, framework);
|
|
3792
3845
|
const dir = dirname3(absTestPath);
|
|
3793
3846
|
if (!existsSync19(dir)) {
|
|
3794
|
-
|
|
3847
|
+
mkdirSync12(dir, { recursive: true });
|
|
3795
3848
|
}
|
|
3796
3849
|
writeFileSync12(absTestPath, content, "utf-8");
|
|
3797
3850
|
results.push({ testFile: testFilePath, framework, scenarios: scenarios.length });
|
|
@@ -3864,7 +3917,7 @@ function generateTestFileContent(featureName, scenarios, framework) {
|
|
|
3864
3917
|
return lines.join("\n");
|
|
3865
3918
|
}
|
|
3866
3919
|
function appendMissingScenarios(testFilePath, scenarios, _framework) {
|
|
3867
|
-
const content =
|
|
3920
|
+
const content = readFileSync13(testFilePath, "utf-8");
|
|
3868
3921
|
const missing = [];
|
|
3869
3922
|
for (const scenario of scenarios) {
|
|
3870
3923
|
if (!content.includes(`Scenario: ${scenario.name}`)) {
|
|
@@ -3905,7 +3958,7 @@ var init_test_scaffolder = __esm({
|
|
|
3905
3958
|
});
|
|
3906
3959
|
|
|
3907
3960
|
// src/core/verifier/mapping-resolver.ts
|
|
3908
|
-
import { readFileSync as
|
|
3961
|
+
import { readFileSync as readFileSync15, existsSync as existsSync21, readdirSync as readdirSync2 } from "fs";
|
|
3909
3962
|
import { join as join24 } from "path";
|
|
3910
3963
|
function resolveMappings(projectRoot2, features, testOutput) {
|
|
3911
3964
|
const results = [];
|
|
@@ -3935,7 +3988,7 @@ function resolveFromMappingFile(projectRoot2, _feature, scenario) {
|
|
|
3935
3988
|
const files = findMappingFiles(mappingsDir);
|
|
3936
3989
|
for (const file of files) {
|
|
3937
3990
|
try {
|
|
3938
|
-
const content =
|
|
3991
|
+
const content = readFileSync15(file, "utf-8");
|
|
3939
3992
|
const mapping = JSON.parse(content);
|
|
3940
3993
|
if (mapping.scenarios && scenario.name in mapping.scenarios) {
|
|
3941
3994
|
const entry = mapping.scenarios[scenario.name];
|
|
@@ -3965,7 +4018,7 @@ function resolveFromTag(projectRoot2, _feature, scenario) {
|
|
|
3965
4018
|
const files = findTestFiles(testsDir);
|
|
3966
4019
|
for (const file of files) {
|
|
3967
4020
|
try {
|
|
3968
|
-
const content =
|
|
4021
|
+
const content = readFileSync15(file, "utf-8");
|
|
3969
4022
|
const tagRegex = /@speclore-scenario:\s*(.+)/g;
|
|
3970
4023
|
let match;
|
|
3971
4024
|
while ((match = tagRegex.exec(content)) !== null) {
|
|
@@ -4145,13 +4198,13 @@ var report_generator_exports = {};
|
|
|
4145
4198
|
__export(report_generator_exports, {
|
|
4146
4199
|
generateReport: () => generateReport
|
|
4147
4200
|
});
|
|
4148
|
-
import { writeFileSync as writeFileSync13, mkdirSync as
|
|
4201
|
+
import { writeFileSync as writeFileSync13, mkdirSync as mkdirSync13, existsSync as existsSync22, readFileSync as readFileSync16 } from "fs";
|
|
4149
4202
|
import { join as join25, dirname as dirname4 } from "path";
|
|
4150
4203
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
4151
4204
|
function generateReport(report, projectRoot2, config) {
|
|
4152
4205
|
const reportsDir = join25(projectRoot2, ".speclore", "reports");
|
|
4153
4206
|
if (!existsSync22(reportsDir)) {
|
|
4154
|
-
|
|
4207
|
+
mkdirSync13(reportsDir, { recursive: true });
|
|
4155
4208
|
}
|
|
4156
4209
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
4157
4210
|
const writtenFiles = [];
|
|
@@ -4208,7 +4261,7 @@ function renderHtmlReport(report) {
|
|
|
4208
4261
|
const templatePath = join25(dirname4(fileURLToPath2(import.meta.url)), "..", "..", "cli", "templates", "report.html");
|
|
4209
4262
|
let template;
|
|
4210
4263
|
try {
|
|
4211
|
-
template =
|
|
4264
|
+
template = readFileSync16(templatePath, "utf-8");
|
|
4212
4265
|
} catch {
|
|
4213
4266
|
template = getInlineTemplate();
|
|
4214
4267
|
}
|
|
@@ -4287,7 +4340,7 @@ var init_verifier = __esm({
|
|
|
4287
4340
|
});
|
|
4288
4341
|
|
|
4289
4342
|
// src/core/analyzer/rdg-builder.ts
|
|
4290
|
-
import { readFileSync as
|
|
4343
|
+
import { readFileSync as readFileSync17, existsSync as existsSync23, readdirSync as readdirSync3 } from "fs";
|
|
4291
4344
|
import { join as join26 } from "path";
|
|
4292
4345
|
var init_rdg_builder = __esm({
|
|
4293
4346
|
"src/core/analyzer/rdg-builder.ts"() {
|
|
@@ -4317,6 +4370,9 @@ import { execFileSync as execFileSync3 } from "child_process";
|
|
|
4317
4370
|
import { globSync as globSync5 } from "glob";
|
|
4318
4371
|
function analyzeImpact(projectRoot2, context, config) {
|
|
4319
4372
|
const changedFiles = getChangedFiles(projectRoot2);
|
|
4373
|
+
return analyzeImpactWithChanges(changedFiles, context, config, projectRoot2);
|
|
4374
|
+
}
|
|
4375
|
+
function analyzeImpactWithChanges(changedFiles, context, config, projectRoot2) {
|
|
4320
4376
|
const affectedModules = determineAffectedModules(changedFiles, context);
|
|
4321
4377
|
const affectedFeatures = determineAffectedFeatures(affectedModules, projectRoot2, config.spec.outputDir);
|
|
4322
4378
|
logger.info(
|
|
@@ -4392,11 +4448,11 @@ var init_analyzer = __esm({
|
|
|
4392
4448
|
});
|
|
4393
4449
|
|
|
4394
4450
|
// src/infra/file-lock.ts
|
|
4395
|
-
import { readFileSync as
|
|
4396
|
-
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";
|
|
4397
4453
|
function acquireLock(specLoreDir) {
|
|
4398
|
-
const lockPath =
|
|
4399
|
-
if (
|
|
4454
|
+
const lockPath = join31(specLoreDir, LOCK_FILENAME);
|
|
4455
|
+
if (existsSync28(lockPath)) {
|
|
4400
4456
|
const existing = readLockInfo(lockPath);
|
|
4401
4457
|
if (existing && !isLockExpired(existing)) {
|
|
4402
4458
|
logger.debug(`Lock already held by PID ${existing.pid}, not expired`);
|
|
@@ -4410,7 +4466,7 @@ function acquireLock(specLoreDir) {
|
|
|
4410
4466
|
timestamp: Date.now()
|
|
4411
4467
|
};
|
|
4412
4468
|
try {
|
|
4413
|
-
|
|
4469
|
+
writeFileSync16(lockPath, JSON.stringify(lockInfo), { flag: "wx", encoding: "utf-8" });
|
|
4414
4470
|
} catch {
|
|
4415
4471
|
logger.debug("Lock write failed \u2014 another process may have acquired it");
|
|
4416
4472
|
return false;
|
|
@@ -4419,8 +4475,8 @@ function acquireLock(specLoreDir) {
|
|
|
4419
4475
|
return true;
|
|
4420
4476
|
}
|
|
4421
4477
|
function releaseLock(specLoreDir) {
|
|
4422
|
-
const lockPath =
|
|
4423
|
-
if (!
|
|
4478
|
+
const lockPath = join31(specLoreDir, LOCK_FILENAME);
|
|
4479
|
+
if (!existsSync28(lockPath)) {
|
|
4424
4480
|
return;
|
|
4425
4481
|
}
|
|
4426
4482
|
const existing = readLockInfo(lockPath);
|
|
@@ -4431,7 +4487,7 @@ function releaseLock(specLoreDir) {
|
|
|
4431
4487
|
}
|
|
4432
4488
|
function readLockInfo(lockPath) {
|
|
4433
4489
|
try {
|
|
4434
|
-
const content =
|
|
4490
|
+
const content = readFileSync21(lockPath, "utf-8");
|
|
4435
4491
|
const parsed = JSON.parse(content);
|
|
4436
4492
|
if (typeof parsed.pid === "number" && typeof parsed.timestamp === "number") {
|
|
4437
4493
|
return parsed;
|
|
@@ -4501,8 +4557,8 @@ var init_schemas = __esm({
|
|
|
4501
4557
|
});
|
|
4502
4558
|
|
|
4503
4559
|
// src/mcp/tools.ts
|
|
4504
|
-
import { join as
|
|
4505
|
-
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";
|
|
4506
4562
|
import { globSync as globSync7 } from "glob";
|
|
4507
4563
|
import { toJSONSchema } from "zod";
|
|
4508
4564
|
async function executeSpecTool(args, projectRoot2) {
|
|
@@ -4513,7 +4569,7 @@ async function executeSpecTool(args, projectRoot2) {
|
|
|
4513
4569
|
const config = loadConfig(projectRoot2);
|
|
4514
4570
|
logger.info(`Reading requirement from: ${args.source.slice(0, 80)}...`);
|
|
4515
4571
|
const req = await readRequirement(args.source);
|
|
4516
|
-
const specLoreDir =
|
|
4572
|
+
const specLoreDir = join32(projectRoot2, ".speclore");
|
|
4517
4573
|
const context = loadContext(specLoreDir) ?? buildContext(projectRoot2, config);
|
|
4518
4574
|
const feature = await generateFeature(req, context, config, projectRoot2);
|
|
4519
4575
|
const createdFiles = [feature.path];
|
|
@@ -4545,7 +4601,7 @@ async function executeCodeTool(args, projectRoot2) {
|
|
|
4545
4601
|
}
|
|
4546
4602
|
ensureProjectReady(projectRoot2);
|
|
4547
4603
|
const config = loadConfig(projectRoot2);
|
|
4548
|
-
const specLoreDir =
|
|
4604
|
+
const specLoreDir = join32(projectRoot2, ".speclore");
|
|
4549
4605
|
const context = loadContext(specLoreDir) ?? buildContext(projectRoot2, config);
|
|
4550
4606
|
const featureFiles = resolveFeatureFiles(args.features, projectRoot2, config);
|
|
4551
4607
|
if (featureFiles.length === 0) {
|
|
@@ -4598,7 +4654,7 @@ async function executeCodeTool(args, projectRoot2) {
|
|
|
4598
4654
|
async function executeVerifyTool(args, projectRoot2) {
|
|
4599
4655
|
ensureProjectReady(projectRoot2);
|
|
4600
4656
|
const config = loadConfig(projectRoot2);
|
|
4601
|
-
const specLoreDir =
|
|
4657
|
+
const specLoreDir = join32(projectRoot2, ".speclore");
|
|
4602
4658
|
const context = loadContext(specLoreDir) ?? buildContext(projectRoot2, config);
|
|
4603
4659
|
const featureFiles = resolveFeatureFiles(args.features, projectRoot2, config);
|
|
4604
4660
|
const stateManager = new StateManager(projectRoot2);
|
|
@@ -4660,22 +4716,22 @@ async function executeVerifyTool(args, projectRoot2) {
|
|
|
4660
4716
|
};
|
|
4661
4717
|
}
|
|
4662
4718
|
function ensureProjectReady(projectRoot2) {
|
|
4663
|
-
const specLoreDir =
|
|
4664
|
-
const configPath =
|
|
4719
|
+
const specLoreDir = join32(projectRoot2, ".speclore");
|
|
4720
|
+
const configPath = join32(specLoreDir, "config.yaml");
|
|
4665
4721
|
let configCreated = false;
|
|
4666
|
-
if (!
|
|
4667
|
-
|
|
4722
|
+
if (!existsSync29(specLoreDir)) {
|
|
4723
|
+
mkdirSync14(specLoreDir, { recursive: true });
|
|
4668
4724
|
}
|
|
4669
|
-
if (!
|
|
4725
|
+
if (!existsSync29(configPath)) {
|
|
4670
4726
|
const projectName = projectRoot2.split(/[/\\]/).pop() ?? "my-project";
|
|
4671
|
-
|
|
4727
|
+
writeFileSync17(configPath, generateDefaultConfigYaml(projectName), "utf-8");
|
|
4672
4728
|
configCreated = true;
|
|
4673
4729
|
logger.info(`Auto-created default config: ${configPath}`);
|
|
4674
4730
|
}
|
|
4675
4731
|
const stateManager = new StateManager(projectRoot2);
|
|
4676
4732
|
stateManager.ensureInitialized();
|
|
4677
4733
|
const config = loadConfig(projectRoot2);
|
|
4678
|
-
const specsDir =
|
|
4734
|
+
const specsDir = join32(projectRoot2, config.spec.outputDir);
|
|
4679
4735
|
const migrated = stateManager.migrateFeatures(specsDir);
|
|
4680
4736
|
if (migrated > 0) {
|
|
4681
4737
|
logger.info(`Migrated ${migrated} existing .feature file(s) into state tracking.`);
|
|
@@ -4712,14 +4768,14 @@ function resolveNextStep(currentState, _summary) {
|
|
|
4712
4768
|
}
|
|
4713
4769
|
}
|
|
4714
4770
|
function resolveFeatureFiles(patterns, projectRoot2, config) {
|
|
4715
|
-
const specsDir =
|
|
4771
|
+
const specsDir = join32(projectRoot2, config.spec.outputDir);
|
|
4716
4772
|
const searchPatterns = patterns && patterns.length > 0 ? patterns : [`${specsDir}/**/*.feature`];
|
|
4717
4773
|
const files = [];
|
|
4718
4774
|
for (const pattern of searchPatterns) {
|
|
4719
4775
|
const matches = globSync7(pattern, { cwd: projectRoot2, absolute: true });
|
|
4720
4776
|
for (const filePath of matches) {
|
|
4721
|
-
if (
|
|
4722
|
-
const content =
|
|
4777
|
+
if (existsSync29(filePath)) {
|
|
4778
|
+
const content = readFileSync22(filePath, "utf-8");
|
|
4723
4779
|
files.push(parseFeatureFile(filePath, content));
|
|
4724
4780
|
}
|
|
4725
4781
|
}
|
|
@@ -4827,8 +4883,8 @@ var init_tools = __esm({
|
|
|
4827
4883
|
});
|
|
4828
4884
|
|
|
4829
4885
|
// src/mcp/status.ts
|
|
4830
|
-
import { readFileSync as
|
|
4831
|
-
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";
|
|
4832
4888
|
import { globSync as globSync8 } from "glob";
|
|
4833
4889
|
function executeStatusTool(args, projectRoot2) {
|
|
4834
4890
|
const { configCreated } = ensureProjectReadyForStatus(projectRoot2);
|
|
@@ -4844,9 +4900,9 @@ function executeStatusTool(args, projectRoot2) {
|
|
|
4844
4900
|
continue;
|
|
4845
4901
|
}
|
|
4846
4902
|
let scenarioCount = 0;
|
|
4847
|
-
if (
|
|
4903
|
+
if (existsSync30(path)) {
|
|
4848
4904
|
try {
|
|
4849
|
-
const content =
|
|
4905
|
+
const content = readFileSync23(path, "utf-8");
|
|
4850
4906
|
const matches = content.match(/Scenario(?: Outline)?:/g);
|
|
4851
4907
|
scenarioCount = matches?.length ?? 0;
|
|
4852
4908
|
} catch {
|
|
@@ -4861,7 +4917,7 @@ function executeStatusTool(args, projectRoot2) {
|
|
|
4861
4917
|
lastVerify: entry.lastVerify ? { passed: entry.lastVerify.passed, failed: entry.lastVerify.failed, timestamp: entry.lastVerify.timestamp } : void 0
|
|
4862
4918
|
});
|
|
4863
4919
|
}
|
|
4864
|
-
const specsDir =
|
|
4920
|
+
const specsDir = join33(projectRoot2, config.spec.outputDir);
|
|
4865
4921
|
const allFeatureFiles = globSync8(`${specsDir}/**/*.feature`, { cwd: projectRoot2, absolute: true });
|
|
4866
4922
|
const trackedPaths = new Set(featureEntries.map((f) => f.path));
|
|
4867
4923
|
for (const filePath of allFeatureFiles) {
|
|
@@ -4869,7 +4925,7 @@ function executeStatusTool(args, projectRoot2) {
|
|
|
4869
4925
|
if (args.feature && !filePath.includes(args.feature)) continue;
|
|
4870
4926
|
let scenarioCount = 0;
|
|
4871
4927
|
try {
|
|
4872
|
-
const content =
|
|
4928
|
+
const content = readFileSync23(filePath, "utf-8");
|
|
4873
4929
|
const matches = content.match(/Scenario(?: Outline)?:/g);
|
|
4874
4930
|
scenarioCount = matches?.length ?? 0;
|
|
4875
4931
|
} catch {
|
|
@@ -4926,22 +4982,22 @@ function buildRecommendedActions(features, summary, testCommand) {
|
|
|
4926
4982
|
return actions;
|
|
4927
4983
|
}
|
|
4928
4984
|
function ensureProjectReadyForStatus(projectRoot2) {
|
|
4929
|
-
const specLoreDir =
|
|
4930
|
-
const configPath =
|
|
4985
|
+
const specLoreDir = join33(projectRoot2, ".speclore");
|
|
4986
|
+
const configPath = join33(specLoreDir, "config.yaml");
|
|
4931
4987
|
let configCreated = false;
|
|
4932
|
-
if (!
|
|
4933
|
-
|
|
4988
|
+
if (!existsSync30(specLoreDir)) {
|
|
4989
|
+
mkdirSync15(specLoreDir, { recursive: true });
|
|
4934
4990
|
}
|
|
4935
|
-
if (!
|
|
4991
|
+
if (!existsSync30(configPath)) {
|
|
4936
4992
|
const projectName = projectRoot2.split(/[/\\]/).pop() ?? "my-project";
|
|
4937
|
-
|
|
4993
|
+
writeFileSync18(configPath, generateDefaultConfigYaml(projectName), "utf-8");
|
|
4938
4994
|
configCreated = true;
|
|
4939
4995
|
logger.info(`Auto-created default config: ${configPath}`);
|
|
4940
4996
|
}
|
|
4941
4997
|
const stateManager = new StateManager(projectRoot2);
|
|
4942
4998
|
stateManager.ensureInitialized();
|
|
4943
4999
|
const config = loadConfig(projectRoot2);
|
|
4944
|
-
const specsDir =
|
|
5000
|
+
const specsDir = join33(projectRoot2, config.spec.outputDir);
|
|
4945
5001
|
const migrated = stateManager.migrateFeatures(specsDir);
|
|
4946
5002
|
if (migrated > 0) {
|
|
4947
5003
|
logger.info(`Migrated ${migrated} existing .feature file(s) into state tracking.`);
|
|
@@ -4965,7 +5021,7 @@ __export(server_exports, {
|
|
|
4965
5021
|
});
|
|
4966
5022
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4967
5023
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4968
|
-
import { mkdirSync as
|
|
5024
|
+
import { mkdirSync as mkdirSync16 } from "fs";
|
|
4969
5025
|
function getProjectRoot() {
|
|
4970
5026
|
return projectRoot;
|
|
4971
5027
|
}
|
|
@@ -4973,7 +5029,7 @@ async function startMcpServer() {
|
|
|
4973
5029
|
projectRoot = process.env.SPECLORE_PROJECT_ROOT ?? process.cwd();
|
|
4974
5030
|
const specLoreDir = `${projectRoot}/.speclore`;
|
|
4975
5031
|
try {
|
|
4976
|
-
|
|
5032
|
+
mkdirSync16(specLoreDir, { recursive: true });
|
|
4977
5033
|
const locked = acquireLock(specLoreDir);
|
|
4978
5034
|
if (!locked) {
|
|
4979
5035
|
logger.warn("Could not acquire lock \u2014 another SpecLore instance may be running.");
|
|
@@ -5086,11 +5142,11 @@ import { Command } from "commander";
|
|
|
5086
5142
|
init_logger();
|
|
5087
5143
|
init_config2();
|
|
5088
5144
|
init_detector();
|
|
5089
|
-
import { existsSync as existsSync5, mkdirSync as
|
|
5145
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
5090
5146
|
import { join as join7 } from "path";
|
|
5091
5147
|
|
|
5092
5148
|
// src/setup/config-writer.ts
|
|
5093
|
-
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";
|
|
5094
5150
|
import { join as join5 } from "path";
|
|
5095
5151
|
|
|
5096
5152
|
// src/infra/install-detector.ts
|
|
@@ -5141,13 +5197,42 @@ function detectCloneInstall(projectRoot2) {
|
|
|
5141
5197
|
|
|
5142
5198
|
// src/setup/config-writer.ts
|
|
5143
5199
|
init_logger();
|
|
5144
|
-
function writeMcpConfig(projectRoot2, _globalMode) {
|
|
5200
|
+
function writeMcpConfig(projectRoot2, tools, _globalMode) {
|
|
5145
5201
|
const installInfo = detectInstallMethod(projectRoot2);
|
|
5146
5202
|
const serverCommand = getServerCommand(installInfo);
|
|
5147
|
-
|
|
5148
|
-
|
|
5149
|
-
|
|
5150
|
-
|
|
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) {
|
|
5218
|
+
const installInfo = detectInstallMethod(projectRoot2);
|
|
5219
|
+
const serverCommand = getServerCommand(installInfo);
|
|
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
|
+
}
|
|
5151
5236
|
}
|
|
5152
5237
|
function getServerCommand(installInfo) {
|
|
5153
5238
|
if (installInfo.mode === "npm") {
|
|
@@ -5171,6 +5256,9 @@ function writeCursorMcp(projectRoot2, serverCmd) {
|
|
|
5171
5256
|
logger.info(` Cursor: ${mcpPath}`);
|
|
5172
5257
|
}
|
|
5173
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;
|
|
5174
5262
|
const mcpPath = join5(projectRoot2, ".mcp.json");
|
|
5175
5263
|
const existing = readExistingJson(mcpPath);
|
|
5176
5264
|
if (!existing.mcpServers) existing.mcpServers = {};
|
|
@@ -5209,7 +5297,7 @@ function readExistingJson(filePath) {
|
|
|
5209
5297
|
|
|
5210
5298
|
// src/setup/rule-writer.ts
|
|
5211
5299
|
init_logger();
|
|
5212
|
-
import { writeFileSync as writeFileSync3, mkdirSync as
|
|
5300
|
+
import { writeFileSync as writeFileSync3, mkdirSync as mkdirSync3 } from "fs";
|
|
5213
5301
|
import { join as join6 } from "path";
|
|
5214
5302
|
function writeRuleFiles(projectRoot2, tools) {
|
|
5215
5303
|
for (const tool of tools) {
|
|
@@ -5228,7 +5316,7 @@ function writeRuleFiles(projectRoot2, tools) {
|
|
|
5228
5316
|
}
|
|
5229
5317
|
function writeCursorRule(projectRoot2) {
|
|
5230
5318
|
const rulesDir = join6(projectRoot2, ".cursor", "rules");
|
|
5231
|
-
|
|
5319
|
+
mkdirSync3(rulesDir, { recursive: true });
|
|
5232
5320
|
const content = `---
|
|
5233
5321
|
description: SpecLore \u2014 AI coding constraints (setup placeholder)
|
|
5234
5322
|
globs:
|
|
@@ -5250,7 +5338,7 @@ SpecLore is configured for this project. When the \`speclore.spec\` MCP tool is
|
|
|
5250
5338
|
}
|
|
5251
5339
|
function writeClaudeRule(projectRoot2) {
|
|
5252
5340
|
const rulesDir = join6(projectRoot2, ".claude", "rules");
|
|
5253
|
-
|
|
5341
|
+
mkdirSync3(rulesDir, { recursive: true });
|
|
5254
5342
|
const content = `# SpecLore
|
|
5255
5343
|
|
|
5256
5344
|
SpecLore is configured for this project. When the \`speclore.spec\` MCP tool is available:
|
|
@@ -5266,7 +5354,7 @@ SpecLore is configured for this project. When the \`speclore.spec\` MCP tool is
|
|
|
5266
5354
|
}
|
|
5267
5355
|
function writeQoderRule(projectRoot2) {
|
|
5268
5356
|
const rulesDir = join6(projectRoot2, ".qoder", "rules");
|
|
5269
|
-
|
|
5357
|
+
mkdirSync3(rulesDir, { recursive: true });
|
|
5270
5358
|
const content = `# SpecLore
|
|
5271
5359
|
|
|
5272
5360
|
SpecLore is configured for this project. When the \`speclore.spec\` MCP tool is available:
|
|
@@ -5290,13 +5378,20 @@ function registerSetupCommand(program) {
|
|
|
5290
5378
|
logger.info("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
5291
5379
|
const tools = detectAITools(projectRoot2);
|
|
5292
5380
|
if (tools.length === 0) {
|
|
5293
|
-
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("");
|
|
5294
5389
|
} else {
|
|
5295
5390
|
logger.info(`Detected AI tools: ${tools.map((t) => t.tool).join(", ")}`);
|
|
5296
5391
|
}
|
|
5297
5392
|
const specLoreDir = opts.global ? join7(process.env.HOME ?? process.env.USERPROFILE ?? "~", ".speclore") : join7(projectRoot2, ".speclore");
|
|
5298
5393
|
if (!existsSync5(specLoreDir)) {
|
|
5299
|
-
|
|
5394
|
+
mkdirSync4(specLoreDir, { recursive: true });
|
|
5300
5395
|
}
|
|
5301
5396
|
const configPath = join7(specLoreDir, "config.yaml");
|
|
5302
5397
|
if (!existsSync5(configPath)) {
|
|
@@ -5304,12 +5399,23 @@ function registerSetupCommand(program) {
|
|
|
5304
5399
|
writeFileSync4(configPath, generateDefaultConfigYaml(projectName), "utf-8");
|
|
5305
5400
|
logger.info(`Created config: ${configPath}`);
|
|
5306
5401
|
}
|
|
5307
|
-
writeMcpConfig(projectRoot2, opts.global ?? false);
|
|
5402
|
+
writeMcpConfig(projectRoot2, tools, opts.global ?? false);
|
|
5308
5403
|
writeRuleFiles(projectRoot2, tools);
|
|
5309
5404
|
logger.info("");
|
|
5310
|
-
logger.info("Setup complete!
|
|
5311
|
-
logger.info("
|
|
5312
|
-
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)");
|
|
5313
5419
|
});
|
|
5314
5420
|
}
|
|
5315
5421
|
|
|
@@ -5319,7 +5425,7 @@ init_logger();
|
|
|
5319
5425
|
init_context_engine();
|
|
5320
5426
|
import { join as join11 } from "path";
|
|
5321
5427
|
function registerInitCommand(program) {
|
|
5322
|
-
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) => {
|
|
5323
5429
|
if (opts.verbose) logger.setLevel("debug");
|
|
5324
5430
|
const projectRoot2 = process.cwd();
|
|
5325
5431
|
logger.info("SpecLore Init");
|
|
@@ -5383,7 +5489,7 @@ init_test_scaffolder();
|
|
|
5383
5489
|
init_context_engine();
|
|
5384
5490
|
init_state_manager();
|
|
5385
5491
|
import { join as join23 } from "path";
|
|
5386
|
-
import { readFileSync as
|
|
5492
|
+
import { readFileSync as readFileSync14, existsSync as existsSync20 } from "fs";
|
|
5387
5493
|
import { globSync as globSync4 } from "glob";
|
|
5388
5494
|
function registerCodeCommand(program) {
|
|
5389
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) => {
|
|
@@ -5439,7 +5545,7 @@ function resolveFeatures(patterns, projectRoot2, config) {
|
|
|
5439
5545
|
const matches = globSync4(pattern, { cwd: projectRoot2, absolute: true });
|
|
5440
5546
|
for (const filePath of matches) {
|
|
5441
5547
|
if (existsSync20(filePath)) {
|
|
5442
|
-
const content =
|
|
5548
|
+
const content = readFileSync14(filePath, "utf-8");
|
|
5443
5549
|
const featureMatch = content.match(/Feature:\s*(.+)/);
|
|
5444
5550
|
files.push({
|
|
5445
5551
|
path: filePath,
|
|
@@ -5463,7 +5569,7 @@ init_context_engine();
|
|
|
5463
5569
|
init_analyzer();
|
|
5464
5570
|
init_state_manager();
|
|
5465
5571
|
import { join as join27 } from "path";
|
|
5466
|
-
import { readFileSync as
|
|
5572
|
+
import { readFileSync as readFileSync18, existsSync as existsSync24 } from "fs";
|
|
5467
5573
|
import { globSync as globSync6 } from "glob";
|
|
5468
5574
|
function registerVerifyCommand(program) {
|
|
5469
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) => {
|
|
@@ -5550,7 +5656,7 @@ function resolveFeatures2(patterns, projectRoot2, config) {
|
|
|
5550
5656
|
const matches = globSync6(pattern, { cwd: projectRoot2, absolute: true });
|
|
5551
5657
|
for (const filePath of matches) {
|
|
5552
5658
|
if (existsSync24(filePath)) {
|
|
5553
|
-
const content =
|
|
5659
|
+
const content = readFileSync18(filePath, "utf-8");
|
|
5554
5660
|
const featureMatch = content.match(/Feature:\s*(.+)/);
|
|
5555
5661
|
files.push({
|
|
5556
5662
|
path: filePath,
|
|
@@ -5571,7 +5677,7 @@ init_logger();
|
|
|
5571
5677
|
|
|
5572
5678
|
// src/setup/cleanup.ts
|
|
5573
5679
|
init_logger();
|
|
5574
|
-
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";
|
|
5575
5681
|
import { join as join28 } from "path";
|
|
5576
5682
|
function runTeardown(projectRoot2, globalMode) {
|
|
5577
5683
|
if (globalMode) {
|
|
@@ -5600,7 +5706,7 @@ function runTeardown(projectRoot2, globalMode) {
|
|
|
5600
5706
|
function removeMcpEntry(mcpPath, serverName) {
|
|
5601
5707
|
if (!existsSync25(mcpPath)) return;
|
|
5602
5708
|
try {
|
|
5603
|
-
const config = JSON.parse(
|
|
5709
|
+
const config = JSON.parse(readFileSync19(mcpPath, "utf-8"));
|
|
5604
5710
|
if (config.mcpServers?.[serverName]) {
|
|
5605
5711
|
delete config.mcpServers[serverName];
|
|
5606
5712
|
writeFileSync14(mcpPath, JSON.stringify(config, null, 2), "utf-8");
|
|
@@ -5696,6 +5802,114 @@ function registerMigrateCommand(program) {
|
|
|
5696
5802
|
});
|
|
5697
5803
|
}
|
|
5698
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
|
+
|
|
5699
5913
|
// src/cli/index.ts
|
|
5700
5914
|
init_version();
|
|
5701
5915
|
function createProgram() {
|
|
@@ -5715,6 +5929,7 @@ function createProgram() {
|
|
|
5715
5929
|
const { startMcpServer: startMcpServer2 } = await Promise.resolve().then(() => (init_server(), server_exports));
|
|
5716
5930
|
await startMcpServer2();
|
|
5717
5931
|
});
|
|
5932
|
+
registerMcpConfigCommands(program);
|
|
5718
5933
|
program.argument("[text...]", "Quick requirement text \u2192 .feature").action(async (text) => {
|
|
5719
5934
|
if (text.length > 0) {
|
|
5720
5935
|
const { loadConfig: loadConfig2 } = await Promise.resolve().then(() => (init_config2(), config_exports));
|