lee-spec-kit 0.4.1 → 0.4.3
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.md +24 -1
- package/dist/index.js +937 -536
- package/package.json +1 -1
- package/templates/en/common/agents/skills/execute-task.md +1 -1
- package/templates/en/common/scripts/README.md +11 -0
- package/templates/en/fullstack/agents/agents.md +1 -1
- package/templates/en/single/agents/agents.md +1 -1
- package/templates/ko/common/agents/skills/execute-task.md +1 -1
- package/templates/ko/common/scripts/README.md +11 -0
- package/templates/ko/fullstack/agents/agents.md +1 -1
- package/templates/ko/single/agents/agents.md +1 -1
package/dist/index.js
CHANGED
|
@@ -2,18 +2,18 @@
|
|
|
2
2
|
import path4 from 'path';
|
|
3
3
|
import { fileURLToPath } from 'url';
|
|
4
4
|
import { program } from 'commander';
|
|
5
|
-
import
|
|
5
|
+
import fs8 from 'fs-extra';
|
|
6
6
|
import prompts from 'prompts';
|
|
7
7
|
import chalk6 from 'chalk';
|
|
8
8
|
import { glob } from 'glob';
|
|
9
|
-
import { spawn, execSync } from 'child_process';
|
|
9
|
+
import { spawn, execSync, execFileSync } from 'child_process';
|
|
10
10
|
import os from 'os';
|
|
11
11
|
|
|
12
12
|
var getFilename = () => fileURLToPath(import.meta.url);
|
|
13
13
|
var getDirname = () => path4.dirname(getFilename());
|
|
14
14
|
var __dirname$1 = /* @__PURE__ */ getDirname();
|
|
15
15
|
async function copyTemplates(src, dest) {
|
|
16
|
-
await
|
|
16
|
+
await fs8.copy(src, dest, {
|
|
17
17
|
overwrite: true,
|
|
18
18
|
errorOnExist: false
|
|
19
19
|
});
|
|
@@ -21,19 +21,19 @@ async function copyTemplates(src, dest) {
|
|
|
21
21
|
async function replaceInFiles(dir, replacements) {
|
|
22
22
|
const files = await glob("**/*.md", { cwd: dir, absolute: true });
|
|
23
23
|
for (const file of files) {
|
|
24
|
-
let content = await
|
|
24
|
+
let content = await fs8.readFile(file, "utf-8");
|
|
25
25
|
for (const [search, replace] of Object.entries(replacements)) {
|
|
26
26
|
content = content.replaceAll(search, replace);
|
|
27
27
|
}
|
|
28
|
-
await
|
|
28
|
+
await fs8.writeFile(file, content, "utf-8");
|
|
29
29
|
}
|
|
30
30
|
const shFiles = await glob("**/*.sh", { cwd: dir, absolute: true });
|
|
31
31
|
for (const file of shFiles) {
|
|
32
|
-
let content = await
|
|
32
|
+
let content = await fs8.readFile(file, "utf-8");
|
|
33
33
|
for (const [search, replace] of Object.entries(replacements)) {
|
|
34
34
|
content = content.replaceAll(search, replace);
|
|
35
35
|
}
|
|
36
|
-
await
|
|
36
|
+
await fs8.writeFile(file, content, "utf-8");
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
var __filename2 = fileURLToPath(import.meta.url);
|
|
@@ -358,8 +358,8 @@ async function runInit(options) {
|
|
|
358
358
|
assertValid(validateSafeName(projectName), "\uD504\uB85C\uC81D\uD2B8 \uC774\uB984");
|
|
359
359
|
assertValid(validateProjectType(projectType), "\uD504\uB85C\uC81D\uD2B8 \uD0C0\uC785");
|
|
360
360
|
assertValid(validateLanguage(lang), "\uC5B8\uC5B4");
|
|
361
|
-
if (await
|
|
362
|
-
const files = await
|
|
361
|
+
if (await fs8.pathExists(targetDir)) {
|
|
362
|
+
const files = await fs8.readdir(targetDir);
|
|
363
363
|
if (files.length > 0) {
|
|
364
364
|
const { overwrite } = await prompts({
|
|
365
365
|
type: "confirm",
|
|
@@ -383,10 +383,10 @@ async function runInit(options) {
|
|
|
383
383
|
const templatesDir = getTemplatesDir();
|
|
384
384
|
const commonPath = path4.join(templatesDir, lang, "common");
|
|
385
385
|
const typePath = path4.join(templatesDir, lang, projectType);
|
|
386
|
-
if (await
|
|
386
|
+
if (await fs8.pathExists(commonPath)) {
|
|
387
387
|
await copyTemplates(commonPath, targetDir);
|
|
388
388
|
}
|
|
389
|
-
if (!await
|
|
389
|
+
if (!await fs8.pathExists(typePath)) {
|
|
390
390
|
throw new Error(`\uD15C\uD50C\uB9BF\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${typePath}`);
|
|
391
391
|
}
|
|
392
392
|
await copyTemplates(typePath, targetDir);
|
|
@@ -414,7 +414,7 @@ async function runInit(options) {
|
|
|
414
414
|
}
|
|
415
415
|
}
|
|
416
416
|
const configPath = path4.join(targetDir, ".lee-spec-kit.json");
|
|
417
|
-
await
|
|
417
|
+
await fs8.writeJson(configPath, config, { spaces: 2 });
|
|
418
418
|
console.log(chalk6.green("\u2705 docs \uAD6C\uC870 \uC0DD\uC131 \uC644\uB8CC!"));
|
|
419
419
|
console.log();
|
|
420
420
|
await initGit(cwd, targetDir, docsRepo, pushDocs, docsRemote);
|
|
@@ -427,28 +427,49 @@ async function runInit(options) {
|
|
|
427
427
|
}
|
|
428
428
|
async function initGit(cwd, targetDir, docsRepo, pushDocs, docsRemote) {
|
|
429
429
|
try {
|
|
430
|
+
const runGit = (args, workdir) => {
|
|
431
|
+
execFileSync("git", args, { cwd: workdir, stdio: "ignore" });
|
|
432
|
+
};
|
|
433
|
+
const getCachedStagedFiles = (workdir) => {
|
|
434
|
+
try {
|
|
435
|
+
const out = execFileSync("git", ["diff", "--cached", "--name-only"], {
|
|
436
|
+
cwd: workdir,
|
|
437
|
+
encoding: "utf-8",
|
|
438
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
439
|
+
}).trim();
|
|
440
|
+
if (!out) return [];
|
|
441
|
+
return out.split("\n").map((s) => s.trim()).filter(Boolean);
|
|
442
|
+
} catch {
|
|
443
|
+
return null;
|
|
444
|
+
}
|
|
445
|
+
};
|
|
430
446
|
try {
|
|
431
|
-
|
|
432
|
-
cwd,
|
|
433
|
-
stdio: "ignore"
|
|
434
|
-
});
|
|
447
|
+
runGit(["rev-parse", "--is-inside-work-tree"], cwd);
|
|
435
448
|
console.log(chalk6.blue("\u{1F4E6} Git \uB808\uD3EC\uC9C0\uD1A0\uB9AC \uAC10\uC9C0, docs \uCEE4\uBC0B \uC911..."));
|
|
436
449
|
} catch {
|
|
437
450
|
console.log(chalk6.blue("\u{1F4E6} Git \uCD08\uAE30\uD654 \uC911..."));
|
|
438
|
-
|
|
451
|
+
runGit(["init"], cwd);
|
|
439
452
|
}
|
|
440
453
|
const relativePath = path4.relative(cwd, targetDir);
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
454
|
+
const stagedBeforeAdd = getCachedStagedFiles(cwd);
|
|
455
|
+
if (relativePath === "." && stagedBeforeAdd && stagedBeforeAdd.length > 0) {
|
|
456
|
+
console.log(
|
|
457
|
+
chalk6.yellow(
|
|
458
|
+
'\u26A0\uFE0F \uD604\uC7AC Git index\uC5D0 \uC774\uBBF8 stage\uB41C \uBCC0\uACBD\uC774 \uC788\uC2B5\uB2C8\uB2E4. (--dir "." \uC778 \uACBD\uC6B0 \uCEE4\uBC0B \uBC94\uC704\uB97C \uC548\uC804\uD558\uAC8C \uC81C\uD55C\uD560 \uC218 \uC5C6\uC5B4 \uC790\uB3D9 \uCEE4\uBC0B\uC744 \uAC74\uB108\uB701\uB2C8\uB2E4)'
|
|
459
|
+
)
|
|
460
|
+
);
|
|
461
|
+
console.log(chalk6.gray(" \uC218\uB3D9\uC73C\uB85C \uBCC0\uACBD \uB0B4\uC6A9\uC744 \uD655\uC778\uD55C \uB4A4 \uCEE4\uBC0B\uD574\uC8FC\uC138\uC694."));
|
|
462
|
+
console.log();
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
runGit(["add", relativePath], cwd);
|
|
466
|
+
runGit(
|
|
467
|
+
["commit", "-m", "init: docs \uAD6C\uC870 \uCD08\uAE30\uD654 (lee-spec-kit)", "--", relativePath],
|
|
468
|
+
cwd
|
|
469
|
+
);
|
|
446
470
|
if (docsRepo === "standalone" && pushDocs && docsRemote) {
|
|
447
471
|
try {
|
|
448
|
-
|
|
449
|
-
cwd,
|
|
450
|
-
stdio: "ignore"
|
|
451
|
-
});
|
|
472
|
+
runGit(["remote", "add", "origin", docsRemote], cwd);
|
|
452
473
|
console.log(chalk6.green(`\u2705 Git remote \uC124\uC815 \uC644\uB8CC: ${docsRemote}`));
|
|
453
474
|
} catch {
|
|
454
475
|
console.log(chalk6.yellow("\u26A0\uFE0F Git remote\uAC00 \uC774\uBBF8 \uC874\uC7AC\uD569\uB2C8\uB2E4."));
|
|
@@ -492,9 +513,9 @@ async function getConfig(cwd) {
|
|
|
492
513
|
if (visitedDocsDirs.has(resolvedDocsDir)) continue;
|
|
493
514
|
visitedDocsDirs.add(resolvedDocsDir);
|
|
494
515
|
const configPath = path4.join(resolvedDocsDir, ".lee-spec-kit.json");
|
|
495
|
-
if (await
|
|
516
|
+
if (await fs8.pathExists(configPath)) {
|
|
496
517
|
try {
|
|
497
|
-
const configFile = await
|
|
518
|
+
const configFile = await fs8.readJson(configPath);
|
|
498
519
|
return {
|
|
499
520
|
docsDir: resolvedDocsDir,
|
|
500
521
|
projectName: configFile.projectName,
|
|
@@ -510,14 +531,14 @@ async function getConfig(cwd) {
|
|
|
510
531
|
}
|
|
511
532
|
const agentsPath = path4.join(resolvedDocsDir, "agents");
|
|
512
533
|
const featuresPath = path4.join(resolvedDocsDir, "features");
|
|
513
|
-
if (await
|
|
534
|
+
if (await fs8.pathExists(agentsPath) && await fs8.pathExists(featuresPath)) {
|
|
514
535
|
const bePath = path4.join(featuresPath, "be");
|
|
515
536
|
const fePath = path4.join(featuresPath, "fe");
|
|
516
|
-
const projectType = await
|
|
537
|
+
const projectType = await fs8.pathExists(bePath) || await fs8.pathExists(fePath) ? "fullstack" : "single";
|
|
517
538
|
const agentsMdPath = path4.join(agentsPath, "agents.md");
|
|
518
539
|
let lang = "ko";
|
|
519
|
-
if (await
|
|
520
|
-
const content = await
|
|
540
|
+
if (await fs8.pathExists(agentsMdPath)) {
|
|
541
|
+
const content = await fs8.readFile(agentsMdPath, "utf-8");
|
|
521
542
|
if (!/[가-힣]/.test(content)) {
|
|
522
543
|
lang = "en";
|
|
523
544
|
}
|
|
@@ -593,26 +614,31 @@ async function runFeature(name, options) {
|
|
|
593
614
|
}
|
|
594
615
|
const featureFolderName = `${featureId}-${name}`;
|
|
595
616
|
const featureDir = path4.join(featuresDir, featureFolderName);
|
|
596
|
-
if (await
|
|
617
|
+
if (await fs8.pathExists(featureDir)) {
|
|
597
618
|
console.error(chalk6.red(`\uC774\uBBF8 \uC874\uC7AC\uD558\uB294 \uD3F4\uB354\uC785\uB2C8\uB2E4: ${featureDir}`));
|
|
598
619
|
process.exit(1);
|
|
599
620
|
}
|
|
600
621
|
const featureBasePath = path4.join(docsDir, "features", "feature-base");
|
|
601
|
-
if (!await
|
|
622
|
+
if (!await fs8.pathExists(featureBasePath)) {
|
|
602
623
|
console.error(chalk6.red("feature-base \uD15C\uD50C\uB9BF\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4."));
|
|
603
624
|
process.exit(1);
|
|
604
625
|
}
|
|
605
|
-
await
|
|
626
|
+
await fs8.copy(featureBasePath, featureDir);
|
|
606
627
|
const idNumber = featureId.replace("F", "");
|
|
607
628
|
const repoName = projectType === "fullstack" && repo ? `{{projectName}}-${repo}` : "{{projectName}}";
|
|
608
629
|
const replacements = {
|
|
630
|
+
// ko placeholders
|
|
609
631
|
"{\uAE30\uB2A5\uBA85}": name,
|
|
610
632
|
"{\uBC88\uD638}": idNumber,
|
|
611
633
|
"YYYY-MM-DD": (/* @__PURE__ */ new Date()).toISOString().split("T")[0],
|
|
612
634
|
"{be|fe}": repo || "",
|
|
613
|
-
"git-dungeon-{be|fe}": repoName,
|
|
614
635
|
"{\uC774\uC288\uBC88\uD638}": "",
|
|
615
|
-
"{{description}}": options.desc || ""
|
|
636
|
+
"{{description}}": options.desc || "",
|
|
637
|
+
// en placeholders
|
|
638
|
+
"{feature-name}": name,
|
|
639
|
+
"{number}": idNumber,
|
|
640
|
+
"{issue-number}": "",
|
|
641
|
+
"{{projectName}}-{be|fe}": repoName
|
|
616
642
|
};
|
|
617
643
|
if (lang === "en") {
|
|
618
644
|
replacements["\uAE30\uB2A5 ID"] = "Feature ID";
|
|
@@ -643,8 +669,8 @@ async function getNextFeatureId(docsDir, projectType) {
|
|
|
643
669
|
scanDirs.push(featuresDir);
|
|
644
670
|
}
|
|
645
671
|
for (const dir of scanDirs) {
|
|
646
|
-
if (!await
|
|
647
|
-
const entries = await
|
|
672
|
+
if (!await fs8.pathExists(dir)) continue;
|
|
673
|
+
const entries = await fs8.readdir(dir, { withFileTypes: true });
|
|
648
674
|
for (const entry of entries) {
|
|
649
675
|
if (!entry.isDirectory()) continue;
|
|
650
676
|
const match = entry.name.match(/^F(\d+)-/);
|
|
@@ -658,380 +684,6 @@ async function getNextFeatureId(docsDir, projectType) {
|
|
|
658
684
|
const width = Math.max(3, String(next).length);
|
|
659
685
|
return `F${String(next).padStart(width, "0")}`;
|
|
660
686
|
}
|
|
661
|
-
function statusCommand(program2) {
|
|
662
|
-
program2.command("status").description("Show feature status").option("-w, --write", "Write status.md file").option("-s, --strict", "Fail on missing/duplicate feature IDs").action(async (options) => {
|
|
663
|
-
try {
|
|
664
|
-
await runStatus(options);
|
|
665
|
-
} catch (error) {
|
|
666
|
-
console.error(chalk6.red("\uC624\uB958:"), error);
|
|
667
|
-
process.exit(1);
|
|
668
|
-
}
|
|
669
|
-
});
|
|
670
|
-
}
|
|
671
|
-
async function runStatus(options) {
|
|
672
|
-
const cwd = process.cwd();
|
|
673
|
-
const config = await getConfig(cwd);
|
|
674
|
-
if (!config) {
|
|
675
|
-
console.error(
|
|
676
|
-
chalk6.red("docs \uD3F4\uB354\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uBA3C\uC800 init\uC744 \uC2E4\uD589\uD558\uC138\uC694.")
|
|
677
|
-
);
|
|
678
|
-
process.exit(1);
|
|
679
|
-
}
|
|
680
|
-
const { docsDir, projectType } = config;
|
|
681
|
-
const featuresDir = path4.join(docsDir, "features");
|
|
682
|
-
const features = [];
|
|
683
|
-
const idMap = /* @__PURE__ */ new Map();
|
|
684
|
-
const scopes = projectType === "fullstack" ? ["be", "fe"] : [""];
|
|
685
|
-
for (const scope of scopes) {
|
|
686
|
-
const scanDir = scope ? path4.join(featuresDir, scope) : featuresDir;
|
|
687
|
-
if (!await fs6.pathExists(scanDir)) continue;
|
|
688
|
-
const entries = await fs6.readdir(scanDir, { withFileTypes: true });
|
|
689
|
-
for (const entry of entries) {
|
|
690
|
-
if (!entry.isDirectory()) continue;
|
|
691
|
-
if (entry.name === "feature-base") continue;
|
|
692
|
-
const featureDir = path4.join(scanDir, entry.name);
|
|
693
|
-
const specPath = path4.join(featureDir, "spec.md");
|
|
694
|
-
const tasksPath = path4.join(featureDir, "tasks.md");
|
|
695
|
-
if (!await fs6.pathExists(specPath)) continue;
|
|
696
|
-
if (!await fs6.pathExists(tasksPath)) continue;
|
|
697
|
-
const specContent = await fs6.readFile(specPath, "utf-8");
|
|
698
|
-
const tasksContent = await fs6.readFile(tasksPath, "utf-8");
|
|
699
|
-
const id = extractSpecValue(specContent, "\uAE30\uB2A5 ID") || extractSpecValue(specContent, "Feature ID") || "UNKNOWN";
|
|
700
|
-
const name = extractSpecValue(specContent, "\uAE30\uB2A5\uBA85") || extractSpecValue(specContent, "Feature Name") || entry.name;
|
|
701
|
-
const repo = extractSpecValue(specContent, "\uB300\uC0C1 \uB808\uD3EC") || extractSpecValue(specContent, "Target Repo") || (scope ? `{{projectName}}-${scope}` : "{{projectName}}");
|
|
702
|
-
const issue = extractSpecValue(specContent, "\uC774\uC288 \uBC88\uD638") || extractSpecValue(specContent, "Issue Number") || "-";
|
|
703
|
-
const relPath = path4.relative(docsDir, featureDir);
|
|
704
|
-
if (!idMap.has(id)) {
|
|
705
|
-
idMap.set(id, []);
|
|
706
|
-
}
|
|
707
|
-
idMap.get(id).push(relPath);
|
|
708
|
-
const { total, done, doing, todo } = countTasks(tasksContent);
|
|
709
|
-
let status = "TODO";
|
|
710
|
-
if (total > 0 && done === total) {
|
|
711
|
-
status = "DONE";
|
|
712
|
-
} else if (doing > 0) {
|
|
713
|
-
status = "DOING";
|
|
714
|
-
} else if (todo > 0) {
|
|
715
|
-
status = "TODO";
|
|
716
|
-
} else if (total === 0) {
|
|
717
|
-
status = "NO_TASKS";
|
|
718
|
-
}
|
|
719
|
-
features.push({
|
|
720
|
-
id,
|
|
721
|
-
name,
|
|
722
|
-
repo,
|
|
723
|
-
issue,
|
|
724
|
-
status,
|
|
725
|
-
progress: `${done}/${total}`,
|
|
726
|
-
path: relPath
|
|
727
|
-
});
|
|
728
|
-
}
|
|
729
|
-
}
|
|
730
|
-
if (features.length === 0) {
|
|
731
|
-
console.log(chalk6.yellow("Feature\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4."));
|
|
732
|
-
return;
|
|
733
|
-
}
|
|
734
|
-
if (options.strict) {
|
|
735
|
-
const duplicates = [...idMap.entries()].filter(
|
|
736
|
-
([, paths]) => paths.length > 1
|
|
737
|
-
);
|
|
738
|
-
if (duplicates.length > 0) {
|
|
739
|
-
console.error(chalk6.red("\uC911\uBCF5 Feature ID \uBC1C\uACAC:"));
|
|
740
|
-
for (const [id, paths] of duplicates) {
|
|
741
|
-
console.error(chalk6.red(` ${id}:`));
|
|
742
|
-
for (const p of paths) {
|
|
743
|
-
console.error(chalk6.red(` - ${p}`));
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
process.exit(1);
|
|
747
|
-
}
|
|
748
|
-
const unknowns = [...idMap.entries()].filter(([id]) => id === "UNKNOWN");
|
|
749
|
-
if (unknowns.length > 0) {
|
|
750
|
-
console.error(chalk6.red("Feature ID\uAC00 \uC5C6\uB294 \uD56D\uBAA9:"));
|
|
751
|
-
for (const [, paths] of unknowns) {
|
|
752
|
-
for (const p of paths) {
|
|
753
|
-
console.error(chalk6.red(` - ${p}`));
|
|
754
|
-
}
|
|
755
|
-
}
|
|
756
|
-
process.exit(1);
|
|
757
|
-
}
|
|
758
|
-
}
|
|
759
|
-
features.sort((a, b) => a.id.localeCompare(b.id));
|
|
760
|
-
const header = "| ID | Name | Repo | Issue | Status | Progress | Path |";
|
|
761
|
-
const separator = "| --- | --- | --- | --- | --- | --- | --- |";
|
|
762
|
-
console.log();
|
|
763
|
-
console.log(header);
|
|
764
|
-
console.log(separator);
|
|
765
|
-
for (const f of features) {
|
|
766
|
-
const statusColor = f.status === "DONE" ? chalk6.green : f.status === "DOING" ? chalk6.yellow : chalk6.gray;
|
|
767
|
-
console.log(
|
|
768
|
-
`| ${f.id} | ${f.name} | ${f.repo} | ${f.issue} | ${statusColor(f.status)} | ${f.progress} | ${f.path} |`
|
|
769
|
-
);
|
|
770
|
-
}
|
|
771
|
-
console.log();
|
|
772
|
-
if (options.write) {
|
|
773
|
-
const outputPath = path4.join(featuresDir, "status.md");
|
|
774
|
-
const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
775
|
-
const content = [
|
|
776
|
-
"# Feature Status",
|
|
777
|
-
"",
|
|
778
|
-
`- Generated: ${date}`,
|
|
779
|
-
"- Source: `tasks.md`, `spec.md`",
|
|
780
|
-
"",
|
|
781
|
-
header,
|
|
782
|
-
separator,
|
|
783
|
-
...features.map(
|
|
784
|
-
(f) => `| ${f.id} | ${f.name} | ${f.repo} | ${f.issue} | ${f.status} | ${f.progress} | ${f.path} |`
|
|
785
|
-
),
|
|
786
|
-
""
|
|
787
|
-
].join("\n");
|
|
788
|
-
await fs6.writeFile(outputPath, content, "utf-8");
|
|
789
|
-
console.log(chalk6.green(`\u2705 ${outputPath} \uC0DD\uC131 \uC644\uB8CC`));
|
|
790
|
-
}
|
|
791
|
-
}
|
|
792
|
-
function extractSpecValue(content, key) {
|
|
793
|
-
const regex = new RegExp(`^- \\*\\*${key}\\*\\*:\\s*(.*)$`, "m");
|
|
794
|
-
const match = content.match(regex);
|
|
795
|
-
return match ? match[1].trim() : "";
|
|
796
|
-
}
|
|
797
|
-
function countTasks(content) {
|
|
798
|
-
let total = 0;
|
|
799
|
-
let done = 0;
|
|
800
|
-
let doing = 0;
|
|
801
|
-
let todo = 0;
|
|
802
|
-
const lines = content.split("\n");
|
|
803
|
-
for (const line of lines) {
|
|
804
|
-
const match = line.match(/^- \[([A-Z]+)\]/);
|
|
805
|
-
if (match) {
|
|
806
|
-
total++;
|
|
807
|
-
const status = match[1];
|
|
808
|
-
if (status === "DONE") done++;
|
|
809
|
-
else if (status === "DOING" || status === "REVIEW") doing++;
|
|
810
|
-
else if (status === "TODO") todo++;
|
|
811
|
-
}
|
|
812
|
-
}
|
|
813
|
-
return { total, done, doing, todo };
|
|
814
|
-
}
|
|
815
|
-
function updateCommand(program2) {
|
|
816
|
-
program2.command("update").description("Update docs templates to the latest version").option("--agents", "Update agents/ folder only").option("--templates", "Update feature-base/ folder only").option("-f, --force", "Force overwrite without confirmation").action(async (options) => {
|
|
817
|
-
try {
|
|
818
|
-
await runUpdate(options);
|
|
819
|
-
} catch (error) {
|
|
820
|
-
if (error instanceof Error && error.message === "canceled") {
|
|
821
|
-
console.log(chalk6.yellow("\n\uC791\uC5C5\uC774 \uCDE8\uC18C\uB418\uC5C8\uC2B5\uB2C8\uB2E4."));
|
|
822
|
-
process.exit(0);
|
|
823
|
-
}
|
|
824
|
-
console.error(chalk6.red("\uC624\uB958:"), error);
|
|
825
|
-
process.exit(1);
|
|
826
|
-
}
|
|
827
|
-
});
|
|
828
|
-
}
|
|
829
|
-
async function runUpdate(options) {
|
|
830
|
-
const cwd = process.cwd();
|
|
831
|
-
const config = await getConfig(cwd);
|
|
832
|
-
if (!config) {
|
|
833
|
-
console.error(
|
|
834
|
-
chalk6.red("docs \uD3F4\uB354\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uBA3C\uC800 init\uC744 \uC2E4\uD589\uD558\uC138\uC694.")
|
|
835
|
-
);
|
|
836
|
-
process.exit(1);
|
|
837
|
-
}
|
|
838
|
-
const { docsDir, projectType, lang } = config;
|
|
839
|
-
const templatesDir = getTemplatesDir();
|
|
840
|
-
const sourceDir = path4.join(templatesDir, lang, projectType);
|
|
841
|
-
const updateAgents = options.agents || !options.agents && !options.templates;
|
|
842
|
-
const updateTemplates = options.templates || !options.agents && !options.templates;
|
|
843
|
-
console.log(chalk6.blue("\u{1F4E6} \uD15C\uD50C\uB9BF \uC5C5\uB370\uC774\uD2B8\uB97C \uC2DC\uC791\uD569\uB2C8\uB2E4..."));
|
|
844
|
-
console.log(chalk6.gray(` - \uC5B8\uC5B4: ${lang}`));
|
|
845
|
-
console.log(chalk6.gray(` - \uD0C0\uC785: ${projectType}`));
|
|
846
|
-
console.log();
|
|
847
|
-
let updatedCount = 0;
|
|
848
|
-
if (updateAgents) {
|
|
849
|
-
console.log(chalk6.blue("\u{1F4C1} agents/ \uD3F4\uB354 \uC5C5\uB370\uC774\uD2B8 \uC911..."));
|
|
850
|
-
const commonAgents = path4.join(templatesDir, lang, "common", "agents");
|
|
851
|
-
const typeAgents = path4.join(templatesDir, lang, projectType, "agents");
|
|
852
|
-
const targetAgents = path4.join(docsDir, "agents");
|
|
853
|
-
const featurePath = projectType === "fullstack" ? "docs/features/{be|fe}" : "docs/features";
|
|
854
|
-
const replacements = {
|
|
855
|
-
"{{featurePath}}": featurePath
|
|
856
|
-
};
|
|
857
|
-
if (await fs6.pathExists(commonAgents)) {
|
|
858
|
-
const count = await updateFolder(
|
|
859
|
-
commonAgents,
|
|
860
|
-
targetAgents,
|
|
861
|
-
options.force,
|
|
862
|
-
replacements
|
|
863
|
-
);
|
|
864
|
-
updatedCount += count;
|
|
865
|
-
}
|
|
866
|
-
if (await fs6.pathExists(typeAgents)) {
|
|
867
|
-
const count = await updateFolder(typeAgents, targetAgents, options.force);
|
|
868
|
-
updatedCount += count;
|
|
869
|
-
}
|
|
870
|
-
console.log(chalk6.green(` \u2705 agents/ \uC5C5\uB370\uC774\uD2B8 \uC644\uB8CC`));
|
|
871
|
-
}
|
|
872
|
-
if (updateTemplates) {
|
|
873
|
-
console.log(chalk6.blue("\u{1F4C1} features/feature-base/ \uD3F4\uB354 \uC5C5\uB370\uC774\uD2B8 \uC911..."));
|
|
874
|
-
const sourceFeatureBase = path4.join(sourceDir, "features", "feature-base");
|
|
875
|
-
const targetFeatureBase = path4.join(docsDir, "features", "feature-base");
|
|
876
|
-
if (await fs6.pathExists(sourceFeatureBase)) {
|
|
877
|
-
const count = await updateFolder(
|
|
878
|
-
sourceFeatureBase,
|
|
879
|
-
targetFeatureBase,
|
|
880
|
-
options.force
|
|
881
|
-
);
|
|
882
|
-
updatedCount += count;
|
|
883
|
-
console.log(chalk6.green(` \u2705 ${count}\uAC1C \uD30C\uC77C \uC5C5\uB370\uC774\uD2B8 \uC644\uB8CC`));
|
|
884
|
-
}
|
|
885
|
-
}
|
|
886
|
-
console.log();
|
|
887
|
-
console.log(chalk6.green(`\u2705 \uCD1D ${updatedCount}\uAC1C \uD30C\uC77C \uC5C5\uB370\uC774\uD2B8 \uC644\uB8CC!`));
|
|
888
|
-
}
|
|
889
|
-
async function updateFolder(sourceDir, targetDir, force, replacements) {
|
|
890
|
-
const protectedFiles = /* @__PURE__ */ new Set(["custom.md", "constitution.md"]);
|
|
891
|
-
await fs6.ensureDir(targetDir);
|
|
892
|
-
const files = await fs6.readdir(sourceDir);
|
|
893
|
-
let updatedCount = 0;
|
|
894
|
-
for (const file of files) {
|
|
895
|
-
const sourcePath = path4.join(sourceDir, file);
|
|
896
|
-
const targetPath = path4.join(targetDir, file);
|
|
897
|
-
const stat = await fs6.stat(sourcePath);
|
|
898
|
-
if (stat.isFile()) {
|
|
899
|
-
if (protectedFiles.has(file)) {
|
|
900
|
-
continue;
|
|
901
|
-
}
|
|
902
|
-
let sourceContent = await fs6.readFile(sourcePath, "utf-8");
|
|
903
|
-
if (replacements) {
|
|
904
|
-
for (const [key, value] of Object.entries(replacements)) {
|
|
905
|
-
sourceContent = sourceContent.replaceAll(key, value);
|
|
906
|
-
}
|
|
907
|
-
}
|
|
908
|
-
let shouldUpdate = true;
|
|
909
|
-
if (await fs6.pathExists(targetPath)) {
|
|
910
|
-
const targetContent = await fs6.readFile(targetPath, "utf-8");
|
|
911
|
-
if (sourceContent === targetContent) {
|
|
912
|
-
continue;
|
|
913
|
-
}
|
|
914
|
-
if (!force) {
|
|
915
|
-
console.log(
|
|
916
|
-
chalk6.yellow(` \u26A0\uFE0F ${file} - \uBCC0\uACBD \uAC10\uC9C0 (--force\uB85C \uB36E\uC5B4\uC4F0\uAE30)`)
|
|
917
|
-
);
|
|
918
|
-
shouldUpdate = false;
|
|
919
|
-
}
|
|
920
|
-
}
|
|
921
|
-
if (shouldUpdate) {
|
|
922
|
-
await fs6.writeFile(targetPath, sourceContent);
|
|
923
|
-
console.log(chalk6.gray(` \u{1F4C4} ${file} \uC5C5\uB370\uC774\uD2B8`));
|
|
924
|
-
updatedCount++;
|
|
925
|
-
}
|
|
926
|
-
} else if (stat.isDirectory()) {
|
|
927
|
-
const subCount = await updateFolder(
|
|
928
|
-
sourcePath,
|
|
929
|
-
targetPath,
|
|
930
|
-
force,
|
|
931
|
-
replacements
|
|
932
|
-
);
|
|
933
|
-
updatedCount += subCount;
|
|
934
|
-
}
|
|
935
|
-
}
|
|
936
|
-
return updatedCount;
|
|
937
|
-
}
|
|
938
|
-
function configCommand(program2) {
|
|
939
|
-
program2.command("config").description("View or modify project configuration").option("--project-root <path>", "Set project root path").option("--repo <repo>", "Repository type for fullstack: fe | be").action(async (options) => {
|
|
940
|
-
try {
|
|
941
|
-
await runConfig(options);
|
|
942
|
-
} catch (error) {
|
|
943
|
-
if (error instanceof Error && error.message === "canceled") {
|
|
944
|
-
console.log(chalk6.yellow("\n\uC791\uC5C5\uC774 \uCDE8\uC18C\uB418\uC5C8\uC2B5\uB2C8\uB2E4."));
|
|
945
|
-
process.exit(0);
|
|
946
|
-
}
|
|
947
|
-
console.error(chalk6.red("\uC624\uB958:"), error);
|
|
948
|
-
process.exit(1);
|
|
949
|
-
}
|
|
950
|
-
});
|
|
951
|
-
}
|
|
952
|
-
async function runConfig(options) {
|
|
953
|
-
const cwd = process.cwd();
|
|
954
|
-
const config = await getConfig(cwd);
|
|
955
|
-
if (!config) {
|
|
956
|
-
console.log(
|
|
957
|
-
chalk6.red("\uC124\uC815 \uD30C\uC77C\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uBA3C\uC800 init\uC744 \uC2E4\uD589\uD574\uC8FC\uC138\uC694.")
|
|
958
|
-
);
|
|
959
|
-
process.exit(1);
|
|
960
|
-
}
|
|
961
|
-
const configPath = path4.join(config.docsDir, ".lee-spec-kit.json");
|
|
962
|
-
if (!options.projectRoot) {
|
|
963
|
-
console.log();
|
|
964
|
-
console.log(chalk6.blue("\u{1F4CB} \uD604\uC7AC \uC124\uC815:"));
|
|
965
|
-
console.log();
|
|
966
|
-
console.log(chalk6.gray(` \uACBD\uB85C: ${configPath}`));
|
|
967
|
-
console.log();
|
|
968
|
-
const configFile2 = await fs6.readJson(configPath);
|
|
969
|
-
console.log(JSON.stringify(configFile2, null, 2));
|
|
970
|
-
console.log();
|
|
971
|
-
return;
|
|
972
|
-
}
|
|
973
|
-
const configFile = await fs6.readJson(configPath);
|
|
974
|
-
if (configFile.docsRepo !== "standalone") {
|
|
975
|
-
console.log(
|
|
976
|
-
chalk6.yellow("\u26A0\uFE0F projectRoot\uB294 standalone \uBAA8\uB4DC\uC5D0\uC11C\uB9CC \uC124\uC815 \uAC00\uB2A5\uD569\uB2C8\uB2E4.")
|
|
977
|
-
);
|
|
978
|
-
return;
|
|
979
|
-
}
|
|
980
|
-
const projectType = configFile.projectType;
|
|
981
|
-
if (projectType === "fullstack") {
|
|
982
|
-
if (!options.repo) {
|
|
983
|
-
const response = await prompts(
|
|
984
|
-
[
|
|
985
|
-
{
|
|
986
|
-
type: "select",
|
|
987
|
-
name: "repo",
|
|
988
|
-
message: "\uC218\uC815\uD560 \uB808\uD3EC\uC9C0\uD1A0\uB9AC\uB97C \uC120\uD0DD\uD558\uC138\uC694:",
|
|
989
|
-
choices: [
|
|
990
|
-
{ title: "Frontend (fe)", value: "fe" },
|
|
991
|
-
{ title: "Backend (be)", value: "be" }
|
|
992
|
-
]
|
|
993
|
-
}
|
|
994
|
-
],
|
|
995
|
-
{
|
|
996
|
-
onCancel: () => {
|
|
997
|
-
throw new Error("canceled");
|
|
998
|
-
}
|
|
999
|
-
}
|
|
1000
|
-
);
|
|
1001
|
-
options.repo = response.repo;
|
|
1002
|
-
}
|
|
1003
|
-
if (!options.repo || !["fe", "be"].includes(options.repo)) {
|
|
1004
|
-
console.log(
|
|
1005
|
-
chalk6.red(
|
|
1006
|
-
"Fullstack \uD504\uB85C\uC81D\uD2B8\uB294 --repo fe \uB610\uB294 --repo be\uB97C \uC9C0\uC815\uD574\uC57C \uD569\uB2C8\uB2E4."
|
|
1007
|
-
)
|
|
1008
|
-
);
|
|
1009
|
-
return;
|
|
1010
|
-
}
|
|
1011
|
-
const currentRoot = configFile.projectRoot || { fe: "", be: "" };
|
|
1012
|
-
if (typeof currentRoot === "string") {
|
|
1013
|
-
configFile.projectRoot = {
|
|
1014
|
-
fe: options.repo === "fe" ? options.projectRoot : "",
|
|
1015
|
-
be: options.repo === "be" ? options.projectRoot : ""
|
|
1016
|
-
};
|
|
1017
|
-
} else {
|
|
1018
|
-
currentRoot[options.repo] = options.projectRoot;
|
|
1019
|
-
configFile.projectRoot = currentRoot;
|
|
1020
|
-
}
|
|
1021
|
-
console.log(
|
|
1022
|
-
chalk6.green(
|
|
1023
|
-
`\u2705 ${options.repo.toUpperCase()} projectRoot \uC124\uC815 \uC644\uB8CC: ${options.projectRoot}`
|
|
1024
|
-
)
|
|
1025
|
-
);
|
|
1026
|
-
} else {
|
|
1027
|
-
configFile.projectRoot = options.projectRoot;
|
|
1028
|
-
console.log(
|
|
1029
|
-
chalk6.green(`\u2705 projectRoot \uC124\uC815 \uC644\uB8CC: ${options.projectRoot}`)
|
|
1030
|
-
);
|
|
1031
|
-
}
|
|
1032
|
-
await fs6.writeJson(configPath, configFile, { spaces: 2 });
|
|
1033
|
-
console.log();
|
|
1034
|
-
}
|
|
1035
687
|
|
|
1036
688
|
// src/utils/context/i18n.ts
|
|
1037
689
|
function formatTemplate(template, vars) {
|
|
@@ -1049,7 +701,7 @@ var I18N = {
|
|
|
1049
701
|
planWrite: "plan.md \uC791\uC131",
|
|
1050
702
|
planApprove: "plan.md \uC2B9\uC778",
|
|
1051
703
|
tasksWrite: "tasks.md \uC791\uC131",
|
|
1052
|
-
docsCommitPlanning: "\uBB38\uC11C \uCEE4\uBC0B(\uAE30\
|
|
704
|
+
docsCommitPlanning: "\uBB38\uC11C \uCEE4\uBC0B(\uB3D9\uAE30\uD654)",
|
|
1053
705
|
issueCreate: "GitHub Issue \uC0DD\uC131",
|
|
1054
706
|
branchCreate: "\uBE0C\uB79C\uCE58 \uC0DD\uC131",
|
|
1055
707
|
tasksExecute: "\uD0DC\uC2A4\uD06C \uC2E4\uD589",
|
|
@@ -1066,9 +718,9 @@ var I18N = {
|
|
|
1066
718
|
planApproval: "plan.md \uB0B4\uC6A9\uC744 \uC0AC\uC6A9\uC790\uC5D0\uAC8C \uACF5\uC720\uD558\uACE0 \uC2B9\uC778(OK)\uC744 \uBC1B\uC73C\uC138\uC694.",
|
|
1067
719
|
tasksCreate: "tasks.md \uD15C\uD50C\uB9BF\uC744 \uBCF5\uC0AC\uD574 \uD0DC\uC2A4\uD06C\uB97C \uC791\uC131\uD558\uC138\uC694. (features/feature-base/tasks.md \uCC38\uACE0)",
|
|
1068
720
|
tasksNeedAtLeastOne: "tasks.md\uC5D0 \uCD5C\uC18C 1\uAC1C \uC774\uC0C1\uC758 \uD0DC\uC2A4\uD06C\uB97C \uC791\uC131\uD558\uC138\uC694.",
|
|
1069
|
-
docsCommitPlanning: 'cd "{docsGitCwd}" && git add "{featurePath}" && git commit -m "docs({folderName}
|
|
721
|
+
docsCommitPlanning: 'cd "{docsGitCwd}" && git add "{featurePath}" && git commit -m "docs(planning): {folderName} \uAE30\uD68D \uBB38\uC11C"',
|
|
1070
722
|
issueCreateAndWrite: "GitHub Issue\uB97C \uC0DD\uC131\uD55C \uB4A4, spec.md/tasks.md\uC758 \uC774\uC288 \uBC88\uD638\uB97C \uCC44\uC6B0\uACE0 \uBB38\uC11C \uCEE4\uBC0B\uC744 \uC900\uBE44\uD558\uC138\uC694. (skills/create-issue.md \uCC38\uACE0)",
|
|
1071
|
-
docsCommitIssueUpdate: 'cd "{docsGitCwd}" && git add "{featurePath}" && git commit -m "docs({
|
|
723
|
+
docsCommitIssueUpdate: 'cd "{docsGitCwd}" && git add "{featurePath}" && git commit -m "docs(#{issueNumber}): {folderName} \uBB38\uC11C \uC5C5\uB370\uC774\uD2B8"',
|
|
1072
724
|
standaloneNeedsProjectRoot: "standalone \uBAA8\uB4DC\uC5D0\uC11C\uB294 projectRoot \uC124\uC815\uC774 \uD544\uC694\uD569\uB2C8\uB2E4. (npx lee-spec-kit config --project-root ...)",
|
|
1073
725
|
createBranch: 'cd "{projectGitCwd}" && git checkout -b feat/{issueNumber}-{slug}',
|
|
1074
726
|
tasksAllDoneButNoChecklist: '\uBAA8\uB4E0 \uD0DC\uC2A4\uD06C\uAC00 DONE\uC774\uC9C0\uB9CC \uC644\uB8CC \uC870\uAC74 \uCCB4\uD06C\uB9AC\uC2A4\uD2B8 \uC139\uC158\uC744 \uCC3E\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4. tasks.md\uC758 "\uC644\uB8CC \uC870\uAC74" \uC139\uC158\uC744 \uCD94\uAC00/\uD655\uC778\uD558\uC138\uC694.',
|
|
@@ -1078,6 +730,7 @@ var I18N = {
|
|
|
1078
730
|
checkTaskStatuses: "\uD0DC\uC2A4\uD06C \uC0C1\uD0DC\uB97C \uD655\uC778\uD558\uC138\uC694. ({done}/{total}) (skills/execute-task.md \uCC38\uACE0)",
|
|
1079
731
|
prLegacyAsk: "tasks.md\uC5D0 PR/PR \uC0C1\uD0DC \uD544\uB4DC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4. \uD15C\uD50C\uB9BF\uC744 \uCD5C\uC2E0 \uD3EC\uB9F7\uC73C\uB85C \uC5C5\uB370\uC774\uD2B8\uD560\uAE4C\uC694? (OK \uD544\uC694)",
|
|
1080
732
|
prCreate: "PR\uC744 \uC0DD\uC131\uD558\uACE0 tasks.md\uC5D0 PR \uB9C1\uD06C\uB97C \uAE30\uB85D\uD558\uC138\uC694. (skills/create-pr.md \uCC38\uACE0)",
|
|
733
|
+
prFillStatus: "tasks.md\uC758 PR \uC0C1\uD0DC\uB97C Draft/Review/Approved \uC911 \uD558\uB098\uB85C \uC124\uC815\uD558\uC138\uC694. (merge \uD6C4 Approved\uB85C \uC5C5\uB370\uC774\uD2B8)",
|
|
1081
734
|
prResolveReview: "\uB9AC\uBDF0 \uCF54\uBA58\uD2B8\uB97C \uD574\uACB0\uD558\uACE0 PR \uC0C1\uD0DC\uB97C \uC5C5\uB370\uC774\uD2B8\uD558\uC138\uC694. (PR \uC0C1\uD0DC: Review \u2192 Approved)",
|
|
1082
735
|
prRequestReview: "\uB9AC\uBDF0\uC5B4\uC5D0\uAC8C \uB9AC\uBDF0\uB97C \uC694\uCCAD\uD558\uACE0 PR \uC0C1\uD0DC\uB97C Review\uB85C \uC5C5\uB370\uC774\uD2B8\uD558\uC138\uC694.",
|
|
1083
736
|
featureDone: "PR\uC774 Approved\uC774\uACE0 \uBAA8\uB4E0 \uD0DC\uC2A4\uD06C/\uC644\uB8CC \uC870\uAC74\uC774 \uCDA9\uC871\uB418\uC5C8\uC2B5\uB2C8\uB2E4. \uC774 Feature\uB294 \uC644\uB8CC \uC0C1\uD0DC\uC785\uB2C8\uB2E4.",
|
|
@@ -1086,7 +739,12 @@ var I18N = {
|
|
|
1086
739
|
warnings: {
|
|
1087
740
|
projectBranchUnavailable: "\uD504\uB85C\uC81D\uD2B8 \uBE0C\uB79C\uCE58\uB97C \uD655\uC778\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. (standalone \uBAA8\uB4DC\uC5D0\uC11C\uB294 projectRoot\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4.)",
|
|
1088
741
|
docsGitUnavailable: "docs \uB808\uD3EC\uC758 git \uC0C1\uD0DC\uB97C \uD655\uC778\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. (\uB808\uD3EC \uC704\uCE58 / git init \uD655\uC778)",
|
|
1089
|
-
legacyTasksPrFields: "\uAD6C\uBC84\uC804 tasks.md \uD3EC\uB9F7\uC785\uB2C8\uB2E4. PR \uB2E8\uACC4 \uC804\uC5D0 `PR` \uBC0F `PR \uC0C1\uD0DC` \uD544\uB4DC\uB97C \uCD94\uAC00\uD558\uC138\uC694."
|
|
742
|
+
legacyTasksPrFields: "\uAD6C\uBC84\uC804 tasks.md \uD3EC\uB9F7\uC785\uB2C8\uB2E4. PR \uB2E8\uACC4 \uC804\uC5D0 `PR` \uBC0F `PR \uC0C1\uD0DC` \uD544\uB4DC\uB97C \uCD94\uAC00\uD558\uC138\uC694.",
|
|
743
|
+
workflowSpecNotApproved: "\uC644\uB8CC \uC0C1\uD0DC\uC774\uC9C0\uB9CC spec.md \uC0C1\uD0DC\uAC00 Approved\uAC00 \uC544\uB2D9\uB2C8\uB2E4. (spec.md\uC758 \uC0C1\uD0DC\uB97C Approved\uB85C \uC5C5\uB370\uC774\uD2B8\uD558\uC138\uC694.)",
|
|
744
|
+
workflowPlanNotApproved: "\uC644\uB8CC \uC0C1\uD0DC\uC774\uC9C0\uB9CC plan.md \uC0C1\uD0DC\uAC00 Approved\uAC00 \uC544\uB2D9\uB2C8\uB2E4. (plan.md\uC758 \uC0C1\uD0DC\uB97C Approved\uB85C \uC5C5\uB370\uC774\uD2B8\uD558\uC138\uC694.)",
|
|
745
|
+
workflowPrLinkMissing: "\uC644\uB8CC \uC0C1\uD0DC\uC774\uC9C0\uB9CC PR \uB9C1\uD06C\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4. (tasks.md\uC758 PR \uD544\uB4DC\uB97C \uCC44\uC6B0\uC138\uC694.)",
|
|
746
|
+
workflowPrStatusMissing: "\uC644\uB8CC \uC0C1\uD0DC\uC774\uC9C0\uB9CC PR \uC0C1\uD0DC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4. (tasks.md\uC758 PR \uC0C1\uD0DC\uB97C Draft/Review/Approved \uC911 \uD558\uB098\uB85C \uC124\uC815\uD558\uC138\uC694.)",
|
|
747
|
+
workflowPrStatusNotApproved: "\uC644\uB8CC \uC0C1\uD0DC\uC774\uC9C0\uB9CC PR \uC0C1\uD0DC\uAC00 Approved\uAC00 \uC544\uB2D9\uB2C8\uB2E4. (merge \uD6C4 tasks.md\uC758 PR \uC0C1\uD0DC\uB97C Approved\uB85C \uC5C5\uB370\uC774\uD2B8\uD558\uC138\uC694.)"
|
|
1090
748
|
}
|
|
1091
749
|
},
|
|
1092
750
|
en: {
|
|
@@ -1097,7 +755,7 @@ var I18N = {
|
|
|
1097
755
|
planWrite: "Write plan.md",
|
|
1098
756
|
planApprove: "Approve plan.md",
|
|
1099
757
|
tasksWrite: "Write tasks.md",
|
|
1100
|
-
docsCommitPlanning: "Commit
|
|
758
|
+
docsCommitPlanning: "Commit docs (sync)",
|
|
1101
759
|
issueCreate: "Create GitHub Issue",
|
|
1102
760
|
branchCreate: "Create branch",
|
|
1103
761
|
tasksExecute: "Execute tasks",
|
|
@@ -1114,9 +772,9 @@ var I18N = {
|
|
|
1114
772
|
planApproval: "Share plan.md with the user and get approval (OK).",
|
|
1115
773
|
tasksCreate: "Copy the tasks.md template and write tasks. (See features/feature-base/tasks.md)",
|
|
1116
774
|
tasksNeedAtLeastOne: "Add at least one task to tasks.md.",
|
|
1117
|
-
docsCommitPlanning: 'cd "{docsGitCwd}" && git add "{featurePath}" && git commit -m "docs({folderName}
|
|
775
|
+
docsCommitPlanning: 'cd "{docsGitCwd}" && git add "{featurePath}" && git commit -m "docs(planning): {folderName} planning docs"',
|
|
1118
776
|
issueCreateAndWrite: "Create a GitHub Issue, then fill in the issue number in spec.md/tasks.md and prepare to commit docs. (See skills/create-issue.md)",
|
|
1119
|
-
docsCommitIssueUpdate: 'cd "{docsGitCwd}" && git add "{featurePath}" && git commit -m "docs({
|
|
777
|
+
docsCommitIssueUpdate: 'cd "{docsGitCwd}" && git add "{featurePath}" && git commit -m "docs(#{issueNumber}): {folderName} docs update"',
|
|
1120
778
|
standaloneNeedsProjectRoot: "In standalone mode, projectRoot is required. (npx lee-spec-kit config --project-root ...)",
|
|
1121
779
|
createBranch: 'cd "{projectGitCwd}" && git checkout -b feat/{issueNumber}-{slug}',
|
|
1122
780
|
tasksAllDoneButNoChecklist: 'All tasks are DONE but no completion checklist section was found. Add/verify the "Completion Criteria" section in tasks.md.',
|
|
@@ -1126,6 +784,7 @@ var I18N = {
|
|
|
1126
784
|
checkTaskStatuses: "Check task statuses. ({done}/{total}) (See skills/execute-task.md)",
|
|
1127
785
|
prLegacyAsk: "Legacy tasks.md format detected (missing PR/PR Status fields). Update to the latest format? (OK required)",
|
|
1128
786
|
prCreate: "Create a PR and record the PR link in tasks.md. (See skills/create-pr.md)",
|
|
787
|
+
prFillStatus: "Set PR Status in tasks.md to Draft/Review/Approved. (After merge, update to Approved)",
|
|
1129
788
|
prResolveReview: "Resolve review comments and update PR status. (PR Status: Review \u2192 Approved)",
|
|
1130
789
|
prRequestReview: "Request reviews and update PR status to Review.",
|
|
1131
790
|
featureDone: "PR is Approved and all tasks/completion criteria are satisfied. This feature is done.",
|
|
@@ -1134,7 +793,12 @@ var I18N = {
|
|
|
1134
793
|
warnings: {
|
|
1135
794
|
projectBranchUnavailable: "Cannot determine project branch. (In standalone mode, projectRoot is required.)",
|
|
1136
795
|
docsGitUnavailable: "Cannot read git status for the docs repo. (Check repo location / git init.)",
|
|
1137
|
-
legacyTasksPrFields: "Legacy tasks.md format detected. Add `PR` and `PR Status` fields before PR steps."
|
|
796
|
+
legacyTasksPrFields: "Legacy tasks.md format detected. Add `PR` and `PR Status` fields before PR steps.",
|
|
797
|
+
workflowSpecNotApproved: "Implementation is done but spec.md Status is not Approved. (Update spec.md Status to Approved.)",
|
|
798
|
+
workflowPlanNotApproved: "Implementation is done but plan.md Status is not Approved. (Update plan.md Status to Approved.)",
|
|
799
|
+
workflowPrLinkMissing: "Implementation is done but PR link is missing. (Fill the PR field in tasks.md.)",
|
|
800
|
+
workflowPrStatusMissing: "Implementation is done but PR Status is missing. (Set PR Status to Draft/Review/Approved in tasks.md.)",
|
|
801
|
+
workflowPrStatusNotApproved: "Implementation is done but PR Status is not Approved. (After merge, update PR Status to Approved in tasks.md.)"
|
|
1138
802
|
}
|
|
1139
803
|
}
|
|
1140
804
|
};
|
|
@@ -1147,11 +811,14 @@ function tr(lang, category, key, vars = {}) {
|
|
|
1147
811
|
function isCompletionChecklistDone(feature) {
|
|
1148
812
|
return !!feature.completionChecklist && feature.completionChecklist.total > 0 && feature.completionChecklist.checked === feature.completionChecklist.total;
|
|
1149
813
|
}
|
|
814
|
+
function isImplementationDone(feature) {
|
|
815
|
+
return feature.docs.tasksExists && feature.tasks.total > 0 && feature.tasks.total === feature.tasks.done && isCompletionChecklistDone(feature);
|
|
816
|
+
}
|
|
1150
817
|
function isPrMetadataConfigured(feature) {
|
|
1151
818
|
return feature.docs.prFieldExists && feature.docs.prStatusFieldExists;
|
|
1152
819
|
}
|
|
1153
820
|
function isFeatureDone(feature) {
|
|
1154
|
-
return feature.docs.tasksExists && feature.tasks.total > 0 && feature.tasks.total === feature.tasks.done && isCompletionChecklistDone(feature) && isPrMetadataConfigured(feature) && !!feature.pr.link && feature.pr.status === "Approved";
|
|
821
|
+
return feature.specStatus === "Approved" && feature.planStatus === "Approved" && feature.docs.tasksExists && feature.tasks.total > 0 && feature.tasks.total === feature.tasks.done && isCompletionChecklistDone(feature) && isPrMetadataConfigured(feature) && !!feature.pr.link && feature.pr.status === "Approved";
|
|
1155
822
|
}
|
|
1156
823
|
function getStepDefinitions(lang) {
|
|
1157
824
|
return [
|
|
@@ -1262,55 +929,57 @@ function getStepDefinitions(lang) {
|
|
|
1262
929
|
step: 7,
|
|
1263
930
|
name: tr(lang, "steps", "docsCommitPlanning"),
|
|
1264
931
|
checklist: {
|
|
1265
|
-
done: (f) => f.
|
|
932
|
+
done: (f) => f.docs.tasksExists && f.tasks.total > 0 && f.specStatus === "Approved" && f.planStatus === "Approved" && !f.git.docsHasUncommittedChanges
|
|
1266
933
|
},
|
|
1267
934
|
current: {
|
|
1268
|
-
when: (f) => f.docs.tasksExists && f.tasks.total > 0 && f.specStatus === "Approved" && f.planStatus === "Approved" && !f.
|
|
1269
|
-
actions: (f) =>
|
|
1270
|
-
{
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
935
|
+
when: (f) => f.docs.tasksExists && f.tasks.total > 0 && f.specStatus === "Approved" && f.planStatus === "Approved" && !f.activeTask && f.git.docsHasUncommittedChanges,
|
|
936
|
+
actions: (f) => {
|
|
937
|
+
if (f.issueNumber) {
|
|
938
|
+
return [
|
|
939
|
+
{
|
|
940
|
+
type: "command",
|
|
941
|
+
requiresUserOk: true,
|
|
942
|
+
scope: "docs",
|
|
943
|
+
cwd: f.git.docsGitCwd,
|
|
944
|
+
cmd: tr(lang, "messages", "docsCommitIssueUpdate", {
|
|
945
|
+
docsGitCwd: f.git.docsGitCwd,
|
|
946
|
+
featurePath: f.docs.featurePathFromDocs,
|
|
947
|
+
issueNumber: f.issueNumber,
|
|
948
|
+
folderName: f.folderName
|
|
949
|
+
})
|
|
950
|
+
}
|
|
951
|
+
];
|
|
1280
952
|
}
|
|
1281
|
-
|
|
953
|
+
return [
|
|
954
|
+
{
|
|
955
|
+
type: "command",
|
|
956
|
+
requiresUserOk: true,
|
|
957
|
+
scope: "docs",
|
|
958
|
+
cwd: f.git.docsGitCwd,
|
|
959
|
+
cmd: tr(lang, "messages", "docsCommitPlanning", {
|
|
960
|
+
docsGitCwd: f.git.docsGitCwd,
|
|
961
|
+
featurePath: f.docs.featurePathFromDocs,
|
|
962
|
+
folderName: f.folderName
|
|
963
|
+
})
|
|
964
|
+
}
|
|
965
|
+
];
|
|
966
|
+
}
|
|
1282
967
|
}
|
|
1283
968
|
},
|
|
1284
969
|
{
|
|
1285
970
|
step: 8,
|
|
1286
971
|
name: tr(lang, "steps", "issueCreate"),
|
|
1287
972
|
checklist: {
|
|
1288
|
-
done: (f) => !!f.issueNumber
|
|
973
|
+
done: (f) => !!f.issueNumber
|
|
1289
974
|
},
|
|
1290
|
-
current: {
|
|
1291
|
-
when: (f) => f.docs.tasksExists && f.tasks.total > 0 && f.specStatus === "Approved" && f.planStatus === "Approved" &&
|
|
1292
|
-
actions: (f) => {
|
|
1293
|
-
if (!f.issueNumber) {
|
|
1294
|
-
return [
|
|
1295
|
-
{
|
|
1296
|
-
type: "instruction",
|
|
1297
|
-
requiresUserOk: true,
|
|
1298
|
-
message: tr(lang, "messages", "issueCreateAndWrite")
|
|
1299
|
-
}
|
|
1300
|
-
];
|
|
1301
|
-
}
|
|
975
|
+
current: {
|
|
976
|
+
when: (f) => f.docs.tasksExists && f.tasks.total > 0 && f.specStatus === "Approved" && f.planStatus === "Approved" && !f.issueNumber,
|
|
977
|
+
actions: (f) => {
|
|
1302
978
|
return [
|
|
1303
979
|
{
|
|
1304
|
-
type: "
|
|
980
|
+
type: "instruction",
|
|
1305
981
|
requiresUserOk: true,
|
|
1306
|
-
|
|
1307
|
-
cwd: f.git.docsGitCwd,
|
|
1308
|
-
cmd: tr(lang, "messages", "docsCommitIssueUpdate", {
|
|
1309
|
-
docsGitCwd: f.git.docsGitCwd,
|
|
1310
|
-
featurePath: f.docs.featurePathFromDocs,
|
|
1311
|
-
issueNumber: f.issueNumber,
|
|
1312
|
-
folderName: f.folderName
|
|
1313
|
-
})
|
|
982
|
+
message: tr(lang, "messages", "issueCreateAndWrite")
|
|
1314
983
|
}
|
|
1315
984
|
];
|
|
1316
985
|
}
|
|
@@ -1321,7 +990,7 @@ function getStepDefinitions(lang) {
|
|
|
1321
990
|
name: tr(lang, "steps", "branchCreate"),
|
|
1322
991
|
checklist: { done: (f) => f.git.onExpectedBranch },
|
|
1323
992
|
current: {
|
|
1324
|
-
when: (f) => !!f.issueNumber && (!f.git.projectBranchAvailable || !f.git.onExpectedBranch),
|
|
993
|
+
when: (f) => !!f.issueNumber && !isImplementationDone(f) && !isFeatureDone(f) && (!f.git.projectBranchAvailable || !f.git.onExpectedBranch),
|
|
1325
994
|
actions: (f) => {
|
|
1326
995
|
if (!f.git.projectBranchAvailable || !f.git.projectGitCwd) {
|
|
1327
996
|
return [
|
|
@@ -1442,6 +1111,15 @@ function getStepDefinitions(lang) {
|
|
|
1442
1111
|
current: {
|
|
1443
1112
|
when: (f) => isPrMetadataConfigured(f) && !!f.pr.link && f.pr.status !== "Approved",
|
|
1444
1113
|
actions: (f) => {
|
|
1114
|
+
if (!f.pr.status) {
|
|
1115
|
+
return [
|
|
1116
|
+
{
|
|
1117
|
+
type: "instruction",
|
|
1118
|
+
requiresUserOk: true,
|
|
1119
|
+
message: tr(lang, "messages", "prFillStatus")
|
|
1120
|
+
}
|
|
1121
|
+
];
|
|
1122
|
+
}
|
|
1445
1123
|
if (f.pr.status === "Review") {
|
|
1446
1124
|
return [
|
|
1447
1125
|
{
|
|
@@ -1587,7 +1265,7 @@ function isExpectedFeatureBranch(branchName, issueNumber, slug, folderName) {
|
|
|
1587
1265
|
function escapeRegExp(value) {
|
|
1588
1266
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1589
1267
|
}
|
|
1590
|
-
function
|
|
1268
|
+
function extractSpecValue(content, key) {
|
|
1591
1269
|
const regex = new RegExp(
|
|
1592
1270
|
`^\\s*-\\s*\\*\\*${escapeRegExp(key)}\\*\\*\\s*:\\s*(.*)$`,
|
|
1593
1271
|
"m"
|
|
@@ -1597,7 +1275,7 @@ function extractSpecValue2(content, key) {
|
|
|
1597
1275
|
}
|
|
1598
1276
|
function extractFirstSpecValue(content, keys) {
|
|
1599
1277
|
for (const key of keys) {
|
|
1600
|
-
const value =
|
|
1278
|
+
const value = extractSpecValue(content, key);
|
|
1601
1279
|
if (value) return value;
|
|
1602
1280
|
}
|
|
1603
1281
|
return void 0;
|
|
@@ -1667,6 +1345,12 @@ function parseCompletionChecklist(content) {
|
|
|
1667
1345
|
}
|
|
1668
1346
|
return total > 0 ? { total, checked } : void 0;
|
|
1669
1347
|
}
|
|
1348
|
+
function isCompletionChecklistDone2(feature) {
|
|
1349
|
+
return !!feature.completionChecklist && feature.completionChecklist.total > 0 && feature.completionChecklist.checked === feature.completionChecklist.total;
|
|
1350
|
+
}
|
|
1351
|
+
function isPrMetadataConfigured2(feature) {
|
|
1352
|
+
return feature.docs.prFieldExists && feature.docs.prStatusFieldExists;
|
|
1353
|
+
}
|
|
1670
1354
|
async function parseFeature(featurePath, type, context, options) {
|
|
1671
1355
|
const lang = options.lang;
|
|
1672
1356
|
const folderName = path4.basename(featurePath);
|
|
@@ -1678,22 +1362,22 @@ async function parseFeature(featurePath, type, context, options) {
|
|
|
1678
1362
|
const tasksPath = path4.join(featurePath, "tasks.md");
|
|
1679
1363
|
let specStatus;
|
|
1680
1364
|
let issueNumber;
|
|
1681
|
-
const specExists = await
|
|
1365
|
+
const specExists = await fs8.pathExists(specPath);
|
|
1682
1366
|
if (specExists) {
|
|
1683
|
-
const content = await
|
|
1367
|
+
const content = await fs8.readFile(specPath, "utf-8");
|
|
1684
1368
|
const statusValue = extractFirstSpecValue(content, ["\uC0C1\uD0DC", "Status"]);
|
|
1685
1369
|
specStatus = parseDocStatus(statusValue);
|
|
1686
1370
|
const issueValue = extractFirstSpecValue(content, ["\uC774\uC288 \uBC88\uD638", "Issue Number", "Issue"]);
|
|
1687
1371
|
issueNumber = parseIssueNumber(issueValue);
|
|
1688
1372
|
}
|
|
1689
1373
|
let planStatus;
|
|
1690
|
-
const planExists = await
|
|
1374
|
+
const planExists = await fs8.pathExists(planPath);
|
|
1691
1375
|
if (planExists) {
|
|
1692
|
-
const content = await
|
|
1376
|
+
const content = await fs8.readFile(planPath, "utf-8");
|
|
1693
1377
|
const statusValue = extractFirstSpecValue(content, ["\uC0C1\uD0DC", "Status"]);
|
|
1694
1378
|
planStatus = parseDocStatus(statusValue);
|
|
1695
1379
|
}
|
|
1696
|
-
const tasksExists = await
|
|
1380
|
+
const tasksExists = await fs8.pathExists(tasksPath);
|
|
1697
1381
|
const tasksSummary = { total: 0, todo: 0, doing: 0, done: 0 };
|
|
1698
1382
|
let activeTask;
|
|
1699
1383
|
let nextTodoTask;
|
|
@@ -1703,7 +1387,7 @@ async function parseFeature(featurePath, type, context, options) {
|
|
|
1703
1387
|
let prFieldExists = false;
|
|
1704
1388
|
let prStatusFieldExists = false;
|
|
1705
1389
|
if (tasksExists) {
|
|
1706
|
-
const content = await
|
|
1390
|
+
const content = await fs8.readFile(tasksPath, "utf-8");
|
|
1707
1391
|
const { summary, activeTask: active, nextTodoTask: nextTodo } = parseTasks(content);
|
|
1708
1392
|
tasksSummary.total = summary.total;
|
|
1709
1393
|
tasksSummary.todo = summary.todo;
|
|
@@ -1742,12 +1426,33 @@ async function parseFeature(featurePath, type, context, options) {
|
|
|
1742
1426
|
if (tasksExists && (!prFieldExists || !prStatusFieldExists)) {
|
|
1743
1427
|
warnings.push(tr(lang, "warnings", "legacyTasksPrFields"));
|
|
1744
1428
|
}
|
|
1429
|
+
const implementationDone = tasksExists && tasksSummary.total > 0 && tasksSummary.total === tasksSummary.done && isCompletionChecklistDone2({ completionChecklist });
|
|
1430
|
+
const workflowDone = implementationDone && specStatus === "Approved" && planStatus === "Approved" && isPrMetadataConfigured2({ docs: { prFieldExists, prStatusFieldExists } }) && !!prLink && prStatus === "Approved";
|
|
1431
|
+
if (implementationDone && !workflowDone) {
|
|
1432
|
+
if (specStatus !== "Approved") {
|
|
1433
|
+
warnings.push(tr(lang, "warnings", "workflowSpecNotApproved"));
|
|
1434
|
+
}
|
|
1435
|
+
if (planStatus !== "Approved") {
|
|
1436
|
+
warnings.push(tr(lang, "warnings", "workflowPlanNotApproved"));
|
|
1437
|
+
}
|
|
1438
|
+
if (prFieldExists && prStatusFieldExists) {
|
|
1439
|
+
if (!prLink) warnings.push(tr(lang, "warnings", "workflowPrLinkMissing"));
|
|
1440
|
+
if (!prStatus) warnings.push(tr(lang, "warnings", "workflowPrStatusMissing"));
|
|
1441
|
+
if (prStatus && prStatus !== "Approved") {
|
|
1442
|
+
warnings.push(tr(lang, "warnings", "workflowPrStatusNotApproved"));
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1745
1446
|
const featureState = {
|
|
1746
1447
|
id,
|
|
1747
1448
|
slug,
|
|
1748
1449
|
folderName,
|
|
1749
1450
|
type,
|
|
1750
1451
|
path: featurePath,
|
|
1452
|
+
completion: {
|
|
1453
|
+
implementationDone,
|
|
1454
|
+
workflowDone
|
|
1455
|
+
},
|
|
1751
1456
|
issueNumber,
|
|
1752
1457
|
specStatus,
|
|
1753
1458
|
planStatus,
|
|
@@ -1813,7 +1518,7 @@ async function scanFeatures(config) {
|
|
|
1813
1518
|
ignore: ["**/feature-base/**"]
|
|
1814
1519
|
});
|
|
1815
1520
|
for (const dir of featureDirs) {
|
|
1816
|
-
if ((await
|
|
1521
|
+
if ((await fs8.stat(dir)).isDirectory()) {
|
|
1817
1522
|
features.push(
|
|
1818
1523
|
await parseFeature(
|
|
1819
1524
|
dir,
|
|
@@ -1830,62 +1535,421 @@ async function scanFeatures(config) {
|
|
|
1830
1535
|
)
|
|
1831
1536
|
);
|
|
1832
1537
|
}
|
|
1833
|
-
}
|
|
1834
|
-
} else {
|
|
1835
|
-
const feDirs = await glob("features/fe/*/", { cwd: config.docsDir, absolute: true });
|
|
1836
|
-
const beDirs = await glob("features/be/*/", { cwd: config.docsDir, absolute: true });
|
|
1837
|
-
for (const dir of feDirs) {
|
|
1838
|
-
if ((await
|
|
1839
|
-
features.push(
|
|
1840
|
-
await parseFeature(
|
|
1841
|
-
dir,
|
|
1842
|
-
"fe",
|
|
1843
|
-
{
|
|
1844
|
-
projectBranch: projectBranches.fe,
|
|
1845
|
-
docsBranch,
|
|
1846
|
-
docsGitCwd: config.docsDir,
|
|
1847
|
-
projectGitCwd: feProject?.cwd ?? void 0,
|
|
1848
|
-
docsDir: config.docsDir,
|
|
1849
|
-
projectBranchAvailable: Boolean(feProject?.cwd)
|
|
1850
|
-
},
|
|
1851
|
-
{ lang: config.lang, stepDefinitions }
|
|
1852
|
-
)
|
|
1853
|
-
);
|
|
1538
|
+
}
|
|
1539
|
+
} else {
|
|
1540
|
+
const feDirs = await glob("features/fe/*/", { cwd: config.docsDir, absolute: true });
|
|
1541
|
+
const beDirs = await glob("features/be/*/", { cwd: config.docsDir, absolute: true });
|
|
1542
|
+
for (const dir of feDirs) {
|
|
1543
|
+
if ((await fs8.stat(dir)).isDirectory()) {
|
|
1544
|
+
features.push(
|
|
1545
|
+
await parseFeature(
|
|
1546
|
+
dir,
|
|
1547
|
+
"fe",
|
|
1548
|
+
{
|
|
1549
|
+
projectBranch: projectBranches.fe,
|
|
1550
|
+
docsBranch,
|
|
1551
|
+
docsGitCwd: config.docsDir,
|
|
1552
|
+
projectGitCwd: feProject?.cwd ?? void 0,
|
|
1553
|
+
docsDir: config.docsDir,
|
|
1554
|
+
projectBranchAvailable: Boolean(feProject?.cwd)
|
|
1555
|
+
},
|
|
1556
|
+
{ lang: config.lang, stepDefinitions }
|
|
1557
|
+
)
|
|
1558
|
+
);
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
for (const dir of beDirs) {
|
|
1562
|
+
if ((await fs8.stat(dir)).isDirectory()) {
|
|
1563
|
+
features.push(
|
|
1564
|
+
await parseFeature(
|
|
1565
|
+
dir,
|
|
1566
|
+
"be",
|
|
1567
|
+
{
|
|
1568
|
+
projectBranch: projectBranches.be,
|
|
1569
|
+
docsBranch,
|
|
1570
|
+
docsGitCwd: config.docsDir,
|
|
1571
|
+
projectGitCwd: beProject?.cwd ?? void 0,
|
|
1572
|
+
docsDir: config.docsDir,
|
|
1573
|
+
projectBranchAvailable: Boolean(beProject?.cwd)
|
|
1574
|
+
},
|
|
1575
|
+
{ lang: config.lang, stepDefinitions }
|
|
1576
|
+
)
|
|
1577
|
+
);
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
return {
|
|
1582
|
+
features,
|
|
1583
|
+
branches: {
|
|
1584
|
+
docs: docsBranch,
|
|
1585
|
+
project: config.projectType === "single" ? { single: projectBranches.single } : { fe: projectBranches.fe, be: projectBranches.be }
|
|
1586
|
+
},
|
|
1587
|
+
warnings
|
|
1588
|
+
};
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
// src/commands/status.ts
|
|
1592
|
+
function statusCommand(program2) {
|
|
1593
|
+
program2.command("status").description("Show feature status").option("-w, --write", "Write status.md file").option("-s, --strict", "Fail on missing/duplicate feature IDs").action(async (options) => {
|
|
1594
|
+
try {
|
|
1595
|
+
await runStatus(options);
|
|
1596
|
+
} catch (error) {
|
|
1597
|
+
console.error(chalk6.red("\uC624\uB958:"), error);
|
|
1598
|
+
process.exit(1);
|
|
1599
|
+
}
|
|
1600
|
+
});
|
|
1601
|
+
}
|
|
1602
|
+
async function runStatus(options) {
|
|
1603
|
+
const cwd = process.cwd();
|
|
1604
|
+
const config = await getConfig(cwd);
|
|
1605
|
+
if (!config) {
|
|
1606
|
+
console.error(
|
|
1607
|
+
chalk6.red("docs \uD3F4\uB354\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uBA3C\uC800 init\uC744 \uC2E4\uD589\uD558\uC138\uC694.")
|
|
1608
|
+
);
|
|
1609
|
+
process.exit(1);
|
|
1610
|
+
}
|
|
1611
|
+
const { docsDir, projectType, projectName } = config;
|
|
1612
|
+
const featuresDir = path4.join(docsDir, "features");
|
|
1613
|
+
const scan = await scanFeatures(config);
|
|
1614
|
+
const features = [];
|
|
1615
|
+
const idMap = /* @__PURE__ */ new Map();
|
|
1616
|
+
for (const f of scan.features) {
|
|
1617
|
+
if (!f.docs.specExists || !f.docs.tasksExists) continue;
|
|
1618
|
+
const id = f.id || "UNKNOWN";
|
|
1619
|
+
const name = await getFeatureNameFromSpec(f.path, f.slug, f.folderName);
|
|
1620
|
+
const repo = projectType === "fullstack" ? `${projectName ?? "{{projectName}}"}-${f.type === "single" ? "" : f.type}`.replace(
|
|
1621
|
+
/-$/,
|
|
1622
|
+
""
|
|
1623
|
+
) : projectName ?? "{{projectName}}";
|
|
1624
|
+
const issue = f.issueNumber ? `#${f.issueNumber}` : "-";
|
|
1625
|
+
const relPath = path4.relative(docsDir, f.path);
|
|
1626
|
+
if (!idMap.has(id)) idMap.set(id, []);
|
|
1627
|
+
idMap.get(id).push(relPath);
|
|
1628
|
+
const total = f.tasks.total;
|
|
1629
|
+
const done = f.tasks.done;
|
|
1630
|
+
const doing = f.tasks.doing;
|
|
1631
|
+
const todo = f.tasks.todo;
|
|
1632
|
+
let status = "TODO";
|
|
1633
|
+
if (total > 0 && done === total) status = "DONE";
|
|
1634
|
+
else if (doing > 0) status = "DOING";
|
|
1635
|
+
else if (todo > 0) status = "TODO";
|
|
1636
|
+
else if (total === 0) status = "NO_TASKS";
|
|
1637
|
+
features.push({
|
|
1638
|
+
id,
|
|
1639
|
+
name,
|
|
1640
|
+
repo,
|
|
1641
|
+
issue,
|
|
1642
|
+
status,
|
|
1643
|
+
progress: `${done}/${total}`,
|
|
1644
|
+
path: relPath
|
|
1645
|
+
});
|
|
1646
|
+
}
|
|
1647
|
+
if (features.length === 0) {
|
|
1648
|
+
console.log(chalk6.yellow("Feature\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4."));
|
|
1649
|
+
return;
|
|
1650
|
+
}
|
|
1651
|
+
if (options.strict) {
|
|
1652
|
+
const duplicates = [...idMap.entries()].filter(
|
|
1653
|
+
([, paths]) => paths.length > 1
|
|
1654
|
+
);
|
|
1655
|
+
if (duplicates.length > 0) {
|
|
1656
|
+
console.error(chalk6.red("\uC911\uBCF5 Feature ID \uBC1C\uACAC:"));
|
|
1657
|
+
for (const [id, paths] of duplicates) {
|
|
1658
|
+
console.error(chalk6.red(` ${id}:`));
|
|
1659
|
+
for (const p of paths) {
|
|
1660
|
+
console.error(chalk6.red(` - ${p}`));
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
process.exit(1);
|
|
1664
|
+
}
|
|
1665
|
+
const unknowns = [...idMap.entries()].filter(([id]) => id === "UNKNOWN");
|
|
1666
|
+
if (unknowns.length > 0) {
|
|
1667
|
+
console.error(chalk6.red("Feature ID\uAC00 \uC5C6\uB294 \uD56D\uBAA9:"));
|
|
1668
|
+
for (const [, paths] of unknowns) {
|
|
1669
|
+
for (const p of paths) {
|
|
1670
|
+
console.error(chalk6.red(` - ${p}`));
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
process.exit(1);
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
features.sort((a, b) => a.id.localeCompare(b.id));
|
|
1677
|
+
const header = "| ID | Name | Repo | Issue | Status | Progress | Path |";
|
|
1678
|
+
const separator = "| --- | --- | --- | --- | --- | --- | --- |";
|
|
1679
|
+
console.log();
|
|
1680
|
+
console.log(header);
|
|
1681
|
+
console.log(separator);
|
|
1682
|
+
for (const f of features) {
|
|
1683
|
+
const statusColor = f.status === "DONE" ? chalk6.green : f.status === "DOING" ? chalk6.yellow : chalk6.gray;
|
|
1684
|
+
console.log(
|
|
1685
|
+
`| ${f.id} | ${f.name} | ${f.repo} | ${f.issue} | ${statusColor(f.status)} | ${f.progress} | ${f.path} |`
|
|
1686
|
+
);
|
|
1687
|
+
}
|
|
1688
|
+
console.log();
|
|
1689
|
+
if (options.write) {
|
|
1690
|
+
const outputPath = path4.join(featuresDir, "status.md");
|
|
1691
|
+
const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
1692
|
+
const content = [
|
|
1693
|
+
"# Feature Status",
|
|
1694
|
+
"",
|
|
1695
|
+
`- Generated: ${date}`,
|
|
1696
|
+
"- Source: `tasks.md`, `spec.md`",
|
|
1697
|
+
"",
|
|
1698
|
+
header,
|
|
1699
|
+
separator,
|
|
1700
|
+
...features.map(
|
|
1701
|
+
(f) => `| ${f.id} | ${f.name} | ${f.repo} | ${f.issue} | ${f.status} | ${f.progress} | ${f.path} |`
|
|
1702
|
+
),
|
|
1703
|
+
""
|
|
1704
|
+
].join("\n");
|
|
1705
|
+
await fs8.writeFile(outputPath, content, "utf-8");
|
|
1706
|
+
console.log(chalk6.green(`\u2705 ${outputPath} \uC0DD\uC131 \uC644\uB8CC`));
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
function escapeRegExp2(value) {
|
|
1710
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1711
|
+
}
|
|
1712
|
+
async function getFeatureNameFromSpec(featureDir, fallbackSlug, fallbackFolderName) {
|
|
1713
|
+
try {
|
|
1714
|
+
const specPath = path4.join(featureDir, "spec.md");
|
|
1715
|
+
if (!await fs8.pathExists(specPath)) return fallbackSlug;
|
|
1716
|
+
const content = await fs8.readFile(specPath, "utf-8");
|
|
1717
|
+
const keys = ["\uAE30\uB2A5\uBA85", "Feature Name"];
|
|
1718
|
+
for (const key of keys) {
|
|
1719
|
+
const regex = new RegExp(
|
|
1720
|
+
`^\\s*-\\s*\\*\\*${escapeRegExp2(key)}\\*\\*\\s*:\\s*(.*)$`,
|
|
1721
|
+
"m"
|
|
1722
|
+
);
|
|
1723
|
+
const match = content.match(regex);
|
|
1724
|
+
const value = match?.[1]?.trim();
|
|
1725
|
+
if (value) return value;
|
|
1726
|
+
}
|
|
1727
|
+
} catch {
|
|
1728
|
+
}
|
|
1729
|
+
return fallbackSlug || fallbackFolderName;
|
|
1730
|
+
}
|
|
1731
|
+
function updateCommand(program2) {
|
|
1732
|
+
program2.command("update").description("Update docs templates to the latest version").option("--agents", "Update agents/ folder only").option("--templates", "Update feature-base/ folder only").option("-f, --force", "Force overwrite without confirmation").action(async (options) => {
|
|
1733
|
+
try {
|
|
1734
|
+
await runUpdate(options);
|
|
1735
|
+
} catch (error) {
|
|
1736
|
+
if (error instanceof Error && error.message === "canceled") {
|
|
1737
|
+
console.log(chalk6.yellow("\n\uC791\uC5C5\uC774 \uCDE8\uC18C\uB418\uC5C8\uC2B5\uB2C8\uB2E4."));
|
|
1738
|
+
process.exit(0);
|
|
1739
|
+
}
|
|
1740
|
+
console.error(chalk6.red("\uC624\uB958:"), error);
|
|
1741
|
+
process.exit(1);
|
|
1742
|
+
}
|
|
1743
|
+
});
|
|
1744
|
+
}
|
|
1745
|
+
async function runUpdate(options) {
|
|
1746
|
+
const cwd = process.cwd();
|
|
1747
|
+
const config = await getConfig(cwd);
|
|
1748
|
+
if (!config) {
|
|
1749
|
+
console.error(
|
|
1750
|
+
chalk6.red("docs \uD3F4\uB354\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uBA3C\uC800 init\uC744 \uC2E4\uD589\uD558\uC138\uC694.")
|
|
1751
|
+
);
|
|
1752
|
+
process.exit(1);
|
|
1753
|
+
}
|
|
1754
|
+
const { docsDir, projectType, lang } = config;
|
|
1755
|
+
const templatesDir = getTemplatesDir();
|
|
1756
|
+
const sourceDir = path4.join(templatesDir, lang, projectType);
|
|
1757
|
+
const updateAgents = options.agents || !options.agents && !options.templates;
|
|
1758
|
+
const updateTemplates = options.templates || !options.agents && !options.templates;
|
|
1759
|
+
console.log(chalk6.blue("\u{1F4E6} \uD15C\uD50C\uB9BF \uC5C5\uB370\uC774\uD2B8\uB97C \uC2DC\uC791\uD569\uB2C8\uB2E4..."));
|
|
1760
|
+
console.log(chalk6.gray(` - \uC5B8\uC5B4: ${lang}`));
|
|
1761
|
+
console.log(chalk6.gray(` - \uD0C0\uC785: ${projectType}`));
|
|
1762
|
+
console.log();
|
|
1763
|
+
let updatedCount = 0;
|
|
1764
|
+
if (updateAgents) {
|
|
1765
|
+
console.log(chalk6.blue("\u{1F4C1} agents/ \uD3F4\uB354 \uC5C5\uB370\uC774\uD2B8 \uC911..."));
|
|
1766
|
+
const commonAgents = path4.join(templatesDir, lang, "common", "agents");
|
|
1767
|
+
const typeAgents = path4.join(templatesDir, lang, projectType, "agents");
|
|
1768
|
+
const targetAgents = path4.join(docsDir, "agents");
|
|
1769
|
+
const featurePath = projectType === "fullstack" ? "docs/features/{be|fe}" : "docs/features";
|
|
1770
|
+
const replacements = {
|
|
1771
|
+
"{{featurePath}}": featurePath
|
|
1772
|
+
};
|
|
1773
|
+
if (await fs8.pathExists(commonAgents)) {
|
|
1774
|
+
const count = await updateFolder(
|
|
1775
|
+
commonAgents,
|
|
1776
|
+
targetAgents,
|
|
1777
|
+
options.force,
|
|
1778
|
+
replacements
|
|
1779
|
+
);
|
|
1780
|
+
updatedCount += count;
|
|
1781
|
+
}
|
|
1782
|
+
if (await fs8.pathExists(typeAgents)) {
|
|
1783
|
+
const count = await updateFolder(typeAgents, targetAgents, options.force);
|
|
1784
|
+
updatedCount += count;
|
|
1785
|
+
}
|
|
1786
|
+
console.log(chalk6.green(` \u2705 agents/ \uC5C5\uB370\uC774\uD2B8 \uC644\uB8CC`));
|
|
1787
|
+
}
|
|
1788
|
+
if (updateTemplates) {
|
|
1789
|
+
console.log(chalk6.blue("\u{1F4C1} features/feature-base/ \uD3F4\uB354 \uC5C5\uB370\uC774\uD2B8 \uC911..."));
|
|
1790
|
+
const sourceFeatureBase = path4.join(sourceDir, "features", "feature-base");
|
|
1791
|
+
const targetFeatureBase = path4.join(docsDir, "features", "feature-base");
|
|
1792
|
+
if (await fs8.pathExists(sourceFeatureBase)) {
|
|
1793
|
+
const count = await updateFolder(
|
|
1794
|
+
sourceFeatureBase,
|
|
1795
|
+
targetFeatureBase,
|
|
1796
|
+
options.force
|
|
1797
|
+
);
|
|
1798
|
+
updatedCount += count;
|
|
1799
|
+
console.log(chalk6.green(` \u2705 ${count}\uAC1C \uD30C\uC77C \uC5C5\uB370\uC774\uD2B8 \uC644\uB8CC`));
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
console.log();
|
|
1803
|
+
console.log(chalk6.green(`\u2705 \uCD1D ${updatedCount}\uAC1C \uD30C\uC77C \uC5C5\uB370\uC774\uD2B8 \uC644\uB8CC!`));
|
|
1804
|
+
}
|
|
1805
|
+
async function updateFolder(sourceDir, targetDir, force, replacements) {
|
|
1806
|
+
const protectedFiles = /* @__PURE__ */ new Set(["custom.md", "constitution.md"]);
|
|
1807
|
+
await fs8.ensureDir(targetDir);
|
|
1808
|
+
const files = await fs8.readdir(sourceDir);
|
|
1809
|
+
let updatedCount = 0;
|
|
1810
|
+
for (const file of files) {
|
|
1811
|
+
const sourcePath = path4.join(sourceDir, file);
|
|
1812
|
+
const targetPath = path4.join(targetDir, file);
|
|
1813
|
+
const stat = await fs8.stat(sourcePath);
|
|
1814
|
+
if (stat.isFile()) {
|
|
1815
|
+
if (protectedFiles.has(file)) {
|
|
1816
|
+
continue;
|
|
1817
|
+
}
|
|
1818
|
+
let sourceContent = await fs8.readFile(sourcePath, "utf-8");
|
|
1819
|
+
if (replacements) {
|
|
1820
|
+
for (const [key, value] of Object.entries(replacements)) {
|
|
1821
|
+
sourceContent = sourceContent.replaceAll(key, value);
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
let shouldUpdate = true;
|
|
1825
|
+
if (await fs8.pathExists(targetPath)) {
|
|
1826
|
+
const targetContent = await fs8.readFile(targetPath, "utf-8");
|
|
1827
|
+
if (sourceContent === targetContent) {
|
|
1828
|
+
continue;
|
|
1829
|
+
}
|
|
1830
|
+
if (!force) {
|
|
1831
|
+
console.log(
|
|
1832
|
+
chalk6.yellow(` \u26A0\uFE0F ${file} - \uBCC0\uACBD \uAC10\uC9C0 (--force\uB85C \uB36E\uC5B4\uC4F0\uAE30)`)
|
|
1833
|
+
);
|
|
1834
|
+
shouldUpdate = false;
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
if (shouldUpdate) {
|
|
1838
|
+
await fs8.writeFile(targetPath, sourceContent);
|
|
1839
|
+
console.log(chalk6.gray(` \u{1F4C4} ${file} \uC5C5\uB370\uC774\uD2B8`));
|
|
1840
|
+
updatedCount++;
|
|
1854
1841
|
}
|
|
1842
|
+
} else if (stat.isDirectory()) {
|
|
1843
|
+
const subCount = await updateFolder(
|
|
1844
|
+
sourcePath,
|
|
1845
|
+
targetPath,
|
|
1846
|
+
force,
|
|
1847
|
+
replacements
|
|
1848
|
+
);
|
|
1849
|
+
updatedCount += subCount;
|
|
1855
1850
|
}
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
docsDir: config.docsDir,
|
|
1868
|
-
projectBranchAvailable: Boolean(beProject?.cwd)
|
|
1869
|
-
},
|
|
1870
|
-
{ lang: config.lang, stepDefinitions }
|
|
1871
|
-
)
|
|
1872
|
-
);
|
|
1851
|
+
}
|
|
1852
|
+
return updatedCount;
|
|
1853
|
+
}
|
|
1854
|
+
function configCommand(program2) {
|
|
1855
|
+
program2.command("config").description("View or modify project configuration").option("--project-root <path>", "Set project root path").option("--repo <repo>", "Repository type for fullstack: fe | be").action(async (options) => {
|
|
1856
|
+
try {
|
|
1857
|
+
await runConfig(options);
|
|
1858
|
+
} catch (error) {
|
|
1859
|
+
if (error instanceof Error && error.message === "canceled") {
|
|
1860
|
+
console.log(chalk6.yellow("\n\uC791\uC5C5\uC774 \uCDE8\uC18C\uB418\uC5C8\uC2B5\uB2C8\uB2E4."));
|
|
1861
|
+
process.exit(0);
|
|
1873
1862
|
}
|
|
1863
|
+
console.error(chalk6.red("\uC624\uB958:"), error);
|
|
1864
|
+
process.exit(1);
|
|
1874
1865
|
}
|
|
1866
|
+
});
|
|
1867
|
+
}
|
|
1868
|
+
async function runConfig(options) {
|
|
1869
|
+
const cwd = process.cwd();
|
|
1870
|
+
const config = await getConfig(cwd);
|
|
1871
|
+
if (!config) {
|
|
1872
|
+
console.log(
|
|
1873
|
+
chalk6.red("\uC124\uC815 \uD30C\uC77C\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uBA3C\uC800 init\uC744 \uC2E4\uD589\uD574\uC8FC\uC138\uC694.")
|
|
1874
|
+
);
|
|
1875
|
+
process.exit(1);
|
|
1875
1876
|
}
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
}
|
|
1882
|
-
|
|
1883
|
-
|
|
1877
|
+
const configPath = path4.join(config.docsDir, ".lee-spec-kit.json");
|
|
1878
|
+
if (!options.projectRoot) {
|
|
1879
|
+
console.log();
|
|
1880
|
+
console.log(chalk6.blue("\u{1F4CB} \uD604\uC7AC \uC124\uC815:"));
|
|
1881
|
+
console.log();
|
|
1882
|
+
console.log(chalk6.gray(` \uACBD\uB85C: ${configPath}`));
|
|
1883
|
+
console.log();
|
|
1884
|
+
const configFile2 = await fs8.readJson(configPath);
|
|
1885
|
+
console.log(JSON.stringify(configFile2, null, 2));
|
|
1886
|
+
console.log();
|
|
1887
|
+
return;
|
|
1888
|
+
}
|
|
1889
|
+
const configFile = await fs8.readJson(configPath);
|
|
1890
|
+
if (configFile.docsRepo !== "standalone") {
|
|
1891
|
+
console.log(
|
|
1892
|
+
chalk6.yellow("\u26A0\uFE0F projectRoot\uB294 standalone \uBAA8\uB4DC\uC5D0\uC11C\uB9CC \uC124\uC815 \uAC00\uB2A5\uD569\uB2C8\uB2E4.")
|
|
1893
|
+
);
|
|
1894
|
+
return;
|
|
1895
|
+
}
|
|
1896
|
+
const projectType = configFile.projectType;
|
|
1897
|
+
if (projectType === "fullstack") {
|
|
1898
|
+
if (!options.repo) {
|
|
1899
|
+
const response = await prompts(
|
|
1900
|
+
[
|
|
1901
|
+
{
|
|
1902
|
+
type: "select",
|
|
1903
|
+
name: "repo",
|
|
1904
|
+
message: "\uC218\uC815\uD560 \uB808\uD3EC\uC9C0\uD1A0\uB9AC\uB97C \uC120\uD0DD\uD558\uC138\uC694:",
|
|
1905
|
+
choices: [
|
|
1906
|
+
{ title: "Frontend (fe)", value: "fe" },
|
|
1907
|
+
{ title: "Backend (be)", value: "be" }
|
|
1908
|
+
]
|
|
1909
|
+
}
|
|
1910
|
+
],
|
|
1911
|
+
{
|
|
1912
|
+
onCancel: () => {
|
|
1913
|
+
throw new Error("canceled");
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
);
|
|
1917
|
+
options.repo = response.repo;
|
|
1918
|
+
}
|
|
1919
|
+
if (!options.repo || !["fe", "be"].includes(options.repo)) {
|
|
1920
|
+
console.log(
|
|
1921
|
+
chalk6.red(
|
|
1922
|
+
"Fullstack \uD504\uB85C\uC81D\uD2B8\uB294 --repo fe \uB610\uB294 --repo be\uB97C \uC9C0\uC815\uD574\uC57C \uD569\uB2C8\uB2E4."
|
|
1923
|
+
)
|
|
1924
|
+
);
|
|
1925
|
+
return;
|
|
1926
|
+
}
|
|
1927
|
+
const currentRoot = configFile.projectRoot || { fe: "", be: "" };
|
|
1928
|
+
if (typeof currentRoot === "string") {
|
|
1929
|
+
configFile.projectRoot = {
|
|
1930
|
+
fe: options.repo === "fe" ? options.projectRoot : "",
|
|
1931
|
+
be: options.repo === "be" ? options.projectRoot : ""
|
|
1932
|
+
};
|
|
1933
|
+
} else {
|
|
1934
|
+
currentRoot[options.repo] = options.projectRoot;
|
|
1935
|
+
configFile.projectRoot = currentRoot;
|
|
1936
|
+
}
|
|
1937
|
+
console.log(
|
|
1938
|
+
chalk6.green(
|
|
1939
|
+
`\u2705 ${options.repo.toUpperCase()} projectRoot \uC124\uC815 \uC644\uB8CC: ${options.projectRoot}`
|
|
1940
|
+
)
|
|
1941
|
+
);
|
|
1942
|
+
} else {
|
|
1943
|
+
configFile.projectRoot = options.projectRoot;
|
|
1944
|
+
console.log(
|
|
1945
|
+
chalk6.green(`\u2705 projectRoot \uC124\uC815 \uC644\uB8CC: ${options.projectRoot}`)
|
|
1946
|
+
);
|
|
1947
|
+
}
|
|
1948
|
+
await fs8.writeJson(configPath, configFile, { spaces: 2 });
|
|
1949
|
+
console.log();
|
|
1884
1950
|
}
|
|
1885
|
-
|
|
1886
|
-
// src/commands/context.ts
|
|
1887
1951
|
function contextCommand(program2) {
|
|
1888
|
-
program2.command("context [feature-name]").description("Show current feature context and next actions").option("--json", "Output in JSON format for agents").option("--repo <repo>", "Repository type for fullstack: fe | be").action(
|
|
1952
|
+
program2.command("context [feature-name]").description("Show current feature context and next actions").option("--json", "Output in JSON format for agents").option("--repo <repo>", "Repository type for fullstack: fe | be").option("--all", "Include completed features when auto-detecting").option("--done", "Show completed (workflow-done) features only").action(
|
|
1889
1953
|
async (featureName, options) => {
|
|
1890
1954
|
try {
|
|
1891
1955
|
await runContext(featureName, options);
|
|
@@ -1931,12 +1995,22 @@ async function runContext(featureName, options) {
|
|
|
1931
1995
|
const stepDefinitions = getStepDefinitions(lang);
|
|
1932
1996
|
const stepsMap = getStepsMap(lang);
|
|
1933
1997
|
const { features, branches, warnings } = await scanFeatures(config);
|
|
1998
|
+
const doneFeatures = features.filter((f2) => f2.completion.workflowDone);
|
|
1999
|
+
const openFeatures = features.filter((f2) => !f2.completion.workflowDone);
|
|
2000
|
+
const inProgressFeatures = openFeatures.filter(
|
|
2001
|
+
(f2) => !f2.completion.implementationDone
|
|
2002
|
+
);
|
|
2003
|
+
const readyToCloseFeatures = openFeatures.filter(
|
|
2004
|
+
(f2) => f2.completion.implementationDone
|
|
2005
|
+
);
|
|
1934
2006
|
let targetFeatures = [];
|
|
2007
|
+
let selectionMode = "explicit";
|
|
1935
2008
|
if (featureName) {
|
|
1936
2009
|
targetFeatures = features.filter((f2) => matchesFeatureSelector(f2, featureName));
|
|
1937
2010
|
if (options.repo) {
|
|
1938
2011
|
targetFeatures = targetFeatures.filter((f2) => f2.type === options.repo);
|
|
1939
2012
|
}
|
|
2013
|
+
selectionMode = "explicit";
|
|
1940
2014
|
} else {
|
|
1941
2015
|
if (config.projectType === "single") {
|
|
1942
2016
|
const branchName = branches.project.single || "";
|
|
@@ -1958,15 +2032,33 @@ async function runContext(featureName, options) {
|
|
|
1958
2032
|
) : [];
|
|
1959
2033
|
targetFeatures = [...feMatches, ...beMatches];
|
|
1960
2034
|
}
|
|
1961
|
-
if (targetFeatures.length
|
|
2035
|
+
if (targetFeatures.length > 0) {
|
|
2036
|
+
selectionMode = "branch";
|
|
2037
|
+
} else if (options.all) {
|
|
2038
|
+
targetFeatures = features;
|
|
2039
|
+
selectionMode = "all";
|
|
2040
|
+
} else if (options.done) {
|
|
2041
|
+
targetFeatures = doneFeatures;
|
|
2042
|
+
selectionMode = "done";
|
|
2043
|
+
} else {
|
|
2044
|
+
targetFeatures = openFeatures;
|
|
2045
|
+
selectionMode = "open";
|
|
2046
|
+
}
|
|
1962
2047
|
}
|
|
1963
2048
|
if (options.json) {
|
|
2049
|
+
const isNoOpen = selectionMode === "open" && features.length > 0 && openFeatures.length === 0;
|
|
1964
2050
|
const result = {
|
|
1965
|
-
status: features.length === 0 ? "no_features" : targetFeatures.length === 1 ? "single_matched" : targetFeatures.length > 1 ? "multiple_active" : "no_match",
|
|
2051
|
+
status: features.length === 0 ? "no_features" : isNoOpen ? "no_open" : targetFeatures.length === 1 ? "single_matched" : targetFeatures.length > 1 ? "multiple_active" : "no_match",
|
|
2052
|
+
selectionMode,
|
|
1966
2053
|
branches,
|
|
1967
2054
|
warnings,
|
|
1968
2055
|
matchedFeature: targetFeatures.length === 1 ? targetFeatures[0] : null,
|
|
1969
2056
|
candidates: targetFeatures.length > 1 ? targetFeatures : [],
|
|
2057
|
+
// "Completed" now means workflow-done.
|
|
2058
|
+
completedCandidates: selectionMode === "open" ? doneFeatures : [],
|
|
2059
|
+
openCandidates: selectionMode === "open" ? openFeatures : [],
|
|
2060
|
+
inProgressCandidates: selectionMode === "open" ? inProgressFeatures : [],
|
|
2061
|
+
readyToCloseCandidates: selectionMode === "open" ? readyToCloseFeatures : [],
|
|
1970
2062
|
actions: targetFeatures.length === 1 ? targetFeatures[0].actions : [],
|
|
1971
2063
|
recommendation: ""
|
|
1972
2064
|
};
|
|
@@ -2022,22 +2114,53 @@ async function runContext(featureName, options) {
|
|
|
2022
2114
|
console.log();
|
|
2023
2115
|
}
|
|
2024
2116
|
if (targetFeatures.length > 1) {
|
|
2025
|
-
|
|
2026
|
-
chalk6.blue(`\u{1F539} ${targetFeatures.length} Active Features Detected:`)
|
|
2027
|
-
);
|
|
2028
|
-
console.log();
|
|
2029
|
-
targetFeatures.forEach((f2) => {
|
|
2030
|
-
const stepName2 = stepsMap[f2.currentStep] || "Unknown";
|
|
2031
|
-
const typeStr = config.projectType === "fullstack" ? chalk6.cyan(`(${f2.type})`) : "";
|
|
2117
|
+
if (selectionMode === "open") {
|
|
2032
2118
|
console.log(
|
|
2033
|
-
|
|
2119
|
+
chalk6.gray(
|
|
2120
|
+
` (\uBE0C\uB79C\uCE58\uB85C Feature\uB97C \uD2B9\uC815\uD558\uC9C0 \uBABB\uD574 \uBBF8\uC644\uB8CC Feature\uB9CC \uD45C\uC2DC\uD569\uB2C8\uB2E4. \uC9C4\uD589 \uC911: ${inProgressFeatures.length}\uAC1C / \uC885\uB8CC \uB300\uAE30: ${readyToCloseFeatures.length}\uAC1C / \uC644\uB8CC: ${doneFeatures.length}\uAC1C)`
|
|
2121
|
+
)
|
|
2034
2122
|
);
|
|
2035
|
-
|
|
2123
|
+
console.log();
|
|
2124
|
+
}
|
|
2125
|
+
if (selectionMode === "open") {
|
|
2126
|
+
console.log(chalk6.blue(`\u{1F539} In Progress (${inProgressFeatures.length})`));
|
|
2127
|
+
inProgressFeatures.forEach((f2) => {
|
|
2128
|
+
const stepName2 = stepsMap[f2.currentStep] || "Unknown";
|
|
2129
|
+
const typeStr = config.projectType === "fullstack" ? chalk6.cyan(`(${f2.type})`) : "";
|
|
2130
|
+
console.log(
|
|
2131
|
+
` \u2022 ${chalk6.bold(f2.folderName)} ${typeStr} - ${chalk6.yellow(stepName2)}`
|
|
2132
|
+
);
|
|
2133
|
+
});
|
|
2134
|
+
console.log();
|
|
2135
|
+
console.log(chalk6.blue(`\u{1F538} Ready To Close (${readyToCloseFeatures.length})`));
|
|
2136
|
+
readyToCloseFeatures.forEach((f2) => {
|
|
2137
|
+
const stepName2 = stepsMap[f2.currentStep] || "Unknown";
|
|
2138
|
+
const typeStr = config.projectType === "fullstack" ? chalk6.cyan(`(${f2.type})`) : "";
|
|
2139
|
+
console.log(
|
|
2140
|
+
` \u2022 ${chalk6.bold(f2.folderName)} ${typeStr} - ${chalk6.yellow(stepName2)}`
|
|
2141
|
+
);
|
|
2142
|
+
});
|
|
2143
|
+
} else {
|
|
2144
|
+
const title = selectionMode === "all" ? `\u{1F539} ${targetFeatures.length} Features:` : selectionMode === "done" ? `\u{1F539} ${targetFeatures.length} Done Features:` : `\u{1F539} ${targetFeatures.length} Features Detected:`;
|
|
2145
|
+
console.log(chalk6.blue(title));
|
|
2146
|
+
console.log();
|
|
2147
|
+
targetFeatures.forEach((f2) => {
|
|
2148
|
+
const stepName2 = stepsMap[f2.currentStep] || "Unknown";
|
|
2149
|
+
const typeStr = config.projectType === "fullstack" ? chalk6.cyan(`(${f2.type})`) : "";
|
|
2150
|
+
console.log(
|
|
2151
|
+
` \u2022 ${chalk6.bold(f2.folderName)} ${typeStr} - ${chalk6.yellow(stepName2)}`
|
|
2152
|
+
);
|
|
2153
|
+
});
|
|
2154
|
+
}
|
|
2036
2155
|
console.log();
|
|
2037
2156
|
console.log(chalk6.gray("Tip: \uD2B9\uC815 Feature\uC758 \uC0C1\uC138 \uC815\uBCF4\uB97C \uBCF4\uB824\uBA74:"));
|
|
2038
2157
|
console.log(
|
|
2039
2158
|
chalk6.gray(" $ npx lee-spec-kit context <slug|F001|F001-slug> [--repo fe|be]")
|
|
2040
2159
|
);
|
|
2160
|
+
if (selectionMode === "open") {
|
|
2161
|
+
console.log(chalk6.gray(" $ npx lee-spec-kit context --all # \uC804\uCCB4 \uBCF4\uAE30"));
|
|
2162
|
+
console.log(chalk6.gray(" $ npx lee-spec-kit context --done # \uC644\uB8CC\uB9CC \uBCF4\uAE30"));
|
|
2163
|
+
}
|
|
2041
2164
|
console.log();
|
|
2042
2165
|
return;
|
|
2043
2166
|
}
|
|
@@ -2047,6 +2170,9 @@ async function runContext(featureName, options) {
|
|
|
2047
2170
|
console.log(
|
|
2048
2171
|
`\u{1F539} Feature: ${chalk6.bold(f.folderName)} ${config.projectType === "fullstack" ? chalk6.cyan(`(${f.type})`) : ""}`
|
|
2049
2172
|
);
|
|
2173
|
+
console.log(
|
|
2174
|
+
` \u2022 Completion: ${f.completion.implementationDone ? chalk6.green("Implementation \u2705") : chalk6.gray("Implementation \u25EF")} / ${f.completion.workflowDone ? chalk6.green("Workflow \u2705") : chalk6.yellow("Workflow \u25EF")}`
|
|
2175
|
+
);
|
|
2050
2176
|
if (f.issueNumber) {
|
|
2051
2177
|
console.log(` \u2022 Issue: #${f.issueNumber}`);
|
|
2052
2178
|
}
|
|
@@ -2114,13 +2240,276 @@ function printChecklist(f, stepDefinitions) {
|
|
|
2114
2240
|
console.log(` ${mark} ${definition.step}. ${label} ${detail}`);
|
|
2115
2241
|
});
|
|
2116
2242
|
}
|
|
2243
|
+
function msg(lang, ko, en) {
|
|
2244
|
+
return lang === "en" ? en : ko;
|
|
2245
|
+
}
|
|
2246
|
+
function formatPath(cwd, p) {
|
|
2247
|
+
if (!p) return "";
|
|
2248
|
+
return path4.isAbsolute(p) ? path4.relative(cwd, p) : p;
|
|
2249
|
+
}
|
|
2250
|
+
function detectPlaceholders(content) {
|
|
2251
|
+
const patterns = [
|
|
2252
|
+
{ key: "{{projectName}}", re: /\{\{projectName\}\}/g },
|
|
2253
|
+
{ key: "{{date}}", re: /\{\{date\}\}/g },
|
|
2254
|
+
{ key: "{{featurePath}}", re: /\{\{featurePath\}\}/g },
|
|
2255
|
+
{ key: "{{description}}", re: /\{\{description\}\}/g },
|
|
2256
|
+
{ key: "{\uAE30\uB2A5\uBA85}", re: /\{기능명\}/g },
|
|
2257
|
+
{ key: "{\uBC88\uD638}", re: /\{번호\}/g },
|
|
2258
|
+
{ key: "{\uC774\uC288\uBC88\uD638}", re: /\{이슈번호\}/g },
|
|
2259
|
+
{ key: "{feature-name}", re: /\{feature-name\}/g },
|
|
2260
|
+
{ key: "{number}", re: /\{number\}/g },
|
|
2261
|
+
{ key: "{issue-number}", re: /\{issue-number\}/g },
|
|
2262
|
+
{ key: "{be|fe}", re: /\{be\|fe\}/g },
|
|
2263
|
+
{ key: "YYYY-MM-DD", re: /\bYYYY-MM-DD\b/g }
|
|
2264
|
+
];
|
|
2265
|
+
const hits = [];
|
|
2266
|
+
for (const { key, re } of patterns) {
|
|
2267
|
+
if (re.test(content)) hits.push(key);
|
|
2268
|
+
}
|
|
2269
|
+
return hits;
|
|
2270
|
+
}
|
|
2271
|
+
async function checkDocsStructure(config, cwd) {
|
|
2272
|
+
const issues = [];
|
|
2273
|
+
const requiredDirs = ["agents", "features", "prd", "designs", "ideas"];
|
|
2274
|
+
for (const dir of requiredDirs) {
|
|
2275
|
+
const p = path4.join(config.docsDir, dir);
|
|
2276
|
+
if (!await fs8.pathExists(p)) {
|
|
2277
|
+
issues.push({
|
|
2278
|
+
level: "error",
|
|
2279
|
+
code: "missing_dir",
|
|
2280
|
+
message: msg(
|
|
2281
|
+
config.lang,
|
|
2282
|
+
`\uD544\uC218 \uD3F4\uB354\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4: ${dir}`,
|
|
2283
|
+
`Missing required directory: ${dir}`
|
|
2284
|
+
),
|
|
2285
|
+
path: formatPath(cwd, p)
|
|
2286
|
+
});
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
const configPath = path4.join(config.docsDir, ".lee-spec-kit.json");
|
|
2290
|
+
if (!await fs8.pathExists(configPath)) {
|
|
2291
|
+
issues.push({
|
|
2292
|
+
level: "warn",
|
|
2293
|
+
code: "missing_config",
|
|
2294
|
+
message: msg(
|
|
2295
|
+
config.lang,
|
|
2296
|
+
"\uC124\uC815 \uD30C\uC77C(.lee-spec-kit.json)\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. \uC77C\uBD80 \uAE30\uB2A5\uC774 \uD3F4\uB354 \uAD6C\uC870 \uCD94\uC815\uC73C\uB85C \uB3D9\uC791\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.",
|
|
2297
|
+
"Missing .lee-spec-kit.json. Some commands may rely on folder-structure heuristics."
|
|
2298
|
+
),
|
|
2299
|
+
path: formatPath(cwd, configPath)
|
|
2300
|
+
});
|
|
2301
|
+
}
|
|
2302
|
+
return issues;
|
|
2303
|
+
}
|
|
2304
|
+
async function checkFeatures(config, cwd, features) {
|
|
2305
|
+
const issues = [];
|
|
2306
|
+
if (features.length === 0) {
|
|
2307
|
+
issues.push({
|
|
2308
|
+
level: "warn",
|
|
2309
|
+
code: "no_features",
|
|
2310
|
+
message: msg(
|
|
2311
|
+
config.lang,
|
|
2312
|
+
"Feature \uD3F4\uB354\uB97C \uCC3E\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4. (feature-base\uB9CC \uC874\uC7AC\uD558\uAC70\uB098 \uC544\uC9C1 feature\uB97C \uB9CC\uB4E4\uC9C0 \uC54A\uC558\uC744 \uC218 \uC788\uC2B5\uB2C8\uB2E4.)",
|
|
2313
|
+
"No feature folders found. (Only feature-base exists, or no features created yet.)"
|
|
2314
|
+
)
|
|
2315
|
+
});
|
|
2316
|
+
return issues;
|
|
2317
|
+
}
|
|
2318
|
+
const idMap = /* @__PURE__ */ new Map();
|
|
2319
|
+
for (const f of features) {
|
|
2320
|
+
const rel = f.docs.featurePathFromDocs || path4.relative(config.docsDir, f.path);
|
|
2321
|
+
const id = f.id || "UNKNOWN";
|
|
2322
|
+
if (!idMap.has(id)) idMap.set(id, []);
|
|
2323
|
+
idMap.get(id).push(rel);
|
|
2324
|
+
const featureDocs = ["spec.md", "plan.md", "tasks.md", "decisions.md"];
|
|
2325
|
+
for (const file of featureDocs) {
|
|
2326
|
+
const p = path4.join(f.path, file);
|
|
2327
|
+
if (!await fs8.pathExists(p)) continue;
|
|
2328
|
+
const content = await fs8.readFile(p, "utf-8");
|
|
2329
|
+
const placeholders = detectPlaceholders(content);
|
|
2330
|
+
if (placeholders.length === 0) continue;
|
|
2331
|
+
issues.push({
|
|
2332
|
+
level: "warn",
|
|
2333
|
+
code: "placeholder_left",
|
|
2334
|
+
message: msg(
|
|
2335
|
+
config.lang,
|
|
2336
|
+
`\uD50C\uB808\uC774\uC2A4\uD640\uB354\uAC00 \uB0A8\uC544\uC788\uC2B5\uB2C8\uB2E4: ${placeholders.join(", ")}`,
|
|
2337
|
+
`Leftover placeholders detected: ${placeholders.join(", ")}`
|
|
2338
|
+
),
|
|
2339
|
+
path: formatPath(cwd, p)
|
|
2340
|
+
});
|
|
2341
|
+
}
|
|
2342
|
+
if (!f.docs.specExists) {
|
|
2343
|
+
issues.push({
|
|
2344
|
+
level: "warn",
|
|
2345
|
+
code: "missing_spec",
|
|
2346
|
+
message: msg(
|
|
2347
|
+
config.lang,
|
|
2348
|
+
"spec.md\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
2349
|
+
"Missing spec.md."
|
|
2350
|
+
),
|
|
2351
|
+
path: formatPath(cwd, f.path)
|
|
2352
|
+
});
|
|
2353
|
+
} else if (!f.specStatus) {
|
|
2354
|
+
issues.push({
|
|
2355
|
+
level: "warn",
|
|
2356
|
+
code: "spec_status_unset",
|
|
2357
|
+
message: msg(
|
|
2358
|
+
config.lang,
|
|
2359
|
+
"spec.md\uC758 Status(\uC0C1\uD0DC)\uAC00 \uC124\uC815\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4. (\uD15C\uD50C\uB9BF \uADF8\uB300\uB85C\uC77C \uC218 \uC788\uC74C)",
|
|
2360
|
+
"spec.md Status is not set. (May still be a template)"
|
|
2361
|
+
),
|
|
2362
|
+
path: formatPath(cwd, path4.join(f.path, "spec.md"))
|
|
2363
|
+
});
|
|
2364
|
+
}
|
|
2365
|
+
if (f.docs.planExists && !f.planStatus) {
|
|
2366
|
+
issues.push({
|
|
2367
|
+
level: "warn",
|
|
2368
|
+
code: "plan_status_unset",
|
|
2369
|
+
message: msg(
|
|
2370
|
+
config.lang,
|
|
2371
|
+
"plan.md\uC758 Status(\uC0C1\uD0DC)\uAC00 \uC124\uC815\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4. (\uD15C\uD50C\uB9BF \uADF8\uB300\uB85C\uC77C \uC218 \uC788\uC74C)",
|
|
2372
|
+
"plan.md Status is not set. (May still be a template)"
|
|
2373
|
+
),
|
|
2374
|
+
path: formatPath(cwd, path4.join(f.path, "plan.md"))
|
|
2375
|
+
});
|
|
2376
|
+
}
|
|
2377
|
+
if (f.docs.tasksExists && f.tasks.total === 0) {
|
|
2378
|
+
issues.push({
|
|
2379
|
+
level: "warn",
|
|
2380
|
+
code: "tasks_empty",
|
|
2381
|
+
message: msg(
|
|
2382
|
+
config.lang,
|
|
2383
|
+
"tasks.md\uC5D0 \uD0DC\uC2A4\uD06C\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
2384
|
+
"tasks.md has no tasks."
|
|
2385
|
+
),
|
|
2386
|
+
path: formatPath(cwd, path4.join(f.path, "tasks.md"))
|
|
2387
|
+
});
|
|
2388
|
+
}
|
|
2389
|
+
}
|
|
2390
|
+
const duplicates = [...idMap.entries()].filter(
|
|
2391
|
+
([id, paths]) => id !== "UNKNOWN" && paths.length > 1
|
|
2392
|
+
);
|
|
2393
|
+
for (const [id, paths] of duplicates) {
|
|
2394
|
+
issues.push({
|
|
2395
|
+
level: "warn",
|
|
2396
|
+
code: "duplicate_feature_id",
|
|
2397
|
+
message: msg(
|
|
2398
|
+
config.lang,
|
|
2399
|
+
`\uC911\uBCF5 Feature ID \uAC10\uC9C0: ${id} (${paths.length}\uAC1C)`,
|
|
2400
|
+
`Duplicate Feature ID detected: ${id} (${paths.length})`
|
|
2401
|
+
),
|
|
2402
|
+
path: formatPath(cwd, paths[0])
|
|
2403
|
+
});
|
|
2404
|
+
}
|
|
2405
|
+
const unknowns = idMap.get("UNKNOWN") || [];
|
|
2406
|
+
for (const p of unknowns) {
|
|
2407
|
+
issues.push({
|
|
2408
|
+
level: "warn",
|
|
2409
|
+
code: "missing_feature_id",
|
|
2410
|
+
message: msg(
|
|
2411
|
+
config.lang,
|
|
2412
|
+
"Feature \uD3F4\uB354\uBA85\uC774 F001-... \uD615\uC2DD\uC774 \uC544\uB2D9\uB2C8\uB2E4. (ID\uB97C \uCD94\uCD9C\uD560 \uC218 \uC5C6\uC74C)",
|
|
2413
|
+
"Feature folder name is not in F001-... format. (Cannot extract ID)"
|
|
2414
|
+
),
|
|
2415
|
+
path: formatPath(cwd, path4.join(config.docsDir, p))
|
|
2416
|
+
});
|
|
2417
|
+
}
|
|
2418
|
+
return issues;
|
|
2419
|
+
}
|
|
2420
|
+
function doctorCommand(program2) {
|
|
2421
|
+
program2.command("doctor").description("Validate docs structure and feature metadata").option("--json", "Output in JSON format for agents").option("-s, --strict", "Exit with non-zero code when issues are found").action(async (options) => {
|
|
2422
|
+
const cwd = process.cwd();
|
|
2423
|
+
const config = await getConfig(cwd);
|
|
2424
|
+
if (!config) {
|
|
2425
|
+
const message = "\uC124\uC815 \uD30C\uC77C\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uBA3C\uC800 init\uC744 \uC2E4\uD589\uD574\uC8FC\uC138\uC694.";
|
|
2426
|
+
if (options.json) {
|
|
2427
|
+
console.log(JSON.stringify({ status: "error", error: message }, null, 2));
|
|
2428
|
+
} else {
|
|
2429
|
+
console.error(chalk6.red("\uC624\uB958:"), message);
|
|
2430
|
+
}
|
|
2431
|
+
process.exit(1);
|
|
2432
|
+
}
|
|
2433
|
+
const { docsDir, projectType, lang } = config;
|
|
2434
|
+
const { features, branches, warnings } = await scanFeatures(config);
|
|
2435
|
+
const issues = [];
|
|
2436
|
+
issues.push(...await checkDocsStructure({ docsDir, lang }, cwd));
|
|
2437
|
+
issues.push(...await checkFeatures({ docsDir, lang }, cwd, features));
|
|
2438
|
+
const hasIssues = issues.length > 0;
|
|
2439
|
+
const hasErrors = issues.some((i) => i.level === "error");
|
|
2440
|
+
const exitCode = options.strict && hasIssues ? 1 : 0;
|
|
2441
|
+
if (options.json) {
|
|
2442
|
+
console.log(
|
|
2443
|
+
JSON.stringify(
|
|
2444
|
+
{
|
|
2445
|
+
status: hasErrors ? "error" : hasIssues ? "warn" : "ok",
|
|
2446
|
+
meta: { docsDir, projectType, lang },
|
|
2447
|
+
branches,
|
|
2448
|
+
warnings,
|
|
2449
|
+
counts: {
|
|
2450
|
+
features: features.length,
|
|
2451
|
+
issues: issues.length,
|
|
2452
|
+
errors: issues.filter((i) => i.level === "error").length,
|
|
2453
|
+
warnings: issues.filter((i) => i.level === "warn").length
|
|
2454
|
+
},
|
|
2455
|
+
issues
|
|
2456
|
+
},
|
|
2457
|
+
null,
|
|
2458
|
+
2
|
|
2459
|
+
)
|
|
2460
|
+
);
|
|
2461
|
+
process.exit(exitCode);
|
|
2462
|
+
}
|
|
2463
|
+
console.log();
|
|
2464
|
+
console.log(chalk6.bold("\u{1F50E} Docs Doctor"));
|
|
2465
|
+
console.log(chalk6.gray(`- Docs: ${path4.relative(cwd, docsDir)}`));
|
|
2466
|
+
console.log(chalk6.gray(`- Type: ${projectType}`));
|
|
2467
|
+
console.log(chalk6.gray(`- Lang: ${lang}`));
|
|
2468
|
+
console.log();
|
|
2469
|
+
if (warnings.length > 0) {
|
|
2470
|
+
console.log(chalk6.yellow("\u26A0\uFE0F Environment warnings:"));
|
|
2471
|
+
warnings.forEach((w) => console.log(chalk6.yellow(` - ${w}`)));
|
|
2472
|
+
console.log();
|
|
2473
|
+
}
|
|
2474
|
+
if (!hasIssues) {
|
|
2475
|
+
console.log(chalk6.green("\u2705 \uBB38\uC81C\uB97C \uCC3E\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4."));
|
|
2476
|
+
console.log();
|
|
2477
|
+
process.exit(0);
|
|
2478
|
+
}
|
|
2479
|
+
const errors = issues.filter((i) => i.level === "error");
|
|
2480
|
+
const warns = issues.filter((i) => i.level === "warn");
|
|
2481
|
+
if (errors.length > 0) {
|
|
2482
|
+
console.log(chalk6.red(`\u274C Errors (${errors.length})`));
|
|
2483
|
+
errors.forEach(
|
|
2484
|
+
(i) => console.log(chalk6.red(` - ${i.message}${i.path ? ` (${i.path})` : ""}`))
|
|
2485
|
+
);
|
|
2486
|
+
console.log();
|
|
2487
|
+
}
|
|
2488
|
+
if (warns.length > 0) {
|
|
2489
|
+
console.log(chalk6.yellow(`\u26A0\uFE0F Warnings (${warns.length})`));
|
|
2490
|
+
warns.forEach(
|
|
2491
|
+
(i) => console.log(
|
|
2492
|
+
chalk6.yellow(` - ${i.message}${i.path ? ` (${i.path})` : ""}`)
|
|
2493
|
+
)
|
|
2494
|
+
);
|
|
2495
|
+
console.log();
|
|
2496
|
+
}
|
|
2497
|
+
console.log(
|
|
2498
|
+
chalk6.gray(
|
|
2499
|
+
`Tip: \uC5D0\uC774\uC804\uD2B8\uC6A9 JSON \uCD9C\uB825: npx lee-spec-kit doctor --json${options.strict ? " --strict" : ""}`
|
|
2500
|
+
)
|
|
2501
|
+
);
|
|
2502
|
+
console.log();
|
|
2503
|
+
process.exit(exitCode);
|
|
2504
|
+
});
|
|
2505
|
+
}
|
|
2117
2506
|
var CACHE_FILE = path4.join(os.homedir(), ".lee-spec-kit-version-cache.json");
|
|
2118
2507
|
var CHECK_INTERVAL = 24 * 60 * 60 * 1e3;
|
|
2119
2508
|
function getCurrentVersion() {
|
|
2120
2509
|
try {
|
|
2121
2510
|
const packageJsonPath = path4.join(__dirname$1, "..", "package.json");
|
|
2122
|
-
if (
|
|
2123
|
-
const pkg =
|
|
2511
|
+
if (fs8.existsSync(packageJsonPath)) {
|
|
2512
|
+
const pkg = fs8.readJsonSync(packageJsonPath);
|
|
2124
2513
|
return pkg.version;
|
|
2125
2514
|
}
|
|
2126
2515
|
} catch {
|
|
@@ -2129,8 +2518,8 @@ function getCurrentVersion() {
|
|
|
2129
2518
|
}
|
|
2130
2519
|
function readCache() {
|
|
2131
2520
|
try {
|
|
2132
|
-
if (
|
|
2133
|
-
return
|
|
2521
|
+
if (fs8.existsSync(CACHE_FILE)) {
|
|
2522
|
+
return fs8.readJsonSync(CACHE_FILE);
|
|
2134
2523
|
}
|
|
2135
2524
|
} catch {
|
|
2136
2525
|
}
|
|
@@ -2194,12 +2583,23 @@ function checkForUpdates() {
|
|
|
2194
2583
|
}
|
|
2195
2584
|
|
|
2196
2585
|
// src/index.ts
|
|
2197
|
-
|
|
2586
|
+
function shouldCheckForUpdates() {
|
|
2587
|
+
const argv = process.argv.slice(2);
|
|
2588
|
+
const hasJsonFlag = argv.includes("--json");
|
|
2589
|
+
const isHelpOrVersion = argv.includes("--help") || argv.includes("-h") || argv.includes("--version") || argv.includes("-V");
|
|
2590
|
+
const disabledByEnv = (process.env.LSK_NO_UPDATE_CHECK || "").trim() === "1" || (process.env.LEE_SPEC_KIT_NO_UPDATE_CHECK || "").trim() === "1";
|
|
2591
|
+
if (hasJsonFlag) return false;
|
|
2592
|
+
if (!process.stdout.isTTY) return false;
|
|
2593
|
+
if (isHelpOrVersion) return false;
|
|
2594
|
+
if (disabledByEnv) return false;
|
|
2595
|
+
return true;
|
|
2596
|
+
}
|
|
2597
|
+
if (shouldCheckForUpdates()) checkForUpdates();
|
|
2198
2598
|
function getCliVersion() {
|
|
2199
2599
|
try {
|
|
2200
2600
|
const packageJsonPath = path4.join(__dirname$1, "..", "package.json");
|
|
2201
|
-
if (
|
|
2202
|
-
const pkg =
|
|
2601
|
+
if (fs8.existsSync(packageJsonPath)) {
|
|
2602
|
+
const pkg = fs8.readJsonSync(packageJsonPath);
|
|
2203
2603
|
if (pkg?.version) return String(pkg.version);
|
|
2204
2604
|
}
|
|
2205
2605
|
} catch {
|
|
@@ -2215,4 +2615,5 @@ statusCommand(program);
|
|
|
2215
2615
|
updateCommand(program);
|
|
2216
2616
|
configCommand(program);
|
|
2217
2617
|
contextCommand(program);
|
|
2218
|
-
program
|
|
2618
|
+
doctorCommand(program);
|
|
2619
|
+
await program.parseAsync();
|