lee-spec-kit 0.4.2 → 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 +902 -503
- package/package.json +1 -1
- package/templates/en/common/scripts/README.md +11 -0
- package/templates/ko/common/scripts/README.md +11 -0
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,394 +669,20 @@ 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
|
-
const match = entry.name.match(/^F(\d+)-/);
|
|
651
|
-
if (match) {
|
|
652
|
-
const num = parseInt(match[1], 10);
|
|
653
|
-
if (num > max) max = num;
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
const next = max + 1;
|
|
658
|
-
const width = Math.max(3, String(next).length);
|
|
659
|
-
return `F${String(next).padStart(width, "0")}`;
|
|
660
|
-
}
|
|
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
|
-
);
|
|
676
|
+
const match = entry.name.match(/^F(\d+)-/);
|
|
677
|
+
if (match) {
|
|
678
|
+
const num = parseInt(match[1], 10);
|
|
679
|
+
if (num > max) max = num;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
1031
682
|
}
|
|
1032
|
-
|
|
1033
|
-
|
|
683
|
+
const next = max + 1;
|
|
684
|
+
const width = Math.max(3, String(next).length);
|
|
685
|
+
return `F${String(next).padStart(width, "0")}`;
|
|
1034
686
|
}
|
|
1035
687
|
|
|
1036
688
|
// src/utils/context/i18n.ts
|
|
@@ -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: {
|
|
@@ -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 [
|
|
@@ -1323,7 +990,7 @@ function getStepDefinitions(lang) {
|
|
|
1323
990
|
name: tr(lang, "steps", "branchCreate"),
|
|
1324
991
|
checklist: { done: (f) => f.git.onExpectedBranch },
|
|
1325
992
|
current: {
|
|
1326
|
-
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),
|
|
1327
994
|
actions: (f) => {
|
|
1328
995
|
if (!f.git.projectBranchAvailable || !f.git.projectGitCwd) {
|
|
1329
996
|
return [
|
|
@@ -1444,6 +1111,15 @@ function getStepDefinitions(lang) {
|
|
|
1444
1111
|
current: {
|
|
1445
1112
|
when: (f) => isPrMetadataConfigured(f) && !!f.pr.link && f.pr.status !== "Approved",
|
|
1446
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
|
+
}
|
|
1447
1123
|
if (f.pr.status === "Review") {
|
|
1448
1124
|
return [
|
|
1449
1125
|
{
|
|
@@ -1589,7 +1265,7 @@ function isExpectedFeatureBranch(branchName, issueNumber, slug, folderName) {
|
|
|
1589
1265
|
function escapeRegExp(value) {
|
|
1590
1266
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1591
1267
|
}
|
|
1592
|
-
function
|
|
1268
|
+
function extractSpecValue(content, key) {
|
|
1593
1269
|
const regex = new RegExp(
|
|
1594
1270
|
`^\\s*-\\s*\\*\\*${escapeRegExp(key)}\\*\\*\\s*:\\s*(.*)$`,
|
|
1595
1271
|
"m"
|
|
@@ -1599,7 +1275,7 @@ function extractSpecValue2(content, key) {
|
|
|
1599
1275
|
}
|
|
1600
1276
|
function extractFirstSpecValue(content, keys) {
|
|
1601
1277
|
for (const key of keys) {
|
|
1602
|
-
const value =
|
|
1278
|
+
const value = extractSpecValue(content, key);
|
|
1603
1279
|
if (value) return value;
|
|
1604
1280
|
}
|
|
1605
1281
|
return void 0;
|
|
@@ -1669,6 +1345,12 @@ function parseCompletionChecklist(content) {
|
|
|
1669
1345
|
}
|
|
1670
1346
|
return total > 0 ? { total, checked } : void 0;
|
|
1671
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
|
+
}
|
|
1672
1354
|
async function parseFeature(featurePath, type, context, options) {
|
|
1673
1355
|
const lang = options.lang;
|
|
1674
1356
|
const folderName = path4.basename(featurePath);
|
|
@@ -1680,22 +1362,22 @@ async function parseFeature(featurePath, type, context, options) {
|
|
|
1680
1362
|
const tasksPath = path4.join(featurePath, "tasks.md");
|
|
1681
1363
|
let specStatus;
|
|
1682
1364
|
let issueNumber;
|
|
1683
|
-
const specExists = await
|
|
1365
|
+
const specExists = await fs8.pathExists(specPath);
|
|
1684
1366
|
if (specExists) {
|
|
1685
|
-
const content = await
|
|
1367
|
+
const content = await fs8.readFile(specPath, "utf-8");
|
|
1686
1368
|
const statusValue = extractFirstSpecValue(content, ["\uC0C1\uD0DC", "Status"]);
|
|
1687
1369
|
specStatus = parseDocStatus(statusValue);
|
|
1688
1370
|
const issueValue = extractFirstSpecValue(content, ["\uC774\uC288 \uBC88\uD638", "Issue Number", "Issue"]);
|
|
1689
1371
|
issueNumber = parseIssueNumber(issueValue);
|
|
1690
1372
|
}
|
|
1691
1373
|
let planStatus;
|
|
1692
|
-
const planExists = await
|
|
1374
|
+
const planExists = await fs8.pathExists(planPath);
|
|
1693
1375
|
if (planExists) {
|
|
1694
|
-
const content = await
|
|
1376
|
+
const content = await fs8.readFile(planPath, "utf-8");
|
|
1695
1377
|
const statusValue = extractFirstSpecValue(content, ["\uC0C1\uD0DC", "Status"]);
|
|
1696
1378
|
planStatus = parseDocStatus(statusValue);
|
|
1697
1379
|
}
|
|
1698
|
-
const tasksExists = await
|
|
1380
|
+
const tasksExists = await fs8.pathExists(tasksPath);
|
|
1699
1381
|
const tasksSummary = { total: 0, todo: 0, doing: 0, done: 0 };
|
|
1700
1382
|
let activeTask;
|
|
1701
1383
|
let nextTodoTask;
|
|
@@ -1705,7 +1387,7 @@ async function parseFeature(featurePath, type, context, options) {
|
|
|
1705
1387
|
let prFieldExists = false;
|
|
1706
1388
|
let prStatusFieldExists = false;
|
|
1707
1389
|
if (tasksExists) {
|
|
1708
|
-
const content = await
|
|
1390
|
+
const content = await fs8.readFile(tasksPath, "utf-8");
|
|
1709
1391
|
const { summary, activeTask: active, nextTodoTask: nextTodo } = parseTasks(content);
|
|
1710
1392
|
tasksSummary.total = summary.total;
|
|
1711
1393
|
tasksSummary.todo = summary.todo;
|
|
@@ -1744,12 +1426,33 @@ async function parseFeature(featurePath, type, context, options) {
|
|
|
1744
1426
|
if (tasksExists && (!prFieldExists || !prStatusFieldExists)) {
|
|
1745
1427
|
warnings.push(tr(lang, "warnings", "legacyTasksPrFields"));
|
|
1746
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
|
+
}
|
|
1747
1446
|
const featureState = {
|
|
1748
1447
|
id,
|
|
1749
1448
|
slug,
|
|
1750
1449
|
folderName,
|
|
1751
1450
|
type,
|
|
1752
1451
|
path: featurePath,
|
|
1452
|
+
completion: {
|
|
1453
|
+
implementationDone,
|
|
1454
|
+
workflowDone
|
|
1455
|
+
},
|
|
1753
1456
|
issueNumber,
|
|
1754
1457
|
specStatus,
|
|
1755
1458
|
planStatus,
|
|
@@ -1815,7 +1518,7 @@ async function scanFeatures(config) {
|
|
|
1815
1518
|
ignore: ["**/feature-base/**"]
|
|
1816
1519
|
});
|
|
1817
1520
|
for (const dir of featureDirs) {
|
|
1818
|
-
if ((await
|
|
1521
|
+
if ((await fs8.stat(dir)).isDirectory()) {
|
|
1819
1522
|
features.push(
|
|
1820
1523
|
await parseFeature(
|
|
1821
1524
|
dir,
|
|
@@ -1832,62 +1535,421 @@ async function scanFeatures(config) {
|
|
|
1832
1535
|
)
|
|
1833
1536
|
);
|
|
1834
1537
|
}
|
|
1835
|
-
}
|
|
1836
|
-
} else {
|
|
1837
|
-
const feDirs = await glob("features/fe/*/", { cwd: config.docsDir, absolute: true });
|
|
1838
|
-
const beDirs = await glob("features/be/*/", { cwd: config.docsDir, absolute: true });
|
|
1839
|
-
for (const dir of feDirs) {
|
|
1840
|
-
if ((await
|
|
1841
|
-
features.push(
|
|
1842
|
-
await parseFeature(
|
|
1843
|
-
dir,
|
|
1844
|
-
"fe",
|
|
1845
|
-
{
|
|
1846
|
-
projectBranch: projectBranches.fe,
|
|
1847
|
-
docsBranch,
|
|
1848
|
-
docsGitCwd: config.docsDir,
|
|
1849
|
-
projectGitCwd: feProject?.cwd ?? void 0,
|
|
1850
|
-
docsDir: config.docsDir,
|
|
1851
|
-
projectBranchAvailable: Boolean(feProject?.cwd)
|
|
1852
|
-
},
|
|
1853
|
-
{ lang: config.lang, stepDefinitions }
|
|
1854
|
-
)
|
|
1855
|
-
);
|
|
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++;
|
|
1856
1841
|
}
|
|
1842
|
+
} else if (stat.isDirectory()) {
|
|
1843
|
+
const subCount = await updateFolder(
|
|
1844
|
+
sourcePath,
|
|
1845
|
+
targetPath,
|
|
1846
|
+
force,
|
|
1847
|
+
replacements
|
|
1848
|
+
);
|
|
1849
|
+
updatedCount += subCount;
|
|
1857
1850
|
}
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
docsDir: config.docsDir,
|
|
1870
|
-
projectBranchAvailable: Boolean(beProject?.cwd)
|
|
1871
|
-
},
|
|
1872
|
-
{ lang: config.lang, stepDefinitions }
|
|
1873
|
-
)
|
|
1874
|
-
);
|
|
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);
|
|
1875
1862
|
}
|
|
1863
|
+
console.error(chalk6.red("\uC624\uB958:"), error);
|
|
1864
|
+
process.exit(1);
|
|
1876
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);
|
|
1877
1876
|
}
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
}
|
|
1884
|
-
|
|
1885
|
-
|
|
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();
|
|
1886
1950
|
}
|
|
1887
|
-
|
|
1888
|
-
// src/commands/context.ts
|
|
1889
1951
|
function contextCommand(program2) {
|
|
1890
|
-
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(
|
|
1891
1953
|
async (featureName, options) => {
|
|
1892
1954
|
try {
|
|
1893
1955
|
await runContext(featureName, options);
|
|
@@ -1933,12 +1995,22 @@ async function runContext(featureName, options) {
|
|
|
1933
1995
|
const stepDefinitions = getStepDefinitions(lang);
|
|
1934
1996
|
const stepsMap = getStepsMap(lang);
|
|
1935
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
|
+
);
|
|
1936
2006
|
let targetFeatures = [];
|
|
2007
|
+
let selectionMode = "explicit";
|
|
1937
2008
|
if (featureName) {
|
|
1938
2009
|
targetFeatures = features.filter((f2) => matchesFeatureSelector(f2, featureName));
|
|
1939
2010
|
if (options.repo) {
|
|
1940
2011
|
targetFeatures = targetFeatures.filter((f2) => f2.type === options.repo);
|
|
1941
2012
|
}
|
|
2013
|
+
selectionMode = "explicit";
|
|
1942
2014
|
} else {
|
|
1943
2015
|
if (config.projectType === "single") {
|
|
1944
2016
|
const branchName = branches.project.single || "";
|
|
@@ -1960,15 +2032,33 @@ async function runContext(featureName, options) {
|
|
|
1960
2032
|
) : [];
|
|
1961
2033
|
targetFeatures = [...feMatches, ...beMatches];
|
|
1962
2034
|
}
|
|
1963
|
-
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
|
+
}
|
|
1964
2047
|
}
|
|
1965
2048
|
if (options.json) {
|
|
2049
|
+
const isNoOpen = selectionMode === "open" && features.length > 0 && openFeatures.length === 0;
|
|
1966
2050
|
const result = {
|
|
1967
|
-
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,
|
|
1968
2053
|
branches,
|
|
1969
2054
|
warnings,
|
|
1970
2055
|
matchedFeature: targetFeatures.length === 1 ? targetFeatures[0] : null,
|
|
1971
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 : [],
|
|
1972
2062
|
actions: targetFeatures.length === 1 ? targetFeatures[0].actions : [],
|
|
1973
2063
|
recommendation: ""
|
|
1974
2064
|
};
|
|
@@ -2024,22 +2114,53 @@ async function runContext(featureName, options) {
|
|
|
2024
2114
|
console.log();
|
|
2025
2115
|
}
|
|
2026
2116
|
if (targetFeatures.length > 1) {
|
|
2027
|
-
|
|
2028
|
-
chalk6.blue(`\u{1F539} ${targetFeatures.length} Active Features Detected:`)
|
|
2029
|
-
);
|
|
2030
|
-
console.log();
|
|
2031
|
-
targetFeatures.forEach((f2) => {
|
|
2032
|
-
const stepName2 = stepsMap[f2.currentStep] || "Unknown";
|
|
2033
|
-
const typeStr = config.projectType === "fullstack" ? chalk6.cyan(`(${f2.type})`) : "";
|
|
2117
|
+
if (selectionMode === "open") {
|
|
2034
2118
|
console.log(
|
|
2035
|
-
|
|
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
|
+
)
|
|
2036
2122
|
);
|
|
2037
|
-
|
|
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
|
+
}
|
|
2038
2155
|
console.log();
|
|
2039
2156
|
console.log(chalk6.gray("Tip: \uD2B9\uC815 Feature\uC758 \uC0C1\uC138 \uC815\uBCF4\uB97C \uBCF4\uB824\uBA74:"));
|
|
2040
2157
|
console.log(
|
|
2041
2158
|
chalk6.gray(" $ npx lee-spec-kit context <slug|F001|F001-slug> [--repo fe|be]")
|
|
2042
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
|
+
}
|
|
2043
2164
|
console.log();
|
|
2044
2165
|
return;
|
|
2045
2166
|
}
|
|
@@ -2049,6 +2170,9 @@ async function runContext(featureName, options) {
|
|
|
2049
2170
|
console.log(
|
|
2050
2171
|
`\u{1F539} Feature: ${chalk6.bold(f.folderName)} ${config.projectType === "fullstack" ? chalk6.cyan(`(${f.type})`) : ""}`
|
|
2051
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
|
+
);
|
|
2052
2176
|
if (f.issueNumber) {
|
|
2053
2177
|
console.log(` \u2022 Issue: #${f.issueNumber}`);
|
|
2054
2178
|
}
|
|
@@ -2116,13 +2240,276 @@ function printChecklist(f, stepDefinitions) {
|
|
|
2116
2240
|
console.log(` ${mark} ${definition.step}. ${label} ${detail}`);
|
|
2117
2241
|
});
|
|
2118
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
|
+
}
|
|
2119
2506
|
var CACHE_FILE = path4.join(os.homedir(), ".lee-spec-kit-version-cache.json");
|
|
2120
2507
|
var CHECK_INTERVAL = 24 * 60 * 60 * 1e3;
|
|
2121
2508
|
function getCurrentVersion() {
|
|
2122
2509
|
try {
|
|
2123
2510
|
const packageJsonPath = path4.join(__dirname$1, "..", "package.json");
|
|
2124
|
-
if (
|
|
2125
|
-
const pkg =
|
|
2511
|
+
if (fs8.existsSync(packageJsonPath)) {
|
|
2512
|
+
const pkg = fs8.readJsonSync(packageJsonPath);
|
|
2126
2513
|
return pkg.version;
|
|
2127
2514
|
}
|
|
2128
2515
|
} catch {
|
|
@@ -2131,8 +2518,8 @@ function getCurrentVersion() {
|
|
|
2131
2518
|
}
|
|
2132
2519
|
function readCache() {
|
|
2133
2520
|
try {
|
|
2134
|
-
if (
|
|
2135
|
-
return
|
|
2521
|
+
if (fs8.existsSync(CACHE_FILE)) {
|
|
2522
|
+
return fs8.readJsonSync(CACHE_FILE);
|
|
2136
2523
|
}
|
|
2137
2524
|
} catch {
|
|
2138
2525
|
}
|
|
@@ -2196,12 +2583,23 @@ function checkForUpdates() {
|
|
|
2196
2583
|
}
|
|
2197
2584
|
|
|
2198
2585
|
// src/index.ts
|
|
2199
|
-
|
|
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();
|
|
2200
2598
|
function getCliVersion() {
|
|
2201
2599
|
try {
|
|
2202
2600
|
const packageJsonPath = path4.join(__dirname$1, "..", "package.json");
|
|
2203
|
-
if (
|
|
2204
|
-
const pkg =
|
|
2601
|
+
if (fs8.existsSync(packageJsonPath)) {
|
|
2602
|
+
const pkg = fs8.readJsonSync(packageJsonPath);
|
|
2205
2603
|
if (pkg?.version) return String(pkg.version);
|
|
2206
2604
|
}
|
|
2207
2605
|
} catch {
|
|
@@ -2217,4 +2615,5 @@ statusCommand(program);
|
|
|
2217
2615
|
updateCommand(program);
|
|
2218
2616
|
configCommand(program);
|
|
2219
2617
|
contextCommand(program);
|
|
2220
|
-
program
|
|
2618
|
+
doctorCommand(program);
|
|
2619
|
+
await program.parseAsync();
|