speclore 0.1.8 → 0.1.10
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 +1 -1
- package/README.md +1 -1
- package/dist/cli/index.js +379 -227
- package/dist/cli/index.js.map +1 -1
- package/dist/index.js +379 -227
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +266 -136
- package/dist/mcp/server.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -308,30 +308,51 @@ var init_config2 = __esm({
|
|
|
308
308
|
}
|
|
309
309
|
});
|
|
310
310
|
|
|
311
|
-
// src/
|
|
311
|
+
// src/infra/path-utils.ts
|
|
312
|
+
import { posix, sep, relative, resolve, isAbsolute, join as join2 } from "path";
|
|
312
313
|
import { existsSync as existsSync2 } from "fs";
|
|
313
|
-
import {
|
|
314
|
+
import { glob, globSync } from "glob";
|
|
315
|
+
function toPosixPath(p) {
|
|
316
|
+
return p.split(/[/\\]/).join(posix.sep);
|
|
317
|
+
}
|
|
318
|
+
function resolveQoderDir(projectRoot2) {
|
|
319
|
+
if (existsSync2(join2(projectRoot2, ".qoder-cn"))) return ".qoder-cn";
|
|
320
|
+
return ".qoder";
|
|
321
|
+
}
|
|
322
|
+
function hasQoderDir(projectRoot2) {
|
|
323
|
+
return existsSync2(join2(projectRoot2, ".qoder")) || existsSync2(join2(projectRoot2, ".qoder-cn"));
|
|
324
|
+
}
|
|
325
|
+
var init_path_utils = __esm({
|
|
326
|
+
"src/infra/path-utils.ts"() {
|
|
327
|
+
"use strict";
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
// src/setup/detector.ts
|
|
332
|
+
import { existsSync as existsSync3 } from "fs";
|
|
333
|
+
import { join as join3 } from "path";
|
|
314
334
|
function detectAITools(projectRoot2) {
|
|
315
335
|
const tools = [];
|
|
316
336
|
const cursorFiles = [".cursor/mcp.json", ".cursor/rules"];
|
|
317
|
-
const cursorConfigs = cursorFiles.filter((f) =>
|
|
337
|
+
const cursorConfigs = cursorFiles.filter((f) => existsSync3(join3(projectRoot2, f)));
|
|
318
338
|
tools.push({
|
|
319
339
|
tool: "cursor",
|
|
320
|
-
detected:
|
|
340
|
+
detected: existsSync3(join3(projectRoot2, ".cursor")),
|
|
321
341
|
configFiles: cursorConfigs
|
|
322
342
|
});
|
|
323
343
|
const claudeFiles = [".claude/", "CLAUDE.md"];
|
|
324
|
-
const claudeConfigs = claudeFiles.filter((f) =>
|
|
344
|
+
const claudeConfigs = claudeFiles.filter((f) => existsSync3(join3(projectRoot2, f)));
|
|
325
345
|
tools.push({
|
|
326
346
|
tool: "claude",
|
|
327
347
|
detected: claudeConfigs.length > 0,
|
|
328
348
|
configFiles: claudeConfigs
|
|
329
349
|
});
|
|
330
|
-
const
|
|
331
|
-
const
|
|
350
|
+
const qoderDir = hasQoderDir(projectRoot2) ? existsSync3(join3(projectRoot2, ".qoder-cn")) ? ".qoder-cn" : ".qoder" : ".qoder";
|
|
351
|
+
const qoderFiles = [`${qoderDir}/mcp.json`, `${qoderDir}/rules`];
|
|
352
|
+
const qoderConfigs = qoderFiles.filter((f) => existsSync3(join3(projectRoot2, f)));
|
|
332
353
|
tools.push({
|
|
333
354
|
tool: "qoder",
|
|
334
|
-
detected:
|
|
355
|
+
detected: hasQoderDir(projectRoot2),
|
|
335
356
|
configFiles: qoderConfigs
|
|
336
357
|
});
|
|
337
358
|
return tools.filter((t) => t.detected);
|
|
@@ -339,23 +360,24 @@ function detectAITools(projectRoot2) {
|
|
|
339
360
|
var init_detector = __esm({
|
|
340
361
|
"src/setup/detector.ts"() {
|
|
341
362
|
"use strict";
|
|
363
|
+
init_path_utils();
|
|
342
364
|
}
|
|
343
365
|
});
|
|
344
366
|
|
|
345
367
|
// src/version.ts
|
|
346
368
|
import { readFileSync as readFileSync2 } from "fs";
|
|
347
369
|
import { fileURLToPath } from "url";
|
|
348
|
-
import { join as
|
|
370
|
+
import { join as join4, dirname } from "path";
|
|
349
371
|
function readVersion() {
|
|
350
372
|
let currentDir = dirname(fileURLToPath(import.meta.url));
|
|
351
373
|
for (let depth = 0; depth < 4; depth++) {
|
|
352
374
|
try {
|
|
353
|
-
const pkgPath =
|
|
375
|
+
const pkgPath = join4(currentDir, "package.json");
|
|
354
376
|
const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
|
|
355
377
|
if (typeof pkg.version === "string") return pkg.version;
|
|
356
378
|
} catch {
|
|
357
379
|
}
|
|
358
|
-
currentDir =
|
|
380
|
+
currentDir = join4(currentDir, "..");
|
|
359
381
|
}
|
|
360
382
|
return "0.0.0";
|
|
361
383
|
}
|
|
@@ -371,7 +393,7 @@ var init_version = __esm({
|
|
|
371
393
|
function readModuleConfig(config) {
|
|
372
394
|
const boundaries = [];
|
|
373
395
|
for (const [name, mod] of Object.entries(config.project.modules)) {
|
|
374
|
-
logger.debug(`Reading module config: ${name}
|
|
396
|
+
logger.debug(`Reading module config: ${name} -> ${mod.path}`);
|
|
375
397
|
boundaries.push({
|
|
376
398
|
name,
|
|
377
399
|
responsibility: mod.responsibility,
|
|
@@ -389,20 +411,8 @@ var init_config_reader = __esm({
|
|
|
389
411
|
}
|
|
390
412
|
});
|
|
391
413
|
|
|
392
|
-
// src/infra/path-utils.ts
|
|
393
|
-
import { posix, sep, relative, resolve, isAbsolute, join as join8 } from "path";
|
|
394
|
-
import { glob, globSync } from "glob";
|
|
395
|
-
function toPosixPath(p) {
|
|
396
|
-
return p.split(/[/\\]/).join(posix.sep);
|
|
397
|
-
}
|
|
398
|
-
var init_path_utils = __esm({
|
|
399
|
-
"src/infra/path-utils.ts"() {
|
|
400
|
-
"use strict";
|
|
401
|
-
}
|
|
402
|
-
});
|
|
403
|
-
|
|
404
414
|
// src/core/context-engine/graph-builder.ts
|
|
405
|
-
import { existsSync as
|
|
415
|
+
import { existsSync as existsSync7, readdirSync, statSync, readFileSync as readFileSync5 } from "fs";
|
|
406
416
|
import { join as join9, basename, extname, relative as relative2 } from "path";
|
|
407
417
|
function detectProjectInfo(projectRoot2) {
|
|
408
418
|
const info = {
|
|
@@ -412,7 +422,7 @@ function detectProjectInfo(projectRoot2) {
|
|
|
412
422
|
testFramework: "unknown",
|
|
413
423
|
directoryStructure: ""
|
|
414
424
|
};
|
|
415
|
-
if (
|
|
425
|
+
if (existsSync7(join9(projectRoot2, "package.json"))) {
|
|
416
426
|
info.language = "typescript";
|
|
417
427
|
info.buildTool = "npm";
|
|
418
428
|
try {
|
|
@@ -428,7 +438,7 @@ function detectProjectInfo(projectRoot2) {
|
|
|
428
438
|
} catch {
|
|
429
439
|
logger.debug("Failed to parse package.json, using default project info");
|
|
430
440
|
}
|
|
431
|
-
} else if (
|
|
441
|
+
} else if (existsSync7(join9(projectRoot2, "pom.xml"))) {
|
|
432
442
|
info.language = "java";
|
|
433
443
|
info.buildTool = "maven";
|
|
434
444
|
info.testFramework = "junit";
|
|
@@ -437,24 +447,24 @@ function detectProjectInfo(projectRoot2) {
|
|
|
437
447
|
if (pom.includes("spring-boot")) info.framework = "spring-boot";
|
|
438
448
|
} catch {
|
|
439
449
|
}
|
|
440
|
-
} else if (
|
|
450
|
+
} else if (existsSync7(join9(projectRoot2, "build.gradle")) || existsSync7(join9(projectRoot2, "build.gradle.kts"))) {
|
|
441
451
|
info.language = "java";
|
|
442
452
|
info.buildTool = "gradle";
|
|
443
453
|
info.testFramework = "junit";
|
|
444
|
-
} else if (
|
|
454
|
+
} else if (existsSync7(join9(projectRoot2, "go.mod"))) {
|
|
445
455
|
info.language = "go";
|
|
446
456
|
info.buildTool = "go";
|
|
447
457
|
info.testFramework = "go-test";
|
|
448
|
-
} else if (
|
|
458
|
+
} else if (existsSync7(join9(projectRoot2, "Cargo.toml"))) {
|
|
449
459
|
info.language = "rust";
|
|
450
460
|
info.buildTool = "cargo";
|
|
451
461
|
info.testFramework = "cargo-test";
|
|
452
|
-
} else if (
|
|
462
|
+
} else if (existsSync7(join9(projectRoot2, "requirements.txt")) || existsSync7(join9(projectRoot2, "pyproject.toml"))) {
|
|
453
463
|
info.language = "python";
|
|
454
464
|
info.buildTool = "pip";
|
|
455
465
|
info.testFramework = "pytest";
|
|
456
466
|
try {
|
|
457
|
-
const reqs =
|
|
467
|
+
const reqs = existsSync7(join9(projectRoot2, "requirements.txt")) ? readFileSync5(join9(projectRoot2, "requirements.txt"), "utf-8") : readFileSync5(join9(projectRoot2, "pyproject.toml"), "utf-8");
|
|
458
468
|
if (reqs.includes("django")) info.framework = "django";
|
|
459
469
|
else if (reqs.includes("flask")) info.framework = "flask";
|
|
460
470
|
else if (reqs.includes("fastapi")) info.framework = "fastapi";
|
|
@@ -502,7 +512,7 @@ function buildDependencyGraph(projectRoot2, modules) {
|
|
|
502
512
|
}
|
|
503
513
|
for (const mod of modules) {
|
|
504
514
|
const modPath = join9(projectRoot2, mod.path);
|
|
505
|
-
if (!
|
|
515
|
+
if (!existsSync7(modPath)) continue;
|
|
506
516
|
try {
|
|
507
517
|
const imports = scanImports(modPath);
|
|
508
518
|
for (const otherMod of modules) {
|
|
@@ -704,7 +714,7 @@ function extractEntities(projectRoot2, modules) {
|
|
|
704
714
|
const seen = /* @__PURE__ */ new Set();
|
|
705
715
|
for (const mod of modules) {
|
|
706
716
|
const modPath = join9(projectRoot2, mod.path);
|
|
707
|
-
if (!
|
|
717
|
+
if (!existsSync7(modPath)) continue;
|
|
708
718
|
try {
|
|
709
719
|
const files = readDirRecursive(modPath);
|
|
710
720
|
for (const file of files) {
|
|
@@ -727,7 +737,7 @@ function extractApis(projectRoot2, modules) {
|
|
|
727
737
|
const seen = /* @__PURE__ */ new Set();
|
|
728
738
|
for (const mod of modules) {
|
|
729
739
|
const modPath = join9(projectRoot2, mod.path);
|
|
730
|
-
if (!
|
|
740
|
+
if (!existsSync7(modPath)) continue;
|
|
731
741
|
try {
|
|
732
742
|
const files = readDirRecursive(modPath);
|
|
733
743
|
for (const file of files) {
|
|
@@ -756,7 +766,7 @@ var init_graph_builder = __esm({
|
|
|
756
766
|
});
|
|
757
767
|
|
|
758
768
|
// src/core/context-engine/context-writer.ts
|
|
759
|
-
import { readFileSync as readFileSync6, writeFileSync as writeFileSync5, existsSync as
|
|
769
|
+
import { readFileSync as readFileSync6, writeFileSync as writeFileSync5, existsSync as existsSync8, statSync as statSync2, mkdirSync as mkdirSync5 } from "fs";
|
|
760
770
|
import { join as join10 } from "path";
|
|
761
771
|
import { execFileSync } from "child_process";
|
|
762
772
|
function buildContext(projectRoot2, config) {
|
|
@@ -801,7 +811,7 @@ function buildContext(projectRoot2, config) {
|
|
|
801
811
|
}
|
|
802
812
|
function loadContext(specLoreDir) {
|
|
803
813
|
const contextPath = join10(specLoreDir, CONTEXT_FILENAME);
|
|
804
|
-
if (!
|
|
814
|
+
if (!existsSync8(contextPath)) {
|
|
805
815
|
logger.debug("No cached context.json found");
|
|
806
816
|
return null;
|
|
807
817
|
}
|
|
@@ -826,7 +836,7 @@ function loadContext(specLoreDir) {
|
|
|
826
836
|
}
|
|
827
837
|
}
|
|
828
838
|
function writeContextFile(specLoreDir, context) {
|
|
829
|
-
if (!
|
|
839
|
+
if (!existsSync8(specLoreDir)) {
|
|
830
840
|
mkdirSync5(specLoreDir, { recursive: true });
|
|
831
841
|
}
|
|
832
842
|
const contextPath = join10(specLoreDir, CONTEXT_FILENAME);
|
|
@@ -849,7 +859,7 @@ function hasGitHeadChanged(specLoreDir) {
|
|
|
849
859
|
stdio: ["pipe", "pipe", "pipe"]
|
|
850
860
|
}).trim();
|
|
851
861
|
const headFile = join10(specLoreDir, ".git-head");
|
|
852
|
-
if (!
|
|
862
|
+
if (!existsSync8(headFile)) return true;
|
|
853
863
|
const savedHead = readFileSync6(headFile, "utf-8").trim();
|
|
854
864
|
if (savedHead !== head) {
|
|
855
865
|
writeFileSync5(headFile, head, "utf-8");
|
|
@@ -951,7 +961,7 @@ var init_cost_tracker = __esm({
|
|
|
951
961
|
totalTokens: promptTokens + completionTokens,
|
|
952
962
|
estimatedCostUsd: cost
|
|
953
963
|
});
|
|
954
|
-
logger.debug(`AI usage: ${model}
|
|
964
|
+
logger.debug(`AI usage: ${model} - ${promptTokens}+${completionTokens} tokens, ~$${cost.toFixed(4)}`);
|
|
955
965
|
return cost;
|
|
956
966
|
}
|
|
957
967
|
/**
|
|
@@ -1018,7 +1028,7 @@ var init_cost_tracker = __esm({
|
|
|
1018
1028
|
});
|
|
1019
1029
|
|
|
1020
1030
|
// src/core/state-manager/index.ts
|
|
1021
|
-
import { readFileSync as readFileSync7, writeFileSync as writeFileSync6, existsSync as
|
|
1031
|
+
import { readFileSync as readFileSync7, writeFileSync as writeFileSync6, existsSync as existsSync9, mkdirSync as mkdirSync6 } from "fs";
|
|
1022
1032
|
import { join as join12 } from "path";
|
|
1023
1033
|
import yaml2 from "js-yaml";
|
|
1024
1034
|
import { globSync as globSync2 } from "glob";
|
|
@@ -1050,7 +1060,7 @@ var init_state_manager = __esm({
|
|
|
1050
1060
|
}
|
|
1051
1061
|
/** Load state from disk, or return default if not exists */
|
|
1052
1062
|
load() {
|
|
1053
|
-
if (!
|
|
1063
|
+
if (!existsSync9(this.statePath)) {
|
|
1054
1064
|
return createDefaultState();
|
|
1055
1065
|
}
|
|
1056
1066
|
try {
|
|
@@ -1066,14 +1076,14 @@ var init_state_manager = __esm({
|
|
|
1066
1076
|
/** Persist state to disk */
|
|
1067
1077
|
save(state) {
|
|
1068
1078
|
const dir = join12(this.projectRoot, ".speclore");
|
|
1069
|
-
if (!
|
|
1079
|
+
if (!existsSync9(dir)) {
|
|
1070
1080
|
mkdirSync6(dir, { recursive: true });
|
|
1071
1081
|
}
|
|
1072
1082
|
writeFileSync6(this.statePath, yaml2.dump(state, { lineWidth: 120 }), "utf-8");
|
|
1073
1083
|
}
|
|
1074
1084
|
/** Ensure state file exists with default values */
|
|
1075
1085
|
ensureInitialized() {
|
|
1076
|
-
if (!
|
|
1086
|
+
if (!existsSync9(this.statePath)) {
|
|
1077
1087
|
const state2 = createDefaultState();
|
|
1078
1088
|
this.save(state2);
|
|
1079
1089
|
return state2;
|
|
@@ -1222,7 +1232,7 @@ var status_exports = {};
|
|
|
1222
1232
|
__export(status_exports, {
|
|
1223
1233
|
registerStatusCommand: () => registerStatusCommand
|
|
1224
1234
|
});
|
|
1225
|
-
import { existsSync as
|
|
1235
|
+
import { existsSync as existsSync10 } from "fs";
|
|
1226
1236
|
import { join as join13 } from "path";
|
|
1227
1237
|
import { globSync as globSync3 } from "glob";
|
|
1228
1238
|
function registerStatusCommand(program) {
|
|
@@ -1235,7 +1245,7 @@ function registerStatusCommand(program) {
|
|
|
1235
1245
|
console.log("");
|
|
1236
1246
|
const config = loadConfig(projectRoot2);
|
|
1237
1247
|
const configPath = join13(projectRoot2, ".speclore", "config.yaml");
|
|
1238
|
-
if (
|
|
1248
|
+
if (existsSync10(configPath)) {
|
|
1239
1249
|
console.log(`\u2713 Config: ${configPath}`);
|
|
1240
1250
|
console.log(` Project: ${config.project.name || "(unnamed)"}`);
|
|
1241
1251
|
console.log(` Profile: ${config.project.profile}`);
|
|
@@ -1244,13 +1254,13 @@ function registerStatusCommand(program) {
|
|
|
1244
1254
|
console.log("\u2717 Config not found. Run `speclore setup` first.");
|
|
1245
1255
|
}
|
|
1246
1256
|
const contextPath = join13(projectRoot2, ".speclore", "context.json");
|
|
1247
|
-
if (
|
|
1257
|
+
if (existsSync10(contextPath)) {
|
|
1248
1258
|
console.log(`\u2713 Context: ${contextPath}`);
|
|
1249
1259
|
} else {
|
|
1250
1260
|
console.log("\u2717 Context not found. Run `speclore init` first.");
|
|
1251
1261
|
}
|
|
1252
1262
|
const specsDir = join13(projectRoot2, config.spec.outputDir);
|
|
1253
|
-
if (
|
|
1263
|
+
if (existsSync10(specsDir)) {
|
|
1254
1264
|
const features = globSync3("**/*.feature", { cwd: specsDir });
|
|
1255
1265
|
console.log(`\u2713 Features: ${features.length} file(s) in ${config.spec.outputDir}/`);
|
|
1256
1266
|
} else {
|
|
@@ -1331,7 +1341,7 @@ var init_status = __esm({
|
|
|
1331
1341
|
});
|
|
1332
1342
|
|
|
1333
1343
|
// src/plugins/builtin/md-reader.ts
|
|
1334
|
-
import { readFileSync as readFileSync8, existsSync as
|
|
1344
|
+
import { readFileSync as readFileSync8, existsSync as existsSync11 } from "fs";
|
|
1335
1345
|
var MarkdownReader;
|
|
1336
1346
|
var init_md_reader = __esm({
|
|
1337
1347
|
"src/plugins/builtin/md-reader.ts"() {
|
|
@@ -1343,7 +1353,7 @@ var init_md_reader = __esm({
|
|
|
1343
1353
|
return /\.md$/i.test(source) || /\.markdown$/i.test(source);
|
|
1344
1354
|
}
|
|
1345
1355
|
read(source) {
|
|
1346
|
-
if (!
|
|
1356
|
+
if (!existsSync11(source)) {
|
|
1347
1357
|
throw new Error(`File not found: ${source}`);
|
|
1348
1358
|
}
|
|
1349
1359
|
const content = readFileSync8(source, "utf-8");
|
|
@@ -1498,8 +1508,8 @@ var init_pdf_reader = __esm({
|
|
|
1498
1508
|
} catch {
|
|
1499
1509
|
throw new Error("pdfjs-dist package is required for PDF support. Install: npm i pdfjs-dist");
|
|
1500
1510
|
}
|
|
1501
|
-
const { readFileSync:
|
|
1502
|
-
const buffer =
|
|
1511
|
+
const { readFileSync: readFileSync25 } = await import("fs");
|
|
1512
|
+
const buffer = readFileSync25(source);
|
|
1503
1513
|
const uint8 = new Uint8Array(buffer);
|
|
1504
1514
|
const doc = await pdfjsLib.getDocument({
|
|
1505
1515
|
data: uint8,
|
|
@@ -2114,13 +2124,13 @@ var init_image_reader = __esm({
|
|
|
2114
2124
|
return /\.(png|jpe?g|webp)$/i.test(source);
|
|
2115
2125
|
}
|
|
2116
2126
|
async read(source) {
|
|
2117
|
-
const { existsSync:
|
|
2118
|
-
if (!
|
|
2127
|
+
const { existsSync: existsSync32, readFileSync: readFileSync25 } = await import("fs");
|
|
2128
|
+
if (!existsSync32(source)) {
|
|
2119
2129
|
throw new Error(`Image file not found: ${source}`);
|
|
2120
2130
|
}
|
|
2121
2131
|
const ext = source.split(".").pop()?.toLowerCase() ?? "png";
|
|
2122
2132
|
const mimeType = ext === "jpg" ? "image/jpeg" : `image/${ext}`;
|
|
2123
|
-
const buffer =
|
|
2133
|
+
const buffer = readFileSync25(source);
|
|
2124
2134
|
let text;
|
|
2125
2135
|
try {
|
|
2126
2136
|
const { createProvider: createProvider2 } = await Promise.resolve().then(() => (init_provider(), provider_exports));
|
|
@@ -2149,7 +2159,7 @@ var init_image_reader = __esm({
|
|
|
2149
2159
|
});
|
|
2150
2160
|
|
|
2151
2161
|
// src/plugins/builtin/cursor-writer.ts
|
|
2152
|
-
import { writeFileSync as writeFileSync7, mkdirSync as mkdirSync7, existsSync as
|
|
2162
|
+
import { writeFileSync as writeFileSync7, mkdirSync as mkdirSync7, existsSync as existsSync12, rmSync } from "fs";
|
|
2153
2163
|
import { join as join14 } from "path";
|
|
2154
2164
|
var CursorWriter;
|
|
2155
2165
|
var init_cursor_writer = __esm({
|
|
@@ -2160,7 +2170,7 @@ var init_cursor_writer = __esm({
|
|
|
2160
2170
|
configFile = ".cursor/rules/speclore.mdc";
|
|
2161
2171
|
projectRoot = "";
|
|
2162
2172
|
detect(projectRoot2) {
|
|
2163
|
-
return
|
|
2173
|
+
return existsSync12(join14(projectRoot2, ".cursor"));
|
|
2164
2174
|
}
|
|
2165
2175
|
write(constraints) {
|
|
2166
2176
|
this.projectRoot = constraints.projectRoot;
|
|
@@ -2180,7 +2190,7 @@ var init_cursor_writer = __esm({
|
|
|
2180
2190
|
}
|
|
2181
2191
|
remove() {
|
|
2182
2192
|
const filePath = join14(this.projectRoot, ".cursor", "rules", "speclore.mdc");
|
|
2183
|
-
if (
|
|
2193
|
+
if (existsSync12(filePath)) rmSync(filePath);
|
|
2184
2194
|
return Promise.resolve();
|
|
2185
2195
|
}
|
|
2186
2196
|
buildMarkdown(c) {
|
|
@@ -2244,7 +2254,7 @@ var init_cursor_writer = __esm({
|
|
|
2244
2254
|
});
|
|
2245
2255
|
|
|
2246
2256
|
// src/plugins/builtin/claude-writer.ts
|
|
2247
|
-
import { writeFileSync as writeFileSync8, mkdirSync as mkdirSync8, existsSync as
|
|
2257
|
+
import { writeFileSync as writeFileSync8, mkdirSync as mkdirSync8, existsSync as existsSync13, rmSync as rmSync2 } from "fs";
|
|
2248
2258
|
import { join as join15 } from "path";
|
|
2249
2259
|
var ClaudeWriter;
|
|
2250
2260
|
var init_claude_writer = __esm({
|
|
@@ -2255,7 +2265,7 @@ var init_claude_writer = __esm({
|
|
|
2255
2265
|
configFile = ".claude/rules/speclore.md";
|
|
2256
2266
|
projectRoot = "";
|
|
2257
2267
|
detect(projectRoot2) {
|
|
2258
|
-
return
|
|
2268
|
+
return existsSync13(join15(projectRoot2, ".claude")) || existsSync13(join15(projectRoot2, ".mcp.json"));
|
|
2259
2269
|
}
|
|
2260
2270
|
write(constraints) {
|
|
2261
2271
|
this.projectRoot = constraints.projectRoot;
|
|
@@ -2267,7 +2277,7 @@ var init_claude_writer = __esm({
|
|
|
2267
2277
|
}
|
|
2268
2278
|
remove() {
|
|
2269
2279
|
const filePath = join15(this.projectRoot, ".claude", "rules", "speclore.md");
|
|
2270
|
-
if (
|
|
2280
|
+
if (existsSync13(filePath)) rmSync2(filePath);
|
|
2271
2281
|
return Promise.resolve();
|
|
2272
2282
|
}
|
|
2273
2283
|
buildMarkdown(c) {
|
|
@@ -2326,30 +2336,33 @@ var init_claude_writer = __esm({
|
|
|
2326
2336
|
});
|
|
2327
2337
|
|
|
2328
2338
|
// src/plugins/builtin/qoder-writer.ts
|
|
2329
|
-
import { writeFileSync as writeFileSync9, mkdirSync as mkdirSync9, existsSync as
|
|
2339
|
+
import { writeFileSync as writeFileSync9, mkdirSync as mkdirSync9, existsSync as existsSync14, rmSync as rmSync3 } from "fs";
|
|
2330
2340
|
import { join as join16 } from "path";
|
|
2331
2341
|
var QoderWriter;
|
|
2332
2342
|
var init_qoder_writer = __esm({
|
|
2333
2343
|
"src/plugins/builtin/qoder-writer.ts"() {
|
|
2334
2344
|
"use strict";
|
|
2345
|
+
init_path_utils();
|
|
2335
2346
|
QoderWriter = class {
|
|
2336
2347
|
toolName = "qoder";
|
|
2337
2348
|
configFile = ".qoder/rules/speclore.md";
|
|
2338
2349
|
projectRoot = "";
|
|
2339
2350
|
detect(projectRoot2) {
|
|
2340
|
-
return
|
|
2351
|
+
return existsSync14(join16(projectRoot2, ".qoder")) || existsSync14(join16(projectRoot2, ".qoder-cn"));
|
|
2341
2352
|
}
|
|
2342
2353
|
write(constraints) {
|
|
2343
2354
|
this.projectRoot = constraints.projectRoot;
|
|
2344
|
-
const
|
|
2355
|
+
const qoderDir = resolveQoderDir(constraints.projectRoot);
|
|
2356
|
+
const rulesDir = join16(constraints.projectRoot, qoderDir, "rules");
|
|
2345
2357
|
mkdirSync9(rulesDir, { recursive: true });
|
|
2346
2358
|
const content = this.buildMarkdown(constraints);
|
|
2347
2359
|
writeFileSync9(join16(rulesDir, "speclore.md"), content, "utf-8");
|
|
2348
2360
|
return Promise.resolve();
|
|
2349
2361
|
}
|
|
2350
2362
|
remove() {
|
|
2351
|
-
const
|
|
2352
|
-
|
|
2363
|
+
const qoderDir = resolveQoderDir(this.projectRoot);
|
|
2364
|
+
const filePath = join16(this.projectRoot, qoderDir, "rules", "speclore.md");
|
|
2365
|
+
if (existsSync14(filePath)) rmSync3(filePath);
|
|
2353
2366
|
return Promise.resolve();
|
|
2354
2367
|
}
|
|
2355
2368
|
buildMarkdown(c) {
|
|
@@ -2943,8 +2956,34 @@ var init_image_reader2 = __esm({
|
|
|
2943
2956
|
}
|
|
2944
2957
|
});
|
|
2945
2958
|
|
|
2959
|
+
// src/core/requirement-reader/text-reader.ts
|
|
2960
|
+
import { readFileSync as readFileSync12 } from "fs";
|
|
2961
|
+
import { basename as basename7, extname as extname7 } from "path";
|
|
2962
|
+
function readTextFile(filePath) {
|
|
2963
|
+
const content = readFileSync12(filePath, "utf-8");
|
|
2964
|
+
const id = deriveId2(filePath);
|
|
2965
|
+
const lines = content.split("\n").filter((line) => line.trim().length > 0);
|
|
2966
|
+
const title = lines[0]?.slice(0, 100) ?? basename7(filePath, extname7(filePath));
|
|
2967
|
+
return Promise.resolve({
|
|
2968
|
+
id,
|
|
2969
|
+
title,
|
|
2970
|
+
description: content,
|
|
2971
|
+
rawContent: content,
|
|
2972
|
+
confidence: 1
|
|
2973
|
+
});
|
|
2974
|
+
}
|
|
2975
|
+
function deriveId2(filePath) {
|
|
2976
|
+
const name = basename7(filePath, extname7(filePath));
|
|
2977
|
+
return name.toLowerCase().replace(/[^a-z0-9\u4e00-\u9fff-]/g, "-").replace(/-+/g, "-");
|
|
2978
|
+
}
|
|
2979
|
+
var init_text_reader = __esm({
|
|
2980
|
+
"src/core/requirement-reader/text-reader.ts"() {
|
|
2981
|
+
"use strict";
|
|
2982
|
+
}
|
|
2983
|
+
});
|
|
2984
|
+
|
|
2946
2985
|
// src/core/requirement-reader/url-reader.ts
|
|
2947
|
-
import { basename as
|
|
2986
|
+
import { basename as basename8 } from "path";
|
|
2948
2987
|
async function readUrl(url) {
|
|
2949
2988
|
validateUrl(url);
|
|
2950
2989
|
logger.info(`Fetching URL: ${url}`);
|
|
@@ -2982,7 +3021,7 @@ async function readUrl(url) {
|
|
|
2982
3021
|
content = content.slice(0, MAX_CONTENT_LENGTH) + "\n... (truncated)";
|
|
2983
3022
|
}
|
|
2984
3023
|
const urlPath = new URL(url).pathname;
|
|
2985
|
-
const id =
|
|
3024
|
+
const id = basename8(urlPath).toLowerCase().replace(/[^a-z0-9\u4e00-\u9fff-]/g, "-").replace(/-+/g, "-") || "url-content";
|
|
2986
3025
|
return {
|
|
2987
3026
|
id,
|
|
2988
3027
|
title,
|
|
@@ -3041,34 +3080,48 @@ var init_url_reader = __esm({
|
|
|
3041
3080
|
// src/core/requirement-reader/index.ts
|
|
3042
3081
|
var requirement_reader_exports = {};
|
|
3043
3082
|
__export(requirement_reader_exports, {
|
|
3044
|
-
readRequirement: () => readRequirement
|
|
3083
|
+
readRequirement: () => readRequirement,
|
|
3084
|
+
sanitizeSource: () => sanitizeSource
|
|
3045
3085
|
});
|
|
3046
|
-
import { existsSync as
|
|
3047
|
-
import { extname as
|
|
3086
|
+
import { existsSync as existsSync15 } from "fs";
|
|
3087
|
+
import { extname as extname8 } from "path";
|
|
3088
|
+
function sanitizeSource(source) {
|
|
3089
|
+
return source.replace(/[\u200B-\u200F\u202A-\u202E\u2060-\u2069\uFEFF\u00AD]/g, "");
|
|
3090
|
+
}
|
|
3048
3091
|
async function readRequirement(source) {
|
|
3049
|
-
const
|
|
3050
|
-
|
|
3092
|
+
const cleaned = sanitizeSource(source);
|
|
3093
|
+
if (cleaned !== source) {
|
|
3094
|
+
logger.debug("Stripped invisible Unicode characters from source input.");
|
|
3095
|
+
}
|
|
3096
|
+
const sourceType = classifySource(cleaned);
|
|
3097
|
+
logger.info(`Reading requirement from ${sourceType}: ${truncate(cleaned, 120)}`);
|
|
3051
3098
|
switch (sourceType) {
|
|
3052
3099
|
case "file":
|
|
3053
|
-
return readFromFile(
|
|
3100
|
+
return readFromFile(cleaned);
|
|
3054
3101
|
case "url":
|
|
3055
|
-
return readFromUrl(
|
|
3102
|
+
return readFromUrl(cleaned);
|
|
3056
3103
|
case "text":
|
|
3057
|
-
return readFromText(
|
|
3104
|
+
return readFromText(cleaned);
|
|
3058
3105
|
}
|
|
3059
3106
|
}
|
|
3060
3107
|
function classifySource(source) {
|
|
3061
3108
|
if (/^https?:\/\//i.test(source)) {
|
|
3062
3109
|
return "url";
|
|
3063
3110
|
}
|
|
3064
|
-
const ext =
|
|
3065
|
-
const supportedExts = [".md", ".docx", ".xlsx", ".xls", ".pdf", ".png", ".jpg", ".jpeg", ".webp"];
|
|
3066
|
-
if (supportedExts.includes(ext) &&
|
|
3111
|
+
const ext = extname8(source).toLowerCase();
|
|
3112
|
+
const supportedExts = [".md", ".txt", ".docx", ".xlsx", ".xls", ".pdf", ".png", ".jpg", ".jpeg", ".webp"];
|
|
3113
|
+
if (supportedExts.includes(ext) && existsSync15(source)) {
|
|
3067
3114
|
return "file";
|
|
3068
3115
|
}
|
|
3069
|
-
if (
|
|
3116
|
+
if (existsSync15(source) && (source.includes("/") || source.includes("\\"))) {
|
|
3070
3117
|
return "file";
|
|
3071
3118
|
}
|
|
3119
|
+
if (supportedExts.includes(ext) && (source.includes("/") || source.includes("\\"))) {
|
|
3120
|
+
throw new Error(
|
|
3121
|
+
`File not found: ${source}
|
|
3122
|
+
The path looks like a file but does not exist. Check the path and try again. If copying from Windows, make sure no invisible characters are included.`
|
|
3123
|
+
);
|
|
3124
|
+
}
|
|
3072
3125
|
return "text";
|
|
3073
3126
|
}
|
|
3074
3127
|
async function readFromFile(filePath) {
|
|
@@ -3082,10 +3135,12 @@ async function readFromFile(filePath) {
|
|
|
3082
3135
|
}
|
|
3083
3136
|
return results[0];
|
|
3084
3137
|
}
|
|
3085
|
-
const ext =
|
|
3138
|
+
const ext = extname8(filePath).toLowerCase();
|
|
3086
3139
|
switch (ext) {
|
|
3087
3140
|
case ".md":
|
|
3088
3141
|
return readMarkdownFile(filePath);
|
|
3142
|
+
case ".txt":
|
|
3143
|
+
return readTextFile(filePath);
|
|
3089
3144
|
case ".docx":
|
|
3090
3145
|
return readDocxFile(filePath);
|
|
3091
3146
|
case ".xlsx":
|
|
@@ -3128,6 +3183,7 @@ var init_requirement_reader = __esm({
|
|
|
3128
3183
|
init_xlsx_reader2();
|
|
3129
3184
|
init_pdf_reader2();
|
|
3130
3185
|
init_image_reader2();
|
|
3186
|
+
init_text_reader();
|
|
3131
3187
|
init_url_reader();
|
|
3132
3188
|
}
|
|
3133
3189
|
});
|
|
@@ -3358,7 +3414,7 @@ var init_prompt_builder = __esm({
|
|
|
3358
3414
|
});
|
|
3359
3415
|
|
|
3360
3416
|
// src/core/feature-generator/generator.ts
|
|
3361
|
-
import { writeFileSync as writeFileSync10, mkdirSync as mkdirSync10, existsSync as
|
|
3417
|
+
import { writeFileSync as writeFileSync10, mkdirSync as mkdirSync10, existsSync as existsSync16 } from "fs";
|
|
3362
3418
|
import { join as join17, dirname as dirname2 } from "path";
|
|
3363
3419
|
import { Parser as Parser2, GherkinClassicTokenMatcher as GherkinClassicTokenMatcher2, AstBuilder as AstBuilder2 } from "@cucumber/gherkin";
|
|
3364
3420
|
import { IdGenerator as IdGenerator2 } from "@cucumber/messages";
|
|
@@ -3426,7 +3482,7 @@ Please fix these issues and regenerate the complete Feature file.`;
|
|
|
3426
3482
|
const outputDir = join17(projectRoot2, config.spec.outputDir);
|
|
3427
3483
|
const moduleDir = inferModule(requirement, context);
|
|
3428
3484
|
const filePath = join17(outputDir, moduleDir, `${requirement.id}.feature`);
|
|
3429
|
-
if (!
|
|
3485
|
+
if (!existsSync16(dirname2(filePath))) {
|
|
3430
3486
|
mkdirSync10(dirname2(filePath), { recursive: true });
|
|
3431
3487
|
}
|
|
3432
3488
|
writeFileSync10(filePath, featureContent, "utf-8");
|
|
@@ -3585,21 +3641,21 @@ var init_feature_generator = __esm({
|
|
|
3585
3641
|
});
|
|
3586
3642
|
|
|
3587
3643
|
// src/core/constraint-coder/ai-tool-detector.ts
|
|
3588
|
-
import { existsSync as
|
|
3644
|
+
import { existsSync as existsSync17 } from "fs";
|
|
3589
3645
|
import { join as join19 } from "path";
|
|
3590
3646
|
function detectAITools2(projectRoot2) {
|
|
3591
3647
|
const tools = [];
|
|
3592
|
-
if (
|
|
3648
|
+
if (existsSync17(join19(projectRoot2, ".cursor"))) {
|
|
3593
3649
|
tools.push("cursor");
|
|
3594
3650
|
logger.debug("Detected Cursor (.cursor/)");
|
|
3595
3651
|
}
|
|
3596
|
-
if (
|
|
3652
|
+
if (existsSync17(join19(projectRoot2, ".claude")) || existsSync17(join19(projectRoot2, "CLAUDE.md"))) {
|
|
3597
3653
|
tools.push("claude");
|
|
3598
3654
|
logger.debug("Detected Claude Code (.claude/ or CLAUDE.md)");
|
|
3599
3655
|
}
|
|
3600
|
-
if (
|
|
3656
|
+
if (hasQoderDir(projectRoot2)) {
|
|
3601
3657
|
tools.push("qoder");
|
|
3602
|
-
logger.debug("Detected Qoder (.qoder/)");
|
|
3658
|
+
logger.debug("Detected Qoder (.qoder/ or .qoder-cn/)");
|
|
3603
3659
|
}
|
|
3604
3660
|
logger.debug(`Detected AI tools: ${tools.length > 0 ? tools.join(", ") : "none"}`);
|
|
3605
3661
|
return tools;
|
|
@@ -3608,11 +3664,12 @@ var init_ai_tool_detector = __esm({
|
|
|
3608
3664
|
"src/core/constraint-coder/ai-tool-detector.ts"() {
|
|
3609
3665
|
"use strict";
|
|
3610
3666
|
init_logger();
|
|
3667
|
+
init_path_utils();
|
|
3611
3668
|
}
|
|
3612
3669
|
});
|
|
3613
3670
|
|
|
3614
3671
|
// src/core/constraint-coder/constraint-writer.ts
|
|
3615
|
-
import { writeFileSync as writeFileSync11, mkdirSync as mkdirSync11, existsSync as
|
|
3672
|
+
import { writeFileSync as writeFileSync11, mkdirSync as mkdirSync11, existsSync as existsSync18 } from "fs";
|
|
3616
3673
|
import { join as join20 } from "path";
|
|
3617
3674
|
function writeConstraints(projectRoot2, tools, content) {
|
|
3618
3675
|
const writtenFiles = [];
|
|
@@ -3686,7 +3743,7 @@ function buildConstraintText(content) {
|
|
|
3686
3743
|
}
|
|
3687
3744
|
function writeCursorRule2(projectRoot2, text) {
|
|
3688
3745
|
const dir = join20(projectRoot2, ".cursor", "rules");
|
|
3689
|
-
if (!
|
|
3746
|
+
if (!existsSync18(dir)) mkdirSync11(dir, { recursive: true });
|
|
3690
3747
|
const filePath = join20(dir, "speclore.mdc");
|
|
3691
3748
|
const frontmatter = [
|
|
3692
3749
|
"---",
|
|
@@ -3701,14 +3758,15 @@ function writeCursorRule2(projectRoot2, text) {
|
|
|
3701
3758
|
}
|
|
3702
3759
|
function writeClaudeRule2(projectRoot2, text) {
|
|
3703
3760
|
const dir = join20(projectRoot2, ".claude", "rules");
|
|
3704
|
-
if (!
|
|
3761
|
+
if (!existsSync18(dir)) mkdirSync11(dir, { recursive: true });
|
|
3705
3762
|
const filePath = join20(dir, "speclore.md");
|
|
3706
3763
|
writeFileSync11(filePath, text, "utf-8");
|
|
3707
3764
|
return filePath;
|
|
3708
3765
|
}
|
|
3709
3766
|
function writeQoderRule2(projectRoot2, text) {
|
|
3710
|
-
const
|
|
3711
|
-
|
|
3767
|
+
const qoderDir = resolveQoderDir(projectRoot2);
|
|
3768
|
+
const dir = join20(projectRoot2, qoderDir, "rules");
|
|
3769
|
+
if (!existsSync18(dir)) mkdirSync11(dir, { recursive: true });
|
|
3712
3770
|
const filePath = join20(dir, "speclore.md");
|
|
3713
3771
|
writeFileSync11(filePath, text, "utf-8");
|
|
3714
3772
|
return filePath;
|
|
@@ -3766,7 +3824,7 @@ async function generateConstraints(projectRoot2, features, _context, config) {
|
|
|
3766
3824
|
try {
|
|
3767
3825
|
await writer.write(content);
|
|
3768
3826
|
writtenFiles.push(writer.configFile);
|
|
3769
|
-
logger.info(`Constraint written via plugin: ${writer.toolName}
|
|
3827
|
+
logger.info(`Constraint written via plugin: ${writer.toolName} -> ${writer.configFile}`);
|
|
3770
3828
|
} catch (err) {
|
|
3771
3829
|
logger.warn(`Writer plugin '${tool}' failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
3772
3830
|
}
|
|
@@ -3795,15 +3853,15 @@ Keep mapping files in sync whenever tests are modified.`;
|
|
|
3795
3853
|
});
|
|
3796
3854
|
|
|
3797
3855
|
// src/core/test-scaffolder/framework-detector.ts
|
|
3798
|
-
import { readFileSync as
|
|
3856
|
+
import { readFileSync as readFileSync13, existsSync as existsSync19 } from "fs";
|
|
3799
3857
|
import { join as join21 } from "path";
|
|
3800
3858
|
function detectTestFramework(projectRoot2) {
|
|
3801
3859
|
const pkgPath = join21(projectRoot2, "package.json");
|
|
3802
|
-
if (!
|
|
3860
|
+
if (!existsSync19(pkgPath)) {
|
|
3803
3861
|
return "vitest";
|
|
3804
3862
|
}
|
|
3805
3863
|
try {
|
|
3806
|
-
const pkg = JSON.parse(
|
|
3864
|
+
const pkg = JSON.parse(readFileSync13(pkgPath, "utf-8"));
|
|
3807
3865
|
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
3808
3866
|
if ("vitest" in deps) return "vitest";
|
|
3809
3867
|
if ("jest" in deps || "@jest/core" in deps) return "jest";
|
|
@@ -3819,8 +3877,8 @@ var init_framework_detector = __esm({
|
|
|
3819
3877
|
});
|
|
3820
3878
|
|
|
3821
3879
|
// src/core/test-scaffolder/index.ts
|
|
3822
|
-
import { readFileSync as
|
|
3823
|
-
import { join as join22, relative as relative3, dirname as dirname3, basename as
|
|
3880
|
+
import { readFileSync as readFileSync14, writeFileSync as writeFileSync12, existsSync as existsSync20, mkdirSync as mkdirSync12 } from "fs";
|
|
3881
|
+
import { join as join22, relative as relative3, dirname as dirname3, basename as basename9 } from "path";
|
|
3824
3882
|
function generateTestScaffolding(projectRoot2, features, config) {
|
|
3825
3883
|
const framework = detectTestFramework(projectRoot2);
|
|
3826
3884
|
const results = [];
|
|
@@ -3832,7 +3890,7 @@ function generateTestScaffolding(projectRoot2, features, config) {
|
|
|
3832
3890
|
}
|
|
3833
3891
|
const absTestPath = join22(projectRoot2, testFilePath);
|
|
3834
3892
|
const scenarios = feature.scenarios;
|
|
3835
|
-
if (
|
|
3893
|
+
if (existsSync20(absTestPath)) {
|
|
3836
3894
|
const appended = appendMissingScenarios(absTestPath, scenarios, framework);
|
|
3837
3895
|
if (appended > 0) {
|
|
3838
3896
|
results.push({ testFile: testFilePath, framework, scenarios: appended });
|
|
@@ -3841,7 +3899,7 @@ function generateTestScaffolding(projectRoot2, features, config) {
|
|
|
3841
3899
|
} else {
|
|
3842
3900
|
const content = generateTestFileContent(feature.featureName, scenarios, framework);
|
|
3843
3901
|
const dir = dirname3(absTestPath);
|
|
3844
|
-
if (!
|
|
3902
|
+
if (!existsSync20(dir)) {
|
|
3845
3903
|
mkdirSync12(dir, { recursive: true });
|
|
3846
3904
|
}
|
|
3847
3905
|
writeFileSync12(absTestPath, content, "utf-8");
|
|
@@ -3860,7 +3918,7 @@ function resolveTestFilePath(projectRoot2, featurePath, config) {
|
|
|
3860
3918
|
return resolveTestFromPattern(match, pattern);
|
|
3861
3919
|
}
|
|
3862
3920
|
}
|
|
3863
|
-
const featureBase =
|
|
3921
|
+
const featureBase = basename9(featurePath, ".feature");
|
|
3864
3922
|
const featureDir = dirname3(relative3(projectRoot2, featurePath));
|
|
3865
3923
|
return join22(featureDir, `${featureBase}.test.ts`).replace(/\\/g, "/");
|
|
3866
3924
|
}
|
|
@@ -3915,7 +3973,7 @@ function generateTestFileContent(featureName, scenarios, framework) {
|
|
|
3915
3973
|
return lines.join("\n");
|
|
3916
3974
|
}
|
|
3917
3975
|
function appendMissingScenarios(testFilePath, scenarios, _framework) {
|
|
3918
|
-
const content =
|
|
3976
|
+
const content = readFileSync14(testFilePath, "utf-8");
|
|
3919
3977
|
const missing = [];
|
|
3920
3978
|
for (const scenario of scenarios) {
|
|
3921
3979
|
if (!content.includes(`Scenario: ${scenario.name}`)) {
|
|
@@ -3956,7 +4014,7 @@ var init_test_scaffolder = __esm({
|
|
|
3956
4014
|
});
|
|
3957
4015
|
|
|
3958
4016
|
// src/core/verifier/mapping-resolver.ts
|
|
3959
|
-
import { readFileSync as
|
|
4017
|
+
import { readFileSync as readFileSync16, existsSync as existsSync22, readdirSync as readdirSync2 } from "fs";
|
|
3960
4018
|
import { join as join24 } from "path";
|
|
3961
4019
|
function resolveMappings(projectRoot2, features, testOutput) {
|
|
3962
4020
|
const results = [];
|
|
@@ -3981,17 +4039,17 @@ function resolveMapping(projectRoot2, feature, scenario, _testOutput) {
|
|
|
3981
4039
|
}
|
|
3982
4040
|
function resolveFromMappingFile(projectRoot2, _feature, scenario) {
|
|
3983
4041
|
const mappingsDir = join24(projectRoot2, ".speclore", "mappings");
|
|
3984
|
-
if (!
|
|
4042
|
+
if (!existsSync22(mappingsDir)) return null;
|
|
3985
4043
|
try {
|
|
3986
4044
|
const files = findMappingFiles(mappingsDir);
|
|
3987
4045
|
for (const file of files) {
|
|
3988
4046
|
try {
|
|
3989
|
-
const content =
|
|
4047
|
+
const content = readFileSync16(file, "utf-8");
|
|
3990
4048
|
const mapping = JSON.parse(content);
|
|
3991
4049
|
if (mapping.scenarios && scenario.name in mapping.scenarios) {
|
|
3992
4050
|
const entry = mapping.scenarios[scenario.name];
|
|
3993
4051
|
if (entry) {
|
|
3994
|
-
logger.debug(`Mapping file hit: ${scenario.name}
|
|
4052
|
+
logger.debug(`Mapping file hit: ${scenario.name} -> ${entry.testMethod}`);
|
|
3995
4053
|
return {
|
|
3996
4054
|
name: scenario.name,
|
|
3997
4055
|
status: "passed",
|
|
@@ -4011,18 +4069,18 @@ function resolveFromMappingFile(projectRoot2, _feature, scenario) {
|
|
|
4011
4069
|
}
|
|
4012
4070
|
function resolveFromTag(projectRoot2, _feature, scenario) {
|
|
4013
4071
|
const testsDir = join24(projectRoot2, "tests");
|
|
4014
|
-
if (!
|
|
4072
|
+
if (!existsSync22(testsDir)) return null;
|
|
4015
4073
|
try {
|
|
4016
4074
|
const files = findTestFiles(testsDir);
|
|
4017
4075
|
for (const file of files) {
|
|
4018
4076
|
try {
|
|
4019
|
-
const content =
|
|
4077
|
+
const content = readFileSync16(file, "utf-8");
|
|
4020
4078
|
const tagRegex = /@speclore-scenario:\s*(.+)/g;
|
|
4021
4079
|
let match;
|
|
4022
4080
|
while ((match = tagRegex.exec(content)) !== null) {
|
|
4023
4081
|
if (match[1].trim() === scenario.name) {
|
|
4024
4082
|
const methodMatch = findTestMethodNearby(content, match.index);
|
|
4025
|
-
logger.debug(`Tag hit: ${scenario.name}
|
|
4083
|
+
logger.debug(`Tag hit: ${scenario.name} -> ${methodMatch}`);
|
|
4026
4084
|
return {
|
|
4027
4085
|
name: scenario.name,
|
|
4028
4086
|
status: "passed",
|
|
@@ -4196,12 +4254,12 @@ var report_generator_exports = {};
|
|
|
4196
4254
|
__export(report_generator_exports, {
|
|
4197
4255
|
generateReport: () => generateReport
|
|
4198
4256
|
});
|
|
4199
|
-
import { writeFileSync as writeFileSync13, mkdirSync as mkdirSync13, existsSync as
|
|
4257
|
+
import { writeFileSync as writeFileSync13, mkdirSync as mkdirSync13, existsSync as existsSync23, readFileSync as readFileSync17 } from "fs";
|
|
4200
4258
|
import { join as join25, dirname as dirname4 } from "path";
|
|
4201
4259
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
4202
4260
|
function generateReport(report, projectRoot2, config) {
|
|
4203
4261
|
const reportsDir = join25(projectRoot2, ".speclore", "reports");
|
|
4204
|
-
if (!
|
|
4262
|
+
if (!existsSync23(reportsDir)) {
|
|
4205
4263
|
mkdirSync13(reportsDir, { recursive: true });
|
|
4206
4264
|
}
|
|
4207
4265
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
@@ -4259,7 +4317,7 @@ function renderHtmlReport(report) {
|
|
|
4259
4317
|
const templatePath = join25(dirname4(fileURLToPath2(import.meta.url)), "..", "..", "cli", "templates", "report.html");
|
|
4260
4318
|
let template;
|
|
4261
4319
|
try {
|
|
4262
|
-
template =
|
|
4320
|
+
template = readFileSync17(templatePath, "utf-8");
|
|
4263
4321
|
} catch {
|
|
4264
4322
|
template = getInlineTemplate();
|
|
4265
4323
|
}
|
|
@@ -4338,7 +4396,7 @@ var init_verifier = __esm({
|
|
|
4338
4396
|
});
|
|
4339
4397
|
|
|
4340
4398
|
// src/core/analyzer/rdg-builder.ts
|
|
4341
|
-
import { readFileSync as
|
|
4399
|
+
import { readFileSync as readFileSync18, existsSync as existsSync24, readdirSync as readdirSync3 } from "fs";
|
|
4342
4400
|
import { join as join26 } from "path";
|
|
4343
4401
|
var init_rdg_builder = __esm({
|
|
4344
4402
|
"src/core/analyzer/rdg-builder.ts"() {
|
|
@@ -4446,11 +4504,11 @@ var init_analyzer = __esm({
|
|
|
4446
4504
|
});
|
|
4447
4505
|
|
|
4448
4506
|
// src/infra/file-lock.ts
|
|
4449
|
-
import { readFileSync as
|
|
4507
|
+
import { readFileSync as readFileSync22, writeFileSync as writeFileSync16, unlinkSync, existsSync as existsSync29, statSync as statSync3 } from "fs";
|
|
4450
4508
|
import { join as join31 } from "path";
|
|
4451
4509
|
function acquireLock(specLoreDir) {
|
|
4452
4510
|
const lockPath = join31(specLoreDir, LOCK_FILENAME);
|
|
4453
|
-
if (
|
|
4511
|
+
if (existsSync29(lockPath)) {
|
|
4454
4512
|
const existing = readLockInfo(lockPath);
|
|
4455
4513
|
if (existing && !isLockExpired(existing)) {
|
|
4456
4514
|
logger.debug(`Lock already held by PID ${existing.pid}, not expired`);
|
|
@@ -4466,7 +4524,7 @@ function acquireLock(specLoreDir) {
|
|
|
4466
4524
|
try {
|
|
4467
4525
|
writeFileSync16(lockPath, JSON.stringify(lockInfo), { flag: "wx", encoding: "utf-8" });
|
|
4468
4526
|
} catch {
|
|
4469
|
-
logger.debug("Lock write failed
|
|
4527
|
+
logger.debug("Lock write failed - another process may have acquired it");
|
|
4470
4528
|
return false;
|
|
4471
4529
|
}
|
|
4472
4530
|
logger.debug(`Lock acquired by PID ${process.pid}`);
|
|
@@ -4474,7 +4532,7 @@ function acquireLock(specLoreDir) {
|
|
|
4474
4532
|
}
|
|
4475
4533
|
function releaseLock(specLoreDir) {
|
|
4476
4534
|
const lockPath = join31(specLoreDir, LOCK_FILENAME);
|
|
4477
|
-
if (!
|
|
4535
|
+
if (!existsSync29(lockPath)) {
|
|
4478
4536
|
return;
|
|
4479
4537
|
}
|
|
4480
4538
|
const existing = readLockInfo(lockPath);
|
|
@@ -4485,7 +4543,7 @@ function releaseLock(specLoreDir) {
|
|
|
4485
4543
|
}
|
|
4486
4544
|
function readLockInfo(lockPath) {
|
|
4487
4545
|
try {
|
|
4488
|
-
const content =
|
|
4546
|
+
const content = readFileSync22(lockPath, "utf-8");
|
|
4489
4547
|
const parsed = JSON.parse(content);
|
|
4490
4548
|
if (typeof parsed.pid === "number" && typeof parsed.timestamp === "number") {
|
|
4491
4549
|
return parsed;
|
|
@@ -4556,17 +4614,18 @@ var init_schemas = __esm({
|
|
|
4556
4614
|
|
|
4557
4615
|
// src/mcp/tools.ts
|
|
4558
4616
|
import { join as join32 } from "path";
|
|
4559
|
-
import { readFileSync as
|
|
4617
|
+
import { readFileSync as readFileSync23, existsSync as existsSync30, mkdirSync as mkdirSync14, writeFileSync as writeFileSync17 } from "fs";
|
|
4560
4618
|
import { globSync as globSync7 } from "glob";
|
|
4561
4619
|
import { toJSONSchema } from "zod";
|
|
4562
4620
|
async function executeSpecTool(args, projectRoot2) {
|
|
4563
4621
|
if (!args.source || args.source.length > 5e4) {
|
|
4564
4622
|
throw new Error("source must be between 1 and 50000 characters");
|
|
4565
4623
|
}
|
|
4624
|
+
const sanitizedSource = sanitizeSource(args.source);
|
|
4566
4625
|
ensureProjectReady(projectRoot2);
|
|
4567
4626
|
const config = loadConfig(projectRoot2);
|
|
4568
|
-
logger.info(`Reading requirement from: ${
|
|
4569
|
-
const req = await readRequirement(
|
|
4627
|
+
logger.info(`Reading requirement from: ${sanitizedSource.slice(0, 120)}`);
|
|
4628
|
+
const req = await readRequirement(sanitizedSource);
|
|
4570
4629
|
const specLoreDir = join32(projectRoot2, ".speclore");
|
|
4571
4630
|
const context = loadContext(specLoreDir) ?? buildContext(projectRoot2, config);
|
|
4572
4631
|
const feature = await generateFeature(req, context, config, projectRoot2);
|
|
@@ -4717,10 +4776,10 @@ function ensureProjectReady(projectRoot2) {
|
|
|
4717
4776
|
const specLoreDir = join32(projectRoot2, ".speclore");
|
|
4718
4777
|
const configPath = join32(specLoreDir, "config.yaml");
|
|
4719
4778
|
let configCreated = false;
|
|
4720
|
-
if (!
|
|
4779
|
+
if (!existsSync30(specLoreDir)) {
|
|
4721
4780
|
mkdirSync14(specLoreDir, { recursive: true });
|
|
4722
4781
|
}
|
|
4723
|
-
if (!
|
|
4782
|
+
if (!existsSync30(configPath)) {
|
|
4724
4783
|
const projectName = projectRoot2.split(/[/\\]/).pop() ?? "my-project";
|
|
4725
4784
|
writeFileSync17(configPath, generateDefaultConfigYaml(projectName), "utf-8");
|
|
4726
4785
|
configCreated = true;
|
|
@@ -4772,8 +4831,8 @@ function resolveFeatureFiles(patterns, projectRoot2, config) {
|
|
|
4772
4831
|
for (const pattern of searchPatterns) {
|
|
4773
4832
|
const matches = globSync7(pattern, { cwd: projectRoot2, absolute: true });
|
|
4774
4833
|
for (const filePath of matches) {
|
|
4775
|
-
if (
|
|
4776
|
-
const content =
|
|
4834
|
+
if (existsSync30(filePath)) {
|
|
4835
|
+
const content = readFileSync23(filePath, "utf-8");
|
|
4777
4836
|
files.push(parseFeatureFile(filePath, content));
|
|
4778
4837
|
}
|
|
4779
4838
|
}
|
|
@@ -4870,10 +4929,23 @@ var init_tools = __esm({
|
|
|
4870
4929
|
init_config2();
|
|
4871
4930
|
init_logger();
|
|
4872
4931
|
init_schemas();
|
|
4873
|
-
SPEC_TOOL_DESC =
|
|
4874
|
-
|
|
4875
|
-
|
|
4876
|
-
|
|
4932
|
+
SPEC_TOOL_DESC = [
|
|
4933
|
+
"Convert a requirement (text, file path, or URL) into structured BDD .feature files with scenario-level acceptance criteria.",
|
|
4934
|
+
"IMPORTANT: After calling this tool, ALWAYS show the user: (1) the created file path, (2) the list of generated scenarios, (3) the recommended next step."
|
|
4935
|
+
].join(" ");
|
|
4936
|
+
CODE_TOOL_DESC = [
|
|
4937
|
+
"Generate AI coding constraint files for Cursor / Claude Code / Qoder based on .feature specs and project context. Requires .feature files to exist.",
|
|
4938
|
+
"IMPORTANT: After calling this tool, ALWAYS show the user: (1) which constraint files were written, (2) test scaffold files generated, (3) the recommended next step."
|
|
4939
|
+
].join(" ");
|
|
4940
|
+
VERIFY_TOOL_DESC = [
|
|
4941
|
+
"Run tests and map results back to .feature scenarios, producing an acceptance report. Requires test scaffolding generated by speclore.code.",
|
|
4942
|
+
"IMPORTANT: After calling this tool, ALWAYS show the user: (1) pass/fail summary, (2) any failed scenario details, (3) the recommended next step."
|
|
4943
|
+
].join(" ");
|
|
4944
|
+
STATUS_TOOL_DESC = [
|
|
4945
|
+
"Get current project workflow status, feature states, and recommended next steps.",
|
|
4946
|
+
"BEST PRACTICE: Call this tool FIRST when starting a new conversation about the project to understand where it stands.",
|
|
4947
|
+
"ALWAYS show the user the full status including: project state, feature list, and recommended actions."
|
|
4948
|
+
].join(" ");
|
|
4877
4949
|
specJsonSchema = toJSONSchema(specInputSchema, { target: "draft-07" });
|
|
4878
4950
|
codeJsonSchema = toJSONSchema(codeInputSchema, { target: "draft-07" });
|
|
4879
4951
|
verifyJsonSchema = toJSONSchema(verifyInputSchema, { target: "draft-07" });
|
|
@@ -4881,7 +4953,7 @@ var init_tools = __esm({
|
|
|
4881
4953
|
});
|
|
4882
4954
|
|
|
4883
4955
|
// src/mcp/status.ts
|
|
4884
|
-
import { readFileSync as
|
|
4956
|
+
import { readFileSync as readFileSync24, existsSync as existsSync31, mkdirSync as mkdirSync15, writeFileSync as writeFileSync18 } from "fs";
|
|
4885
4957
|
import { join as join33 } from "path";
|
|
4886
4958
|
import { globSync as globSync8 } from "glob";
|
|
4887
4959
|
function executeStatusTool(args, projectRoot2) {
|
|
@@ -4898,9 +4970,9 @@ function executeStatusTool(args, projectRoot2) {
|
|
|
4898
4970
|
continue;
|
|
4899
4971
|
}
|
|
4900
4972
|
let scenarioCount = 0;
|
|
4901
|
-
if (
|
|
4973
|
+
if (existsSync31(path)) {
|
|
4902
4974
|
try {
|
|
4903
|
-
const content =
|
|
4975
|
+
const content = readFileSync24(path, "utf-8");
|
|
4904
4976
|
const matches = content.match(/Scenario(?: Outline)?:/g);
|
|
4905
4977
|
scenarioCount = matches?.length ?? 0;
|
|
4906
4978
|
} catch {
|
|
@@ -4923,7 +4995,7 @@ function executeStatusTool(args, projectRoot2) {
|
|
|
4923
4995
|
if (args.feature && !filePath.includes(args.feature)) continue;
|
|
4924
4996
|
let scenarioCount = 0;
|
|
4925
4997
|
try {
|
|
4926
|
-
const content =
|
|
4998
|
+
const content = readFileSync24(filePath, "utf-8");
|
|
4927
4999
|
const matches = content.match(/Scenario(?: Outline)?:/g);
|
|
4928
5000
|
scenarioCount = matches?.length ?? 0;
|
|
4929
5001
|
} catch {
|
|
@@ -4983,10 +5055,10 @@ function ensureProjectReadyForStatus(projectRoot2) {
|
|
|
4983
5055
|
const specLoreDir = join33(projectRoot2, ".speclore");
|
|
4984
5056
|
const configPath = join33(specLoreDir, "config.yaml");
|
|
4985
5057
|
let configCreated = false;
|
|
4986
|
-
if (!
|
|
5058
|
+
if (!existsSync31(specLoreDir)) {
|
|
4987
5059
|
mkdirSync15(specLoreDir, { recursive: true });
|
|
4988
5060
|
}
|
|
4989
|
-
if (!
|
|
5061
|
+
if (!existsSync31(configPath)) {
|
|
4990
5062
|
const projectName = projectRoot2.split(/[/\\]/).pop() ?? "my-project";
|
|
4991
5063
|
writeFileSync18(configPath, generateDefaultConfigYaml(projectName), "utf-8");
|
|
4992
5064
|
configCreated = true;
|
|
@@ -5030,10 +5102,10 @@ async function startMcpServer() {
|
|
|
5030
5102
|
mkdirSync16(specLoreDir, { recursive: true });
|
|
5031
5103
|
const locked = acquireLock(specLoreDir);
|
|
5032
5104
|
if (!locked) {
|
|
5033
|
-
logger.warn("Could not acquire lock
|
|
5105
|
+
logger.warn("Could not acquire lock - another SpecLore instance may be running.");
|
|
5034
5106
|
}
|
|
5035
5107
|
} catch {
|
|
5036
|
-
logger.warn("Could not acquire lock
|
|
5108
|
+
logger.warn("Could not acquire lock - .speclore/ directory may not exist.");
|
|
5037
5109
|
}
|
|
5038
5110
|
const transport = new StdioServerTransport();
|
|
5039
5111
|
await server.connect(transport);
|
|
@@ -5053,6 +5125,58 @@ async function startMcpServer() {
|
|
|
5053
5125
|
process.exit(0);
|
|
5054
5126
|
});
|
|
5055
5127
|
}
|
|
5128
|
+
function buildSpecSummary(result) {
|
|
5129
|
+
const lines = [];
|
|
5130
|
+
lines.push("[SpecLore] Feature file generated successfully.");
|
|
5131
|
+
lines.push(` File: ${result.createdFiles.join(", ")}`);
|
|
5132
|
+
lines.push(` Scenarios: ${result.scenarios.length}`);
|
|
5133
|
+
for (const sc of result.scenarios) {
|
|
5134
|
+
lines.push(` - ${sc.name}`);
|
|
5135
|
+
}
|
|
5136
|
+
lines.push(` Next step: ${result.nextSteps}`);
|
|
5137
|
+
return lines.join("\n");
|
|
5138
|
+
}
|
|
5139
|
+
function buildCodeSummary(result) {
|
|
5140
|
+
const lines = [];
|
|
5141
|
+
lines.push("[SpecLore] Constraints and test scaffolding generated.");
|
|
5142
|
+
lines.push(` Constraint files: ${result.writtenFiles.length > 0 ? result.writtenFiles.join(", ") : "none"}`);
|
|
5143
|
+
lines.push(` Test scaffold files: ${result.scaffoldFiles.length > 0 ? result.scaffoldFiles.map((s) => s.testFile).join(", ") : "none"}`);
|
|
5144
|
+
lines.push(` Next step: ${result.workflow.nextStep}`);
|
|
5145
|
+
return lines.join("\n");
|
|
5146
|
+
}
|
|
5147
|
+
function buildVerifySummary(result) {
|
|
5148
|
+
const lines = [];
|
|
5149
|
+
const icon = result.failed === 0 ? "[PASS]" : "[FAIL]";
|
|
5150
|
+
lines.push(`[SpecLore] Verification ${icon} ${result.summary}`);
|
|
5151
|
+
if (result.failed > 0) {
|
|
5152
|
+
lines.push(` Failed: ${result.failed} scenario(s)`);
|
|
5153
|
+
for (const d of result.failedDetails) {
|
|
5154
|
+
lines.push(` - ${d.feature} > ${d.scenario}: ${d.error}`);
|
|
5155
|
+
}
|
|
5156
|
+
}
|
|
5157
|
+
if (result.unmapped > 0) {
|
|
5158
|
+
lines.push(` Unmapped: ${result.unmapped} scenario(s) without test mapping`);
|
|
5159
|
+
}
|
|
5160
|
+
lines.push(` Next step: ${result.workflow.nextStep}`);
|
|
5161
|
+
return lines.join("\n");
|
|
5162
|
+
}
|
|
5163
|
+
function buildStatusSummary(result) {
|
|
5164
|
+
const lines = [];
|
|
5165
|
+
lines.push(`[SpecLore] Project status: ${result.project.initialized ? "initialized" : "not initialized"}`);
|
|
5166
|
+
lines.push(` AI tools detected: ${result.project.aiToolsDetected.length > 0 ? result.project.aiToolsDetected.join(", ") : "none"}`);
|
|
5167
|
+
lines.push(` Features: ${result.summary.total} total`);
|
|
5168
|
+
if (result.summary.specified > 0) lines.push(` - ${result.summary.specified} specified`);
|
|
5169
|
+
if (result.summary.constrained > 0) lines.push(` - ${result.summary.constrained} constrained`);
|
|
5170
|
+
if (result.summary.coding > 0) lines.push(` - ${result.summary.coding} coding`);
|
|
5171
|
+
if (result.summary.verified > 0) lines.push(` - ${result.summary.verified} verified`);
|
|
5172
|
+
if (result.recommendedActions.length > 0) {
|
|
5173
|
+
lines.push(" Recommended actions:");
|
|
5174
|
+
for (const action of result.recommendedActions) {
|
|
5175
|
+
lines.push(` -> ${action}`);
|
|
5176
|
+
}
|
|
5177
|
+
}
|
|
5178
|
+
return lines.join("\n");
|
|
5179
|
+
}
|
|
5056
5180
|
var server, projectRoot;
|
|
5057
5181
|
var init_server = __esm({
|
|
5058
5182
|
"src/mcp/server.ts"() {
|
|
@@ -5081,7 +5205,11 @@ var init_server = __esm({
|
|
|
5081
5205
|
{ source: args.source, module: args.module },
|
|
5082
5206
|
getProjectRoot()
|
|
5083
5207
|
);
|
|
5084
|
-
|
|
5208
|
+
const summary = buildSpecSummary(result);
|
|
5209
|
+
return { content: [{ type: "text", text: `${summary}
|
|
5210
|
+
|
|
5211
|
+
---
|
|
5212
|
+
${JSON.stringify(result, null, 2)}` }] };
|
|
5085
5213
|
}
|
|
5086
5214
|
);
|
|
5087
5215
|
server.registerTool(
|
|
@@ -5098,7 +5226,11 @@ var init_server = __esm({
|
|
|
5098
5226
|
{ features: args.features, tools: args.tools },
|
|
5099
5227
|
getProjectRoot()
|
|
5100
5228
|
);
|
|
5101
|
-
|
|
5229
|
+
const summary = buildCodeSummary(result);
|
|
5230
|
+
return { content: [{ type: "text", text: `${summary}
|
|
5231
|
+
|
|
5232
|
+
---
|
|
5233
|
+
${JSON.stringify(result, null, 2)}` }] };
|
|
5102
5234
|
}
|
|
5103
5235
|
);
|
|
5104
5236
|
server.registerTool(
|
|
@@ -5112,7 +5244,11 @@ var init_server = __esm({
|
|
|
5112
5244
|
{ features: args.features, impact: args.impact },
|
|
5113
5245
|
getProjectRoot()
|
|
5114
5246
|
);
|
|
5115
|
-
|
|
5247
|
+
const summary = buildVerifySummary(result);
|
|
5248
|
+
return { content: [{ type: "text", text: `${summary}
|
|
5249
|
+
|
|
5250
|
+
---
|
|
5251
|
+
${JSON.stringify(result, null, 2)}` }] };
|
|
5116
5252
|
}
|
|
5117
5253
|
);
|
|
5118
5254
|
server.registerTool(
|
|
@@ -5126,7 +5262,11 @@ var init_server = __esm({
|
|
|
5126
5262
|
{ feature: args.feature },
|
|
5127
5263
|
getProjectRoot()
|
|
5128
5264
|
);
|
|
5129
|
-
|
|
5265
|
+
const summary = buildStatusSummary(result);
|
|
5266
|
+
return { content: [{ type: "text", text: `${summary}
|
|
5267
|
+
|
|
5268
|
+
---
|
|
5269
|
+
${JSON.stringify(result, null, 2)}` }] };
|
|
5130
5270
|
}
|
|
5131
5271
|
);
|
|
5132
5272
|
projectRoot = process.env.SPECLORE_PROJECT_ROOT ?? process.cwd();
|
|
@@ -5140,18 +5280,18 @@ import { Command } from "commander";
|
|
|
5140
5280
|
init_logger();
|
|
5141
5281
|
init_config2();
|
|
5142
5282
|
init_detector();
|
|
5143
|
-
import { existsSync as
|
|
5144
|
-
import { join as
|
|
5283
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
5284
|
+
import { join as join8 } from "path";
|
|
5145
5285
|
|
|
5146
5286
|
// src/setup/config-writer.ts
|
|
5147
|
-
import { writeFileSync as writeFileSync2, existsSync as
|
|
5148
|
-
import { join as
|
|
5287
|
+
import { writeFileSync as writeFileSync2, existsSync as existsSync5, readFileSync as readFileSync4, mkdirSync as mkdirSync2 } from "fs";
|
|
5288
|
+
import { join as join6 } from "path";
|
|
5149
5289
|
|
|
5150
5290
|
// src/infra/install-detector.ts
|
|
5151
5291
|
init_logger();
|
|
5152
5292
|
init_version();
|
|
5153
|
-
import { readFileSync as readFileSync3, existsSync as
|
|
5154
|
-
import { join as
|
|
5293
|
+
import { readFileSync as readFileSync3, existsSync as existsSync4, writeFileSync, mkdirSync } from "fs";
|
|
5294
|
+
import { join as join5 } from "path";
|
|
5155
5295
|
import { createRequire } from "module";
|
|
5156
5296
|
var INSTALL_PATH_FILE = ".install-path";
|
|
5157
5297
|
function detectInstallMethod(projectRoot2) {
|
|
@@ -5179,13 +5319,13 @@ function detectNpmInstall() {
|
|
|
5179
5319
|
}
|
|
5180
5320
|
}
|
|
5181
5321
|
function detectCloneInstall(projectRoot2) {
|
|
5182
|
-
const installPathFile =
|
|
5183
|
-
if (!
|
|
5322
|
+
const installPathFile = join5(projectRoot2, ".speclore", INSTALL_PATH_FILE);
|
|
5323
|
+
if (!existsSync4(installPathFile)) {
|
|
5184
5324
|
return null;
|
|
5185
5325
|
}
|
|
5186
5326
|
try {
|
|
5187
5327
|
const localPath = readFileSync3(installPathFile, "utf-8").trim();
|
|
5188
|
-
if (
|
|
5328
|
+
if (existsSync4(localPath)) {
|
|
5189
5329
|
return { mode: "clone", version: VERSION, localPath };
|
|
5190
5330
|
}
|
|
5191
5331
|
} catch {
|
|
@@ -5195,6 +5335,7 @@ function detectCloneInstall(projectRoot2) {
|
|
|
5195
5335
|
|
|
5196
5336
|
// src/setup/config-writer.ts
|
|
5197
5337
|
init_logger();
|
|
5338
|
+
init_path_utils();
|
|
5198
5339
|
function writeMcpConfig(projectRoot2, tools, _globalMode) {
|
|
5199
5340
|
const installInfo = detectInstallMethod(projectRoot2);
|
|
5200
5341
|
const serverCommand = getServerCommand(installInfo);
|
|
@@ -5217,19 +5358,21 @@ function writeMcpForClient(projectRoot2, client) {
|
|
|
5217
5358
|
const serverCommand = getServerCommand(installInfo);
|
|
5218
5359
|
switch (client) {
|
|
5219
5360
|
case "cursor":
|
|
5220
|
-
mkdirSync2(
|
|
5361
|
+
mkdirSync2(join6(projectRoot2, ".cursor"), { recursive: true });
|
|
5221
5362
|
writeCursorMcp(projectRoot2, serverCommand);
|
|
5222
5363
|
break;
|
|
5223
5364
|
case "claude":
|
|
5224
|
-
if (!
|
|
5225
|
-
mkdirSync2(
|
|
5365
|
+
if (!existsSync5(join6(projectRoot2, ".claude"))) {
|
|
5366
|
+
mkdirSync2(join6(projectRoot2, ".claude"), { recursive: true });
|
|
5226
5367
|
}
|
|
5227
5368
|
writeClaudeMcp(projectRoot2, serverCommand);
|
|
5228
5369
|
break;
|
|
5229
|
-
case "qoder":
|
|
5230
|
-
|
|
5370
|
+
case "qoder": {
|
|
5371
|
+
const qoderDir = resolveQoderDir(projectRoot2);
|
|
5372
|
+
mkdirSync2(join6(projectRoot2, qoderDir), { recursive: true });
|
|
5231
5373
|
writeQoderMcp(projectRoot2, serverCommand);
|
|
5232
5374
|
break;
|
|
5375
|
+
}
|
|
5233
5376
|
}
|
|
5234
5377
|
}
|
|
5235
5378
|
function getServerCommand(installInfo) {
|
|
@@ -5237,12 +5380,12 @@ function getServerCommand(installInfo) {
|
|
|
5237
5380
|
return { command: "npx", args: ["speclore", "mcp"] };
|
|
5238
5381
|
}
|
|
5239
5382
|
const localPath = installInfo.localPath ?? process.cwd();
|
|
5240
|
-
return { command: "node", args: [
|
|
5383
|
+
return { command: "node", args: [join6(localPath, "dist", "mcp", "server.js")] };
|
|
5241
5384
|
}
|
|
5242
5385
|
function writeCursorMcp(projectRoot2, serverCmd) {
|
|
5243
|
-
const mcpDir =
|
|
5244
|
-
if (!
|
|
5245
|
-
const mcpPath =
|
|
5386
|
+
const mcpDir = join6(projectRoot2, ".cursor");
|
|
5387
|
+
if (!existsSync5(mcpDir)) return;
|
|
5388
|
+
const mcpPath = join6(mcpDir, "mcp.json");
|
|
5246
5389
|
const existing = readExistingJson(mcpPath);
|
|
5247
5390
|
if (!existing.mcpServers) existing.mcpServers = {};
|
|
5248
5391
|
existing.mcpServers.speclore = {
|
|
@@ -5254,10 +5397,10 @@ function writeCursorMcp(projectRoot2, serverCmd) {
|
|
|
5254
5397
|
logger.info(` Cursor: ${mcpPath}`);
|
|
5255
5398
|
}
|
|
5256
5399
|
function writeClaudeMcp(projectRoot2, serverCmd) {
|
|
5257
|
-
const hasClaudeDir =
|
|
5258
|
-
const hasClaudeMd =
|
|
5400
|
+
const hasClaudeDir = existsSync5(join6(projectRoot2, ".claude"));
|
|
5401
|
+
const hasClaudeMd = existsSync5(join6(projectRoot2, "CLAUDE.md"));
|
|
5259
5402
|
if (!hasClaudeDir && !hasClaudeMd) return;
|
|
5260
|
-
const mcpPath =
|
|
5403
|
+
const mcpPath = join6(projectRoot2, ".mcp.json");
|
|
5261
5404
|
const existing = readExistingJson(mcpPath);
|
|
5262
5405
|
if (!existing.mcpServers) existing.mcpServers = {};
|
|
5263
5406
|
existing.mcpServers.speclore = {
|
|
@@ -5269,9 +5412,10 @@ function writeClaudeMcp(projectRoot2, serverCmd) {
|
|
|
5269
5412
|
logger.info(` Claude Code: ${mcpPath}`);
|
|
5270
5413
|
}
|
|
5271
5414
|
function writeQoderMcp(projectRoot2, serverCmd) {
|
|
5272
|
-
const
|
|
5273
|
-
|
|
5274
|
-
|
|
5415
|
+
const qoderDir = resolveQoderDir(projectRoot2);
|
|
5416
|
+
const mcpDir = join6(projectRoot2, qoderDir);
|
|
5417
|
+
if (!existsSync5(mcpDir)) return;
|
|
5418
|
+
const mcpPath = join6(mcpDir, "mcp.json");
|
|
5275
5419
|
const existing = readExistingJson(mcpPath);
|
|
5276
5420
|
if (!existing.mcpServers) existing.mcpServers = {};
|
|
5277
5421
|
existing.mcpServers.speclore = {
|
|
@@ -5283,7 +5427,7 @@ function writeQoderMcp(projectRoot2, serverCmd) {
|
|
|
5283
5427
|
logger.info(` Qoder: ${mcpPath}`);
|
|
5284
5428
|
}
|
|
5285
5429
|
function readExistingJson(filePath) {
|
|
5286
|
-
if (
|
|
5430
|
+
if (existsSync5(filePath)) {
|
|
5287
5431
|
try {
|
|
5288
5432
|
return JSON.parse(readFileSync4(filePath, "utf-8"));
|
|
5289
5433
|
} catch {
|
|
@@ -5294,9 +5438,10 @@ function readExistingJson(filePath) {
|
|
|
5294
5438
|
}
|
|
5295
5439
|
|
|
5296
5440
|
// src/setup/rule-writer.ts
|
|
5441
|
+
init_path_utils();
|
|
5297
5442
|
init_logger();
|
|
5298
5443
|
import { writeFileSync as writeFileSync3, mkdirSync as mkdirSync3 } from "fs";
|
|
5299
|
-
import { join as
|
|
5444
|
+
import { join as join7 } from "path";
|
|
5300
5445
|
function writeRuleFiles(projectRoot2, tools) {
|
|
5301
5446
|
for (const tool of tools) {
|
|
5302
5447
|
switch (tool.tool) {
|
|
@@ -5313,7 +5458,7 @@ function writeRuleFiles(projectRoot2, tools) {
|
|
|
5313
5458
|
}
|
|
5314
5459
|
}
|
|
5315
5460
|
function writeCursorRule(projectRoot2) {
|
|
5316
|
-
const rulesDir =
|
|
5461
|
+
const rulesDir = join7(projectRoot2, ".cursor", "rules");
|
|
5317
5462
|
mkdirSync3(rulesDir, { recursive: true });
|
|
5318
5463
|
const content = `---
|
|
5319
5464
|
description: SpecLore \u2014 AI coding constraints (setup placeholder)
|
|
@@ -5331,11 +5476,11 @@ SpecLore is configured for this project. When the \`speclore.spec\` MCP tool is
|
|
|
5331
5476
|
|
|
5332
5477
|
> This is a placeholder. Run \`speclore code\` to generate full constraints with module boundaries.
|
|
5333
5478
|
`;
|
|
5334
|
-
writeFileSync3(
|
|
5479
|
+
writeFileSync3(join7(rulesDir, "speclore.mdc"), content, "utf-8");
|
|
5335
5480
|
logger.info(` Cursor rule: .cursor/rules/speclore.mdc`);
|
|
5336
5481
|
}
|
|
5337
5482
|
function writeClaudeRule(projectRoot2) {
|
|
5338
|
-
const rulesDir =
|
|
5483
|
+
const rulesDir = join7(projectRoot2, ".claude", "rules");
|
|
5339
5484
|
mkdirSync3(rulesDir, { recursive: true });
|
|
5340
5485
|
const content = `# SpecLore
|
|
5341
5486
|
|
|
@@ -5347,11 +5492,12 @@ SpecLore is configured for this project. When the \`speclore.spec\` MCP tool is
|
|
|
5347
5492
|
|
|
5348
5493
|
> This is a placeholder. Run \`speclore code\` to generate full constraints with module boundaries.
|
|
5349
5494
|
`;
|
|
5350
|
-
writeFileSync3(
|
|
5495
|
+
writeFileSync3(join7(rulesDir, "speclore.md"), content, "utf-8");
|
|
5351
5496
|
logger.info(` Claude Code rule: .claude/rules/speclore.md`);
|
|
5352
5497
|
}
|
|
5353
5498
|
function writeQoderRule(projectRoot2) {
|
|
5354
|
-
const
|
|
5499
|
+
const qoderDir = resolveQoderDir(projectRoot2);
|
|
5500
|
+
const rulesDir = join7(projectRoot2, qoderDir, "rules");
|
|
5355
5501
|
mkdirSync3(rulesDir, { recursive: true });
|
|
5356
5502
|
const content = `# SpecLore
|
|
5357
5503
|
|
|
@@ -5363,8 +5509,8 @@ SpecLore is configured for this project. When the \`speclore.spec\` MCP tool is
|
|
|
5363
5509
|
|
|
5364
5510
|
> This is a placeholder. Run \`speclore code\` to generate full constraints with module boundaries.
|
|
5365
5511
|
`;
|
|
5366
|
-
writeFileSync3(
|
|
5367
|
-
logger.info(` Qoder rule:
|
|
5512
|
+
writeFileSync3(join7(rulesDir, "speclore.md"), content, "utf-8");
|
|
5513
|
+
logger.info(` Qoder rule: ${qoderDir}/rules/speclore.md`);
|
|
5368
5514
|
}
|
|
5369
5515
|
|
|
5370
5516
|
// src/cli/commands/setup.ts
|
|
@@ -5373,26 +5519,26 @@ function registerSetupCommand(program) {
|
|
|
5373
5519
|
if (opts.verbose) logger.setLevel("debug");
|
|
5374
5520
|
const projectRoot2 = process.cwd();
|
|
5375
5521
|
logger.info("SpecLore Setup");
|
|
5376
|
-
logger.info("
|
|
5522
|
+
logger.info("---------------");
|
|
5377
5523
|
const tools = detectAITools(projectRoot2);
|
|
5378
5524
|
if (tools.length === 0) {
|
|
5379
5525
|
logger.warn("No supported AI tools detected.");
|
|
5380
5526
|
logger.info("");
|
|
5381
5527
|
logger.info(" To enable MCP integration, open your AI client in this project first,");
|
|
5382
5528
|
logger.info(" then re-run setup. Or use manual configuration:");
|
|
5383
|
-
logger.info(" speclore mcp add cursor
|
|
5384
|
-
logger.info(" speclore mcp add claude
|
|
5385
|
-
logger.info(" speclore mcp add qoder
|
|
5529
|
+
logger.info(" speclore mcp add cursor - configure for Cursor");
|
|
5530
|
+
logger.info(" speclore mcp add claude - configure for Claude Code");
|
|
5531
|
+
logger.info(" speclore mcp add qoder - configure for Qoder");
|
|
5386
5532
|
logger.info("");
|
|
5387
5533
|
} else {
|
|
5388
5534
|
logger.info(`Detected AI tools: ${tools.map((t) => t.tool).join(", ")}`);
|
|
5389
5535
|
}
|
|
5390
|
-
const specLoreDir = opts.global ?
|
|
5391
|
-
if (!
|
|
5536
|
+
const specLoreDir = opts.global ? join8(process.env.HOME ?? process.env.USERPROFILE ?? "~", ".speclore") : join8(projectRoot2, ".speclore");
|
|
5537
|
+
if (!existsSync6(specLoreDir)) {
|
|
5392
5538
|
mkdirSync4(specLoreDir, { recursive: true });
|
|
5393
5539
|
}
|
|
5394
|
-
const configPath =
|
|
5395
|
-
if (!
|
|
5540
|
+
const configPath = join8(specLoreDir, "config.yaml");
|
|
5541
|
+
if (!existsSync6(configPath)) {
|
|
5396
5542
|
const projectName = projectRoot2.split(/[/\\]/).pop() ?? "my-project";
|
|
5397
5543
|
writeFileSync4(configPath, generateDefaultConfigYaml(projectName), "utf-8");
|
|
5398
5544
|
logger.info(`Created config: ${configPath}`);
|
|
@@ -5402,18 +5548,18 @@ function registerSetupCommand(program) {
|
|
|
5402
5548
|
logger.info("");
|
|
5403
5549
|
logger.info("Setup complete! Next steps:");
|
|
5404
5550
|
logger.info("");
|
|
5405
|
-
logger.info(" Option A
|
|
5406
|
-
logger.info(' speclore spec "your requirement"
|
|
5407
|
-
logger.info(" speclore code
|
|
5408
|
-
logger.info(" speclore verify
|
|
5551
|
+
logger.info(" Option A - CLI workflow (terminal users):");
|
|
5552
|
+
logger.info(' speclore spec "your requirement" -> generate .feature');
|
|
5553
|
+
logger.info(" speclore code -> generate constraints + tests");
|
|
5554
|
+
logger.info(" speclore verify -> run acceptance");
|
|
5409
5555
|
logger.info("");
|
|
5410
|
-
logger.info(" Option B
|
|
5556
|
+
logger.info(" Option B - AI client workflow (recommended):");
|
|
5411
5557
|
logger.info(" Open Cursor / Qoder / Claude Code and start chatting.");
|
|
5412
|
-
logger.info(" MCP is already configured
|
|
5558
|
+
logger.info(" MCP is already configured - AI handles the full pipeline.");
|
|
5413
5559
|
logger.info("");
|
|
5414
|
-
logger.info(" Optional
|
|
5415
|
-
logger.info(" speclore init
|
|
5416
|
-
logger.info(" (Not required
|
|
5560
|
+
logger.info(" Optional - Pre-scan project context:");
|
|
5561
|
+
logger.info(" speclore init -> scan modules/entities/APIs for better AI context");
|
|
5562
|
+
logger.info(" (Not required - context is auto-built on first spec/code call)");
|
|
5417
5563
|
});
|
|
5418
5564
|
}
|
|
5419
5565
|
|
|
@@ -5427,7 +5573,7 @@ function registerInitCommand(program) {
|
|
|
5427
5573
|
if (opts.verbose) logger.setLevel("debug");
|
|
5428
5574
|
const projectRoot2 = process.cwd();
|
|
5429
5575
|
logger.info("SpecLore Init");
|
|
5430
|
-
logger.info("
|
|
5576
|
+
logger.info("--------------");
|
|
5431
5577
|
const config = loadConfig(projectRoot2);
|
|
5432
5578
|
logger.info(`Project: ${config.project.name || "(unnamed)"}`);
|
|
5433
5579
|
const context = buildContext(projectRoot2, config);
|
|
@@ -5458,11 +5604,12 @@ function registerSpecCommand(program) {
|
|
|
5458
5604
|
program.command("spec").description("Convert a requirement source into BDD .feature file(s)").argument("<source>", "Requirement source: file path, URL, or text").option("-m, --module <name>", "Target module name").option("-v, --verbose", "Enable debug logging").action(async (source, opts) => {
|
|
5459
5605
|
if (opts.verbose) logger.setLevel("debug");
|
|
5460
5606
|
const projectRoot2 = process.cwd();
|
|
5461
|
-
|
|
5607
|
+
const cleanedSource = sanitizeSource(source);
|
|
5608
|
+
logger.info(`Reading requirement: ${cleanedSource.slice(0, 120)}`);
|
|
5462
5609
|
const config = loadConfig(projectRoot2);
|
|
5463
5610
|
const specLoreDir = join18(projectRoot2, ".speclore");
|
|
5464
5611
|
const context = loadContext(specLoreDir) ?? buildContext(projectRoot2, config);
|
|
5465
|
-
const requirements = await readRequirement(
|
|
5612
|
+
const requirements = await readRequirement(cleanedSource);
|
|
5466
5613
|
const features = await generateFeature(requirements, context, config, projectRoot2);
|
|
5467
5614
|
logger.info(`Created: ${features.path} (${features.scenarios.length} scenarios)`);
|
|
5468
5615
|
if (features.needsReview.length > 0) {
|
|
@@ -5487,7 +5634,7 @@ init_test_scaffolder();
|
|
|
5487
5634
|
init_context_engine();
|
|
5488
5635
|
init_state_manager();
|
|
5489
5636
|
import { join as join23 } from "path";
|
|
5490
|
-
import { readFileSync as
|
|
5637
|
+
import { readFileSync as readFileSync15, existsSync as existsSync21 } from "fs";
|
|
5491
5638
|
import { globSync as globSync4 } from "glob";
|
|
5492
5639
|
function registerCodeCommand(program) {
|
|
5493
5640
|
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) => {
|
|
@@ -5542,8 +5689,8 @@ function resolveFeatures(patterns, projectRoot2, config) {
|
|
|
5542
5689
|
for (const pattern of searchPatterns) {
|
|
5543
5690
|
const matches = globSync4(pattern, { cwd: projectRoot2, absolute: true });
|
|
5544
5691
|
for (const filePath of matches) {
|
|
5545
|
-
if (
|
|
5546
|
-
const content =
|
|
5692
|
+
if (existsSync21(filePath)) {
|
|
5693
|
+
const content = readFileSync15(filePath, "utf-8");
|
|
5547
5694
|
const featureMatch = content.match(/Feature:\s*(.+)/);
|
|
5548
5695
|
files.push({
|
|
5549
5696
|
path: filePath,
|
|
@@ -5567,7 +5714,7 @@ init_context_engine();
|
|
|
5567
5714
|
init_analyzer();
|
|
5568
5715
|
init_state_manager();
|
|
5569
5716
|
import { join as join27 } from "path";
|
|
5570
|
-
import { readFileSync as
|
|
5717
|
+
import { readFileSync as readFileSync19, existsSync as existsSync25 } from "fs";
|
|
5571
5718
|
import { globSync as globSync6 } from "glob";
|
|
5572
5719
|
function registerVerifyCommand(program) {
|
|
5573
5720
|
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) => {
|
|
@@ -5653,8 +5800,8 @@ function resolveFeatures2(patterns, projectRoot2, config) {
|
|
|
5653
5800
|
for (const pattern of searchPatterns) {
|
|
5654
5801
|
const matches = globSync6(pattern, { cwd: projectRoot2, absolute: true });
|
|
5655
5802
|
for (const filePath of matches) {
|
|
5656
|
-
if (
|
|
5657
|
-
const content =
|
|
5803
|
+
if (existsSync25(filePath)) {
|
|
5804
|
+
const content = readFileSync19(filePath, "utf-8");
|
|
5658
5805
|
const featureMatch = content.match(/Feature:\s*(.+)/);
|
|
5659
5806
|
files.push({
|
|
5660
5807
|
path: filePath,
|
|
@@ -5675,36 +5822,38 @@ init_logger();
|
|
|
5675
5822
|
|
|
5676
5823
|
// src/setup/cleanup.ts
|
|
5677
5824
|
init_logger();
|
|
5678
|
-
|
|
5825
|
+
init_path_utils();
|
|
5826
|
+
import { rmSync as rmSync4, existsSync as existsSync26, readFileSync as readFileSync20, writeFileSync as writeFileSync14 } from "fs";
|
|
5679
5827
|
import { join as join28 } from "path";
|
|
5680
5828
|
function runTeardown(projectRoot2, globalMode) {
|
|
5681
5829
|
if (globalMode) {
|
|
5682
5830
|
const homeDir = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
5683
5831
|
const globalDir = join28(homeDir, ".speclore");
|
|
5684
|
-
if (
|
|
5832
|
+
if (existsSync26(globalDir)) {
|
|
5685
5833
|
rmSync4(globalDir, { recursive: true, force: true });
|
|
5686
5834
|
logger.info(`Removed: ${globalDir}`);
|
|
5687
5835
|
}
|
|
5688
5836
|
} else {
|
|
5689
5837
|
const specLoreDir = join28(projectRoot2, ".speclore");
|
|
5690
|
-
if (
|
|
5838
|
+
if (existsSync26(specLoreDir)) {
|
|
5691
5839
|
rmSync4(specLoreDir, { recursive: true, force: true });
|
|
5692
5840
|
logger.info(`Removed: ${specLoreDir}`);
|
|
5693
5841
|
}
|
|
5694
5842
|
removeMcpEntry(join28(projectRoot2, ".cursor", "mcp.json"), "speclore");
|
|
5695
5843
|
removeMcpEntry(join28(projectRoot2, ".mcp.json"), "speclore");
|
|
5696
|
-
|
|
5844
|
+
const qoderDir = resolveQoderDir(projectRoot2);
|
|
5845
|
+
removeMcpEntry(join28(projectRoot2, qoderDir, "mcp.json"), "speclore");
|
|
5697
5846
|
removeIfExists(join28(projectRoot2, ".cursor", "rules", "speclore.mdc"));
|
|
5698
5847
|
removeIfExists(join28(projectRoot2, ".claude", "rules", "speclore.md"));
|
|
5699
|
-
removeIfExists(join28(projectRoot2,
|
|
5848
|
+
removeIfExists(join28(projectRoot2, qoderDir, "rules", "speclore.md"));
|
|
5700
5849
|
logger.info("Project teardown complete.");
|
|
5701
5850
|
}
|
|
5702
5851
|
return Promise.resolve();
|
|
5703
5852
|
}
|
|
5704
5853
|
function removeMcpEntry(mcpPath, serverName) {
|
|
5705
|
-
if (!
|
|
5854
|
+
if (!existsSync26(mcpPath)) return;
|
|
5706
5855
|
try {
|
|
5707
|
-
const config = JSON.parse(
|
|
5856
|
+
const config = JSON.parse(readFileSync20(mcpPath, "utf-8"));
|
|
5708
5857
|
if (config.mcpServers?.[serverName]) {
|
|
5709
5858
|
delete config.mcpServers[serverName];
|
|
5710
5859
|
writeFileSync14(mcpPath, JSON.stringify(config, null, 2), "utf-8");
|
|
@@ -5714,7 +5863,7 @@ function removeMcpEntry(mcpPath, serverName) {
|
|
|
5714
5863
|
}
|
|
5715
5864
|
}
|
|
5716
5865
|
function removeIfExists(filePath) {
|
|
5717
|
-
if (
|
|
5866
|
+
if (existsSync26(filePath)) {
|
|
5718
5867
|
rmSync4(filePath);
|
|
5719
5868
|
logger.info(` Removed: ${filePath}`);
|
|
5720
5869
|
}
|
|
@@ -5748,7 +5897,7 @@ init_config2();
|
|
|
5748
5897
|
init_logger();
|
|
5749
5898
|
init_state_manager();
|
|
5750
5899
|
import { join as join29 } from "path";
|
|
5751
|
-
import { existsSync as
|
|
5900
|
+
import { existsSync as existsSync27 } from "fs";
|
|
5752
5901
|
function registerMigrateCommand(program) {
|
|
5753
5902
|
program.command("migrate").description("Register existing .feature files into workflow state (for upgrading projects)").option("-v, --verbose", "Enable debug logging").action((opts) => {
|
|
5754
5903
|
if (opts.verbose) logger.setLevel("debug");
|
|
@@ -5758,7 +5907,7 @@ function registerMigrateCommand(program) {
|
|
|
5758
5907
|
console.log("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
|
|
5759
5908
|
console.log("");
|
|
5760
5909
|
const configPath = join29(projectRoot2, ".speclore", "config.yaml");
|
|
5761
|
-
if (!
|
|
5910
|
+
if (!existsSync27(configPath)) {
|
|
5762
5911
|
console.log("\u2717 No .speclore/config.yaml found. Run `speclore setup` first.");
|
|
5763
5912
|
console.log("");
|
|
5764
5913
|
return;
|
|
@@ -5769,7 +5918,7 @@ function registerMigrateCommand(program) {
|
|
|
5769
5918
|
stateManager.ensureInitialized();
|
|
5770
5919
|
console.log("\u2713 State file ready");
|
|
5771
5920
|
const specsDir = join29(projectRoot2, config.spec.outputDir);
|
|
5772
|
-
if (!
|
|
5921
|
+
if (!existsSync27(specsDir)) {
|
|
5773
5922
|
console.log(`\u25CB No ${config.spec.outputDir}/ directory found \u2014 nothing to migrate.`);
|
|
5774
5923
|
console.log("");
|
|
5775
5924
|
return;
|
|
@@ -5802,8 +5951,9 @@ function registerMigrateCommand(program) {
|
|
|
5802
5951
|
|
|
5803
5952
|
// src/cli/commands/mcp-config.ts
|
|
5804
5953
|
init_logger();
|
|
5805
|
-
import { existsSync as
|
|
5954
|
+
import { existsSync as existsSync28, readFileSync as readFileSync21, writeFileSync as writeFileSync15, rmSync as rmSync5 } from "fs";
|
|
5806
5955
|
import { join as join30 } from "path";
|
|
5956
|
+
init_path_utils();
|
|
5807
5957
|
var VALID_CLIENTS = ["cursor", "claude", "qoder"];
|
|
5808
5958
|
function getMcpPath(projectRoot2, client) {
|
|
5809
5959
|
switch (client) {
|
|
@@ -5811,8 +5961,10 @@ function getMcpPath(projectRoot2, client) {
|
|
|
5811
5961
|
return join30(projectRoot2, ".cursor", "mcp.json");
|
|
5812
5962
|
case "claude":
|
|
5813
5963
|
return join30(projectRoot2, ".mcp.json");
|
|
5814
|
-
case "qoder":
|
|
5815
|
-
|
|
5964
|
+
case "qoder": {
|
|
5965
|
+
const qoderDir = resolveQoderDir(projectRoot2);
|
|
5966
|
+
return join30(projectRoot2, qoderDir, "mcp.json");
|
|
5967
|
+
}
|
|
5816
5968
|
}
|
|
5817
5969
|
}
|
|
5818
5970
|
function clientLabel(client) {
|
|
@@ -5834,20 +5986,20 @@ function registerMcpConfigCommands(program) {
|
|
|
5834
5986
|
if (!client) return;
|
|
5835
5987
|
logger.info(`Configuring MCP for ${clientLabel(client)}...`);
|
|
5836
5988
|
writeMcpForClient(projectRoot2, client);
|
|
5837
|
-
logger.info(
|
|
5989
|
+
logger.info(`[OK] ${clientLabel(client)} MCP configured at ${getMcpPath(projectRoot2, client)}`);
|
|
5838
5990
|
});
|
|
5839
5991
|
mcpCmd.command("remove <client>").description("Remove SpecLore MCP config from a specific AI client (cursor | claude | qoder)").action((clientArg) => {
|
|
5840
5992
|
const projectRoot2 = process.cwd();
|
|
5841
5993
|
const client = validateClient(clientArg);
|
|
5842
5994
|
if (!client) return;
|
|
5843
5995
|
const mcpPath = getMcpPath(projectRoot2, client);
|
|
5844
|
-
if (!
|
|
5996
|
+
if (!existsSync28(mcpPath)) {
|
|
5845
5997
|
logger.info(`${clientLabel(client)}: no MCP config file found at ${mcpPath}`);
|
|
5846
5998
|
return;
|
|
5847
5999
|
}
|
|
5848
6000
|
const removed = removeMcpServerEntry(mcpPath, "speclore");
|
|
5849
6001
|
if (removed) {
|
|
5850
|
-
logger.info(
|
|
6002
|
+
logger.info(`[OK] Removed speclore from ${clientLabel(client)}: ${mcpPath}`);
|
|
5851
6003
|
} else {
|
|
5852
6004
|
logger.info(`${clientLabel(client)}: speclore entry not found in ${mcpPath}`);
|
|
5853
6005
|
}
|
|
@@ -5855,24 +6007,24 @@ function registerMcpConfigCommands(program) {
|
|
|
5855
6007
|
mcpCmd.command("list").description("Show current SpecLore MCP configuration status for all clients").action(() => {
|
|
5856
6008
|
const projectRoot2 = process.cwd();
|
|
5857
6009
|
logger.info("SpecLore MCP Configuration Status");
|
|
5858
|
-
logger.info("
|
|
6010
|
+
logger.info("------------------------------");
|
|
5859
6011
|
let anyFound = false;
|
|
5860
6012
|
for (const client of VALID_CLIENTS) {
|
|
5861
6013
|
const mcpPath = getMcpPath(projectRoot2, client);
|
|
5862
|
-
if (!
|
|
5863
|
-
logger.info(`
|
|
6014
|
+
if (!existsSync28(mcpPath)) {
|
|
6015
|
+
logger.info(` [-] ${clientLabel(client)}: no config file (${mcpPath})`);
|
|
5864
6016
|
continue;
|
|
5865
6017
|
}
|
|
5866
6018
|
try {
|
|
5867
|
-
const config = JSON.parse(
|
|
6019
|
+
const config = JSON.parse(readFileSync21(mcpPath, "utf-8"));
|
|
5868
6020
|
if (config.mcpServers?.["speclore"]) {
|
|
5869
|
-
logger.info(`
|
|
6021
|
+
logger.info(` [OK] ${clientLabel(client)}: configured -> ${mcpPath}`);
|
|
5870
6022
|
anyFound = true;
|
|
5871
6023
|
} else {
|
|
5872
|
-
logger.info(`
|
|
6024
|
+
logger.info(` [-] ${clientLabel(client)}: config file exists but speclore not registered -> ${mcpPath}`);
|
|
5873
6025
|
}
|
|
5874
6026
|
} catch {
|
|
5875
|
-
logger.info(`
|
|
6027
|
+
logger.info(` [!] ${clientLabel(client)}: failed to parse ${mcpPath}`);
|
|
5876
6028
|
}
|
|
5877
6029
|
}
|
|
5878
6030
|
if (!anyFound) {
|
|
@@ -5891,9 +6043,9 @@ function validateClient(arg) {
|
|
|
5891
6043
|
return normalised;
|
|
5892
6044
|
}
|
|
5893
6045
|
function removeMcpServerEntry(mcpPath, serverName) {
|
|
5894
|
-
if (!
|
|
6046
|
+
if (!existsSync28(mcpPath)) return false;
|
|
5895
6047
|
try {
|
|
5896
|
-
const config = JSON.parse(
|
|
6048
|
+
const config = JSON.parse(readFileSync21(mcpPath, "utf-8"));
|
|
5897
6049
|
if (!config.mcpServers?.[serverName]) return false;
|
|
5898
6050
|
delete config.mcpServers[serverName];
|
|
5899
6051
|
const remainingServers = Object.keys(config.mcpServers ?? {});
|