lee-spec-kit 0.6.4 → 0.6.6
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/dist/index.js +560 -321
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import
|
|
2
|
+
import path18 from 'path';
|
|
3
3
|
import { fileURLToPath } from 'url';
|
|
4
4
|
import { program } from 'commander';
|
|
5
|
-
import
|
|
5
|
+
import fs14 from 'fs-extra';
|
|
6
6
|
import prompts from 'prompts';
|
|
7
7
|
import chalk6 from 'chalk';
|
|
8
8
|
import { glob } from 'glob';
|
|
9
9
|
import { spawn, execSync, spawnSync, execFileSync } from 'child_process';
|
|
10
|
-
import { createHash } from 'crypto';
|
|
11
10
|
import os from 'os';
|
|
11
|
+
import { createHash } from 'crypto';
|
|
12
12
|
|
|
13
13
|
var getFilename = () => fileURLToPath(import.meta.url);
|
|
14
|
-
var getDirname = () =>
|
|
14
|
+
var getDirname = () => path18.dirname(getFilename());
|
|
15
15
|
var __dirname$1 = /* @__PURE__ */ getDirname();
|
|
16
16
|
async function copyTemplates(src, dest) {
|
|
17
|
-
await
|
|
17
|
+
await fs14.copy(src, dest, {
|
|
18
18
|
overwrite: true,
|
|
19
19
|
errorOnExist: false
|
|
20
20
|
});
|
|
@@ -30,22 +30,22 @@ function applyReplacements(content, replacements) {
|
|
|
30
30
|
async function replaceInFiles(dir, replacements) {
|
|
31
31
|
const files = await glob("**/*.md", { cwd: dir, absolute: true });
|
|
32
32
|
for (const file of files) {
|
|
33
|
-
let content = await
|
|
33
|
+
let content = await fs14.readFile(file, "utf-8");
|
|
34
34
|
content = applyReplacements(content, replacements);
|
|
35
|
-
await
|
|
35
|
+
await fs14.writeFile(file, content, "utf-8");
|
|
36
36
|
}
|
|
37
37
|
const shFiles = await glob("**/*.sh", { cwd: dir, absolute: true });
|
|
38
38
|
for (const file of shFiles) {
|
|
39
|
-
let content = await
|
|
39
|
+
let content = await fs14.readFile(file, "utf-8");
|
|
40
40
|
content = applyReplacements(content, replacements);
|
|
41
|
-
await
|
|
41
|
+
await fs14.writeFile(file, content, "utf-8");
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
var __filename2 = fileURLToPath(import.meta.url);
|
|
45
|
-
var __dirname2 =
|
|
45
|
+
var __dirname2 = path18.dirname(__filename2);
|
|
46
46
|
function getTemplatesDir() {
|
|
47
|
-
const rootDir =
|
|
48
|
-
return
|
|
47
|
+
const rootDir = path18.resolve(__dirname2, "..");
|
|
48
|
+
return path18.join(rootDir, "templates");
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
// src/utils/i18n.ts
|
|
@@ -396,8 +396,8 @@ var I18N = {
|
|
|
396
396
|
issueCreateAndWrite: "`npx lee-spec-kit docs get create-issue --json`\uC73C\uB85C \uC808\uCC28\uB97C \uD655\uC778\uD55C \uB4A4, `npx lee-spec-kit github issue {featureRef} --json`\uC73C\uB85C \uCD08\uC548\uC744 \uC0DD\uC131\uD558\uC138\uC694. \uBAA9\uD45C/\uC644\uB8CC \uAE30\uC900\uC744 \uAC80\uD1A0\xB7\uBCF4\uC644\uD558\uACE0 \uC0AC\uC6A9\uC790 \uC2B9\uC778(OK) \uD6C4 `--create --confirm OK`\uB85C \uC0DD\uC131\uD55C \uB2E4\uC74C, spec.md/tasks.md\uC758 \uC774\uC288 \uBC88\uD638\uB97C \uCC44\uC6B0\uACE0 \uBB38\uC11C \uCEE4\uBC0B\uC744 \uC900\uBE44\uD558\uC138\uC694.",
|
|
397
397
|
docsCommitIssueUpdate: 'cd "{docsGitCwd}" && git add "{featurePath}" && git commit -m "docs(#{issueNumber}): {folderName} \uBB38\uC11C \uC5C5\uB370\uC774\uD2B8"',
|
|
398
398
|
docsCommitUpdate: 'cd "{docsGitCwd}" && git add "{featurePath}" && git commit -m "docs: {folderName} \uBB38\uC11C \uC5C5\uB370\uC774\uD2B8"',
|
|
399
|
-
projectCommitIssueUpdate: 'cd "{projectGitCwd}" && git add
|
|
400
|
-
projectCommitUpdate: 'cd "{projectGitCwd}" && git add
|
|
399
|
+
projectCommitIssueUpdate: 'cd "{projectGitCwd}" && (git diff --cached --quiet && echo "\uC2A4\uD14C\uC774\uC9D5\uB41C \uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. \uC774\uBC88 \uD0DC\uC2A4\uD06C\uC5D0\uC11C \uC218\uC815\uD55C \uD30C\uC77C\uB9CC \uC120\uD0DD\uD574 git add [files] \uD6C4 \uB2E4\uC2DC \uC2E4\uD589\uD558\uC138\uC694." && exit 1 || git commit -m "feat(#{issueNumber}): {commitTopic}")',
|
|
400
|
+
projectCommitUpdate: 'cd "{projectGitCwd}" && (git diff --cached --quiet && echo "\uC2A4\uD14C\uC774\uC9D5\uB41C \uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. \uC774\uBC88 \uD0DC\uC2A4\uD06C\uC5D0\uC11C \uC218\uC815\uD55C \uD30C\uC77C\uB9CC \uC120\uD0DD\uD574 git add [files] \uD6C4 \uB2E4\uC2DC \uC2E4\uD589\uD558\uC138\uC694." && exit 1 || git commit -m "feat({folderName}): {commitTopic}")',
|
|
401
401
|
standaloneNeedsProjectRoot: "standalone \uBAA8\uB4DC\uC5D0\uC11C\uB294 projectRoot \uC124\uC815\uC774 \uD544\uC694\uD569\uB2C8\uB2E4. (npx lee-spec-kit config --project-root ...)",
|
|
402
402
|
createBranch: 'cd "{projectGitCwd}" && git checkout -b feat/{issueNumber}-{slug}',
|
|
403
403
|
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.',
|
|
@@ -772,8 +772,8 @@ var I18N = {
|
|
|
772
772
|
issueCreateAndWrite: "Review procedure with `npx lee-spec-kit docs get create-issue --json`, then generate a draft via `npx lee-spec-kit github issue {featureRef} --json`. Refine goals/completion criteria, get explicit user OK, run `--create --confirm OK`, then update issue number in spec.md/tasks.md and prepare a docs commit.",
|
|
773
773
|
docsCommitIssueUpdate: 'cd "{docsGitCwd}" && git add "{featurePath}" && git commit -m "docs(#{issueNumber}): {folderName} docs update"',
|
|
774
774
|
docsCommitUpdate: 'cd "{docsGitCwd}" && git add "{featurePath}" && git commit -m "docs: {folderName} docs update"',
|
|
775
|
-
projectCommitIssueUpdate: 'cd "{projectGitCwd}" && git add
|
|
776
|
-
projectCommitUpdate: 'cd "{projectGitCwd}" && git add
|
|
775
|
+
projectCommitIssueUpdate: 'cd "{projectGitCwd}" && (git diff --cached --quiet && echo "No staged files. Stage only files changed in this task with git add [files], then run again." && exit 1 || git commit -m "feat(#{issueNumber}): {commitTopic}")',
|
|
776
|
+
projectCommitUpdate: 'cd "{projectGitCwd}" && (git diff --cached --quiet && echo "No staged files. Stage only files changed in this task with git add [files], then run again." && exit 1 || git commit -m "feat({folderName}): {commitTopic}")',
|
|
777
777
|
standaloneNeedsProjectRoot: "Standalone mode requires projectRoot. (npx lee-spec-kit config --project-root ...)",
|
|
778
778
|
createBranch: 'cd "{projectGitCwd}" && git checkout -b feat/{issueNumber}-{slug}',
|
|
779
779
|
tasksAllDoneButNoChecklist: 'All tasks are DONE, but no completion checklist section was found. Add/verify the "Completion Criteria" section in tasks.md.',
|
|
@@ -1203,17 +1203,38 @@ function sleep(ms) {
|
|
|
1203
1203
|
return new Promise((resolve) => globalThis.setTimeout(resolve, ms));
|
|
1204
1204
|
}
|
|
1205
1205
|
function getDocsLockPath(docsDir) {
|
|
1206
|
-
return
|
|
1206
|
+
return path18.join(docsDir, ".lee-spec-kit.lock");
|
|
1207
1207
|
}
|
|
1208
1208
|
function getInitLockPath(targetDir) {
|
|
1209
|
-
return
|
|
1210
|
-
|
|
1211
|
-
`.lee-spec-kit.${
|
|
1209
|
+
return path18.join(
|
|
1210
|
+
path18.dirname(targetDir),
|
|
1211
|
+
`.lee-spec-kit.${path18.basename(targetDir)}.lock`
|
|
1212
1212
|
);
|
|
1213
1213
|
}
|
|
1214
|
+
function getTempProjectLockPath(cwd) {
|
|
1215
|
+
const key = createHash("sha1").update(path18.resolve(cwd)).digest("hex");
|
|
1216
|
+
return path18.join(os.tmpdir(), "lee-spec-kit-locks", `${key}.project.lock`);
|
|
1217
|
+
}
|
|
1218
|
+
function getProjectExecutionLockPath(cwd) {
|
|
1219
|
+
try {
|
|
1220
|
+
const out = execFileSync(
|
|
1221
|
+
"git",
|
|
1222
|
+
["rev-parse", "--git-path", "lee-spec-kit.project.lock"],
|
|
1223
|
+
{
|
|
1224
|
+
cwd,
|
|
1225
|
+
encoding: "utf-8",
|
|
1226
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
1227
|
+
}
|
|
1228
|
+
).trim();
|
|
1229
|
+
if (!out) return getTempProjectLockPath(cwd);
|
|
1230
|
+
return path18.isAbsolute(out) ? out : path18.resolve(cwd, out);
|
|
1231
|
+
} catch {
|
|
1232
|
+
return getTempProjectLockPath(cwd);
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1214
1235
|
async function isStaleLock(lockPath, staleMs) {
|
|
1215
1236
|
try {
|
|
1216
|
-
const stat = await
|
|
1237
|
+
const stat = await fs14.stat(lockPath);
|
|
1217
1238
|
if (Date.now() - stat.mtimeMs <= staleMs) {
|
|
1218
1239
|
return false;
|
|
1219
1240
|
}
|
|
@@ -1228,7 +1249,7 @@ async function isStaleLock(lockPath, staleMs) {
|
|
|
1228
1249
|
}
|
|
1229
1250
|
async function readLockPayload(lockPath) {
|
|
1230
1251
|
try {
|
|
1231
|
-
const raw = await
|
|
1252
|
+
const raw = await fs14.readFile(lockPath, "utf8");
|
|
1232
1253
|
const parsed = JSON.parse(raw);
|
|
1233
1254
|
if (!parsed || typeof parsed !== "object") return null;
|
|
1234
1255
|
return parsed;
|
|
@@ -1250,17 +1271,17 @@ function isProcessAlive(pid) {
|
|
|
1250
1271
|
}
|
|
1251
1272
|
}
|
|
1252
1273
|
async function tryAcquire(lockPath, owner) {
|
|
1253
|
-
await
|
|
1274
|
+
await fs14.ensureDir(path18.dirname(lockPath));
|
|
1254
1275
|
try {
|
|
1255
|
-
const fd = await
|
|
1276
|
+
const fd = await fs14.open(lockPath, "wx");
|
|
1256
1277
|
const payload = JSON.stringify(
|
|
1257
1278
|
{ pid: process.pid, owner: owner ?? "unknown", createdAt: (/* @__PURE__ */ new Date()).toISOString() },
|
|
1258
1279
|
null,
|
|
1259
1280
|
2
|
|
1260
1281
|
);
|
|
1261
|
-
await
|
|
1282
|
+
await fs14.writeFile(fd, `${payload}
|
|
1262
1283
|
`, { encoding: "utf8" });
|
|
1263
|
-
await
|
|
1284
|
+
await fs14.close(fd);
|
|
1264
1285
|
return true;
|
|
1265
1286
|
} catch (error) {
|
|
1266
1287
|
if (error.code === "EEXIST") {
|
|
@@ -1274,9 +1295,9 @@ async function waitForLockRelease(lockPath, options = {}) {
|
|
|
1274
1295
|
const pollMs = options.pollMs ?? DEFAULT_POLL_MS;
|
|
1275
1296
|
const staleMs = options.staleMs ?? DEFAULT_STALE_MS;
|
|
1276
1297
|
const startedAt = Date.now();
|
|
1277
|
-
while (await
|
|
1298
|
+
while (await fs14.pathExists(lockPath)) {
|
|
1278
1299
|
if (await isStaleLock(lockPath, staleMs)) {
|
|
1279
|
-
await
|
|
1300
|
+
await fs14.remove(lockPath);
|
|
1280
1301
|
break;
|
|
1281
1302
|
}
|
|
1282
1303
|
if (Date.now() - startedAt > timeoutMs) {
|
|
@@ -1294,7 +1315,7 @@ async function withFileLock(lockPath, task, options = {}) {
|
|
|
1294
1315
|
const acquired = await tryAcquire(lockPath, options.owner);
|
|
1295
1316
|
if (acquired) break;
|
|
1296
1317
|
if (await isStaleLock(lockPath, staleMs)) {
|
|
1297
|
-
await
|
|
1318
|
+
await fs14.remove(lockPath);
|
|
1298
1319
|
continue;
|
|
1299
1320
|
}
|
|
1300
1321
|
if (Date.now() - startedAt > timeoutMs) {
|
|
@@ -1308,7 +1329,7 @@ async function withFileLock(lockPath, task, options = {}) {
|
|
|
1308
1329
|
try {
|
|
1309
1330
|
return await task();
|
|
1310
1331
|
} finally {
|
|
1311
|
-
await
|
|
1332
|
+
await fs14.remove(lockPath).catch(() => {
|
|
1312
1333
|
});
|
|
1313
1334
|
}
|
|
1314
1335
|
}
|
|
@@ -1327,30 +1348,30 @@ var ENGINE_MANAGED_AGENT_FILES = [
|
|
|
1327
1348
|
"pr-template.md"
|
|
1328
1349
|
];
|
|
1329
1350
|
var ENGINE_MANAGED_AGENT_DIRS = ["skills"];
|
|
1330
|
-
var ENGINE_MANAGED_FEATURE_PATH =
|
|
1351
|
+
var ENGINE_MANAGED_FEATURE_PATH = path18.join(
|
|
1331
1352
|
"features",
|
|
1332
1353
|
"feature-base"
|
|
1333
1354
|
);
|
|
1334
1355
|
async function pruneEngineManagedDocs(docsDir) {
|
|
1335
1356
|
const removed = [];
|
|
1336
1357
|
for (const file of ENGINE_MANAGED_AGENT_FILES) {
|
|
1337
|
-
const target =
|
|
1338
|
-
if (await
|
|
1339
|
-
await
|
|
1340
|
-
removed.push(
|
|
1358
|
+
const target = path18.join(docsDir, "agents", file);
|
|
1359
|
+
if (await fs14.pathExists(target)) {
|
|
1360
|
+
await fs14.remove(target);
|
|
1361
|
+
removed.push(path18.relative(docsDir, target));
|
|
1341
1362
|
}
|
|
1342
1363
|
}
|
|
1343
1364
|
for (const dir of ENGINE_MANAGED_AGENT_DIRS) {
|
|
1344
|
-
const target =
|
|
1345
|
-
if (await
|
|
1346
|
-
await
|
|
1347
|
-
removed.push(
|
|
1365
|
+
const target = path18.join(docsDir, "agents", dir);
|
|
1366
|
+
if (await fs14.pathExists(target)) {
|
|
1367
|
+
await fs14.remove(target);
|
|
1368
|
+
removed.push(path18.relative(docsDir, target));
|
|
1348
1369
|
}
|
|
1349
1370
|
}
|
|
1350
|
-
const featureBasePath =
|
|
1351
|
-
if (await
|
|
1352
|
-
await
|
|
1353
|
-
removed.push(
|
|
1371
|
+
const featureBasePath = path18.join(docsDir, ENGINE_MANAGED_FEATURE_PATH);
|
|
1372
|
+
if (await fs14.pathExists(featureBasePath)) {
|
|
1373
|
+
await fs14.remove(featureBasePath);
|
|
1374
|
+
removed.push(path18.relative(docsDir, featureBasePath));
|
|
1354
1375
|
}
|
|
1355
1376
|
return removed;
|
|
1356
1377
|
}
|
|
@@ -1403,7 +1424,7 @@ ${tr(lang2, "cli", "common.canceled")}`)
|
|
|
1403
1424
|
}
|
|
1404
1425
|
async function runInit(options) {
|
|
1405
1426
|
const cwd = process.cwd();
|
|
1406
|
-
const defaultName =
|
|
1427
|
+
const defaultName = path18.basename(cwd);
|
|
1407
1428
|
let projectName = options.name || defaultName;
|
|
1408
1429
|
let projectType = options.type;
|
|
1409
1430
|
let components = parseComponentsOption(options.components);
|
|
@@ -1413,7 +1434,7 @@ async function runInit(options) {
|
|
|
1413
1434
|
let pushDocs = typeof options.pushDocs === "boolean" ? options.pushDocs : void 0;
|
|
1414
1435
|
let docsRemote = options.docsRemote;
|
|
1415
1436
|
let projectRoot;
|
|
1416
|
-
const targetDir =
|
|
1437
|
+
const targetDir = path18.resolve(cwd, options.dir || "./docs");
|
|
1417
1438
|
const skipPrompts = !!options.yes || !!options.nonInteractive;
|
|
1418
1439
|
if (options.docsRepo && !["embedded", "standalone"].includes(options.docsRepo)) {
|
|
1419
1440
|
throw createCliError(
|
|
@@ -1731,8 +1752,8 @@ async function runInit(options) {
|
|
|
1731
1752
|
await withFileLock(
|
|
1732
1753
|
initLockPath,
|
|
1733
1754
|
async () => {
|
|
1734
|
-
if (await
|
|
1735
|
-
const files = await
|
|
1755
|
+
if (await fs14.pathExists(targetDir)) {
|
|
1756
|
+
const files = await fs14.readdir(targetDir);
|
|
1736
1757
|
if (files.length > 0) {
|
|
1737
1758
|
if (options.force) {
|
|
1738
1759
|
} else if (options.nonInteractive) {
|
|
@@ -1768,28 +1789,28 @@ async function runInit(options) {
|
|
|
1768
1789
|
);
|
|
1769
1790
|
console.log();
|
|
1770
1791
|
const templatesDir = getTemplatesDir();
|
|
1771
|
-
const commonPath =
|
|
1792
|
+
const commonPath = path18.join(templatesDir, lang, "common");
|
|
1772
1793
|
const templateProjectType = toTemplateProjectType(projectType);
|
|
1773
|
-
const typePath =
|
|
1774
|
-
if (await
|
|
1794
|
+
const typePath = path18.join(templatesDir, lang, templateProjectType);
|
|
1795
|
+
if (await fs14.pathExists(commonPath)) {
|
|
1775
1796
|
await copyTemplates(commonPath, targetDir);
|
|
1776
1797
|
}
|
|
1777
|
-
if (!await
|
|
1798
|
+
if (!await fs14.pathExists(typePath)) {
|
|
1778
1799
|
throw new Error(
|
|
1779
1800
|
tr(lang, "cli", "init.error.templateNotFound", { path: typePath })
|
|
1780
1801
|
);
|
|
1781
1802
|
}
|
|
1782
1803
|
await copyTemplates(typePath, targetDir);
|
|
1783
1804
|
if (projectType === "multi" && !isDefaultFullstackComponents(components)) {
|
|
1784
|
-
const featuresRoot =
|
|
1785
|
-
await
|
|
1786
|
-
await
|
|
1805
|
+
const featuresRoot = path18.join(targetDir, "features");
|
|
1806
|
+
await fs14.remove(path18.join(featuresRoot, "fe"));
|
|
1807
|
+
await fs14.remove(path18.join(featuresRoot, "be"));
|
|
1787
1808
|
for (const component of components) {
|
|
1788
|
-
const componentDir =
|
|
1789
|
-
await
|
|
1790
|
-
const readmePath =
|
|
1791
|
-
if (!await
|
|
1792
|
-
await
|
|
1809
|
+
const componentDir = path18.join(featuresRoot, component);
|
|
1810
|
+
await fs14.ensureDir(componentDir);
|
|
1811
|
+
const readmePath = path18.join(componentDir, "README.md");
|
|
1812
|
+
if (!await fs14.pathExists(readmePath)) {
|
|
1813
|
+
await fs14.writeFile(
|
|
1793
1814
|
readmePath,
|
|
1794
1815
|
`# ${component.toUpperCase()} Features
|
|
1795
1816
|
|
|
@@ -1838,8 +1859,8 @@ Store ${component} feature specs here.
|
|
|
1838
1859
|
config.projectRoot = projectRoot;
|
|
1839
1860
|
}
|
|
1840
1861
|
}
|
|
1841
|
-
const configPath =
|
|
1842
|
-
await
|
|
1862
|
+
const configPath = path18.join(targetDir, ".lee-spec-kit.json");
|
|
1863
|
+
await fs14.writeJson(configPath, config, { spaces: 2 });
|
|
1843
1864
|
console.log(chalk6.green(tr(lang, "cli", "init.log.docsCreated")));
|
|
1844
1865
|
console.log();
|
|
1845
1866
|
await initGit(cwd, targetDir, docsRepo, lang, pushDocs, docsRemote);
|
|
@@ -1907,7 +1928,7 @@ async function initGit(cwd, targetDir, docsRepo, lang, pushDocs, docsRemote) {
|
|
|
1907
1928
|
console.log(chalk6.blue(tr(lang, "cli", "init.log.gitInit")));
|
|
1908
1929
|
runGit(["init"], cwd);
|
|
1909
1930
|
}
|
|
1910
|
-
const relativePath =
|
|
1931
|
+
const relativePath = path18.relative(cwd, targetDir);
|
|
1911
1932
|
const stagedBeforeAdd = getCachedStagedFiles(cwd);
|
|
1912
1933
|
if (relativePath === "." && stagedBeforeAdd && stagedBeforeAdd.length > 0) {
|
|
1913
1934
|
console.log(
|
|
@@ -1964,17 +1985,17 @@ async function initGit(cwd, targetDir, docsRepo, lang, pushDocs, docsRemote) {
|
|
|
1964
1985
|
}
|
|
1965
1986
|
function getAncestorDirs(startDir) {
|
|
1966
1987
|
const dirs = [];
|
|
1967
|
-
let current =
|
|
1988
|
+
let current = path18.resolve(startDir);
|
|
1968
1989
|
while (true) {
|
|
1969
1990
|
dirs.push(current);
|
|
1970
|
-
const parent =
|
|
1991
|
+
const parent = path18.dirname(current);
|
|
1971
1992
|
if (parent === current) break;
|
|
1972
1993
|
current = parent;
|
|
1973
1994
|
}
|
|
1974
1995
|
return dirs;
|
|
1975
1996
|
}
|
|
1976
1997
|
function hasWorkspaceBoundary(dir) {
|
|
1977
|
-
return
|
|
1998
|
+
return fs14.existsSync(path18.join(dir, "package.json")) || fs14.existsSync(path18.join(dir, ".git"));
|
|
1978
1999
|
}
|
|
1979
2000
|
function getSearchBaseDirs(cwd) {
|
|
1980
2001
|
const ancestors = getAncestorDirs(cwd);
|
|
@@ -1987,24 +2008,24 @@ function getSearchBaseDirs(cwd) {
|
|
|
1987
2008
|
async function getConfig(cwd) {
|
|
1988
2009
|
const explicitDocsDir = (process.env.LEE_SPEC_KIT_DOCS_DIR || "").trim();
|
|
1989
2010
|
const baseDirs = [
|
|
1990
|
-
...explicitDocsDir ? [
|
|
2011
|
+
...explicitDocsDir ? [path18.resolve(explicitDocsDir)] : [],
|
|
1991
2012
|
...getSearchBaseDirs(cwd)
|
|
1992
2013
|
];
|
|
1993
2014
|
const visitedBaseDirs = /* @__PURE__ */ new Set();
|
|
1994
2015
|
const visitedDocsDirs = /* @__PURE__ */ new Set();
|
|
1995
2016
|
for (const baseDir of baseDirs) {
|
|
1996
|
-
const resolvedBaseDir =
|
|
2017
|
+
const resolvedBaseDir = path18.resolve(baseDir);
|
|
1997
2018
|
if (visitedBaseDirs.has(resolvedBaseDir)) continue;
|
|
1998
2019
|
visitedBaseDirs.add(resolvedBaseDir);
|
|
1999
|
-
const possibleDocsDirs = [
|
|
2020
|
+
const possibleDocsDirs = [path18.join(resolvedBaseDir, "docs"), resolvedBaseDir];
|
|
2000
2021
|
for (const docsDir of possibleDocsDirs) {
|
|
2001
|
-
const resolvedDocsDir =
|
|
2022
|
+
const resolvedDocsDir = path18.resolve(docsDir);
|
|
2002
2023
|
if (visitedDocsDirs.has(resolvedDocsDir)) continue;
|
|
2003
2024
|
visitedDocsDirs.add(resolvedDocsDir);
|
|
2004
|
-
const configPath =
|
|
2005
|
-
if (await
|
|
2025
|
+
const configPath = path18.join(resolvedDocsDir, ".lee-spec-kit.json");
|
|
2026
|
+
if (await fs14.pathExists(configPath)) {
|
|
2006
2027
|
try {
|
|
2007
|
-
const configFile = await
|
|
2028
|
+
const configFile = await fs14.readJson(configPath);
|
|
2008
2029
|
const projectType = normalizeProjectType(configFile.projectType);
|
|
2009
2030
|
const components = resolveProjectComponents(
|
|
2010
2031
|
projectType,
|
|
@@ -2027,22 +2048,22 @@ async function getConfig(cwd) {
|
|
|
2027
2048
|
} catch {
|
|
2028
2049
|
}
|
|
2029
2050
|
}
|
|
2030
|
-
const agentsPath =
|
|
2031
|
-
const featuresPath =
|
|
2032
|
-
if (await
|
|
2033
|
-
const bePath =
|
|
2034
|
-
const fePath =
|
|
2035
|
-
const projectType = await
|
|
2051
|
+
const agentsPath = path18.join(resolvedDocsDir, "agents");
|
|
2052
|
+
const featuresPath = path18.join(resolvedDocsDir, "features");
|
|
2053
|
+
if (await fs14.pathExists(agentsPath) && await fs14.pathExists(featuresPath)) {
|
|
2054
|
+
const bePath = path18.join(featuresPath, "be");
|
|
2055
|
+
const fePath = path18.join(featuresPath, "fe");
|
|
2056
|
+
const projectType = await fs14.pathExists(bePath) || await fs14.pathExists(fePath) ? "multi" : "single";
|
|
2036
2057
|
const components = projectType === "multi" ? resolveProjectComponents("multi", ["fe", "be"]) : void 0;
|
|
2037
2058
|
const langProbeCandidates = [
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2059
|
+
path18.join(agentsPath, "custom.md"),
|
|
2060
|
+
path18.join(agentsPath, "constitution.md"),
|
|
2061
|
+
path18.join(agentsPath, "agents.md")
|
|
2041
2062
|
];
|
|
2042
2063
|
let lang = "en";
|
|
2043
2064
|
for (const candidate of langProbeCandidates) {
|
|
2044
|
-
if (!await
|
|
2045
|
-
const content = await
|
|
2065
|
+
if (!await fs14.pathExists(candidate)) continue;
|
|
2066
|
+
const content = await fs14.readFile(candidate, "utf-8");
|
|
2046
2067
|
if (/[가-힣]/.test(content)) {
|
|
2047
2068
|
lang = "ko";
|
|
2048
2069
|
break;
|
|
@@ -2088,14 +2109,14 @@ function sanitizeTasksForLocal(content, lang) {
|
|
|
2088
2109
|
return normalizeTrailingBlankLines(next);
|
|
2089
2110
|
}
|
|
2090
2111
|
async function patchMarkdownIfExists(filePath, transform) {
|
|
2091
|
-
if (!await
|
|
2092
|
-
const content = await
|
|
2093
|
-
await
|
|
2112
|
+
if (!await fs14.pathExists(filePath)) return;
|
|
2113
|
+
const content = await fs14.readFile(filePath, "utf-8");
|
|
2114
|
+
await fs14.writeFile(filePath, transform(content), "utf-8");
|
|
2094
2115
|
}
|
|
2095
2116
|
async function applyLocalWorkflowTemplateToFeatureDir(featureDir, lang) {
|
|
2096
|
-
await patchMarkdownIfExists(
|
|
2117
|
+
await patchMarkdownIfExists(path18.join(featureDir, "spec.md"), sanitizeSpecForLocal);
|
|
2097
2118
|
await patchMarkdownIfExists(
|
|
2098
|
-
|
|
2119
|
+
path18.join(featureDir, "tasks.md"),
|
|
2099
2120
|
(content) => sanitizeTasksForLocal(content, lang)
|
|
2100
2121
|
);
|
|
2101
2122
|
}
|
|
@@ -2245,29 +2266,29 @@ async function runFeature(name, options) {
|
|
|
2245
2266
|
}
|
|
2246
2267
|
let featuresDir;
|
|
2247
2268
|
if (projectType === "multi") {
|
|
2248
|
-
featuresDir =
|
|
2269
|
+
featuresDir = path18.join(docsDir, "features", component);
|
|
2249
2270
|
} else {
|
|
2250
|
-
featuresDir =
|
|
2271
|
+
featuresDir = path18.join(docsDir, "features");
|
|
2251
2272
|
}
|
|
2252
2273
|
const featureFolderName = `${featureId}-${name}`;
|
|
2253
|
-
const featureDir =
|
|
2254
|
-
if (await
|
|
2274
|
+
const featureDir = path18.join(featuresDir, featureFolderName);
|
|
2275
|
+
if (await fs14.pathExists(featureDir)) {
|
|
2255
2276
|
throw createCliError(
|
|
2256
2277
|
"INVALID_ARGUMENT",
|
|
2257
2278
|
tr(lang, "cli", "feature.folderExists", { path: featureDir })
|
|
2258
2279
|
);
|
|
2259
2280
|
}
|
|
2260
|
-
const featureBasePath =
|
|
2281
|
+
const featureBasePath = path18.join(
|
|
2261
2282
|
getTemplatesDir(),
|
|
2262
2283
|
lang,
|
|
2263
2284
|
toTemplateProjectType(projectType),
|
|
2264
2285
|
"features",
|
|
2265
2286
|
"feature-base"
|
|
2266
2287
|
);
|
|
2267
|
-
if (!await
|
|
2288
|
+
if (!await fs14.pathExists(featureBasePath)) {
|
|
2268
2289
|
throw createCliError("DOCS_NOT_FOUND", tr(lang, "cli", "feature.baseNotFound"));
|
|
2269
2290
|
}
|
|
2270
|
-
await
|
|
2291
|
+
await fs14.copy(featureBasePath, featureDir);
|
|
2271
2292
|
const idNumber = featureId.replace("F", "");
|
|
2272
2293
|
const repoName = projectType === "multi" ? `{{projectName}}-${component}` : "{{projectName}}";
|
|
2273
2294
|
const replacements = {
|
|
@@ -2314,7 +2335,7 @@ async function runFeature(name, options) {
|
|
|
2314
2335
|
featureName: name,
|
|
2315
2336
|
component: projectType === "multi" ? component : void 0,
|
|
2316
2337
|
featurePath: featureDir,
|
|
2317
|
-
featurePathFromDocs:
|
|
2338
|
+
featurePathFromDocs: path18.relative(docsDir, featureDir)
|
|
2318
2339
|
};
|
|
2319
2340
|
},
|
|
2320
2341
|
{ owner: "feature" }
|
|
@@ -2326,9 +2347,9 @@ function sleep2(ms) {
|
|
|
2326
2347
|
async function waitForConfigAfterInit(cwd, timeoutMs = 8e3) {
|
|
2327
2348
|
const explicitDocsDir = (process.env.LEE_SPEC_KIT_DOCS_DIR || "").trim();
|
|
2328
2349
|
const candidates = [
|
|
2329
|
-
...explicitDocsDir ? [
|
|
2330
|
-
|
|
2331
|
-
|
|
2350
|
+
...explicitDocsDir ? [path18.resolve(explicitDocsDir)] : [],
|
|
2351
|
+
path18.resolve(cwd, "docs"),
|
|
2352
|
+
path18.resolve(cwd)
|
|
2332
2353
|
];
|
|
2333
2354
|
const endAt = Date.now() + timeoutMs;
|
|
2334
2355
|
while (Date.now() < endAt) {
|
|
@@ -2339,7 +2360,7 @@ async function waitForConfigAfterInit(cwd, timeoutMs = 8e3) {
|
|
|
2339
2360
|
const initLockPath = getInitLockPath(dir);
|
|
2340
2361
|
const docsLockPath = getDocsLockPath(dir);
|
|
2341
2362
|
for (const lockPath of [initLockPath, docsLockPath]) {
|
|
2342
|
-
if (await
|
|
2363
|
+
if (await fs14.pathExists(lockPath)) {
|
|
2343
2364
|
sawLock = true;
|
|
2344
2365
|
await waitForLockRelease(lockPath, {
|
|
2345
2366
|
timeoutMs: Math.max(200, endAt - Date.now()),
|
|
@@ -2355,17 +2376,17 @@ async function waitForConfigAfterInit(cwd, timeoutMs = 8e3) {
|
|
|
2355
2376
|
return getConfig(cwd);
|
|
2356
2377
|
}
|
|
2357
2378
|
async function getNextFeatureId(docsDir, projectType, components) {
|
|
2358
|
-
const featuresDir =
|
|
2379
|
+
const featuresDir = path18.join(docsDir, "features");
|
|
2359
2380
|
let max = 0;
|
|
2360
2381
|
const scanDirs = [];
|
|
2361
2382
|
if (projectType === "multi") {
|
|
2362
|
-
scanDirs.push(...components.map((component) =>
|
|
2383
|
+
scanDirs.push(...components.map((component) => path18.join(featuresDir, component)));
|
|
2363
2384
|
} else {
|
|
2364
2385
|
scanDirs.push(featuresDir);
|
|
2365
2386
|
}
|
|
2366
2387
|
for (const dir of scanDirs) {
|
|
2367
|
-
if (!await
|
|
2368
|
-
const entries = await
|
|
2388
|
+
if (!await fs14.pathExists(dir)) continue;
|
|
2389
|
+
const entries = await fs14.readdir(dir, { withFileTypes: true });
|
|
2369
2390
|
for (const entry of entries) {
|
|
2370
2391
|
if (!entry.isDirectory()) continue;
|
|
2371
2392
|
const match = entry.name.match(/^F(\d+)-/);
|
|
@@ -2473,6 +2494,21 @@ function formatSkillList(skills) {
|
|
|
2473
2494
|
function getFindingsPolicyText(lang, blockOnFindings) {
|
|
2474
2495
|
return blockOnFindings ? tr(lang, "messages", "prePrReviewFindingsBlock") : tr(lang, "messages", "prePrReviewFindingsWarn");
|
|
2475
2496
|
}
|
|
2497
|
+
function normalizeCommitTopicText(value) {
|
|
2498
|
+
return value.replace(/\s+/g, " ").trim();
|
|
2499
|
+
}
|
|
2500
|
+
function toShellSafeCommitTopic(value) {
|
|
2501
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\$/g, "\\$").replace(/`/g, "\\`");
|
|
2502
|
+
}
|
|
2503
|
+
function resolveProjectCommitTopic(feature) {
|
|
2504
|
+
const raw = feature.activeTask?.title || feature.lastDoneTask?.title || feature.nextTodoTask?.title || feature.folderName;
|
|
2505
|
+
const withoutTaskId = normalizeCommitTopicText(raw).replace(
|
|
2506
|
+
/^T-[A-Za-z0-9-]+\s+/,
|
|
2507
|
+
""
|
|
2508
|
+
);
|
|
2509
|
+
const topic = withoutTaskId || normalizeCommitTopicText(feature.folderName);
|
|
2510
|
+
return toShellSafeCommitTopic(topic);
|
|
2511
|
+
}
|
|
2476
2512
|
function getStepDefinitions(lang, workflow) {
|
|
2477
2513
|
const workflowPolicy = resolveWorkflowPolicy(workflow);
|
|
2478
2514
|
const prePrReviewPolicy = resolvePrePrReviewPolicy(workflow);
|
|
@@ -2796,10 +2832,12 @@ function getStepDefinitions(lang, workflow) {
|
|
|
2796
2832
|
cmd: f.issueNumber ? tr(lang, "messages", "projectCommitIssueUpdate", {
|
|
2797
2833
|
projectGitCwd: f.git.projectGitCwd,
|
|
2798
2834
|
issueNumber: f.issueNumber,
|
|
2799
|
-
folderName: f.folderName
|
|
2835
|
+
folderName: f.folderName,
|
|
2836
|
+
commitTopic: resolveProjectCommitTopic(f)
|
|
2800
2837
|
}) : tr(lang, "messages", "projectCommitUpdate", {
|
|
2801
2838
|
projectGitCwd: f.git.projectGitCwd,
|
|
2802
|
-
folderName: f.folderName
|
|
2839
|
+
folderName: f.folderName,
|
|
2840
|
+
commitTopic: resolveProjectCommitTopic(f)
|
|
2803
2841
|
})
|
|
2804
2842
|
}
|
|
2805
2843
|
];
|
|
@@ -2880,10 +2918,12 @@ function getStepDefinitions(lang, workflow) {
|
|
|
2880
2918
|
cmd: f.issueNumber ? tr(lang, "messages", "projectCommitIssueUpdate", {
|
|
2881
2919
|
projectGitCwd: f.git.projectGitCwd,
|
|
2882
2920
|
issueNumber: f.issueNumber,
|
|
2883
|
-
folderName: f.folderName
|
|
2921
|
+
folderName: f.folderName,
|
|
2922
|
+
commitTopic: resolveProjectCommitTopic(f)
|
|
2884
2923
|
}) : tr(lang, "messages", "projectCommitUpdate", {
|
|
2885
2924
|
projectGitCwd: f.git.projectGitCwd,
|
|
2886
|
-
folderName: f.folderName
|
|
2925
|
+
folderName: f.folderName,
|
|
2926
|
+
commitTopic: resolveProjectCommitTopic(f)
|
|
2887
2927
|
})
|
|
2888
2928
|
}
|
|
2889
2929
|
];
|
|
@@ -3133,6 +3173,59 @@ function getGitStatusPorcelain(cwd, relativePaths) {
|
|
|
3133
3173
|
return void 0;
|
|
3134
3174
|
}
|
|
3135
3175
|
}
|
|
3176
|
+
function normalizeInputPath(value) {
|
|
3177
|
+
return value.replace(/\\/g, "/").replace(/^\.\/+/, "").replace(/\/+$/, "");
|
|
3178
|
+
}
|
|
3179
|
+
function toUniqueNormalizedPaths(relativePaths) {
|
|
3180
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3181
|
+
const out = [];
|
|
3182
|
+
for (const value of relativePaths) {
|
|
3183
|
+
const normalized = normalizeInputPath(value);
|
|
3184
|
+
if (!normalized) continue;
|
|
3185
|
+
if (seen.has(normalized)) continue;
|
|
3186
|
+
seen.add(normalized);
|
|
3187
|
+
out.push(normalized);
|
|
3188
|
+
}
|
|
3189
|
+
return out;
|
|
3190
|
+
}
|
|
3191
|
+
function getTrackedGitPaths(cwd, relativePaths) {
|
|
3192
|
+
const inputs = toUniqueNormalizedPaths(relativePaths);
|
|
3193
|
+
if (inputs.length === 0) return /* @__PURE__ */ new Set();
|
|
3194
|
+
try {
|
|
3195
|
+
const out = execFileSync("git", ["ls-files", "--", ...inputs], {
|
|
3196
|
+
cwd,
|
|
3197
|
+
encoding: "utf-8",
|
|
3198
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
3199
|
+
});
|
|
3200
|
+
return new Set(
|
|
3201
|
+
out.split("\n").map((line) => normalizeInputPath(line)).filter(Boolean)
|
|
3202
|
+
);
|
|
3203
|
+
} catch {
|
|
3204
|
+
return void 0;
|
|
3205
|
+
}
|
|
3206
|
+
}
|
|
3207
|
+
function getIgnoredGitPaths(cwd, relativePaths) {
|
|
3208
|
+
const inputs = toUniqueNormalizedPaths(relativePaths);
|
|
3209
|
+
if (inputs.length === 0) return /* @__PURE__ */ new Set();
|
|
3210
|
+
try {
|
|
3211
|
+
const out = execFileSync("git", ["check-ignore", "--stdin"], {
|
|
3212
|
+
cwd,
|
|
3213
|
+
encoding: "utf-8",
|
|
3214
|
+
input: `${inputs.join("\n")}
|
|
3215
|
+
`,
|
|
3216
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
3217
|
+
});
|
|
3218
|
+
return new Set(
|
|
3219
|
+
out.split("\n").map((line) => normalizeInputPath(line)).filter(Boolean)
|
|
3220
|
+
);
|
|
3221
|
+
} catch (error) {
|
|
3222
|
+
if (error && typeof error === "object" && "status" in error) {
|
|
3223
|
+
const status = error.status;
|
|
3224
|
+
if (status === 1) return /* @__PURE__ */ new Set();
|
|
3225
|
+
}
|
|
3226
|
+
return void 0;
|
|
3227
|
+
}
|
|
3228
|
+
}
|
|
3136
3229
|
function getLastCommitForPath(cwd, relativePath) {
|
|
3137
3230
|
try {
|
|
3138
3231
|
const out = execSync(`git rev-list -n 1 HEAD -- "${relativePath}"`, {
|
|
@@ -3280,13 +3373,13 @@ function parsePrLink(value) {
|
|
|
3280
3373
|
return trimmed;
|
|
3281
3374
|
}
|
|
3282
3375
|
function normalizeGitPath(value) {
|
|
3283
|
-
return value.split(
|
|
3376
|
+
return value.split(path18.sep).join("/");
|
|
3284
3377
|
}
|
|
3285
3378
|
function resolveProjectStatusPaths(projectGitCwd, docsDir) {
|
|
3286
|
-
const relativeDocsDir =
|
|
3379
|
+
const relativeDocsDir = path18.relative(projectGitCwd, docsDir);
|
|
3287
3380
|
if (!relativeDocsDir) return [];
|
|
3288
|
-
if (
|
|
3289
|
-
if (relativeDocsDir === ".." || relativeDocsDir.startsWith(`..${
|
|
3381
|
+
if (path18.isAbsolute(relativeDocsDir)) return [];
|
|
3382
|
+
if (relativeDocsDir === ".." || relativeDocsDir.startsWith(`..${path18.sep}`)) {
|
|
3290
3383
|
return [];
|
|
3291
3384
|
}
|
|
3292
3385
|
const normalizedDocsDir = normalizeGitPath(relativeDocsDir).replace(/\/+$/, "");
|
|
@@ -3305,6 +3398,8 @@ function uniqueNormalizedPaths(values) {
|
|
|
3305
3398
|
}
|
|
3306
3399
|
return out;
|
|
3307
3400
|
}
|
|
3401
|
+
var PROJECT_DIRTY_STATUS_CACHE = /* @__PURE__ */ new Map();
|
|
3402
|
+
var COMPONENT_STATUS_PATH_CACHE = /* @__PURE__ */ new Map();
|
|
3308
3403
|
async function resolveComponentStatusPaths(projectGitCwd, component, workflow) {
|
|
3309
3404
|
const configured = workflow?.componentPaths?.[component];
|
|
3310
3405
|
const configuredCandidates = Array.isArray(configured) ? configured.map((value) => String(value).trim()).filter(Boolean) : [];
|
|
@@ -3318,24 +3413,33 @@ async function resolveComponentStatusPaths(projectGitCwd, component, workflow) {
|
|
|
3318
3413
|
const normalizedCandidates = uniqueNormalizedPaths(
|
|
3319
3414
|
candidates.map((candidate) => {
|
|
3320
3415
|
if (!candidate) return "";
|
|
3321
|
-
if (!
|
|
3322
|
-
const relative =
|
|
3416
|
+
if (!path18.isAbsolute(candidate)) return candidate;
|
|
3417
|
+
const relative = path18.relative(projectGitCwd, candidate);
|
|
3323
3418
|
if (!relative) return "";
|
|
3324
|
-
if (relative === ".." || relative.startsWith(`..${
|
|
3419
|
+
if (relative === ".." || relative.startsWith(`..${path18.sep}`)) return "";
|
|
3325
3420
|
return relative;
|
|
3326
3421
|
}).filter(Boolean)
|
|
3327
3422
|
);
|
|
3423
|
+
const cacheKey = JSON.stringify({
|
|
3424
|
+
projectGitCwd,
|
|
3425
|
+
component,
|
|
3426
|
+
normalizedCandidates
|
|
3427
|
+
});
|
|
3428
|
+
const cached = COMPONENT_STATUS_PATH_CACHE.get(cacheKey);
|
|
3429
|
+
if (cached) return [...cached];
|
|
3328
3430
|
const existing = [];
|
|
3329
3431
|
for (const candidate of normalizedCandidates) {
|
|
3330
|
-
if (await
|
|
3432
|
+
if (await fs14.pathExists(path18.join(projectGitCwd, candidate))) {
|
|
3331
3433
|
existing.push(candidate);
|
|
3332
3434
|
}
|
|
3333
3435
|
}
|
|
3436
|
+
COMPONENT_STATUS_PATH_CACHE.set(cacheKey, [...existing]);
|
|
3334
3437
|
return existing;
|
|
3335
3438
|
}
|
|
3336
3439
|
function parseTasks(content) {
|
|
3337
3440
|
const summary = { total: 0, todo: 0, doing: 0, done: 0 };
|
|
3338
3441
|
let activeTask;
|
|
3442
|
+
let lastDoneTask;
|
|
3339
3443
|
let nextTodoTask;
|
|
3340
3444
|
const lines = content.split("\n");
|
|
3341
3445
|
let inCodeBlock = false;
|
|
@@ -3356,11 +3460,14 @@ function parseTasks(content) {
|
|
|
3356
3460
|
if (!activeTask && (status === "DOING" || status === "REVIEW")) {
|
|
3357
3461
|
activeTask = { status, title };
|
|
3358
3462
|
}
|
|
3463
|
+
if (status === "DONE") {
|
|
3464
|
+
lastDoneTask = { status: "DONE", title };
|
|
3465
|
+
}
|
|
3359
3466
|
if (!nextTodoTask && status === "TODO") {
|
|
3360
3467
|
nextTodoTask = { status: "TODO", title };
|
|
3361
3468
|
}
|
|
3362
3469
|
}
|
|
3363
|
-
return { summary, activeTask, nextTodoTask };
|
|
3470
|
+
return { summary, activeTask, lastDoneTask, nextTodoTask };
|
|
3364
3471
|
}
|
|
3365
3472
|
function parseCompletionChecklist(content) {
|
|
3366
3473
|
const lines = content.split("\n");
|
|
@@ -3390,33 +3497,34 @@ async function parseFeature(featurePath, type, context, options) {
|
|
|
3390
3497
|
const lang = options.lang;
|
|
3391
3498
|
const workflowPolicy = resolveWorkflowPolicy(options.workflow);
|
|
3392
3499
|
const prePrReviewPolicy = resolvePrePrReviewPolicy(options.workflow);
|
|
3393
|
-
const folderName =
|
|
3500
|
+
const folderName = path18.basename(featurePath);
|
|
3394
3501
|
const match = folderName.match(/^(F\d+)-(.+)$/);
|
|
3395
3502
|
const id = match?.[1];
|
|
3396
3503
|
const slug = match?.[2] || folderName;
|
|
3397
|
-
const specPath =
|
|
3398
|
-
const planPath =
|
|
3399
|
-
const tasksPath =
|
|
3504
|
+
const specPath = path18.join(featurePath, "spec.md");
|
|
3505
|
+
const planPath = path18.join(featurePath, "plan.md");
|
|
3506
|
+
const tasksPath = path18.join(featurePath, "tasks.md");
|
|
3400
3507
|
let specStatus;
|
|
3401
3508
|
let issueNumber;
|
|
3402
|
-
const specExists = await
|
|
3509
|
+
const specExists = await fs14.pathExists(specPath);
|
|
3403
3510
|
if (specExists) {
|
|
3404
|
-
const content = await
|
|
3511
|
+
const content = await fs14.readFile(specPath, "utf-8");
|
|
3405
3512
|
const statusValue = extractFirstSpecValue(content, ["\uC0C1\uD0DC", "Status"]);
|
|
3406
3513
|
specStatus = parseDocStatus(statusValue);
|
|
3407
3514
|
const issueValue = extractFirstSpecValue(content, ["\uC774\uC288 \uBC88\uD638", "Issue Number", "Issue"]);
|
|
3408
3515
|
issueNumber = parseIssueNumber(issueValue);
|
|
3409
3516
|
}
|
|
3410
3517
|
let planStatus;
|
|
3411
|
-
const planExists = await
|
|
3518
|
+
const planExists = await fs14.pathExists(planPath);
|
|
3412
3519
|
if (planExists) {
|
|
3413
|
-
const content = await
|
|
3520
|
+
const content = await fs14.readFile(planPath, "utf-8");
|
|
3414
3521
|
const statusValue = extractFirstSpecValue(content, ["\uC0C1\uD0DC", "Status"]);
|
|
3415
3522
|
planStatus = parseDocStatus(statusValue);
|
|
3416
3523
|
}
|
|
3417
|
-
const tasksExists = await
|
|
3524
|
+
const tasksExists = await fs14.pathExists(tasksPath);
|
|
3418
3525
|
const tasksSummary = { total: 0, todo: 0, doing: 0, done: 0 };
|
|
3419
3526
|
let activeTask;
|
|
3527
|
+
let lastDoneTask;
|
|
3420
3528
|
let nextTodoTask;
|
|
3421
3529
|
let tasksDocStatus;
|
|
3422
3530
|
let tasksDocStatusFieldExists = false;
|
|
@@ -3428,13 +3536,19 @@ async function parseFeature(featurePath, type, context, options) {
|
|
|
3428
3536
|
let prStatusFieldExists = false;
|
|
3429
3537
|
let prePrReviewFieldExists = false;
|
|
3430
3538
|
if (tasksExists) {
|
|
3431
|
-
const content = await
|
|
3432
|
-
const {
|
|
3539
|
+
const content = await fs14.readFile(tasksPath, "utf-8");
|
|
3540
|
+
const {
|
|
3541
|
+
summary,
|
|
3542
|
+
activeTask: active,
|
|
3543
|
+
lastDoneTask: lastDone,
|
|
3544
|
+
nextTodoTask: nextTodo
|
|
3545
|
+
} = parseTasks(content);
|
|
3433
3546
|
tasksSummary.total = summary.total;
|
|
3434
3547
|
tasksSummary.todo = summary.todo;
|
|
3435
3548
|
tasksSummary.doing = summary.doing;
|
|
3436
3549
|
tasksSummary.done = summary.done;
|
|
3437
3550
|
activeTask = active;
|
|
3551
|
+
lastDoneTask = lastDone;
|
|
3438
3552
|
nextTodoTask = nextTodo;
|
|
3439
3553
|
completionChecklist = parseCompletionChecklist(content);
|
|
3440
3554
|
if (!issueNumber) {
|
|
@@ -3470,44 +3584,77 @@ async function parseFeature(featurePath, type, context, options) {
|
|
|
3470
3584
|
slug,
|
|
3471
3585
|
folderName
|
|
3472
3586
|
);
|
|
3473
|
-
const relativeFeaturePathFromDocs =
|
|
3474
|
-
const
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
const componentStatusPaths = await resolveComponentStatusPaths(
|
|
3485
|
-
context.projectGitCwd,
|
|
3486
|
-
type,
|
|
3487
|
-
options.workflow
|
|
3488
|
-
);
|
|
3489
|
-
projectStatusPaths = componentStatusPaths.length > 0 ? componentStatusPaths : resolveProjectStatusPaths(context.projectGitCwd, context.docsDir);
|
|
3587
|
+
const relativeFeaturePathFromDocs = path18.relative(context.docsDir, featurePath);
|
|
3588
|
+
const normalizedFeaturePathFromDocs = normalizeGitPath(relativeFeaturePathFromDocs);
|
|
3589
|
+
const docsPathIgnored = typeof context.docsPathIgnored === "boolean" ? context.docsPathIgnored : isGitPathIgnored(context.docsGitCwd, normalizedFeaturePathFromDocs);
|
|
3590
|
+
let docsHasUncommittedChanges = typeof context.docsHasUncommittedChanges === "boolean" ? context.docsHasUncommittedChanges : false;
|
|
3591
|
+
let docsEverCommitted = typeof context.docsEverCommitted === "boolean" ? context.docsEverCommitted : false;
|
|
3592
|
+
let docsGitUnavailable = !!context.docsGitUnavailable;
|
|
3593
|
+
if (typeof context.docsHasUncommittedChanges !== "boolean") {
|
|
3594
|
+
const docsStatus = getGitStatusPorcelain(context.docsGitCwd, [normalizedFeaturePathFromDocs]);
|
|
3595
|
+
if (docsStatus === void 0) {
|
|
3596
|
+
docsGitUnavailable = true;
|
|
3597
|
+
docsHasUncommittedChanges = true;
|
|
3490
3598
|
} else {
|
|
3491
|
-
|
|
3599
|
+
docsHasUncommittedChanges = docsStatus.trim().length > 0;
|
|
3600
|
+
}
|
|
3601
|
+
}
|
|
3602
|
+
if (typeof context.docsEverCommitted !== "boolean") {
|
|
3603
|
+
const docsLastCommit = getLastCommitForPath(
|
|
3604
|
+
context.docsGitCwd,
|
|
3605
|
+
normalizedFeaturePathFromDocs
|
|
3606
|
+
);
|
|
3607
|
+
docsEverCommitted = !!docsLastCommit;
|
|
3608
|
+
}
|
|
3609
|
+
let projectHasUncommittedChanges = typeof context.projectHasUncommittedChanges === "boolean" ? context.projectHasUncommittedChanges : false;
|
|
3610
|
+
let projectStatusUnavailable = !!context.projectStatusUnavailable;
|
|
3611
|
+
if (typeof context.projectHasUncommittedChanges !== "boolean" && context.projectGitCwd) {
|
|
3612
|
+
const dirtyScopePolicy = resolveCodeDirtyScopePolicy(options.workflow, options.projectType);
|
|
3613
|
+
const projectCacheKey = JSON.stringify({
|
|
3614
|
+
projectGitCwd: context.projectGitCwd,
|
|
3615
|
+
docsDir: context.docsDir,
|
|
3616
|
+
type,
|
|
3617
|
+
dirtyScopePolicy,
|
|
3618
|
+
componentPaths: options.workflow?.componentPaths?.[type] || []
|
|
3619
|
+
});
|
|
3620
|
+
const cachedStatus = PROJECT_DIRTY_STATUS_CACHE.get(projectCacheKey);
|
|
3621
|
+
if (cachedStatus) {
|
|
3622
|
+
projectHasUncommittedChanges = cachedStatus.hasUncommittedChanges;
|
|
3623
|
+
projectStatusUnavailable = cachedStatus.statusUnavailable;
|
|
3624
|
+
} else {
|
|
3625
|
+
let projectStatusPaths = [];
|
|
3626
|
+
if (dirtyScopePolicy === "component" && type !== "single") {
|
|
3627
|
+
const componentStatusPaths = await resolveComponentStatusPaths(
|
|
3628
|
+
context.projectGitCwd,
|
|
3629
|
+
type,
|
|
3630
|
+
options.workflow
|
|
3631
|
+
);
|
|
3632
|
+
projectStatusPaths = componentStatusPaths.length > 0 ? componentStatusPaths : resolveProjectStatusPaths(context.projectGitCwd, context.docsDir);
|
|
3633
|
+
} else {
|
|
3634
|
+
projectStatusPaths = resolveProjectStatusPaths(
|
|
3635
|
+
context.projectGitCwd,
|
|
3636
|
+
context.docsDir
|
|
3637
|
+
);
|
|
3638
|
+
}
|
|
3639
|
+
const projectStatus = getGitStatusPorcelain(
|
|
3492
3640
|
context.projectGitCwd,
|
|
3493
|
-
|
|
3641
|
+
projectStatusPaths
|
|
3494
3642
|
);
|
|
3643
|
+
projectStatusUnavailable = projectStatus === void 0;
|
|
3644
|
+
projectHasUncommittedChanges = projectStatus === void 0 ? false : projectStatus.trim().length > 0;
|
|
3645
|
+
PROJECT_DIRTY_STATUS_CACHE.set(projectCacheKey, {
|
|
3646
|
+
hasUncommittedChanges: projectHasUncommittedChanges,
|
|
3647
|
+
statusUnavailable: projectStatusUnavailable
|
|
3648
|
+
});
|
|
3495
3649
|
}
|
|
3496
3650
|
}
|
|
3497
|
-
|
|
3498
|
-
const projectHasUncommittedChanges = projectStatus === void 0 ? false : projectStatus.trim().length > 0;
|
|
3499
|
-
const docsLastCommit = getLastCommitForPath(
|
|
3500
|
-
context.docsGitCwd,
|
|
3501
|
-
relativeFeaturePathFromDocs
|
|
3502
|
-
);
|
|
3503
|
-
const docsEverCommitted = !!docsLastCommit;
|
|
3504
|
-
if (docsStatus === void 0) {
|
|
3651
|
+
if (docsGitUnavailable) {
|
|
3505
3652
|
warnings.push(tr(lang, "warnings", "docsGitUnavailable"));
|
|
3506
3653
|
}
|
|
3507
3654
|
if (docsPathIgnored === true) {
|
|
3508
3655
|
warnings.push(
|
|
3509
3656
|
tr(lang, "warnings", "docsPathIgnored", {
|
|
3510
|
-
path:
|
|
3657
|
+
path: normalizedFeaturePathFromDocs
|
|
3511
3658
|
})
|
|
3512
3659
|
);
|
|
3513
3660
|
}
|
|
@@ -3575,6 +3722,7 @@ async function parseFeature(featurePath, type, context, options) {
|
|
|
3575
3722
|
tasksDocStatus,
|
|
3576
3723
|
tasks: tasksSummary,
|
|
3577
3724
|
activeTask,
|
|
3725
|
+
lastDoneTask,
|
|
3578
3726
|
nextTodoTask,
|
|
3579
3727
|
completionChecklist,
|
|
3580
3728
|
prePrReview: {
|
|
@@ -3612,90 +3760,181 @@ async function parseFeature(featurePath, type, context, options) {
|
|
|
3612
3760
|
);
|
|
3613
3761
|
return { ...featureState, currentStep, actions, nextAction, warnings };
|
|
3614
3762
|
}
|
|
3763
|
+
function normalizeRelPath(value) {
|
|
3764
|
+
return value.replace(/\\/g, "/").replace(/^\.\/+/, "").replace(/\/+$/, "");
|
|
3765
|
+
}
|
|
3766
|
+
function parsePorcelainChangedPaths(porcelain) {
|
|
3767
|
+
const changed = [];
|
|
3768
|
+
for (const rawLine of porcelain.split("\n")) {
|
|
3769
|
+
if (!rawLine.trim()) continue;
|
|
3770
|
+
const payload = rawLine.slice(3).trim();
|
|
3771
|
+
if (!payload) continue;
|
|
3772
|
+
const pathCandidate = payload.includes(" -> ") ? payload.split(" -> ").at(-1) || "" : payload;
|
|
3773
|
+
const normalized = normalizeRelPath(pathCandidate.replace(/^"+|"+$/g, ""));
|
|
3774
|
+
if (!normalized) continue;
|
|
3775
|
+
changed.push(normalized);
|
|
3776
|
+
}
|
|
3777
|
+
return changed;
|
|
3778
|
+
}
|
|
3779
|
+
function findFeaturePathPrefix(normalizedPath, relativeFeaturePaths) {
|
|
3780
|
+
for (const featurePath of relativeFeaturePaths) {
|
|
3781
|
+
if (normalizedPath === featurePath) return featurePath;
|
|
3782
|
+
if (normalizedPath.startsWith(`${featurePath}/`)) return featurePath;
|
|
3783
|
+
const nestedPrefix = `/${featurePath}`;
|
|
3784
|
+
if (normalizedPath.endsWith(nestedPrefix)) return featurePath;
|
|
3785
|
+
if (normalizedPath.includes(`${nestedPrefix}/`)) return featurePath;
|
|
3786
|
+
}
|
|
3787
|
+
return void 0;
|
|
3788
|
+
}
|
|
3789
|
+
function buildDefaultDocsFeatureGitMeta(relativeFeaturePaths) {
|
|
3790
|
+
const map = /* @__PURE__ */ new Map();
|
|
3791
|
+
for (const featurePath of relativeFeaturePaths) {
|
|
3792
|
+
map.set(featurePath, {
|
|
3793
|
+
docsPathIgnored: false,
|
|
3794
|
+
docsHasUncommittedChanges: false,
|
|
3795
|
+
docsEverCommitted: false,
|
|
3796
|
+
docsGitUnavailable: false
|
|
3797
|
+
});
|
|
3798
|
+
}
|
|
3799
|
+
return map;
|
|
3800
|
+
}
|
|
3801
|
+
function buildDocsFeatureGitMeta(docsGitCwd, relativeFeaturePaths) {
|
|
3802
|
+
const normalizedFeaturePaths = relativeFeaturePaths.map(
|
|
3803
|
+
(value) => normalizeRelPath(value)
|
|
3804
|
+
);
|
|
3805
|
+
const map = buildDefaultDocsFeatureGitMeta(normalizedFeaturePaths);
|
|
3806
|
+
if (normalizedFeaturePaths.length === 0) return map;
|
|
3807
|
+
const docsStatus = getGitStatusPorcelain(docsGitCwd, normalizedFeaturePaths);
|
|
3808
|
+
if (docsStatus === void 0) {
|
|
3809
|
+
for (const featurePath of normalizedFeaturePaths) {
|
|
3810
|
+
const current = map.get(featurePath);
|
|
3811
|
+
if (!current) continue;
|
|
3812
|
+
current.docsGitUnavailable = true;
|
|
3813
|
+
current.docsHasUncommittedChanges = true;
|
|
3814
|
+
}
|
|
3815
|
+
} else {
|
|
3816
|
+
const changedPaths = parsePorcelainChangedPaths(docsStatus);
|
|
3817
|
+
for (const changedPath of changedPaths) {
|
|
3818
|
+
const featurePath = findFeaturePathPrefix(changedPath, normalizedFeaturePaths);
|
|
3819
|
+
if (!featurePath) continue;
|
|
3820
|
+
const current = map.get(featurePath);
|
|
3821
|
+
if (!current) continue;
|
|
3822
|
+
current.docsHasUncommittedChanges = true;
|
|
3823
|
+
}
|
|
3824
|
+
}
|
|
3825
|
+
const trackedPaths = getTrackedGitPaths(docsGitCwd, normalizedFeaturePaths);
|
|
3826
|
+
if (trackedPaths) {
|
|
3827
|
+
for (const trackedPath of trackedPaths) {
|
|
3828
|
+
const featurePath = findFeaturePathPrefix(
|
|
3829
|
+
normalizeRelPath(trackedPath),
|
|
3830
|
+
normalizedFeaturePaths
|
|
3831
|
+
);
|
|
3832
|
+
if (!featurePath) continue;
|
|
3833
|
+
const current = map.get(featurePath);
|
|
3834
|
+
if (!current) continue;
|
|
3835
|
+
current.docsEverCommitted = true;
|
|
3836
|
+
}
|
|
3837
|
+
}
|
|
3838
|
+
const ignoredPaths = getIgnoredGitPaths(docsGitCwd, normalizedFeaturePaths);
|
|
3839
|
+
if (ignoredPaths) {
|
|
3840
|
+
for (const ignoredPath of ignoredPaths) {
|
|
3841
|
+
const featurePath = findFeaturePathPrefix(
|
|
3842
|
+
normalizeRelPath(ignoredPath),
|
|
3843
|
+
normalizedFeaturePaths
|
|
3844
|
+
);
|
|
3845
|
+
if (!featurePath) continue;
|
|
3846
|
+
const current = map.get(featurePath);
|
|
3847
|
+
if (!current) continue;
|
|
3848
|
+
current.docsPathIgnored = true;
|
|
3849
|
+
}
|
|
3850
|
+
}
|
|
3851
|
+
return map;
|
|
3852
|
+
}
|
|
3615
3853
|
async function scanFeatures(config) {
|
|
3616
3854
|
const features = [];
|
|
3617
3855
|
const warnings = [];
|
|
3618
3856
|
const stepDefinitions = getStepDefinitions(config.lang, config.workflow);
|
|
3619
3857
|
const docsBranch = getCurrentBranch(config.docsDir);
|
|
3620
3858
|
const projectBranches = {};
|
|
3859
|
+
const projectGitCwds = {};
|
|
3621
3860
|
let singleProject;
|
|
3622
3861
|
if (config.projectType === "single") {
|
|
3623
3862
|
singleProject = resolveProjectGitCwd(config, "single", config.lang);
|
|
3624
3863
|
if (singleProject.warning) warnings.push(singleProject.warning);
|
|
3625
3864
|
projectBranches.single = singleProject.cwd ? getCurrentBranch(singleProject.cwd) : "";
|
|
3865
|
+
projectGitCwds.single = singleProject.cwd ?? void 0;
|
|
3626
3866
|
} else {
|
|
3627
3867
|
const components = resolveProjectComponents(config.projectType, config.components);
|
|
3628
3868
|
for (const component of components) {
|
|
3629
3869
|
const project = resolveProjectGitCwd(config, component, config.lang);
|
|
3630
3870
|
if (project.warning) warnings.push(project.warning);
|
|
3631
3871
|
projectBranches[component] = project.cwd ? getCurrentBranch(project.cwd) : "";
|
|
3872
|
+
projectGitCwds[component] = project.cwd ?? void 0;
|
|
3632
3873
|
}
|
|
3633
3874
|
}
|
|
3875
|
+
const allFeatureDirs = [];
|
|
3876
|
+
const componentFeatureDirs = /* @__PURE__ */ new Map();
|
|
3634
3877
|
if (config.projectType === "single") {
|
|
3635
3878
|
const featureDirs = await glob("features/*/", {
|
|
3636
3879
|
cwd: config.docsDir,
|
|
3637
3880
|
absolute: true,
|
|
3638
3881
|
ignore: ["**/feature-base/**"]
|
|
3639
3882
|
});
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
features.push(
|
|
3643
|
-
await parseFeature(
|
|
3644
|
-
dir,
|
|
3645
|
-
"single",
|
|
3646
|
-
{
|
|
3647
|
-
projectBranch: projectBranches.single,
|
|
3648
|
-
docsBranch,
|
|
3649
|
-
docsGitCwd: config.docsDir,
|
|
3650
|
-
projectGitCwd: singleProject?.cwd ?? void 0,
|
|
3651
|
-
docsDir: config.docsDir,
|
|
3652
|
-
projectBranchAvailable: Boolean(singleProject?.cwd)
|
|
3653
|
-
},
|
|
3654
|
-
{
|
|
3655
|
-
lang: config.lang,
|
|
3656
|
-
stepDefinitions,
|
|
3657
|
-
approval: config.approval,
|
|
3658
|
-
workflow: config.workflow,
|
|
3659
|
-
projectType: config.projectType
|
|
3660
|
-
}
|
|
3661
|
-
)
|
|
3662
|
-
);
|
|
3663
|
-
}
|
|
3664
|
-
}
|
|
3883
|
+
componentFeatureDirs.set("single", featureDirs);
|
|
3884
|
+
allFeatureDirs.push(...featureDirs);
|
|
3665
3885
|
} else {
|
|
3666
3886
|
const components = resolveProjectComponents(config.projectType, config.components);
|
|
3667
3887
|
for (const component of components) {
|
|
3668
|
-
const project = resolveProjectGitCwd(config, component, config.lang);
|
|
3669
3888
|
const componentDirs = await glob(`features/${component}/*/`, {
|
|
3670
3889
|
cwd: config.docsDir,
|
|
3671
3890
|
absolute: true
|
|
3672
3891
|
});
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
features.push(
|
|
3676
|
-
await parseFeature(
|
|
3677
|
-
dir,
|
|
3678
|
-
component,
|
|
3679
|
-
{
|
|
3680
|
-
projectBranch: projectBranches[component] || "",
|
|
3681
|
-
docsBranch,
|
|
3682
|
-
docsGitCwd: config.docsDir,
|
|
3683
|
-
projectGitCwd: project.cwd ?? void 0,
|
|
3684
|
-
docsDir: config.docsDir,
|
|
3685
|
-
projectBranchAvailable: Boolean(project.cwd)
|
|
3686
|
-
},
|
|
3687
|
-
{
|
|
3688
|
-
lang: config.lang,
|
|
3689
|
-
stepDefinitions,
|
|
3690
|
-
approval: config.approval,
|
|
3691
|
-
workflow: config.workflow,
|
|
3692
|
-
projectType: config.projectType
|
|
3693
|
-
}
|
|
3694
|
-
)
|
|
3695
|
-
);
|
|
3696
|
-
}
|
|
3892
|
+
componentFeatureDirs.set(component, componentDirs);
|
|
3893
|
+
allFeatureDirs.push(...componentDirs);
|
|
3697
3894
|
}
|
|
3698
3895
|
}
|
|
3896
|
+
const relativeFeaturePaths = allFeatureDirs.map(
|
|
3897
|
+
(dir) => normalizeRelPath(path18.relative(config.docsDir, dir))
|
|
3898
|
+
);
|
|
3899
|
+
const docsGitMeta = buildDocsFeatureGitMeta(config.docsDir, relativeFeaturePaths);
|
|
3900
|
+
const parseTargets = config.projectType === "single" ? [{ type: "single", dirs: componentFeatureDirs.get("single") || [] }] : resolveProjectComponents(config.projectType, config.components).map((component) => ({
|
|
3901
|
+
type: component,
|
|
3902
|
+
dirs: componentFeatureDirs.get(component) || []
|
|
3903
|
+
}));
|
|
3904
|
+
for (const target of parseTargets) {
|
|
3905
|
+
const parsed = await Promise.all(
|
|
3906
|
+
target.dirs.map(async (dir) => {
|
|
3907
|
+
const relativeFeaturePathFromDocs = normalizeRelPath(
|
|
3908
|
+
path18.relative(config.docsDir, dir)
|
|
3909
|
+
);
|
|
3910
|
+
const docsMeta = docsGitMeta.get(relativeFeaturePathFromDocs);
|
|
3911
|
+
return parseFeature(
|
|
3912
|
+
dir,
|
|
3913
|
+
target.type,
|
|
3914
|
+
{
|
|
3915
|
+
projectBranch: projectBranches[target.type] || "",
|
|
3916
|
+
docsBranch,
|
|
3917
|
+
docsGitCwd: config.docsDir,
|
|
3918
|
+
projectGitCwd: projectGitCwds[target.type],
|
|
3919
|
+
docsDir: config.docsDir,
|
|
3920
|
+
projectBranchAvailable: Boolean(projectGitCwds[target.type]),
|
|
3921
|
+
docsPathIgnored: docsMeta?.docsPathIgnored,
|
|
3922
|
+
docsHasUncommittedChanges: docsMeta?.docsHasUncommittedChanges,
|
|
3923
|
+
docsEverCommitted: docsMeta?.docsEverCommitted,
|
|
3924
|
+
docsGitUnavailable: docsMeta?.docsGitUnavailable
|
|
3925
|
+
},
|
|
3926
|
+
{
|
|
3927
|
+
lang: config.lang,
|
|
3928
|
+
stepDefinitions,
|
|
3929
|
+
approval: config.approval,
|
|
3930
|
+
workflow: config.workflow,
|
|
3931
|
+
projectType: config.projectType
|
|
3932
|
+
}
|
|
3933
|
+
);
|
|
3934
|
+
})
|
|
3935
|
+
);
|
|
3936
|
+
features.push(...parsed);
|
|
3937
|
+
}
|
|
3699
3938
|
return {
|
|
3700
3939
|
features,
|
|
3701
3940
|
branches: {
|
|
@@ -3735,13 +3974,13 @@ async function runStatus(options) {
|
|
|
3735
3974
|
);
|
|
3736
3975
|
}
|
|
3737
3976
|
const { docsDir, projectType, projectName, lang } = config;
|
|
3738
|
-
const featuresDir =
|
|
3977
|
+
const featuresDir = path18.join(docsDir, "features");
|
|
3739
3978
|
const scan = await scanFeatures(config);
|
|
3740
3979
|
const features = [];
|
|
3741
3980
|
const idMap = /* @__PURE__ */ new Map();
|
|
3742
3981
|
for (const f of scan.features) {
|
|
3743
3982
|
const id = f.id || "UNKNOWN";
|
|
3744
|
-
const relPath =
|
|
3983
|
+
const relPath = path18.relative(docsDir, f.path);
|
|
3745
3984
|
if (!idMap.has(id)) idMap.set(id, []);
|
|
3746
3985
|
idMap.get(id).push(relPath);
|
|
3747
3986
|
if (!f.docs.specExists || !f.docs.tasksExists) continue;
|
|
@@ -3822,7 +4061,7 @@ async function runStatus(options) {
|
|
|
3822
4061
|
}
|
|
3823
4062
|
console.log();
|
|
3824
4063
|
if (options.write) {
|
|
3825
|
-
const outputPath =
|
|
4064
|
+
const outputPath = path18.join(featuresDir, "status.md");
|
|
3826
4065
|
const date = getLocalDateString();
|
|
3827
4066
|
const content = [
|
|
3828
4067
|
"# Feature Status",
|
|
@@ -3837,7 +4076,7 @@ async function runStatus(options) {
|
|
|
3837
4076
|
),
|
|
3838
4077
|
""
|
|
3839
4078
|
].join("\n");
|
|
3840
|
-
await
|
|
4079
|
+
await fs14.writeFile(outputPath, content, "utf-8");
|
|
3841
4080
|
console.log(
|
|
3842
4081
|
chalk6.green(
|
|
3843
4082
|
tr(lang, "cli", "status.wrote", { path: outputPath })
|
|
@@ -3850,9 +4089,9 @@ function escapeRegExp2(value) {
|
|
|
3850
4089
|
}
|
|
3851
4090
|
async function getFeatureNameFromSpec(featureDir, fallbackSlug, fallbackFolderName) {
|
|
3852
4091
|
try {
|
|
3853
|
-
const specPath =
|
|
3854
|
-
if (!await
|
|
3855
|
-
const content = await
|
|
4092
|
+
const specPath = path18.join(featureDir, "spec.md");
|
|
4093
|
+
if (!await fs14.pathExists(specPath)) return fallbackSlug;
|
|
4094
|
+
const content = await fs14.readFile(specPath, "utf-8");
|
|
3856
4095
|
const keys = ["\uAE30\uB2A5\uBA85", "Feature Name"];
|
|
3857
4096
|
for (const key of keys) {
|
|
3858
4097
|
const regex = new RegExp(
|
|
@@ -3931,17 +4170,17 @@ async function runUpdate(options) {
|
|
|
3931
4170
|
console.log(chalk6.blue(tr(lang, "cli", "update.updatingAgents")));
|
|
3932
4171
|
}
|
|
3933
4172
|
if (agentsMode === "all") {
|
|
3934
|
-
const commonAgentsBase =
|
|
3935
|
-
const typeAgentsBase =
|
|
4173
|
+
const commonAgentsBase = path18.join(templatesDir, lang, "common", "agents");
|
|
4174
|
+
const typeAgentsBase = path18.join(
|
|
3936
4175
|
templatesDir,
|
|
3937
4176
|
lang,
|
|
3938
4177
|
toTemplateProjectType(projectType),
|
|
3939
4178
|
"agents"
|
|
3940
4179
|
);
|
|
3941
|
-
const targetAgentsBase =
|
|
3942
|
-
const commonAgents = agentsMode === "skills" ?
|
|
3943
|
-
const typeAgents = agentsMode === "skills" ?
|
|
3944
|
-
const targetAgents = agentsMode === "skills" ?
|
|
4180
|
+
const targetAgentsBase = path18.join(docsDir, "agents");
|
|
4181
|
+
const commonAgents = agentsMode === "skills" ? path18.join(commonAgentsBase, "skills") : commonAgentsBase;
|
|
4182
|
+
const typeAgents = agentsMode === "skills" ? path18.join(typeAgentsBase, "skills") : typeAgentsBase;
|
|
4183
|
+
const targetAgents = agentsMode === "skills" ? path18.join(targetAgentsBase, "skills") : targetAgentsBase;
|
|
3945
4184
|
const featurePath = projectType === "multi" ? isDefaultFullstackComponents(config.components || []) ? "docs/features/{be|fe}" : "docs/features/{component}" : "docs/features";
|
|
3946
4185
|
const projectName = config.projectName ?? "{{projectName}}";
|
|
3947
4186
|
const commonReplacements = {
|
|
@@ -3951,7 +4190,7 @@ async function runUpdate(options) {
|
|
|
3951
4190
|
const typeReplacements = {
|
|
3952
4191
|
"{{projectName}}": projectName
|
|
3953
4192
|
};
|
|
3954
|
-
if (await
|
|
4193
|
+
if (await fs14.pathExists(commonAgents)) {
|
|
3955
4194
|
const count = await updateFolder(
|
|
3956
4195
|
commonAgents,
|
|
3957
4196
|
targetAgents,
|
|
@@ -3969,7 +4208,7 @@ async function runUpdate(options) {
|
|
|
3969
4208
|
);
|
|
3970
4209
|
updatedCount += count;
|
|
3971
4210
|
}
|
|
3972
|
-
if (await
|
|
4211
|
+
if (await fs14.pathExists(typeAgents)) {
|
|
3973
4212
|
const count = await updateFolder(
|
|
3974
4213
|
typeAgents,
|
|
3975
4214
|
targetAgents,
|
|
@@ -4019,24 +4258,24 @@ async function runUpdate(options) {
|
|
|
4019
4258
|
async function updateFolder(sourceDir, targetDir, force, replacements, lang = DEFAULT_LANG, options = {}) {
|
|
4020
4259
|
const protectedFiles = options.protectedFiles ?? /* @__PURE__ */ new Set(["custom.md", "constitution.md"]);
|
|
4021
4260
|
const skipDirectories = options.skipDirectories ?? /* @__PURE__ */ new Set();
|
|
4022
|
-
await
|
|
4023
|
-
const files = await
|
|
4261
|
+
await fs14.ensureDir(targetDir);
|
|
4262
|
+
const files = await fs14.readdir(sourceDir);
|
|
4024
4263
|
let updatedCount = 0;
|
|
4025
4264
|
for (const file of files) {
|
|
4026
|
-
const sourcePath =
|
|
4027
|
-
const targetPath =
|
|
4028
|
-
const stat = await
|
|
4265
|
+
const sourcePath = path18.join(sourceDir, file);
|
|
4266
|
+
const targetPath = path18.join(targetDir, file);
|
|
4267
|
+
const stat = await fs14.stat(sourcePath);
|
|
4029
4268
|
if (stat.isFile()) {
|
|
4030
4269
|
if (protectedFiles.has(file)) {
|
|
4031
4270
|
continue;
|
|
4032
4271
|
}
|
|
4033
|
-
let sourceContent = await
|
|
4272
|
+
let sourceContent = await fs14.readFile(sourcePath, "utf-8");
|
|
4034
4273
|
if (replacements) {
|
|
4035
4274
|
sourceContent = applyReplacements(sourceContent, replacements);
|
|
4036
4275
|
}
|
|
4037
4276
|
let shouldUpdate = true;
|
|
4038
|
-
if (await
|
|
4039
|
-
const targetContent = await
|
|
4277
|
+
if (await fs14.pathExists(targetPath)) {
|
|
4278
|
+
const targetContent = await fs14.readFile(targetPath, "utf-8");
|
|
4040
4279
|
if (sourceContent === targetContent) {
|
|
4041
4280
|
continue;
|
|
4042
4281
|
}
|
|
@@ -4050,7 +4289,7 @@ async function updateFolder(sourceDir, targetDir, force, replacements, lang = DE
|
|
|
4050
4289
|
}
|
|
4051
4290
|
}
|
|
4052
4291
|
if (shouldUpdate) {
|
|
4053
|
-
await
|
|
4292
|
+
await fs14.writeFile(targetPath, sourceContent);
|
|
4054
4293
|
console.log(
|
|
4055
4294
|
chalk6.gray(` \u{1F4C4} ${tr(lang, "cli", "update.fileUpdated", { file })}`)
|
|
4056
4295
|
);
|
|
@@ -4107,7 +4346,7 @@ function extractPorcelainPaths(line) {
|
|
|
4107
4346
|
function getDocsPorcelainStatus(docsDir, ignoredAbsPaths = []) {
|
|
4108
4347
|
const top = getGitTopLevel2(docsDir);
|
|
4109
4348
|
if (!top) return null;
|
|
4110
|
-
const rel =
|
|
4349
|
+
const rel = path18.relative(top, docsDir) || ".";
|
|
4111
4350
|
try {
|
|
4112
4351
|
const output = execFileSync("git", ["status", "--porcelain=v1", "--", rel], {
|
|
4113
4352
|
cwd: top,
|
|
@@ -4119,7 +4358,7 @@ function getDocsPorcelainStatus(docsDir, ignoredAbsPaths = []) {
|
|
|
4119
4358
|
}
|
|
4120
4359
|
const ignoredRelPaths = new Set(
|
|
4121
4360
|
ignoredAbsPaths.map(
|
|
4122
|
-
(absPath) => normalizeGitPath2(
|
|
4361
|
+
(absPath) => normalizeGitPath2(path18.relative(top, absPath) || ".")
|
|
4123
4362
|
)
|
|
4124
4363
|
);
|
|
4125
4364
|
const filtered = output.split("\n").filter((line) => {
|
|
@@ -4176,7 +4415,7 @@ ${tr(lang2, "cli", "common.canceled")}`));
|
|
|
4176
4415
|
}
|
|
4177
4416
|
async function runConfig(options) {
|
|
4178
4417
|
const cwd = process.cwd();
|
|
4179
|
-
const targetCwd = options.dir ?
|
|
4418
|
+
const targetCwd = options.dir ? path18.resolve(cwd, options.dir) : cwd;
|
|
4180
4419
|
const config = await getConfig(targetCwd);
|
|
4181
4420
|
if (!config) {
|
|
4182
4421
|
throw createCliError(
|
|
@@ -4184,7 +4423,7 @@ async function runConfig(options) {
|
|
|
4184
4423
|
tr(DEFAULT_LANG, "cli", "common.configNotFound")
|
|
4185
4424
|
);
|
|
4186
4425
|
}
|
|
4187
|
-
const configPath =
|
|
4426
|
+
const configPath = path18.join(config.docsDir, ".lee-spec-kit.json");
|
|
4188
4427
|
if (!options.projectRoot) {
|
|
4189
4428
|
console.log();
|
|
4190
4429
|
console.log(chalk6.blue(tr(config.lang, "cli", "config.currentTitle")));
|
|
@@ -4195,7 +4434,7 @@ async function runConfig(options) {
|
|
|
4195
4434
|
)
|
|
4196
4435
|
);
|
|
4197
4436
|
console.log();
|
|
4198
|
-
const configFile = await
|
|
4437
|
+
const configFile = await fs14.readJson(configPath);
|
|
4199
4438
|
console.log(JSON.stringify(configFile, null, 2));
|
|
4200
4439
|
console.log();
|
|
4201
4440
|
return;
|
|
@@ -4203,7 +4442,7 @@ async function runConfig(options) {
|
|
|
4203
4442
|
await withFileLock(
|
|
4204
4443
|
getDocsLockPath(config.docsDir),
|
|
4205
4444
|
async () => {
|
|
4206
|
-
const configFile = await
|
|
4445
|
+
const configFile = await fs14.readJson(configPath);
|
|
4207
4446
|
if (configFile.docsRepo !== "standalone") {
|
|
4208
4447
|
console.log(
|
|
4209
4448
|
chalk6.yellow(tr(config.lang, "cli", "config.projectRootStandaloneOnly"))
|
|
@@ -4282,7 +4521,7 @@ async function runConfig(options) {
|
|
|
4282
4521
|
)
|
|
4283
4522
|
);
|
|
4284
4523
|
}
|
|
4285
|
-
await
|
|
4524
|
+
await fs14.writeJson(configPath, configFile, { spaces: 2 });
|
|
4286
4525
|
console.log();
|
|
4287
4526
|
},
|
|
4288
4527
|
{ owner: "config" }
|
|
@@ -4536,42 +4775,42 @@ var BUILTIN_DOC_DEFINITIONS = [
|
|
|
4536
4775
|
{
|
|
4537
4776
|
id: "agents",
|
|
4538
4777
|
title: { ko: "\uC5D0\uC774\uC804\uD2B8 \uC6B4\uC601 \uADDC\uCE59", en: "Agent Operating Rules" },
|
|
4539
|
-
relativePath: (projectType, lang) =>
|
|
4778
|
+
relativePath: (projectType, lang) => path18.join(lang, toTemplateProjectType(projectType), "agents", "agents.md")
|
|
4540
4779
|
},
|
|
4541
4780
|
{
|
|
4542
4781
|
id: "git-workflow",
|
|
4543
4782
|
title: { ko: "Git \uC6CC\uD06C\uD50C\uB85C\uC6B0", en: "Git Workflow" },
|
|
4544
|
-
relativePath: (_, lang) =>
|
|
4783
|
+
relativePath: (_, lang) => path18.join(lang, "common", "agents", "git-workflow.md")
|
|
4545
4784
|
},
|
|
4546
4785
|
{
|
|
4547
4786
|
id: "issue-template",
|
|
4548
4787
|
title: { ko: "Issue \uD15C\uD50C\uB9BF", en: "Issue Template" },
|
|
4549
|
-
relativePath: (_, lang) =>
|
|
4788
|
+
relativePath: (_, lang) => path18.join(lang, "common", "agents", "issue-template.md")
|
|
4550
4789
|
},
|
|
4551
4790
|
{
|
|
4552
4791
|
id: "pr-template",
|
|
4553
4792
|
title: { ko: "PR \uD15C\uD50C\uB9BF", en: "PR Template" },
|
|
4554
|
-
relativePath: (_, lang) =>
|
|
4793
|
+
relativePath: (_, lang) => path18.join(lang, "common", "agents", "pr-template.md")
|
|
4555
4794
|
},
|
|
4556
4795
|
{
|
|
4557
4796
|
id: "create-feature",
|
|
4558
4797
|
title: { ko: "create-feature \uC2A4\uD0AC", en: "create-feature skill" },
|
|
4559
|
-
relativePath: (_, lang) =>
|
|
4798
|
+
relativePath: (_, lang) => path18.join(lang, "common", "agents", "skills", "create-feature.md")
|
|
4560
4799
|
},
|
|
4561
4800
|
{
|
|
4562
4801
|
id: "execute-task",
|
|
4563
4802
|
title: { ko: "execute-task \uC2A4\uD0AC", en: "execute-task skill" },
|
|
4564
|
-
relativePath: (_, lang) =>
|
|
4803
|
+
relativePath: (_, lang) => path18.join(lang, "common", "agents", "skills", "execute-task.md")
|
|
4565
4804
|
},
|
|
4566
4805
|
{
|
|
4567
4806
|
id: "create-issue",
|
|
4568
4807
|
title: { ko: "create-issue \uC2A4\uD0AC", en: "create-issue skill" },
|
|
4569
|
-
relativePath: (_, lang) =>
|
|
4808
|
+
relativePath: (_, lang) => path18.join(lang, "common", "agents", "skills", "create-issue.md")
|
|
4570
4809
|
},
|
|
4571
4810
|
{
|
|
4572
4811
|
id: "create-pr",
|
|
4573
4812
|
title: { ko: "create-pr \uC2A4\uD0AC", en: "create-pr skill" },
|
|
4574
|
-
relativePath: (_, lang) =>
|
|
4813
|
+
relativePath: (_, lang) => path18.join(lang, "common", "agents", "skills", "create-pr.md")
|
|
4575
4814
|
}
|
|
4576
4815
|
];
|
|
4577
4816
|
var DOC_FOLLOWUPS = {
|
|
@@ -4655,7 +4894,7 @@ function listBuiltinDocs(projectType, lang) {
|
|
|
4655
4894
|
id: doc.id,
|
|
4656
4895
|
title: doc.title[lang],
|
|
4657
4896
|
relativePath,
|
|
4658
|
-
absolutePath:
|
|
4897
|
+
absolutePath: path18.join(templatesDir, relativePath)
|
|
4659
4898
|
};
|
|
4660
4899
|
});
|
|
4661
4900
|
}
|
|
@@ -4664,7 +4903,7 @@ async function getBuiltinDoc(docId, projectType, lang) {
|
|
|
4664
4903
|
if (!entry) {
|
|
4665
4904
|
throw new Error(`Unknown builtin doc: ${docId}`);
|
|
4666
4905
|
}
|
|
4667
|
-
const content = await
|
|
4906
|
+
const content = await fs14.readFile(entry.absolutePath, "utf-8");
|
|
4668
4907
|
const hash = createHash("sha256").update(content).digest("hex").slice(0, 12);
|
|
4669
4908
|
return {
|
|
4670
4909
|
entry,
|
|
@@ -4749,7 +4988,7 @@ function getCommandExecutionLockPath(action, config) {
|
|
|
4749
4988
|
if (action.scope === "docs") {
|
|
4750
4989
|
return getDocsLockPath(config.docsDir);
|
|
4751
4990
|
}
|
|
4752
|
-
return
|
|
4991
|
+
return getProjectExecutionLockPath(action.cwd);
|
|
4753
4992
|
}
|
|
4754
4993
|
function contextCommand(program2) {
|
|
4755
4994
|
program2.command("context [feature-name]").description("Show current feature context and next actions").option("--json", "Output in JSON format for agents").option("--repo <repo>", "Component name for multi projects").option("--component <component>", "Component name for multi projects").option("--all", "Include completed features when auto-detecting").option("--done", "Show completed (workflow-done) features only").option("--approve <reply>", "Approve one labeled option: A or A OK").option("--execute", "Execute approved option when it is a command").option(
|
|
@@ -5143,7 +5382,7 @@ async function runContext(featureName, options) {
|
|
|
5143
5382
|
if (f.issueNumber) {
|
|
5144
5383
|
console.log(` \u2022 Issue: #${f.issueNumber}`);
|
|
5145
5384
|
}
|
|
5146
|
-
console.log(` \u2022 Path: ${
|
|
5385
|
+
console.log(` \u2022 Path: ${path.relative(cwd, f.path)}`);
|
|
5147
5386
|
if (f.git.projectBranch) {
|
|
5148
5387
|
console.log(` \u2022 Project Branch: ${f.git.projectBranch}`);
|
|
5149
5388
|
}
|
|
@@ -5397,7 +5636,7 @@ var FIXABLE_ISSUE_CODES = /* @__PURE__ */ new Set([
|
|
|
5397
5636
|
]);
|
|
5398
5637
|
function formatPath(cwd, p) {
|
|
5399
5638
|
if (!p) return "";
|
|
5400
|
-
return
|
|
5639
|
+
return path18.isAbsolute(p) ? path18.relative(cwd, p) : p;
|
|
5401
5640
|
}
|
|
5402
5641
|
function detectPlaceholders(content) {
|
|
5403
5642
|
const patterns = [
|
|
@@ -5540,7 +5779,7 @@ async function applyDoctorFixes(config, cwd, features, dryRun) {
|
|
|
5540
5779
|
const placeholderContext = {
|
|
5541
5780
|
projectName: config.projectName,
|
|
5542
5781
|
featureName: f.slug,
|
|
5543
|
-
featurePath: f.docs.featurePathFromDocs ||
|
|
5782
|
+
featurePath: f.docs.featurePathFromDocs || path18.relative(config.docsDir, f.path),
|
|
5544
5783
|
repoType: f.type,
|
|
5545
5784
|
featureNumber
|
|
5546
5785
|
};
|
|
@@ -5550,9 +5789,9 @@ async function applyDoctorFixes(config, cwd, features, dryRun) {
|
|
|
5550
5789
|
"tasks.md"
|
|
5551
5790
|
];
|
|
5552
5791
|
for (const file of files) {
|
|
5553
|
-
const fullPath =
|
|
5554
|
-
if (!await
|
|
5555
|
-
const original = await
|
|
5792
|
+
const fullPath = path18.join(f.path, file);
|
|
5793
|
+
if (!await fs14.pathExists(fullPath)) continue;
|
|
5794
|
+
const original = await fs14.readFile(fullPath, "utf-8");
|
|
5556
5795
|
let next = original;
|
|
5557
5796
|
const changes = [];
|
|
5558
5797
|
const placeholderFix = applyPlaceholderFixes(next, placeholderContext, config.lang);
|
|
@@ -5576,7 +5815,7 @@ async function applyDoctorFixes(config, cwd, features, dryRun) {
|
|
|
5576
5815
|
}
|
|
5577
5816
|
if (next === original) continue;
|
|
5578
5817
|
if (!dryRun) {
|
|
5579
|
-
await
|
|
5818
|
+
await fs14.writeFile(fullPath, next, "utf-8");
|
|
5580
5819
|
}
|
|
5581
5820
|
entries.push({
|
|
5582
5821
|
path: formatPath(cwd, fullPath),
|
|
@@ -5595,8 +5834,8 @@ async function checkDocsStructure(config, cwd) {
|
|
|
5595
5834
|
const issues = [];
|
|
5596
5835
|
const requiredDirs = ["agents", "features", "prd", "designs", "ideas"];
|
|
5597
5836
|
for (const dir of requiredDirs) {
|
|
5598
|
-
const p =
|
|
5599
|
-
if (!await
|
|
5837
|
+
const p = path18.join(config.docsDir, dir);
|
|
5838
|
+
if (!await fs14.pathExists(p)) {
|
|
5600
5839
|
issues.push({
|
|
5601
5840
|
level: "error",
|
|
5602
5841
|
code: "missing_dir",
|
|
@@ -5605,8 +5844,8 @@ async function checkDocsStructure(config, cwd) {
|
|
|
5605
5844
|
});
|
|
5606
5845
|
}
|
|
5607
5846
|
}
|
|
5608
|
-
const configPath =
|
|
5609
|
-
if (!await
|
|
5847
|
+
const configPath = path18.join(config.docsDir, ".lee-spec-kit.json");
|
|
5848
|
+
if (!await fs14.pathExists(configPath)) {
|
|
5610
5849
|
issues.push({
|
|
5611
5850
|
level: "warn",
|
|
5612
5851
|
code: "missing_config",
|
|
@@ -5628,7 +5867,7 @@ async function checkFeatures(config, cwd, features, decisionsPlaceholderMode) {
|
|
|
5628
5867
|
}
|
|
5629
5868
|
const idMap = /* @__PURE__ */ new Map();
|
|
5630
5869
|
for (const f of features) {
|
|
5631
|
-
const rel = f.docs.featurePathFromDocs ||
|
|
5870
|
+
const rel = f.docs.featurePathFromDocs || path18.relative(config.docsDir, f.path);
|
|
5632
5871
|
const id = f.id || "UNKNOWN";
|
|
5633
5872
|
if (!idMap.has(id)) idMap.set(id, []);
|
|
5634
5873
|
idMap.get(id).push(rel);
|
|
@@ -5636,9 +5875,9 @@ async function checkFeatures(config, cwd, features, decisionsPlaceholderMode) {
|
|
|
5636
5875
|
if (!isInitialTemplateState) {
|
|
5637
5876
|
const featureDocs = ["spec.md", "plan.md", "tasks.md"];
|
|
5638
5877
|
for (const file of featureDocs) {
|
|
5639
|
-
const p =
|
|
5640
|
-
if (!await
|
|
5641
|
-
const content = await
|
|
5878
|
+
const p = path18.join(f.path, file);
|
|
5879
|
+
if (!await fs14.pathExists(p)) continue;
|
|
5880
|
+
const content = await fs14.readFile(p, "utf-8");
|
|
5642
5881
|
const placeholders = detectPlaceholders(content);
|
|
5643
5882
|
if (placeholders.length === 0) continue;
|
|
5644
5883
|
issues.push({
|
|
@@ -5651,9 +5890,9 @@ async function checkFeatures(config, cwd, features, decisionsPlaceholderMode) {
|
|
|
5651
5890
|
});
|
|
5652
5891
|
}
|
|
5653
5892
|
if (decisionsPlaceholderMode !== "off") {
|
|
5654
|
-
const decisionsPath =
|
|
5655
|
-
if (await
|
|
5656
|
-
const content = await
|
|
5893
|
+
const decisionsPath = path18.join(f.path, "decisions.md");
|
|
5894
|
+
if (await fs14.pathExists(decisionsPath)) {
|
|
5895
|
+
const content = await fs14.readFile(decisionsPath, "utf-8");
|
|
5657
5896
|
const placeholders = detectPlaceholders(content);
|
|
5658
5897
|
if (placeholders.length > 0) {
|
|
5659
5898
|
issues.push({
|
|
@@ -5680,7 +5919,7 @@ async function checkFeatures(config, cwd, features, decisionsPlaceholderMode) {
|
|
|
5680
5919
|
level: "warn",
|
|
5681
5920
|
code: "spec_status_unset",
|
|
5682
5921
|
message: tr(config.lang, "cli", "doctor.issue.specStatusUnset"),
|
|
5683
|
-
path: formatPath(cwd,
|
|
5922
|
+
path: formatPath(cwd, path18.join(f.path, "spec.md"))
|
|
5684
5923
|
});
|
|
5685
5924
|
}
|
|
5686
5925
|
if (f.docs.planExists && !f.planStatus && !isInitialTemplateState) {
|
|
@@ -5688,7 +5927,7 @@ async function checkFeatures(config, cwd, features, decisionsPlaceholderMode) {
|
|
|
5688
5927
|
level: "warn",
|
|
5689
5928
|
code: "plan_status_unset",
|
|
5690
5929
|
message: tr(config.lang, "cli", "doctor.issue.planStatusUnset"),
|
|
5691
|
-
path: formatPath(cwd,
|
|
5930
|
+
path: formatPath(cwd, path18.join(f.path, "plan.md"))
|
|
5692
5931
|
});
|
|
5693
5932
|
}
|
|
5694
5933
|
if (f.docs.tasksExists && f.tasks.total === 0 && !isInitialTemplateState) {
|
|
@@ -5696,7 +5935,7 @@ async function checkFeatures(config, cwd, features, decisionsPlaceholderMode) {
|
|
|
5696
5935
|
level: "warn",
|
|
5697
5936
|
code: "tasks_empty",
|
|
5698
5937
|
message: tr(config.lang, "cli", "doctor.issue.tasksEmpty"),
|
|
5699
|
-
path: formatPath(cwd,
|
|
5938
|
+
path: formatPath(cwd, path18.join(f.path, "tasks.md"))
|
|
5700
5939
|
});
|
|
5701
5940
|
}
|
|
5702
5941
|
if (f.docs.tasksExists && !f.docs.tasksDocStatusFieldExists && !isInitialTemplateState) {
|
|
@@ -5704,7 +5943,7 @@ async function checkFeatures(config, cwd, features, decisionsPlaceholderMode) {
|
|
|
5704
5943
|
level: "warn",
|
|
5705
5944
|
code: "tasks_doc_status_missing",
|
|
5706
5945
|
message: tr(config.lang, "cli", "doctor.issue.tasksDocStatusMissing"),
|
|
5707
|
-
path: formatPath(cwd,
|
|
5946
|
+
path: formatPath(cwd, path18.join(f.path, "tasks.md"))
|
|
5708
5947
|
});
|
|
5709
5948
|
}
|
|
5710
5949
|
if (f.docs.tasksExists && f.docs.tasksDocStatusFieldExists && !f.tasksDocStatus && !isInitialTemplateState) {
|
|
@@ -5712,7 +5951,7 @@ async function checkFeatures(config, cwd, features, decisionsPlaceholderMode) {
|
|
|
5712
5951
|
level: "warn",
|
|
5713
5952
|
code: "tasks_doc_status_unset",
|
|
5714
5953
|
message: tr(config.lang, "cli", "doctor.issue.tasksDocStatusUnset"),
|
|
5715
|
-
path: formatPath(cwd,
|
|
5954
|
+
path: formatPath(cwd, path18.join(f.path, "tasks.md"))
|
|
5716
5955
|
});
|
|
5717
5956
|
}
|
|
5718
5957
|
}
|
|
@@ -5736,7 +5975,7 @@ async function checkFeatures(config, cwd, features, decisionsPlaceholderMode) {
|
|
|
5736
5975
|
level: "warn",
|
|
5737
5976
|
code: "missing_feature_id",
|
|
5738
5977
|
message: tr(config.lang, "cli", "doctor.issue.missingFeatureId"),
|
|
5739
|
-
path: formatPath(cwd,
|
|
5978
|
+
path: formatPath(cwd, path18.join(config.docsDir, p))
|
|
5740
5979
|
});
|
|
5741
5980
|
}
|
|
5742
5981
|
return issues;
|
|
@@ -5858,7 +6097,7 @@ function doctorCommand(program2) {
|
|
|
5858
6097
|
}
|
|
5859
6098
|
console.log();
|
|
5860
6099
|
console.log(chalk6.bold(tr(lang, "cli", "doctor.title")));
|
|
5861
|
-
console.log(chalk6.gray(`- Docs: ${
|
|
6100
|
+
console.log(chalk6.gray(`- Docs: ${path18.relative(cwd, docsDir)}`));
|
|
5862
6101
|
console.log(chalk6.gray(`- Type: ${projectType}`));
|
|
5863
6102
|
console.log(chalk6.gray(`- Lang: ${lang}`));
|
|
5864
6103
|
console.log();
|
|
@@ -6037,7 +6276,7 @@ async function runView(featureName, options) {
|
|
|
6037
6276
|
}
|
|
6038
6277
|
console.log();
|
|
6039
6278
|
console.log(chalk6.bold("\u{1F4CA} Workflow View"));
|
|
6040
|
-
console.log(chalk6.gray(`- Docs: ${
|
|
6279
|
+
console.log(chalk6.gray(`- Docs: ${path18.relative(cwd, config.docsDir)}`));
|
|
6041
6280
|
console.log(
|
|
6042
6281
|
chalk6.gray(
|
|
6043
6282
|
`- Features: ${state.features.length} (open ${state.openFeatures.length} / done ${state.doneFeatures.length})`
|
|
@@ -6387,40 +6626,40 @@ function tg(lang, key, vars = {}) {
|
|
|
6387
6626
|
}
|
|
6388
6627
|
function detectGithubCliLangSync(cwd) {
|
|
6389
6628
|
const explicitDocsDir = (process.env.LEE_SPEC_KIT_DOCS_DIR || "").trim();
|
|
6390
|
-
const startDirs = [explicitDocsDir ?
|
|
6629
|
+
const startDirs = [explicitDocsDir ? path18.resolve(explicitDocsDir) : "", path18.resolve(cwd)].filter(Boolean);
|
|
6391
6630
|
const scanOrder = [];
|
|
6392
6631
|
const seen = /* @__PURE__ */ new Set();
|
|
6393
6632
|
for (const start of startDirs) {
|
|
6394
6633
|
let current = start;
|
|
6395
6634
|
while (true) {
|
|
6396
|
-
const abs =
|
|
6635
|
+
const abs = path18.resolve(current);
|
|
6397
6636
|
if (!seen.has(abs)) {
|
|
6398
6637
|
scanOrder.push(abs);
|
|
6399
6638
|
seen.add(abs);
|
|
6400
6639
|
}
|
|
6401
|
-
const parent =
|
|
6640
|
+
const parent = path18.dirname(abs);
|
|
6402
6641
|
if (parent === abs) break;
|
|
6403
6642
|
current = parent;
|
|
6404
6643
|
}
|
|
6405
6644
|
}
|
|
6406
6645
|
for (const base of scanOrder) {
|
|
6407
|
-
for (const docsDir of [
|
|
6408
|
-
const configPath =
|
|
6409
|
-
if (
|
|
6646
|
+
for (const docsDir of [path18.join(base, "docs"), base]) {
|
|
6647
|
+
const configPath = path18.join(docsDir, ".lee-spec-kit.json");
|
|
6648
|
+
if (fs14.existsSync(configPath)) {
|
|
6410
6649
|
try {
|
|
6411
|
-
const parsed =
|
|
6650
|
+
const parsed = fs14.readJsonSync(configPath);
|
|
6412
6651
|
if (parsed?.lang === "ko" || parsed?.lang === "en") return parsed.lang;
|
|
6413
6652
|
} catch {
|
|
6414
6653
|
}
|
|
6415
6654
|
}
|
|
6416
|
-
const agentsPath =
|
|
6417
|
-
const featuresPath =
|
|
6418
|
-
if (!
|
|
6655
|
+
const agentsPath = path18.join(docsDir, "agents");
|
|
6656
|
+
const featuresPath = path18.join(docsDir, "features");
|
|
6657
|
+
if (!fs14.existsSync(agentsPath) || !fs14.existsSync(featuresPath)) continue;
|
|
6419
6658
|
for (const probe of ["custom.md", "constitution.md", "agents.md"]) {
|
|
6420
|
-
const file =
|
|
6421
|
-
if (!
|
|
6659
|
+
const file = path18.join(agentsPath, probe);
|
|
6660
|
+
if (!fs14.existsSync(file)) continue;
|
|
6422
6661
|
try {
|
|
6423
|
-
const content =
|
|
6662
|
+
const content = fs14.readFileSync(file, "utf-8");
|
|
6424
6663
|
if (/[가-힣]/.test(content)) return "ko";
|
|
6425
6664
|
} catch {
|
|
6426
6665
|
}
|
|
@@ -6521,7 +6760,7 @@ function ensureSections(body, sections, kind, lang) {
|
|
|
6521
6760
|
}
|
|
6522
6761
|
function ensureDocsExist(docsDir, relativePaths, lang) {
|
|
6523
6762
|
const missing = relativePaths.filter(
|
|
6524
|
-
(relativePath) => !
|
|
6763
|
+
(relativePath) => !fs14.existsSync(path18.join(docsDir, relativePath))
|
|
6525
6764
|
);
|
|
6526
6765
|
if (missing.length > 0) {
|
|
6527
6766
|
throw createCliError(
|
|
@@ -6531,13 +6770,13 @@ function ensureDocsExist(docsDir, relativePaths, lang) {
|
|
|
6531
6770
|
}
|
|
6532
6771
|
}
|
|
6533
6772
|
function buildDefaultBodyFileName(kind, docsDir, component) {
|
|
6534
|
-
const key = `${
|
|
6773
|
+
const key = `${path18.resolve(docsDir)}::${component.trim().toLowerCase()}`;
|
|
6535
6774
|
const digest = createHash("sha1").update(key).digest("hex").slice(0, 12);
|
|
6536
6775
|
return `lee-spec-kit.${digest}.${kind}.md`;
|
|
6537
6776
|
}
|
|
6538
6777
|
function toBodyFilePath(raw, kind, docsDir, component) {
|
|
6539
|
-
const selected = raw?.trim() ||
|
|
6540
|
-
return
|
|
6778
|
+
const selected = raw?.trim() || path18.join(os.tmpdir(), buildDefaultBodyFileName(kind, docsDir, component));
|
|
6779
|
+
return path18.resolve(selected);
|
|
6541
6780
|
}
|
|
6542
6781
|
function toProjectRootDocsPath(relativePathFromDocs) {
|
|
6543
6782
|
const normalized = relativePathFromDocs.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+/, "");
|
|
@@ -7264,10 +7503,10 @@ function insertFieldInGithubIssueSection(content, key, value) {
|
|
|
7264
7503
|
return { content: lines.join("\n"), changed: true };
|
|
7265
7504
|
}
|
|
7266
7505
|
function syncTasksPrMetadata(tasksPath, prUrl, nextStatus, lang) {
|
|
7267
|
-
if (!
|
|
7506
|
+
if (!fs14.existsSync(tasksPath)) {
|
|
7268
7507
|
throw createCliError("DOCS_NOT_FOUND", tg(lang, "tasksNotFound", { path: tasksPath }));
|
|
7269
7508
|
}
|
|
7270
|
-
const original =
|
|
7509
|
+
const original = fs14.readFileSync(tasksPath, "utf-8");
|
|
7271
7510
|
let next = original;
|
|
7272
7511
|
let changed = false;
|
|
7273
7512
|
const prReplaced = replaceListField(next, ["PR", "Pull Request"], prUrl);
|
|
@@ -7291,7 +7530,7 @@ function syncTasksPrMetadata(tasksPath, prUrl, nextStatus, lang) {
|
|
|
7291
7530
|
changed = changed || inserted.changed;
|
|
7292
7531
|
}
|
|
7293
7532
|
if (changed) {
|
|
7294
|
-
|
|
7533
|
+
fs14.writeFileSync(tasksPath, next, "utf-8");
|
|
7295
7534
|
}
|
|
7296
7535
|
return { changed, path: tasksPath };
|
|
7297
7536
|
}
|
|
@@ -7319,7 +7558,7 @@ function ensureCleanWorktree(cwd, lang) {
|
|
|
7319
7558
|
}
|
|
7320
7559
|
}
|
|
7321
7560
|
function commitAndPushPath(cwd, absPath, message, lang) {
|
|
7322
|
-
const relativePath =
|
|
7561
|
+
const relativePath = path18.relative(cwd, absPath) || absPath;
|
|
7323
7562
|
const status = runProcessOrThrow(
|
|
7324
7563
|
"git",
|
|
7325
7564
|
["status", "--porcelain=v1", "--", relativePath],
|
|
@@ -7451,9 +7690,9 @@ function githubCommand(program2) {
|
|
|
7451
7690
|
[paths.specPath, paths.planPath, paths.tasksPath],
|
|
7452
7691
|
config.lang
|
|
7453
7692
|
);
|
|
7454
|
-
const specContent = await
|
|
7455
|
-
const planContent = await
|
|
7456
|
-
const tasksContent = await
|
|
7693
|
+
const specContent = await fs14.readFile(path18.join(config.docsDir, paths.specPath), "utf-8");
|
|
7694
|
+
const planContent = await fs14.readFile(path18.join(config.docsDir, paths.planPath), "utf-8");
|
|
7695
|
+
const tasksContent = await fs14.readFile(path18.join(config.docsDir, paths.tasksPath), "utf-8");
|
|
7457
7696
|
const overview = resolveOverviewFromSpec(specContent, feature, config.lang);
|
|
7458
7697
|
const title = options.title?.trim() || tg(config.lang, "issueDefaultTitle", {
|
|
7459
7698
|
slug: feature.slug,
|
|
@@ -7482,8 +7721,8 @@ function githubCommand(program2) {
|
|
|
7482
7721
|
);
|
|
7483
7722
|
const explicitBodyFile = (options.bodyFile || "").trim();
|
|
7484
7723
|
let body = generatedBody;
|
|
7485
|
-
if (options.create && explicitBodyFile && await
|
|
7486
|
-
body = await
|
|
7724
|
+
if (options.create && explicitBodyFile && await fs14.pathExists(bodyFile)) {
|
|
7725
|
+
body = await fs14.readFile(bodyFile, "utf-8");
|
|
7487
7726
|
ensureSections(
|
|
7488
7727
|
body,
|
|
7489
7728
|
getRequiredIssueSections(config.lang),
|
|
@@ -7491,8 +7730,8 @@ function githubCommand(program2) {
|
|
|
7491
7730
|
config.lang
|
|
7492
7731
|
);
|
|
7493
7732
|
} else {
|
|
7494
|
-
await
|
|
7495
|
-
await
|
|
7733
|
+
await fs14.ensureDir(path18.dirname(bodyFile));
|
|
7734
|
+
await fs14.writeFile(bodyFile, generatedBody, "utf-8");
|
|
7496
7735
|
}
|
|
7497
7736
|
let issueUrl;
|
|
7498
7737
|
if (options.create) {
|
|
@@ -7589,10 +7828,10 @@ function githubCommand(program2) {
|
|
|
7589
7828
|
const labels = parseLabels(options.labels, config.lang);
|
|
7590
7829
|
const paths = getFeatureDocPaths(feature);
|
|
7591
7830
|
ensureDocsExist(config.docsDir, [paths.specPath, paths.tasksPath], config.lang);
|
|
7592
|
-
const specContent = await
|
|
7593
|
-
const planPath =
|
|
7594
|
-
const planContent = await
|
|
7595
|
-
const tasksContent = await
|
|
7831
|
+
const specContent = await fs14.readFile(path18.join(config.docsDir, paths.specPath), "utf-8");
|
|
7832
|
+
const planPath = path18.join(config.docsDir, paths.planPath);
|
|
7833
|
+
const planContent = await fs14.pathExists(planPath) ? await fs14.readFile(planPath, "utf-8") : "";
|
|
7834
|
+
const tasksContent = await fs14.readFile(path18.join(config.docsDir, paths.tasksPath), "utf-8");
|
|
7596
7835
|
const overview = resolveOverviewFromSpec(specContent, feature, config.lang);
|
|
7597
7836
|
const defaultTitle = feature.issueNumber ? tg(config.lang, "prDefaultTitleWithIssue", {
|
|
7598
7837
|
issue: feature.issueNumber,
|
|
@@ -7626,8 +7865,8 @@ function githubCommand(program2) {
|
|
|
7626
7865
|
);
|
|
7627
7866
|
const explicitBodyFile = (options.bodyFile || "").trim();
|
|
7628
7867
|
let body = generatedBody;
|
|
7629
|
-
if (options.create && explicitBodyFile && await
|
|
7630
|
-
body = await
|
|
7868
|
+
if (options.create && explicitBodyFile && await fs14.pathExists(bodyFile)) {
|
|
7869
|
+
body = await fs14.readFile(bodyFile, "utf-8");
|
|
7631
7870
|
ensureSections(
|
|
7632
7871
|
body,
|
|
7633
7872
|
getRequiredPrSections(config.lang),
|
|
@@ -7635,8 +7874,8 @@ function githubCommand(program2) {
|
|
|
7635
7874
|
config.lang
|
|
7636
7875
|
);
|
|
7637
7876
|
} else {
|
|
7638
|
-
await
|
|
7639
|
-
await
|
|
7877
|
+
await fs14.ensureDir(path18.dirname(bodyFile));
|
|
7878
|
+
await fs14.writeFile(bodyFile, generatedBody, "utf-8");
|
|
7640
7879
|
}
|
|
7641
7880
|
const retryCount = toRetryCount(options.retry, config.lang);
|
|
7642
7881
|
let prUrl = options.pr?.trim() || "";
|
|
@@ -7688,7 +7927,7 @@ function githubCommand(program2) {
|
|
|
7688
7927
|
}
|
|
7689
7928
|
if (prUrl && options.syncTasks !== false) {
|
|
7690
7929
|
const synced = syncTasksPrMetadata(
|
|
7691
|
-
|
|
7930
|
+
path18.join(config.docsDir, paths.tasksPath),
|
|
7692
7931
|
prUrl,
|
|
7693
7932
|
"Review",
|
|
7694
7933
|
config.lang
|
|
@@ -7909,7 +8148,7 @@ function docsCommand(program2) {
|
|
|
7909
8148
|
);
|
|
7910
8149
|
return;
|
|
7911
8150
|
}
|
|
7912
|
-
const relativeFromCwd =
|
|
8151
|
+
const relativeFromCwd = path18.relative(process.cwd(), loaded.entry.absolutePath);
|
|
7913
8152
|
console.log();
|
|
7914
8153
|
console.log(chalk6.bold(`\u{1F4C4} ${loaded.entry.id}: ${loaded.entry.title}`));
|
|
7915
8154
|
console.log(
|
|
@@ -7999,13 +8238,13 @@ ${version}
|
|
|
7999
8238
|
}
|
|
8000
8239
|
return `${ascii}${footer}`;
|
|
8001
8240
|
}
|
|
8002
|
-
var CACHE_FILE =
|
|
8241
|
+
var CACHE_FILE = path18.join(os.homedir(), ".lee-spec-kit-version-cache.json");
|
|
8003
8242
|
var CHECK_INTERVAL = 24 * 60 * 60 * 1e3;
|
|
8004
8243
|
function getCurrentVersion() {
|
|
8005
8244
|
try {
|
|
8006
|
-
const packageJsonPath =
|
|
8007
|
-
if (
|
|
8008
|
-
const pkg =
|
|
8245
|
+
const packageJsonPath = path18.join(__dirname$1, "..", "package.json");
|
|
8246
|
+
if (fs14.existsSync(packageJsonPath)) {
|
|
8247
|
+
const pkg = fs14.readJsonSync(packageJsonPath);
|
|
8009
8248
|
return pkg.version;
|
|
8010
8249
|
}
|
|
8011
8250
|
} catch {
|
|
@@ -8014,8 +8253,8 @@ function getCurrentVersion() {
|
|
|
8014
8253
|
}
|
|
8015
8254
|
function readCache() {
|
|
8016
8255
|
try {
|
|
8017
|
-
if (
|
|
8018
|
-
return
|
|
8256
|
+
if (fs14.existsSync(CACHE_FILE)) {
|
|
8257
|
+
return fs14.readJsonSync(CACHE_FILE);
|
|
8019
8258
|
}
|
|
8020
8259
|
} catch {
|
|
8021
8260
|
}
|
|
@@ -8103,9 +8342,9 @@ function shouldCheckForUpdates() {
|
|
|
8103
8342
|
if (shouldCheckForUpdates()) checkForUpdates();
|
|
8104
8343
|
function getCliVersion() {
|
|
8105
8344
|
try {
|
|
8106
|
-
const packageJsonPath =
|
|
8107
|
-
if (
|
|
8108
|
-
const pkg =
|
|
8345
|
+
const packageJsonPath = path18.join(__dirname$1, "..", "package.json");
|
|
8346
|
+
if (fs14.existsSync(packageJsonPath)) {
|
|
8347
|
+
const pkg = fs14.readJsonSync(packageJsonPath);
|
|
8109
8348
|
if (pkg?.version) return String(pkg.version);
|
|
8110
8349
|
}
|
|
8111
8350
|
} catch {
|