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