lean-spec 0.2.5 → 0.2.6-dev.20251125015611
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/{chunk-7WXYOHZU.js → chunk-RTEGSMVL.js} +1253 -678
- package/dist/chunk-RTEGSMVL.js.map +1 -0
- package/dist/cli.js +7 -1
- package/dist/cli.js.map +1 -1
- package/dist/mcp-server.js +1 -1
- package/package.json +2 -3
- package/templates/detailed/AGENTS.md +113 -0
- package/templates/detailed/README.md +28 -0
- package/templates/detailed/files/DESIGN.md +43 -0
- package/templates/detailed/files/PLAN.md +59 -0
- package/templates/detailed/files/README.md +30 -0
- package/templates/detailed/files/TEST.md +71 -0
- package/templates/examples/api-refactor/README.md +81 -0
- package/templates/examples/api-refactor/package.json +16 -0
- package/templates/examples/api-refactor/src/app.js +40 -0
- package/templates/examples/api-refactor/src/services/currencyService.js +43 -0
- package/templates/examples/api-refactor/src/services/timezoneService.js +41 -0
- package/templates/examples/api-refactor/src/services/weatherService.js +42 -0
- package/templates/examples/dark-theme/README.md +66 -0
- package/templates/examples/dark-theme/package.json +16 -0
- package/templates/examples/dark-theme/src/public/app.js +277 -0
- package/templates/examples/dark-theme/src/public/index.html +225 -0
- package/templates/examples/dark-theme/src/public/style.css +625 -0
- package/templates/examples/dark-theme/src/server.js +18 -0
- package/templates/examples/dashboard-widgets/README.md +70 -0
- package/templates/examples/dashboard-widgets/index.html +12 -0
- package/templates/examples/dashboard-widgets/package.json +22 -0
- package/templates/examples/dashboard-widgets/src/App.css +20 -0
- package/templates/examples/dashboard-widgets/src/App.jsx +16 -0
- package/templates/examples/dashboard-widgets/src/components/Dashboard.css +17 -0
- package/templates/examples/dashboard-widgets/src/components/Dashboard.jsx +15 -0
- package/templates/examples/dashboard-widgets/src/components/WidgetWrapper.css +23 -0
- package/templates/examples/dashboard-widgets/src/components/WidgetWrapper.jsx +16 -0
- package/templates/examples/dashboard-widgets/src/components/widgets/ChartWidget.css +33 -0
- package/templates/examples/dashboard-widgets/src/components/widgets/ChartWidget.jsx +28 -0
- package/templates/examples/dashboard-widgets/src/components/widgets/StatsWidget.css +24 -0
- package/templates/examples/dashboard-widgets/src/components/widgets/StatsWidget.jsx +22 -0
- package/templates/examples/dashboard-widgets/src/index.css +13 -0
- package/templates/examples/dashboard-widgets/src/main.jsx +10 -0
- package/templates/examples/dashboard-widgets/src/utils/mockData.js +30 -0
- package/templates/examples/dashboard-widgets/vite.config.js +6 -0
- package/templates/standard/AGENTS.md +113 -0
- package/templates/standard/README.md +4 -2
- package/dist/chunk-7WXYOHZU.js.map +0 -1
- package/templates/_shared/agents-components/core-rules-base-additions.md +0 -4
- package/templates/_shared/agents-components/core-rules-enterprise-additions.md +0 -4
- package/templates/_shared/agents-components/core-rules-shared.md +0 -1
- package/templates/_shared/agents-components/discovery-commands-enterprise-additions.md +0 -6
- package/templates/_shared/agents-components/discovery-commands-minimal-additions.md +0 -0
- package/templates/_shared/agents-components/discovery-commands-shared.md +0 -8
- package/templates/_shared/agents-components/discovery-commands-standard-additions.md +0 -3
- package/templates/_shared/agents-components/enterprise-approval.md +0 -10
- package/templates/_shared/agents-components/enterprise-compliance.md +0 -12
- package/templates/_shared/agents-components/enterprise-when-required.md +0 -13
- package/templates/_shared/agents-components/essential-commands-enterprise-additions.md +0 -29
- package/templates/_shared/agents-components/essential-commands-minimal-additions.md +0 -1
- package/templates/_shared/agents-components/essential-commands-shared.md +0 -15
- package/templates/_shared/agents-components/essential-commands-standard-additions.md +0 -18
- package/templates/_shared/agents-components/frontmatter-enterprise.md +0 -33
- package/templates/_shared/agents-components/frontmatter-minimal.md +0 -18
- package/templates/_shared/agents-components/frontmatter-standard.md +0 -23
- package/templates/_shared/agents-components/quality-standards-enterprise-additions.md +0 -4
- package/templates/_shared/agents-components/quality-standards-minimal-additions.md +0 -3
- package/templates/_shared/agents-components/quality-standards-shared.md +0 -6
- package/templates/_shared/agents-components/status-update-triggers.md +0 -14
- package/templates/_shared/agents-components/when-to-use-enterprise.md +0 -11
- package/templates/_shared/agents-components/when-to-use-minimal.md +0 -9
- package/templates/_shared/agents-components/when-to-use-standard.md +0 -9
- package/templates/_shared/agents-components/workflow-enterprise.md +0 -11
- package/templates/_shared/agents-components/workflow-standard-detailed.md +0 -10
- package/templates/_shared/agents-components/workflow-standard.md +0 -8
- package/templates/_shared/agents-template.hbs +0 -43
- package/templates/enterprise/README.md +0 -25
- package/templates/enterprise/agents-config.json +0 -16
- package/templates/enterprise/files/AGENTS.md +0 -194
- package/templates/enterprise/spec-template.md +0 -80
- package/templates/minimal/README.md +0 -18
- package/templates/minimal/agents-config.json +0 -13
- package/templates/minimal/config.json +0 -15
- package/templates/minimal/files/AGENTS.md +0 -116
- package/templates/minimal/spec-template.md +0 -25
- package/templates/standard/agents-config.json +0 -13
- package/templates/standard/files/AGENTS.md +0 -142
- /package/templates/{enterprise → detailed}/config.json +0 -0
- /package/templates/standard/{spec-template.md → files/README.md} +0 -0
|
@@ -3,12 +3,12 @@ import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mc
|
|
|
3
3
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
4
|
import { readFileSync, existsSync } from 'fs';
|
|
5
5
|
import { fileURLToPath } from 'url';
|
|
6
|
-
import * as
|
|
6
|
+
import * as path15 from 'path';
|
|
7
7
|
import { dirname, join, resolve } from 'path';
|
|
8
8
|
import { z } from 'zod';
|
|
9
9
|
import * as fs9 from 'fs/promises';
|
|
10
10
|
import { readFile, writeFile } from 'fs/promises';
|
|
11
|
-
import
|
|
11
|
+
import chalk19 from 'chalk';
|
|
12
12
|
import matter2 from 'gray-matter';
|
|
13
13
|
import yaml2 from 'js-yaml';
|
|
14
14
|
import { Command } from 'commander';
|
|
@@ -16,7 +16,6 @@ import { execSync, spawn } from 'child_process';
|
|
|
16
16
|
import ora from 'ora';
|
|
17
17
|
import stripAnsi from 'strip-ansi';
|
|
18
18
|
import { select } from '@inquirer/prompts';
|
|
19
|
-
import { encoding_for_model } from 'tiktoken';
|
|
20
19
|
import dayjs3 from 'dayjs';
|
|
21
20
|
import { marked } from 'marked';
|
|
22
21
|
import { markedTerminal } from 'marked-terminal';
|
|
@@ -42,7 +41,7 @@ var DEFAULT_CONFIG = {
|
|
|
42
41
|
}
|
|
43
42
|
};
|
|
44
43
|
async function loadConfig(cwd = process.cwd()) {
|
|
45
|
-
const configPath =
|
|
44
|
+
const configPath = path15.join(cwd, ".lean-spec", "config.json");
|
|
46
45
|
try {
|
|
47
46
|
const content = await fs9.readFile(configPath, "utf-8");
|
|
48
47
|
const userConfig = JSON.parse(content);
|
|
@@ -54,8 +53,8 @@ async function loadConfig(cwd = process.cwd()) {
|
|
|
54
53
|
}
|
|
55
54
|
}
|
|
56
55
|
async function saveConfig(config, cwd = process.cwd()) {
|
|
57
|
-
const configDir =
|
|
58
|
-
const configPath =
|
|
56
|
+
const configDir = path15.join(cwd, ".lean-spec");
|
|
57
|
+
const configPath = path15.join(configDir, "config.json");
|
|
59
58
|
await fs9.mkdir(configDir, { recursive: true });
|
|
60
59
|
await fs9.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
|
|
61
60
|
}
|
|
@@ -154,7 +153,7 @@ async function getGlobalNextSeq(specsDir, digits) {
|
|
|
154
153
|
}
|
|
155
154
|
}
|
|
156
155
|
if (entry.name === "archived") continue;
|
|
157
|
-
const subDir =
|
|
156
|
+
const subDir = path15.join(dir, entry.name);
|
|
158
157
|
await scanDirectory(subDir);
|
|
159
158
|
}
|
|
160
159
|
} catch {
|
|
@@ -171,7 +170,7 @@ async function getGlobalNextSeq(specsDir, digits) {
|
|
|
171
170
|
}
|
|
172
171
|
}
|
|
173
172
|
async function resolveSpecPath(specPath, cwd, specsDir) {
|
|
174
|
-
if (
|
|
173
|
+
if (path15.isAbsolute(specPath)) {
|
|
175
174
|
try {
|
|
176
175
|
await fs9.access(specPath);
|
|
177
176
|
return specPath;
|
|
@@ -179,13 +178,13 @@ async function resolveSpecPath(specPath, cwd, specsDir) {
|
|
|
179
178
|
return null;
|
|
180
179
|
}
|
|
181
180
|
}
|
|
182
|
-
const cwdPath =
|
|
181
|
+
const cwdPath = path15.resolve(cwd, specPath);
|
|
183
182
|
try {
|
|
184
183
|
await fs9.access(cwdPath);
|
|
185
184
|
return cwdPath;
|
|
186
185
|
} catch {
|
|
187
186
|
}
|
|
188
|
-
const specsPath =
|
|
187
|
+
const specsPath = path15.join(specsDir, specPath);
|
|
189
188
|
try {
|
|
190
189
|
await fs9.access(specsPath);
|
|
191
190
|
return specsPath;
|
|
@@ -212,10 +211,10 @@ async function searchBySequence(specsDir, seqNum) {
|
|
|
212
211
|
if (match) {
|
|
213
212
|
const entrySeq = parseInt(match[1], 10);
|
|
214
213
|
if (entrySeq === seqNum) {
|
|
215
|
-
return
|
|
214
|
+
return path15.join(dir, entry.name);
|
|
216
215
|
}
|
|
217
216
|
}
|
|
218
|
-
const subDir =
|
|
217
|
+
const subDir = path15.join(dir, entry.name);
|
|
219
218
|
const result = await scanDirectory(subDir);
|
|
220
219
|
if (result) return result;
|
|
221
220
|
}
|
|
@@ -232,9 +231,9 @@ async function searchInAllDirectories(specsDir, specName) {
|
|
|
232
231
|
for (const entry of entries) {
|
|
233
232
|
if (!entry.isDirectory()) continue;
|
|
234
233
|
if (entry.name === specName) {
|
|
235
|
-
return
|
|
234
|
+
return path15.join(dir, entry.name);
|
|
236
235
|
}
|
|
237
|
-
const subDir =
|
|
236
|
+
const subDir = path15.join(dir, entry.name);
|
|
238
237
|
const result = await scanDirectory(subDir);
|
|
239
238
|
if (result) return result;
|
|
240
239
|
}
|
|
@@ -265,7 +264,7 @@ async function getGitInfo() {
|
|
|
265
264
|
}
|
|
266
265
|
async function getProjectName(cwd = process.cwd()) {
|
|
267
266
|
try {
|
|
268
|
-
const packageJsonPath =
|
|
267
|
+
const packageJsonPath = path15.join(cwd, "package.json");
|
|
269
268
|
const content = await fs9.readFile(packageJsonPath, "utf-8");
|
|
270
269
|
const packageJson2 = JSON.parse(content);
|
|
271
270
|
return packageJson2.name || null;
|
|
@@ -354,9 +353,9 @@ async function loadSubFiles(specDir, options = {}) {
|
|
|
354
353
|
for (const entry of entries) {
|
|
355
354
|
if (entry.name === "README.md") continue;
|
|
356
355
|
if (entry.isDirectory()) continue;
|
|
357
|
-
const filePath =
|
|
356
|
+
const filePath = path15.join(specDir, entry.name);
|
|
358
357
|
const stat6 = await fs9.stat(filePath);
|
|
359
|
-
const ext =
|
|
358
|
+
const ext = path15.extname(entry.name).toLowerCase();
|
|
360
359
|
const isDocument = ext === ".md";
|
|
361
360
|
const subFile = {
|
|
362
361
|
name: entry.name,
|
|
@@ -382,7 +381,7 @@ async function loadSubFiles(specDir, options = {}) {
|
|
|
382
381
|
async function loadAllSpecs(options = {}) {
|
|
383
382
|
const config = await loadConfig();
|
|
384
383
|
const cwd = process.cwd();
|
|
385
|
-
const specsDir =
|
|
384
|
+
const specsDir = path15.join(cwd, config.specsDir);
|
|
386
385
|
const specs = [];
|
|
387
386
|
try {
|
|
388
387
|
await fs9.access(specsDir);
|
|
@@ -396,7 +395,7 @@ async function loadAllSpecs(options = {}) {
|
|
|
396
395
|
for (const entry of entries) {
|
|
397
396
|
if (!entry.isDirectory()) continue;
|
|
398
397
|
if (entry.name === "archived" && relativePath === "") continue;
|
|
399
|
-
const entryPath =
|
|
398
|
+
const entryPath = path15.join(dir, entry.name);
|
|
400
399
|
const entryRelativePath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
|
|
401
400
|
if (specPattern.test(entry.name)) {
|
|
402
401
|
const specFile = await getSpecFile(entryPath, config.structure.defaultFile);
|
|
@@ -445,7 +444,7 @@ async function loadAllSpecs(options = {}) {
|
|
|
445
444
|
}
|
|
446
445
|
await loadSpecsFromDir(specsDir);
|
|
447
446
|
if (options.includeArchived) {
|
|
448
|
-
const archivedPath =
|
|
447
|
+
const archivedPath = path15.join(specsDir, "archived");
|
|
449
448
|
await loadSpecsFromDir(archivedPath, "archived");
|
|
450
449
|
}
|
|
451
450
|
const sortBy = options.sortBy || "id";
|
|
@@ -493,12 +492,12 @@ async function loadAllSpecs(options = {}) {
|
|
|
493
492
|
async function getSpec(specPath) {
|
|
494
493
|
const config = await loadConfig();
|
|
495
494
|
const cwd = process.cwd();
|
|
496
|
-
const specsDir =
|
|
495
|
+
const specsDir = path15.join(cwd, config.specsDir);
|
|
497
496
|
let fullPath;
|
|
498
|
-
if (
|
|
497
|
+
if (path15.isAbsolute(specPath)) {
|
|
499
498
|
fullPath = specPath;
|
|
500
499
|
} else {
|
|
501
|
-
fullPath =
|
|
500
|
+
fullPath = path15.join(specsDir, specPath);
|
|
502
501
|
}
|
|
503
502
|
try {
|
|
504
503
|
await fs9.access(fullPath);
|
|
@@ -510,8 +509,8 @@ async function getSpec(specPath) {
|
|
|
510
509
|
const frontmatter = await parseFrontmatter(specFile, config);
|
|
511
510
|
if (!frontmatter) return null;
|
|
512
511
|
const content = await fs9.readFile(specFile, "utf-8");
|
|
513
|
-
const relativePath =
|
|
514
|
-
const parts = relativePath.split(
|
|
512
|
+
const relativePath = path15.relative(specsDir, fullPath);
|
|
513
|
+
const parts = relativePath.split(path15.sep);
|
|
515
514
|
const date = parts[0] === "archived" ? parts[1] : parts[0];
|
|
516
515
|
const name = parts[parts.length - 1];
|
|
517
516
|
return {
|
|
@@ -551,7 +550,7 @@ async function withSpinner(text, fn, options) {
|
|
|
551
550
|
|
|
552
551
|
// src/commands/check.ts
|
|
553
552
|
function checkCommand() {
|
|
554
|
-
return new Command("check").description("Check for sequence conflicts").option("-q, --quiet", "Brief output").action(async (options) => {
|
|
553
|
+
return new Command("check").description("Check for sequence conflicts").option("-q, --quiet", "Brief output").option("--json", "Output as JSON").action(async (options) => {
|
|
555
554
|
const hasNoConflicts = await checkSpecs(options);
|
|
556
555
|
process.exit(hasNoConflicts ? 0 : 1);
|
|
557
556
|
});
|
|
@@ -559,12 +558,12 @@ function checkCommand() {
|
|
|
559
558
|
async function checkSpecs(options = {}) {
|
|
560
559
|
const config = await loadConfig();
|
|
561
560
|
const cwd = process.cwd();
|
|
562
|
-
|
|
561
|
+
path15.join(cwd, config.specsDir);
|
|
563
562
|
const specs = await loadAllSpecs();
|
|
564
563
|
const sequenceMap = /* @__PURE__ */ new Map();
|
|
565
564
|
const specPattern = createSpecDirPattern();
|
|
566
565
|
for (const spec of specs) {
|
|
567
|
-
const specName =
|
|
566
|
+
const specName = path15.basename(spec.path);
|
|
568
567
|
const match = specName.match(specPattern);
|
|
569
568
|
if (match) {
|
|
570
569
|
const seq = parseInt(match[1], 10);
|
|
@@ -579,30 +578,45 @@ async function checkSpecs(options = {}) {
|
|
|
579
578
|
const conflicts = Array.from(sequenceMap.entries()).filter(([_, paths]) => paths.length > 1).sort(([a], [b]) => a - b);
|
|
580
579
|
if (conflicts.length === 0) {
|
|
581
580
|
if (!options.quiet && !options.silent) {
|
|
582
|
-
|
|
581
|
+
if (options.json) {
|
|
582
|
+
console.log(JSON.stringify({ conflicts: [], hasConflicts: false }, null, 2));
|
|
583
|
+
} else {
|
|
584
|
+
console.log(chalk19.green("\u2713 No sequence conflicts detected"));
|
|
585
|
+
}
|
|
583
586
|
}
|
|
584
587
|
return true;
|
|
585
588
|
}
|
|
589
|
+
if (options.json) {
|
|
590
|
+
const jsonOutput = {
|
|
591
|
+
hasConflicts: true,
|
|
592
|
+
conflicts: conflicts.map(([seq, paths]) => ({
|
|
593
|
+
sequence: seq,
|
|
594
|
+
specs: paths
|
|
595
|
+
}))
|
|
596
|
+
};
|
|
597
|
+
console.log(JSON.stringify(jsonOutput, null, 2));
|
|
598
|
+
return false;
|
|
599
|
+
}
|
|
586
600
|
if (!options.silent) {
|
|
587
601
|
if (!options.quiet) {
|
|
588
602
|
console.log("");
|
|
589
|
-
console.log(
|
|
603
|
+
console.log(chalk19.yellow("\u26A0\uFE0F Sequence conflicts detected:\n"));
|
|
590
604
|
for (const [seq, paths] of conflicts) {
|
|
591
|
-
console.log(
|
|
605
|
+
console.log(chalk19.red(` Sequence ${String(seq).padStart(config.structure.sequenceDigits, "0")}:`));
|
|
592
606
|
for (const p of paths) {
|
|
593
|
-
console.log(
|
|
607
|
+
console.log(chalk19.gray(` - ${sanitizeUserInput(p)}`));
|
|
594
608
|
}
|
|
595
609
|
console.log("");
|
|
596
610
|
}
|
|
597
|
-
console.log(
|
|
598
|
-
console.log(
|
|
611
|
+
console.log(chalk19.cyan("Tip: Use date prefix to prevent conflicts:"));
|
|
612
|
+
console.log(chalk19.gray(' Edit .lean-spec/config.json \u2192 structure.prefix: "{YYYYMMDD}-"'));
|
|
599
613
|
console.log("");
|
|
600
|
-
console.log(
|
|
614
|
+
console.log(chalk19.cyan("Or rename folders manually to resolve."));
|
|
601
615
|
console.log("");
|
|
602
616
|
} else {
|
|
603
617
|
console.log("");
|
|
604
|
-
console.log(
|
|
605
|
-
console.log(
|
|
618
|
+
console.log(chalk19.yellow(`\u26A0\uFE0F Conflict warning: ${conflicts.length} sequence conflict(s) detected`));
|
|
619
|
+
console.log(chalk19.gray("Run: lean-spec check"));
|
|
606
620
|
console.log("");
|
|
607
621
|
}
|
|
608
622
|
}
|
|
@@ -655,7 +669,7 @@ function createCommand() {
|
|
|
655
669
|
async function createSpec(name, options = {}) {
|
|
656
670
|
const config = await loadConfig();
|
|
657
671
|
const cwd = process.cwd();
|
|
658
|
-
const specsDir =
|
|
672
|
+
const specsDir = path15.join(cwd, config.specsDir);
|
|
659
673
|
await fs9.mkdir(specsDir, { recursive: true });
|
|
660
674
|
const seq = await getGlobalNextSeq(specsDir, config.structure.sequenceDigits);
|
|
661
675
|
let specRelativePath;
|
|
@@ -676,8 +690,8 @@ async function createSpec(name, options = {}) {
|
|
|
676
690
|
} else {
|
|
677
691
|
throw new Error(`Unknown pattern: ${config.structure.pattern}`);
|
|
678
692
|
}
|
|
679
|
-
const specDir =
|
|
680
|
-
const specFile =
|
|
693
|
+
const specDir = path15.join(specsDir, specRelativePath);
|
|
694
|
+
const specFile = path15.join(specDir, config.structure.defaultFile);
|
|
681
695
|
try {
|
|
682
696
|
await fs9.access(specDir);
|
|
683
697
|
throw new Error(`Spec already exists: ${sanitizeUserInput(specDir)}`);
|
|
@@ -687,7 +701,7 @@ async function createSpec(name, options = {}) {
|
|
|
687
701
|
}
|
|
688
702
|
}
|
|
689
703
|
await fs9.mkdir(specDir, { recursive: true });
|
|
690
|
-
const templatesDir =
|
|
704
|
+
const templatesDir = path15.join(cwd, ".lean-spec", "templates");
|
|
691
705
|
let templateName;
|
|
692
706
|
if (options.template) {
|
|
693
707
|
if (config.templates?.[options.template]) {
|
|
@@ -699,13 +713,33 @@ async function createSpec(name, options = {}) {
|
|
|
699
713
|
} else {
|
|
700
714
|
templateName = config.template || "spec-template.md";
|
|
701
715
|
}
|
|
702
|
-
|
|
716
|
+
let templatePath = path15.join(templatesDir, templateName);
|
|
717
|
+
try {
|
|
718
|
+
await fs9.access(templatePath);
|
|
719
|
+
} catch {
|
|
720
|
+
const legacyPath = path15.join(templatesDir, "spec-template.md");
|
|
721
|
+
try {
|
|
722
|
+
await fs9.access(legacyPath);
|
|
723
|
+
templatePath = legacyPath;
|
|
724
|
+
templateName = "spec-template.md";
|
|
725
|
+
} catch {
|
|
726
|
+
const readmePath = path15.join(templatesDir, "README.md");
|
|
727
|
+
try {
|
|
728
|
+
await fs9.access(readmePath);
|
|
729
|
+
templatePath = readmePath;
|
|
730
|
+
templateName = "README.md";
|
|
731
|
+
} catch {
|
|
732
|
+
throw new Error(`Template not found: ${templatePath}. Run: lean-spec init`);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
}
|
|
703
736
|
let content;
|
|
737
|
+
let varContext;
|
|
704
738
|
try {
|
|
705
739
|
const template = await fs9.readFile(templatePath, "utf-8");
|
|
706
740
|
const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
707
741
|
const title = options.title || name;
|
|
708
|
-
|
|
742
|
+
varContext = await buildVariableContext(config, { name: title, date });
|
|
709
743
|
content = resolveVariables(template, varContext);
|
|
710
744
|
const parsed = matter2(content, {
|
|
711
745
|
engines: {
|
|
@@ -747,8 +781,29 @@ ${options.description}`
|
|
|
747
781
|
throw new Error(`Template not found: ${templatePath}. Run: lean-spec init`);
|
|
748
782
|
}
|
|
749
783
|
await fs9.writeFile(specFile, content, "utf-8");
|
|
750
|
-
|
|
751
|
-
|
|
784
|
+
try {
|
|
785
|
+
const templateFiles = await fs9.readdir(templatesDir);
|
|
786
|
+
const additionalFiles = templateFiles.filter(
|
|
787
|
+
(f) => f.endsWith(".md") && f !== templateName && f !== "spec-template.md" && f !== config.structure.defaultFile
|
|
788
|
+
);
|
|
789
|
+
if (additionalFiles.length > 0) {
|
|
790
|
+
for (const file of additionalFiles) {
|
|
791
|
+
const srcPath = path15.join(templatesDir, file);
|
|
792
|
+
const destPath = path15.join(specDir, file);
|
|
793
|
+
let fileContent = await fs9.readFile(srcPath, "utf-8");
|
|
794
|
+
fileContent = resolveVariables(fileContent, varContext);
|
|
795
|
+
await fs9.writeFile(destPath, fileContent, "utf-8");
|
|
796
|
+
}
|
|
797
|
+
console.log(chalk19.green(`\u2713 Created: ${sanitizeUserInput(specDir)}/`));
|
|
798
|
+
console.log(chalk19.gray(` Files: ${config.structure.defaultFile}, ${additionalFiles.join(", ")}`));
|
|
799
|
+
} else {
|
|
800
|
+
console.log(chalk19.green(`\u2713 Created: ${sanitizeUserInput(specDir)}/`));
|
|
801
|
+
console.log(chalk19.gray(` Edit: ${sanitizeUserInput(specFile)}`));
|
|
802
|
+
}
|
|
803
|
+
} catch (error) {
|
|
804
|
+
console.log(chalk19.green(`\u2713 Created: ${sanitizeUserInput(specDir)}/`));
|
|
805
|
+
console.log(chalk19.gray(` Edit: ${sanitizeUserInput(specFile)}`));
|
|
806
|
+
}
|
|
752
807
|
await autoCheckIfEnabled();
|
|
753
808
|
}
|
|
754
809
|
function archiveCommand() {
|
|
@@ -760,7 +815,7 @@ async function archiveSpec(specPath) {
|
|
|
760
815
|
await autoCheckIfEnabled();
|
|
761
816
|
const config = await loadConfig();
|
|
762
817
|
const cwd = process.cwd();
|
|
763
|
-
const specsDir =
|
|
818
|
+
const specsDir = path15.join(cwd, config.specsDir);
|
|
764
819
|
const resolvedPath = await resolveSpecPath(specPath, cwd, specsDir);
|
|
765
820
|
if (!resolvedPath) {
|
|
766
821
|
throw new Error(`Spec not found: ${sanitizeUserInput(specPath)}`);
|
|
@@ -769,12 +824,12 @@ async function archiveSpec(specPath) {
|
|
|
769
824
|
if (specFile) {
|
|
770
825
|
await updateFrontmatter(specFile, { status: "archived" });
|
|
771
826
|
}
|
|
772
|
-
const archiveDir =
|
|
827
|
+
const archiveDir = path15.join(specsDir, "archived");
|
|
773
828
|
await fs9.mkdir(archiveDir, { recursive: true });
|
|
774
|
-
const specName =
|
|
775
|
-
const archivePath =
|
|
829
|
+
const specName = path15.basename(resolvedPath);
|
|
830
|
+
const archivePath = path15.join(archiveDir, specName);
|
|
776
831
|
await fs9.rename(resolvedPath, archivePath);
|
|
777
|
-
console.log(
|
|
832
|
+
console.log(chalk19.green(`\u2713 Archived: ${sanitizeUserInput(archivePath)}`));
|
|
778
833
|
}
|
|
779
834
|
|
|
780
835
|
// src/utils/pattern-detection.ts
|
|
@@ -804,59 +859,59 @@ var STATUS_CONFIG = {
|
|
|
804
859
|
planned: {
|
|
805
860
|
emoji: "\u{1F4C5}",
|
|
806
861
|
label: "Planned",
|
|
807
|
-
colorFn:
|
|
808
|
-
badge: (s = "planned") =>
|
|
862
|
+
colorFn: chalk19.blue,
|
|
863
|
+
badge: (s = "planned") => chalk19.blue(`[${s}]`)
|
|
809
864
|
},
|
|
810
865
|
"in-progress": {
|
|
811
866
|
emoji: "\u23F3",
|
|
812
867
|
label: "In Progress",
|
|
813
|
-
colorFn:
|
|
814
|
-
badge: (s = "in-progress") =>
|
|
868
|
+
colorFn: chalk19.yellow,
|
|
869
|
+
badge: (s = "in-progress") => chalk19.yellow(`[${s}]`)
|
|
815
870
|
},
|
|
816
871
|
complete: {
|
|
817
872
|
emoji: "\u2705",
|
|
818
873
|
label: "Complete",
|
|
819
|
-
colorFn:
|
|
820
|
-
badge: (s = "complete") =>
|
|
874
|
+
colorFn: chalk19.green,
|
|
875
|
+
badge: (s = "complete") => chalk19.green(`[${s}]`)
|
|
821
876
|
},
|
|
822
877
|
archived: {
|
|
823
878
|
emoji: "\u{1F4E6}",
|
|
824
879
|
label: "Archived",
|
|
825
|
-
colorFn:
|
|
826
|
-
badge: (s = "archived") =>
|
|
880
|
+
colorFn: chalk19.gray,
|
|
881
|
+
badge: (s = "archived") => chalk19.gray(`[${s}]`)
|
|
827
882
|
}
|
|
828
883
|
};
|
|
829
884
|
var PRIORITY_CONFIG = {
|
|
830
885
|
critical: {
|
|
831
886
|
emoji: "\u{1F534}",
|
|
832
|
-
colorFn:
|
|
833
|
-
badge: (s = "critical") =>
|
|
887
|
+
colorFn: chalk19.red.bold,
|
|
888
|
+
badge: (s = "critical") => chalk19.red.bold(`[${s}]`)
|
|
834
889
|
},
|
|
835
890
|
high: {
|
|
836
891
|
emoji: "\u{1F7E0}",
|
|
837
|
-
colorFn:
|
|
838
|
-
badge: (s = "high") =>
|
|
892
|
+
colorFn: chalk19.hex("#FFA500"),
|
|
893
|
+
badge: (s = "high") => chalk19.hex("#FFA500")(`[${s}]`)
|
|
839
894
|
},
|
|
840
895
|
medium: {
|
|
841
896
|
emoji: "\u{1F7E1}",
|
|
842
|
-
colorFn:
|
|
843
|
-
badge: (s = "medium") =>
|
|
897
|
+
colorFn: chalk19.yellow,
|
|
898
|
+
badge: (s = "medium") => chalk19.yellow(`[${s}]`)
|
|
844
899
|
},
|
|
845
900
|
low: {
|
|
846
901
|
emoji: "\u{1F7E2}",
|
|
847
|
-
colorFn:
|
|
848
|
-
badge: (s = "low") =>
|
|
902
|
+
colorFn: chalk19.gray,
|
|
903
|
+
badge: (s = "low") => chalk19.gray(`[${s}]`)
|
|
849
904
|
}
|
|
850
905
|
};
|
|
851
906
|
function formatStatusBadge(status) {
|
|
852
|
-
return STATUS_CONFIG[status]?.badge() ||
|
|
907
|
+
return STATUS_CONFIG[status]?.badge() || chalk19.white(`[${status}]`);
|
|
853
908
|
}
|
|
854
909
|
function formatPriorityBadge(priority) {
|
|
855
|
-
return PRIORITY_CONFIG[priority]?.badge() ||
|
|
910
|
+
return PRIORITY_CONFIG[priority]?.badge() || chalk19.white(`[${priority}]`);
|
|
856
911
|
}
|
|
857
912
|
function getStatusIndicator(status) {
|
|
858
913
|
const config = STATUS_CONFIG[status];
|
|
859
|
-
if (!config) return
|
|
914
|
+
if (!config) return chalk19.gray("[unknown]");
|
|
860
915
|
return config.colorFn(`[${status}]`);
|
|
861
916
|
}
|
|
862
917
|
function getStatusEmoji(status) {
|
|
@@ -868,7 +923,7 @@ function getPriorityEmoji(priority) {
|
|
|
868
923
|
|
|
869
924
|
// src/commands/list.ts
|
|
870
925
|
function listCommand() {
|
|
871
|
-
return new Command("list").description("List all specs").option("--archived", "Include archived specs").option("--status <status>", "Filter by status (planned, in-progress, complete, archived)").option("--tag <tag...>", "Filter by tag (can specify multiple)").option("--priority <priority>", "Filter by priority (low, medium, high, critical)").option("--assignee <name>", "Filter by assignee").option("--field <name=value...>", "Filter by custom field (can specify multiple)").option("--sort <field>", "Sort by field (id, created, name, status, priority)", "id").option("--order <order>", "Sort order (asc, desc)", "desc").action(async (options) => {
|
|
926
|
+
return new Command("list").description("List all specs").option("--archived", "Include archived specs").option("--status <status>", "Filter by status (planned, in-progress, complete, archived)").option("--tag <tag...>", "Filter by tag (can specify multiple)").option("--priority <priority>", "Filter by priority (low, medium, high, critical)").option("--assignee <name>", "Filter by assignee").option("--field <name=value...>", "Filter by custom field (can specify multiple)").option("--sort <field>", "Sort by field (id, created, name, status, priority)", "id").option("--order <order>", "Sort order (asc, desc)", "desc").option("--json", "Output as JSON").action(async (options) => {
|
|
872
927
|
const customFields = parseCustomFieldOptions(options.field);
|
|
873
928
|
const listOptions = {
|
|
874
929
|
showArchived: options.archived,
|
|
@@ -878,7 +933,8 @@ function listCommand() {
|
|
|
878
933
|
assignee: options.assignee,
|
|
879
934
|
customFields: Object.keys(customFields).length > 0 ? customFields : void 0,
|
|
880
935
|
sortBy: options.sort || "id",
|
|
881
|
-
sortOrder: options.order || "desc"
|
|
936
|
+
sortOrder: options.order || "desc",
|
|
937
|
+
json: options.json
|
|
882
938
|
};
|
|
883
939
|
await listSpecs(listOptions);
|
|
884
940
|
});
|
|
@@ -887,7 +943,7 @@ async function listSpecs(options = {}) {
|
|
|
887
943
|
await autoCheckIfEnabled();
|
|
888
944
|
const config = await loadConfig();
|
|
889
945
|
const cwd = process.cwd();
|
|
890
|
-
const specsDir =
|
|
946
|
+
const specsDir = path15.join(cwd, config.specsDir);
|
|
891
947
|
try {
|
|
892
948
|
await fs9.access(specsDir);
|
|
893
949
|
} catch {
|
|
@@ -914,10 +970,33 @@ async function listSpecs(options = {}) {
|
|
|
914
970
|
})
|
|
915
971
|
);
|
|
916
972
|
if (specs.length === 0) {
|
|
917
|
-
|
|
973
|
+
if (options.json) {
|
|
974
|
+
console.log(JSON.stringify({ specs: [], total: 0 }, null, 2));
|
|
975
|
+
} else {
|
|
976
|
+
console.log(chalk19.dim("No specs found."));
|
|
977
|
+
}
|
|
918
978
|
return;
|
|
919
979
|
}
|
|
920
|
-
|
|
980
|
+
if (options.json) {
|
|
981
|
+
const jsonOutput = {
|
|
982
|
+
specs: specs.map((spec) => ({
|
|
983
|
+
path: spec.path,
|
|
984
|
+
name: spec.name,
|
|
985
|
+
status: spec.frontmatter.status,
|
|
986
|
+
priority: spec.frontmatter.priority,
|
|
987
|
+
tags: spec.frontmatter.tags,
|
|
988
|
+
assignee: spec.frontmatter.assignee,
|
|
989
|
+
created: spec.frontmatter.created,
|
|
990
|
+
completed: spec.frontmatter.completed,
|
|
991
|
+
subFiles: spec.subFiles?.length || 0
|
|
992
|
+
})),
|
|
993
|
+
total: specs.length,
|
|
994
|
+
filter: options
|
|
995
|
+
};
|
|
996
|
+
console.log(JSON.stringify(jsonOutput, null, 2));
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
console.log(chalk19.bold.cyan("\u{1F4C4} Spec List"));
|
|
921
1000
|
const filterParts = [];
|
|
922
1001
|
if (options.status) {
|
|
923
1002
|
const statusStr = Array.isArray(options.status) ? options.status.join(",") : options.status;
|
|
@@ -930,7 +1009,7 @@ async function listSpecs(options = {}) {
|
|
|
930
1009
|
}
|
|
931
1010
|
if (options.assignee) filterParts.push(`assignee=${options.assignee}`);
|
|
932
1011
|
if (filterParts.length > 0) {
|
|
933
|
-
console.log(
|
|
1012
|
+
console.log(chalk19.dim(`Filtered by: ${filterParts.join(", ")}`));
|
|
934
1013
|
}
|
|
935
1014
|
console.log("");
|
|
936
1015
|
const patternInfo = detectPatternType(config);
|
|
@@ -940,7 +1019,7 @@ async function listSpecs(options = {}) {
|
|
|
940
1019
|
renderFlatList(specs);
|
|
941
1020
|
}
|
|
942
1021
|
console.log("");
|
|
943
|
-
console.log(
|
|
1022
|
+
console.log(chalk19.bold(`Total: ${chalk19.green(specs.length)} spec${specs.length !== 1 ? "s" : ""}`));
|
|
944
1023
|
}
|
|
945
1024
|
function renderFlatList(specs) {
|
|
946
1025
|
for (const spec of specs) {
|
|
@@ -948,25 +1027,25 @@ function renderFlatList(specs) {
|
|
|
948
1027
|
const priorityEmoji = getPriorityEmoji(spec.frontmatter.priority);
|
|
949
1028
|
let assigneeStr = "";
|
|
950
1029
|
if (spec.frontmatter.assignee) {
|
|
951
|
-
assigneeStr = " " +
|
|
1030
|
+
assigneeStr = " " + chalk19.cyan(`@${sanitizeUserInput(spec.frontmatter.assignee)}`);
|
|
952
1031
|
}
|
|
953
1032
|
let tagsStr = "";
|
|
954
1033
|
if (spec.frontmatter.tags?.length) {
|
|
955
1034
|
const tags = Array.isArray(spec.frontmatter.tags) ? spec.frontmatter.tags : [];
|
|
956
1035
|
if (tags.length > 0) {
|
|
957
1036
|
const tagStr = tags.map((tag) => `#${sanitizeUserInput(tag)}`).join(" ");
|
|
958
|
-
tagsStr = " " +
|
|
1037
|
+
tagsStr = " " + chalk19.dim(chalk19.magenta(tagStr));
|
|
959
1038
|
}
|
|
960
1039
|
}
|
|
961
1040
|
let subSpecStr = "";
|
|
962
1041
|
if (spec.subFiles) {
|
|
963
1042
|
const docCount = spec.subFiles.filter((f) => f.type === "document").length;
|
|
964
1043
|
if (docCount > 0) {
|
|
965
|
-
subSpecStr = " " +
|
|
1044
|
+
subSpecStr = " " + chalk19.dim(chalk19.yellow(`(+${docCount} sub-spec${docCount > 1 ? "s" : ""})`));
|
|
966
1045
|
}
|
|
967
1046
|
}
|
|
968
1047
|
const priorityPrefix = priorityEmoji ? `${priorityEmoji} ` : "";
|
|
969
|
-
console.log(`${priorityPrefix}${statusEmoji} ${
|
|
1048
|
+
console.log(`${priorityPrefix}${statusEmoji} ${chalk19.cyan(sanitizeUserInput(spec.path))}${assigneeStr}${tagsStr}${subSpecStr}`);
|
|
970
1049
|
}
|
|
971
1050
|
}
|
|
972
1051
|
function renderGroupedList(specs, groupExtractor) {
|
|
@@ -995,7 +1074,7 @@ function renderGroupedList(specs, groupExtractor) {
|
|
|
995
1074
|
const groupName = sortedGroups[i];
|
|
996
1075
|
const groupSpecs = groups.get(groupName);
|
|
997
1076
|
const groupEmoji = /^\d{8}$/.test(groupName) ? "\u{1F4C5}" : groupName.startsWith("milestone") ? "\u{1F3AF}" : "\u{1F4C1}";
|
|
998
|
-
console.log(`${
|
|
1077
|
+
console.log(`${chalk19.bold.cyan(`${groupEmoji} ${groupName}/`)} ${chalk19.dim(`(${groupSpecs.length})`)}`);
|
|
999
1078
|
console.log("");
|
|
1000
1079
|
for (const spec of groupSpecs) {
|
|
1001
1080
|
const statusEmoji = getStatusEmoji(spec.frontmatter.status);
|
|
@@ -1003,25 +1082,25 @@ function renderGroupedList(specs, groupExtractor) {
|
|
|
1003
1082
|
const displayPath = spec.path.includes("/") ? spec.path.split("/").slice(1).join("/") : spec.path;
|
|
1004
1083
|
let assigneeStr = "";
|
|
1005
1084
|
if (spec.frontmatter.assignee) {
|
|
1006
|
-
assigneeStr = " " +
|
|
1085
|
+
assigneeStr = " " + chalk19.cyan(`@${sanitizeUserInput(spec.frontmatter.assignee)}`);
|
|
1007
1086
|
}
|
|
1008
1087
|
let tagsStr = "";
|
|
1009
1088
|
if (spec.frontmatter.tags?.length) {
|
|
1010
1089
|
const tags = Array.isArray(spec.frontmatter.tags) ? spec.frontmatter.tags : [];
|
|
1011
1090
|
if (tags.length > 0) {
|
|
1012
1091
|
const tagStr = tags.map((tag) => `#${sanitizeUserInput(tag)}`).join(" ");
|
|
1013
|
-
tagsStr = " " +
|
|
1092
|
+
tagsStr = " " + chalk19.dim(chalk19.magenta(tagStr));
|
|
1014
1093
|
}
|
|
1015
1094
|
}
|
|
1016
1095
|
let subSpecStr = "";
|
|
1017
1096
|
if (spec.subFiles) {
|
|
1018
1097
|
const docCount = spec.subFiles.filter((f) => f.type === "document").length;
|
|
1019
1098
|
if (docCount > 0) {
|
|
1020
|
-
subSpecStr = " " +
|
|
1099
|
+
subSpecStr = " " + chalk19.dim(chalk19.yellow(`(+${docCount} sub-spec${docCount > 1 ? "s" : ""})`));
|
|
1021
1100
|
}
|
|
1022
1101
|
}
|
|
1023
1102
|
const priorityPrefix = priorityEmoji ? `${priorityEmoji} ` : "";
|
|
1024
|
-
console.log(` ${priorityPrefix}${statusEmoji} ${
|
|
1103
|
+
console.log(` ${priorityPrefix}${statusEmoji} ${chalk19.cyan(sanitizeUserInput(displayPath))}${assigneeStr}${tagsStr}${subSpecStr}`);
|
|
1025
1104
|
}
|
|
1026
1105
|
if (i < sortedGroups.length - 1) {
|
|
1027
1106
|
console.log("");
|
|
@@ -1050,11 +1129,11 @@ function updateCommand() {
|
|
|
1050
1129
|
await updateSpec(specPath, updates);
|
|
1051
1130
|
});
|
|
1052
1131
|
}
|
|
1053
|
-
async function updateSpec(specPath, updates) {
|
|
1132
|
+
async function updateSpec(specPath, updates, options = {}) {
|
|
1054
1133
|
await autoCheckIfEnabled();
|
|
1055
|
-
const
|
|
1056
|
-
const
|
|
1057
|
-
const specsDir =
|
|
1134
|
+
const cwd = options.cwd ?? process.cwd();
|
|
1135
|
+
const config = await loadConfig(cwd);
|
|
1136
|
+
const specsDir = path15.join(cwd, config.specsDir);
|
|
1058
1137
|
const resolvedPath = await resolveSpecPath(specPath, cwd, specsDir);
|
|
1059
1138
|
if (!resolvedPath) {
|
|
1060
1139
|
throw new Error(`Spec not found: ${sanitizeUserInput(specPath)}. Tried: ${sanitizeUserInput(specPath)}, specs/${sanitizeUserInput(specPath)}, and searching in date directories`);
|
|
@@ -1076,12 +1155,12 @@ async function updateSpec(specPath, updates) {
|
|
|
1076
1155
|
});
|
|
1077
1156
|
}
|
|
1078
1157
|
await updateFrontmatter(specFile, allUpdates);
|
|
1079
|
-
console.log(
|
|
1158
|
+
console.log(chalk19.green(`\u2713 Updated: ${sanitizeUserInput(path15.relative(cwd, resolvedPath))}`));
|
|
1080
1159
|
const updatedFields = Object.keys(updates).filter((k) => k !== "customFields");
|
|
1081
1160
|
if (updates.customFields) {
|
|
1082
1161
|
updatedFields.push(...Object.keys(updates.customFields));
|
|
1083
1162
|
}
|
|
1084
|
-
console.log(
|
|
1163
|
+
console.log(chalk19.gray(` Fields: ${updatedFields.join(", ")}`));
|
|
1085
1164
|
}
|
|
1086
1165
|
function linkCommand() {
|
|
1087
1166
|
return new Command("link").description("Add relationships between specs (depends_on, related)").argument("<spec>", "Spec to update").option("--depends-on <specs>", "Add dependencies (comma-separated spec numbers or names)").option("--related <specs>", "Add related specs (comma-separated spec numbers or names)").action(async (specPath, options) => {
|
|
@@ -1096,7 +1175,7 @@ async function linkSpec(specPath, options) {
|
|
|
1096
1175
|
await autoCheckIfEnabled();
|
|
1097
1176
|
const config = await loadConfig();
|
|
1098
1177
|
const cwd = process.cwd();
|
|
1099
|
-
const specsDir =
|
|
1178
|
+
const specsDir = path15.join(cwd, config.specsDir);
|
|
1100
1179
|
const resolvedPath = await resolveSpecPath(specPath, cwd, specsDir);
|
|
1101
1180
|
if (!resolvedPath) {
|
|
1102
1181
|
throw new Error(`Spec not found: ${sanitizeUserInput(specPath)}`);
|
|
@@ -1109,7 +1188,7 @@ async function linkSpec(specPath, options) {
|
|
|
1109
1188
|
const specMap = new Map(allSpecs.map((s) => [s.path, s]));
|
|
1110
1189
|
const dependsOnSpecs = options.dependsOn ? options.dependsOn.split(",").map((s) => s.trim()) : [];
|
|
1111
1190
|
const relatedSpecs = options.related ? options.related.split(",").map((s) => s.trim()) : [];
|
|
1112
|
-
const targetSpecName =
|
|
1191
|
+
const targetSpecName = path15.basename(resolvedPath);
|
|
1113
1192
|
const allRelationshipSpecs = [...dependsOnSpecs, ...relatedSpecs];
|
|
1114
1193
|
const resolvedRelationships = /* @__PURE__ */ new Map();
|
|
1115
1194
|
for (const relSpec of allRelationshipSpecs) {
|
|
@@ -1123,7 +1202,7 @@ async function linkSpec(specPath, options) {
|
|
|
1123
1202
|
if (relResolvedPath === resolvedPath) {
|
|
1124
1203
|
throw new Error(`Cannot link spec to itself: ${sanitizeUserInput(relSpec)}`);
|
|
1125
1204
|
}
|
|
1126
|
-
const relSpecName =
|
|
1205
|
+
const relSpecName = path15.basename(relResolvedPath);
|
|
1127
1206
|
resolvedRelationships.set(relSpec, relSpecName);
|
|
1128
1207
|
}
|
|
1129
1208
|
const { parseFrontmatter: parseFrontmatter2 } = await import('./frontmatter-R2DANL5X.js');
|
|
@@ -1143,7 +1222,7 @@ async function linkSpec(specPath, options) {
|
|
|
1143
1222
|
}
|
|
1144
1223
|
updates.depends_on = newDependsOn;
|
|
1145
1224
|
if (added === 0) {
|
|
1146
|
-
console.log(
|
|
1225
|
+
console.log(chalk19.gray(`\u2139 Dependencies already exist, no changes made`));
|
|
1147
1226
|
}
|
|
1148
1227
|
}
|
|
1149
1228
|
if (relatedSpecs.length > 0) {
|
|
@@ -1170,19 +1249,19 @@ async function linkSpec(specPath, options) {
|
|
|
1170
1249
|
await updateFrontmatter(relSpecFile, {
|
|
1171
1250
|
related: [...relCurrentRelated, targetSpecName]
|
|
1172
1251
|
});
|
|
1173
|
-
console.log(
|
|
1252
|
+
console.log(chalk19.gray(` Updated: ${sanitizeUserInput(relSpecName)} (bidirectional)`));
|
|
1174
1253
|
}
|
|
1175
1254
|
}
|
|
1176
1255
|
}
|
|
1177
1256
|
}
|
|
1178
1257
|
if (added === 0) {
|
|
1179
|
-
console.log(
|
|
1258
|
+
console.log(chalk19.gray(`\u2139 Related specs already exist, no changes made`));
|
|
1180
1259
|
}
|
|
1181
1260
|
}
|
|
1182
1261
|
if (updates.depends_on && updates.depends_on.length > 0) {
|
|
1183
1262
|
const cycles = detectCycles(targetSpecName, updates.depends_on, specMap);
|
|
1184
1263
|
if (cycles.length > 0) {
|
|
1185
|
-
console.log(
|
|
1264
|
+
console.log(chalk19.yellow(`\u26A0\uFE0F Dependency cycle detected: ${cycles.join(" \u2192 ")}`));
|
|
1186
1265
|
}
|
|
1187
1266
|
}
|
|
1188
1267
|
await updateFrontmatter(specFile, updates);
|
|
@@ -1193,8 +1272,8 @@ async function linkSpec(specPath, options) {
|
|
|
1193
1272
|
if (relatedSpecs.length > 0) {
|
|
1194
1273
|
updatedFields.push(`related: ${relatedSpecs.join(", ")}`);
|
|
1195
1274
|
}
|
|
1196
|
-
console.log(
|
|
1197
|
-
console.log(
|
|
1275
|
+
console.log(chalk19.green(`\u2713 Added relationships: ${updatedFields.join(", ")}`));
|
|
1276
|
+
console.log(chalk19.gray(` Updated: ${sanitizeUserInput(path15.relative(cwd, resolvedPath))}`));
|
|
1198
1277
|
}
|
|
1199
1278
|
function detectCycles(startSpec, dependsOn, specMap, visited = /* @__PURE__ */ new Set(), path31 = []) {
|
|
1200
1279
|
if (visited.has(startSpec)) {
|
|
@@ -1230,7 +1309,7 @@ async function unlinkSpec(specPath, options) {
|
|
|
1230
1309
|
await autoCheckIfEnabled();
|
|
1231
1310
|
const config = await loadConfig();
|
|
1232
1311
|
const cwd = process.cwd();
|
|
1233
|
-
const specsDir =
|
|
1312
|
+
const specsDir = path15.join(cwd, config.specsDir);
|
|
1234
1313
|
const resolvedPath = await resolveSpecPath(specPath, cwd, specsDir);
|
|
1235
1314
|
if (!resolvedPath) {
|
|
1236
1315
|
throw new Error(`Spec not found: ${sanitizeUserInput(specPath)}`);
|
|
@@ -1239,7 +1318,7 @@ async function unlinkSpec(specPath, options) {
|
|
|
1239
1318
|
if (!specFile) {
|
|
1240
1319
|
throw new Error(`No spec file found in: ${sanitizeUserInput(specPath)}`);
|
|
1241
1320
|
}
|
|
1242
|
-
const targetSpecName =
|
|
1321
|
+
const targetSpecName = path15.basename(resolvedPath);
|
|
1243
1322
|
const { parseFrontmatter: parseFrontmatter2 } = await import('./frontmatter-R2DANL5X.js');
|
|
1244
1323
|
const currentFrontmatter = await parseFrontmatter2(specFile);
|
|
1245
1324
|
const currentDependsOn = currentFrontmatter?.depends_on || [];
|
|
@@ -1256,7 +1335,7 @@ async function unlinkSpec(specPath, options) {
|
|
|
1256
1335
|
for (const spec of toRemove) {
|
|
1257
1336
|
const resolvedSpecPath = await resolveSpecPath(spec, cwd, specsDir);
|
|
1258
1337
|
if (resolvedSpecPath) {
|
|
1259
|
-
resolvedToRemove.add(
|
|
1338
|
+
resolvedToRemove.add(path15.basename(resolvedSpecPath));
|
|
1260
1339
|
} else {
|
|
1261
1340
|
resolvedToRemove.add(spec);
|
|
1262
1341
|
}
|
|
@@ -1280,7 +1359,7 @@ async function unlinkSpec(specPath, options) {
|
|
|
1280
1359
|
await updateFrontmatter(relSpecFile, {
|
|
1281
1360
|
related: relNewRelated
|
|
1282
1361
|
});
|
|
1283
|
-
console.log(
|
|
1362
|
+
console.log(chalk19.gray(` Updated: ${sanitizeUserInput(relSpec)} (bidirectional)`));
|
|
1284
1363
|
}
|
|
1285
1364
|
}
|
|
1286
1365
|
}
|
|
@@ -1293,7 +1372,7 @@ async function unlinkSpec(specPath, options) {
|
|
|
1293
1372
|
for (const spec of toRemove) {
|
|
1294
1373
|
const resolvedSpecPath = await resolveSpecPath(spec, cwd, specsDir);
|
|
1295
1374
|
if (resolvedSpecPath) {
|
|
1296
|
-
const specName =
|
|
1375
|
+
const specName = path15.basename(resolvedSpecPath);
|
|
1297
1376
|
resolvedToRemove.add(specName);
|
|
1298
1377
|
const relSpecFile = await getSpecFile(resolvedSpecPath, config.structure.defaultFile);
|
|
1299
1378
|
if (relSpecFile) {
|
|
@@ -1304,7 +1383,7 @@ async function unlinkSpec(specPath, options) {
|
|
|
1304
1383
|
await updateFrontmatter(relSpecFile, {
|
|
1305
1384
|
related: relNewRelated
|
|
1306
1385
|
});
|
|
1307
|
-
console.log(
|
|
1386
|
+
console.log(chalk19.gray(` Updated: ${sanitizeUserInput(specName)} (bidirectional)`));
|
|
1308
1387
|
}
|
|
1309
1388
|
}
|
|
1310
1389
|
} else {
|
|
@@ -1318,7 +1397,7 @@ async function unlinkSpec(specPath, options) {
|
|
|
1318
1397
|
}
|
|
1319
1398
|
await updateFrontmatter(specFile, updates);
|
|
1320
1399
|
if (removedCount === 0) {
|
|
1321
|
-
console.log(
|
|
1400
|
+
console.log(chalk19.gray(`\u2139 No matching relationships found to remove`));
|
|
1322
1401
|
} else {
|
|
1323
1402
|
const updatedFields = [];
|
|
1324
1403
|
if (options.dependsOn !== void 0) {
|
|
@@ -1327,8 +1406,8 @@ async function unlinkSpec(specPath, options) {
|
|
|
1327
1406
|
if (options.related !== void 0) {
|
|
1328
1407
|
updatedFields.push(`related`);
|
|
1329
1408
|
}
|
|
1330
|
-
console.log(
|
|
1331
|
-
console.log(
|
|
1409
|
+
console.log(chalk19.green(`\u2713 Removed relationships: ${updatedFields.join(", ")} (${removedCount} total)`));
|
|
1410
|
+
console.log(chalk19.gray(` Updated: ${sanitizeUserInput(path15.relative(cwd, resolvedPath))}`));
|
|
1332
1411
|
}
|
|
1333
1412
|
}
|
|
1334
1413
|
function isGitRepository() {
|
|
@@ -1459,13 +1538,14 @@ function fileExistsInGit(filePath) {
|
|
|
1459
1538
|
|
|
1460
1539
|
// src/commands/backfill.ts
|
|
1461
1540
|
function backfillCommand() {
|
|
1462
|
-
return new Command("backfill").description("Backfill timestamps from git history").argument("[specs...]", "Specific specs to backfill (optional)").option("--dry-run", "Show what would be updated without making changes").option("--force", "Overwrite existing timestamp values").option("--assignee", "Include assignee from first commit author").option("--transitions", "Include full status transition history").option("--all", "Include all optional fields (assignee + transitions)").action(async (specs, options) => {
|
|
1541
|
+
return new Command("backfill").description("Backfill timestamps from git history").argument("[specs...]", "Specific specs to backfill (optional)").option("--dry-run", "Show what would be updated without making changes").option("--force", "Overwrite existing timestamp values").option("--assignee", "Include assignee from first commit author").option("--transitions", "Include full status transition history").option("--all", "Include all optional fields (assignee + transitions)").option("--json", "Output as JSON").action(async (specs, options) => {
|
|
1463
1542
|
await backfillTimestamps({
|
|
1464
1543
|
dryRun: options.dryRun,
|
|
1465
1544
|
force: options.force,
|
|
1466
1545
|
includeAssignee: options.assignee || options.all,
|
|
1467
1546
|
includeTransitions: options.transitions || options.all,
|
|
1468
|
-
specs: specs && specs.length > 0 ? specs : void 0
|
|
1547
|
+
specs: specs && specs.length > 0 ? specs : void 0,
|
|
1548
|
+
json: options.json
|
|
1469
1549
|
});
|
|
1470
1550
|
});
|
|
1471
1551
|
}
|
|
@@ -1481,7 +1561,7 @@ async function backfillTimestamps(options = {}) {
|
|
|
1481
1561
|
specs = [];
|
|
1482
1562
|
const config = await loadConfig();
|
|
1483
1563
|
const cwd = process.cwd();
|
|
1484
|
-
const specsDir =
|
|
1564
|
+
const specsDir = path15.join(cwd, config.specsDir);
|
|
1485
1565
|
for (const specPath of options.specs) {
|
|
1486
1566
|
const resolved = await resolveSpecPath(specPath, cwd, specsDir);
|
|
1487
1567
|
if (!resolved) {
|
|
@@ -1677,79 +1757,79 @@ function templatesCommand() {
|
|
|
1677
1757
|
}
|
|
1678
1758
|
async function listTemplates(cwd = process.cwd()) {
|
|
1679
1759
|
const config = await loadConfig(cwd);
|
|
1680
|
-
const templatesDir =
|
|
1760
|
+
const templatesDir = path15.join(cwd, ".lean-spec", "templates");
|
|
1681
1761
|
console.log("");
|
|
1682
|
-
console.log(
|
|
1762
|
+
console.log(chalk19.green("=== Project Templates ==="));
|
|
1683
1763
|
console.log("");
|
|
1684
1764
|
try {
|
|
1685
1765
|
await fs9.access(templatesDir);
|
|
1686
1766
|
} catch {
|
|
1687
|
-
console.log(
|
|
1688
|
-
console.log(
|
|
1767
|
+
console.log(chalk19.yellow("No templates directory found."));
|
|
1768
|
+
console.log(chalk19.gray("Run: lean-spec init"));
|
|
1689
1769
|
console.log("");
|
|
1690
1770
|
return;
|
|
1691
1771
|
}
|
|
1692
1772
|
const files = await fs9.readdir(templatesDir);
|
|
1693
1773
|
const templateFiles = files.filter((f) => f.endsWith(".md"));
|
|
1694
1774
|
if (templateFiles.length === 0) {
|
|
1695
|
-
console.log(
|
|
1775
|
+
console.log(chalk19.yellow("No templates found."));
|
|
1696
1776
|
console.log("");
|
|
1697
1777
|
return;
|
|
1698
1778
|
}
|
|
1699
1779
|
if (config.templates && Object.keys(config.templates).length > 0) {
|
|
1700
|
-
console.log(
|
|
1780
|
+
console.log(chalk19.cyan("Registered:"));
|
|
1701
1781
|
for (const [name, file] of Object.entries(config.templates)) {
|
|
1702
1782
|
const isDefault = config.template === file;
|
|
1703
|
-
const marker = isDefault ?
|
|
1704
|
-
console.log(` ${
|
|
1783
|
+
const marker = isDefault ? chalk19.green("\u2713 (default)") : "";
|
|
1784
|
+
console.log(` ${chalk19.bold(name)}: ${file} ${marker}`);
|
|
1705
1785
|
}
|
|
1706
1786
|
console.log("");
|
|
1707
1787
|
}
|
|
1708
|
-
console.log(
|
|
1788
|
+
console.log(chalk19.cyan("Available files:"));
|
|
1709
1789
|
for (const file of templateFiles) {
|
|
1710
|
-
const filePath =
|
|
1790
|
+
const filePath = path15.join(templatesDir, file);
|
|
1711
1791
|
const stat6 = await fs9.stat(filePath);
|
|
1712
1792
|
const sizeKB = (stat6.size / 1024).toFixed(1);
|
|
1713
1793
|
console.log(` ${file} (${sizeKB} KB)`);
|
|
1714
1794
|
}
|
|
1715
1795
|
console.log("");
|
|
1716
|
-
console.log(
|
|
1796
|
+
console.log(chalk19.gray("Use templates with: lean-spec create <name> --template=<template-name>"));
|
|
1717
1797
|
console.log("");
|
|
1718
1798
|
}
|
|
1719
1799
|
async function showTemplate(templateName, cwd = process.cwd()) {
|
|
1720
1800
|
const config = await loadConfig(cwd);
|
|
1721
1801
|
if (!config.templates?.[templateName]) {
|
|
1722
|
-
console.error(
|
|
1723
|
-
console.error(
|
|
1802
|
+
console.error(chalk19.red(`Template not found: ${templateName}`));
|
|
1803
|
+
console.error(chalk19.gray(`Available: ${Object.keys(config.templates || {}).join(", ")}`));
|
|
1724
1804
|
process.exit(1);
|
|
1725
1805
|
}
|
|
1726
|
-
const templatesDir =
|
|
1806
|
+
const templatesDir = path15.join(cwd, ".lean-spec", "templates");
|
|
1727
1807
|
const templateFile = config.templates[templateName];
|
|
1728
|
-
const templatePath =
|
|
1808
|
+
const templatePath = path15.join(templatesDir, templateFile);
|
|
1729
1809
|
try {
|
|
1730
1810
|
const content = await fs9.readFile(templatePath, "utf-8");
|
|
1731
1811
|
console.log("");
|
|
1732
|
-
console.log(
|
|
1812
|
+
console.log(chalk19.cyan(`=== Template: ${templateName} (${templateFile}) ===`));
|
|
1733
1813
|
console.log("");
|
|
1734
1814
|
console.log(content);
|
|
1735
1815
|
console.log("");
|
|
1736
1816
|
} catch (error) {
|
|
1737
|
-
console.error(
|
|
1817
|
+
console.error(chalk19.red(`Error reading template: ${templateFile}`));
|
|
1738
1818
|
console.error(error);
|
|
1739
1819
|
process.exit(1);
|
|
1740
1820
|
}
|
|
1741
1821
|
}
|
|
1742
1822
|
async function addTemplate(name, file, cwd = process.cwd()) {
|
|
1743
1823
|
const config = await loadConfig(cwd);
|
|
1744
|
-
const templatesDir =
|
|
1745
|
-
const templatePath =
|
|
1824
|
+
const templatesDir = path15.join(cwd, ".lean-spec", "templates");
|
|
1825
|
+
const templatePath = path15.join(templatesDir, file);
|
|
1746
1826
|
try {
|
|
1747
1827
|
await fs9.access(templatePath);
|
|
1748
1828
|
} catch {
|
|
1749
|
-
console.error(
|
|
1750
|
-
console.error(
|
|
1829
|
+
console.error(chalk19.red(`Template file not found: ${file}`));
|
|
1830
|
+
console.error(chalk19.gray(`Expected at: ${templatePath}`));
|
|
1751
1831
|
console.error(
|
|
1752
|
-
|
|
1832
|
+
chalk19.yellow("Create the file first or use: lean-spec templates copy <source> <target>")
|
|
1753
1833
|
);
|
|
1754
1834
|
process.exit(1);
|
|
1755
1835
|
}
|
|
@@ -1757,60 +1837,60 @@ async function addTemplate(name, file, cwd = process.cwd()) {
|
|
|
1757
1837
|
config.templates = {};
|
|
1758
1838
|
}
|
|
1759
1839
|
if (config.templates[name]) {
|
|
1760
|
-
console.log(
|
|
1840
|
+
console.log(chalk19.yellow(`Warning: Template '${name}' already exists, updating...`));
|
|
1761
1841
|
}
|
|
1762
1842
|
config.templates[name] = file;
|
|
1763
1843
|
await saveConfig(config, cwd);
|
|
1764
|
-
console.log(
|
|
1765
|
-
console.log(
|
|
1844
|
+
console.log(chalk19.green(`\u2713 Added template: ${name} \u2192 ${file}`));
|
|
1845
|
+
console.log(chalk19.gray(` Use with: lean-spec create <spec-name> --template=${name}`));
|
|
1766
1846
|
}
|
|
1767
1847
|
async function removeTemplate(name, cwd = process.cwd()) {
|
|
1768
1848
|
const config = await loadConfig(cwd);
|
|
1769
1849
|
if (!config.templates?.[name]) {
|
|
1770
|
-
console.error(
|
|
1771
|
-
console.error(
|
|
1850
|
+
console.error(chalk19.red(`Template not found: ${name}`));
|
|
1851
|
+
console.error(chalk19.gray(`Available: ${Object.keys(config.templates || {}).join(", ")}`));
|
|
1772
1852
|
process.exit(1);
|
|
1773
1853
|
}
|
|
1774
1854
|
if (name === "default") {
|
|
1775
|
-
console.error(
|
|
1855
|
+
console.error(chalk19.red("Cannot remove default template"));
|
|
1776
1856
|
process.exit(1);
|
|
1777
1857
|
}
|
|
1778
1858
|
const file = config.templates[name];
|
|
1779
1859
|
delete config.templates[name];
|
|
1780
1860
|
await saveConfig(config, cwd);
|
|
1781
|
-
console.log(
|
|
1782
|
-
console.log(
|
|
1861
|
+
console.log(chalk19.green(`\u2713 Removed template: ${name}`));
|
|
1862
|
+
console.log(chalk19.gray(` Note: Template file ${file} still exists in .lean-spec/templates/`));
|
|
1783
1863
|
}
|
|
1784
1864
|
async function copyTemplate(source, target, cwd = process.cwd()) {
|
|
1785
1865
|
const config = await loadConfig(cwd);
|
|
1786
|
-
const templatesDir =
|
|
1866
|
+
const templatesDir = path15.join(cwd, ".lean-spec", "templates");
|
|
1787
1867
|
let sourceFile;
|
|
1788
1868
|
if (config.templates?.[source]) {
|
|
1789
1869
|
sourceFile = config.templates[source];
|
|
1790
1870
|
} else {
|
|
1791
1871
|
sourceFile = source;
|
|
1792
1872
|
}
|
|
1793
|
-
const sourcePath =
|
|
1873
|
+
const sourcePath = path15.join(templatesDir, sourceFile);
|
|
1794
1874
|
try {
|
|
1795
1875
|
await fs9.access(sourcePath);
|
|
1796
1876
|
} catch {
|
|
1797
|
-
console.error(
|
|
1798
|
-
console.error(
|
|
1877
|
+
console.error(chalk19.red(`Source template not found: ${source}`));
|
|
1878
|
+
console.error(chalk19.gray(`Expected at: ${sourcePath}`));
|
|
1799
1879
|
process.exit(1);
|
|
1800
1880
|
}
|
|
1801
1881
|
const targetFile = target.endsWith(".md") ? target : `${target}.md`;
|
|
1802
|
-
const targetPath =
|
|
1882
|
+
const targetPath = path15.join(templatesDir, targetFile);
|
|
1803
1883
|
await fs9.copyFile(sourcePath, targetPath);
|
|
1804
|
-
console.log(
|
|
1884
|
+
console.log(chalk19.green(`\u2713 Copied: ${sourceFile} \u2192 ${targetFile}`));
|
|
1805
1885
|
if (!config.templates) {
|
|
1806
1886
|
config.templates = {};
|
|
1807
1887
|
}
|
|
1808
1888
|
const templateName = target.replace(/\.md$/, "");
|
|
1809
1889
|
config.templates[templateName] = targetFile;
|
|
1810
1890
|
await saveConfig(config, cwd);
|
|
1811
|
-
console.log(
|
|
1812
|
-
console.log(
|
|
1813
|
-
console.log(
|
|
1891
|
+
console.log(chalk19.green(`\u2713 Registered template: ${templateName}`));
|
|
1892
|
+
console.log(chalk19.gray(` Edit: ${targetPath}`));
|
|
1893
|
+
console.log(chalk19.gray(` Use with: lean-spec create <spec-name> --template=${templateName}`));
|
|
1814
1894
|
}
|
|
1815
1895
|
async function detectExistingSystemPrompts(cwd) {
|
|
1816
1896
|
const commonFiles = [
|
|
@@ -1821,7 +1901,7 @@ async function detectExistingSystemPrompts(cwd) {
|
|
|
1821
1901
|
const found = [];
|
|
1822
1902
|
for (const file of commonFiles) {
|
|
1823
1903
|
try {
|
|
1824
|
-
await fs9.access(
|
|
1904
|
+
await fs9.access(path15.join(cwd, file));
|
|
1825
1905
|
found.push(file);
|
|
1826
1906
|
} catch {
|
|
1827
1907
|
}
|
|
@@ -1830,8 +1910,8 @@ async function detectExistingSystemPrompts(cwd) {
|
|
|
1830
1910
|
}
|
|
1831
1911
|
async function handleExistingFiles(action, existingFiles, templateDir, cwd, variables = {}) {
|
|
1832
1912
|
for (const file of existingFiles) {
|
|
1833
|
-
const filePath =
|
|
1834
|
-
const templateFilePath =
|
|
1913
|
+
const filePath = path15.join(cwd, file);
|
|
1914
|
+
const templateFilePath = path15.join(templateDir, file);
|
|
1835
1915
|
try {
|
|
1836
1916
|
await fs9.access(templateFilePath);
|
|
1837
1917
|
} catch {
|
|
@@ -1843,7 +1923,7 @@ async function handleExistingFiles(action, existingFiles, templateDir, cwd, vari
|
|
|
1843
1923
|
for (const [key, value] of Object.entries(variables)) {
|
|
1844
1924
|
template = template.replace(new RegExp(`\\{${key}\\}`, "g"), value);
|
|
1845
1925
|
}
|
|
1846
|
-
const promptPath =
|
|
1926
|
+
const promptPath = path15.join(cwd, ".lean-spec", "MERGE-AGENTS-PROMPT.md");
|
|
1847
1927
|
const aiPrompt = `# AI Prompt: Consolidate AGENTS.md
|
|
1848
1928
|
|
|
1849
1929
|
## Task
|
|
@@ -1875,16 +1955,16 @@ Create a single consolidated AGENTS.md that:
|
|
|
1875
1955
|
- Maintains clear structure and readability
|
|
1876
1956
|
- Removes any duplicate or conflicting guidance
|
|
1877
1957
|
`;
|
|
1878
|
-
await fs9.mkdir(
|
|
1958
|
+
await fs9.mkdir(path15.dirname(promptPath), { recursive: true });
|
|
1879
1959
|
await fs9.writeFile(promptPath, aiPrompt, "utf-8");
|
|
1880
|
-
console.log(
|
|
1881
|
-
console.log(
|
|
1960
|
+
console.log(chalk19.green(`\u2713 Created AI consolidation prompt`));
|
|
1961
|
+
console.log(chalk19.cyan(` \u2192 ${promptPath}`));
|
|
1882
1962
|
console.log("");
|
|
1883
|
-
console.log(
|
|
1884
|
-
console.log(
|
|
1885
|
-
console.log(
|
|
1886
|
-
console.log(
|
|
1887
|
-
console.log(
|
|
1963
|
+
console.log(chalk19.yellow("\u{1F4DD} Next steps:"));
|
|
1964
|
+
console.log(chalk19.gray(" 1. Open .lean-spec/MERGE-AGENTS-PROMPT.md"));
|
|
1965
|
+
console.log(chalk19.gray(" 2. Send it to your AI coding assistant (GitHub Copilot, Cursor, etc.)"));
|
|
1966
|
+
console.log(chalk19.gray(" 3. Let AI create the consolidated AGENTS.md"));
|
|
1967
|
+
console.log(chalk19.gray(" 4. Review and commit the result"));
|
|
1888
1968
|
console.log("");
|
|
1889
1969
|
} else if (action === "merge-append" && file === "AGENTS.md") {
|
|
1890
1970
|
const existing = await fs9.readFile(filePath, "utf-8");
|
|
@@ -1900,19 +1980,19 @@ Create a single consolidated AGENTS.md that:
|
|
|
1900
1980
|
|
|
1901
1981
|
${template.split("\n").slice(1).join("\n")}`;
|
|
1902
1982
|
await fs9.writeFile(filePath, merged, "utf-8");
|
|
1903
|
-
console.log(
|
|
1904
|
-
console.log(
|
|
1983
|
+
console.log(chalk19.green(`\u2713 Appended LeanSpec section to ${file}`));
|
|
1984
|
+
console.log(chalk19.yellow(" \u26A0 Note: May be verbose. Consider consolidating later."));
|
|
1905
1985
|
} else if (action === "overwrite") {
|
|
1906
1986
|
const backupPath = `${filePath}.backup`;
|
|
1907
1987
|
await fs9.rename(filePath, backupPath);
|
|
1908
|
-
console.log(
|
|
1988
|
+
console.log(chalk19.yellow(`\u2713 Backed up ${file} \u2192 ${file}.backup`));
|
|
1909
1989
|
let content = await fs9.readFile(templateFilePath, "utf-8");
|
|
1910
1990
|
for (const [key, value] of Object.entries(variables)) {
|
|
1911
1991
|
content = content.replace(new RegExp(`\\{${key}\\}`, "g"), value);
|
|
1912
1992
|
}
|
|
1913
1993
|
await fs9.writeFile(filePath, content, "utf-8");
|
|
1914
|
-
console.log(
|
|
1915
|
-
console.log(
|
|
1994
|
+
console.log(chalk19.green(`\u2713 Created new ${file}`));
|
|
1995
|
+
console.log(chalk19.gray(` \u{1F4A1} Your original content is preserved in ${file}.backup`));
|
|
1916
1996
|
}
|
|
1917
1997
|
}
|
|
1918
1998
|
}
|
|
@@ -1920,8 +2000,8 @@ async function copyDirectory(src, dest, skipFiles = [], variables = {}) {
|
|
|
1920
2000
|
await fs9.mkdir(dest, { recursive: true });
|
|
1921
2001
|
const entries = await fs9.readdir(src, { withFileTypes: true });
|
|
1922
2002
|
for (const entry of entries) {
|
|
1923
|
-
const srcPath =
|
|
1924
|
-
const destPath =
|
|
2003
|
+
const srcPath = path15.join(src, entry.name);
|
|
2004
|
+
const destPath = path15.join(dest, entry.name);
|
|
1925
2005
|
if (skipFiles.includes(entry.name)) {
|
|
1926
2006
|
continue;
|
|
1927
2007
|
}
|
|
@@ -1942,7 +2022,7 @@ async function copyDirectory(src, dest, skipFiles = [], variables = {}) {
|
|
|
1942
2022
|
}
|
|
1943
2023
|
async function getProjectName2(cwd) {
|
|
1944
2024
|
try {
|
|
1945
|
-
const packageJsonPath =
|
|
2025
|
+
const packageJsonPath = path15.join(cwd, "package.json");
|
|
1946
2026
|
const content = await fs9.readFile(packageJsonPath, "utf-8");
|
|
1947
2027
|
const pkg = JSON.parse(content);
|
|
1948
2028
|
if (pkg.name) {
|
|
@@ -1950,77 +2030,148 @@ async function getProjectName2(cwd) {
|
|
|
1950
2030
|
}
|
|
1951
2031
|
} catch {
|
|
1952
2032
|
}
|
|
1953
|
-
return
|
|
2033
|
+
return path15.basename(cwd);
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
// src/utils/examples.ts
|
|
2037
|
+
var EXAMPLES = {
|
|
2038
|
+
"dark-theme": {
|
|
2039
|
+
name: "dark-theme",
|
|
2040
|
+
title: "Dark Theme Support",
|
|
2041
|
+
description: "Add dark theme support to a professional admin dashboard with charts",
|
|
2042
|
+
difficulty: "beginner",
|
|
2043
|
+
tutorial: "Your First Spec with AI",
|
|
2044
|
+
tutorialUrl: "https://leanspec.dev/docs/tutorials/first-spec-with-ai",
|
|
2045
|
+
tech: ["HTML", "CSS", "JavaScript", "Chart.js", "Express.js"],
|
|
2046
|
+
files: 6,
|
|
2047
|
+
lines: 420
|
|
2048
|
+
},
|
|
2049
|
+
"dashboard-widgets": {
|
|
2050
|
+
name: "dashboard-widgets",
|
|
2051
|
+
title: "Dashboard Widgets",
|
|
2052
|
+
description: "Add three new widgets to an analytics dashboard",
|
|
2053
|
+
difficulty: "intermediate",
|
|
2054
|
+
tutorial: "Managing Multiple Features",
|
|
2055
|
+
tutorialUrl: "https://leanspec.dev/docs/tutorials/multiple-features",
|
|
2056
|
+
tech: ["React", "Vite"],
|
|
2057
|
+
files: 17,
|
|
2058
|
+
lines: 300
|
|
2059
|
+
},
|
|
2060
|
+
"api-refactor": {
|
|
2061
|
+
name: "api-refactor",
|
|
2062
|
+
title: "API Client Refactor",
|
|
2063
|
+
description: "Extract reusable API client from monolithic code",
|
|
2064
|
+
difficulty: "intermediate",
|
|
2065
|
+
tutorial: "Refactoring with Specs",
|
|
2066
|
+
tutorialUrl: "https://leanspec.dev/docs/tutorials/refactoring-specs",
|
|
2067
|
+
tech: ["Node.js"],
|
|
2068
|
+
files: 7,
|
|
2069
|
+
lines: 250
|
|
2070
|
+
}
|
|
2071
|
+
};
|
|
2072
|
+
function getExamplesList() {
|
|
2073
|
+
return Object.values(EXAMPLES);
|
|
2074
|
+
}
|
|
2075
|
+
function getExample(name) {
|
|
2076
|
+
return EXAMPLES[name];
|
|
2077
|
+
}
|
|
2078
|
+
function exampleExists(name) {
|
|
2079
|
+
return name in EXAMPLES;
|
|
1954
2080
|
}
|
|
1955
2081
|
|
|
1956
2082
|
// src/commands/init.ts
|
|
1957
|
-
var __dirname =
|
|
1958
|
-
var TEMPLATES_DIR =
|
|
2083
|
+
var __dirname = path15.dirname(fileURLToPath(import.meta.url));
|
|
2084
|
+
var TEMPLATES_DIR = path15.join(__dirname, "..", "templates");
|
|
2085
|
+
var EXAMPLES_DIR = path15.join(TEMPLATES_DIR, "examples");
|
|
1959
2086
|
function initCommand() {
|
|
1960
|
-
return new Command("init").description("Initialize LeanSpec in current directory").action(async () => {
|
|
1961
|
-
|
|
2087
|
+
return new Command("init").description("Initialize LeanSpec in current directory").option("-y, --yes", "Skip prompts and use defaults (quick start with standard template)").option("--template <name>", "Use specific template (standard or detailed)").option("--example [name]", "Scaffold an example project for tutorials (interactive if no name provided)").option("--name <dirname>", "Custom directory name for example project").option("--list", "List available example projects").action(async (options) => {
|
|
2088
|
+
if (options.list) {
|
|
2089
|
+
await listExamples();
|
|
2090
|
+
return;
|
|
2091
|
+
}
|
|
2092
|
+
if (options.example !== void 0) {
|
|
2093
|
+
await scaffoldExample(options.example, options.name);
|
|
2094
|
+
return;
|
|
2095
|
+
}
|
|
2096
|
+
await initProject(options.yes, options.template);
|
|
1962
2097
|
});
|
|
1963
2098
|
}
|
|
1964
|
-
async function initProject() {
|
|
2099
|
+
async function initProject(skipPrompts = false, templateOption) {
|
|
1965
2100
|
const cwd = process.cwd();
|
|
1966
2101
|
try {
|
|
1967
|
-
await fs9.access(
|
|
1968
|
-
console.log(
|
|
1969
|
-
console.log(
|
|
2102
|
+
await fs9.access(path15.join(cwd, ".lean-spec", "config.json"));
|
|
2103
|
+
console.log(chalk19.yellow("\u26A0 LeanSpec already initialized in this directory."));
|
|
2104
|
+
console.log(chalk19.gray("To reinitialize, delete .lean-spec/ directory first."));
|
|
1970
2105
|
return;
|
|
1971
2106
|
} catch {
|
|
1972
2107
|
}
|
|
1973
2108
|
console.log("");
|
|
1974
|
-
console.log(
|
|
2109
|
+
console.log(chalk19.green("Welcome to LeanSpec!"));
|
|
1975
2110
|
console.log("");
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
{
|
|
1985
|
-
name: "Choose template",
|
|
1986
|
-
value: "template",
|
|
1987
|
-
description: "Pick from: minimal, standard, enterprise"
|
|
1988
|
-
}
|
|
1989
|
-
// TODO: Re-enable when custom setup mode is implemented
|
|
1990
|
-
// {
|
|
1991
|
-
// name: 'Customize everything',
|
|
1992
|
-
// value: 'custom',
|
|
1993
|
-
// description: 'Full control over structure and settings',
|
|
1994
|
-
// },
|
|
1995
|
-
]
|
|
1996
|
-
});
|
|
1997
|
-
let templateName = "standard";
|
|
1998
|
-
if (setupMode === "template") {
|
|
1999
|
-
templateName = await select({
|
|
2000
|
-
message: "Select template:",
|
|
2111
|
+
let setupMode = "quick";
|
|
2112
|
+
let templateName = templateOption || "standard";
|
|
2113
|
+
if (skipPrompts) {
|
|
2114
|
+
console.log(chalk19.gray("Using defaults: quick start with standard template"));
|
|
2115
|
+
console.log("");
|
|
2116
|
+
} else if (!templateOption) {
|
|
2117
|
+
setupMode = await select({
|
|
2118
|
+
message: "How would you like to set up?",
|
|
2001
2119
|
choices: [
|
|
2002
|
-
{ name: "minimal", value: "minimal", description: "Just folder structure, no extras" },
|
|
2003
|
-
{ name: "standard", value: "standard", description: "Recommended - includes AGENTS.md" },
|
|
2004
2120
|
{
|
|
2005
|
-
name: "
|
|
2006
|
-
value: "
|
|
2007
|
-
description: "
|
|
2121
|
+
name: "Quick start (recommended)",
|
|
2122
|
+
value: "quick",
|
|
2123
|
+
description: "Use standard template, start immediately"
|
|
2124
|
+
},
|
|
2125
|
+
{
|
|
2126
|
+
name: "Choose template",
|
|
2127
|
+
value: "template",
|
|
2128
|
+
description: "Pick from: standard, detailed"
|
|
2008
2129
|
}
|
|
2130
|
+
// TODO: Re-enable when custom setup mode is implemented
|
|
2131
|
+
// {
|
|
2132
|
+
// name: 'Customize everything',
|
|
2133
|
+
// value: 'custom',
|
|
2134
|
+
// description: 'Full control over structure and settings',
|
|
2135
|
+
// },
|
|
2009
2136
|
]
|
|
2010
2137
|
});
|
|
2138
|
+
if (setupMode === "template") {
|
|
2139
|
+
templateName = await select({
|
|
2140
|
+
message: "Select template:",
|
|
2141
|
+
choices: [
|
|
2142
|
+
{ name: "standard", value: "standard", description: "Recommended - single-file specs with AGENTS.md" },
|
|
2143
|
+
{
|
|
2144
|
+
name: "detailed",
|
|
2145
|
+
value: "detailed",
|
|
2146
|
+
description: "Complex specs with sub-spec structure (DESIGN, PLAN, TEST)"
|
|
2147
|
+
}
|
|
2148
|
+
]
|
|
2149
|
+
});
|
|
2150
|
+
}
|
|
2011
2151
|
}
|
|
2012
|
-
|
|
2013
|
-
|
|
2152
|
+
if (templateName === "minimal") {
|
|
2153
|
+
console.log(chalk19.yellow('\u26A0 The "minimal" template has been removed.'));
|
|
2154
|
+
console.log(chalk19.gray(' Using "standard" template instead (same lightweight approach).'));
|
|
2155
|
+
console.log("");
|
|
2156
|
+
templateName = "standard";
|
|
2157
|
+
} else if (templateName === "enterprise") {
|
|
2158
|
+
console.log(chalk19.yellow('\u26A0 The "enterprise" template has been renamed to "detailed".'));
|
|
2159
|
+
console.log(chalk19.gray(' Using "detailed" template (sub-spec structure for complex specs).'));
|
|
2160
|
+
console.log("");
|
|
2161
|
+
templateName = "detailed";
|
|
2162
|
+
}
|
|
2163
|
+
const templateDir = path15.join(TEMPLATES_DIR, templateName);
|
|
2164
|
+
const templateConfigPath = path15.join(templateDir, "config.json");
|
|
2014
2165
|
let templateConfig;
|
|
2015
2166
|
try {
|
|
2016
2167
|
const content = await fs9.readFile(templateConfigPath, "utf-8");
|
|
2017
2168
|
templateConfig = JSON.parse(content).config;
|
|
2018
2169
|
} catch {
|
|
2019
|
-
console.error(
|
|
2170
|
+
console.error(chalk19.red(`Error: Template not found: ${templateName}`));
|
|
2020
2171
|
process.exit(1);
|
|
2021
2172
|
}
|
|
2022
2173
|
let patternChoice = "simple";
|
|
2023
|
-
if (setupMode !== "quick") {
|
|
2174
|
+
if (setupMode !== "quick" && !skipPrompts) {
|
|
2024
2175
|
patternChoice = await select({
|
|
2025
2176
|
message: "Select folder pattern:",
|
|
2026
2177
|
choices: [
|
|
@@ -2059,89 +2210,272 @@ async function initProject() {
|
|
|
2059
2210
|
templateConfig.structure.prefix = "{YYYYMMDD}-";
|
|
2060
2211
|
} else if (patternChoice === "custom") {
|
|
2061
2212
|
console.log("");
|
|
2062
|
-
console.log(
|
|
2063
|
-
console.log(
|
|
2064
|
-
console.log(
|
|
2213
|
+
console.log(chalk19.yellow("\u26A0 Custom pattern input is not yet implemented."));
|
|
2214
|
+
console.log(chalk19.gray(" You can manually edit .lean-spec/config.json after initialization."));
|
|
2215
|
+
console.log(chalk19.gray(" Using simple pattern for now."));
|
|
2065
2216
|
console.log("");
|
|
2066
2217
|
templateConfig.structure.pattern = "flat";
|
|
2067
2218
|
templateConfig.structure.prefix = "";
|
|
2068
2219
|
}
|
|
2069
|
-
const templatesDir =
|
|
2220
|
+
const templatesDir = path15.join(cwd, ".lean-spec", "templates");
|
|
2070
2221
|
try {
|
|
2071
2222
|
await fs9.mkdir(templatesDir, { recursive: true });
|
|
2072
2223
|
} catch (error) {
|
|
2073
|
-
console.error(
|
|
2224
|
+
console.error(chalk19.red("Error creating templates directory:"), error);
|
|
2074
2225
|
process.exit(1);
|
|
2075
2226
|
}
|
|
2076
|
-
const
|
|
2077
|
-
const targetSpecPath = path4.join(templatesDir, "spec-template.md");
|
|
2227
|
+
const templateFilesDir = path15.join(templateDir, "files");
|
|
2078
2228
|
try {
|
|
2079
|
-
await fs9.
|
|
2080
|
-
|
|
2229
|
+
const files = await fs9.readdir(templateFilesDir);
|
|
2230
|
+
if (templateName === "standard") {
|
|
2231
|
+
const readmePath = path15.join(templateFilesDir, "README.md");
|
|
2232
|
+
const targetSpecPath = path15.join(templatesDir, "spec-template.md");
|
|
2233
|
+
await fs9.copyFile(readmePath, targetSpecPath);
|
|
2234
|
+
console.log(chalk19.green("\u2713 Created .lean-spec/templates/spec-template.md"));
|
|
2235
|
+
templateConfig.template = "spec-template.md";
|
|
2236
|
+
templateConfig.templates = {
|
|
2237
|
+
default: "spec-template.md"
|
|
2238
|
+
};
|
|
2239
|
+
} else if (templateName === "detailed") {
|
|
2240
|
+
for (const file of files) {
|
|
2241
|
+
const srcPath = path15.join(templateFilesDir, file);
|
|
2242
|
+
const destPath = path15.join(templatesDir, file);
|
|
2243
|
+
await fs9.copyFile(srcPath, destPath);
|
|
2244
|
+
}
|
|
2245
|
+
console.log(chalk19.green(`\u2713 Created .lean-spec/templates/ with ${files.length} files`));
|
|
2246
|
+
console.log(chalk19.gray(` Files: ${files.join(", ")}`));
|
|
2247
|
+
templateConfig.template = "README.md";
|
|
2248
|
+
templateConfig.templates = {
|
|
2249
|
+
default: "README.md"
|
|
2250
|
+
};
|
|
2251
|
+
}
|
|
2081
2252
|
} catch (error) {
|
|
2082
|
-
console.error(
|
|
2253
|
+
console.error(chalk19.red("Error copying template files:"), error);
|
|
2083
2254
|
process.exit(1);
|
|
2084
2255
|
}
|
|
2085
|
-
templateConfig.template = "spec-template.md";
|
|
2086
|
-
templateConfig.templates = {
|
|
2087
|
-
default: "spec-template.md"
|
|
2088
|
-
};
|
|
2089
2256
|
await saveConfig(templateConfig, cwd);
|
|
2090
|
-
console.log(
|
|
2257
|
+
console.log(chalk19.green("\u2713 Created .lean-spec/config.json"));
|
|
2091
2258
|
const existingFiles = await detectExistingSystemPrompts(cwd);
|
|
2092
2259
|
let skipFiles = [];
|
|
2093
2260
|
if (existingFiles.length > 0) {
|
|
2094
2261
|
console.log("");
|
|
2095
|
-
console.log(
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2262
|
+
console.log(chalk19.yellow(`Found existing: ${existingFiles.join(", ")}`));
|
|
2263
|
+
if (skipPrompts) {
|
|
2264
|
+
console.log(chalk19.gray("Using AI-Assisted Merge for existing AGENTS.md"));
|
|
2265
|
+
const projectName2 = await getProjectName2(cwd);
|
|
2266
|
+
await handleExistingFiles("merge-ai", existingFiles, templateDir, cwd, { project_name: projectName2 });
|
|
2267
|
+
} else {
|
|
2268
|
+
const action = await select({
|
|
2269
|
+
message: "How would you like to handle existing AGENTS.md?",
|
|
2270
|
+
choices: [
|
|
2271
|
+
{
|
|
2272
|
+
name: "AI-Assisted Merge (recommended)",
|
|
2273
|
+
value: "merge-ai",
|
|
2274
|
+
description: "Creates prompt for AI to intelligently consolidate both files"
|
|
2275
|
+
},
|
|
2276
|
+
{
|
|
2277
|
+
name: "Simple Append",
|
|
2278
|
+
value: "merge-append",
|
|
2279
|
+
description: "Quickly appends LeanSpec section (may be verbose)"
|
|
2280
|
+
},
|
|
2281
|
+
{
|
|
2282
|
+
name: "Replace with LeanSpec",
|
|
2283
|
+
value: "overwrite",
|
|
2284
|
+
description: "Backs up existing, creates fresh AGENTS.md from template"
|
|
2285
|
+
},
|
|
2286
|
+
{
|
|
2287
|
+
name: "Keep Existing Only",
|
|
2288
|
+
value: "skip",
|
|
2289
|
+
description: "Skips AGENTS.md, only adds .lean-spec config and specs/"
|
|
2290
|
+
}
|
|
2291
|
+
]
|
|
2292
|
+
});
|
|
2293
|
+
const projectName2 = await getProjectName2(cwd);
|
|
2294
|
+
await handleExistingFiles(action, existingFiles, templateDir, cwd, { project_name: projectName2 });
|
|
2295
|
+
if (action === "skip") {
|
|
2296
|
+
skipFiles = existingFiles;
|
|
2297
|
+
}
|
|
2125
2298
|
}
|
|
2126
2299
|
}
|
|
2127
2300
|
const projectName = await getProjectName2(cwd);
|
|
2128
|
-
|
|
2301
|
+
if (!skipFiles.includes("AGENTS.md")) {
|
|
2302
|
+
const agentsSourcePath = path15.join(templateDir, "AGENTS.md");
|
|
2303
|
+
const agentsTargetPath = path15.join(cwd, "AGENTS.md");
|
|
2304
|
+
try {
|
|
2305
|
+
let agentsContent = await fs9.readFile(agentsSourcePath, "utf-8");
|
|
2306
|
+
agentsContent = agentsContent.replace(/\{project_name\}/g, projectName);
|
|
2307
|
+
await fs9.writeFile(agentsTargetPath, agentsContent, "utf-8");
|
|
2308
|
+
console.log(chalk19.green("\u2713 Created AGENTS.md"));
|
|
2309
|
+
} catch (error) {
|
|
2310
|
+
console.error(chalk19.red("Error copying AGENTS.md:"), error);
|
|
2311
|
+
process.exit(1);
|
|
2312
|
+
}
|
|
2313
|
+
}
|
|
2314
|
+
const filesDir = path15.join(templateDir, "files");
|
|
2315
|
+
try {
|
|
2316
|
+
const filesToCopy = await fs9.readdir(filesDir);
|
|
2317
|
+
const hasOtherFiles = filesToCopy.some((f) => !f.match(/\.(md)$/i) || !["README.md", "DESIGN.md", "PLAN.md", "TEST.md"].includes(f));
|
|
2318
|
+
if (hasOtherFiles) {
|
|
2319
|
+
await copyDirectory(filesDir, cwd, [...skipFiles, "README.md", "DESIGN.md", "PLAN.md", "TEST.md"], { project_name: projectName });
|
|
2320
|
+
}
|
|
2321
|
+
console.log(chalk19.green("\u2713 Initialized project structure"));
|
|
2322
|
+
} catch (error) {
|
|
2323
|
+
console.error(chalk19.red("Error copying template files:"), error);
|
|
2324
|
+
process.exit(1);
|
|
2325
|
+
}
|
|
2326
|
+
const specsDir = path15.join(cwd, "specs");
|
|
2129
2327
|
try {
|
|
2130
|
-
await
|
|
2131
|
-
console.log(
|
|
2328
|
+
await fs9.mkdir(specsDir, { recursive: true });
|
|
2329
|
+
console.log(chalk19.green("\u2713 Created specs/ directory"));
|
|
2132
2330
|
} catch (error) {
|
|
2133
|
-
console.error(
|
|
2331
|
+
console.error(chalk19.red("Error creating specs directory:"), error);
|
|
2134
2332
|
process.exit(1);
|
|
2135
2333
|
}
|
|
2136
2334
|
console.log("");
|
|
2137
|
-
console.log(
|
|
2335
|
+
console.log(chalk19.green("\u2713 LeanSpec initialized!"));
|
|
2138
2336
|
console.log("");
|
|
2139
2337
|
console.log("Next steps:");
|
|
2140
|
-
console.log(
|
|
2141
|
-
console.log(
|
|
2142
|
-
console.log(
|
|
2338
|
+
console.log(chalk19.gray(" - Review and customize AGENTS.md"));
|
|
2339
|
+
console.log(chalk19.gray(" - Check out example spec in specs/"));
|
|
2340
|
+
console.log(chalk19.gray(" - Create your first spec: lean-spec create my-feature"));
|
|
2341
|
+
console.log("");
|
|
2342
|
+
}
|
|
2343
|
+
async function listExamples() {
|
|
2344
|
+
const examples = getExamplesList();
|
|
2345
|
+
console.log("");
|
|
2346
|
+
console.log(chalk19.bold("Available Examples:"));
|
|
2347
|
+
console.log("");
|
|
2348
|
+
for (const example of examples) {
|
|
2349
|
+
const difficultyColor = example.difficulty === "beginner" ? chalk19.green : example.difficulty === "intermediate" ? chalk19.yellow : chalk19.red;
|
|
2350
|
+
console.log(chalk19.cyan(` ${example.name}`));
|
|
2351
|
+
console.log(` ${example.description}`);
|
|
2352
|
+
console.log(` ${difficultyColor(example.difficulty)} \u2022 ${example.tech.join(", ")} \u2022 ~${example.lines} lines`);
|
|
2353
|
+
console.log(` Tutorial: ${chalk19.gray(example.tutorialUrl)}`);
|
|
2354
|
+
console.log("");
|
|
2355
|
+
}
|
|
2356
|
+
console.log("Usage:");
|
|
2357
|
+
console.log(chalk19.gray(" lean-spec init --example <name>"));
|
|
2358
|
+
console.log(chalk19.gray(" lean-spec init --example dark-theme"));
|
|
2143
2359
|
console.log("");
|
|
2144
2360
|
}
|
|
2361
|
+
async function scaffoldExample(exampleName, customName) {
|
|
2362
|
+
if (!exampleName) {
|
|
2363
|
+
exampleName = await selectExample();
|
|
2364
|
+
}
|
|
2365
|
+
if (!exampleExists(exampleName)) {
|
|
2366
|
+
console.error(chalk19.red(`Error: Example "${exampleName}" not found.`));
|
|
2367
|
+
console.log("");
|
|
2368
|
+
console.log("Available examples:");
|
|
2369
|
+
getExamplesList().forEach((ex) => {
|
|
2370
|
+
console.log(` - ${ex.name}`);
|
|
2371
|
+
});
|
|
2372
|
+
console.log("");
|
|
2373
|
+
console.log("Use: lean-spec init --list");
|
|
2374
|
+
process.exit(1);
|
|
2375
|
+
}
|
|
2376
|
+
const example = getExample(exampleName);
|
|
2377
|
+
const targetDirName = customName || exampleName;
|
|
2378
|
+
const targetPath = path15.join(process.cwd(), targetDirName);
|
|
2379
|
+
try {
|
|
2380
|
+
const files = await fs9.readdir(targetPath);
|
|
2381
|
+
const nonGitFiles = files.filter((f) => f !== ".git");
|
|
2382
|
+
if (nonGitFiles.length > 0) {
|
|
2383
|
+
console.error(chalk19.red(`Error: Directory "${targetDirName}" already exists and is not empty.`));
|
|
2384
|
+
console.log(chalk19.gray("Choose a different name with --name option."));
|
|
2385
|
+
process.exit(1);
|
|
2386
|
+
}
|
|
2387
|
+
} catch {
|
|
2388
|
+
}
|
|
2389
|
+
console.log("");
|
|
2390
|
+
console.log(chalk19.green(`Setting up example: ${example.title}`));
|
|
2391
|
+
console.log(chalk19.gray(example.description));
|
|
2392
|
+
console.log("");
|
|
2393
|
+
await fs9.mkdir(targetPath, { recursive: true });
|
|
2394
|
+
console.log(chalk19.green(`\u2713 Created directory: ${targetDirName}/`));
|
|
2395
|
+
const examplePath = path15.join(EXAMPLES_DIR, exampleName);
|
|
2396
|
+
await copyDirectoryRecursive(examplePath, targetPath);
|
|
2397
|
+
console.log(chalk19.green("\u2713 Copied example project"));
|
|
2398
|
+
const originalCwd = process.cwd();
|
|
2399
|
+
try {
|
|
2400
|
+
process.chdir(targetPath);
|
|
2401
|
+
console.log(chalk19.gray("Initializing LeanSpec..."));
|
|
2402
|
+
await initProject(true);
|
|
2403
|
+
console.log(chalk19.green("\u2713 Initialized LeanSpec"));
|
|
2404
|
+
} catch (error) {
|
|
2405
|
+
console.error(chalk19.red("Error initializing LeanSpec:"), error);
|
|
2406
|
+
process.exit(1);
|
|
2407
|
+
} finally {
|
|
2408
|
+
process.chdir(originalCwd);
|
|
2409
|
+
}
|
|
2410
|
+
const packageManager = await detectPackageManager();
|
|
2411
|
+
console.log(chalk19.gray(`Installing dependencies with ${packageManager}...`));
|
|
2412
|
+
try {
|
|
2413
|
+
const { execSync: execSync3 } = await import('child_process');
|
|
2414
|
+
execSync3(`${packageManager} install`, {
|
|
2415
|
+
cwd: targetPath,
|
|
2416
|
+
stdio: "inherit"
|
|
2417
|
+
});
|
|
2418
|
+
console.log(chalk19.green("\u2713 Installed dependencies"));
|
|
2419
|
+
} catch (error) {
|
|
2420
|
+
console.log(chalk19.yellow("\u26A0 Failed to install dependencies automatically"));
|
|
2421
|
+
console.log(chalk19.gray(` Run: cd ${targetDirName} && ${packageManager} install`));
|
|
2422
|
+
}
|
|
2423
|
+
console.log("");
|
|
2424
|
+
console.log(chalk19.green("\u2713 Example project ready!"));
|
|
2425
|
+
console.log("");
|
|
2426
|
+
console.log(chalk19.gray("Created:"));
|
|
2427
|
+
console.log(chalk19.gray(` - Application code (${example.tech.join(", ")})`));
|
|
2428
|
+
console.log(chalk19.gray(" - LeanSpec files (AGENTS.md, .lean-spec/, specs/)"));
|
|
2429
|
+
console.log("");
|
|
2430
|
+
console.log("Next steps:");
|
|
2431
|
+
console.log(chalk19.cyan(` 1. cd ${targetDirName}`));
|
|
2432
|
+
console.log(chalk19.cyan(" 2. Open this project in your editor"));
|
|
2433
|
+
console.log(chalk19.cyan(` 3. Follow the tutorial: ${example.tutorialUrl}`));
|
|
2434
|
+
console.log(chalk19.cyan(` 4. Ask your AI: "Help me with this tutorial using LeanSpec"`));
|
|
2435
|
+
console.log("");
|
|
2436
|
+
}
|
|
2437
|
+
async function selectExample() {
|
|
2438
|
+
const examples = getExamplesList();
|
|
2439
|
+
const choice = await select({
|
|
2440
|
+
message: "Select an example project:",
|
|
2441
|
+
choices: examples.map((ex) => {
|
|
2442
|
+
const difficultyLabel = ex.difficulty === "beginner" ? "\u2605\u2606\u2606" : ex.difficulty === "intermediate" ? "\u2605\u2605\u2606" : "\u2605\u2605\u2605";
|
|
2443
|
+
return {
|
|
2444
|
+
name: `${ex.title} (${difficultyLabel})`,
|
|
2445
|
+
value: ex.name,
|
|
2446
|
+
description: `${ex.description} \u2022 ${ex.tech.join(", ")}`
|
|
2447
|
+
};
|
|
2448
|
+
})
|
|
2449
|
+
});
|
|
2450
|
+
return choice;
|
|
2451
|
+
}
|
|
2452
|
+
async function copyDirectoryRecursive(src, dest) {
|
|
2453
|
+
const entries = await fs9.readdir(src, { withFileTypes: true });
|
|
2454
|
+
for (const entry of entries) {
|
|
2455
|
+
const srcPath = path15.join(src, entry.name);
|
|
2456
|
+
const destPath = path15.join(dest, entry.name);
|
|
2457
|
+
if (entry.isDirectory()) {
|
|
2458
|
+
await fs9.mkdir(destPath, { recursive: true });
|
|
2459
|
+
await copyDirectoryRecursive(srcPath, destPath);
|
|
2460
|
+
} else {
|
|
2461
|
+
await fs9.copyFile(srcPath, destPath);
|
|
2462
|
+
}
|
|
2463
|
+
}
|
|
2464
|
+
}
|
|
2465
|
+
async function detectPackageManager() {
|
|
2466
|
+
const cwd = process.cwd();
|
|
2467
|
+
try {
|
|
2468
|
+
await fs9.access(path15.join(cwd, "..", "pnpm-lock.yaml"));
|
|
2469
|
+
return "pnpm";
|
|
2470
|
+
} catch {
|
|
2471
|
+
}
|
|
2472
|
+
try {
|
|
2473
|
+
await fs9.access(path15.join(cwd, "..", "yarn.lock"));
|
|
2474
|
+
return "yarn";
|
|
2475
|
+
} catch {
|
|
2476
|
+
}
|
|
2477
|
+
return "npm";
|
|
2478
|
+
}
|
|
2145
2479
|
function normalizeDateFields2(data) {
|
|
2146
2480
|
const dateFields = ["created", "completed", "updated", "due"];
|
|
2147
2481
|
for (const field of dateFields) {
|
|
@@ -2449,21 +2783,28 @@ function analyzeMarkdownStructure(content) {
|
|
|
2449
2783
|
};
|
|
2450
2784
|
}
|
|
2451
2785
|
var TokenCounter = class {
|
|
2452
|
-
encoding;
|
|
2453
|
-
|
|
2454
|
-
this.encoding
|
|
2786
|
+
encoding = null;
|
|
2787
|
+
async getEncoding() {
|
|
2788
|
+
if (!this.encoding) {
|
|
2789
|
+
const { encoding_for_model } = await import('tiktoken');
|
|
2790
|
+
this.encoding = encoding_for_model("gpt-4");
|
|
2791
|
+
}
|
|
2792
|
+
return this.encoding;
|
|
2455
2793
|
}
|
|
2456
2794
|
/**
|
|
2457
2795
|
* Clean up resources (important to prevent memory leaks)
|
|
2458
2796
|
*/
|
|
2459
2797
|
dispose() {
|
|
2460
|
-
this.encoding
|
|
2798
|
+
if (this.encoding) {
|
|
2799
|
+
this.encoding.free();
|
|
2800
|
+
}
|
|
2461
2801
|
}
|
|
2462
2802
|
/**
|
|
2463
2803
|
* Count tokens in a string
|
|
2464
2804
|
*/
|
|
2465
|
-
countString(text) {
|
|
2466
|
-
const
|
|
2805
|
+
async countString(text) {
|
|
2806
|
+
const encoding = await this.getEncoding();
|
|
2807
|
+
const tokens = encoding.encode(text);
|
|
2467
2808
|
return tokens.length;
|
|
2468
2809
|
}
|
|
2469
2810
|
/**
|
|
@@ -2478,7 +2819,7 @@ var TokenCounter = class {
|
|
|
2478
2819
|
*/
|
|
2479
2820
|
async countFile(filePath, options = {}) {
|
|
2480
2821
|
const content = await fs9.readFile(filePath, "utf-8");
|
|
2481
|
-
const tokens = this.countString(content);
|
|
2822
|
+
const tokens = await this.countString(content);
|
|
2482
2823
|
const lines = content.split("\n").length;
|
|
2483
2824
|
const result = {
|
|
2484
2825
|
total: tokens,
|
|
@@ -2526,9 +2867,9 @@ var TokenCounter = class {
|
|
|
2526
2867
|
};
|
|
2527
2868
|
}
|
|
2528
2869
|
for (const file of filesToCount) {
|
|
2529
|
-
const filePath =
|
|
2870
|
+
const filePath = path15.join(specPath, file);
|
|
2530
2871
|
const content = await fs9.readFile(filePath, "utf-8");
|
|
2531
|
-
const tokens = this.countString(content);
|
|
2872
|
+
const tokens = await this.countString(content);
|
|
2532
2873
|
const lines = content.split("\n").length;
|
|
2533
2874
|
fileCounts.push({
|
|
2534
2875
|
path: file,
|
|
@@ -2566,7 +2907,7 @@ var TokenCounter = class {
|
|
|
2566
2907
|
const parsed = matter2(content);
|
|
2567
2908
|
body = parsed.content;
|
|
2568
2909
|
frontmatterContent = parsed.matter;
|
|
2569
|
-
breakdown.frontmatter = this.countString(frontmatterContent);
|
|
2910
|
+
breakdown.frontmatter = await this.countString(frontmatterContent);
|
|
2570
2911
|
} catch {
|
|
2571
2912
|
}
|
|
2572
2913
|
let inCodeBlock = false;
|
|
@@ -2577,23 +2918,23 @@ var TokenCounter = class {
|
|
|
2577
2918
|
const trimmed = line.trim();
|
|
2578
2919
|
if (trimmed.startsWith("```")) {
|
|
2579
2920
|
inCodeBlock = !inCodeBlock;
|
|
2580
|
-
breakdown.code += this.countString(line + "\n");
|
|
2921
|
+
breakdown.code += await this.countString(line + "\n");
|
|
2581
2922
|
continue;
|
|
2582
2923
|
}
|
|
2583
2924
|
if (inCodeBlock) {
|
|
2584
|
-
breakdown.code += this.countString(line + "\n");
|
|
2925
|
+
breakdown.code += await this.countString(line + "\n");
|
|
2585
2926
|
continue;
|
|
2586
2927
|
}
|
|
2587
2928
|
const isTableSeparator = trimmed.includes("|") && /[-:]{3,}/.test(trimmed);
|
|
2588
2929
|
const isTableRow = trimmed.includes("|") && trimmed.startsWith("|");
|
|
2589
2930
|
if (isTableSeparator || inTable && isTableRow) {
|
|
2590
2931
|
inTable = true;
|
|
2591
|
-
breakdown.tables += this.countString(line + "\n");
|
|
2932
|
+
breakdown.tables += await this.countString(line + "\n");
|
|
2592
2933
|
continue;
|
|
2593
2934
|
} else if (inTable && !isTableRow) {
|
|
2594
2935
|
inTable = false;
|
|
2595
2936
|
}
|
|
2596
|
-
breakdown.prose += this.countString(line + "\n");
|
|
2937
|
+
breakdown.prose += await this.countString(line + "\n");
|
|
2597
2938
|
}
|
|
2598
2939
|
return breakdown;
|
|
2599
2940
|
}
|
|
@@ -2674,12 +3015,12 @@ async function countTokens(input, options) {
|
|
|
2674
3015
|
try {
|
|
2675
3016
|
if (typeof input === "string") {
|
|
2676
3017
|
return {
|
|
2677
|
-
total: counter.countString(input),
|
|
3018
|
+
total: await counter.countString(input),
|
|
2678
3019
|
files: []
|
|
2679
3020
|
};
|
|
2680
3021
|
} else if ("content" in input) {
|
|
2681
3022
|
return {
|
|
2682
|
-
total: counter.countString(input.content),
|
|
3023
|
+
total: await counter.countString(input.content),
|
|
2683
3024
|
files: []
|
|
2684
3025
|
};
|
|
2685
3026
|
} else if ("filePath" in input) {
|
|
@@ -2857,12 +3198,12 @@ var ComplexityValidator = class {
|
|
|
2857
3198
|
const listItemCount = lines.filter((line) => line.match(/^[\s]*[-*]\s/) || line.match(/^[\s]*\d+\.\s/)).length;
|
|
2858
3199
|
const tableCount = lines.filter((line) => line.includes("|") && line.match(/[-:]{3,}/)).length;
|
|
2859
3200
|
const counter = new TokenCounter();
|
|
2860
|
-
const tokenCount = counter.countString(content);
|
|
3201
|
+
const tokenCount = await counter.countString(content);
|
|
2861
3202
|
counter.dispose();
|
|
2862
3203
|
let hasSubSpecs = false;
|
|
2863
3204
|
let subSpecCount = 0;
|
|
2864
3205
|
try {
|
|
2865
|
-
const specDir =
|
|
3206
|
+
const specDir = path15.dirname(spec.filePath);
|
|
2866
3207
|
const files = await fs9.readdir(specDir);
|
|
2867
3208
|
const mdFiles = files.filter(
|
|
2868
3209
|
(f) => f.endsWith(".md") && f !== "README.md"
|
|
@@ -3431,7 +3772,7 @@ function filesCommand(specPath, options = {}) {
|
|
|
3431
3772
|
if (typeof specPath === "string") {
|
|
3432
3773
|
return showFiles(specPath, options);
|
|
3433
3774
|
}
|
|
3434
|
-
return new Command("files").description("List files in a spec").argument("<spec>", "Spec to list files for").option("--type <type>", "Filter by type: docs, assets").option("--tree", "Show tree structure").action(async (target, opts) => {
|
|
3775
|
+
return new Command("files").description("List files in a spec").argument("<spec>", "Spec to list files for").option("--type <type>", "Filter by type: docs, assets").option("--tree", "Show tree structure").option("--json", "Output as JSON").action(async (target, opts) => {
|
|
3435
3776
|
await showFiles(target, opts);
|
|
3436
3777
|
});
|
|
3437
3778
|
}
|
|
@@ -3439,7 +3780,7 @@ async function showFiles(specPath, options = {}) {
|
|
|
3439
3780
|
await autoCheckIfEnabled();
|
|
3440
3781
|
const config = await loadConfig();
|
|
3441
3782
|
const cwd = process.cwd();
|
|
3442
|
-
const specsDir =
|
|
3783
|
+
const specsDir = path15.join(cwd, config.specsDir);
|
|
3443
3784
|
const resolvedPath = await resolveSpecPath(specPath, cwd, specsDir);
|
|
3444
3785
|
if (!resolvedPath) {
|
|
3445
3786
|
throw new Error(`Spec not found: ${sanitizeUserInput(specPath)}. Try using the full path or spec name (e.g., 001-my-spec)`);
|
|
@@ -3449,15 +3790,40 @@ async function showFiles(specPath, options = {}) {
|
|
|
3449
3790
|
throw new Error(`Could not load spec: ${sanitizeUserInput(specPath)}`);
|
|
3450
3791
|
}
|
|
3451
3792
|
const subFiles = await loadSubFiles(spec.fullPath);
|
|
3793
|
+
if (options.json) {
|
|
3794
|
+
const readmeStat2 = await fs9.stat(spec.filePath);
|
|
3795
|
+
const readmeContent2 = await fs9.readFile(spec.filePath, "utf-8");
|
|
3796
|
+
const readmeTokens2 = await countTokens({ content: readmeContent2 });
|
|
3797
|
+
const jsonOutput = {
|
|
3798
|
+
spec: spec.name,
|
|
3799
|
+
path: spec.fullPath,
|
|
3800
|
+
files: [
|
|
3801
|
+
{
|
|
3802
|
+
name: "README.md",
|
|
3803
|
+
type: "required",
|
|
3804
|
+
size: readmeStat2.size,
|
|
3805
|
+
tokens: readmeTokens2.total
|
|
3806
|
+
},
|
|
3807
|
+
...subFiles.map((f) => ({
|
|
3808
|
+
name: f.name,
|
|
3809
|
+
type: f.type,
|
|
3810
|
+
size: f.size
|
|
3811
|
+
}))
|
|
3812
|
+
],
|
|
3813
|
+
total: subFiles.length + 1
|
|
3814
|
+
};
|
|
3815
|
+
console.log(JSON.stringify(jsonOutput, null, 2));
|
|
3816
|
+
return;
|
|
3817
|
+
}
|
|
3452
3818
|
console.log("");
|
|
3453
|
-
console.log(
|
|
3819
|
+
console.log(chalk19.cyan(`\u{1F4C4} Files in ${sanitizeUserInput(spec.name)}`));
|
|
3454
3820
|
console.log("");
|
|
3455
|
-
console.log(
|
|
3821
|
+
console.log(chalk19.green("Required:"));
|
|
3456
3822
|
const readmeStat = await fs9.stat(spec.filePath);
|
|
3457
3823
|
const readmeSize = formatSize(readmeStat.size);
|
|
3458
3824
|
const readmeContent = await fs9.readFile(spec.filePath, "utf-8");
|
|
3459
3825
|
const readmeTokens = await countTokens({ content: readmeContent });
|
|
3460
|
-
console.log(
|
|
3826
|
+
console.log(chalk19.green(` \u2713 README.md (${readmeSize}, ~${readmeTokens.total.toLocaleString()} tokens) Main spec`));
|
|
3461
3827
|
console.log("");
|
|
3462
3828
|
let filteredFiles = subFiles;
|
|
3463
3829
|
if (options.type === "docs") {
|
|
@@ -3466,27 +3832,27 @@ async function showFiles(specPath, options = {}) {
|
|
|
3466
3832
|
filteredFiles = subFiles.filter((f) => f.type === "asset");
|
|
3467
3833
|
}
|
|
3468
3834
|
if (filteredFiles.length === 0) {
|
|
3469
|
-
console.log(
|
|
3835
|
+
console.log(chalk19.gray("No additional files"));
|
|
3470
3836
|
console.log("");
|
|
3471
3837
|
return;
|
|
3472
3838
|
}
|
|
3473
3839
|
const documents = filteredFiles.filter((f) => f.type === "document");
|
|
3474
3840
|
const assets = filteredFiles.filter((f) => f.type === "asset");
|
|
3475
3841
|
if (documents.length > 0 && (!options.type || options.type === "docs")) {
|
|
3476
|
-
console.log(
|
|
3842
|
+
console.log(chalk19.cyan("Documents:"));
|
|
3477
3843
|
for (const file of documents) {
|
|
3478
3844
|
const size = formatSize(file.size);
|
|
3479
3845
|
const content = await fs9.readFile(file.path, "utf-8");
|
|
3480
3846
|
const tokenCount = await countTokens({ content });
|
|
3481
|
-
console.log(
|
|
3847
|
+
console.log(chalk19.cyan(` \u2713 ${sanitizeUserInput(file.name).padEnd(20)} (${size}, ~${tokenCount.total.toLocaleString()} tokens)`));
|
|
3482
3848
|
}
|
|
3483
3849
|
console.log("");
|
|
3484
3850
|
}
|
|
3485
3851
|
if (assets.length > 0 && (!options.type || options.type === "assets")) {
|
|
3486
|
-
console.log(
|
|
3852
|
+
console.log(chalk19.yellow("Assets:"));
|
|
3487
3853
|
for (const file of assets) {
|
|
3488
3854
|
const size = formatSize(file.size);
|
|
3489
|
-
console.log(
|
|
3855
|
+
console.log(chalk19.yellow(` \u2713 ${sanitizeUserInput(file.name).padEnd(20)} (${size})`));
|
|
3490
3856
|
}
|
|
3491
3857
|
console.log("");
|
|
3492
3858
|
}
|
|
@@ -3494,7 +3860,7 @@ async function showFiles(specPath, options = {}) {
|
|
|
3494
3860
|
const totalSize = formatSize(
|
|
3495
3861
|
readmeStat.size + filteredFiles.reduce((sum, f) => sum + f.size, 0)
|
|
3496
3862
|
);
|
|
3497
|
-
console.log(
|
|
3863
|
+
console.log(chalk19.gray(`Total: ${totalFiles} files, ${totalSize}`));
|
|
3498
3864
|
console.log("");
|
|
3499
3865
|
}
|
|
3500
3866
|
function formatSize(bytes) {
|
|
@@ -3506,6 +3872,41 @@ function formatSize(bytes) {
|
|
|
3506
3872
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
3507
3873
|
}
|
|
3508
3874
|
}
|
|
3875
|
+
function examplesCommand() {
|
|
3876
|
+
return new Command("examples").description("List available example projects for tutorials").action(async () => {
|
|
3877
|
+
await listExamples2();
|
|
3878
|
+
});
|
|
3879
|
+
}
|
|
3880
|
+
async function listExamples2() {
|
|
3881
|
+
const examples = getExamplesList();
|
|
3882
|
+
console.log("");
|
|
3883
|
+
console.log(chalk19.bold("LeanSpec Example Projects"));
|
|
3884
|
+
console.log("");
|
|
3885
|
+
console.log("Scaffold complete example projects to follow tutorials:");
|
|
3886
|
+
console.log("");
|
|
3887
|
+
for (const example of examples) {
|
|
3888
|
+
const difficultyColor = example.difficulty === "beginner" ? chalk19.green : example.difficulty === "intermediate" ? chalk19.yellow : chalk19.red;
|
|
3889
|
+
const difficultyStars = example.difficulty === "beginner" ? "\u2605\u2606\u2606" : example.difficulty === "intermediate" ? "\u2605\u2605\u2606" : "\u2605\u2605\u2605";
|
|
3890
|
+
console.log(chalk19.cyan.bold(` ${example.title}`));
|
|
3891
|
+
console.log(` ${chalk19.gray(example.name)}`);
|
|
3892
|
+
console.log(` ${example.description}`);
|
|
3893
|
+
console.log(` ${difficultyColor(difficultyStars + " " + example.difficulty)} \u2022 ${example.tech.join(", ")} \u2022 ~${example.lines} lines`);
|
|
3894
|
+
console.log(` ${chalk19.gray("Tutorial:")} ${example.tutorial}`);
|
|
3895
|
+
console.log(` ${chalk19.gray(example.tutorialUrl)}`);
|
|
3896
|
+
console.log("");
|
|
3897
|
+
}
|
|
3898
|
+
console.log(chalk19.bold("Usage:"));
|
|
3899
|
+
console.log("");
|
|
3900
|
+
console.log(" # Scaffold an example");
|
|
3901
|
+
console.log(chalk19.cyan(" lean-spec init --example <name>"));
|
|
3902
|
+
console.log("");
|
|
3903
|
+
console.log(" # Interactive selection");
|
|
3904
|
+
console.log(chalk19.cyan(" lean-spec init --example"));
|
|
3905
|
+
console.log("");
|
|
3906
|
+
console.log(" # Custom directory name");
|
|
3907
|
+
console.log(chalk19.cyan(" lean-spec init --example dark-theme --name my-demo"));
|
|
3908
|
+
console.log("");
|
|
3909
|
+
}
|
|
3509
3910
|
var FrontmatterValidator = class {
|
|
3510
3911
|
name = "frontmatter";
|
|
3511
3912
|
description = "Validate spec frontmatter for required fields and valid values";
|
|
@@ -3993,7 +4394,7 @@ var SubSpecValidator = class {
|
|
|
3993
4394
|
*/
|
|
3994
4395
|
validateNamingConventions(subSpecs, warnings) {
|
|
3995
4396
|
for (const subSpec of subSpecs) {
|
|
3996
|
-
const baseName =
|
|
4397
|
+
const baseName = path15.basename(subSpec.name, ".md");
|
|
3997
4398
|
if (baseName !== baseName.toUpperCase()) {
|
|
3998
4399
|
warnings.push({
|
|
3999
4400
|
message: `Sub-spec filename should be uppercase: ${subSpec.name}`,
|
|
@@ -4157,17 +4558,17 @@ function formatFileIssues(fileResult, specsDir) {
|
|
|
4157
4558
|
const priority = fileResult.spec.frontmatter.priority || "medium";
|
|
4158
4559
|
const statusBadge = formatStatusBadge(status);
|
|
4159
4560
|
const priorityBadge = formatPriorityBadge(priority);
|
|
4160
|
-
lines.push(
|
|
4561
|
+
lines.push(chalk19.bold.cyan(`${specName} ${statusBadge} ${priorityBadge}`));
|
|
4161
4562
|
} else {
|
|
4162
|
-
lines.push(
|
|
4563
|
+
lines.push(chalk19.cyan.underline(relativePath));
|
|
4163
4564
|
}
|
|
4164
4565
|
for (const issue of fileResult.issues) {
|
|
4165
|
-
const severityColor = issue.severity === "error" ?
|
|
4566
|
+
const severityColor = issue.severity === "error" ? chalk19.red : chalk19.yellow;
|
|
4166
4567
|
const severityText = severityColor(issue.severity.padEnd(9));
|
|
4167
|
-
const ruleText =
|
|
4568
|
+
const ruleText = chalk19.gray(issue.ruleName);
|
|
4168
4569
|
lines.push(` ${severityText}${issue.message.padEnd(60)} ${ruleText}`);
|
|
4169
4570
|
if (issue.suggestion) {
|
|
4170
|
-
lines.push(
|
|
4571
|
+
lines.push(chalk19.gray(` \u2192 ${issue.suggestion}`));
|
|
4171
4572
|
}
|
|
4172
4573
|
}
|
|
4173
4574
|
lines.push("");
|
|
@@ -4177,25 +4578,25 @@ function formatSummary(totalSpecs, errorCount, warningCount, cleanCount) {
|
|
|
4177
4578
|
if (errorCount > 0) {
|
|
4178
4579
|
const errorText = errorCount === 1 ? "error" : "errors";
|
|
4179
4580
|
const warningText = warningCount === 1 ? "warning" : "warnings";
|
|
4180
|
-
return
|
|
4581
|
+
return chalk19.red.bold(
|
|
4181
4582
|
`\u2716 ${errorCount} ${errorText}, ${warningCount} ${warningText} (${totalSpecs} specs checked, ${cleanCount} clean)`
|
|
4182
4583
|
);
|
|
4183
4584
|
} else if (warningCount > 0) {
|
|
4184
4585
|
const warningText = warningCount === 1 ? "warning" : "warnings";
|
|
4185
|
-
return
|
|
4586
|
+
return chalk19.yellow.bold(
|
|
4186
4587
|
`\u26A0 ${warningCount} ${warningText} (${totalSpecs} specs checked, ${cleanCount} clean)`
|
|
4187
4588
|
);
|
|
4188
4589
|
} else {
|
|
4189
|
-
return
|
|
4590
|
+
return chalk19.green.bold(`\u2713 All ${totalSpecs} specs passed`);
|
|
4190
4591
|
}
|
|
4191
4592
|
}
|
|
4192
4593
|
function formatPassingSpecs(specs, specsDir) {
|
|
4193
4594
|
const lines = [];
|
|
4194
|
-
lines.push(
|
|
4595
|
+
lines.push(chalk19.green.bold(`
|
|
4195
4596
|
\u2713 ${specs.length} specs passed:`));
|
|
4196
4597
|
for (const spec of specs) {
|
|
4197
4598
|
const relativePath = normalizeFilePath(spec.filePath);
|
|
4198
|
-
lines.push(
|
|
4599
|
+
lines.push(chalk19.gray(` ${relativePath}`));
|
|
4199
4600
|
}
|
|
4200
4601
|
return lines.join("\n");
|
|
4201
4602
|
}
|
|
@@ -4241,13 +4642,13 @@ function formatValidationResults(results, specs, specsDir, options = {}) {
|
|
|
4241
4642
|
return formatJson(displayResults, specs.length, errorCount2, warningCount2);
|
|
4242
4643
|
}
|
|
4243
4644
|
const lines = [];
|
|
4244
|
-
lines.push(
|
|
4645
|
+
lines.push(chalk19.bold(`
|
|
4245
4646
|
Validating ${specs.length} specs...
|
|
4246
4647
|
`));
|
|
4247
4648
|
let previousSpecName;
|
|
4248
4649
|
for (const fileResult of displayResults) {
|
|
4249
4650
|
if (fileResult.spec && previousSpecName && fileResult.spec.name !== previousSpecName) {
|
|
4250
|
-
lines.push(
|
|
4651
|
+
lines.push(chalk19.gray("\u2500".repeat(80)));
|
|
4251
4652
|
lines.push("");
|
|
4252
4653
|
}
|
|
4253
4654
|
lines.push(formatFileIssues(fileResult));
|
|
@@ -4271,20 +4672,20 @@ Validating ${specs.length} specs...
|
|
|
4271
4672
|
lines.push(formatPassingSpecs(passingSpecs));
|
|
4272
4673
|
}
|
|
4273
4674
|
if (!options.verbose && cleanCount > 0 && displayResults.length > 0) {
|
|
4274
|
-
lines.push(
|
|
4675
|
+
lines.push(chalk19.gray("\nRun with --verbose to see passing specs."));
|
|
4275
4676
|
}
|
|
4276
4677
|
return lines.join("\n");
|
|
4277
4678
|
}
|
|
4278
4679
|
|
|
4279
4680
|
// src/commands/validate.ts
|
|
4280
4681
|
function validateCommand() {
|
|
4281
|
-
return new Command("validate").description("Validate specs for quality issues").argument("[specs...]", "Specific specs to validate (optional)").option("--max-lines <number>", "Custom line limit (default: 400)", parseInt).option("--verbose", "Show passing specs").option("--quiet", "Suppress warnings, only show errors").option("--format <format>", "Output format: default, json, compact", "default").option("--rule <rule>", "Filter by specific rule name (e.g., max-lines, frontmatter)").option("--warnings-only", "Treat all issues as warnings, never fail (useful for CI pre-release checks)").action(async (specs, options) => {
|
|
4682
|
+
return new Command("validate").description("Validate specs for quality issues").argument("[specs...]", "Specific specs to validate (optional)").option("--max-lines <number>", "Custom line limit (default: 400)", parseInt).option("--verbose", "Show passing specs").option("--quiet", "Suppress warnings, only show errors").option("--format <format>", "Output format: default, json, compact", "default").option("--json", "Output as JSON (shorthand for --format json)").option("--rule <rule>", "Filter by specific rule name (e.g., max-lines, frontmatter)").option("--warnings-only", "Treat all issues as warnings, never fail (useful for CI pre-release checks)").action(async (specs, options) => {
|
|
4282
4683
|
const passed = await validateSpecs({
|
|
4283
4684
|
maxLines: options.maxLines,
|
|
4284
4685
|
specs: specs && specs.length > 0 ? specs : void 0,
|
|
4285
4686
|
verbose: options.verbose,
|
|
4286
4687
|
quiet: options.quiet,
|
|
4287
|
-
format: options.format,
|
|
4688
|
+
format: options.json ? "json" : options.format,
|
|
4288
4689
|
rule: options.rule,
|
|
4289
4690
|
warningsOnly: options.warningsOnly
|
|
4290
4691
|
});
|
|
@@ -4299,12 +4700,12 @@ async function validateSpecs(options = {}) {
|
|
|
4299
4700
|
specs = [];
|
|
4300
4701
|
for (const specPath of options.specs) {
|
|
4301
4702
|
const spec = allSpecs.find(
|
|
4302
|
-
(s) => s.path.includes(specPath) ||
|
|
4703
|
+
(s) => s.path.includes(specPath) || path15.basename(s.path).includes(specPath)
|
|
4303
4704
|
);
|
|
4304
4705
|
if (spec) {
|
|
4305
4706
|
specs.push(spec);
|
|
4306
4707
|
} else {
|
|
4307
|
-
console.error(
|
|
4708
|
+
console.error(chalk19.red(`Error: Spec not found: ${specPath}`));
|
|
4308
4709
|
return false;
|
|
4309
4710
|
}
|
|
4310
4711
|
}
|
|
@@ -4332,7 +4733,7 @@ async function validateSpecs(options = {}) {
|
|
|
4332
4733
|
try {
|
|
4333
4734
|
content = await fs9.readFile(spec.filePath, "utf-8");
|
|
4334
4735
|
} catch (error) {
|
|
4335
|
-
console.error(
|
|
4736
|
+
console.error(chalk19.red(`Error reading ${spec.filePath}:`), error);
|
|
4336
4737
|
continue;
|
|
4337
4738
|
}
|
|
4338
4739
|
for (const validator of validators) {
|
|
@@ -4345,7 +4746,7 @@ async function validateSpecs(options = {}) {
|
|
|
4345
4746
|
content
|
|
4346
4747
|
});
|
|
4347
4748
|
} catch (error) {
|
|
4348
|
-
console.error(
|
|
4749
|
+
console.error(chalk19.yellow(`Warning: Validator ${validator.name} failed:`), error instanceof Error ? error.message : error);
|
|
4349
4750
|
}
|
|
4350
4751
|
}
|
|
4351
4752
|
}
|
|
@@ -4414,7 +4815,7 @@ async function scanDocuments(dirPath) {
|
|
|
4414
4815
|
async function scanRecursive(currentPath) {
|
|
4415
4816
|
const entries = await fs9.readdir(currentPath, { withFileTypes: true });
|
|
4416
4817
|
for (const entry of entries) {
|
|
4417
|
-
const fullPath =
|
|
4818
|
+
const fullPath = path15.join(currentPath, entry.name);
|
|
4418
4819
|
if (entry.isDirectory()) {
|
|
4419
4820
|
if (!entry.name.startsWith(".") && entry.name !== "node_modules") {
|
|
4420
4821
|
await scanRecursive(fullPath);
|
|
@@ -4759,7 +5160,7 @@ function calculateVelocityMetrics(specs) {
|
|
|
4759
5160
|
|
|
4760
5161
|
// src/commands/board.ts
|
|
4761
5162
|
function boardCommand() {
|
|
4762
|
-
return new Command("board").description("Show Kanban-style board view with project completion summary").option("--complete", "Include complete specs (default: hidden)").option("--simple", "Hide completion summary (kanban only)").option("--completion-only", "Show only completion summary (no kanban)").option("--tag <tag>", "Filter by tag").option("--assignee <name>", "Filter by assignee").action(async (options) => {
|
|
5163
|
+
return new Command("board").description("Show Kanban-style board view with project completion summary").option("--complete", "Include complete specs (default: hidden)").option("--simple", "Hide completion summary (kanban only)").option("--completion-only", "Show only completion summary (no kanban)").option("--tag <tag>", "Filter by tag").option("--assignee <name>", "Filter by assignee").option("--json", "Output as JSON").action(async (options) => {
|
|
4763
5164
|
await showBoard(options);
|
|
4764
5165
|
});
|
|
4765
5166
|
}
|
|
@@ -4780,7 +5181,11 @@ async function showBoard(options) {
|
|
|
4780
5181
|
})
|
|
4781
5182
|
);
|
|
4782
5183
|
if (specs.length === 0) {
|
|
4783
|
-
|
|
5184
|
+
if (options.json) {
|
|
5185
|
+
console.log(JSON.stringify({ columns: {}, total: 0 }, null, 2));
|
|
5186
|
+
} else {
|
|
5187
|
+
console.log(chalk19.dim("No specs found."));
|
|
5188
|
+
}
|
|
4784
5189
|
return;
|
|
4785
5190
|
}
|
|
4786
5191
|
const columns = {
|
|
@@ -4795,12 +5200,35 @@ async function showBoard(options) {
|
|
|
4795
5200
|
columns[status].push(spec);
|
|
4796
5201
|
}
|
|
4797
5202
|
}
|
|
4798
|
-
|
|
5203
|
+
if (options.json) {
|
|
5204
|
+
const completionMetrics = calculateCompletion(specs);
|
|
5205
|
+
const velocityMetrics = calculateVelocityMetrics(specs);
|
|
5206
|
+
const jsonOutput = {
|
|
5207
|
+
columns: {
|
|
5208
|
+
planned: columns.planned.map((s) => ({ path: s.path, priority: s.frontmatter.priority, assignee: s.frontmatter.assignee, tags: s.frontmatter.tags })),
|
|
5209
|
+
"in-progress": columns["in-progress"].map((s) => ({ path: s.path, priority: s.frontmatter.priority, assignee: s.frontmatter.assignee, tags: s.frontmatter.tags })),
|
|
5210
|
+
complete: columns.complete.map((s) => ({ path: s.path, priority: s.frontmatter.priority, assignee: s.frontmatter.assignee, tags: s.frontmatter.tags }))
|
|
5211
|
+
},
|
|
5212
|
+
summary: {
|
|
5213
|
+
total: completionMetrics.totalSpecs,
|
|
5214
|
+
active: completionMetrics.activeSpecs,
|
|
5215
|
+
complete: completionMetrics.completeSpecs,
|
|
5216
|
+
completionRate: completionMetrics.score,
|
|
5217
|
+
velocity: {
|
|
5218
|
+
avgCycleTime: velocityMetrics.cycleTime.average,
|
|
5219
|
+
throughputPerWeek: velocityMetrics.throughput.perWeek / 7 * 7
|
|
5220
|
+
}
|
|
5221
|
+
}
|
|
5222
|
+
};
|
|
5223
|
+
console.log(JSON.stringify(jsonOutput, null, 2));
|
|
5224
|
+
return;
|
|
5225
|
+
}
|
|
5226
|
+
console.log(chalk19.bold.cyan("\u{1F4CB} Spec Kanban Board"));
|
|
4799
5227
|
if (options.tag || options.assignee) {
|
|
4800
5228
|
const filterParts = [];
|
|
4801
5229
|
if (options.tag) filterParts.push(`tag=${options.tag}`);
|
|
4802
5230
|
if (options.assignee) filterParts.push(`assignee=${options.assignee}`);
|
|
4803
|
-
console.log(
|
|
5231
|
+
console.log(chalk19.dim(`Filtered by: ${filterParts.join(", ")}`));
|
|
4804
5232
|
}
|
|
4805
5233
|
console.log("");
|
|
4806
5234
|
if (!options.simple) {
|
|
@@ -4815,12 +5243,12 @@ async function showBoard(options) {
|
|
|
4815
5243
|
const padding = boxWidth - 2 - visibleLength;
|
|
4816
5244
|
return content + " ".repeat(Math.max(0, padding));
|
|
4817
5245
|
};
|
|
4818
|
-
console.log(
|
|
4819
|
-
const headerLine =
|
|
4820
|
-
console.log(
|
|
4821
|
-
const percentageColor = completionMetrics.score >= 70 ?
|
|
5246
|
+
console.log(chalk19.dim(topBorder));
|
|
5247
|
+
const headerLine = chalk19.bold(" Project Overview");
|
|
5248
|
+
console.log(chalk19.dim("\u2551") + padLine(headerLine) + chalk19.dim("\u2551"));
|
|
5249
|
+
const percentageColor = completionMetrics.score >= 70 ? chalk19.green : completionMetrics.score >= 40 ? chalk19.yellow : chalk19.red;
|
|
4822
5250
|
const line1 = ` ${completionMetrics.totalSpecs} total \xB7 ${completionMetrics.activeSpecs} active \xB7 ${completionMetrics.completeSpecs} complete ${percentageColor("(" + completionMetrics.score + "%)")}`;
|
|
4823
|
-
console.log(
|
|
5251
|
+
console.log(chalk19.dim("\u2551") + padLine(line1) + chalk19.dim("\u2551"));
|
|
4824
5252
|
if (completionMetrics.criticalIssues.length > 0 || completionMetrics.warnings.length > 0) {
|
|
4825
5253
|
const alerts = [];
|
|
4826
5254
|
if (completionMetrics.criticalIssues.length > 0) {
|
|
@@ -4829,27 +5257,27 @@ async function showBoard(options) {
|
|
|
4829
5257
|
if (completionMetrics.warnings.length > 0) {
|
|
4830
5258
|
alerts.push(`${completionMetrics.warnings.length} specs WIP > 7 days`);
|
|
4831
5259
|
}
|
|
4832
|
-
const alertLine = ` ${
|
|
4833
|
-
console.log(
|
|
5260
|
+
const alertLine = ` ${chalk19.yellow("\u26A0\uFE0F " + alerts.join(" \xB7 "))}`;
|
|
5261
|
+
console.log(chalk19.dim("\u2551") + padLine(alertLine) + chalk19.dim("\u2551"));
|
|
4834
5262
|
}
|
|
4835
|
-
const velocityLine = ` ${
|
|
4836
|
-
console.log(
|
|
4837
|
-
console.log(
|
|
5263
|
+
const velocityLine = ` ${chalk19.cyan("\u{1F680} Velocity:")} ${velocityMetrics.cycleTime.average.toFixed(1)}d avg cycle \xB7 ${(velocityMetrics.throughput.perWeek / 7 * 7).toFixed(1)}/wk throughput`;
|
|
5264
|
+
console.log(chalk19.dim("\u2551") + padLine(velocityLine) + chalk19.dim("\u2551"));
|
|
5265
|
+
console.log(chalk19.dim(bottomBorder));
|
|
4838
5266
|
console.log("");
|
|
4839
5267
|
if (options.completionOnly) {
|
|
4840
5268
|
return;
|
|
4841
5269
|
}
|
|
4842
5270
|
}
|
|
4843
5271
|
renderColumn(STATUS_CONFIG.planned.label, STATUS_CONFIG.planned.emoji, columns.planned, true, STATUS_CONFIG.planned.colorFn);
|
|
4844
|
-
console.log(
|
|
5272
|
+
console.log(chalk19.dim("\u2501".repeat(70)));
|
|
4845
5273
|
console.log("");
|
|
4846
5274
|
renderColumn(STATUS_CONFIG["in-progress"].label, STATUS_CONFIG["in-progress"].emoji, columns["in-progress"], true, STATUS_CONFIG["in-progress"].colorFn);
|
|
4847
|
-
console.log(
|
|
5275
|
+
console.log(chalk19.dim("\u2501".repeat(70)));
|
|
4848
5276
|
console.log("");
|
|
4849
5277
|
renderColumn(STATUS_CONFIG.complete.label, STATUS_CONFIG.complete.emoji, columns.complete, options.showComplete || false, STATUS_CONFIG.complete.colorFn);
|
|
4850
5278
|
}
|
|
4851
5279
|
function renderColumn(title, emoji, specs, expanded, colorFn) {
|
|
4852
|
-
console.log(`${emoji} ${colorFn(
|
|
5280
|
+
console.log(`${emoji} ${colorFn(chalk19.bold(`${title} (${specs.length})`))}`);
|
|
4853
5281
|
console.log("");
|
|
4854
5282
|
if (expanded && specs.length > 0) {
|
|
4855
5283
|
const priorityGroups = {
|
|
@@ -4874,30 +5302,30 @@ function renderColumn(title, emoji, specs, expanded, colorFn) {
|
|
|
4874
5302
|
firstGroup = false;
|
|
4875
5303
|
const priorityLabel = priority === "none" ? "No Priority" : priority.charAt(0).toUpperCase() + priority.slice(1);
|
|
4876
5304
|
const priorityEmoji = priority === "none" ? "\u26AA" : PRIORITY_CONFIG[priority].emoji;
|
|
4877
|
-
const priorityColor = priority === "none" ?
|
|
4878
|
-
console.log(` ${priorityColor(`${priorityEmoji} ${
|
|
5305
|
+
const priorityColor = priority === "none" ? chalk19.dim : PRIORITY_CONFIG[priority].colorFn;
|
|
5306
|
+
console.log(` ${priorityColor(`${priorityEmoji} ${chalk19.bold(priorityLabel)} ${chalk19.dim(`(${groupSpecs.length})`)}`)}`);
|
|
4879
5307
|
for (const spec of groupSpecs) {
|
|
4880
5308
|
let assigneeStr = "";
|
|
4881
5309
|
if (spec.frontmatter.assignee) {
|
|
4882
|
-
assigneeStr = " " +
|
|
5310
|
+
assigneeStr = " " + chalk19.cyan(`@${sanitizeUserInput(spec.frontmatter.assignee)}`);
|
|
4883
5311
|
}
|
|
4884
5312
|
let tagsStr = "";
|
|
4885
5313
|
if (spec.frontmatter.tags?.length) {
|
|
4886
5314
|
const tags = Array.isArray(spec.frontmatter.tags) ? spec.frontmatter.tags : [];
|
|
4887
5315
|
if (tags.length > 0) {
|
|
4888
5316
|
const tagStr = tags.map((tag) => `#${sanitizeUserInput(tag)}`).join(" ");
|
|
4889
|
-
tagsStr = " " +
|
|
5317
|
+
tagsStr = " " + chalk19.dim(chalk19.magenta(tagStr));
|
|
4890
5318
|
}
|
|
4891
5319
|
}
|
|
4892
|
-
console.log(` ${
|
|
5320
|
+
console.log(` ${chalk19.cyan(sanitizeUserInput(spec.path))}${assigneeStr}${tagsStr}`);
|
|
4893
5321
|
}
|
|
4894
5322
|
}
|
|
4895
5323
|
console.log("");
|
|
4896
5324
|
} else if (!expanded && specs.length > 0) {
|
|
4897
|
-
console.log(` ${
|
|
5325
|
+
console.log(` ${chalk19.dim("(collapsed, use --complete to expand)")}`);
|
|
4898
5326
|
console.log("");
|
|
4899
5327
|
} else {
|
|
4900
|
-
console.log(` ${
|
|
5328
|
+
console.log(` ${chalk19.dim("(empty)")}`);
|
|
4901
5329
|
console.log("");
|
|
4902
5330
|
}
|
|
4903
5331
|
}
|
|
@@ -5066,26 +5494,26 @@ async function showStats(options) {
|
|
|
5066
5494
|
console.log(JSON.stringify(data, null, 2));
|
|
5067
5495
|
return;
|
|
5068
5496
|
}
|
|
5069
|
-
console.log(
|
|
5497
|
+
console.log(chalk19.bold.cyan("\u{1F4CA} Spec Stats"));
|
|
5070
5498
|
console.log("");
|
|
5071
5499
|
if (options.tag || options.assignee) {
|
|
5072
5500
|
const filterParts = [];
|
|
5073
5501
|
if (options.tag) filterParts.push(`tag=${options.tag}`);
|
|
5074
5502
|
if (options.assignee) filterParts.push(`assignee=${options.assignee}`);
|
|
5075
|
-
console.log(
|
|
5503
|
+
console.log(chalk19.dim(`Filtered by: ${filterParts.join(", ")}`));
|
|
5076
5504
|
console.log("");
|
|
5077
5505
|
}
|
|
5078
5506
|
if (showSimplified) {
|
|
5079
|
-
console.log(
|
|
5507
|
+
console.log(chalk19.bold("\u{1F4C8} Overview"));
|
|
5080
5508
|
console.log("");
|
|
5081
5509
|
const completionStatus = getCompletionStatus(completionMetrics.score);
|
|
5082
|
-
const completionColor = completionStatus.color === "green" ?
|
|
5083
|
-
console.log(` Total Specs ${
|
|
5084
|
-
console.log(` Active (Planned+WIP) ${
|
|
5085
|
-
console.log(` Complete ${
|
|
5510
|
+
const completionColor = completionStatus.color === "green" ? chalk19.green : completionStatus.color === "yellow" ? chalk19.yellow : chalk19.red;
|
|
5511
|
+
console.log(` Total Specs ${chalk19.cyan(completionMetrics.totalSpecs)}`);
|
|
5512
|
+
console.log(` Active (Planned+WIP) ${chalk19.yellow(completionMetrics.activeSpecs)}`);
|
|
5513
|
+
console.log(` Complete ${chalk19.green(completionMetrics.completeSpecs)}`);
|
|
5086
5514
|
console.log(` Completion Rate ${completionColor(`${completionMetrics.score}% ${completionStatus.emoji}`)}`);
|
|
5087
5515
|
console.log("");
|
|
5088
|
-
console.log(
|
|
5516
|
+
console.log(chalk19.bold("\u{1F4CA} Status"));
|
|
5089
5517
|
console.log("");
|
|
5090
5518
|
const labelWidth2 = 15;
|
|
5091
5519
|
const barWidth2 = 20;
|
|
@@ -5094,19 +5522,19 @@ async function showStats(options) {
|
|
|
5094
5522
|
const width = Math.round(count / maxCount * barWidth2);
|
|
5095
5523
|
const filledWidth = Math.min(width, barWidth2);
|
|
5096
5524
|
const emptyWidth = barWidth2 - filledWidth;
|
|
5097
|
-
return char.repeat(filledWidth) +
|
|
5525
|
+
return char.repeat(filledWidth) + chalk19.dim("\u2591").repeat(emptyWidth);
|
|
5098
5526
|
};
|
|
5099
|
-
console.log(` \u{1F4C5} ${"Planned".padEnd(labelWidth2)} ${
|
|
5100
|
-
console.log(` \u23F3 ${"In Progress".padEnd(labelWidth2)} ${
|
|
5101
|
-
console.log(` \u2705 ${"Complete".padEnd(labelWidth2)} ${
|
|
5527
|
+
console.log(` \u{1F4C5} ${"Planned".padEnd(labelWidth2)} ${chalk19.cyan(createBar2(statusCounts.planned, maxStatusCount))} ${chalk19.cyan(statusCounts.planned)}`);
|
|
5528
|
+
console.log(` \u23F3 ${"In Progress".padEnd(labelWidth2)} ${chalk19.yellow(createBar2(statusCounts["in-progress"], maxStatusCount))} ${chalk19.yellow(statusCounts["in-progress"])}`);
|
|
5529
|
+
console.log(` \u2705 ${"Complete".padEnd(labelWidth2)} ${chalk19.green(createBar2(statusCounts.complete, maxStatusCount))} ${chalk19.green(statusCounts.complete)}`);
|
|
5102
5530
|
if (statusCounts.archived > 0) {
|
|
5103
|
-
console.log(` \u{1F4E6} ${"Archived".padEnd(labelWidth2)} ${
|
|
5531
|
+
console.log(` \u{1F4E6} ${"Archived".padEnd(labelWidth2)} ${chalk19.dim(createBar2(statusCounts.archived, maxStatusCount))} ${chalk19.dim(statusCounts.archived)}`);
|
|
5104
5532
|
}
|
|
5105
5533
|
console.log("");
|
|
5106
5534
|
const criticalCount = priorityCounts.critical || 0;
|
|
5107
5535
|
const highCount = priorityCounts.high || 0;
|
|
5108
5536
|
if (criticalCount > 0 || highCount > 0) {
|
|
5109
|
-
console.log(
|
|
5537
|
+
console.log(chalk19.bold("\u{1F3AF} Priority Focus"));
|
|
5110
5538
|
console.log("");
|
|
5111
5539
|
if (criticalCount > 0) {
|
|
5112
5540
|
const criticalPlanned = specs.filter((s) => s.frontmatter.priority === "critical" && s.frontmatter.status === "planned").length;
|
|
@@ -5116,11 +5544,11 @@ async function showStats(options) {
|
|
|
5116
5544
|
(s) => s.frontmatter.priority === "critical" && s.frontmatter.due && dayjs3(s.frontmatter.due).isBefore(dayjs3(), "day") && s.frontmatter.status !== "complete"
|
|
5117
5545
|
).length;
|
|
5118
5546
|
const parts = [];
|
|
5119
|
-
if (criticalPlanned > 0) parts.push(
|
|
5547
|
+
if (criticalPlanned > 0) parts.push(chalk19.dim(`${criticalPlanned} planned`));
|
|
5120
5548
|
if (criticalInProgress > 0) parts.push(`${criticalInProgress} in-progress`);
|
|
5121
|
-
if (criticalComplete > 0) parts.push(
|
|
5122
|
-
if (criticalOverdue > 0) parts.push(
|
|
5123
|
-
console.log(` \u{1F534} Critical ${
|
|
5549
|
+
if (criticalComplete > 0) parts.push(chalk19.green(`${criticalComplete} complete`));
|
|
5550
|
+
if (criticalOverdue > 0) parts.push(chalk19.red(`${criticalOverdue} overdue!`));
|
|
5551
|
+
console.log(` \u{1F534} Critical ${chalk19.red(criticalCount)} specs${parts.length > 0 ? ` (${parts.join(", ")})` : ""}`);
|
|
5124
5552
|
}
|
|
5125
5553
|
if (highCount > 0) {
|
|
5126
5554
|
const highPlanned = specs.filter((s) => s.frontmatter.priority === "high" && s.frontmatter.status === "planned").length;
|
|
@@ -5130,52 +5558,52 @@ async function showStats(options) {
|
|
|
5130
5558
|
(s) => s.frontmatter.priority === "high" && s.frontmatter.due && dayjs3(s.frontmatter.due).isBefore(dayjs3(), "day") && s.frontmatter.status !== "complete"
|
|
5131
5559
|
).length;
|
|
5132
5560
|
const parts = [];
|
|
5133
|
-
if (highPlanned > 0) parts.push(
|
|
5561
|
+
if (highPlanned > 0) parts.push(chalk19.dim(`${highPlanned} planned`));
|
|
5134
5562
|
if (highInProgress > 0) parts.push(`${highInProgress} in-progress`);
|
|
5135
|
-
if (highComplete > 0) parts.push(
|
|
5136
|
-
if (highOverdue > 0) parts.push(
|
|
5137
|
-
console.log(` \u{1F7E0} High ${
|
|
5563
|
+
if (highComplete > 0) parts.push(chalk19.green(`${highComplete} complete`));
|
|
5564
|
+
if (highOverdue > 0) parts.push(chalk19.yellow(`${highOverdue} overdue`));
|
|
5565
|
+
console.log(` \u{1F7E0} High ${chalk19.hex("#FFA500")(highCount)} specs${parts.length > 0 ? ` (${parts.join(", ")})` : ""}`);
|
|
5138
5566
|
}
|
|
5139
5567
|
console.log("");
|
|
5140
5568
|
}
|
|
5141
5569
|
if (insights.length > 0) {
|
|
5142
|
-
console.log(
|
|
5570
|
+
console.log(chalk19.bold.yellow("\u26A0\uFE0F Needs Attention"));
|
|
5143
5571
|
console.log("");
|
|
5144
5572
|
for (const insight of insights) {
|
|
5145
|
-
const color = insight.severity === "critical" ?
|
|
5573
|
+
const color = insight.severity === "critical" ? chalk19.red : insight.severity === "warning" ? chalk19.yellow : chalk19.cyan;
|
|
5146
5574
|
console.log(` ${color("\u2022")} ${insight.message}`);
|
|
5147
5575
|
for (const specPath of insight.specs.slice(0, 3)) {
|
|
5148
5576
|
const spec = specs.find((s) => s.path === specPath);
|
|
5149
5577
|
const details = spec ? getSpecInsightDetails(spec) : null;
|
|
5150
|
-
console.log(` ${
|
|
5578
|
+
console.log(` ${chalk19.dim(specPath)}${details ? chalk19.dim(` (${details})`) : ""}`);
|
|
5151
5579
|
}
|
|
5152
5580
|
if (insight.specs.length > 3) {
|
|
5153
|
-
console.log(` ${
|
|
5581
|
+
console.log(` ${chalk19.dim(`...and ${insight.specs.length - 3} more`)}`);
|
|
5154
5582
|
}
|
|
5155
5583
|
}
|
|
5156
5584
|
console.log("");
|
|
5157
5585
|
} else if (completionMetrics.activeSpecs === 0 && completionMetrics.completeSpecs > 0) {
|
|
5158
|
-
console.log(
|
|
5586
|
+
console.log(chalk19.bold.green("\u{1F389} All Specs Complete!"));
|
|
5159
5587
|
console.log("");
|
|
5160
|
-
console.log(` ${
|
|
5588
|
+
console.log(` ${chalk19.dim("Great work! All active specs are complete.")}`);
|
|
5161
5589
|
console.log("");
|
|
5162
5590
|
} else if (completionMetrics.activeSpecs > 0) {
|
|
5163
|
-
console.log(
|
|
5591
|
+
console.log(chalk19.bold.green("\u2728 All Clear!"));
|
|
5164
5592
|
console.log("");
|
|
5165
|
-
console.log(` ${
|
|
5593
|
+
console.log(` ${chalk19.dim("No critical issues detected. Keep up the good work!")}`);
|
|
5166
5594
|
console.log("");
|
|
5167
5595
|
}
|
|
5168
|
-
console.log(
|
|
5596
|
+
console.log(chalk19.bold("\u{1F680} Velocity Summary"));
|
|
5169
5597
|
console.log("");
|
|
5170
|
-
const cycleTimeStatus = velocityMetrics.cycleTime.average <= 7 ?
|
|
5171
|
-
const throughputTrend = velocityMetrics.throughput.trend === "up" ?
|
|
5172
|
-
console.log(` Avg Cycle Time ${
|
|
5173
|
-
console.log(` Throughput ${
|
|
5174
|
-
console.log(` WIP ${
|
|
5598
|
+
const cycleTimeStatus = velocityMetrics.cycleTime.average <= 7 ? chalk19.green("\u2713") : chalk19.yellow("\u26A0");
|
|
5599
|
+
const throughputTrend = velocityMetrics.throughput.trend === "up" ? chalk19.green("\u2191") : velocityMetrics.throughput.trend === "down" ? chalk19.red("\u2193") : chalk19.yellow("\u2192");
|
|
5600
|
+
console.log(` Avg Cycle Time ${chalk19.cyan(velocityMetrics.cycleTime.average.toFixed(1))} days ${cycleTimeStatus}${velocityMetrics.cycleTime.average <= 7 ? chalk19.dim(" (target: 7d)") : ""}`);
|
|
5601
|
+
console.log(` Throughput ${chalk19.cyan((velocityMetrics.throughput.perWeek / 7 * 7).toFixed(1))}/week ${throughputTrend}`);
|
|
5602
|
+
console.log(` WIP ${chalk19.yellow(velocityMetrics.wip.current)} specs`);
|
|
5175
5603
|
console.log("");
|
|
5176
|
-
console.log(
|
|
5177
|
-
console.log(
|
|
5178
|
-
console.log(
|
|
5604
|
+
console.log(chalk19.dim("\u{1F4A1} Use `lean-spec stats --full` for detailed analytics"));
|
|
5605
|
+
console.log(chalk19.dim(" Use `lean-spec stats --velocity` for velocity breakdown"));
|
|
5606
|
+
console.log(chalk19.dim(" Use `lean-spec stats --timeline` for activity timeline"));
|
|
5179
5607
|
console.log("");
|
|
5180
5608
|
return;
|
|
5181
5609
|
}
|
|
@@ -5191,97 +5619,97 @@ async function showStats(options) {
|
|
|
5191
5619
|
(sum, count) => sum + count,
|
|
5192
5620
|
0
|
|
5193
5621
|
);
|
|
5194
|
-
console.log(
|
|
5622
|
+
console.log(chalk19.bold("\u{1F4C8} Overview"));
|
|
5195
5623
|
console.log("");
|
|
5196
5624
|
console.log(
|
|
5197
5625
|
` ${"Metric".padEnd(labelWidth)} ${"Value".padStart(valueWidth)}`
|
|
5198
5626
|
);
|
|
5199
5627
|
console.log(
|
|
5200
|
-
` ${
|
|
5628
|
+
` ${chalk19.dim("\u2500".repeat(labelWidth))} ${chalk19.dim("\u2500".repeat(valueWidth))}`
|
|
5201
5629
|
);
|
|
5202
5630
|
console.log(
|
|
5203
|
-
` ${"Total Specs".padEnd(labelWidth)} ${
|
|
5631
|
+
` ${"Total Specs".padEnd(labelWidth)} ${chalk19.green(specs.length.toString().padStart(valueWidth))}`
|
|
5204
5632
|
);
|
|
5205
5633
|
console.log(
|
|
5206
|
-
` ${"With Priority".padEnd(labelWidth)} ${
|
|
5634
|
+
` ${"With Priority".padEnd(labelWidth)} ${chalk19.cyan(totalWithPriority.toString().padStart(valueWidth))}`
|
|
5207
5635
|
);
|
|
5208
5636
|
console.log(
|
|
5209
|
-
` ${"Unique Tags".padEnd(labelWidth)} ${
|
|
5637
|
+
` ${"Unique Tags".padEnd(labelWidth)} ${chalk19.magenta(Object.keys(tagCounts).length.toString().padStart(valueWidth))}`
|
|
5210
5638
|
);
|
|
5211
5639
|
console.log("");
|
|
5212
|
-
console.log(
|
|
5640
|
+
console.log(chalk19.bold("\u{1F4CA} Status Distribution"));
|
|
5213
5641
|
console.log("");
|
|
5214
5642
|
const maxStatusCount = Math.max(...Object.values(statusCounts));
|
|
5215
5643
|
const colWidth = barWidth + 3;
|
|
5216
5644
|
console.log(
|
|
5217
|
-
` ${"Status".padEnd(labelWidth)} ${
|
|
5645
|
+
` ${"Status".padEnd(labelWidth)} ${chalk19.cyan("Count".padEnd(colWidth))}`
|
|
5218
5646
|
);
|
|
5219
5647
|
console.log(
|
|
5220
|
-
` ${
|
|
5648
|
+
` ${chalk19.dim("\u2500".repeat(labelWidth))} ${chalk19.dim("\u2500".repeat(colWidth))}`
|
|
5221
5649
|
);
|
|
5222
5650
|
console.log(
|
|
5223
|
-
` \u{1F4CB} ${"Planned".padEnd(labelWidth - 3)} ${
|
|
5651
|
+
` \u{1F4CB} ${"Planned".padEnd(labelWidth - 3)} ${chalk19.cyan(createBar(statusCounts.planned, maxStatusCount).padEnd(barWidth))}${chalk19.cyan(statusCounts.planned.toString().padStart(3))}`
|
|
5224
5652
|
);
|
|
5225
5653
|
console.log(
|
|
5226
|
-
` \u23F3 ${"In Progress".padEnd(labelWidth - 3)} ${
|
|
5654
|
+
` \u23F3 ${"In Progress".padEnd(labelWidth - 3)} ${chalk19.yellow(createBar(statusCounts["in-progress"], maxStatusCount).padEnd(barWidth))}${chalk19.yellow(statusCounts["in-progress"].toString().padStart(3))}`
|
|
5227
5655
|
);
|
|
5228
5656
|
console.log(
|
|
5229
|
-
` \u2705 ${"Complete".padEnd(labelWidth - 3)} ${
|
|
5657
|
+
` \u2705 ${"Complete".padEnd(labelWidth - 3)} ${chalk19.green(createBar(statusCounts.complete, maxStatusCount).padEnd(barWidth))}${chalk19.green(statusCounts.complete.toString().padStart(3))}`
|
|
5230
5658
|
);
|
|
5231
5659
|
console.log(
|
|
5232
|
-
` \u{1F4E6} ${"Archived".padEnd(labelWidth - 3)} ${
|
|
5660
|
+
` \u{1F4E6} ${"Archived".padEnd(labelWidth - 3)} ${chalk19.dim(createBar(statusCounts.archived, maxStatusCount).padEnd(barWidth))}${chalk19.dim(statusCounts.archived.toString().padStart(3))}`
|
|
5233
5661
|
);
|
|
5234
5662
|
console.log("");
|
|
5235
5663
|
if (totalWithPriority > 0) {
|
|
5236
|
-
console.log(
|
|
5664
|
+
console.log(chalk19.bold("\u{1F3AF} Priority Breakdown"));
|
|
5237
5665
|
console.log("");
|
|
5238
5666
|
const maxPriorityCount = Math.max(
|
|
5239
5667
|
...Object.values(priorityCounts).filter((c) => c > 0)
|
|
5240
5668
|
);
|
|
5241
5669
|
console.log(
|
|
5242
|
-
` ${"Priority".padEnd(labelWidth)} ${
|
|
5670
|
+
` ${"Priority".padEnd(labelWidth)} ${chalk19.cyan("Count".padEnd(colWidth))}`
|
|
5243
5671
|
);
|
|
5244
5672
|
console.log(
|
|
5245
|
-
` ${
|
|
5673
|
+
` ${chalk19.dim("\u2500".repeat(labelWidth))} ${chalk19.dim("\u2500".repeat(colWidth))}`
|
|
5246
5674
|
);
|
|
5247
5675
|
if (priorityCounts.critical > 0) {
|
|
5248
5676
|
console.log(
|
|
5249
|
-
` \u{1F534} ${"Critical".padEnd(labelWidth - 3)} ${
|
|
5677
|
+
` \u{1F534} ${"Critical".padEnd(labelWidth - 3)} ${chalk19.red(createBar(priorityCounts.critical, maxPriorityCount).padEnd(barWidth))}${chalk19.red(priorityCounts.critical.toString().padStart(3))}`
|
|
5250
5678
|
);
|
|
5251
5679
|
}
|
|
5252
5680
|
if (priorityCounts.high > 0) {
|
|
5253
5681
|
console.log(
|
|
5254
|
-
` \u{1F7E0} ${"High".padEnd(labelWidth - 3)} ${
|
|
5682
|
+
` \u{1F7E0} ${"High".padEnd(labelWidth - 3)} ${chalk19.hex("#FFA500")(createBar(priorityCounts.high, maxPriorityCount).padEnd(barWidth))}${chalk19.hex("#FFA500")(priorityCounts.high.toString().padStart(3))}`
|
|
5255
5683
|
);
|
|
5256
5684
|
}
|
|
5257
5685
|
if (priorityCounts.medium > 0) {
|
|
5258
5686
|
console.log(
|
|
5259
|
-
` \u{1F7E1} ${"Medium".padEnd(labelWidth - 3)} ${
|
|
5687
|
+
` \u{1F7E1} ${"Medium".padEnd(labelWidth - 3)} ${chalk19.yellow(createBar(priorityCounts.medium, maxPriorityCount).padEnd(barWidth))}${chalk19.yellow(priorityCounts.medium.toString().padStart(3))}`
|
|
5260
5688
|
);
|
|
5261
5689
|
}
|
|
5262
5690
|
if (priorityCounts.low > 0) {
|
|
5263
5691
|
console.log(
|
|
5264
|
-
` \u{1F7E2} ${"Low".padEnd(labelWidth - 3)} ${
|
|
5692
|
+
` \u{1F7E2} ${"Low".padEnd(labelWidth - 3)} ${chalk19.green(createBar(priorityCounts.low, maxPriorityCount).padEnd(barWidth))}${chalk19.green(priorityCounts.low.toString().padStart(3))}`
|
|
5265
5693
|
);
|
|
5266
5694
|
}
|
|
5267
5695
|
console.log("");
|
|
5268
5696
|
}
|
|
5269
5697
|
const topTags = Object.entries(tagCounts).sort((a, b) => b[1] - a[1]).slice(0, 5);
|
|
5270
5698
|
if (topTags.length > 0) {
|
|
5271
|
-
console.log(
|
|
5699
|
+
console.log(chalk19.bold("\u{1F3F7}\uFE0F Popular Tags"));
|
|
5272
5700
|
console.log("");
|
|
5273
5701
|
const maxTagCount = Math.max(...topTags.map(([, count]) => count));
|
|
5274
5702
|
console.log(
|
|
5275
|
-
` ${"Tag".padEnd(labelWidth)} ${
|
|
5703
|
+
` ${"Tag".padEnd(labelWidth)} ${chalk19.magenta("Count".padEnd(colWidth))}`
|
|
5276
5704
|
);
|
|
5277
5705
|
console.log(
|
|
5278
|
-
` ${
|
|
5706
|
+
` ${chalk19.dim("\u2500".repeat(labelWidth))} ${chalk19.dim("\u2500".repeat(colWidth))}`
|
|
5279
5707
|
);
|
|
5280
5708
|
for (const [tag, count] of topTags) {
|
|
5281
5709
|
const truncatedTag = tag.length > labelWidth ? tag.substring(0, labelWidth - 1) + "\u2026" : tag;
|
|
5282
5710
|
const bar = createBar(count, maxTagCount);
|
|
5283
5711
|
console.log(
|
|
5284
|
-
` ${truncatedTag.padEnd(labelWidth)} ${
|
|
5712
|
+
` ${truncatedTag.padEnd(labelWidth)} ${chalk19.magenta(bar.padEnd(barWidth))}${chalk19.magenta(count.toString().padStart(3))}`
|
|
5285
5713
|
);
|
|
5286
5714
|
}
|
|
5287
5715
|
console.log("");
|
|
@@ -5313,14 +5741,14 @@ async function showStats(options) {
|
|
|
5313
5741
|
]);
|
|
5314
5742
|
const sortedDates = Array.from(allDates).sort();
|
|
5315
5743
|
if (sortedDates.length > 0) {
|
|
5316
|
-
console.log(
|
|
5744
|
+
console.log(chalk19.bold(`\u{1F4C5} Activity (Last ${days} Days)`));
|
|
5317
5745
|
console.log("");
|
|
5318
5746
|
const colWidth = barWidth + 3;
|
|
5319
5747
|
console.log(
|
|
5320
|
-
` ${"Date".padEnd(15)} ${
|
|
5748
|
+
` ${"Date".padEnd(15)} ${chalk19.cyan("Created".padEnd(colWidth))} ${chalk19.green("Completed".padEnd(colWidth))}`
|
|
5321
5749
|
);
|
|
5322
5750
|
console.log(
|
|
5323
|
-
` ${
|
|
5751
|
+
` ${chalk19.dim("\u2500".repeat(15))} ${chalk19.dim("\u2500".repeat(colWidth))} ${chalk19.dim("\u2500".repeat(colWidth))}`
|
|
5324
5752
|
);
|
|
5325
5753
|
const maxCount = Math.max(
|
|
5326
5754
|
...Object.values(createdByDate),
|
|
@@ -5334,85 +5762,85 @@ async function showStats(options) {
|
|
|
5334
5762
|
const createdCol = `${createdBar.padEnd(barWidth)}${created.toString().padStart(3)}`;
|
|
5335
5763
|
const completedCol = `${completedBar.padEnd(barWidth)}${completed.toString().padStart(3)}`;
|
|
5336
5764
|
console.log(
|
|
5337
|
-
` ${
|
|
5765
|
+
` ${chalk19.dim(date.padEnd(15))} ${chalk19.cyan(createdCol)} ${chalk19.green(completedCol)}`
|
|
5338
5766
|
);
|
|
5339
5767
|
}
|
|
5340
5768
|
console.log("");
|
|
5341
5769
|
}
|
|
5342
5770
|
}
|
|
5343
5771
|
if (showVelocity) {
|
|
5344
|
-
console.log(
|
|
5772
|
+
console.log(chalk19.bold("\u{1F680} Velocity Metrics"));
|
|
5345
5773
|
console.log("");
|
|
5346
|
-
console.log(
|
|
5774
|
+
console.log(chalk19.bold("\u23F1\uFE0F Cycle Time (Created \u2192 Completed)"));
|
|
5347
5775
|
console.log("");
|
|
5348
5776
|
console.log(
|
|
5349
5777
|
` ${"Metric".padEnd(labelWidth)} ${"Days".padStart(valueWidth)}`
|
|
5350
5778
|
);
|
|
5351
5779
|
console.log(
|
|
5352
|
-
` ${
|
|
5780
|
+
` ${chalk19.dim("\u2500".repeat(labelWidth))} ${chalk19.dim("\u2500".repeat(valueWidth))}`
|
|
5353
5781
|
);
|
|
5354
5782
|
console.log(
|
|
5355
|
-
` ${"Average".padEnd(labelWidth)} ${
|
|
5783
|
+
` ${"Average".padEnd(labelWidth)} ${chalk19.cyan(velocityMetrics.cycleTime.average.toFixed(1).padStart(valueWidth))}`
|
|
5356
5784
|
);
|
|
5357
5785
|
console.log(
|
|
5358
|
-
` ${"Median".padEnd(labelWidth)} ${
|
|
5786
|
+
` ${"Median".padEnd(labelWidth)} ${chalk19.cyan(velocityMetrics.cycleTime.median.toFixed(1).padStart(valueWidth))}`
|
|
5359
5787
|
);
|
|
5360
5788
|
console.log(
|
|
5361
|
-
` ${"90th Percentile".padEnd(labelWidth)} ${
|
|
5789
|
+
` ${"90th Percentile".padEnd(labelWidth)} ${chalk19.yellow(velocityMetrics.cycleTime.p90.toFixed(1).padStart(valueWidth))}`
|
|
5362
5790
|
);
|
|
5363
5791
|
console.log("");
|
|
5364
|
-
console.log(
|
|
5792
|
+
console.log(chalk19.bold("\u{1F4E6} Throughput"));
|
|
5365
5793
|
console.log("");
|
|
5366
5794
|
console.log(
|
|
5367
5795
|
` ${"Period".padEnd(labelWidth)} ${"Specs".padStart(valueWidth)}`
|
|
5368
5796
|
);
|
|
5369
5797
|
console.log(
|
|
5370
|
-
` ${
|
|
5798
|
+
` ${chalk19.dim("\u2500".repeat(labelWidth))} ${chalk19.dim("\u2500".repeat(valueWidth))}`
|
|
5371
5799
|
);
|
|
5372
5800
|
console.log(
|
|
5373
|
-
` ${"Last 7 days".padEnd(labelWidth)} ${
|
|
5801
|
+
` ${"Last 7 days".padEnd(labelWidth)} ${chalk19.green(velocityMetrics.throughput.perWeek.toString().padStart(valueWidth))}`
|
|
5374
5802
|
);
|
|
5375
5803
|
console.log(
|
|
5376
|
-
` ${"Last 30 days".padEnd(labelWidth)} ${
|
|
5804
|
+
` ${"Last 30 days".padEnd(labelWidth)} ${chalk19.green(velocityMetrics.throughput.perMonth.toString().padStart(valueWidth))}`
|
|
5377
5805
|
);
|
|
5378
|
-
const trendColor = velocityMetrics.throughput.trend === "up" ?
|
|
5806
|
+
const trendColor = velocityMetrics.throughput.trend === "up" ? chalk19.green : velocityMetrics.throughput.trend === "down" ? chalk19.red : chalk19.yellow;
|
|
5379
5807
|
const trendSymbol = velocityMetrics.throughput.trend === "up" ? "\u2191" : velocityMetrics.throughput.trend === "down" ? "\u2193" : "\u2192";
|
|
5380
5808
|
console.log(
|
|
5381
5809
|
` ${"Trend".padEnd(labelWidth)} ${trendColor(trendSymbol + " " + velocityMetrics.throughput.trend.padStart(valueWidth - 2))}`
|
|
5382
5810
|
);
|
|
5383
5811
|
console.log("");
|
|
5384
|
-
console.log(
|
|
5812
|
+
console.log(chalk19.bold("\u{1F504} Work In Progress"));
|
|
5385
5813
|
console.log("");
|
|
5386
5814
|
console.log(
|
|
5387
5815
|
` ${"Metric".padEnd(labelWidth)} ${"Specs".padStart(valueWidth)}`
|
|
5388
5816
|
);
|
|
5389
5817
|
console.log(
|
|
5390
|
-
` ${
|
|
5818
|
+
` ${chalk19.dim("\u2500".repeat(labelWidth))} ${chalk19.dim("\u2500".repeat(valueWidth))}`
|
|
5391
5819
|
);
|
|
5392
5820
|
console.log(
|
|
5393
|
-
` ${"Current".padEnd(labelWidth)} ${
|
|
5821
|
+
` ${"Current".padEnd(labelWidth)} ${chalk19.yellow(velocityMetrics.wip.current.toString().padStart(valueWidth))}`
|
|
5394
5822
|
);
|
|
5395
5823
|
console.log(
|
|
5396
|
-
` ${"30-day Average".padEnd(labelWidth)} ${
|
|
5824
|
+
` ${"30-day Average".padEnd(labelWidth)} ${chalk19.cyan(velocityMetrics.wip.average.toFixed(1).padStart(valueWidth))}`
|
|
5397
5825
|
);
|
|
5398
5826
|
console.log("");
|
|
5399
5827
|
if (velocityMetrics.leadTime.plannedToInProgress > 0 || velocityMetrics.leadTime.inProgressToComplete > 0) {
|
|
5400
|
-
console.log(
|
|
5828
|
+
console.log(chalk19.bold("\u{1F500} Lead Time by Stage"));
|
|
5401
5829
|
console.log("");
|
|
5402
5830
|
console.log(
|
|
5403
5831
|
` ${"Stage".padEnd(labelWidth)} ${"Days".padStart(valueWidth)}`
|
|
5404
5832
|
);
|
|
5405
5833
|
console.log(
|
|
5406
|
-
` ${
|
|
5834
|
+
` ${chalk19.dim("\u2500".repeat(labelWidth))} ${chalk19.dim("\u2500".repeat(valueWidth))}`
|
|
5407
5835
|
);
|
|
5408
5836
|
if (velocityMetrics.leadTime.plannedToInProgress > 0) {
|
|
5409
5837
|
console.log(
|
|
5410
|
-
` ${"Planned \u2192 In Progress".padEnd(labelWidth)} ${
|
|
5838
|
+
` ${"Planned \u2192 In Progress".padEnd(labelWidth)} ${chalk19.cyan(velocityMetrics.leadTime.plannedToInProgress.toFixed(1).padStart(valueWidth))}`
|
|
5411
5839
|
);
|
|
5412
5840
|
}
|
|
5413
5841
|
if (velocityMetrics.leadTime.inProgressToComplete > 0) {
|
|
5414
5842
|
console.log(
|
|
5415
|
-
` ${"In Progress \u2192 Complete".padEnd(labelWidth)} ${
|
|
5843
|
+
` ${"In Progress \u2192 Complete".padEnd(labelWidth)} ${chalk19.green(velocityMetrics.leadTime.inProgressToComplete.toFixed(1).padStart(valueWidth))}`
|
|
5416
5844
|
);
|
|
5417
5845
|
}
|
|
5418
5846
|
console.log("");
|
|
@@ -5420,14 +5848,15 @@ async function showStats(options) {
|
|
|
5420
5848
|
}
|
|
5421
5849
|
}
|
|
5422
5850
|
function searchCommand() {
|
|
5423
|
-
return new Command("search").description("Full-text search with metadata filters").argument("<query>", "Search query").option("--status <status>", "Filter by status").option("--tag <tag>", "Filter by tag").option("--priority <priority>", "Filter by priority").option("--assignee <name>", "Filter by assignee").option("--field <name=value...>", "Filter by custom field (can specify multiple)").action(async (query, options) => {
|
|
5851
|
+
return new Command("search").description("Full-text search with metadata filters").argument("<query>", "Search query").option("--status <status>", "Filter by status").option("--tag <tag>", "Filter by tag").option("--priority <priority>", "Filter by priority").option("--assignee <name>", "Filter by assignee").option("--field <name=value...>", "Filter by custom field (can specify multiple)").option("--json", "Output as JSON").action(async (query, options) => {
|
|
5424
5852
|
const customFields = parseCustomFieldOptions(options.field);
|
|
5425
5853
|
await performSearch(query, {
|
|
5426
5854
|
status: options.status,
|
|
5427
5855
|
tag: options.tag,
|
|
5428
5856
|
priority: options.priority,
|
|
5429
5857
|
assignee: options.assignee,
|
|
5430
|
-
customFields: Object.keys(customFields).length > 0 ? customFields : void 0
|
|
5858
|
+
customFields: Object.keys(customFields).length > 0 ? customFields : void 0,
|
|
5859
|
+
json: options.json
|
|
5431
5860
|
});
|
|
5432
5861
|
});
|
|
5433
5862
|
}
|
|
@@ -5466,36 +5895,55 @@ async function performSearch(query, options) {
|
|
|
5466
5895
|
contextLength: 80
|
|
5467
5896
|
});
|
|
5468
5897
|
const { results, metadata } = searchResult;
|
|
5898
|
+
if (options.json) {
|
|
5899
|
+
const jsonOutput = {
|
|
5900
|
+
query,
|
|
5901
|
+
results: results.map((r) => ({
|
|
5902
|
+
spec: r.spec.path,
|
|
5903
|
+
score: r.score,
|
|
5904
|
+
totalMatches: r.totalMatches,
|
|
5905
|
+
matches: r.matches.map((m) => ({
|
|
5906
|
+
field: m.field,
|
|
5907
|
+
text: m.text,
|
|
5908
|
+
lineNumber: m.lineNumber
|
|
5909
|
+
}))
|
|
5910
|
+
})),
|
|
5911
|
+
metadata,
|
|
5912
|
+
filters: filter
|
|
5913
|
+
};
|
|
5914
|
+
console.log(JSON.stringify(jsonOutput, null, 2));
|
|
5915
|
+
return;
|
|
5916
|
+
}
|
|
5469
5917
|
if (results.length === 0) {
|
|
5470
5918
|
console.log("");
|
|
5471
|
-
console.log(
|
|
5919
|
+
console.log(chalk19.yellow(`\u{1F50D} No specs found matching "${sanitizeUserInput(query)}"`));
|
|
5472
5920
|
if (Object.keys(filter).length > 0) {
|
|
5473
5921
|
const filters = [];
|
|
5474
5922
|
if (options.status) filters.push(`status=${sanitizeUserInput(options.status)}`);
|
|
5475
5923
|
if (options.tag) filters.push(`tag=${sanitizeUserInput(options.tag)}`);
|
|
5476
5924
|
if (options.priority) filters.push(`priority=${sanitizeUserInput(options.priority)}`);
|
|
5477
5925
|
if (options.assignee) filters.push(`assignee=${sanitizeUserInput(options.assignee)}`);
|
|
5478
|
-
console.log(
|
|
5926
|
+
console.log(chalk19.gray(`With filters: ${filters.join(", ")}`));
|
|
5479
5927
|
}
|
|
5480
5928
|
console.log("");
|
|
5481
5929
|
return;
|
|
5482
5930
|
}
|
|
5483
5931
|
console.log("");
|
|
5484
|
-
console.log(
|
|
5485
|
-
console.log(
|
|
5932
|
+
console.log(chalk19.green(`\u{1F50D} Found ${results.length} spec${results.length === 1 ? "" : "s"} matching "${sanitizeUserInput(query)}"`));
|
|
5933
|
+
console.log(chalk19.gray(` Searched ${metadata.specsSearched} specs in ${metadata.searchTime}ms`));
|
|
5486
5934
|
if (Object.keys(filter).length > 0) {
|
|
5487
5935
|
const filters = [];
|
|
5488
5936
|
if (options.status) filters.push(`status=${sanitizeUserInput(options.status)}`);
|
|
5489
5937
|
if (options.tag) filters.push(`tag=${sanitizeUserInput(options.tag)}`);
|
|
5490
5938
|
if (options.priority) filters.push(`priority=${sanitizeUserInput(options.priority)}`);
|
|
5491
5939
|
if (options.assignee) filters.push(`assignee=${sanitizeUserInput(options.assignee)}`);
|
|
5492
|
-
console.log(
|
|
5940
|
+
console.log(chalk19.gray(` With filters: ${filters.join(", ")}`));
|
|
5493
5941
|
}
|
|
5494
5942
|
console.log("");
|
|
5495
5943
|
for (const result of results) {
|
|
5496
5944
|
const { spec, matches, score, totalMatches } = result;
|
|
5497
5945
|
const statusEmoji = spec.status === "in-progress" ? "\u{1F528}" : spec.status === "complete" ? "\u2705" : "\u{1F4C5}";
|
|
5498
|
-
console.log(
|
|
5946
|
+
console.log(chalk19.cyan(`${statusEmoji} ${sanitizeUserInput(spec.path)} ${chalk19.gray(`(${score}% match)`)}`));
|
|
5499
5947
|
const meta = [];
|
|
5500
5948
|
if (spec.priority) {
|
|
5501
5949
|
const priorityEmoji = spec.priority === "critical" ? "\u{1F534}" : spec.priority === "high" ? "\u{1F7E1}" : spec.priority === "medium" ? "\u{1F7E0}" : "\u{1F7E2}";
|
|
@@ -5505,30 +5953,30 @@ async function performSearch(query, options) {
|
|
|
5505
5953
|
meta.push(`[${spec.tags.map((tag) => sanitizeUserInput(tag)).join(", ")}]`);
|
|
5506
5954
|
}
|
|
5507
5955
|
if (meta.length > 0) {
|
|
5508
|
-
console.log(
|
|
5956
|
+
console.log(chalk19.gray(` ${meta.join(" \u2022 ")}`));
|
|
5509
5957
|
}
|
|
5510
5958
|
const titleMatch = matches.find((m) => m.field === "title");
|
|
5511
5959
|
if (titleMatch) {
|
|
5512
|
-
console.log(` ${
|
|
5960
|
+
console.log(` ${chalk19.bold("Title:")} ${highlightMatches(titleMatch.text, titleMatch.highlights)}`);
|
|
5513
5961
|
}
|
|
5514
5962
|
const descMatch = matches.find((m) => m.field === "description");
|
|
5515
5963
|
if (descMatch) {
|
|
5516
|
-
console.log(` ${
|
|
5964
|
+
console.log(` ${chalk19.bold("Description:")} ${highlightMatches(descMatch.text, descMatch.highlights)}`);
|
|
5517
5965
|
}
|
|
5518
5966
|
const tagMatches = matches.filter((m) => m.field === "tags");
|
|
5519
5967
|
if (tagMatches.length > 0) {
|
|
5520
|
-
console.log(` ${
|
|
5968
|
+
console.log(` ${chalk19.bold("Tags:")} ${tagMatches.map((m) => highlightMatches(m.text, m.highlights)).join(", ")}`);
|
|
5521
5969
|
}
|
|
5522
5970
|
const contentMatches = matches.filter((m) => m.field === "content");
|
|
5523
5971
|
if (contentMatches.length > 0) {
|
|
5524
|
-
console.log(` ${
|
|
5972
|
+
console.log(` ${chalk19.bold("Content matches:")}`);
|
|
5525
5973
|
for (const match of contentMatches) {
|
|
5526
|
-
const lineInfo = match.lineNumber ?
|
|
5974
|
+
const lineInfo = match.lineNumber ? chalk19.gray(`[L${match.lineNumber}]`) : "";
|
|
5527
5975
|
console.log(` ${lineInfo} ${highlightMatches(match.text, match.highlights)}`);
|
|
5528
5976
|
}
|
|
5529
5977
|
}
|
|
5530
5978
|
if (totalMatches > matches.length) {
|
|
5531
|
-
console.log(
|
|
5979
|
+
console.log(chalk19.gray(` ... and ${totalMatches - matches.length} more match${totalMatches - matches.length === 1 ? "" : "es"}`));
|
|
5532
5980
|
}
|
|
5533
5981
|
console.log("");
|
|
5534
5982
|
}
|
|
@@ -5539,7 +5987,7 @@ function highlightMatches(text, highlights) {
|
|
|
5539
5987
|
let lastEnd = 0;
|
|
5540
5988
|
for (const [start, end] of highlights) {
|
|
5541
5989
|
result += text.substring(lastEnd, start);
|
|
5542
|
-
result +=
|
|
5990
|
+
result += chalk19.yellow(text.substring(start, end));
|
|
5543
5991
|
lastEnd = end;
|
|
5544
5992
|
}
|
|
5545
5993
|
result += text.substring(lastEnd);
|
|
@@ -5557,7 +6005,7 @@ async function showDeps(specPath, options = {}) {
|
|
|
5557
6005
|
await autoCheckIfEnabled();
|
|
5558
6006
|
const config = await loadConfig();
|
|
5559
6007
|
const cwd = process.cwd();
|
|
5560
|
-
const specsDir =
|
|
6008
|
+
const specsDir = path15.join(cwd, config.specsDir);
|
|
5561
6009
|
const resolvedPath = await resolveSpecPath(specPath, cwd, specsDir);
|
|
5562
6010
|
if (!resolvedPath) {
|
|
5563
6011
|
throw new Error(`Spec not found: ${sanitizeUserInput(specPath)}`);
|
|
@@ -5621,17 +6069,17 @@ async function showDeps(specPath, options = {}) {
|
|
|
5621
6069
|
return;
|
|
5622
6070
|
}
|
|
5623
6071
|
console.log("");
|
|
5624
|
-
console.log(
|
|
6072
|
+
console.log(chalk19.green(`\u{1F4E6} Dependencies for ${chalk19.cyan(sanitizeUserInput(spec.path))}`));
|
|
5625
6073
|
console.log("");
|
|
5626
6074
|
const hasAnyRelationships = dependsOn.length > 0 || requiredBy.length > 0 || related.length > 0;
|
|
5627
6075
|
if (!hasAnyRelationships) {
|
|
5628
|
-
console.log(
|
|
6076
|
+
console.log(chalk19.gray(" No dependencies or relationships"));
|
|
5629
6077
|
console.log("");
|
|
5630
6078
|
return;
|
|
5631
6079
|
}
|
|
5632
6080
|
if ((mode === "complete" || mode === "upstream" || mode === "impact") && dependsOn.length > 0) {
|
|
5633
6081
|
const label = mode === "complete" ? "Depends On" : mode === "upstream" ? "Upstream Dependencies" : "Upstream (Impact)";
|
|
5634
|
-
console.log(
|
|
6082
|
+
console.log(chalk19.bold(`${label}:`));
|
|
5635
6083
|
for (const dep of dependsOn) {
|
|
5636
6084
|
const status = getStatusIndicator(dep.frontmatter.status);
|
|
5637
6085
|
console.log(` \u2192 ${sanitizeUserInput(dep.path)} ${status}`);
|
|
@@ -5640,7 +6088,7 @@ async function showDeps(specPath, options = {}) {
|
|
|
5640
6088
|
}
|
|
5641
6089
|
if ((mode === "complete" || mode === "downstream" || mode === "impact") && requiredBy.length > 0) {
|
|
5642
6090
|
const label = mode === "complete" ? "Required By" : mode === "downstream" ? "Downstream Dependents" : "Downstream (Impact)";
|
|
5643
|
-
console.log(
|
|
6091
|
+
console.log(chalk19.bold(`${label}:`));
|
|
5644
6092
|
for (const blocked of requiredBy) {
|
|
5645
6093
|
const status = getStatusIndicator(blocked.frontmatter.status);
|
|
5646
6094
|
console.log(` \u2190 ${sanitizeUserInput(blocked.path)} ${status}`);
|
|
@@ -5648,7 +6096,7 @@ async function showDeps(specPath, options = {}) {
|
|
|
5648
6096
|
console.log("");
|
|
5649
6097
|
}
|
|
5650
6098
|
if ((mode === "complete" || mode === "impact") && related.length > 0) {
|
|
5651
|
-
console.log(
|
|
6099
|
+
console.log(chalk19.bold("Related Specs:"));
|
|
5652
6100
|
for (const rel of related) {
|
|
5653
6101
|
const status = getStatusIndicator(rel.frontmatter.status);
|
|
5654
6102
|
console.log(` \u27F7 ${sanitizeUserInput(rel.path)} ${status}`);
|
|
@@ -5656,15 +6104,15 @@ async function showDeps(specPath, options = {}) {
|
|
|
5656
6104
|
console.log("");
|
|
5657
6105
|
}
|
|
5658
6106
|
if (mode === "complete" && (options.graph || dependsOn.length > 0)) {
|
|
5659
|
-
console.log(
|
|
6107
|
+
console.log(chalk19.bold("Dependency Chain:"));
|
|
5660
6108
|
const chain = buildDependencyChain(spec, specMap, options.depth || 3);
|
|
5661
6109
|
displayChain(chain, 0);
|
|
5662
6110
|
console.log("");
|
|
5663
6111
|
}
|
|
5664
6112
|
if (mode === "impact") {
|
|
5665
6113
|
const total = dependsOn.length + requiredBy.length + related.length;
|
|
5666
|
-
console.log(
|
|
5667
|
-
console.log(` Changing this spec affects ${
|
|
6114
|
+
console.log(chalk19.bold(`Impact Summary:`));
|
|
6115
|
+
console.log(` Changing this spec affects ${chalk19.yellow(total)} specs total`);
|
|
5668
6116
|
console.log(` Upstream: ${dependsOn.length} | Downstream: ${requiredBy.length} | Related: ${related.length}`);
|
|
5669
6117
|
console.log("");
|
|
5670
6118
|
}
|
|
@@ -5694,7 +6142,7 @@ function buildDependencyChain(spec, specMap, maxDepth, currentDepth = 0, visited
|
|
|
5694
6142
|
function displayChain(node, level) {
|
|
5695
6143
|
const indent = " ".repeat(level);
|
|
5696
6144
|
const status = getStatusIndicator(node.spec.frontmatter.status);
|
|
5697
|
-
const name = level === 0 ?
|
|
6145
|
+
const name = level === 0 ? chalk19.cyan(node.spec.path) : node.spec.path;
|
|
5698
6146
|
console.log(`${indent}${name} ${status}`);
|
|
5699
6147
|
for (const dep of node.dependencies) {
|
|
5700
6148
|
const prefix = " ".repeat(level) + "\u2514\u2500 ";
|
|
@@ -5706,7 +6154,7 @@ function displayChain(node, level) {
|
|
|
5706
6154
|
}
|
|
5707
6155
|
}
|
|
5708
6156
|
function timelineCommand() {
|
|
5709
|
-
return new Command("timeline").description("Show creation/completion over time").option("--days <n>", "Show last N days (default: 30)", parseInt).option("--by-tag", "Group by tag").option("--by-assignee", "Group by assignee").action(async (options) => {
|
|
6157
|
+
return new Command("timeline").description("Show creation/completion over time").option("--days <n>", "Show last N days (default: 30)", parseInt).option("--by-tag", "Group by tag").option("--by-assignee", "Group by assignee").option("--json", "Output as JSON").action(async (options) => {
|
|
5710
6158
|
await showTimeline(options);
|
|
5711
6159
|
});
|
|
5712
6160
|
}
|
|
@@ -5745,19 +6193,29 @@ async function showTimeline(options) {
|
|
|
5745
6193
|
}
|
|
5746
6194
|
}
|
|
5747
6195
|
}
|
|
5748
|
-
|
|
6196
|
+
if (options.json) {
|
|
6197
|
+
const jsonOutput = {
|
|
6198
|
+
days,
|
|
6199
|
+
createdByDate,
|
|
6200
|
+
completedByDate,
|
|
6201
|
+
createdByMonth
|
|
6202
|
+
};
|
|
6203
|
+
console.log(JSON.stringify(jsonOutput, null, 2));
|
|
6204
|
+
return;
|
|
6205
|
+
}
|
|
6206
|
+
console.log(chalk19.bold.cyan("\u{1F4C8} Spec Timeline"));
|
|
5749
6207
|
console.log("");
|
|
5750
6208
|
const allDates = /* @__PURE__ */ new Set([...Object.keys(createdByDate), ...Object.keys(completedByDate)]);
|
|
5751
6209
|
const sortedDates = Array.from(allDates).sort();
|
|
5752
6210
|
if (sortedDates.length > 0) {
|
|
5753
|
-
console.log(
|
|
6211
|
+
console.log(chalk19.bold(`\u{1F4C5} Activity (Last ${days} Days)`));
|
|
5754
6212
|
console.log("");
|
|
5755
6213
|
const labelWidth2 = 15;
|
|
5756
6214
|
const barWidth = 20;
|
|
5757
6215
|
const specsWidth = 3;
|
|
5758
6216
|
const colWidth = barWidth + specsWidth;
|
|
5759
|
-
console.log(` ${"Date".padEnd(labelWidth2)} ${
|
|
5760
|
-
console.log(` ${
|
|
6217
|
+
console.log(` ${"Date".padEnd(labelWidth2)} ${chalk19.cyan("Created".padEnd(colWidth))} ${chalk19.green("Completed".padEnd(colWidth))}`);
|
|
6218
|
+
console.log(` ${chalk19.dim("\u2500".repeat(labelWidth2))} ${chalk19.dim("\u2500".repeat(colWidth))} ${chalk19.dim("\u2500".repeat(colWidth))}`);
|
|
5761
6219
|
const maxCount = Math.max(...Object.values(createdByDate), ...Object.values(completedByDate));
|
|
5762
6220
|
for (const date of sortedDates) {
|
|
5763
6221
|
const created = createdByDate[date] || 0;
|
|
@@ -5766,7 +6224,7 @@ async function showTimeline(options) {
|
|
|
5766
6224
|
const completedBar = createBar(completed, maxCount, barWidth);
|
|
5767
6225
|
const createdCol = `${createdBar.padEnd(barWidth)}${created.toString().padStart(specsWidth)}`;
|
|
5768
6226
|
const completedCol = `${completedBar.padEnd(barWidth)}${completed.toString().padStart(specsWidth)}`;
|
|
5769
|
-
console.log(` ${
|
|
6227
|
+
console.log(` ${chalk19.dim(date.padEnd(labelWidth2))} ${chalk19.cyan(createdCol)} ${chalk19.green(completedCol)}`);
|
|
5770
6228
|
}
|
|
5771
6229
|
console.log("");
|
|
5772
6230
|
}
|
|
@@ -5776,18 +6234,18 @@ async function showTimeline(options) {
|
|
|
5776
6234
|
return dateB.diff(dateA);
|
|
5777
6235
|
}).slice(0, 6);
|
|
5778
6236
|
if (sortedMonths.length > 0) {
|
|
5779
|
-
console.log(
|
|
6237
|
+
console.log(chalk19.bold("\u{1F4CA} Monthly Overview"));
|
|
5780
6238
|
console.log("");
|
|
5781
6239
|
const labelWidth2 = 15;
|
|
5782
6240
|
const barWidth = 20;
|
|
5783
6241
|
const specsWidth = 3;
|
|
5784
6242
|
const colWidth = barWidth + specsWidth;
|
|
5785
|
-
console.log(` ${"Month".padEnd(labelWidth2)} ${
|
|
5786
|
-
console.log(` ${
|
|
6243
|
+
console.log(` ${"Month".padEnd(labelWidth2)} ${chalk19.magenta("Specs".padEnd(colWidth))}`);
|
|
6244
|
+
console.log(` ${chalk19.dim("\u2500".repeat(labelWidth2))} ${chalk19.dim("\u2500".repeat(colWidth))}`);
|
|
5787
6245
|
const maxCount = Math.max(...sortedMonths.map(([, count]) => count));
|
|
5788
6246
|
for (const [month, count] of sortedMonths) {
|
|
5789
6247
|
const bar = createBar(count, maxCount, barWidth);
|
|
5790
|
-
console.log(` ${month.padEnd(labelWidth2)} ${
|
|
6248
|
+
console.log(` ${month.padEnd(labelWidth2)} ${chalk19.magenta(bar.padEnd(barWidth))}${chalk19.magenta(count.toString().padStart(specsWidth))}`);
|
|
5791
6249
|
}
|
|
5792
6250
|
console.log("");
|
|
5793
6251
|
}
|
|
@@ -5801,14 +6259,14 @@ async function showTimeline(options) {
|
|
|
5801
6259
|
const completed = dayjs3(s.frontmatter.completed);
|
|
5802
6260
|
return completed.isAfter(today.subtract(30, "day"));
|
|
5803
6261
|
}).length;
|
|
5804
|
-
console.log(
|
|
6262
|
+
console.log(chalk19.bold("\u2705 Completion Rate"));
|
|
5805
6263
|
console.log("");
|
|
5806
6264
|
const labelWidth = 15;
|
|
5807
6265
|
const valueWidth = 5;
|
|
5808
6266
|
console.log(` ${"Period".padEnd(labelWidth)} ${"Specs".padStart(valueWidth)}`);
|
|
5809
|
-
console.log(` ${
|
|
5810
|
-
console.log(` ${"Last 7 days".padEnd(labelWidth)} ${
|
|
5811
|
-
console.log(` ${"Last 30 days".padEnd(labelWidth)} ${
|
|
6267
|
+
console.log(` ${chalk19.dim("\u2500".repeat(labelWidth))} ${chalk19.dim("\u2500".repeat(valueWidth))}`);
|
|
6268
|
+
console.log(` ${"Last 7 days".padEnd(labelWidth)} ${chalk19.green(last7Days.toString().padStart(valueWidth))}`);
|
|
6269
|
+
console.log(` ${"Last 30 days".padEnd(labelWidth)} ${chalk19.green(last30Days.toString().padStart(valueWidth))}`);
|
|
5812
6270
|
console.log("");
|
|
5813
6271
|
if (options.byTag) {
|
|
5814
6272
|
const tagStats = {};
|
|
@@ -5830,9 +6288,9 @@ async function showTimeline(options) {
|
|
|
5830
6288
|
}
|
|
5831
6289
|
const sortedTags = Object.entries(tagStats).sort((a, b) => b[1].created - a[1].created).slice(0, 10);
|
|
5832
6290
|
if (sortedTags.length > 0) {
|
|
5833
|
-
console.log(
|
|
6291
|
+
console.log(chalk19.bold("\u{1F3F7}\uFE0F By Tag"));
|
|
5834
6292
|
for (const [tag, stats] of sortedTags) {
|
|
5835
|
-
console.log(` ${
|
|
6293
|
+
console.log(` ${chalk19.dim("#")}${tag.padEnd(20)} ${chalk19.cyan(stats.created)} created \xB7 ${chalk19.green(stats.completed)} completed`);
|
|
5836
6294
|
}
|
|
5837
6295
|
console.log("");
|
|
5838
6296
|
}
|
|
@@ -5857,9 +6315,9 @@ async function showTimeline(options) {
|
|
|
5857
6315
|
}
|
|
5858
6316
|
const sortedAssignees = Object.entries(assigneeStats).sort((a, b) => b[1].created - a[1].created);
|
|
5859
6317
|
if (sortedAssignees.length > 0) {
|
|
5860
|
-
console.log(
|
|
6318
|
+
console.log(chalk19.bold("\u{1F464} By Assignee"));
|
|
5861
6319
|
for (const [assignee, stats] of sortedAssignees) {
|
|
5862
|
-
console.log(` ${
|
|
6320
|
+
console.log(` ${chalk19.dim("@")}${assignee.padEnd(20)} ${chalk19.cyan(stats.created)} created \xB7 ${chalk19.green(stats.completed)} completed`);
|
|
5863
6321
|
}
|
|
5864
6322
|
console.log("");
|
|
5865
6323
|
}
|
|
@@ -5877,13 +6335,13 @@ var STATUS_CONFIG2 = {
|
|
|
5877
6335
|
archived: { emoji: "\u{1F4E6}", color: "gray" }
|
|
5878
6336
|
};
|
|
5879
6337
|
var PRIORITY_CONFIG3 = {
|
|
5880
|
-
critical: { emoji: "\u{1F534}", label: "CRITICAL", colorFn:
|
|
5881
|
-
high: { emoji: "\u{1F7E0}", label: "HIGH", colorFn:
|
|
5882
|
-
medium: { emoji: "\u{1F7E1}", label: "MEDIUM", colorFn:
|
|
5883
|
-
low: { emoji: "\u{1F7E2}", label: "LOW", colorFn:
|
|
6338
|
+
critical: { emoji: "\u{1F534}", label: "CRITICAL", colorFn: chalk19.red },
|
|
6339
|
+
high: { emoji: "\u{1F7E0}", label: "HIGH", colorFn: chalk19.hex("#FFA500") },
|
|
6340
|
+
medium: { emoji: "\u{1F7E1}", label: "MEDIUM", colorFn: chalk19.yellow },
|
|
6341
|
+
low: { emoji: "\u{1F7E2}", label: "LOW", colorFn: chalk19.green }
|
|
5884
6342
|
};
|
|
5885
6343
|
function ganttCommand() {
|
|
5886
|
-
return new Command("gantt").description("Show timeline with dependencies").option("--weeks <n>", "Show N weeks (default: 4)", parseInt).option("--show-complete", "Include completed specs").option("--critical-path", "Highlight critical path").action(async (options) => {
|
|
6344
|
+
return new Command("gantt").description("Show timeline with dependencies").option("--weeks <n>", "Show N weeks (default: 4)", parseInt).option("--show-complete", "Include completed specs").option("--critical-path", "Highlight critical path").option("--json", "Output as JSON").action(async (options) => {
|
|
5887
6345
|
await showGantt(options);
|
|
5888
6346
|
});
|
|
5889
6347
|
}
|
|
@@ -5908,8 +6366,28 @@ async function showGantt(options) {
|
|
|
5908
6366
|
return spec.frontmatter.status !== "archived";
|
|
5909
6367
|
});
|
|
5910
6368
|
if (relevantSpecs.length === 0) {
|
|
5911
|
-
|
|
5912
|
-
|
|
6369
|
+
if (options.json) {
|
|
6370
|
+
console.log(JSON.stringify({ specs: [], weeks }, null, 2));
|
|
6371
|
+
} else {
|
|
6372
|
+
console.log(chalk19.dim("No active specs found."));
|
|
6373
|
+
console.log(chalk19.dim("Tip: Use --show-complete to include completed specs."));
|
|
6374
|
+
}
|
|
6375
|
+
return;
|
|
6376
|
+
}
|
|
6377
|
+
if (options.json) {
|
|
6378
|
+
const jsonOutput = {
|
|
6379
|
+
weeks,
|
|
6380
|
+
specs: relevantSpecs.map((spec) => ({
|
|
6381
|
+
path: spec.path,
|
|
6382
|
+
status: spec.frontmatter.status,
|
|
6383
|
+
priority: spec.frontmatter.priority,
|
|
6384
|
+
created: spec.frontmatter.created,
|
|
6385
|
+
completed: spec.frontmatter.completed,
|
|
6386
|
+
due: spec.frontmatter.due,
|
|
6387
|
+
dependsOn: spec.frontmatter.depends_on
|
|
6388
|
+
}))
|
|
6389
|
+
};
|
|
6390
|
+
console.log(JSON.stringify(jsonOutput, null, 2));
|
|
5913
6391
|
return;
|
|
5914
6392
|
}
|
|
5915
6393
|
const groupedSpecs = {
|
|
@@ -5945,7 +6423,7 @@ async function showGantt(options) {
|
|
|
5945
6423
|
const overdue = relevantSpecs.filter(
|
|
5946
6424
|
(s) => s.frontmatter.due && dayjs3(s.frontmatter.due).isBefore(today) && s.frontmatter.status !== "complete"
|
|
5947
6425
|
).length;
|
|
5948
|
-
console.log(
|
|
6426
|
+
console.log(chalk19.bold.cyan(`\u{1F4C5} Gantt Chart (${weeks} weeks from ${startDate.format("MMM D, YYYY")})`));
|
|
5949
6427
|
console.log("");
|
|
5950
6428
|
const specHeader = "Spec".padEnd(SPEC_COLUMN_WIDTH);
|
|
5951
6429
|
const timelineHeader = "Timeline";
|
|
@@ -5957,17 +6435,17 @@ async function showGantt(options) {
|
|
|
5957
6435
|
calendarDates.push(dateStr);
|
|
5958
6436
|
}
|
|
5959
6437
|
const dateRow = " ".repeat(SPEC_COLUMN_WIDTH) + COLUMN_SEPARATOR + calendarDates.join("");
|
|
5960
|
-
console.log(
|
|
6438
|
+
console.log(chalk19.dim(dateRow));
|
|
5961
6439
|
const specSeparator = "\u2500".repeat(SPEC_COLUMN_WIDTH);
|
|
5962
6440
|
const timelineSeparator = "\u2500".repeat(timelineColumnWidth);
|
|
5963
|
-
console.log(
|
|
6441
|
+
console.log(chalk19.dim(specSeparator + COLUMN_SEPARATOR + timelineSeparator));
|
|
5964
6442
|
const todayWeekOffset = today.diff(startDate, "week");
|
|
5965
6443
|
const todayMarkerPos = todayWeekOffset * 8;
|
|
5966
6444
|
let todayMarker = " ".repeat(SPEC_COLUMN_WIDTH) + COLUMN_SEPARATOR;
|
|
5967
6445
|
if (todayMarkerPos >= 0 && todayMarkerPos < timelineColumnWidth) {
|
|
5968
6446
|
todayMarker += " ".repeat(todayMarkerPos) + "\u2502 Today";
|
|
5969
6447
|
}
|
|
5970
|
-
console.log(
|
|
6448
|
+
console.log(chalk19.dim(todayMarker));
|
|
5971
6449
|
console.log("");
|
|
5972
6450
|
const priorities = ["critical", "high", "medium", "low"];
|
|
5973
6451
|
for (const priority of priorities) {
|
|
@@ -5985,9 +6463,9 @@ async function showGantt(options) {
|
|
|
5985
6463
|
const summaryParts = [];
|
|
5986
6464
|
if (inProgress > 0) summaryParts.push(`${inProgress} in-progress`);
|
|
5987
6465
|
if (planned > 0) summaryParts.push(`${planned} planned`);
|
|
5988
|
-
if (overdue > 0) summaryParts.push(
|
|
5989
|
-
console.log(
|
|
5990
|
-
console.log(
|
|
6466
|
+
if (overdue > 0) summaryParts.push(chalk19.red(`${overdue} overdue`));
|
|
6467
|
+
console.log(chalk19.bold("Summary: ") + summaryParts.join(" \xB7 "));
|
|
6468
|
+
console.log(chalk19.dim('\u{1F4A1} Tip: Add "due: YYYY-MM-DD" to frontmatter for timeline planning'));
|
|
5991
6469
|
}
|
|
5992
6470
|
function renderSpecRow(spec, startDate, endDate, weeks, today) {
|
|
5993
6471
|
const statusConfig = STATUS_CONFIG2[spec.frontmatter.status];
|
|
@@ -6000,7 +6478,7 @@ function renderSpecRow(spec, startDate, endDate, weeks, today) {
|
|
|
6000
6478
|
const specColumn = `${SPEC_INDENT}${emoji} ${specName}`.padEnd(SPEC_COLUMN_WIDTH);
|
|
6001
6479
|
let timelineColumn;
|
|
6002
6480
|
if (!spec.frontmatter.due) {
|
|
6003
|
-
timelineColumn =
|
|
6481
|
+
timelineColumn = chalk19.dim("(no due date set)");
|
|
6004
6482
|
} else {
|
|
6005
6483
|
timelineColumn = renderTimelineBar(spec, startDate, endDate, weeks, today);
|
|
6006
6484
|
}
|
|
@@ -6023,13 +6501,13 @@ function renderTimelineBar(spec, startDate, endDate, weeks, today) {
|
|
|
6023
6501
|
result += " ".repeat(barStart);
|
|
6024
6502
|
}
|
|
6025
6503
|
if (spec.frontmatter.status === "complete") {
|
|
6026
|
-
result +=
|
|
6504
|
+
result += chalk19.green(FILLED_BAR_CHAR.repeat(barLength));
|
|
6027
6505
|
} else if (spec.frontmatter.status === "in-progress") {
|
|
6028
6506
|
const halfLength = Math.floor(barLength / 2);
|
|
6029
|
-
result +=
|
|
6030
|
-
result +=
|
|
6507
|
+
result += chalk19.yellow(FILLED_BAR_CHAR.repeat(halfLength));
|
|
6508
|
+
result += chalk19.dim(EMPTY_BAR_CHAR.repeat(barLength - halfLength));
|
|
6031
6509
|
} else {
|
|
6032
|
-
result +=
|
|
6510
|
+
result += chalk19.dim(EMPTY_BAR_CHAR.repeat(barLength));
|
|
6033
6511
|
}
|
|
6034
6512
|
const trailingSpace = totalChars - barEnd;
|
|
6035
6513
|
if (trailingSpace > 0) {
|
|
@@ -6055,12 +6533,12 @@ async function countSpecTokens(specPath, options = {}) {
|
|
|
6055
6533
|
try {
|
|
6056
6534
|
const config = await loadConfig();
|
|
6057
6535
|
const cwd = process.cwd();
|
|
6058
|
-
const specsDir =
|
|
6536
|
+
const specsDir = path15.join(cwd, config.specsDir);
|
|
6059
6537
|
const resolvedPath = await resolveSpecPath(specPath, cwd, specsDir);
|
|
6060
6538
|
if (!resolvedPath) {
|
|
6061
6539
|
throw new Error(`Spec not found: ${sanitizeUserInput(specPath)}`);
|
|
6062
6540
|
}
|
|
6063
|
-
const specName =
|
|
6541
|
+
const specName = path15.basename(resolvedPath);
|
|
6064
6542
|
const result = await counter.countSpec(resolvedPath, {
|
|
6065
6543
|
detailed: options.detailed,
|
|
6066
6544
|
includeSubSpecs: options.includeSubSpecs
|
|
@@ -6073,42 +6551,42 @@ async function countSpecTokens(specPath, options = {}) {
|
|
|
6073
6551
|
}, null, 2));
|
|
6074
6552
|
return;
|
|
6075
6553
|
}
|
|
6076
|
-
console.log(
|
|
6554
|
+
console.log(chalk19.bold.cyan(`\u{1F4CA} Token Count: ${specName}`));
|
|
6077
6555
|
console.log("");
|
|
6078
6556
|
const indicators = counter.getPerformanceIndicators(result.total);
|
|
6079
6557
|
const levelEmoji = indicators.level === "excellent" ? "\u2705" : indicators.level === "good" ? "\u{1F44D}" : indicators.level === "warning" ? "\u26A0\uFE0F" : "\u{1F534}";
|
|
6080
|
-
console.log(` Total: ${
|
|
6558
|
+
console.log(` Total: ${chalk19.cyan(result.total.toLocaleString())} tokens ${levelEmoji}`);
|
|
6081
6559
|
console.log("");
|
|
6082
6560
|
if (result.files.length > 1 || options.detailed) {
|
|
6083
|
-
console.log(
|
|
6561
|
+
console.log(chalk19.bold("Files:"));
|
|
6084
6562
|
console.log("");
|
|
6085
6563
|
for (const file of result.files) {
|
|
6086
|
-
const lineInfo = file.lines ?
|
|
6087
|
-
console.log(` ${file.path.padEnd(25)} ${
|
|
6564
|
+
const lineInfo = file.lines ? chalk19.dim(` (${file.lines} lines)`) : "";
|
|
6565
|
+
console.log(` ${file.path.padEnd(25)} ${chalk19.cyan(file.tokens.toLocaleString().padStart(6))} tokens${lineInfo}`);
|
|
6088
6566
|
}
|
|
6089
6567
|
console.log("");
|
|
6090
6568
|
}
|
|
6091
6569
|
if (options.detailed && result.breakdown) {
|
|
6092
6570
|
const b = result.breakdown;
|
|
6093
6571
|
const total = b.code + b.prose + b.tables + b.frontmatter;
|
|
6094
|
-
console.log(
|
|
6572
|
+
console.log(chalk19.bold("Content Breakdown:"));
|
|
6095
6573
|
console.log("");
|
|
6096
|
-
console.log(` Prose ${
|
|
6097
|
-
console.log(` Code ${
|
|
6098
|
-
console.log(` Tables ${
|
|
6099
|
-
console.log(` Frontmatter ${
|
|
6574
|
+
console.log(` Prose ${chalk19.cyan(b.prose.toLocaleString().padStart(6))} tokens ${chalk19.dim(`(${Math.round(b.prose / total * 100)}%)`)}`);
|
|
6575
|
+
console.log(` Code ${chalk19.cyan(b.code.toLocaleString().padStart(6))} tokens ${chalk19.dim(`(${Math.round(b.code / total * 100)}%)`)}`);
|
|
6576
|
+
console.log(` Tables ${chalk19.cyan(b.tables.toLocaleString().padStart(6))} tokens ${chalk19.dim(`(${Math.round(b.tables / total * 100)}%)`)}`);
|
|
6577
|
+
console.log(` Frontmatter ${chalk19.cyan(b.frontmatter.toLocaleString().padStart(6))} tokens ${chalk19.dim(`(${Math.round(b.frontmatter / total * 100)}%)`)}`);
|
|
6100
6578
|
console.log("");
|
|
6101
6579
|
}
|
|
6102
|
-
console.log(
|
|
6580
|
+
console.log(chalk19.bold("Performance Indicators:"));
|
|
6103
6581
|
console.log("");
|
|
6104
|
-
const costColor = indicators.costMultiplier < 2 ?
|
|
6105
|
-
const effectivenessColor = indicators.effectiveness >= 95 ?
|
|
6106
|
-
console.log(` Cost multiplier: ${costColor(`${indicators.costMultiplier}x`)} ${
|
|
6107
|
-
console.log(` AI effectiveness: ${effectivenessColor(`~${indicators.effectiveness}%`)} ${
|
|
6582
|
+
const costColor = indicators.costMultiplier < 2 ? chalk19.green : indicators.costMultiplier < 4 ? chalk19.yellow : chalk19.red;
|
|
6583
|
+
const effectivenessColor = indicators.effectiveness >= 95 ? chalk19.green : indicators.effectiveness >= 85 ? chalk19.yellow : chalk19.red;
|
|
6584
|
+
console.log(` Cost multiplier: ${costColor(`${indicators.costMultiplier}x`)} ${chalk19.dim("vs 1,200 token baseline")}`);
|
|
6585
|
+
console.log(` AI effectiveness: ${effectivenessColor(`~${indicators.effectiveness}%`)} ${chalk19.dim("(hypothesis)")}`);
|
|
6108
6586
|
console.log(` Context Economy: ${levelEmoji} ${indicators.recommendation}`);
|
|
6109
6587
|
console.log("");
|
|
6110
6588
|
if (!options.includeSubSpecs && result.files.length === 1) {
|
|
6111
|
-
console.log(
|
|
6589
|
+
console.log(chalk19.dim("\u{1F4A1} Use `--include-sub-specs` to count all sub-spec files"));
|
|
6112
6590
|
}
|
|
6113
6591
|
} finally {
|
|
6114
6592
|
counter.dispose();
|
|
@@ -6154,46 +6632,46 @@ async function tokensAllCommand(options = {}) {
|
|
|
6154
6632
|
console.log(JSON.stringify(results, null, 2));
|
|
6155
6633
|
return;
|
|
6156
6634
|
}
|
|
6157
|
-
console.log(
|
|
6635
|
+
console.log(chalk19.bold.cyan("\u{1F4CA} Token Counts"));
|
|
6158
6636
|
console.log("");
|
|
6159
|
-
console.log(
|
|
6637
|
+
console.log(chalk19.dim(`Sorted by: ${sortBy}`));
|
|
6160
6638
|
console.log("");
|
|
6161
6639
|
const totalTokens = results.reduce((sum, r) => sum + r.tokens, 0);
|
|
6162
6640
|
const avgTokens = Math.round(totalTokens / results.length);
|
|
6163
6641
|
const warningCount = results.filter((r) => r.level === "warning" || r.level === "problem").length;
|
|
6164
|
-
console.log(
|
|
6642
|
+
console.log(chalk19.bold("Summary:"));
|
|
6165
6643
|
console.log("");
|
|
6166
|
-
console.log(` Total specs: ${
|
|
6167
|
-
console.log(` Total tokens: ${
|
|
6168
|
-
console.log(` Average tokens: ${
|
|
6644
|
+
console.log(` Total specs: ${chalk19.cyan(results.length)}`);
|
|
6645
|
+
console.log(` Total tokens: ${chalk19.cyan(totalTokens.toLocaleString())}`);
|
|
6646
|
+
console.log(` Average tokens: ${chalk19.cyan(avgTokens.toLocaleString())}`);
|
|
6169
6647
|
if (warningCount > 0) {
|
|
6170
|
-
console.log(` Needs review: ${
|
|
6648
|
+
console.log(` Needs review: ${chalk19.yellow(warningCount)} specs ${chalk19.dim("(\u26A0\uFE0F or \u{1F534})")}`);
|
|
6171
6649
|
}
|
|
6172
6650
|
console.log("");
|
|
6173
6651
|
const nameCol = 35;
|
|
6174
6652
|
const tokensCol = 10;
|
|
6175
6653
|
const linesCol = 8;
|
|
6176
|
-
console.log(
|
|
6654
|
+
console.log(chalk19.bold(
|
|
6177
6655
|
"Spec".padEnd(nameCol) + "Tokens".padStart(tokensCol) + "Lines".padStart(linesCol) + " Status"
|
|
6178
6656
|
));
|
|
6179
|
-
console.log(
|
|
6657
|
+
console.log(chalk19.dim("\u2500".repeat(nameCol + tokensCol + linesCol + 10)));
|
|
6180
6658
|
const displayCount = options.all ? results.length : Math.min(20, results.length);
|
|
6181
6659
|
for (let i = 0; i < displayCount; i++) {
|
|
6182
6660
|
const r = results[i];
|
|
6183
6661
|
const emoji = r.level === "excellent" ? "\u2705" : r.level === "good" ? "\u{1F44D}" : r.level === "warning" ? "\u26A0\uFE0F" : "\u{1F534}";
|
|
6184
|
-
const tokensColor = r.level === "excellent" || r.level === "good" ?
|
|
6662
|
+
const tokensColor = r.level === "excellent" || r.level === "good" ? chalk19.cyan : r.level === "warning" ? chalk19.yellow : chalk19.red;
|
|
6185
6663
|
const name = r.name.length > nameCol - 2 ? r.name.substring(0, nameCol - 3) + "\u2026" : r.name;
|
|
6186
6664
|
console.log(
|
|
6187
|
-
name.padEnd(nameCol) + tokensColor(r.tokens.toLocaleString().padStart(tokensCol)) +
|
|
6665
|
+
name.padEnd(nameCol) + tokensColor(r.tokens.toLocaleString().padStart(tokensCol)) + chalk19.dim(r.lines.toString().padStart(linesCol)) + ` ${emoji}`
|
|
6188
6666
|
);
|
|
6189
6667
|
}
|
|
6190
6668
|
if (results.length > displayCount) {
|
|
6191
6669
|
console.log("");
|
|
6192
|
-
console.log(
|
|
6193
|
-
console.log(
|
|
6670
|
+
console.log(chalk19.dim(`... and ${results.length - displayCount} more specs`));
|
|
6671
|
+
console.log(chalk19.dim(`Use --all to show all specs`));
|
|
6194
6672
|
}
|
|
6195
6673
|
console.log("");
|
|
6196
|
-
console.log(
|
|
6674
|
+
console.log(chalk19.dim("Legend: \u2705 excellent (<2K) | \u{1F44D} good (<3.5K) | \u26A0\uFE0F warning (<5K) | \u{1F534} problem (>5K)"));
|
|
6197
6675
|
console.log("");
|
|
6198
6676
|
}
|
|
6199
6677
|
function analyzeCommand() {
|
|
@@ -6207,13 +6685,13 @@ async function analyzeSpec(specPath, options = {}) {
|
|
|
6207
6685
|
try {
|
|
6208
6686
|
const config = await loadConfig();
|
|
6209
6687
|
const cwd = process.cwd();
|
|
6210
|
-
const specsDir =
|
|
6688
|
+
const specsDir = path15.join(cwd, config.specsDir);
|
|
6211
6689
|
const resolvedPath = await resolveSpecPath(specPath, cwd, specsDir);
|
|
6212
6690
|
if (!resolvedPath) {
|
|
6213
6691
|
throw new Error(`Spec not found: ${sanitizeUserInput(specPath)}`);
|
|
6214
6692
|
}
|
|
6215
|
-
const specName =
|
|
6216
|
-
const readmePath =
|
|
6693
|
+
const specName = path15.basename(resolvedPath);
|
|
6694
|
+
const readmePath = path15.join(resolvedPath, "README.md");
|
|
6217
6695
|
const content = await readFile(readmePath, "utf-8");
|
|
6218
6696
|
const structure = analyzeMarkdownStructure(content);
|
|
6219
6697
|
const tokenResult = await counter.countSpec(resolvedPath, {
|
|
@@ -6316,44 +6794,44 @@ function getThresholdLimit(level) {
|
|
|
6316
6794
|
}
|
|
6317
6795
|
}
|
|
6318
6796
|
function displayAnalysis(result, verbose) {
|
|
6319
|
-
console.log(
|
|
6797
|
+
console.log(chalk19.bold.cyan(`\u{1F4CA} Spec Analysis: ${result.spec}`));
|
|
6320
6798
|
console.log("");
|
|
6321
6799
|
const statusEmoji = result.threshold.status === "excellent" ? "\u2705" : result.threshold.status === "good" ? "\u{1F44D}" : result.threshold.status === "warning" ? "\u26A0\uFE0F" : "\u{1F534}";
|
|
6322
|
-
const tokenColor = result.threshold.status === "excellent" || result.threshold.status === "good" ?
|
|
6323
|
-
console.log(
|
|
6324
|
-
console.log(
|
|
6325
|
-
console.log(
|
|
6800
|
+
const tokenColor = result.threshold.status === "excellent" || result.threshold.status === "good" ? chalk19.cyan : result.threshold.status === "warning" ? chalk19.yellow : chalk19.red;
|
|
6801
|
+
console.log(chalk19.bold("Token Count:"), tokenColor(result.metrics.tokens.toLocaleString()), "tokens", statusEmoji);
|
|
6802
|
+
console.log(chalk19.dim(` Threshold: ${result.threshold.limit.toLocaleString()} tokens`));
|
|
6803
|
+
console.log(chalk19.dim(` Status: ${result.threshold.message}`));
|
|
6326
6804
|
console.log("");
|
|
6327
|
-
console.log(
|
|
6328
|
-
console.log(` Lines: ${
|
|
6329
|
-
console.log(` Sections: ${
|
|
6330
|
-
console.log(` Code blocks: ${
|
|
6331
|
-
console.log(` Max nesting: ${
|
|
6805
|
+
console.log(chalk19.bold("Structure:"));
|
|
6806
|
+
console.log(` Lines: ${chalk19.cyan(result.metrics.lines.toLocaleString())}`);
|
|
6807
|
+
console.log(` Sections: ${chalk19.cyan(result.metrics.sections.total)} (H1:${result.metrics.sections.h1}, H2:${result.metrics.sections.h2}, H3:${result.metrics.sections.h3}, H4:${result.metrics.sections.h4})`);
|
|
6808
|
+
console.log(` Code blocks: ${chalk19.cyan(result.metrics.codeBlocks)}`);
|
|
6809
|
+
console.log(` Max nesting: ${chalk19.cyan(result.metrics.maxNesting)} levels`);
|
|
6332
6810
|
console.log("");
|
|
6333
6811
|
if (verbose && result.structure.length > 0) {
|
|
6334
6812
|
const topSections = result.structure.filter((s) => s.level <= 2).sort((a, b) => b.tokens - a.tokens).slice(0, 5);
|
|
6335
|
-
console.log(
|
|
6813
|
+
console.log(chalk19.bold("Top Sections by Size:"));
|
|
6336
6814
|
console.log("");
|
|
6337
6815
|
for (let i = 0; i < topSections.length; i++) {
|
|
6338
6816
|
const s = topSections[i];
|
|
6339
6817
|
const percentage = Math.round(s.tokens / result.metrics.tokens * 100);
|
|
6340
6818
|
const indent = " ".repeat(s.level - 1);
|
|
6341
6819
|
console.log(` ${i + 1}. ${indent}${s.section}`);
|
|
6342
|
-
console.log(` ${
|
|
6343
|
-
console.log(
|
|
6820
|
+
console.log(` ${chalk19.cyan(s.tokens.toLocaleString())} tokens / ${s.lineRange[1] - s.lineRange[0] + 1} lines ${chalk19.dim(`(${percentage}%)`)}`);
|
|
6821
|
+
console.log(chalk19.dim(` Lines ${s.lineRange[0]}-${s.lineRange[1]}`));
|
|
6344
6822
|
}
|
|
6345
6823
|
console.log("");
|
|
6346
6824
|
}
|
|
6347
|
-
const actionColor = result.recommendation.action === "none" ?
|
|
6348
|
-
console.log(
|
|
6349
|
-
console.log(
|
|
6350
|
-
console.log(
|
|
6825
|
+
const actionColor = result.recommendation.action === "none" ? chalk19.green : result.recommendation.action === "compact" ? chalk19.yellow : result.recommendation.action === "split" ? chalk19.red : chalk19.blue;
|
|
6826
|
+
console.log(chalk19.bold("Recommendation:"), actionColor(result.recommendation.action.toUpperCase()));
|
|
6827
|
+
console.log(chalk19.dim(` ${result.recommendation.reason}`));
|
|
6828
|
+
console.log(chalk19.dim(` Confidence: ${result.recommendation.confidence}`));
|
|
6351
6829
|
console.log("");
|
|
6352
6830
|
if (result.recommendation.action === "split") {
|
|
6353
|
-
console.log(
|
|
6354
|
-
console.log(
|
|
6831
|
+
console.log(chalk19.dim("\u{1F4A1} Use `lean-spec split` to partition into sub-specs"));
|
|
6832
|
+
console.log(chalk19.dim("\u{1F4A1} Consider splitting by H2 sections (concerns)"));
|
|
6355
6833
|
} else if (result.recommendation.action === "compact") {
|
|
6356
|
-
console.log(
|
|
6834
|
+
console.log(chalk19.dim("\u{1F4A1} Use `lean-spec compact` to remove redundancy"));
|
|
6357
6835
|
}
|
|
6358
6836
|
console.log("");
|
|
6359
6837
|
}
|
|
@@ -6385,13 +6863,13 @@ async function splitSpec(specPath, options) {
|
|
|
6385
6863
|
}
|
|
6386
6864
|
const config = await loadConfig();
|
|
6387
6865
|
const cwd = process.cwd();
|
|
6388
|
-
const specsDir =
|
|
6866
|
+
const specsDir = path15.join(cwd, config.specsDir);
|
|
6389
6867
|
const resolvedPath = await resolveSpecPath(specPath, cwd, specsDir);
|
|
6390
6868
|
if (!resolvedPath) {
|
|
6391
6869
|
throw new Error(`Spec not found: ${sanitizeUserInput(specPath)}`);
|
|
6392
6870
|
}
|
|
6393
|
-
const specName =
|
|
6394
|
-
const readmePath =
|
|
6871
|
+
const specName = path15.basename(resolvedPath);
|
|
6872
|
+
const readmePath = path15.join(resolvedPath, "README.md");
|
|
6395
6873
|
const content = await readFile(readmePath, "utf-8");
|
|
6396
6874
|
const parsedOutputs = parseOutputSpecs(options.outputs);
|
|
6397
6875
|
validateNoOverlaps(parsedOutputs);
|
|
@@ -6414,7 +6892,7 @@ async function splitSpec(specPath, options) {
|
|
|
6414
6892
|
await executeSplit(resolvedPath, specName, content, extractions, options);
|
|
6415
6893
|
} catch (error) {
|
|
6416
6894
|
if (error instanceof Error) {
|
|
6417
|
-
console.error(
|
|
6895
|
+
console.error(chalk19.red(`Error: ${error.message}`));
|
|
6418
6896
|
}
|
|
6419
6897
|
throw error;
|
|
6420
6898
|
}
|
|
@@ -6452,30 +6930,30 @@ function validateNoOverlaps(outputs) {
|
|
|
6452
6930
|
}
|
|
6453
6931
|
}
|
|
6454
6932
|
async function displayDryRun(specName, extractions) {
|
|
6455
|
-
console.log(
|
|
6933
|
+
console.log(chalk19.bold.cyan(`\u{1F4CB} Split Preview: ${specName}`));
|
|
6456
6934
|
console.log("");
|
|
6457
|
-
console.log(
|
|
6935
|
+
console.log(chalk19.bold("Would create:"));
|
|
6458
6936
|
console.log("");
|
|
6459
6937
|
for (const ext of extractions) {
|
|
6460
|
-
console.log(` ${
|
|
6938
|
+
console.log(` ${chalk19.cyan(ext.file)}`);
|
|
6461
6939
|
console.log(` Lines: ${ext.lines}`);
|
|
6462
6940
|
const previewLines = ext.content.split("\n").slice(0, 3);
|
|
6463
|
-
console.log(
|
|
6941
|
+
console.log(chalk19.dim(" Preview:"));
|
|
6464
6942
|
for (const line of previewLines) {
|
|
6465
|
-
console.log(
|
|
6943
|
+
console.log(chalk19.dim(` ${line.substring(0, 60)}${line.length > 60 ? "..." : ""}`));
|
|
6466
6944
|
}
|
|
6467
6945
|
console.log("");
|
|
6468
6946
|
}
|
|
6469
|
-
console.log(
|
|
6470
|
-
console.log(
|
|
6947
|
+
console.log(chalk19.dim("No files modified (dry run)"));
|
|
6948
|
+
console.log(chalk19.dim("Run without --dry-run to apply changes"));
|
|
6471
6949
|
console.log("");
|
|
6472
6950
|
}
|
|
6473
6951
|
async function executeSplit(specPath, specName, originalContent, extractions, options) {
|
|
6474
|
-
console.log(
|
|
6952
|
+
console.log(chalk19.bold.cyan(`\u2702\uFE0F Splitting: ${specName}`));
|
|
6475
6953
|
console.log("");
|
|
6476
6954
|
const frontmatter = parseFrontmatterFromString(originalContent);
|
|
6477
6955
|
for (const ext of extractions) {
|
|
6478
|
-
const outputPath =
|
|
6956
|
+
const outputPath = path15.join(specPath, ext.file);
|
|
6479
6957
|
let finalContent = ext.content;
|
|
6480
6958
|
if (ext.file === "README.md" && frontmatter) {
|
|
6481
6959
|
const { content: contentWithFrontmatter } = createUpdatedFrontmatter(
|
|
@@ -6485,21 +6963,21 @@ async function executeSplit(specPath, specName, originalContent, extractions, op
|
|
|
6485
6963
|
finalContent = contentWithFrontmatter;
|
|
6486
6964
|
}
|
|
6487
6965
|
await writeFile(outputPath, finalContent, "utf-8");
|
|
6488
|
-
console.log(
|
|
6966
|
+
console.log(chalk19.green(`\u2713 Created ${ext.file} (${ext.lines} lines)`));
|
|
6489
6967
|
}
|
|
6490
6968
|
if (options.updateRefs) {
|
|
6491
|
-
const readmePath =
|
|
6969
|
+
const readmePath = path15.join(specPath, "README.md");
|
|
6492
6970
|
const readmeContent = await readFile(readmePath, "utf-8");
|
|
6493
6971
|
const updatedReadme = await addSubSpecLinks(
|
|
6494
6972
|
readmeContent,
|
|
6495
6973
|
extractions.map((e) => e.file).filter((f) => f !== "README.md")
|
|
6496
6974
|
);
|
|
6497
6975
|
await writeFile(readmePath, updatedReadme, "utf-8");
|
|
6498
|
-
console.log(
|
|
6976
|
+
console.log(chalk19.green(`\u2713 Updated README.md with sub-spec links`));
|
|
6499
6977
|
}
|
|
6500
6978
|
console.log("");
|
|
6501
|
-
console.log(
|
|
6502
|
-
console.log(
|
|
6979
|
+
console.log(chalk19.bold.green("Split complete!"));
|
|
6980
|
+
console.log(chalk19.dim(`Created ${extractions.length} files in ${specName}`));
|
|
6503
6981
|
console.log("");
|
|
6504
6982
|
}
|
|
6505
6983
|
async function addSubSpecLinks(content, subSpecs) {
|
|
@@ -6567,13 +7045,13 @@ async function compactSpec(specPath, options) {
|
|
|
6567
7045
|
}
|
|
6568
7046
|
const config = await loadConfig();
|
|
6569
7047
|
const cwd = process.cwd();
|
|
6570
|
-
const specsDir =
|
|
7048
|
+
const specsDir = path15.join(cwd, config.specsDir);
|
|
6571
7049
|
const resolvedPath = await resolveSpecPath(specPath, cwd, specsDir);
|
|
6572
7050
|
if (!resolvedPath) {
|
|
6573
7051
|
throw new Error(`Spec not found: ${sanitizeUserInput(specPath)}`);
|
|
6574
7052
|
}
|
|
6575
|
-
const specName =
|
|
6576
|
-
const readmePath =
|
|
7053
|
+
const specName = path15.basename(resolvedPath);
|
|
7054
|
+
const readmePath = path15.join(resolvedPath, "README.md");
|
|
6577
7055
|
const content = await readFile(readmePath, "utf-8");
|
|
6578
7056
|
const parsedRemoves = parseRemoveSpecs(options.removes);
|
|
6579
7057
|
validateNoOverlaps2(parsedRemoves);
|
|
@@ -6584,7 +7062,7 @@ async function compactSpec(specPath, options) {
|
|
|
6584
7062
|
await executeCompact(readmePath, specName, content, parsedRemoves);
|
|
6585
7063
|
} catch (error) {
|
|
6586
7064
|
if (error instanceof Error) {
|
|
6587
|
-
console.error(
|
|
7065
|
+
console.error(chalk19.red(`Error: ${error.message}`));
|
|
6588
7066
|
}
|
|
6589
7067
|
throw error;
|
|
6590
7068
|
}
|
|
@@ -6623,9 +7101,9 @@ function validateNoOverlaps2(removes) {
|
|
|
6623
7101
|
}
|
|
6624
7102
|
}
|
|
6625
7103
|
async function displayDryRun2(specName, content, removes) {
|
|
6626
|
-
console.log(
|
|
7104
|
+
console.log(chalk19.bold.cyan(`\u{1F4CB} Compact Preview: ${specName}`));
|
|
6627
7105
|
console.log("");
|
|
6628
|
-
console.log(
|
|
7106
|
+
console.log(chalk19.bold("Would remove:"));
|
|
6629
7107
|
console.log("");
|
|
6630
7108
|
let totalLines = 0;
|
|
6631
7109
|
for (const remove of removes) {
|
|
@@ -6634,29 +7112,29 @@ async function displayDryRun2(specName, content, removes) {
|
|
|
6634
7112
|
const removedContent = extractLines(content, remove.startLine, remove.endLine);
|
|
6635
7113
|
const previewLines = removedContent.split("\n").slice(0, 3);
|
|
6636
7114
|
console.log(` Lines ${remove.startLine}-${remove.endLine} (${lineCount} lines)`);
|
|
6637
|
-
console.log(
|
|
7115
|
+
console.log(chalk19.dim(" Preview:"));
|
|
6638
7116
|
for (const line of previewLines) {
|
|
6639
|
-
console.log(
|
|
7117
|
+
console.log(chalk19.dim(` ${line.substring(0, 60)}${line.length > 60 ? "..." : ""}`));
|
|
6640
7118
|
}
|
|
6641
7119
|
if (removedContent.split("\n").length > 3) {
|
|
6642
|
-
console.log(
|
|
7120
|
+
console.log(chalk19.dim(` ... (${removedContent.split("\n").length - 3} more lines)`));
|
|
6643
7121
|
}
|
|
6644
7122
|
console.log("");
|
|
6645
7123
|
}
|
|
6646
7124
|
const originalLines = countLines(content);
|
|
6647
7125
|
const remainingLines = originalLines - totalLines;
|
|
6648
7126
|
const percentage = Math.round(totalLines / originalLines * 100);
|
|
6649
|
-
console.log(
|
|
6650
|
-
console.log(` Original lines: ${
|
|
6651
|
-
console.log(` Removing: ${
|
|
6652
|
-
console.log(` Remaining lines: ${
|
|
7127
|
+
console.log(chalk19.bold("Summary:"));
|
|
7128
|
+
console.log(` Original lines: ${chalk19.cyan(originalLines)}`);
|
|
7129
|
+
console.log(` Removing: ${chalk19.yellow(totalLines)} lines (${percentage}%)`);
|
|
7130
|
+
console.log(` Remaining lines: ${chalk19.cyan(remainingLines)}`);
|
|
6653
7131
|
console.log("");
|
|
6654
|
-
console.log(
|
|
6655
|
-
console.log(
|
|
7132
|
+
console.log(chalk19.dim("No files modified (dry run)"));
|
|
7133
|
+
console.log(chalk19.dim("Run without --dry-run to apply changes"));
|
|
6656
7134
|
console.log("");
|
|
6657
7135
|
}
|
|
6658
7136
|
async function executeCompact(readmePath, specName, content, removes) {
|
|
6659
|
-
console.log(
|
|
7137
|
+
console.log(chalk19.bold.cyan(`\u{1F5DC}\uFE0F Compacting: ${specName}`));
|
|
6660
7138
|
console.log("");
|
|
6661
7139
|
const sorted = [...removes].sort((a, b) => b.startLine - a.startLine);
|
|
6662
7140
|
let updatedContent = content;
|
|
@@ -6665,22 +7143,22 @@ async function executeCompact(readmePath, specName, content, removes) {
|
|
|
6665
7143
|
const lineCount = remove.endLine - remove.startLine + 1;
|
|
6666
7144
|
updatedContent = removeLines(updatedContent, remove.startLine, remove.endLine);
|
|
6667
7145
|
totalRemoved += lineCount;
|
|
6668
|
-
console.log(
|
|
7146
|
+
console.log(chalk19.green(`\u2713 Removed lines ${remove.startLine}-${remove.endLine} (${lineCount} lines)`));
|
|
6669
7147
|
}
|
|
6670
7148
|
await writeFile(readmePath, updatedContent, "utf-8");
|
|
6671
7149
|
const originalLines = countLines(content);
|
|
6672
7150
|
const finalLines = countLines(updatedContent);
|
|
6673
7151
|
const percentage = Math.round(totalRemoved / originalLines * 100);
|
|
6674
7152
|
console.log("");
|
|
6675
|
-
console.log(
|
|
6676
|
-
console.log(
|
|
6677
|
-
console.log(
|
|
7153
|
+
console.log(chalk19.bold.green("Compaction complete!"));
|
|
7154
|
+
console.log(chalk19.dim(`Removed ${totalRemoved} lines (${percentage}%)`));
|
|
7155
|
+
console.log(chalk19.dim(`${originalLines} \u2192 ${finalLines} lines`));
|
|
6678
7156
|
console.log("");
|
|
6679
7157
|
}
|
|
6680
7158
|
marked.use(markedTerminal());
|
|
6681
7159
|
async function readSpecContent(specPath, cwd = process.cwd()) {
|
|
6682
7160
|
const config = await loadConfig(cwd);
|
|
6683
|
-
const specsDir =
|
|
7161
|
+
const specsDir = path15.join(cwd, config.specsDir);
|
|
6684
7162
|
let resolvedPath = null;
|
|
6685
7163
|
let targetFile = null;
|
|
6686
7164
|
const pathParts = specPath.split("/").filter((p) => p);
|
|
@@ -6689,7 +7167,7 @@ async function readSpecContent(specPath, cwd = process.cwd()) {
|
|
|
6689
7167
|
const filePart = pathParts[pathParts.length - 1];
|
|
6690
7168
|
resolvedPath = await resolveSpecPath(specPart, cwd, specsDir);
|
|
6691
7169
|
if (resolvedPath) {
|
|
6692
|
-
targetFile =
|
|
7170
|
+
targetFile = path15.join(resolvedPath, filePart);
|
|
6693
7171
|
try {
|
|
6694
7172
|
await fs9.access(targetFile);
|
|
6695
7173
|
} catch {
|
|
@@ -6711,7 +7189,7 @@ async function readSpecContent(specPath, cwd = process.cwd()) {
|
|
|
6711
7189
|
return null;
|
|
6712
7190
|
}
|
|
6713
7191
|
const rawContent = await fs9.readFile(targetFile, "utf-8");
|
|
6714
|
-
const fileName =
|
|
7192
|
+
const fileName = path15.basename(targetFile);
|
|
6715
7193
|
const isSubSpec = fileName !== config.structure.defaultFile;
|
|
6716
7194
|
let frontmatter = null;
|
|
6717
7195
|
if (!isSubSpec) {
|
|
@@ -6740,7 +7218,7 @@ async function readSpecContent(specPath, cwd = process.cwd()) {
|
|
|
6740
7218
|
}
|
|
6741
7219
|
}
|
|
6742
7220
|
const content = lines.slice(contentStartIndex).join("\n").trim();
|
|
6743
|
-
const specName =
|
|
7221
|
+
const specName = path15.basename(resolvedPath);
|
|
6744
7222
|
const displayName = isSubSpec ? `${specName}/${fileName}` : specName;
|
|
6745
7223
|
return {
|
|
6746
7224
|
frontmatter,
|
|
@@ -6763,7 +7241,7 @@ function formatFrontmatter(frontmatter) {
|
|
|
6763
7241
|
archived: "\u{1F4E6}"
|
|
6764
7242
|
};
|
|
6765
7243
|
const statusEmoji = statusEmojis[frontmatter.status] || "\u{1F4C4}";
|
|
6766
|
-
lines.push(
|
|
7244
|
+
lines.push(chalk19.bold(`${statusEmoji} Status: `) + chalk19.cyan(frontmatter.status));
|
|
6767
7245
|
if (frontmatter.priority) {
|
|
6768
7246
|
const priorityEmojis = {
|
|
6769
7247
|
low: "\u{1F7E2}",
|
|
@@ -6772,25 +7250,25 @@ function formatFrontmatter(frontmatter) {
|
|
|
6772
7250
|
critical: "\u{1F534}"
|
|
6773
7251
|
};
|
|
6774
7252
|
const priorityEmoji = priorityEmojis[frontmatter.priority] || "";
|
|
6775
|
-
lines.push(
|
|
7253
|
+
lines.push(chalk19.bold(`${priorityEmoji} Priority: `) + chalk19.yellow(frontmatter.priority));
|
|
6776
7254
|
}
|
|
6777
7255
|
if (frontmatter.created) {
|
|
6778
|
-
lines.push(
|
|
7256
|
+
lines.push(chalk19.bold("\u{1F4C6} Created: ") + chalk19.gray(String(frontmatter.created)));
|
|
6779
7257
|
}
|
|
6780
7258
|
if (frontmatter.tags && frontmatter.tags.length > 0) {
|
|
6781
|
-
const tagStr = frontmatter.tags.map((tag) =>
|
|
6782
|
-
lines.push(
|
|
7259
|
+
const tagStr = frontmatter.tags.map((tag) => chalk19.blue(`#${tag}`)).join(" ");
|
|
7260
|
+
lines.push(chalk19.bold("\u{1F3F7}\uFE0F Tags: ") + tagStr);
|
|
6783
7261
|
}
|
|
6784
7262
|
if (frontmatter.assignee) {
|
|
6785
|
-
lines.push(
|
|
7263
|
+
lines.push(chalk19.bold("\u{1F464} Assignee: ") + chalk19.green(frontmatter.assignee));
|
|
6786
7264
|
}
|
|
6787
7265
|
const standardFields = ["status", "priority", "created", "tags", "assignee"];
|
|
6788
7266
|
const customFields = Object.entries(frontmatter).filter(([key]) => !standardFields.includes(key)).filter(([_, value]) => value !== void 0 && value !== null);
|
|
6789
7267
|
if (customFields.length > 0) {
|
|
6790
7268
|
lines.push("");
|
|
6791
|
-
lines.push(
|
|
7269
|
+
lines.push(chalk19.bold("Custom Fields:"));
|
|
6792
7270
|
for (const [key, value] of customFields) {
|
|
6793
|
-
lines.push(` ${
|
|
7271
|
+
lines.push(` ${chalk19.gray(key)}: ${chalk19.white(String(value))}`);
|
|
6794
7272
|
}
|
|
6795
7273
|
}
|
|
6796
7274
|
return lines.join("\n");
|
|
@@ -6798,11 +7276,11 @@ function formatFrontmatter(frontmatter) {
|
|
|
6798
7276
|
function displayFormattedSpec(spec) {
|
|
6799
7277
|
const output = [];
|
|
6800
7278
|
output.push("");
|
|
6801
|
-
output.push(
|
|
7279
|
+
output.push(chalk19.bold.cyan(`\u2501\u2501\u2501 ${spec.name} \u2501\u2501\u2501`));
|
|
6802
7280
|
output.push("");
|
|
6803
7281
|
output.push(formatFrontmatter(spec.frontmatter));
|
|
6804
7282
|
output.push("");
|
|
6805
|
-
output.push(
|
|
7283
|
+
output.push(chalk19.gray("\u2500".repeat(60)));
|
|
6806
7284
|
output.push("");
|
|
6807
7285
|
return output.join("\n");
|
|
6808
7286
|
}
|
|
@@ -6864,7 +7342,7 @@ function openCommand(specPath, options = {}) {
|
|
|
6864
7342
|
async function openSpec(specPath, options = {}) {
|
|
6865
7343
|
const cwd = process.cwd();
|
|
6866
7344
|
const config = await loadConfig(cwd);
|
|
6867
|
-
const specsDir =
|
|
7345
|
+
const specsDir = path15.join(cwd, config.specsDir);
|
|
6868
7346
|
let resolvedPath = null;
|
|
6869
7347
|
let targetFile = null;
|
|
6870
7348
|
const pathParts = specPath.split("/").filter((p) => p);
|
|
@@ -6873,7 +7351,7 @@ async function openSpec(specPath, options = {}) {
|
|
|
6873
7351
|
const filePart = pathParts[pathParts.length - 1];
|
|
6874
7352
|
resolvedPath = await resolveSpecPath(specPart, cwd, specsDir);
|
|
6875
7353
|
if (resolvedPath) {
|
|
6876
|
-
targetFile =
|
|
7354
|
+
targetFile = path15.join(resolvedPath, filePart);
|
|
6877
7355
|
try {
|
|
6878
7356
|
await fs9.access(targetFile);
|
|
6879
7357
|
} catch {
|
|
@@ -6907,7 +7385,7 @@ async function openSpec(specPath, options = {}) {
|
|
|
6907
7385
|
editor = "xdg-open";
|
|
6908
7386
|
}
|
|
6909
7387
|
}
|
|
6910
|
-
console.log(
|
|
7388
|
+
console.log(chalk19.gray(`Opening ${targetFile} with ${editor}...`));
|
|
6911
7389
|
const child = spawn(editor, [targetFile], {
|
|
6912
7390
|
stdio: "inherit",
|
|
6913
7391
|
shell: true
|
|
@@ -6953,7 +7431,7 @@ async function startMcpServer() {
|
|
|
6953
7431
|
process.exit(1);
|
|
6954
7432
|
}
|
|
6955
7433
|
}
|
|
6956
|
-
function
|
|
7434
|
+
function detectPackageManager2(baseDir = process.cwd()) {
|
|
6957
7435
|
const userAgent = process.env.npm_config_user_agent || "";
|
|
6958
7436
|
if (userAgent.includes("pnpm")) {
|
|
6959
7437
|
return "pnpm";
|
|
@@ -6978,7 +7456,7 @@ function detectPackageManager(baseDir = process.cwd()) {
|
|
|
6978
7456
|
|
|
6979
7457
|
// src/commands/ui.ts
|
|
6980
7458
|
function uiCommand() {
|
|
6981
|
-
return new Command("ui").description("Start local web UI for spec management").option("-s, --specs <dir>", "Specs directory (auto-detected if not specified)").option("-p, --port <port>", "Port to run on", "3000").option("--no-open", "Don't open browser automatically").option("--dev", "Run in development mode (only works in LeanSpec monorepo)").option("--dry-run", "Show what would run without executing").action(async (options) => {
|
|
7459
|
+
return new Command("ui").description("Start local web UI for spec management").option("-s, --specs <dir>", "Specs directory (auto-detected if not specified)").option("-p, --port <port>", "Port to run on", "3000").option("--no-open", "Don't open browser automatically").option("--multi-project", "Enable multi-project mode").option("--add-project <path>", "Add a project to multi-project registry").option("--discover <path>", "Discover LeanSpec projects in directory tree").option("--dev", "Run in development mode (only works in LeanSpec monorepo)").option("--dry-run", "Show what would run without executing").action(async (options) => {
|
|
6982
7460
|
try {
|
|
6983
7461
|
await startUi(options);
|
|
6984
7462
|
} catch (error) {
|
|
@@ -6989,34 +7467,45 @@ function uiCommand() {
|
|
|
6989
7467
|
async function startUi(options) {
|
|
6990
7468
|
const portNum = parseInt(options.port, 10);
|
|
6991
7469
|
if (isNaN(portNum) || portNum < 1 || portNum > 65535) {
|
|
6992
|
-
console.error(
|
|
6993
|
-
console.log(
|
|
7470
|
+
console.error(chalk19.red(`\u2717 Invalid port number: ${options.port}`));
|
|
7471
|
+
console.log(chalk19.dim("Port must be between 1 and 65535"));
|
|
6994
7472
|
throw new Error(`Invalid port: ${options.port}`);
|
|
6995
7473
|
}
|
|
6996
7474
|
const cwd = process.cwd();
|
|
6997
|
-
|
|
6998
|
-
|
|
6999
|
-
|
|
7475
|
+
const specsMode = options.multiProject ? "multi-project" : "filesystem";
|
|
7476
|
+
let specsDir = "";
|
|
7477
|
+
if (!options.multiProject) {
|
|
7478
|
+
if (options.specs) {
|
|
7479
|
+
specsDir = resolve(cwd, options.specs);
|
|
7480
|
+
} else {
|
|
7481
|
+
const config = await loadConfig(cwd);
|
|
7482
|
+
specsDir = join(cwd, config.specsDir);
|
|
7483
|
+
}
|
|
7484
|
+
if (!existsSync(specsDir)) {
|
|
7485
|
+
console.error(chalk19.red(`\u2717 Specs directory not found: ${specsDir}`));
|
|
7486
|
+
console.log(chalk19.dim("\nRun `lean-spec init` to initialize LeanSpec in this directory."));
|
|
7487
|
+
throw new Error(`Specs directory not found: ${specsDir}`);
|
|
7488
|
+
}
|
|
7000
7489
|
} else {
|
|
7001
|
-
|
|
7002
|
-
|
|
7003
|
-
}
|
|
7004
|
-
|
|
7005
|
-
|
|
7006
|
-
|
|
7007
|
-
|
|
7490
|
+
console.log(chalk19.cyan("\u2192 Multi-project mode enabled"));
|
|
7491
|
+
if (options.addProject) {
|
|
7492
|
+
console.log(chalk19.dim(` Adding project: ${options.addProject}`));
|
|
7493
|
+
}
|
|
7494
|
+
if (options.discover) {
|
|
7495
|
+
console.log(chalk19.dim(` Will discover projects in: ${options.discover}`));
|
|
7496
|
+
}
|
|
7008
7497
|
}
|
|
7009
7498
|
if (options.dev) {
|
|
7010
7499
|
const isLeanSpecMonorepo = checkIsLeanSpecMonorepo(cwd);
|
|
7011
7500
|
if (!isLeanSpecMonorepo) {
|
|
7012
|
-
console.error(
|
|
7013
|
-
console.log(
|
|
7501
|
+
console.error(chalk19.red(`\u2717 Development mode only works in the LeanSpec monorepo`));
|
|
7502
|
+
console.log(chalk19.dim("Remove --dev flag to use production mode"));
|
|
7014
7503
|
throw new Error("Not in LeanSpec monorepo");
|
|
7015
7504
|
}
|
|
7016
7505
|
const localUiDir = join(cwd, "packages/ui");
|
|
7017
|
-
return runLocalWeb(localUiDir, specsDir, options);
|
|
7506
|
+
return runLocalWeb(localUiDir, specsDir, specsMode, options);
|
|
7018
7507
|
}
|
|
7019
|
-
return runPublishedUI(cwd, specsDir, options);
|
|
7508
|
+
return runPublishedUI(cwd, specsDir, specsMode, options);
|
|
7020
7509
|
}
|
|
7021
7510
|
function checkIsLeanSpecMonorepo(cwd) {
|
|
7022
7511
|
const localUiDir = join(cwd, "packages/ui");
|
|
@@ -7031,24 +7520,24 @@ function checkIsLeanSpecMonorepo(cwd) {
|
|
|
7031
7520
|
return false;
|
|
7032
7521
|
}
|
|
7033
7522
|
}
|
|
7034
|
-
async function runLocalWeb(uiDir, specsDir, options) {
|
|
7035
|
-
console.log(
|
|
7523
|
+
async function runLocalWeb(uiDir, specsDir, specsMode, options) {
|
|
7524
|
+
console.log(chalk19.dim("\u2192 Detected LeanSpec monorepo, using local ui package\n"));
|
|
7036
7525
|
const repoRoot = resolve(uiDir, "..", "..");
|
|
7037
|
-
const packageManager =
|
|
7526
|
+
const packageManager = detectPackageManager2(repoRoot);
|
|
7038
7527
|
if (options.dryRun) {
|
|
7039
|
-
console.log(
|
|
7040
|
-
console.log(
|
|
7041
|
-
console.log(
|
|
7528
|
+
console.log(chalk19.cyan("Would run:"));
|
|
7529
|
+
console.log(chalk19.dim(` cd ${uiDir}`));
|
|
7530
|
+
console.log(chalk19.dim(` SPECS_MODE=${specsMode} SPECS_DIR=${specsDir} PORT=${options.port} ${packageManager} run dev`));
|
|
7042
7531
|
if (options.open) {
|
|
7043
|
-
console.log(
|
|
7532
|
+
console.log(chalk19.dim(` open http://localhost:${options.port}`));
|
|
7044
7533
|
}
|
|
7045
7534
|
return;
|
|
7046
7535
|
}
|
|
7047
7536
|
const spinner = ora("Starting web UI...").start();
|
|
7048
7537
|
const env = {
|
|
7049
7538
|
...process.env,
|
|
7050
|
-
SPECS_MODE:
|
|
7051
|
-
SPECS_DIR: specsDir,
|
|
7539
|
+
SPECS_MODE: specsMode,
|
|
7540
|
+
SPECS_DIR: specsDir || "",
|
|
7052
7541
|
PORT: options.port
|
|
7053
7542
|
};
|
|
7054
7543
|
const child = spawn(packageManager, ["run", "dev"], {
|
|
@@ -7059,19 +7548,67 @@ async function runLocalWeb(uiDir, specsDir, options) {
|
|
|
7059
7548
|
});
|
|
7060
7549
|
const readyTimeout = setTimeout(async () => {
|
|
7061
7550
|
spinner.succeed("Web UI running");
|
|
7062
|
-
console.log(
|
|
7551
|
+
console.log(chalk19.green(`
|
|
7063
7552
|
\u2728 LeanSpec UI: http://localhost:${options.port}
|
|
7064
7553
|
`));
|
|
7065
|
-
|
|
7554
|
+
if (options.multiProject) {
|
|
7555
|
+
console.log(chalk19.cyan("Multi-project mode is active"));
|
|
7556
|
+
if (options.addProject) {
|
|
7557
|
+
try {
|
|
7558
|
+
const res = await fetch(`http://localhost:${options.port}/api/projects`, {
|
|
7559
|
+
method: "POST",
|
|
7560
|
+
headers: { "Content-Type": "application/json" },
|
|
7561
|
+
body: JSON.stringify({ path: options.addProject })
|
|
7562
|
+
});
|
|
7563
|
+
if (res.ok) {
|
|
7564
|
+
console.log(chalk19.green(` \u2713 Added project: ${options.addProject}`));
|
|
7565
|
+
} else {
|
|
7566
|
+
console.error(chalk19.red(` \u2717 Failed to add project: ${options.addProject}`));
|
|
7567
|
+
}
|
|
7568
|
+
} catch (e) {
|
|
7569
|
+
console.error(chalk19.red(` \u2717 Failed to connect to server for adding project`));
|
|
7570
|
+
}
|
|
7571
|
+
}
|
|
7572
|
+
if (options.discover) {
|
|
7573
|
+
console.log(chalk19.dim(` Discovering projects in: ${options.discover}...`));
|
|
7574
|
+
try {
|
|
7575
|
+
const res = await fetch(`http://localhost:${options.port}/api/local-projects/discover`, {
|
|
7576
|
+
method: "POST",
|
|
7577
|
+
headers: { "Content-Type": "application/json" },
|
|
7578
|
+
body: JSON.stringify({ path: options.discover })
|
|
7579
|
+
});
|
|
7580
|
+
if (res.ok) {
|
|
7581
|
+
const data = await res.json();
|
|
7582
|
+
const discovered = data.discovered || [];
|
|
7583
|
+
console.log(chalk19.green(` \u2713 Found ${discovered.length} projects`));
|
|
7584
|
+
for (const p of discovered) {
|
|
7585
|
+
const addRes = await fetch(`http://localhost:${options.port}/api/projects`, {
|
|
7586
|
+
method: "POST",
|
|
7587
|
+
headers: { "Content-Type": "application/json" },
|
|
7588
|
+
body: JSON.stringify({ path: p.path })
|
|
7589
|
+
});
|
|
7590
|
+
if (addRes.ok) {
|
|
7591
|
+
console.log(chalk19.dim(` + Added: ${p.name} (${p.path})`));
|
|
7592
|
+
}
|
|
7593
|
+
}
|
|
7594
|
+
} else {
|
|
7595
|
+
console.error(chalk19.red(` \u2717 Failed to discover projects`));
|
|
7596
|
+
}
|
|
7597
|
+
} catch (e) {
|
|
7598
|
+
console.error(chalk19.red(` \u2717 Failed to connect to server for discovery`));
|
|
7599
|
+
}
|
|
7600
|
+
}
|
|
7601
|
+
}
|
|
7602
|
+
console.log(chalk19.dim("\nPress Ctrl+C to stop\n"));
|
|
7066
7603
|
if (options.open) {
|
|
7067
7604
|
try {
|
|
7068
7605
|
const openModule = await import('open');
|
|
7069
7606
|
const open = openModule.default;
|
|
7070
7607
|
await open(`http://localhost:${options.port}`);
|
|
7071
7608
|
} catch (error) {
|
|
7072
|
-
console.log(
|
|
7073
|
-
console.log(
|
|
7074
|
-
console.error(
|
|
7609
|
+
console.log(chalk19.yellow("\u26A0 Could not open browser automatically"));
|
|
7610
|
+
console.log(chalk19.dim("Please visit the URL above manually\n"));
|
|
7611
|
+
console.error(chalk19.dim(`Debug: ${error instanceof Error ? error.message : String(error)}`));
|
|
7075
7612
|
}
|
|
7076
7613
|
}
|
|
7077
7614
|
}, 3e3);
|
|
@@ -7088,7 +7625,7 @@ async function runLocalWeb(uiDir, specsDir, options) {
|
|
|
7088
7625
|
}
|
|
7089
7626
|
} catch (err) {
|
|
7090
7627
|
}
|
|
7091
|
-
console.log(
|
|
7628
|
+
console.log(chalk19.dim("\n\u2713 Web UI stopped"));
|
|
7092
7629
|
process.exit(0);
|
|
7093
7630
|
};
|
|
7094
7631
|
process.once("SIGINT", () => shutdown("SIGINT"));
|
|
@@ -7102,20 +7639,20 @@ async function runLocalWeb(uiDir, specsDir, options) {
|
|
|
7102
7639
|
spinner.stop();
|
|
7103
7640
|
if (code !== 0 && code !== null) {
|
|
7104
7641
|
spinner.fail("Web UI failed to start");
|
|
7105
|
-
console.error(
|
|
7642
|
+
console.error(chalk19.red(`
|
|
7106
7643
|
Process exited with code ${code}`));
|
|
7107
7644
|
process.exit(code);
|
|
7108
7645
|
}
|
|
7109
7646
|
process.exit(0);
|
|
7110
7647
|
});
|
|
7111
7648
|
}
|
|
7112
|
-
async function runPublishedUI(cwd, specsDir, options) {
|
|
7113
|
-
console.log(
|
|
7114
|
-
const packageManager =
|
|
7115
|
-
const { command, args, preview } = buildUiRunner(packageManager, specsDir, options.port, options.open);
|
|
7649
|
+
async function runPublishedUI(cwd, specsDir, specsMode, options) {
|
|
7650
|
+
console.log(chalk19.dim("\u2192 Using published @leanspec/ui package\n"));
|
|
7651
|
+
const packageManager = detectPackageManager2(cwd);
|
|
7652
|
+
const { command, args, preview } = buildUiRunner(packageManager, specsDir, specsMode, options.port, options.open, options.multiProject);
|
|
7116
7653
|
if (options.dryRun) {
|
|
7117
|
-
console.log(
|
|
7118
|
-
console.log(
|
|
7654
|
+
console.log(chalk19.cyan("Would run:"));
|
|
7655
|
+
console.log(chalk19.dim(` ${preview}`));
|
|
7119
7656
|
return;
|
|
7120
7657
|
}
|
|
7121
7658
|
const child = spawn(command, args, {
|
|
@@ -7134,7 +7671,7 @@ async function runPublishedUI(cwd, specsDir, options) {
|
|
|
7134
7671
|
}
|
|
7135
7672
|
} catch (err) {
|
|
7136
7673
|
}
|
|
7137
|
-
console.log(
|
|
7674
|
+
console.log(chalk19.dim("\n\u2713 Web UI stopped"));
|
|
7138
7675
|
process.exit(0);
|
|
7139
7676
|
};
|
|
7140
7677
|
process.once("SIGINT", () => shutdownPublished("SIGINT"));
|
|
@@ -7148,19 +7685,25 @@ async function runPublishedUI(cwd, specsDir, options) {
|
|
|
7148
7685
|
process.exit(0);
|
|
7149
7686
|
return;
|
|
7150
7687
|
}
|
|
7151
|
-
console.error(
|
|
7688
|
+
console.error(chalk19.red(`
|
|
7152
7689
|
@leanspec/ui exited with code ${code}`));
|
|
7153
|
-
console.log(
|
|
7690
|
+
console.log(chalk19.dim("Make sure npm can download @leanspec/ui (https://www.npmjs.com/package/@leanspec/ui)."));
|
|
7154
7691
|
process.exit(code);
|
|
7155
7692
|
});
|
|
7156
7693
|
child.on("error", (error) => {
|
|
7157
|
-
console.error(
|
|
7158
|
-
console.log(
|
|
7694
|
+
console.error(chalk19.red(`Failed to launch @leanspec/ui: ${error instanceof Error ? error.message : String(error)}`));
|
|
7695
|
+
console.log(chalk19.dim("You can also run it manually with `npx @leanspec/ui --specs <dir>`"));
|
|
7159
7696
|
process.exit(1);
|
|
7160
7697
|
});
|
|
7161
7698
|
}
|
|
7162
|
-
function buildUiRunner(packageManager, specsDir, port, openBrowser) {
|
|
7163
|
-
const uiArgs = ["@leanspec/ui"
|
|
7699
|
+
function buildUiRunner(packageManager, specsDir, specsMode, port, openBrowser, multiProject) {
|
|
7700
|
+
const uiArgs = ["@leanspec/ui"];
|
|
7701
|
+
if (!multiProject) {
|
|
7702
|
+
uiArgs.push("--specs", specsDir);
|
|
7703
|
+
} else {
|
|
7704
|
+
uiArgs.push("--multi-project");
|
|
7705
|
+
}
|
|
7706
|
+
uiArgs.push("--port", port);
|
|
7164
7707
|
if (!openBrowser) {
|
|
7165
7708
|
uiArgs.push("--no-open");
|
|
7166
7709
|
}
|
|
@@ -7492,7 +8035,7 @@ function createTool() {
|
|
|
7492
8035
|
async function getDepsData(specPath, mode = "complete") {
|
|
7493
8036
|
const config = await loadConfig();
|
|
7494
8037
|
const cwd = process.cwd();
|
|
7495
|
-
const specsDir =
|
|
8038
|
+
const specsDir = path15.join(cwd, config.specsDir);
|
|
7496
8039
|
const resolvedPath = await resolveSpecPath(specPath, cwd, specsDir);
|
|
7497
8040
|
if (!resolvedPath) {
|
|
7498
8041
|
throw new Error(`Spec not found: ${specPath}`);
|
|
@@ -7940,7 +8483,7 @@ function tokensTool() {
|
|
|
7940
8483
|
try {
|
|
7941
8484
|
const config = await loadConfig();
|
|
7942
8485
|
const cwd = process.cwd();
|
|
7943
|
-
const specsDir =
|
|
8486
|
+
const specsDir = path15.join(cwd, config.specsDir);
|
|
7944
8487
|
const resolvedPath = await resolveSpecPath(input.specPath, cwd, specsDir);
|
|
7945
8488
|
if (!resolvedPath) {
|
|
7946
8489
|
return {
|
|
@@ -7951,7 +8494,7 @@ function tokensTool() {
|
|
|
7951
8494
|
isError: true
|
|
7952
8495
|
};
|
|
7953
8496
|
}
|
|
7954
|
-
const specName =
|
|
8497
|
+
const specName = path15.basename(resolvedPath);
|
|
7955
8498
|
const result = await counter.countSpec(resolvedPath, {
|
|
7956
8499
|
detailed: input.detailed,
|
|
7957
8500
|
includeSubSpecs: input.includeSubSpecs
|
|
@@ -8342,53 +8885,83 @@ function registerResources(server) {
|
|
|
8342
8885
|
server.registerResource(...specResource());
|
|
8343
8886
|
server.registerResource(...statsResource());
|
|
8344
8887
|
}
|
|
8345
|
-
function
|
|
8888
|
+
function planProjectRoadmapPrompt() {
|
|
8346
8889
|
return [
|
|
8347
|
-
"
|
|
8890
|
+
"plan-project-roadmap",
|
|
8348
8891
|
{
|
|
8349
|
-
title: "
|
|
8350
|
-
description: "
|
|
8892
|
+
title: "Plan Project Roadmap",
|
|
8893
|
+
description: "Interactive roadmap planning with phases, tasks, and dependencies",
|
|
8351
8894
|
argsSchema: {
|
|
8352
|
-
|
|
8353
|
-
description: z.string().optional()
|
|
8895
|
+
goal: z.string()
|
|
8354
8896
|
}
|
|
8355
8897
|
},
|
|
8356
|
-
({
|
|
8898
|
+
({ goal }) => ({
|
|
8357
8899
|
messages: [
|
|
8358
8900
|
{
|
|
8359
8901
|
role: "user",
|
|
8360
8902
|
content: {
|
|
8361
8903
|
type: "text",
|
|
8362
|
-
text: `
|
|
8904
|
+
text: `Plan a project roadmap for: ${goal}
|
|
8363
8905
|
|
|
8364
|
-
|
|
8906
|
+
1. **Review Existing Work**: Analyze current specs using \`list\`/\`board\`, identify what's already planned/in-progress, assess how existing work relates to the new goal
|
|
8907
|
+
2. **Break Down Goal**: Decompose the goal into logical phases or milestones
|
|
8908
|
+
3. **Identify Tasks**: List key tasks and work items for each phase
|
|
8909
|
+
4. **Map Dependencies**: Establish dependencies between tasks (what must be done first)
|
|
8910
|
+
5. **Create Specs**: Create specification documents for major work items using the \`create\` tool
|
|
8911
|
+
6. **Set Relationships**: Use \`link\` tool to establish \`depends_on\` and \`related\` relationships
|
|
8912
|
+
7. **Timeline Estimation**: Provide realistic timeline based on task complexity and project velocity
|
|
8913
|
+
8. **Risk Analysis**: Identify risks, unknowns, and mitigation strategies
|
|
8365
8914
|
|
|
8366
|
-
|
|
8915
|
+
Use the following tools to build the roadmap:
|
|
8916
|
+
- \`list\` / \`board\` / \`stats\` - Understand current project state
|
|
8917
|
+
- \`create\` - Create new specs for roadmap items
|
|
8918
|
+
- \`link\` - Establish dependencies between specs
|
|
8919
|
+
- \`update\` - Set priority and metadata
|
|
8920
|
+
|
|
8921
|
+
Provide a clear roadmap with:
|
|
8922
|
+
- Phases/milestones with descriptions
|
|
8923
|
+
- Key specs to create
|
|
8924
|
+
- Dependency relationships
|
|
8925
|
+
- Recommended execution order
|
|
8926
|
+
- Actionable next steps to implement this plan`
|
|
8367
8927
|
}
|
|
8368
8928
|
}
|
|
8369
8929
|
]
|
|
8370
8930
|
})
|
|
8371
8931
|
];
|
|
8372
8932
|
}
|
|
8373
|
-
|
|
8933
|
+
|
|
8934
|
+
// src/mcp/prompts/project-progress-overview.ts
|
|
8935
|
+
function projectProgressOverviewPrompt() {
|
|
8374
8936
|
return [
|
|
8375
|
-
"
|
|
8937
|
+
"project-progress-overview",
|
|
8376
8938
|
{
|
|
8377
|
-
title: "
|
|
8378
|
-
description: "
|
|
8379
|
-
argsSchema: {
|
|
8380
|
-
topic: z.string()
|
|
8381
|
-
}
|
|
8939
|
+
title: "Project Progress Overview",
|
|
8940
|
+
description: "Generate comprehensive project status report combining specs, git history, and metrics"
|
|
8382
8941
|
},
|
|
8383
|
-
(
|
|
8942
|
+
() => ({
|
|
8384
8943
|
messages: [
|
|
8385
8944
|
{
|
|
8386
8945
|
role: "user",
|
|
8387
8946
|
content: {
|
|
8388
8947
|
type: "text",
|
|
8389
|
-
text: `
|
|
8948
|
+
text: `Analyze project progress and provide a comprehensive overview:
|
|
8390
8949
|
|
|
8391
|
-
|
|
8950
|
+
1. **Spec Analysis**: Review all specs using \`board\` and \`stats\`, group by status (planned/in-progress/complete), highlight any blockers or dependencies
|
|
8951
|
+
2. **Recent Activity**: Examine git commit history (last 2 weeks), identify key changes and development patterns
|
|
8952
|
+
3. **Current State**: Assess what's actively being worked on, what's completed, what's planned
|
|
8953
|
+
4. **Velocity Metrics**: Calculate completion rates, average time in each status, and throughput trends
|
|
8954
|
+
5. **Risk Assessment**: Identify stalled specs, missing dependencies, potential bottlenecks
|
|
8955
|
+
6. **Next Steps**: Recommend priority actions based on current project state
|
|
8956
|
+
|
|
8957
|
+
Use the following tools to gather data:
|
|
8958
|
+
- \`board\` - Get Kanban view of specs by status
|
|
8959
|
+
- \`stats\` - Get project metrics
|
|
8960
|
+
- \`list\` - List specs with filters
|
|
8961
|
+
- \`deps\` - Analyze dependencies for critical specs
|
|
8962
|
+
- Terminal git commands - Analyze recent commit history
|
|
8963
|
+
|
|
8964
|
+
Provide a clear, actionable summary that helps understand project health and next steps.`
|
|
8392
8965
|
}
|
|
8393
8966
|
}
|
|
8394
8967
|
]
|
|
@@ -8403,16 +8976,18 @@ function updateSpecStatusPrompt() {
|
|
|
8403
8976
|
description: "Quick workflow to update specification status",
|
|
8404
8977
|
argsSchema: {
|
|
8405
8978
|
specPath: z.string(),
|
|
8406
|
-
|
|
8979
|
+
status: z.enum(["planned", "in-progress", "complete", "archived"])
|
|
8407
8980
|
}
|
|
8408
8981
|
},
|
|
8409
|
-
({ specPath,
|
|
8982
|
+
({ specPath, status }) => ({
|
|
8410
8983
|
messages: [
|
|
8411
8984
|
{
|
|
8412
8985
|
role: "user",
|
|
8413
8986
|
content: {
|
|
8414
8987
|
type: "text",
|
|
8415
|
-
text: `Update the status of spec "${specPath}" to "${
|
|
8988
|
+
text: `Update the status of spec "${specPath}" to "${status}".
|
|
8989
|
+
|
|
8990
|
+
Use the \`update\` tool: \`update <spec> --status ${status}\``
|
|
8416
8991
|
}
|
|
8417
8992
|
}
|
|
8418
8993
|
]
|
|
@@ -8422,8 +8997,8 @@ function updateSpecStatusPrompt() {
|
|
|
8422
8997
|
|
|
8423
8998
|
// src/mcp/prompts/registry.ts
|
|
8424
8999
|
function registerPrompts(server) {
|
|
8425
|
-
server.registerPrompt(...
|
|
8426
|
-
server.registerPrompt(...
|
|
9000
|
+
server.registerPrompt(...projectProgressOverviewPrompt());
|
|
9001
|
+
server.registerPrompt(...planProjectRoadmapPrompt());
|
|
8427
9002
|
server.registerPrompt(...updateSpecStatusPrompt());
|
|
8428
9003
|
}
|
|
8429
9004
|
|
|
@@ -8458,6 +9033,6 @@ if (import.meta.url === `file://${process.argv[1]}`) {
|
|
|
8458
9033
|
main().catch(console.error);
|
|
8459
9034
|
}
|
|
8460
9035
|
|
|
8461
|
-
export { analyzeCommand, archiveCommand, backfillCommand, boardCommand, checkCommand, compactCommand, createCommand, createMcpServer, depsCommand, filesCommand, ganttCommand, initCommand, linkCommand, listCommand, mcpCommand, migrateCommand, openCommand, searchCommand, splitCommand, statsCommand, templatesCommand, timelineCommand, tokensCommand, uiCommand, unlinkCommand, updateCommand, validateCommand, viewCommand };
|
|
8462
|
-
//# sourceMappingURL=chunk-
|
|
8463
|
-
//# sourceMappingURL=chunk-
|
|
9036
|
+
export { analyzeCommand, archiveCommand, backfillCommand, boardCommand, checkCommand, compactCommand, createCommand, createMcpServer, depsCommand, examplesCommand, filesCommand, ganttCommand, initCommand, linkCommand, listCommand, mcpCommand, migrateCommand, openCommand, searchCommand, splitCommand, statsCommand, templatesCommand, timelineCommand, tokensCommand, uiCommand, unlinkCommand, updateCommand, validateCommand, viewCommand };
|
|
9037
|
+
//# sourceMappingURL=chunk-RTEGSMVL.js.map
|
|
9038
|
+
//# sourceMappingURL=chunk-RTEGSMVL.js.map
|