lean-spec 0.2.0 → 0.2.1
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-J7ZSZ5VJ.js → chunk-ER23B6KS.js} +603 -633
- package/dist/chunk-ER23B6KS.js.map +1 -0
- package/dist/{chunk-S4YNQ5KE.js → chunk-LVD7ZAVZ.js} +9 -17
- package/dist/chunk-LVD7ZAVZ.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +18 -36
- package/dist/cli.js.map +1 -1
- package/dist/commands-ZNL7ZCHU.js +4 -0
- package/dist/commands-ZNL7ZCHU.js.map +1 -0
- package/dist/frontmatter-R2DANL5X.js +3 -0
- package/dist/frontmatter-R2DANL5X.js.map +1 -0
- package/dist/mcp-server.d.ts +16 -0
- package/dist/mcp-server.js +3 -7
- package/dist/mcp-server.js.map +1 -1
- package/package.json +14 -21
- package/CHANGELOG.md +0 -326
- package/LICENSE +0 -21
- package/README.md +0 -421
- package/dist/chunk-J7ZSZ5VJ.js.map +0 -1
- package/dist/chunk-S4YNQ5KE.js.map +0 -1
- package/dist/frontmatter-26SOQGYM.js +0 -23
- package/dist/frontmatter-26SOQGYM.js.map +0 -1
|
@@ -1,23 +1,24 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
} from
|
|
1
|
+
import { normalizeDateFields, getSpecFile, updateFrontmatter, parseFrontmatter, matchesFilter } from './chunk-LVD7ZAVZ.js';
|
|
2
|
+
import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import * as fs9 from 'fs/promises';
|
|
6
|
+
import * as path2 from 'path';
|
|
7
|
+
import { dirname, join } from 'path';
|
|
8
|
+
import chalk16 from 'chalk';
|
|
9
|
+
import matter from 'gray-matter';
|
|
10
|
+
import yaml from 'js-yaml';
|
|
11
|
+
import { spawn, execSync } from 'child_process';
|
|
12
|
+
import ora from 'ora';
|
|
13
|
+
import stripAnsi from 'strip-ansi';
|
|
14
|
+
import { fileURLToPath } from 'url';
|
|
15
|
+
import { select } from '@inquirer/prompts';
|
|
16
|
+
import { ComplexityValidator, TokenCounter, countTokens } from '@leanspec/core';
|
|
17
|
+
import dayjs2 from 'dayjs';
|
|
18
|
+
import { marked } from 'marked';
|
|
19
|
+
import { markedTerminal } from 'marked-terminal';
|
|
20
|
+
import { readFileSync } from 'fs';
|
|
8
21
|
|
|
9
|
-
// src/mcp-server.ts
|
|
10
|
-
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
11
|
-
import { StdioServerTransport as StdioServerTransport2 } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
12
|
-
import { z } from "zod";
|
|
13
|
-
|
|
14
|
-
// src/spec-loader.ts
|
|
15
|
-
import * as fs2 from "fs/promises";
|
|
16
|
-
import * as path2 from "path";
|
|
17
|
-
|
|
18
|
-
// src/config.ts
|
|
19
|
-
import * as fs from "fs/promises";
|
|
20
|
-
import * as path from "path";
|
|
21
22
|
var DEFAULT_CONFIG = {
|
|
22
23
|
template: "spec-template.md",
|
|
23
24
|
templates: {
|
|
@@ -39,9 +40,9 @@ var DEFAULT_CONFIG = {
|
|
|
39
40
|
}
|
|
40
41
|
};
|
|
41
42
|
async function loadConfig(cwd = process.cwd()) {
|
|
42
|
-
const configPath =
|
|
43
|
+
const configPath = path2.join(cwd, ".lean-spec", "config.json");
|
|
43
44
|
try {
|
|
44
|
-
const content = await
|
|
45
|
+
const content = await fs9.readFile(configPath, "utf-8");
|
|
45
46
|
const userConfig = JSON.parse(content);
|
|
46
47
|
const merged = { ...DEFAULT_CONFIG, ...userConfig };
|
|
47
48
|
normalizeLegacyPattern(merged);
|
|
@@ -51,10 +52,10 @@ async function loadConfig(cwd = process.cwd()) {
|
|
|
51
52
|
}
|
|
52
53
|
}
|
|
53
54
|
async function saveConfig(config, cwd = process.cwd()) {
|
|
54
|
-
const configDir =
|
|
55
|
-
const configPath =
|
|
56
|
-
await
|
|
57
|
-
await
|
|
55
|
+
const configDir = path2.join(cwd, ".lean-spec");
|
|
56
|
+
const configPath = path2.join(configDir, "config.json");
|
|
57
|
+
await fs9.mkdir(configDir, { recursive: true });
|
|
58
|
+
await fs9.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
|
|
58
59
|
}
|
|
59
60
|
function getToday(format = "YYYYMMDD") {
|
|
60
61
|
const now = /* @__PURE__ */ new Date();
|
|
@@ -136,12 +137,12 @@ function extractGroup(extractor, dateFormat = "YYYYMMDD", fields, fallback) {
|
|
|
136
137
|
async function loadSubFiles(specDir, options = {}) {
|
|
137
138
|
const subFiles = [];
|
|
138
139
|
try {
|
|
139
|
-
const entries = await
|
|
140
|
+
const entries = await fs9.readdir(specDir, { withFileTypes: true });
|
|
140
141
|
for (const entry of entries) {
|
|
141
142
|
if (entry.name === "README.md") continue;
|
|
142
143
|
if (entry.isDirectory()) continue;
|
|
143
144
|
const filePath = path2.join(specDir, entry.name);
|
|
144
|
-
const stat5 = await
|
|
145
|
+
const stat5 = await fs9.stat(filePath);
|
|
145
146
|
const ext = path2.extname(entry.name).toLowerCase();
|
|
146
147
|
const isDocument = ext === ".md";
|
|
147
148
|
const subFile = {
|
|
@@ -151,7 +152,7 @@ async function loadSubFiles(specDir, options = {}) {
|
|
|
151
152
|
type: isDocument ? "document" : "asset"
|
|
152
153
|
};
|
|
153
154
|
if (isDocument && options.includeContent) {
|
|
154
|
-
subFile.content = await
|
|
155
|
+
subFile.content = await fs9.readFile(filePath, "utf-8");
|
|
155
156
|
}
|
|
156
157
|
subFiles.push(subFile);
|
|
157
158
|
}
|
|
@@ -171,14 +172,14 @@ async function loadAllSpecs(options = {}) {
|
|
|
171
172
|
const specsDir = path2.join(cwd, config.specsDir);
|
|
172
173
|
const specs = [];
|
|
173
174
|
try {
|
|
174
|
-
await
|
|
175
|
+
await fs9.access(specsDir);
|
|
175
176
|
} catch {
|
|
176
177
|
return [];
|
|
177
178
|
}
|
|
178
179
|
const specPattern = /^(\d{2,})-/;
|
|
179
180
|
async function loadSpecsFromDir(dir, relativePath = "") {
|
|
180
181
|
try {
|
|
181
|
-
const entries = await
|
|
182
|
+
const entries = await fs9.readdir(dir, { withFileTypes: true });
|
|
182
183
|
for (const entry of entries) {
|
|
183
184
|
if (!entry.isDirectory()) continue;
|
|
184
185
|
if (entry.name === "archived" && relativePath === "") continue;
|
|
@@ -212,7 +213,7 @@ async function loadAllSpecs(options = {}) {
|
|
|
212
213
|
frontmatter
|
|
213
214
|
};
|
|
214
215
|
if (options.includeContent) {
|
|
215
|
-
specInfo.content = await
|
|
216
|
+
specInfo.content = await fs9.readFile(specFile, "utf-8");
|
|
216
217
|
}
|
|
217
218
|
if (options.includeSubFiles) {
|
|
218
219
|
specInfo.subFiles = await loadSubFiles(entryPath, {
|
|
@@ -287,7 +288,7 @@ async function getSpec(specPath) {
|
|
|
287
288
|
fullPath = path2.join(specsDir, specPath);
|
|
288
289
|
}
|
|
289
290
|
try {
|
|
290
|
-
await
|
|
291
|
+
await fs9.access(fullPath);
|
|
291
292
|
} catch {
|
|
292
293
|
return null;
|
|
293
294
|
}
|
|
@@ -295,7 +296,7 @@ async function getSpec(specPath) {
|
|
|
295
296
|
if (!specFile) return null;
|
|
296
297
|
const frontmatter = await parseFrontmatter(specFile, config);
|
|
297
298
|
if (!frontmatter) return null;
|
|
298
|
-
const content = await
|
|
299
|
+
const content = await fs9.readFile(specFile, "utf-8");
|
|
299
300
|
const relativePath = path2.relative(specsDir, fullPath);
|
|
300
301
|
const parts = relativePath.split(path2.sep);
|
|
301
302
|
const date = parts[0] === "archived" ? parts[1] : parts[0];
|
|
@@ -310,17 +311,6 @@ async function getSpec(specPath) {
|
|
|
310
311
|
content
|
|
311
312
|
};
|
|
312
313
|
}
|
|
313
|
-
|
|
314
|
-
// src/commands/create.ts
|
|
315
|
-
import * as fs5 from "fs/promises";
|
|
316
|
-
import * as path6 from "path";
|
|
317
|
-
import chalk4 from "chalk";
|
|
318
|
-
import matter from "gray-matter";
|
|
319
|
-
import yaml from "js-yaml";
|
|
320
|
-
|
|
321
|
-
// src/utils/path-helpers.ts
|
|
322
|
-
import * as fs3 from "fs/promises";
|
|
323
|
-
import * as path3 from "path";
|
|
324
314
|
function createSpecDirPattern() {
|
|
325
315
|
return /(?:^|\D)(\d{2,4})-[a-z]/i;
|
|
326
316
|
}
|
|
@@ -330,7 +320,7 @@ async function getGlobalNextSeq(specsDir, digits) {
|
|
|
330
320
|
const specPattern = createSpecDirPattern();
|
|
331
321
|
async function scanDirectory(dir) {
|
|
332
322
|
try {
|
|
333
|
-
const entries = await
|
|
323
|
+
const entries = await fs9.readdir(dir, { withFileTypes: true });
|
|
334
324
|
for (const entry of entries) {
|
|
335
325
|
if (!entry.isDirectory()) continue;
|
|
336
326
|
const match = entry.name.match(specPattern);
|
|
@@ -341,7 +331,7 @@ async function getGlobalNextSeq(specsDir, digits) {
|
|
|
341
331
|
}
|
|
342
332
|
}
|
|
343
333
|
if (entry.name === "archived") continue;
|
|
344
|
-
const subDir =
|
|
334
|
+
const subDir = path2.join(dir, entry.name);
|
|
345
335
|
await scanDirectory(subDir);
|
|
346
336
|
}
|
|
347
337
|
} catch {
|
|
@@ -358,23 +348,23 @@ async function getGlobalNextSeq(specsDir, digits) {
|
|
|
358
348
|
}
|
|
359
349
|
}
|
|
360
350
|
async function resolveSpecPath(specPath, cwd, specsDir) {
|
|
361
|
-
if (
|
|
351
|
+
if (path2.isAbsolute(specPath)) {
|
|
362
352
|
try {
|
|
363
|
-
await
|
|
353
|
+
await fs9.access(specPath);
|
|
364
354
|
return specPath;
|
|
365
355
|
} catch {
|
|
366
356
|
return null;
|
|
367
357
|
}
|
|
368
358
|
}
|
|
369
|
-
const cwdPath =
|
|
359
|
+
const cwdPath = path2.resolve(cwd, specPath);
|
|
370
360
|
try {
|
|
371
|
-
await
|
|
361
|
+
await fs9.access(cwdPath);
|
|
372
362
|
return cwdPath;
|
|
373
363
|
} catch {
|
|
374
364
|
}
|
|
375
|
-
const specsPath =
|
|
365
|
+
const specsPath = path2.join(specsDir, specPath);
|
|
376
366
|
try {
|
|
377
|
-
await
|
|
367
|
+
await fs9.access(specsPath);
|
|
378
368
|
return specsPath;
|
|
379
369
|
} catch {
|
|
380
370
|
}
|
|
@@ -392,17 +382,17 @@ async function searchBySequence(specsDir, seqNum) {
|
|
|
392
382
|
const specPattern = createSpecDirPattern();
|
|
393
383
|
async function scanDirectory(dir) {
|
|
394
384
|
try {
|
|
395
|
-
const entries = await
|
|
385
|
+
const entries = await fs9.readdir(dir, { withFileTypes: true });
|
|
396
386
|
for (const entry of entries) {
|
|
397
387
|
if (!entry.isDirectory()) continue;
|
|
398
388
|
const match = entry.name.match(specPattern);
|
|
399
389
|
if (match) {
|
|
400
390
|
const entrySeq = parseInt(match[1], 10);
|
|
401
391
|
if (entrySeq === seqNum) {
|
|
402
|
-
return
|
|
392
|
+
return path2.join(dir, entry.name);
|
|
403
393
|
}
|
|
404
394
|
}
|
|
405
|
-
const subDir =
|
|
395
|
+
const subDir = path2.join(dir, entry.name);
|
|
406
396
|
const result = await scanDirectory(subDir);
|
|
407
397
|
if (result) return result;
|
|
408
398
|
}
|
|
@@ -415,13 +405,13 @@ async function searchBySequence(specsDir, seqNum) {
|
|
|
415
405
|
async function searchInAllDirectories(specsDir, specName) {
|
|
416
406
|
async function scanDirectory(dir) {
|
|
417
407
|
try {
|
|
418
|
-
const entries = await
|
|
408
|
+
const entries = await fs9.readdir(dir, { withFileTypes: true });
|
|
419
409
|
for (const entry of entries) {
|
|
420
410
|
if (!entry.isDirectory()) continue;
|
|
421
411
|
if (entry.name === specName) {
|
|
422
|
-
return
|
|
412
|
+
return path2.join(dir, entry.name);
|
|
423
413
|
}
|
|
424
|
-
const subDir =
|
|
414
|
+
const subDir = path2.join(dir, entry.name);
|
|
425
415
|
const result = await scanDirectory(subDir);
|
|
426
416
|
if (result) return result;
|
|
427
417
|
}
|
|
@@ -431,11 +421,6 @@ async function searchInAllDirectories(specsDir, specName) {
|
|
|
431
421
|
}
|
|
432
422
|
return scanDirectory(specsDir);
|
|
433
423
|
}
|
|
434
|
-
|
|
435
|
-
// src/utils/variable-resolver.ts
|
|
436
|
-
import * as fs4 from "fs/promises";
|
|
437
|
-
import * as path4 from "path";
|
|
438
|
-
import { execSync } from "child_process";
|
|
439
424
|
async function getGitInfo() {
|
|
440
425
|
try {
|
|
441
426
|
const user = execSync("git config user.name", { encoding: "utf-8" }).trim();
|
|
@@ -457,8 +442,8 @@ async function getGitInfo() {
|
|
|
457
442
|
}
|
|
458
443
|
async function getProjectName(cwd = process.cwd()) {
|
|
459
444
|
try {
|
|
460
|
-
const packageJsonPath =
|
|
461
|
-
const content = await
|
|
445
|
+
const packageJsonPath = path2.join(cwd, "package.json");
|
|
446
|
+
const content = await fs9.readFile(packageJsonPath, "utf-8");
|
|
462
447
|
const packageJson2 = JSON.parse(content);
|
|
463
448
|
return packageJson2.name || null;
|
|
464
449
|
} catch {
|
|
@@ -539,18 +524,6 @@ async function buildVariableContext(config, options = {}) {
|
|
|
539
524
|
context.gitInfo = await getGitInfo() ?? void 0;
|
|
540
525
|
return context;
|
|
541
526
|
}
|
|
542
|
-
|
|
543
|
-
// src/commands/check.ts
|
|
544
|
-
import * as path5 from "path";
|
|
545
|
-
import chalk3 from "chalk";
|
|
546
|
-
|
|
547
|
-
// src/utils/ui.ts
|
|
548
|
-
import ora from "ora";
|
|
549
|
-
import chalk2 from "chalk";
|
|
550
|
-
|
|
551
|
-
// src/utils/safe-output.ts
|
|
552
|
-
import chalk from "chalk";
|
|
553
|
-
import stripAnsi from "strip-ansi";
|
|
554
527
|
function sanitizeUserInput(input) {
|
|
555
528
|
if (typeof input !== "string") {
|
|
556
529
|
return "";
|
|
@@ -571,7 +544,7 @@ async function withSpinner(text, fn, options) {
|
|
|
571
544
|
spinner.succeed(options?.successText || text);
|
|
572
545
|
return result;
|
|
573
546
|
} catch (error) {
|
|
574
|
-
spinner.fail(
|
|
547
|
+
spinner.fail(`${text} failed`);
|
|
575
548
|
throw error;
|
|
576
549
|
}
|
|
577
550
|
}
|
|
@@ -580,12 +553,12 @@ async function withSpinner(text, fn, options) {
|
|
|
580
553
|
async function checkSpecs(options = {}) {
|
|
581
554
|
const config = await loadConfig();
|
|
582
555
|
const cwd = process.cwd();
|
|
583
|
-
|
|
556
|
+
path2.join(cwd, config.specsDir);
|
|
584
557
|
const specs = await loadAllSpecs();
|
|
585
558
|
const sequenceMap = /* @__PURE__ */ new Map();
|
|
586
559
|
const specPattern = createSpecDirPattern();
|
|
587
560
|
for (const spec of specs) {
|
|
588
|
-
const specName =
|
|
561
|
+
const specName = path2.basename(spec.path);
|
|
589
562
|
const match = specName.match(specPattern);
|
|
590
563
|
if (match) {
|
|
591
564
|
const seq = parseInt(match[1], 10);
|
|
@@ -600,30 +573,30 @@ async function checkSpecs(options = {}) {
|
|
|
600
573
|
const conflicts = Array.from(sequenceMap.entries()).filter(([_, paths]) => paths.length > 1).sort(([a], [b]) => a - b);
|
|
601
574
|
if (conflicts.length === 0) {
|
|
602
575
|
if (!options.quiet && !options.silent) {
|
|
603
|
-
console.log(
|
|
576
|
+
console.log(chalk16.green("\u2713 No sequence conflicts detected"));
|
|
604
577
|
}
|
|
605
578
|
return true;
|
|
606
579
|
}
|
|
607
580
|
if (!options.silent) {
|
|
608
581
|
if (!options.quiet) {
|
|
609
582
|
console.log("");
|
|
610
|
-
console.log(
|
|
583
|
+
console.log(chalk16.yellow("\u26A0\uFE0F Sequence conflicts detected:\n"));
|
|
611
584
|
for (const [seq, paths] of conflicts) {
|
|
612
|
-
console.log(
|
|
585
|
+
console.log(chalk16.red(` Sequence ${String(seq).padStart(config.structure.sequenceDigits, "0")}:`));
|
|
613
586
|
for (const p of paths) {
|
|
614
|
-
console.log(
|
|
587
|
+
console.log(chalk16.gray(` - ${sanitizeUserInput(p)}`));
|
|
615
588
|
}
|
|
616
589
|
console.log("");
|
|
617
590
|
}
|
|
618
|
-
console.log(
|
|
619
|
-
console.log(
|
|
591
|
+
console.log(chalk16.cyan("Tip: Use date prefix to prevent conflicts:"));
|
|
592
|
+
console.log(chalk16.gray(' Edit .lean-spec/config.json \u2192 structure.prefix: "{YYYYMMDD}-"'));
|
|
620
593
|
console.log("");
|
|
621
|
-
console.log(
|
|
594
|
+
console.log(chalk16.cyan("Or rename folders manually to resolve."));
|
|
622
595
|
console.log("");
|
|
623
596
|
} else {
|
|
624
597
|
console.log("");
|
|
625
|
-
console.log(
|
|
626
|
-
console.log(
|
|
598
|
+
console.log(chalk16.yellow(`\u26A0\uFE0F Conflict warning: ${conflicts.length} sequence conflict(s) detected`));
|
|
599
|
+
console.log(chalk16.gray("Run: lean-spec check"));
|
|
627
600
|
console.log("");
|
|
628
601
|
}
|
|
629
602
|
}
|
|
@@ -644,8 +617,8 @@ async function autoCheckIfEnabled() {
|
|
|
644
617
|
async function createSpec(name, options = {}) {
|
|
645
618
|
const config = await loadConfig();
|
|
646
619
|
const cwd = process.cwd();
|
|
647
|
-
const specsDir =
|
|
648
|
-
await
|
|
620
|
+
const specsDir = path2.join(cwd, config.specsDir);
|
|
621
|
+
await fs9.mkdir(specsDir, { recursive: true });
|
|
649
622
|
const seq = await getGlobalNextSeq(specsDir, config.structure.sequenceDigits);
|
|
650
623
|
let specRelativePath;
|
|
651
624
|
if (config.structure.pattern === "flat") {
|
|
@@ -665,19 +638,18 @@ async function createSpec(name, options = {}) {
|
|
|
665
638
|
} else {
|
|
666
639
|
throw new Error(`Unknown pattern: ${config.structure.pattern}`);
|
|
667
640
|
}
|
|
668
|
-
const specDir =
|
|
669
|
-
const specFile =
|
|
641
|
+
const specDir = path2.join(specsDir, specRelativePath);
|
|
642
|
+
const specFile = path2.join(specDir, config.structure.defaultFile);
|
|
670
643
|
try {
|
|
671
|
-
await
|
|
644
|
+
await fs9.access(specDir);
|
|
672
645
|
throw new Error(`Spec already exists: ${sanitizeUserInput(specDir)}`);
|
|
673
646
|
} catch (error) {
|
|
674
|
-
if (error.code === "ENOENT") {
|
|
675
|
-
} else {
|
|
647
|
+
if (error.code === "ENOENT") ; else {
|
|
676
648
|
throw error;
|
|
677
649
|
}
|
|
678
650
|
}
|
|
679
|
-
await
|
|
680
|
-
const templatesDir =
|
|
651
|
+
await fs9.mkdir(specDir, { recursive: true });
|
|
652
|
+
const templatesDir = path2.join(cwd, ".lean-spec", "templates");
|
|
681
653
|
let templateName;
|
|
682
654
|
if (options.template) {
|
|
683
655
|
if (config.templates?.[options.template]) {
|
|
@@ -689,10 +661,10 @@ async function createSpec(name, options = {}) {
|
|
|
689
661
|
} else {
|
|
690
662
|
templateName = config.template || "spec-template.md";
|
|
691
663
|
}
|
|
692
|
-
const templatePath =
|
|
664
|
+
const templatePath = path2.join(templatesDir, templateName);
|
|
693
665
|
let content;
|
|
694
666
|
try {
|
|
695
|
-
const template = await
|
|
667
|
+
const template = await fs9.readFile(templatePath, "utf-8");
|
|
696
668
|
const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
697
669
|
const title = options.title || name;
|
|
698
670
|
const varContext = await buildVariableContext(config, { name: title, date });
|
|
@@ -722,7 +694,7 @@ async function createSpec(name, options = {}) {
|
|
|
722
694
|
frontmatter: parsed.data
|
|
723
695
|
};
|
|
724
696
|
parsed.content = resolveVariables(parsed.content, contextWithFrontmatter);
|
|
725
|
-
const { enrichWithTimestamps } = await import(
|
|
697
|
+
const { enrichWithTimestamps } = await import('./frontmatter-R2DANL5X.js');
|
|
726
698
|
enrichWithTimestamps(parsed.data);
|
|
727
699
|
content = matter.stringify(parsed.content, parsed.data);
|
|
728
700
|
if (options.description) {
|
|
@@ -736,21 +708,16 @@ ${options.description}`
|
|
|
736
708
|
} catch (error) {
|
|
737
709
|
throw new Error(`Template not found: ${templatePath}. Run: lean-spec init`);
|
|
738
710
|
}
|
|
739
|
-
await
|
|
740
|
-
console.log(
|
|
741
|
-
console.log(
|
|
711
|
+
await fs9.writeFile(specFile, content, "utf-8");
|
|
712
|
+
console.log(chalk16.green(`\u2713 Created: ${sanitizeUserInput(specDir)}/`));
|
|
713
|
+
console.log(chalk16.gray(` Edit: ${sanitizeUserInput(specFile)}`));
|
|
742
714
|
await autoCheckIfEnabled();
|
|
743
715
|
}
|
|
744
|
-
|
|
745
|
-
// src/commands/archive.ts
|
|
746
|
-
import * as fs6 from "fs/promises";
|
|
747
|
-
import * as path7 from "path";
|
|
748
|
-
import chalk5 from "chalk";
|
|
749
716
|
async function archiveSpec(specPath) {
|
|
750
717
|
await autoCheckIfEnabled();
|
|
751
718
|
const config = await loadConfig();
|
|
752
719
|
const cwd = process.cwd();
|
|
753
|
-
const specsDir =
|
|
720
|
+
const specsDir = path2.join(cwd, config.specsDir);
|
|
754
721
|
const resolvedPath = await resolveSpecPath(specPath, cwd, specsDir);
|
|
755
722
|
if (!resolvedPath) {
|
|
756
723
|
throw new Error(`Spec not found: ${sanitizeUserInput(specPath)}`);
|
|
@@ -759,19 +726,14 @@ async function archiveSpec(specPath) {
|
|
|
759
726
|
if (specFile) {
|
|
760
727
|
await updateFrontmatter(specFile, { status: "archived" });
|
|
761
728
|
}
|
|
762
|
-
const archiveDir =
|
|
763
|
-
await
|
|
764
|
-
const specName =
|
|
765
|
-
const archivePath =
|
|
766
|
-
await
|
|
767
|
-
console.log(
|
|
729
|
+
const archiveDir = path2.join(specsDir, "archived");
|
|
730
|
+
await fs9.mkdir(archiveDir, { recursive: true });
|
|
731
|
+
const specName = path2.basename(resolvedPath);
|
|
732
|
+
const archivePath = path2.join(archiveDir, specName);
|
|
733
|
+
await fs9.rename(resolvedPath, archivePath);
|
|
734
|
+
console.log(chalk16.green(`\u2713 Archived: ${sanitizeUserInput(archivePath)}`));
|
|
768
735
|
}
|
|
769
736
|
|
|
770
|
-
// src/commands/list.ts
|
|
771
|
-
import chalk7 from "chalk";
|
|
772
|
-
import * as fs7 from "fs/promises";
|
|
773
|
-
import * as path8 from "path";
|
|
774
|
-
|
|
775
737
|
// src/utils/pattern-detection.ts
|
|
776
738
|
function detectPatternType(config) {
|
|
777
739
|
const { pattern, groupExtractor } = config.structure;
|
|
@@ -795,66 +757,63 @@ function detectPatternType(config) {
|
|
|
795
757
|
shouldGroup: false
|
|
796
758
|
};
|
|
797
759
|
}
|
|
798
|
-
|
|
799
|
-
// src/utils/colors.ts
|
|
800
|
-
import chalk6 from "chalk";
|
|
801
760
|
var STATUS_CONFIG = {
|
|
802
761
|
planned: {
|
|
803
762
|
emoji: "\u{1F4C5}",
|
|
804
763
|
label: "Planned",
|
|
805
|
-
colorFn:
|
|
806
|
-
badge: (s = "planned") =>
|
|
764
|
+
colorFn: chalk16.blue,
|
|
765
|
+
badge: (s = "planned") => chalk16.blue(`[${s}]`)
|
|
807
766
|
},
|
|
808
767
|
"in-progress": {
|
|
809
768
|
emoji: "\u23F3",
|
|
810
769
|
label: "In Progress",
|
|
811
|
-
colorFn:
|
|
812
|
-
badge: (s = "in-progress") =>
|
|
770
|
+
colorFn: chalk16.yellow,
|
|
771
|
+
badge: (s = "in-progress") => chalk16.yellow(`[${s}]`)
|
|
813
772
|
},
|
|
814
773
|
complete: {
|
|
815
774
|
emoji: "\u2705",
|
|
816
775
|
label: "Complete",
|
|
817
|
-
colorFn:
|
|
818
|
-
badge: (s = "complete") =>
|
|
776
|
+
colorFn: chalk16.green,
|
|
777
|
+
badge: (s = "complete") => chalk16.green(`[${s}]`)
|
|
819
778
|
},
|
|
820
779
|
archived: {
|
|
821
780
|
emoji: "\u{1F4E6}",
|
|
822
781
|
label: "Archived",
|
|
823
|
-
colorFn:
|
|
824
|
-
badge: (s = "archived") =>
|
|
782
|
+
colorFn: chalk16.gray,
|
|
783
|
+
badge: (s = "archived") => chalk16.gray(`[${s}]`)
|
|
825
784
|
}
|
|
826
785
|
};
|
|
827
786
|
var PRIORITY_CONFIG = {
|
|
828
787
|
critical: {
|
|
829
788
|
emoji: "\u{1F534}",
|
|
830
|
-
colorFn:
|
|
831
|
-
badge: (s = "critical") =>
|
|
789
|
+
colorFn: chalk16.red.bold,
|
|
790
|
+
badge: (s = "critical") => chalk16.red.bold(`[${s}]`)
|
|
832
791
|
},
|
|
833
792
|
high: {
|
|
834
793
|
emoji: "\u{1F7E0}",
|
|
835
|
-
colorFn:
|
|
836
|
-
badge: (s = "high") =>
|
|
794
|
+
colorFn: chalk16.hex("#FFA500"),
|
|
795
|
+
badge: (s = "high") => chalk16.hex("#FFA500")(`[${s}]`)
|
|
837
796
|
},
|
|
838
797
|
medium: {
|
|
839
798
|
emoji: "\u{1F7E1}",
|
|
840
|
-
colorFn:
|
|
841
|
-
badge: (s = "medium") =>
|
|
799
|
+
colorFn: chalk16.yellow,
|
|
800
|
+
badge: (s = "medium") => chalk16.yellow(`[${s}]`)
|
|
842
801
|
},
|
|
843
802
|
low: {
|
|
844
803
|
emoji: "\u{1F7E2}",
|
|
845
|
-
colorFn:
|
|
846
|
-
badge: (s = "low") =>
|
|
804
|
+
colorFn: chalk16.gray,
|
|
805
|
+
badge: (s = "low") => chalk16.gray(`[${s}]`)
|
|
847
806
|
}
|
|
848
807
|
};
|
|
849
808
|
function formatStatusBadge(status) {
|
|
850
|
-
return STATUS_CONFIG[status]?.badge() ||
|
|
809
|
+
return STATUS_CONFIG[status]?.badge() || chalk16.white(`[${status}]`);
|
|
851
810
|
}
|
|
852
811
|
function formatPriorityBadge(priority) {
|
|
853
|
-
return PRIORITY_CONFIG[priority]?.badge() ||
|
|
812
|
+
return PRIORITY_CONFIG[priority]?.badge() || chalk16.white(`[${priority}]`);
|
|
854
813
|
}
|
|
855
814
|
function getStatusIndicator(status) {
|
|
856
815
|
const config = STATUS_CONFIG[status];
|
|
857
|
-
if (!config) return
|
|
816
|
+
if (!config) return chalk16.gray("[unknown]");
|
|
858
817
|
return config.colorFn(`[${status}]`);
|
|
859
818
|
}
|
|
860
819
|
function getStatusEmoji(status) {
|
|
@@ -869,9 +828,9 @@ async function listSpecs(options = {}) {
|
|
|
869
828
|
await autoCheckIfEnabled();
|
|
870
829
|
const config = await loadConfig();
|
|
871
830
|
const cwd = process.cwd();
|
|
872
|
-
const specsDir =
|
|
831
|
+
const specsDir = path2.join(cwd, config.specsDir);
|
|
873
832
|
try {
|
|
874
|
-
await
|
|
833
|
+
await fs9.access(specsDir);
|
|
875
834
|
} catch {
|
|
876
835
|
console.log("");
|
|
877
836
|
console.log("No specs directory found. Initialize with: lean-spec init");
|
|
@@ -894,10 +853,10 @@ async function listSpecs(options = {}) {
|
|
|
894
853
|
})
|
|
895
854
|
);
|
|
896
855
|
if (specs.length === 0) {
|
|
897
|
-
console.log(
|
|
856
|
+
console.log(chalk16.dim("No specs found."));
|
|
898
857
|
return;
|
|
899
858
|
}
|
|
900
|
-
console.log(
|
|
859
|
+
console.log(chalk16.bold.cyan("\u{1F4C4} Spec List"));
|
|
901
860
|
const filterParts = [];
|
|
902
861
|
if (options.status) {
|
|
903
862
|
const statusStr = Array.isArray(options.status) ? options.status.join(",") : options.status;
|
|
@@ -910,7 +869,7 @@ async function listSpecs(options = {}) {
|
|
|
910
869
|
}
|
|
911
870
|
if (options.assignee) filterParts.push(`assignee=${options.assignee}`);
|
|
912
871
|
if (filterParts.length > 0) {
|
|
913
|
-
console.log(
|
|
872
|
+
console.log(chalk16.dim(`Filtered by: ${filterParts.join(", ")}`));
|
|
914
873
|
}
|
|
915
874
|
console.log("");
|
|
916
875
|
const patternInfo = detectPatternType(config);
|
|
@@ -920,7 +879,7 @@ async function listSpecs(options = {}) {
|
|
|
920
879
|
renderFlatList(specs);
|
|
921
880
|
}
|
|
922
881
|
console.log("");
|
|
923
|
-
console.log(
|
|
882
|
+
console.log(chalk16.bold(`Total: ${chalk16.green(specs.length)} spec${specs.length !== 1 ? "s" : ""}`));
|
|
924
883
|
}
|
|
925
884
|
function renderFlatList(specs) {
|
|
926
885
|
for (const spec of specs) {
|
|
@@ -928,18 +887,18 @@ function renderFlatList(specs) {
|
|
|
928
887
|
const priorityEmoji = getPriorityEmoji(spec.frontmatter.priority);
|
|
929
888
|
let assigneeStr = "";
|
|
930
889
|
if (spec.frontmatter.assignee) {
|
|
931
|
-
assigneeStr = " " +
|
|
890
|
+
assigneeStr = " " + chalk16.cyan(`@${sanitizeUserInput(spec.frontmatter.assignee)}`);
|
|
932
891
|
}
|
|
933
892
|
let tagsStr = "";
|
|
934
893
|
if (spec.frontmatter.tags?.length) {
|
|
935
894
|
const tags = Array.isArray(spec.frontmatter.tags) ? spec.frontmatter.tags : [];
|
|
936
895
|
if (tags.length > 0) {
|
|
937
896
|
const tagStr = tags.map((tag) => `#${sanitizeUserInput(tag)}`).join(" ");
|
|
938
|
-
tagsStr = " " +
|
|
897
|
+
tagsStr = " " + chalk16.dim(chalk16.magenta(tagStr));
|
|
939
898
|
}
|
|
940
899
|
}
|
|
941
900
|
const priorityPrefix = priorityEmoji ? `${priorityEmoji} ` : "";
|
|
942
|
-
console.log(`${priorityPrefix}${statusEmoji} ${
|
|
901
|
+
console.log(`${priorityPrefix}${statusEmoji} ${chalk16.cyan(sanitizeUserInput(spec.path))}${assigneeStr}${tagsStr}`);
|
|
943
902
|
}
|
|
944
903
|
}
|
|
945
904
|
function renderGroupedList(specs, groupExtractor) {
|
|
@@ -968,7 +927,7 @@ function renderGroupedList(specs, groupExtractor) {
|
|
|
968
927
|
const groupName = sortedGroups[i];
|
|
969
928
|
const groupSpecs = groups.get(groupName);
|
|
970
929
|
const groupEmoji = /^\d{8}$/.test(groupName) ? "\u{1F4C5}" : groupName.startsWith("milestone") ? "\u{1F3AF}" : "\u{1F4C1}";
|
|
971
|
-
console.log(`${
|
|
930
|
+
console.log(`${chalk16.bold.cyan(`${groupEmoji} ${groupName}/`)} ${chalk16.dim(`(${groupSpecs.length})`)}`);
|
|
972
931
|
console.log("");
|
|
973
932
|
for (const spec of groupSpecs) {
|
|
974
933
|
const statusEmoji = getStatusEmoji(spec.frontmatter.status);
|
|
@@ -976,33 +935,29 @@ function renderGroupedList(specs, groupExtractor) {
|
|
|
976
935
|
const displayPath = spec.path.includes("/") ? spec.path.split("/").slice(1).join("/") : spec.path;
|
|
977
936
|
let assigneeStr = "";
|
|
978
937
|
if (spec.frontmatter.assignee) {
|
|
979
|
-
assigneeStr = " " +
|
|
938
|
+
assigneeStr = " " + chalk16.cyan(`@${sanitizeUserInput(spec.frontmatter.assignee)}`);
|
|
980
939
|
}
|
|
981
940
|
let tagsStr = "";
|
|
982
941
|
if (spec.frontmatter.tags?.length) {
|
|
983
942
|
const tags = Array.isArray(spec.frontmatter.tags) ? spec.frontmatter.tags : [];
|
|
984
943
|
if (tags.length > 0) {
|
|
985
944
|
const tagStr = tags.map((tag) => `#${sanitizeUserInput(tag)}`).join(" ");
|
|
986
|
-
tagsStr = " " +
|
|
945
|
+
tagsStr = " " + chalk16.dim(chalk16.magenta(tagStr));
|
|
987
946
|
}
|
|
988
947
|
}
|
|
989
948
|
const priorityPrefix = priorityEmoji ? `${priorityEmoji} ` : "";
|
|
990
|
-
console.log(` ${priorityPrefix}${statusEmoji} ${
|
|
949
|
+
console.log(` ${priorityPrefix}${statusEmoji} ${chalk16.cyan(sanitizeUserInput(displayPath))}${assigneeStr}${tagsStr}`);
|
|
991
950
|
}
|
|
992
951
|
if (i < sortedGroups.length - 1) {
|
|
993
952
|
console.log("");
|
|
994
953
|
}
|
|
995
954
|
}
|
|
996
955
|
}
|
|
997
|
-
|
|
998
|
-
// src/commands/update.ts
|
|
999
|
-
import * as path9 from "path";
|
|
1000
|
-
import chalk8 from "chalk";
|
|
1001
956
|
async function updateSpec(specPath, updates) {
|
|
1002
957
|
await autoCheckIfEnabled();
|
|
1003
958
|
const config = await loadConfig();
|
|
1004
959
|
const cwd = process.cwd();
|
|
1005
|
-
const specsDir =
|
|
960
|
+
const specsDir = path2.join(cwd, config.specsDir);
|
|
1006
961
|
const resolvedPath = await resolveSpecPath(specPath, cwd, specsDir);
|
|
1007
962
|
if (!resolvedPath) {
|
|
1008
963
|
throw new Error(`Spec not found: ${sanitizeUserInput(specPath)}. Tried: ${sanitizeUserInput(specPath)}, specs/${sanitizeUserInput(specPath)}, and searching in date directories`);
|
|
@@ -1024,22 +979,16 @@ async function updateSpec(specPath, updates) {
|
|
|
1024
979
|
});
|
|
1025
980
|
}
|
|
1026
981
|
await updateFrontmatter(specFile, allUpdates);
|
|
1027
|
-
console.log(
|
|
982
|
+
console.log(chalk16.green(`\u2713 Updated: ${sanitizeUserInput(path2.relative(cwd, resolvedPath))}`));
|
|
1028
983
|
const updatedFields = Object.keys(updates).filter((k) => k !== "customFields");
|
|
1029
984
|
if (updates.customFields) {
|
|
1030
985
|
updatedFields.push(...Object.keys(updates.customFields));
|
|
1031
986
|
}
|
|
1032
|
-
console.log(
|
|
987
|
+
console.log(chalk16.gray(` Fields: ${updatedFields.join(", ")}`));
|
|
1033
988
|
}
|
|
1034
|
-
|
|
1035
|
-
// src/commands/backfill.ts
|
|
1036
|
-
import * as path10 from "path";
|
|
1037
|
-
|
|
1038
|
-
// src/utils/git-timestamps.ts
|
|
1039
|
-
import { execSync as execSync2 } from "child_process";
|
|
1040
989
|
function isGitRepository() {
|
|
1041
990
|
try {
|
|
1042
|
-
|
|
991
|
+
execSync("git rev-parse --is-inside-work-tree", {
|
|
1043
992
|
stdio: "ignore",
|
|
1044
993
|
encoding: "utf-8"
|
|
1045
994
|
});
|
|
@@ -1050,7 +999,7 @@ function isGitRepository() {
|
|
|
1050
999
|
}
|
|
1051
1000
|
function getFirstCommitTimestamp(filePath) {
|
|
1052
1001
|
try {
|
|
1053
|
-
const timestamp =
|
|
1002
|
+
const timestamp = execSync(
|
|
1054
1003
|
`git log --follow --format="%aI" --diff-filter=A -- "${filePath}" | tail -1`,
|
|
1055
1004
|
{ encoding: "utf-8", stdio: ["pipe", "pipe", "ignore"] }
|
|
1056
1005
|
).trim();
|
|
@@ -1061,7 +1010,7 @@ function getFirstCommitTimestamp(filePath) {
|
|
|
1061
1010
|
}
|
|
1062
1011
|
function getLastCommitTimestamp(filePath) {
|
|
1063
1012
|
try {
|
|
1064
|
-
const timestamp =
|
|
1013
|
+
const timestamp = execSync(
|
|
1065
1014
|
`git log --format="%aI" -n 1 -- "${filePath}"`,
|
|
1066
1015
|
{ encoding: "utf-8", stdio: ["pipe", "pipe", "ignore"] }
|
|
1067
1016
|
).trim();
|
|
@@ -1072,7 +1021,7 @@ function getLastCommitTimestamp(filePath) {
|
|
|
1072
1021
|
}
|
|
1073
1022
|
function getCompletionTimestamp(filePath) {
|
|
1074
1023
|
try {
|
|
1075
|
-
const gitLog =
|
|
1024
|
+
const gitLog = execSync(
|
|
1076
1025
|
`git log --format="%H|%aI" -p -- "${filePath}"`,
|
|
1077
1026
|
{ encoding: "utf-8", stdio: ["pipe", "pipe", "ignore"] }
|
|
1078
1027
|
);
|
|
@@ -1093,7 +1042,7 @@ function getCompletionTimestamp(filePath) {
|
|
|
1093
1042
|
}
|
|
1094
1043
|
function getFirstCommitAuthor(filePath) {
|
|
1095
1044
|
try {
|
|
1096
|
-
const author =
|
|
1045
|
+
const author = execSync(
|
|
1097
1046
|
`git log --follow --format="%an" --diff-filter=A -- "${filePath}" | tail -1`,
|
|
1098
1047
|
{ encoding: "utf-8", stdio: ["pipe", "pipe", "ignore"] }
|
|
1099
1048
|
).trim();
|
|
@@ -1105,7 +1054,7 @@ function getFirstCommitAuthor(filePath) {
|
|
|
1105
1054
|
function parseStatusTransitions(filePath) {
|
|
1106
1055
|
const transitions = [];
|
|
1107
1056
|
try {
|
|
1108
|
-
const gitLog =
|
|
1057
|
+
const gitLog = execSync(
|
|
1109
1058
|
`git log --format="%H|%aI" -p --reverse -- "${filePath}"`,
|
|
1110
1059
|
{ encoding: "utf-8", stdio: ["pipe", "pipe", "ignore"] }
|
|
1111
1060
|
);
|
|
@@ -1153,7 +1102,7 @@ function extractGitTimestamps(filePath, options = {}) {
|
|
|
1153
1102
|
}
|
|
1154
1103
|
function fileExistsInGit(filePath) {
|
|
1155
1104
|
try {
|
|
1156
|
-
|
|
1105
|
+
execSync(
|
|
1157
1106
|
`git log -n 1 -- "${filePath}"`,
|
|
1158
1107
|
{ stdio: "ignore", encoding: "utf-8" }
|
|
1159
1108
|
);
|
|
@@ -1176,7 +1125,7 @@ async function backfillTimestamps(options = {}) {
|
|
|
1176
1125
|
specs = [];
|
|
1177
1126
|
const config = await loadConfig();
|
|
1178
1127
|
const cwd = process.cwd();
|
|
1179
|
-
const specsDir =
|
|
1128
|
+
const specsDir = path2.join(cwd, config.specsDir);
|
|
1180
1129
|
for (const specPath of options.specs) {
|
|
1181
1130
|
const resolved = await resolveSpecPath(specPath, cwd, specsDir);
|
|
1182
1131
|
if (!resolved) {
|
|
@@ -1348,86 +1297,81 @@ function printSummary(results, options) {
|
|
|
1348
1297
|
console.log(" Run \x1B[36mlspec stats\x1B[0m to see velocity metrics");
|
|
1349
1298
|
}
|
|
1350
1299
|
}
|
|
1351
|
-
|
|
1352
|
-
// src/commands/templates.ts
|
|
1353
|
-
import * as fs8 from "fs/promises";
|
|
1354
|
-
import * as path11 from "path";
|
|
1355
|
-
import chalk9 from "chalk";
|
|
1356
1300
|
async function listTemplates(cwd = process.cwd()) {
|
|
1357
1301
|
const config = await loadConfig(cwd);
|
|
1358
|
-
const templatesDir =
|
|
1302
|
+
const templatesDir = path2.join(cwd, ".lean-spec", "templates");
|
|
1359
1303
|
console.log("");
|
|
1360
|
-
console.log(
|
|
1304
|
+
console.log(chalk16.green("=== Project Templates ==="));
|
|
1361
1305
|
console.log("");
|
|
1362
1306
|
try {
|
|
1363
|
-
await
|
|
1307
|
+
await fs9.access(templatesDir);
|
|
1364
1308
|
} catch {
|
|
1365
|
-
console.log(
|
|
1366
|
-
console.log(
|
|
1309
|
+
console.log(chalk16.yellow("No templates directory found."));
|
|
1310
|
+
console.log(chalk16.gray("Run: lean-spec init"));
|
|
1367
1311
|
console.log("");
|
|
1368
1312
|
return;
|
|
1369
1313
|
}
|
|
1370
|
-
const files = await
|
|
1314
|
+
const files = await fs9.readdir(templatesDir);
|
|
1371
1315
|
const templateFiles = files.filter((f) => f.endsWith(".md"));
|
|
1372
1316
|
if (templateFiles.length === 0) {
|
|
1373
|
-
console.log(
|
|
1317
|
+
console.log(chalk16.yellow("No templates found."));
|
|
1374
1318
|
console.log("");
|
|
1375
1319
|
return;
|
|
1376
1320
|
}
|
|
1377
1321
|
if (config.templates && Object.keys(config.templates).length > 0) {
|
|
1378
|
-
console.log(
|
|
1322
|
+
console.log(chalk16.cyan("Registered:"));
|
|
1379
1323
|
for (const [name, file] of Object.entries(config.templates)) {
|
|
1380
1324
|
const isDefault = config.template === file;
|
|
1381
|
-
const marker = isDefault ?
|
|
1382
|
-
console.log(` ${
|
|
1325
|
+
const marker = isDefault ? chalk16.green("\u2713 (default)") : "";
|
|
1326
|
+
console.log(` ${chalk16.bold(name)}: ${file} ${marker}`);
|
|
1383
1327
|
}
|
|
1384
1328
|
console.log("");
|
|
1385
1329
|
}
|
|
1386
|
-
console.log(
|
|
1330
|
+
console.log(chalk16.cyan("Available files:"));
|
|
1387
1331
|
for (const file of templateFiles) {
|
|
1388
|
-
const filePath =
|
|
1389
|
-
const stat5 = await
|
|
1332
|
+
const filePath = path2.join(templatesDir, file);
|
|
1333
|
+
const stat5 = await fs9.stat(filePath);
|
|
1390
1334
|
const sizeKB = (stat5.size / 1024).toFixed(1);
|
|
1391
1335
|
console.log(` ${file} (${sizeKB} KB)`);
|
|
1392
1336
|
}
|
|
1393
1337
|
console.log("");
|
|
1394
|
-
console.log(
|
|
1338
|
+
console.log(chalk16.gray("Use templates with: lean-spec create <name> --template=<template-name>"));
|
|
1395
1339
|
console.log("");
|
|
1396
1340
|
}
|
|
1397
1341
|
async function showTemplate(templateName, cwd = process.cwd()) {
|
|
1398
1342
|
const config = await loadConfig(cwd);
|
|
1399
1343
|
if (!config.templates?.[templateName]) {
|
|
1400
|
-
console.error(
|
|
1401
|
-
console.error(
|
|
1344
|
+
console.error(chalk16.red(`Template not found: ${templateName}`));
|
|
1345
|
+
console.error(chalk16.gray(`Available: ${Object.keys(config.templates || {}).join(", ")}`));
|
|
1402
1346
|
process.exit(1);
|
|
1403
1347
|
}
|
|
1404
|
-
const templatesDir =
|
|
1348
|
+
const templatesDir = path2.join(cwd, ".lean-spec", "templates");
|
|
1405
1349
|
const templateFile = config.templates[templateName];
|
|
1406
|
-
const templatePath =
|
|
1350
|
+
const templatePath = path2.join(templatesDir, templateFile);
|
|
1407
1351
|
try {
|
|
1408
|
-
const content = await
|
|
1352
|
+
const content = await fs9.readFile(templatePath, "utf-8");
|
|
1409
1353
|
console.log("");
|
|
1410
|
-
console.log(
|
|
1354
|
+
console.log(chalk16.cyan(`=== Template: ${templateName} (${templateFile}) ===`));
|
|
1411
1355
|
console.log("");
|
|
1412
1356
|
console.log(content);
|
|
1413
1357
|
console.log("");
|
|
1414
1358
|
} catch (error) {
|
|
1415
|
-
console.error(
|
|
1359
|
+
console.error(chalk16.red(`Error reading template: ${templateFile}`));
|
|
1416
1360
|
console.error(error);
|
|
1417
1361
|
process.exit(1);
|
|
1418
1362
|
}
|
|
1419
1363
|
}
|
|
1420
1364
|
async function addTemplate(name, file, cwd = process.cwd()) {
|
|
1421
1365
|
const config = await loadConfig(cwd);
|
|
1422
|
-
const templatesDir =
|
|
1423
|
-
const templatePath =
|
|
1366
|
+
const templatesDir = path2.join(cwd, ".lean-spec", "templates");
|
|
1367
|
+
const templatePath = path2.join(templatesDir, file);
|
|
1424
1368
|
try {
|
|
1425
|
-
await
|
|
1369
|
+
await fs9.access(templatePath);
|
|
1426
1370
|
} catch {
|
|
1427
|
-
console.error(
|
|
1428
|
-
console.error(
|
|
1371
|
+
console.error(chalk16.red(`Template file not found: ${file}`));
|
|
1372
|
+
console.error(chalk16.gray(`Expected at: ${templatePath}`));
|
|
1429
1373
|
console.error(
|
|
1430
|
-
|
|
1374
|
+
chalk16.yellow("Create the file first or use: lean-spec templates copy <source> <target>")
|
|
1431
1375
|
);
|
|
1432
1376
|
process.exit(1);
|
|
1433
1377
|
}
|
|
@@ -1435,73 +1379,61 @@ async function addTemplate(name, file, cwd = process.cwd()) {
|
|
|
1435
1379
|
config.templates = {};
|
|
1436
1380
|
}
|
|
1437
1381
|
if (config.templates[name]) {
|
|
1438
|
-
console.log(
|
|
1382
|
+
console.log(chalk16.yellow(`Warning: Template '${name}' already exists, updating...`));
|
|
1439
1383
|
}
|
|
1440
1384
|
config.templates[name] = file;
|
|
1441
1385
|
await saveConfig(config, cwd);
|
|
1442
|
-
console.log(
|
|
1443
|
-
console.log(
|
|
1386
|
+
console.log(chalk16.green(`\u2713 Added template: ${name} \u2192 ${file}`));
|
|
1387
|
+
console.log(chalk16.gray(` Use with: lean-spec create <spec-name> --template=${name}`));
|
|
1444
1388
|
}
|
|
1445
1389
|
async function removeTemplate(name, cwd = process.cwd()) {
|
|
1446
1390
|
const config = await loadConfig(cwd);
|
|
1447
1391
|
if (!config.templates?.[name]) {
|
|
1448
|
-
console.error(
|
|
1449
|
-
console.error(
|
|
1392
|
+
console.error(chalk16.red(`Template not found: ${name}`));
|
|
1393
|
+
console.error(chalk16.gray(`Available: ${Object.keys(config.templates || {}).join(", ")}`));
|
|
1450
1394
|
process.exit(1);
|
|
1451
1395
|
}
|
|
1452
1396
|
if (name === "default") {
|
|
1453
|
-
console.error(
|
|
1397
|
+
console.error(chalk16.red("Cannot remove default template"));
|
|
1454
1398
|
process.exit(1);
|
|
1455
1399
|
}
|
|
1456
1400
|
const file = config.templates[name];
|
|
1457
1401
|
delete config.templates[name];
|
|
1458
1402
|
await saveConfig(config, cwd);
|
|
1459
|
-
console.log(
|
|
1460
|
-
console.log(
|
|
1403
|
+
console.log(chalk16.green(`\u2713 Removed template: ${name}`));
|
|
1404
|
+
console.log(chalk16.gray(` Note: Template file ${file} still exists in .lean-spec/templates/`));
|
|
1461
1405
|
}
|
|
1462
1406
|
async function copyTemplate(source, target, cwd = process.cwd()) {
|
|
1463
1407
|
const config = await loadConfig(cwd);
|
|
1464
|
-
const templatesDir =
|
|
1408
|
+
const templatesDir = path2.join(cwd, ".lean-spec", "templates");
|
|
1465
1409
|
let sourceFile;
|
|
1466
1410
|
if (config.templates?.[source]) {
|
|
1467
1411
|
sourceFile = config.templates[source];
|
|
1468
1412
|
} else {
|
|
1469
1413
|
sourceFile = source;
|
|
1470
1414
|
}
|
|
1471
|
-
const sourcePath =
|
|
1415
|
+
const sourcePath = path2.join(templatesDir, sourceFile);
|
|
1472
1416
|
try {
|
|
1473
|
-
await
|
|
1417
|
+
await fs9.access(sourcePath);
|
|
1474
1418
|
} catch {
|
|
1475
|
-
console.error(
|
|
1476
|
-
console.error(
|
|
1419
|
+
console.error(chalk16.red(`Source template not found: ${source}`));
|
|
1420
|
+
console.error(chalk16.gray(`Expected at: ${sourcePath}`));
|
|
1477
1421
|
process.exit(1);
|
|
1478
1422
|
}
|
|
1479
1423
|
const targetFile = target.endsWith(".md") ? target : `${target}.md`;
|
|
1480
|
-
const targetPath =
|
|
1481
|
-
await
|
|
1482
|
-
console.log(
|
|
1424
|
+
const targetPath = path2.join(templatesDir, targetFile);
|
|
1425
|
+
await fs9.copyFile(sourcePath, targetPath);
|
|
1426
|
+
console.log(chalk16.green(`\u2713 Copied: ${sourceFile} \u2192 ${targetFile}`));
|
|
1483
1427
|
if (!config.templates) {
|
|
1484
1428
|
config.templates = {};
|
|
1485
1429
|
}
|
|
1486
1430
|
const templateName = target.replace(/\.md$/, "");
|
|
1487
1431
|
config.templates[templateName] = targetFile;
|
|
1488
1432
|
await saveConfig(config, cwd);
|
|
1489
|
-
console.log(
|
|
1490
|
-
console.log(
|
|
1491
|
-
console.log(
|
|
1433
|
+
console.log(chalk16.green(`\u2713 Registered template: ${templateName}`));
|
|
1434
|
+
console.log(chalk16.gray(` Edit: ${targetPath}`));
|
|
1435
|
+
console.log(chalk16.gray(` Use with: lean-spec create <spec-name> --template=${templateName}`));
|
|
1492
1436
|
}
|
|
1493
|
-
|
|
1494
|
-
// src/commands/init.ts
|
|
1495
|
-
import * as fs10 from "fs/promises";
|
|
1496
|
-
import * as path13 from "path";
|
|
1497
|
-
import { fileURLToPath } from "url";
|
|
1498
|
-
import chalk11 from "chalk";
|
|
1499
|
-
import { select } from "@inquirer/prompts";
|
|
1500
|
-
|
|
1501
|
-
// src/utils/template-helpers.ts
|
|
1502
|
-
import * as fs9 from "fs/promises";
|
|
1503
|
-
import * as path12 from "path";
|
|
1504
|
-
import chalk10 from "chalk";
|
|
1505
1437
|
async function detectExistingSystemPrompts(cwd) {
|
|
1506
1438
|
const commonFiles = [
|
|
1507
1439
|
"AGENTS.md",
|
|
@@ -1511,7 +1443,7 @@ async function detectExistingSystemPrompts(cwd) {
|
|
|
1511
1443
|
const found = [];
|
|
1512
1444
|
for (const file of commonFiles) {
|
|
1513
1445
|
try {
|
|
1514
|
-
await fs9.access(
|
|
1446
|
+
await fs9.access(path2.join(cwd, file));
|
|
1515
1447
|
found.push(file);
|
|
1516
1448
|
} catch {
|
|
1517
1449
|
}
|
|
@@ -1520,8 +1452,8 @@ async function detectExistingSystemPrompts(cwd) {
|
|
|
1520
1452
|
}
|
|
1521
1453
|
async function handleExistingFiles(action, existingFiles, templateDir, cwd, variables = {}) {
|
|
1522
1454
|
for (const file of existingFiles) {
|
|
1523
|
-
const filePath =
|
|
1524
|
-
const templateFilePath =
|
|
1455
|
+
const filePath = path2.join(cwd, file);
|
|
1456
|
+
const templateFilePath = path2.join(templateDir, "files", file);
|
|
1525
1457
|
try {
|
|
1526
1458
|
await fs9.access(templateFilePath);
|
|
1527
1459
|
} catch {
|
|
@@ -1533,7 +1465,7 @@ async function handleExistingFiles(action, existingFiles, templateDir, cwd, vari
|
|
|
1533
1465
|
for (const [key, value] of Object.entries(variables)) {
|
|
1534
1466
|
template = template.replace(new RegExp(`\\{${key}\\}`, "g"), value);
|
|
1535
1467
|
}
|
|
1536
|
-
const promptPath =
|
|
1468
|
+
const promptPath = path2.join(cwd, ".lean-spec", "MERGE-AGENTS-PROMPT.md");
|
|
1537
1469
|
const aiPrompt = `# AI Prompt: Consolidate AGENTS.md
|
|
1538
1470
|
|
|
1539
1471
|
## Task
|
|
@@ -1565,16 +1497,16 @@ Create a single consolidated AGENTS.md that:
|
|
|
1565
1497
|
- Maintains clear structure and readability
|
|
1566
1498
|
- Removes any duplicate or conflicting guidance
|
|
1567
1499
|
`;
|
|
1568
|
-
await fs9.mkdir(
|
|
1500
|
+
await fs9.mkdir(path2.dirname(promptPath), { recursive: true });
|
|
1569
1501
|
await fs9.writeFile(promptPath, aiPrompt, "utf-8");
|
|
1570
|
-
console.log(
|
|
1571
|
-
console.log(
|
|
1502
|
+
console.log(chalk16.green(`\u2713 Created AI consolidation prompt`));
|
|
1503
|
+
console.log(chalk16.cyan(` \u2192 ${promptPath}`));
|
|
1572
1504
|
console.log("");
|
|
1573
|
-
console.log(
|
|
1574
|
-
console.log(
|
|
1575
|
-
console.log(
|
|
1576
|
-
console.log(
|
|
1577
|
-
console.log(
|
|
1505
|
+
console.log(chalk16.yellow("\u{1F4DD} Next steps:"));
|
|
1506
|
+
console.log(chalk16.gray(" 1. Open .lean-spec/MERGE-AGENTS-PROMPT.md"));
|
|
1507
|
+
console.log(chalk16.gray(" 2. Send it to your AI coding assistant (GitHub Copilot, Cursor, etc.)"));
|
|
1508
|
+
console.log(chalk16.gray(" 3. Let AI create the consolidated AGENTS.md"));
|
|
1509
|
+
console.log(chalk16.gray(" 4. Review and commit the result"));
|
|
1578
1510
|
console.log("");
|
|
1579
1511
|
} else if (action === "merge-append" && file === "AGENTS.md") {
|
|
1580
1512
|
const existing = await fs9.readFile(filePath, "utf-8");
|
|
@@ -1590,19 +1522,19 @@ Create a single consolidated AGENTS.md that:
|
|
|
1590
1522
|
|
|
1591
1523
|
${template.split("\n").slice(1).join("\n")}`;
|
|
1592
1524
|
await fs9.writeFile(filePath, merged, "utf-8");
|
|
1593
|
-
console.log(
|
|
1594
|
-
console.log(
|
|
1525
|
+
console.log(chalk16.green(`\u2713 Appended LeanSpec section to ${file}`));
|
|
1526
|
+
console.log(chalk16.yellow(" \u26A0 Note: May be verbose. Consider consolidating later."));
|
|
1595
1527
|
} else if (action === "overwrite") {
|
|
1596
1528
|
const backupPath = `${filePath}.backup`;
|
|
1597
1529
|
await fs9.rename(filePath, backupPath);
|
|
1598
|
-
console.log(
|
|
1530
|
+
console.log(chalk16.yellow(`\u2713 Backed up ${file} \u2192 ${file}.backup`));
|
|
1599
1531
|
let content = await fs9.readFile(templateFilePath, "utf-8");
|
|
1600
1532
|
for (const [key, value] of Object.entries(variables)) {
|
|
1601
1533
|
content = content.replace(new RegExp(`\\{${key}\\}`, "g"), value);
|
|
1602
1534
|
}
|
|
1603
1535
|
await fs9.writeFile(filePath, content, "utf-8");
|
|
1604
|
-
console.log(
|
|
1605
|
-
console.log(
|
|
1536
|
+
console.log(chalk16.green(`\u2713 Created new ${file}`));
|
|
1537
|
+
console.log(chalk16.gray(` \u{1F4A1} Your original content is preserved in ${file}.backup`));
|
|
1606
1538
|
}
|
|
1607
1539
|
}
|
|
1608
1540
|
}
|
|
@@ -1610,8 +1542,8 @@ async function copyDirectory(src, dest, skipFiles = [], variables = {}) {
|
|
|
1610
1542
|
await fs9.mkdir(dest, { recursive: true });
|
|
1611
1543
|
const entries = await fs9.readdir(src, { withFileTypes: true });
|
|
1612
1544
|
for (const entry of entries) {
|
|
1613
|
-
const srcPath =
|
|
1614
|
-
const destPath =
|
|
1545
|
+
const srcPath = path2.join(src, entry.name);
|
|
1546
|
+
const destPath = path2.join(dest, entry.name);
|
|
1615
1547
|
if (skipFiles.includes(entry.name)) {
|
|
1616
1548
|
continue;
|
|
1617
1549
|
}
|
|
@@ -1632,7 +1564,7 @@ async function copyDirectory(src, dest, skipFiles = [], variables = {}) {
|
|
|
1632
1564
|
}
|
|
1633
1565
|
async function getProjectName2(cwd) {
|
|
1634
1566
|
try {
|
|
1635
|
-
const packageJsonPath =
|
|
1567
|
+
const packageJsonPath = path2.join(cwd, "package.json");
|
|
1636
1568
|
const content = await fs9.readFile(packageJsonPath, "utf-8");
|
|
1637
1569
|
const pkg = JSON.parse(content);
|
|
1638
1570
|
if (pkg.name) {
|
|
@@ -1640,23 +1572,23 @@ async function getProjectName2(cwd) {
|
|
|
1640
1572
|
}
|
|
1641
1573
|
} catch {
|
|
1642
1574
|
}
|
|
1643
|
-
return
|
|
1575
|
+
return path2.basename(cwd);
|
|
1644
1576
|
}
|
|
1645
1577
|
|
|
1646
1578
|
// src/commands/init.ts
|
|
1647
|
-
var
|
|
1648
|
-
var TEMPLATES_DIR =
|
|
1579
|
+
var __dirname = path2.dirname(fileURLToPath(import.meta.url));
|
|
1580
|
+
var TEMPLATES_DIR = path2.join(__dirname, "..", "templates");
|
|
1649
1581
|
async function initProject() {
|
|
1650
1582
|
const cwd = process.cwd();
|
|
1651
1583
|
try {
|
|
1652
|
-
await
|
|
1653
|
-
console.log(
|
|
1654
|
-
console.log(
|
|
1584
|
+
await fs9.access(path2.join(cwd, ".lean-spec", "config.json"));
|
|
1585
|
+
console.log(chalk16.yellow("\u26A0 LeanSpec already initialized in this directory."));
|
|
1586
|
+
console.log(chalk16.gray("To reinitialize, delete .lean-spec/ directory first."));
|
|
1655
1587
|
return;
|
|
1656
1588
|
} catch {
|
|
1657
1589
|
}
|
|
1658
1590
|
console.log("");
|
|
1659
|
-
console.log(
|
|
1591
|
+
console.log(chalk16.green("Welcome to LeanSpec!"));
|
|
1660
1592
|
console.log("");
|
|
1661
1593
|
const setupMode = await select({
|
|
1662
1594
|
message: "How would you like to set up?",
|
|
@@ -1694,14 +1626,14 @@ async function initProject() {
|
|
|
1694
1626
|
]
|
|
1695
1627
|
});
|
|
1696
1628
|
}
|
|
1697
|
-
const templateDir =
|
|
1698
|
-
const templateConfigPath =
|
|
1629
|
+
const templateDir = path2.join(TEMPLATES_DIR, templateName);
|
|
1630
|
+
const templateConfigPath = path2.join(templateDir, "config.json");
|
|
1699
1631
|
let templateConfig;
|
|
1700
1632
|
try {
|
|
1701
|
-
const content = await
|
|
1633
|
+
const content = await fs9.readFile(templateConfigPath, "utf-8");
|
|
1702
1634
|
templateConfig = JSON.parse(content).config;
|
|
1703
1635
|
} catch {
|
|
1704
|
-
console.error(
|
|
1636
|
+
console.error(chalk16.red(`Error: Template not found: ${templateName}`));
|
|
1705
1637
|
process.exit(1);
|
|
1706
1638
|
}
|
|
1707
1639
|
let patternChoice = "simple";
|
|
@@ -1744,27 +1676,27 @@ async function initProject() {
|
|
|
1744
1676
|
templateConfig.structure.prefix = "{YYYYMMDD}-";
|
|
1745
1677
|
} else if (patternChoice === "custom") {
|
|
1746
1678
|
console.log("");
|
|
1747
|
-
console.log(
|
|
1748
|
-
console.log(
|
|
1749
|
-
console.log(
|
|
1679
|
+
console.log(chalk16.yellow("\u26A0 Custom pattern input is not yet implemented."));
|
|
1680
|
+
console.log(chalk16.gray(" You can manually edit .lean-spec/config.json after initialization."));
|
|
1681
|
+
console.log(chalk16.gray(" Using simple pattern for now."));
|
|
1750
1682
|
console.log("");
|
|
1751
1683
|
templateConfig.structure.pattern = "flat";
|
|
1752
1684
|
templateConfig.structure.prefix = "";
|
|
1753
1685
|
}
|
|
1754
|
-
const templatesDir =
|
|
1686
|
+
const templatesDir = path2.join(cwd, ".lean-spec", "templates");
|
|
1755
1687
|
try {
|
|
1756
|
-
await
|
|
1688
|
+
await fs9.mkdir(templatesDir, { recursive: true });
|
|
1757
1689
|
} catch (error) {
|
|
1758
|
-
console.error(
|
|
1690
|
+
console.error(chalk16.red("Error creating templates directory:"), error);
|
|
1759
1691
|
process.exit(1);
|
|
1760
1692
|
}
|
|
1761
|
-
const templateSpecPath =
|
|
1762
|
-
const targetSpecPath =
|
|
1693
|
+
const templateSpecPath = path2.join(templateDir, "spec-template.md");
|
|
1694
|
+
const targetSpecPath = path2.join(templatesDir, "spec-template.md");
|
|
1763
1695
|
try {
|
|
1764
|
-
await
|
|
1765
|
-
console.log(
|
|
1696
|
+
await fs9.copyFile(templateSpecPath, targetSpecPath);
|
|
1697
|
+
console.log(chalk16.green("\u2713 Created .lean-spec/templates/spec-template.md"));
|
|
1766
1698
|
} catch (error) {
|
|
1767
|
-
console.error(
|
|
1699
|
+
console.error(chalk16.red("Error copying template:"), error);
|
|
1768
1700
|
process.exit(1);
|
|
1769
1701
|
}
|
|
1770
1702
|
templateConfig.template = "spec-template.md";
|
|
@@ -1772,12 +1704,12 @@ async function initProject() {
|
|
|
1772
1704
|
default: "spec-template.md"
|
|
1773
1705
|
};
|
|
1774
1706
|
await saveConfig(templateConfig, cwd);
|
|
1775
|
-
console.log(
|
|
1707
|
+
console.log(chalk16.green("\u2713 Created .lean-spec/config.json"));
|
|
1776
1708
|
const existingFiles = await detectExistingSystemPrompts(cwd);
|
|
1777
1709
|
let skipFiles = [];
|
|
1778
1710
|
if (existingFiles.length > 0) {
|
|
1779
1711
|
console.log("");
|
|
1780
|
-
console.log(
|
|
1712
|
+
console.log(chalk16.yellow(`Found existing: ${existingFiles.join(", ")}`));
|
|
1781
1713
|
const action = await select({
|
|
1782
1714
|
message: "How would you like to handle existing AGENTS.md?",
|
|
1783
1715
|
choices: [
|
|
@@ -1810,33 +1742,28 @@ async function initProject() {
|
|
|
1810
1742
|
}
|
|
1811
1743
|
}
|
|
1812
1744
|
const projectName = await getProjectName2(cwd);
|
|
1813
|
-
const filesDir =
|
|
1745
|
+
const filesDir = path2.join(templateDir, "files");
|
|
1814
1746
|
try {
|
|
1815
1747
|
await copyDirectory(filesDir, cwd, skipFiles, { project_name: projectName });
|
|
1816
|
-
console.log(
|
|
1748
|
+
console.log(chalk16.green("\u2713 Initialized project structure"));
|
|
1817
1749
|
} catch (error) {
|
|
1818
|
-
console.error(
|
|
1750
|
+
console.error(chalk16.red("Error copying template files:"), error);
|
|
1819
1751
|
process.exit(1);
|
|
1820
1752
|
}
|
|
1821
1753
|
console.log("");
|
|
1822
|
-
console.log(
|
|
1754
|
+
console.log(chalk16.green("\u2713 LeanSpec initialized!"));
|
|
1823
1755
|
console.log("");
|
|
1824
1756
|
console.log("Next steps:");
|
|
1825
|
-
console.log(
|
|
1826
|
-
console.log(
|
|
1827
|
-
console.log(
|
|
1757
|
+
console.log(chalk16.gray(" - Review and customize AGENTS.md"));
|
|
1758
|
+
console.log(chalk16.gray(" - Check out example spec in specs/"));
|
|
1759
|
+
console.log(chalk16.gray(" - Create your first spec: lean-spec create my-feature"));
|
|
1828
1760
|
console.log("");
|
|
1829
1761
|
}
|
|
1830
|
-
|
|
1831
|
-
// src/commands/files.ts
|
|
1832
|
-
import * as fs11 from "fs/promises";
|
|
1833
|
-
import * as path14 from "path";
|
|
1834
|
-
import chalk12 from "chalk";
|
|
1835
1762
|
async function filesCommand(specPath, options = {}) {
|
|
1836
1763
|
await autoCheckIfEnabled();
|
|
1837
1764
|
const config = await loadConfig();
|
|
1838
1765
|
const cwd = process.cwd();
|
|
1839
|
-
const specsDir =
|
|
1766
|
+
const specsDir = path2.join(cwd, config.specsDir);
|
|
1840
1767
|
const resolvedPath = await resolveSpecPath(specPath, cwd, specsDir);
|
|
1841
1768
|
if (!resolvedPath) {
|
|
1842
1769
|
throw new Error(`Spec not found: ${sanitizeUserInput(specPath)}. Try using the full path or spec name (e.g., 001-my-spec)`);
|
|
@@ -1847,12 +1774,12 @@ async function filesCommand(specPath, options = {}) {
|
|
|
1847
1774
|
}
|
|
1848
1775
|
const subFiles = await loadSubFiles(spec.fullPath);
|
|
1849
1776
|
console.log("");
|
|
1850
|
-
console.log(
|
|
1777
|
+
console.log(chalk16.cyan(`\u{1F4C4} Files in ${sanitizeUserInput(spec.name)}`));
|
|
1851
1778
|
console.log("");
|
|
1852
|
-
console.log(
|
|
1853
|
-
const readmeStat = await
|
|
1779
|
+
console.log(chalk16.green("Required:"));
|
|
1780
|
+
const readmeStat = await fs9.stat(spec.filePath);
|
|
1854
1781
|
const readmeSize = formatSize(readmeStat.size);
|
|
1855
|
-
console.log(
|
|
1782
|
+
console.log(chalk16.green(` \u2713 README.md (${readmeSize}) Main spec`));
|
|
1856
1783
|
console.log("");
|
|
1857
1784
|
let filteredFiles = subFiles;
|
|
1858
1785
|
if (options.type === "docs") {
|
|
@@ -1861,25 +1788,25 @@ async function filesCommand(specPath, options = {}) {
|
|
|
1861
1788
|
filteredFiles = subFiles.filter((f) => f.type === "asset");
|
|
1862
1789
|
}
|
|
1863
1790
|
if (filteredFiles.length === 0) {
|
|
1864
|
-
console.log(
|
|
1791
|
+
console.log(chalk16.gray("No additional files"));
|
|
1865
1792
|
console.log("");
|
|
1866
1793
|
return;
|
|
1867
1794
|
}
|
|
1868
1795
|
const documents = filteredFiles.filter((f) => f.type === "document");
|
|
1869
1796
|
const assets = filteredFiles.filter((f) => f.type === "asset");
|
|
1870
1797
|
if (documents.length > 0 && (!options.type || options.type === "docs")) {
|
|
1871
|
-
console.log(
|
|
1798
|
+
console.log(chalk16.cyan("Documents:"));
|
|
1872
1799
|
for (const file of documents) {
|
|
1873
1800
|
const size = formatSize(file.size);
|
|
1874
|
-
console.log(
|
|
1801
|
+
console.log(chalk16.cyan(` \u2713 ${sanitizeUserInput(file.name).padEnd(20)} (${size})`));
|
|
1875
1802
|
}
|
|
1876
1803
|
console.log("");
|
|
1877
1804
|
}
|
|
1878
1805
|
if (assets.length > 0 && (!options.type || options.type === "assets")) {
|
|
1879
|
-
console.log(
|
|
1806
|
+
console.log(chalk16.yellow("Assets:"));
|
|
1880
1807
|
for (const file of assets) {
|
|
1881
1808
|
const size = formatSize(file.size);
|
|
1882
|
-
console.log(
|
|
1809
|
+
console.log(chalk16.yellow(` \u2713 ${sanitizeUserInput(file.name).padEnd(20)} (${size})`));
|
|
1883
1810
|
}
|
|
1884
1811
|
console.log("");
|
|
1885
1812
|
}
|
|
@@ -1887,7 +1814,7 @@ async function filesCommand(specPath, options = {}) {
|
|
|
1887
1814
|
const totalSize = formatSize(
|
|
1888
1815
|
readmeStat.size + filteredFiles.reduce((sum, f) => sum + f.size, 0)
|
|
1889
1816
|
);
|
|
1890
|
-
console.log(
|
|
1817
|
+
console.log(chalk16.gray(`Total: ${totalFiles} files, ${totalSize}`));
|
|
1891
1818
|
console.log("");
|
|
1892
1819
|
}
|
|
1893
1820
|
function formatSize(bytes) {
|
|
@@ -1899,55 +1826,6 @@ function formatSize(bytes) {
|
|
|
1899
1826
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
1900
1827
|
}
|
|
1901
1828
|
}
|
|
1902
|
-
|
|
1903
|
-
// src/commands/validate.ts
|
|
1904
|
-
import * as fs12 from "fs/promises";
|
|
1905
|
-
import * as path16 from "path";
|
|
1906
|
-
import chalk14 from "chalk";
|
|
1907
|
-
|
|
1908
|
-
// src/validators/line-count.ts
|
|
1909
|
-
var LineCountValidator = class {
|
|
1910
|
-
name = "max-lines";
|
|
1911
|
-
description = "Enforce Context Economy: specs must be <400 lines";
|
|
1912
|
-
maxLines;
|
|
1913
|
-
warningThreshold;
|
|
1914
|
-
constructor(options = {}) {
|
|
1915
|
-
this.maxLines = options.maxLines ?? 400;
|
|
1916
|
-
this.warningThreshold = options.warningThreshold ?? 300;
|
|
1917
|
-
}
|
|
1918
|
-
validate(_spec, content) {
|
|
1919
|
-
const lines = content.split("\n").length;
|
|
1920
|
-
if (lines > this.maxLines) {
|
|
1921
|
-
return {
|
|
1922
|
-
passed: false,
|
|
1923
|
-
errors: [{
|
|
1924
|
-
message: `Spec exceeds ${this.maxLines} lines (${lines} lines)`,
|
|
1925
|
-
suggestion: "Consider splitting into sub-specs using spec 012 pattern"
|
|
1926
|
-
}],
|
|
1927
|
-
warnings: []
|
|
1928
|
-
};
|
|
1929
|
-
}
|
|
1930
|
-
if (lines > this.warningThreshold) {
|
|
1931
|
-
return {
|
|
1932
|
-
passed: true,
|
|
1933
|
-
errors: [],
|
|
1934
|
-
warnings: [{
|
|
1935
|
-
message: `Spec approaching limit (${lines}/${this.maxLines} lines)`,
|
|
1936
|
-
suggestion: "Consider simplification or splitting"
|
|
1937
|
-
}]
|
|
1938
|
-
};
|
|
1939
|
-
}
|
|
1940
|
-
return {
|
|
1941
|
-
passed: true,
|
|
1942
|
-
errors: [],
|
|
1943
|
-
warnings: []
|
|
1944
|
-
};
|
|
1945
|
-
}
|
|
1946
|
-
};
|
|
1947
|
-
|
|
1948
|
-
// src/validators/frontmatter.ts
|
|
1949
|
-
import matter2 from "gray-matter";
|
|
1950
|
-
import yaml2 from "js-yaml";
|
|
1951
1829
|
var FrontmatterValidator = class {
|
|
1952
1830
|
name = "frontmatter";
|
|
1953
1831
|
description = "Validate spec frontmatter for required fields and valid values";
|
|
@@ -1962,9 +1840,9 @@ var FrontmatterValidator = class {
|
|
|
1962
1840
|
const warnings = [];
|
|
1963
1841
|
let parsed;
|
|
1964
1842
|
try {
|
|
1965
|
-
parsed =
|
|
1843
|
+
parsed = matter(content, {
|
|
1966
1844
|
engines: {
|
|
1967
|
-
yaml: (str) =>
|
|
1845
|
+
yaml: (str) => yaml.load(str, { schema: yaml.FAILSAFE_SCHEMA })
|
|
1968
1846
|
}
|
|
1969
1847
|
});
|
|
1970
1848
|
} catch (error) {
|
|
@@ -2078,9 +1956,6 @@ var FrontmatterValidator = class {
|
|
|
2078
1956
|
return { valid: true };
|
|
2079
1957
|
}
|
|
2080
1958
|
};
|
|
2081
|
-
|
|
2082
|
-
// src/validators/structure.ts
|
|
2083
|
-
import matter3 from "gray-matter";
|
|
2084
1959
|
var StructureValidator = class {
|
|
2085
1960
|
name = "structure";
|
|
2086
1961
|
description = "Validate spec structure and required sections";
|
|
@@ -2095,7 +1970,7 @@ var StructureValidator = class {
|
|
|
2095
1970
|
const warnings = [];
|
|
2096
1971
|
let parsed;
|
|
2097
1972
|
try {
|
|
2098
|
-
parsed =
|
|
1973
|
+
parsed = matter(content);
|
|
2099
1974
|
} catch (error) {
|
|
2100
1975
|
errors.push({
|
|
2101
1976
|
message: "Failed to parse frontmatter",
|
|
@@ -2424,18 +2299,19 @@ var CorruptionValidator = class {
|
|
|
2424
2299
|
return errors;
|
|
2425
2300
|
}
|
|
2426
2301
|
};
|
|
2427
|
-
|
|
2428
|
-
// src/validators/sub-spec.ts
|
|
2429
|
-
import * as path15 from "path";
|
|
2430
2302
|
var SubSpecValidator = class {
|
|
2431
2303
|
name = "sub-specs";
|
|
2432
|
-
description = "Validate sub-spec files
|
|
2433
|
-
|
|
2304
|
+
description = "Validate sub-spec files using direct token thresholds (spec 071)";
|
|
2305
|
+
excellentThreshold;
|
|
2306
|
+
goodThreshold;
|
|
2434
2307
|
warningThreshold;
|
|
2308
|
+
maxLines;
|
|
2435
2309
|
checkCrossReferences;
|
|
2436
2310
|
constructor(options = {}) {
|
|
2437
|
-
this.
|
|
2438
|
-
this.
|
|
2311
|
+
this.excellentThreshold = options.excellentThreshold ?? 2e3;
|
|
2312
|
+
this.goodThreshold = options.goodThreshold ?? 3500;
|
|
2313
|
+
this.warningThreshold = options.warningThreshold ?? 5e3;
|
|
2314
|
+
this.maxLines = options.maxLines ?? 500;
|
|
2439
2315
|
this.checkCrossReferences = options.checkCrossReferences ?? true;
|
|
2440
2316
|
}
|
|
2441
2317
|
async validate(spec, content) {
|
|
@@ -2447,7 +2323,7 @@ var SubSpecValidator = class {
|
|
|
2447
2323
|
return { passed: true, errors, warnings };
|
|
2448
2324
|
}
|
|
2449
2325
|
this.validateNamingConventions(subSpecs, warnings);
|
|
2450
|
-
await this.
|
|
2326
|
+
await this.validateComplexity(subSpecs, errors, warnings);
|
|
2451
2327
|
this.checkOrphanedSubSpecs(subSpecs, content, warnings);
|
|
2452
2328
|
if (this.checkCrossReferences) {
|
|
2453
2329
|
await this.validateCrossReferences(subSpecs, spec, warnings);
|
|
@@ -2464,7 +2340,7 @@ var SubSpecValidator = class {
|
|
|
2464
2340
|
*/
|
|
2465
2341
|
validateNamingConventions(subSpecs, warnings) {
|
|
2466
2342
|
for (const subSpec of subSpecs) {
|
|
2467
|
-
const baseName =
|
|
2343
|
+
const baseName = path2.basename(subSpec.name, ".md");
|
|
2468
2344
|
if (baseName !== baseName.toUpperCase()) {
|
|
2469
2345
|
warnings.push({
|
|
2470
2346
|
message: `Sub-spec filename should be uppercase: ${subSpec.name}`,
|
|
@@ -2474,23 +2350,50 @@ var SubSpecValidator = class {
|
|
|
2474
2350
|
}
|
|
2475
2351
|
}
|
|
2476
2352
|
/**
|
|
2477
|
-
* Validate
|
|
2353
|
+
* Validate complexity for each sub-spec file using direct token thresholds
|
|
2354
|
+
* Same approach as ComplexityValidator (spec 071)
|
|
2478
2355
|
*/
|
|
2479
|
-
async
|
|
2356
|
+
async validateComplexity(subSpecs, errors, warnings) {
|
|
2480
2357
|
for (const subSpec of subSpecs) {
|
|
2481
2358
|
if (!subSpec.content) {
|
|
2482
2359
|
continue;
|
|
2483
2360
|
}
|
|
2484
|
-
const lines = subSpec.content.split("\n")
|
|
2485
|
-
|
|
2361
|
+
const lines = subSpec.content.split("\n");
|
|
2362
|
+
const lineCount = lines.length;
|
|
2363
|
+
let sectionCount = 0;
|
|
2364
|
+
let inCodeBlock = false;
|
|
2365
|
+
for (const line of lines) {
|
|
2366
|
+
if (line.trim().startsWith("```")) {
|
|
2367
|
+
inCodeBlock = !inCodeBlock;
|
|
2368
|
+
continue;
|
|
2369
|
+
}
|
|
2370
|
+
if (!inCodeBlock && line.match(/^#{2,4}\s/)) {
|
|
2371
|
+
sectionCount++;
|
|
2372
|
+
}
|
|
2373
|
+
}
|
|
2374
|
+
const tokenResult = await countTokens(subSpec.content);
|
|
2375
|
+
const tokenCount = tokenResult.total;
|
|
2376
|
+
if (tokenCount > this.warningThreshold) {
|
|
2486
2377
|
errors.push({
|
|
2487
|
-
message: `Sub-spec ${subSpec.name}
|
|
2488
|
-
suggestion: "Consider
|
|
2378
|
+
message: `Sub-spec ${subSpec.name} has ${tokenCount.toLocaleString()} tokens (threshold: ${this.warningThreshold.toLocaleString()}) - should split`,
|
|
2379
|
+
suggestion: "Consider splitting for Context Economy (attention and cognitive load)"
|
|
2380
|
+
});
|
|
2381
|
+
} else if (tokenCount > this.goodThreshold) {
|
|
2382
|
+
warnings.push({
|
|
2383
|
+
message: `Sub-spec ${subSpec.name} has ${tokenCount.toLocaleString()} tokens (threshold: ${this.goodThreshold.toLocaleString()})`,
|
|
2384
|
+
suggestion: "Consider simplification or further splitting"
|
|
2385
|
+
});
|
|
2386
|
+
}
|
|
2387
|
+
if (sectionCount < 8 && lineCount > 200) {
|
|
2388
|
+
warnings.push({
|
|
2389
|
+
message: `Sub-spec ${subSpec.name} has only ${sectionCount} sections - too monolithic`,
|
|
2390
|
+
suggestion: "Break into 15-35 sections for better readability (7\xB12 cognitive chunks)"
|
|
2489
2391
|
});
|
|
2490
|
-
}
|
|
2392
|
+
}
|
|
2393
|
+
if (lineCount > this.maxLines) {
|
|
2491
2394
|
warnings.push({
|
|
2492
|
-
message: `Sub-spec ${subSpec.name}
|
|
2493
|
-
suggestion: "Consider
|
|
2395
|
+
message: `Sub-spec ${subSpec.name} is very long (${lineCount} lines)`,
|
|
2396
|
+
suggestion: "Consider splitting even if token count is acceptable"
|
|
2494
2397
|
});
|
|
2495
2398
|
}
|
|
2496
2399
|
}
|
|
@@ -2536,9 +2439,6 @@ var SubSpecValidator = class {
|
|
|
2536
2439
|
}
|
|
2537
2440
|
}
|
|
2538
2441
|
};
|
|
2539
|
-
|
|
2540
|
-
// src/utils/validate-formatter.ts
|
|
2541
|
-
import chalk13 from "chalk";
|
|
2542
2442
|
function groupIssuesByFile(results) {
|
|
2543
2443
|
const fileMap = /* @__PURE__ */ new Map();
|
|
2544
2444
|
const addIssue = (filePath, issue, spec) => {
|
|
@@ -2605,17 +2505,17 @@ function formatFileIssues(fileResult, specsDir) {
|
|
|
2605
2505
|
const priority = fileResult.spec.frontmatter.priority || "medium";
|
|
2606
2506
|
const statusBadge = formatStatusBadge(status);
|
|
2607
2507
|
const priorityBadge = formatPriorityBadge(priority);
|
|
2608
|
-
lines.push(
|
|
2508
|
+
lines.push(chalk16.bold.cyan(`${specName} ${statusBadge} ${priorityBadge}`));
|
|
2609
2509
|
} else {
|
|
2610
|
-
lines.push(
|
|
2510
|
+
lines.push(chalk16.cyan.underline(relativePath));
|
|
2611
2511
|
}
|
|
2612
2512
|
for (const issue of fileResult.issues) {
|
|
2613
|
-
const severityColor = issue.severity === "error" ?
|
|
2513
|
+
const severityColor = issue.severity === "error" ? chalk16.red : chalk16.yellow;
|
|
2614
2514
|
const severityText = severityColor(issue.severity.padEnd(9));
|
|
2615
|
-
const ruleText =
|
|
2515
|
+
const ruleText = chalk16.gray(issue.ruleName);
|
|
2616
2516
|
lines.push(` ${severityText}${issue.message.padEnd(60)} ${ruleText}`);
|
|
2617
2517
|
if (issue.suggestion) {
|
|
2618
|
-
lines.push(
|
|
2518
|
+
lines.push(chalk16.gray(` \u2192 ${issue.suggestion}`));
|
|
2619
2519
|
}
|
|
2620
2520
|
}
|
|
2621
2521
|
lines.push("");
|
|
@@ -2625,25 +2525,25 @@ function formatSummary(totalSpecs, errorCount, warningCount, cleanCount) {
|
|
|
2625
2525
|
if (errorCount > 0) {
|
|
2626
2526
|
const errorText = errorCount === 1 ? "error" : "errors";
|
|
2627
2527
|
const warningText = warningCount === 1 ? "warning" : "warnings";
|
|
2628
|
-
return
|
|
2528
|
+
return chalk16.red.bold(
|
|
2629
2529
|
`\u2716 ${errorCount} ${errorText}, ${warningCount} ${warningText} (${totalSpecs} specs checked, ${cleanCount} clean)`
|
|
2630
2530
|
);
|
|
2631
2531
|
} else if (warningCount > 0) {
|
|
2632
2532
|
const warningText = warningCount === 1 ? "warning" : "warnings";
|
|
2633
|
-
return
|
|
2533
|
+
return chalk16.yellow.bold(
|
|
2634
2534
|
`\u26A0 ${warningCount} ${warningText} (${totalSpecs} specs checked, ${cleanCount} clean)`
|
|
2635
2535
|
);
|
|
2636
2536
|
} else {
|
|
2637
|
-
return
|
|
2537
|
+
return chalk16.green.bold(`\u2713 All ${totalSpecs} specs passed`);
|
|
2638
2538
|
}
|
|
2639
2539
|
}
|
|
2640
2540
|
function formatPassingSpecs(specs, specsDir) {
|
|
2641
2541
|
const lines = [];
|
|
2642
|
-
lines.push(
|
|
2542
|
+
lines.push(chalk16.green.bold(`
|
|
2643
2543
|
\u2713 ${specs.length} specs passed:`));
|
|
2644
2544
|
for (const spec of specs) {
|
|
2645
2545
|
const relativePath = normalizeFilePath(spec.filePath);
|
|
2646
|
-
lines.push(
|
|
2546
|
+
lines.push(chalk16.gray(` ${relativePath}`));
|
|
2647
2547
|
}
|
|
2648
2548
|
return lines.join("\n");
|
|
2649
2549
|
}
|
|
@@ -2689,16 +2589,16 @@ function formatValidationResults(results, specs, specsDir, options = {}) {
|
|
|
2689
2589
|
return formatJson(displayResults, specs.length, errorCount2, warningCount2);
|
|
2690
2590
|
}
|
|
2691
2591
|
const lines = [];
|
|
2692
|
-
lines.push(
|
|
2592
|
+
lines.push(chalk16.bold(`
|
|
2693
2593
|
Validating ${specs.length} specs...
|
|
2694
2594
|
`));
|
|
2695
2595
|
let previousSpecName;
|
|
2696
2596
|
for (const fileResult of displayResults) {
|
|
2697
2597
|
if (fileResult.spec && previousSpecName && fileResult.spec.name !== previousSpecName) {
|
|
2698
|
-
lines.push(
|
|
2598
|
+
lines.push(chalk16.gray("\u2500".repeat(80)));
|
|
2699
2599
|
lines.push("");
|
|
2700
2600
|
}
|
|
2701
|
-
lines.push(formatFileIssues(fileResult
|
|
2601
|
+
lines.push(formatFileIssues(fileResult));
|
|
2702
2602
|
if (fileResult.spec) {
|
|
2703
2603
|
previousSpecName = fileResult.spec.name;
|
|
2704
2604
|
}
|
|
@@ -2716,10 +2616,10 @@ Validating ${specs.length} specs...
|
|
|
2716
2616
|
if (options.verbose && cleanCount > 0) {
|
|
2717
2617
|
const specsWithIssues = new Set(fileResults.map((fr) => fr.filePath));
|
|
2718
2618
|
const passingSpecs = specs.filter((spec) => !specsWithIssues.has(spec.filePath));
|
|
2719
|
-
lines.push(formatPassingSpecs(passingSpecs
|
|
2619
|
+
lines.push(formatPassingSpecs(passingSpecs));
|
|
2720
2620
|
}
|
|
2721
2621
|
if (!options.verbose && cleanCount > 0 && displayResults.length > 0) {
|
|
2722
|
-
lines.push(
|
|
2622
|
+
lines.push(chalk16.gray("\nRun with --verbose to see passing specs."));
|
|
2723
2623
|
}
|
|
2724
2624
|
return lines.join("\n");
|
|
2725
2625
|
}
|
|
@@ -2733,12 +2633,12 @@ async function validateCommand(options = {}) {
|
|
|
2733
2633
|
specs = [];
|
|
2734
2634
|
for (const specPath of options.specs) {
|
|
2735
2635
|
const spec = allSpecs.find(
|
|
2736
|
-
(s) => s.path.includes(specPath) ||
|
|
2636
|
+
(s) => s.path.includes(specPath) || path2.basename(s.path).includes(specPath)
|
|
2737
2637
|
);
|
|
2738
2638
|
if (spec) {
|
|
2739
2639
|
specs.push(spec);
|
|
2740
2640
|
} else {
|
|
2741
|
-
console.error(
|
|
2641
|
+
console.error(chalk16.red(`Error: Spec not found: ${specPath}`));
|
|
2742
2642
|
return false;
|
|
2743
2643
|
}
|
|
2744
2644
|
}
|
|
@@ -2753,7 +2653,8 @@ async function validateCommand(options = {}) {
|
|
|
2753
2653
|
return true;
|
|
2754
2654
|
}
|
|
2755
2655
|
const validators = [
|
|
2756
|
-
new
|
|
2656
|
+
new ComplexityValidator({ maxLines: options.maxLines }),
|
|
2657
|
+
// Token-based complexity (primary), line count (backstop)
|
|
2757
2658
|
new FrontmatterValidator(),
|
|
2758
2659
|
new StructureValidator(),
|
|
2759
2660
|
new CorruptionValidator(),
|
|
@@ -2763,19 +2664,23 @@ async function validateCommand(options = {}) {
|
|
|
2763
2664
|
for (const spec of specs) {
|
|
2764
2665
|
let content;
|
|
2765
2666
|
try {
|
|
2766
|
-
content = await
|
|
2667
|
+
content = await fs9.readFile(spec.filePath, "utf-8");
|
|
2767
2668
|
} catch (error) {
|
|
2768
|
-
console.error(
|
|
2669
|
+
console.error(chalk16.red(`Error reading ${spec.filePath}:`), error);
|
|
2769
2670
|
continue;
|
|
2770
2671
|
}
|
|
2771
2672
|
for (const validator of validators) {
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2673
|
+
try {
|
|
2674
|
+
const result = await validator.validate(spec, content);
|
|
2675
|
+
results.push({
|
|
2676
|
+
spec,
|
|
2677
|
+
validatorName: validator.name,
|
|
2678
|
+
result,
|
|
2679
|
+
content
|
|
2680
|
+
});
|
|
2681
|
+
} catch (error) {
|
|
2682
|
+
console.error(chalk16.yellow(`Warning: Validator ${validator.name} failed:`), error instanceof Error ? error.message : error);
|
|
2683
|
+
}
|
|
2779
2684
|
}
|
|
2780
2685
|
}
|
|
2781
2686
|
const formatOptions = {
|
|
@@ -2789,14 +2694,10 @@ async function validateCommand(options = {}) {
|
|
|
2789
2694
|
const hasErrors = results.some((r) => !r.result.passed);
|
|
2790
2695
|
return !hasErrors;
|
|
2791
2696
|
}
|
|
2792
|
-
|
|
2793
|
-
// src/commands/migrate.ts
|
|
2794
|
-
import * as fs13 from "fs/promises";
|
|
2795
|
-
import * as path17 from "path";
|
|
2796
2697
|
async function migrateCommand(inputPath, options = {}) {
|
|
2797
2698
|
const config = await loadConfig();
|
|
2798
2699
|
try {
|
|
2799
|
-
const stats = await
|
|
2700
|
+
const stats = await fs9.stat(inputPath);
|
|
2800
2701
|
if (!stats.isDirectory()) {
|
|
2801
2702
|
console.error("\x1B[31m\u274C Error:\x1B[0m Input path must be a directory");
|
|
2802
2703
|
process.exit(1);
|
|
@@ -2824,16 +2725,16 @@ async function migrateCommand(inputPath, options = {}) {
|
|
|
2824
2725
|
async function scanDocuments(dirPath) {
|
|
2825
2726
|
const documents = [];
|
|
2826
2727
|
async function scanRecursive(currentPath) {
|
|
2827
|
-
const entries = await
|
|
2728
|
+
const entries = await fs9.readdir(currentPath, { withFileTypes: true });
|
|
2828
2729
|
for (const entry of entries) {
|
|
2829
|
-
const fullPath =
|
|
2730
|
+
const fullPath = path2.join(currentPath, entry.name);
|
|
2830
2731
|
if (entry.isDirectory()) {
|
|
2831
2732
|
if (!entry.name.startsWith(".") && entry.name !== "node_modules") {
|
|
2832
2733
|
await scanRecursive(fullPath);
|
|
2833
2734
|
}
|
|
2834
2735
|
} else if (entry.isFile()) {
|
|
2835
2736
|
if (entry.name.endsWith(".md") || entry.name.endsWith(".markdown")) {
|
|
2836
|
-
const stats = await
|
|
2737
|
+
const stats = await fs9.stat(fullPath);
|
|
2837
2738
|
documents.push({
|
|
2838
2739
|
path: fullPath,
|
|
2839
2740
|
name: entry.name,
|
|
@@ -2847,7 +2748,7 @@ async function scanDocuments(dirPath) {
|
|
|
2847
2748
|
return documents;
|
|
2848
2749
|
}
|
|
2849
2750
|
async function outputManualInstructions(inputPath, documents, config) {
|
|
2850
|
-
|
|
2751
|
+
config.specsDir || "specs";
|
|
2851
2752
|
console.log("\u2550".repeat(70));
|
|
2852
2753
|
console.log("\x1B[1m\x1B[36m\u{1F4CB} LeanSpec Migration Instructions\x1B[0m");
|
|
2853
2754
|
console.log("\u2550".repeat(70));
|
|
@@ -2966,7 +2867,7 @@ async function verifyAITool(provider) {
|
|
|
2966
2867
|
let installed = false;
|
|
2967
2868
|
let version;
|
|
2968
2869
|
try {
|
|
2969
|
-
const { execSync: execSync3 } = await import(
|
|
2870
|
+
const { execSync: execSync3 } = await import('child_process');
|
|
2970
2871
|
execSync3(`which ${toolDef.cliCommand}`, { stdio: "ignore" });
|
|
2971
2872
|
installed = true;
|
|
2972
2873
|
try {
|
|
@@ -3003,12 +2904,6 @@ function satisfiesVersion(version, minVersion) {
|
|
|
3003
2904
|
}
|
|
3004
2905
|
return true;
|
|
3005
2906
|
}
|
|
3006
|
-
|
|
3007
|
-
// src/commands/board.ts
|
|
3008
|
-
import chalk15 from "chalk";
|
|
3009
|
-
|
|
3010
|
-
// src/utils/completion.ts
|
|
3011
|
-
import dayjs from "dayjs";
|
|
3012
2907
|
function isCriticalOverdue(spec) {
|
|
3013
2908
|
if (spec.frontmatter.status === "complete" || spec.frontmatter.status === "archived") {
|
|
3014
2909
|
return false;
|
|
@@ -3016,7 +2911,7 @@ function isCriticalOverdue(spec) {
|
|
|
3016
2911
|
if (!spec.frontmatter.due) {
|
|
3017
2912
|
return false;
|
|
3018
2913
|
}
|
|
3019
|
-
const isOverdue =
|
|
2914
|
+
const isOverdue = dayjs2(spec.frontmatter.due).isBefore(dayjs2(), "day");
|
|
3020
2915
|
const isCritical = spec.frontmatter.priority === "critical" || spec.frontmatter.priority === "high";
|
|
3021
2916
|
return isOverdue && isCritical;
|
|
3022
2917
|
}
|
|
@@ -3028,7 +2923,7 @@ function isLongRunning(spec) {
|
|
|
3028
2923
|
if (!updatedAt) {
|
|
3029
2924
|
return false;
|
|
3030
2925
|
}
|
|
3031
|
-
const daysSinceUpdate =
|
|
2926
|
+
const daysSinceUpdate = dayjs2().diff(dayjs2(updatedAt), "day");
|
|
3032
2927
|
return daysSinceUpdate > 7;
|
|
3033
2928
|
}
|
|
3034
2929
|
function calculateCompletion(specs) {
|
|
@@ -3071,9 +2966,6 @@ function getCompletionStatus(score) {
|
|
|
3071
2966
|
return { emoji: "\u2717", label: "Needs Attention", color: "red" };
|
|
3072
2967
|
}
|
|
3073
2968
|
}
|
|
3074
|
-
|
|
3075
|
-
// src/utils/velocity.ts
|
|
3076
|
-
import dayjs2 from "dayjs";
|
|
3077
2969
|
function calculateCycleTime(spec) {
|
|
3078
2970
|
if (spec.frontmatter.status !== "complete" && spec.frontmatter.status !== "archived") {
|
|
3079
2971
|
return null;
|
|
@@ -3196,7 +3088,7 @@ async function boardCommand(options) {
|
|
|
3196
3088
|
})
|
|
3197
3089
|
);
|
|
3198
3090
|
if (specs.length === 0) {
|
|
3199
|
-
console.log(
|
|
3091
|
+
console.log(chalk16.dim("No specs found."));
|
|
3200
3092
|
return;
|
|
3201
3093
|
}
|
|
3202
3094
|
const columns = {
|
|
@@ -3211,18 +3103,18 @@ async function boardCommand(options) {
|
|
|
3211
3103
|
columns[status].push(spec);
|
|
3212
3104
|
}
|
|
3213
3105
|
}
|
|
3214
|
-
console.log(
|
|
3106
|
+
console.log(chalk16.bold.cyan("\u{1F4CB} Spec Kanban Board"));
|
|
3215
3107
|
if (options.tag || options.assignee) {
|
|
3216
3108
|
const filterParts = [];
|
|
3217
3109
|
if (options.tag) filterParts.push(`tag=${options.tag}`);
|
|
3218
3110
|
if (options.assignee) filterParts.push(`assignee=${options.assignee}`);
|
|
3219
|
-
console.log(
|
|
3111
|
+
console.log(chalk16.dim(`Filtered by: ${filterParts.join(", ")}`));
|
|
3220
3112
|
}
|
|
3221
3113
|
console.log("");
|
|
3222
3114
|
if (!options.simple) {
|
|
3223
3115
|
const completionMetrics = calculateCompletion(specs);
|
|
3224
3116
|
const velocityMetrics = calculateVelocityMetrics(specs);
|
|
3225
|
-
|
|
3117
|
+
getCompletionStatus(completionMetrics.score);
|
|
3226
3118
|
const boxWidth = 62;
|
|
3227
3119
|
const topBorder = "\u2554" + "\u2550".repeat(boxWidth - 2) + "\u2557";
|
|
3228
3120
|
const bottomBorder = "\u255A" + "\u2550".repeat(boxWidth - 2) + "\u255D";
|
|
@@ -3231,12 +3123,12 @@ async function boardCommand(options) {
|
|
|
3231
3123
|
const padding = boxWidth - 2 - visibleLength;
|
|
3232
3124
|
return content + " ".repeat(Math.max(0, padding));
|
|
3233
3125
|
};
|
|
3234
|
-
console.log(
|
|
3235
|
-
const headerLine =
|
|
3236
|
-
console.log(
|
|
3237
|
-
const percentageColor = completionMetrics.score >= 70 ?
|
|
3126
|
+
console.log(chalk16.dim(topBorder));
|
|
3127
|
+
const headerLine = chalk16.bold(" Project Overview");
|
|
3128
|
+
console.log(chalk16.dim("\u2551") + padLine(headerLine) + chalk16.dim("\u2551"));
|
|
3129
|
+
const percentageColor = completionMetrics.score >= 70 ? chalk16.green : completionMetrics.score >= 40 ? chalk16.yellow : chalk16.red;
|
|
3238
3130
|
const line1 = ` ${completionMetrics.totalSpecs} total \xB7 ${completionMetrics.activeSpecs} active \xB7 ${completionMetrics.completeSpecs} complete ${percentageColor("(" + completionMetrics.score + "%)")}`;
|
|
3239
|
-
console.log(
|
|
3131
|
+
console.log(chalk16.dim("\u2551") + padLine(line1) + chalk16.dim("\u2551"));
|
|
3240
3132
|
if (completionMetrics.criticalIssues.length > 0 || completionMetrics.warnings.length > 0) {
|
|
3241
3133
|
const alerts = [];
|
|
3242
3134
|
if (completionMetrics.criticalIssues.length > 0) {
|
|
@@ -3245,27 +3137,27 @@ async function boardCommand(options) {
|
|
|
3245
3137
|
if (completionMetrics.warnings.length > 0) {
|
|
3246
3138
|
alerts.push(`${completionMetrics.warnings.length} specs WIP > 7 days`);
|
|
3247
3139
|
}
|
|
3248
|
-
const alertLine = ` ${
|
|
3249
|
-
console.log(
|
|
3140
|
+
const alertLine = ` ${chalk16.yellow("\u26A0\uFE0F " + alerts.join(" \xB7 "))}`;
|
|
3141
|
+
console.log(chalk16.dim("\u2551") + padLine(alertLine) + chalk16.dim("\u2551"));
|
|
3250
3142
|
}
|
|
3251
|
-
const velocityLine = ` ${
|
|
3252
|
-
console.log(
|
|
3253
|
-
console.log(
|
|
3143
|
+
const velocityLine = ` ${chalk16.cyan("\u{1F680} Velocity:")} ${velocityMetrics.cycleTime.average.toFixed(1)}d avg cycle \xB7 ${(velocityMetrics.throughput.perWeek / 7 * 7).toFixed(1)}/wk throughput`;
|
|
3144
|
+
console.log(chalk16.dim("\u2551") + padLine(velocityLine) + chalk16.dim("\u2551"));
|
|
3145
|
+
console.log(chalk16.dim(bottomBorder));
|
|
3254
3146
|
console.log("");
|
|
3255
3147
|
if (options.completionOnly) {
|
|
3256
3148
|
return;
|
|
3257
3149
|
}
|
|
3258
3150
|
}
|
|
3259
3151
|
renderColumn(STATUS_CONFIG.planned.label, STATUS_CONFIG.planned.emoji, columns.planned, true, STATUS_CONFIG.planned.colorFn);
|
|
3260
|
-
console.log(
|
|
3152
|
+
console.log(chalk16.dim("\u2501".repeat(70)));
|
|
3261
3153
|
console.log("");
|
|
3262
3154
|
renderColumn(STATUS_CONFIG["in-progress"].label, STATUS_CONFIG["in-progress"].emoji, columns["in-progress"], true, STATUS_CONFIG["in-progress"].colorFn);
|
|
3263
|
-
console.log(
|
|
3155
|
+
console.log(chalk16.dim("\u2501".repeat(70)));
|
|
3264
3156
|
console.log("");
|
|
3265
3157
|
renderColumn(STATUS_CONFIG.complete.label, STATUS_CONFIG.complete.emoji, columns.complete, options.showComplete || false, STATUS_CONFIG.complete.colorFn);
|
|
3266
3158
|
}
|
|
3267
3159
|
function renderColumn(title, emoji, specs, expanded, colorFn) {
|
|
3268
|
-
console.log(`${emoji} ${colorFn(
|
|
3160
|
+
console.log(`${emoji} ${colorFn(chalk16.bold(`${title} (${specs.length})`))}`);
|
|
3269
3161
|
console.log("");
|
|
3270
3162
|
if (expanded && specs.length > 0) {
|
|
3271
3163
|
const priorityGroups = {
|
|
@@ -3290,31 +3182,30 @@ function renderColumn(title, emoji, specs, expanded, colorFn) {
|
|
|
3290
3182
|
firstGroup = false;
|
|
3291
3183
|
const priorityLabel = priority === "none" ? "No Priority" : priority.charAt(0).toUpperCase() + priority.slice(1);
|
|
3292
3184
|
const priorityEmoji = priority === "none" ? "\u26AA" : PRIORITY_CONFIG[priority].emoji;
|
|
3293
|
-
const priorityColor = priority === "none" ?
|
|
3294
|
-
console.log(` ${priorityColor(`${priorityEmoji} ${
|
|
3295
|
-
;
|
|
3185
|
+
const priorityColor = priority === "none" ? chalk16.dim : PRIORITY_CONFIG[priority].colorFn;
|
|
3186
|
+
console.log(` ${priorityColor(`${priorityEmoji} ${chalk16.bold(priorityLabel)} ${chalk16.dim(`(${groupSpecs.length})`)}`)}`);
|
|
3296
3187
|
for (const spec of groupSpecs) {
|
|
3297
3188
|
let assigneeStr = "";
|
|
3298
3189
|
if (spec.frontmatter.assignee) {
|
|
3299
|
-
assigneeStr = " " +
|
|
3190
|
+
assigneeStr = " " + chalk16.cyan(`@${sanitizeUserInput(spec.frontmatter.assignee)}`);
|
|
3300
3191
|
}
|
|
3301
3192
|
let tagsStr = "";
|
|
3302
3193
|
if (spec.frontmatter.tags?.length) {
|
|
3303
3194
|
const tags = Array.isArray(spec.frontmatter.tags) ? spec.frontmatter.tags : [];
|
|
3304
3195
|
if (tags.length > 0) {
|
|
3305
3196
|
const tagStr = tags.map((tag) => `#${sanitizeUserInput(tag)}`).join(" ");
|
|
3306
|
-
tagsStr = " " +
|
|
3197
|
+
tagsStr = " " + chalk16.dim(chalk16.magenta(tagStr));
|
|
3307
3198
|
}
|
|
3308
3199
|
}
|
|
3309
|
-
console.log(` ${
|
|
3200
|
+
console.log(` ${chalk16.cyan(sanitizeUserInput(spec.path))}${assigneeStr}${tagsStr}`);
|
|
3310
3201
|
}
|
|
3311
3202
|
}
|
|
3312
3203
|
console.log("");
|
|
3313
3204
|
} else if (!expanded && specs.length > 0) {
|
|
3314
|
-
console.log(` ${
|
|
3205
|
+
console.log(` ${chalk16.dim("(collapsed, use --complete to expand)")}`);
|
|
3315
3206
|
console.log("");
|
|
3316
3207
|
} else {
|
|
3317
|
-
console.log(` ${
|
|
3208
|
+
console.log(` ${chalk16.dim("(empty)")}`);
|
|
3318
3209
|
console.log("");
|
|
3319
3210
|
}
|
|
3320
3211
|
}
|
|
@@ -3322,10 +3213,6 @@ function stripAnsi2(str) {
|
|
|
3322
3213
|
return str.replace(/\u001b\[\d+m/g, "");
|
|
3323
3214
|
}
|
|
3324
3215
|
|
|
3325
|
-
// src/commands/stats.ts
|
|
3326
|
-
import chalk16 from "chalk";
|
|
3327
|
-
import dayjs4 from "dayjs";
|
|
3328
|
-
|
|
3329
3216
|
// src/utils/spec-stats.ts
|
|
3330
3217
|
function countSpecsByStatusAndPriority(specs) {
|
|
3331
3218
|
const statusCounts = {
|
|
@@ -3359,13 +3246,10 @@ function countSpecsByStatusAndPriority(specs) {
|
|
|
3359
3246
|
}
|
|
3360
3247
|
return { statusCounts, priorityCounts, tagCounts };
|
|
3361
3248
|
}
|
|
3362
|
-
|
|
3363
|
-
// src/utils/insights.ts
|
|
3364
|
-
import dayjs3 from "dayjs";
|
|
3365
3249
|
function generateInsights(specs) {
|
|
3366
3250
|
const insights = [];
|
|
3367
3251
|
const criticalOverdue = specs.filter(
|
|
3368
|
-
(s) => s.frontmatter.priority === "critical" && s.frontmatter.due &&
|
|
3252
|
+
(s) => s.frontmatter.priority === "critical" && s.frontmatter.due && dayjs2(s.frontmatter.due).isBefore(dayjs2(), "day") && s.frontmatter.status !== "complete" && s.frontmatter.status !== "archived"
|
|
3369
3253
|
);
|
|
3370
3254
|
if (criticalOverdue.length > 0) {
|
|
3371
3255
|
insights.push({
|
|
@@ -3375,7 +3259,7 @@ function generateInsights(specs) {
|
|
|
3375
3259
|
});
|
|
3376
3260
|
}
|
|
3377
3261
|
const highOverdue = specs.filter(
|
|
3378
|
-
(s) => s.frontmatter.priority === "high" && s.frontmatter.due &&
|
|
3262
|
+
(s) => s.frontmatter.priority === "high" && s.frontmatter.due && dayjs2(s.frontmatter.due).isBefore(dayjs2(), "day") && s.frontmatter.status !== "complete" && s.frontmatter.status !== "archived"
|
|
3379
3263
|
);
|
|
3380
3264
|
if (highOverdue.length > 0) {
|
|
3381
3265
|
insights.push({
|
|
@@ -3392,7 +3276,7 @@ function generateInsights(specs) {
|
|
|
3392
3276
|
if (!updatedAt) {
|
|
3393
3277
|
return false;
|
|
3394
3278
|
}
|
|
3395
|
-
const daysSinceUpdate =
|
|
3279
|
+
const daysSinceUpdate = dayjs2().diff(dayjs2(updatedAt), "day");
|
|
3396
3280
|
return daysSinceUpdate > 7;
|
|
3397
3281
|
});
|
|
3398
3282
|
if (longRunning.length > 0) {
|
|
@@ -3425,14 +3309,14 @@ function generateInsights(specs) {
|
|
|
3425
3309
|
return insights.slice(0, 5);
|
|
3426
3310
|
}
|
|
3427
3311
|
function getSpecInsightDetails(spec) {
|
|
3428
|
-
if (spec.frontmatter.due &&
|
|
3429
|
-
const daysOverdue =
|
|
3312
|
+
if (spec.frontmatter.due && dayjs2(spec.frontmatter.due).isBefore(dayjs2(), "day") && spec.frontmatter.status !== "complete" && spec.frontmatter.status !== "archived") {
|
|
3313
|
+
const daysOverdue = dayjs2().diff(dayjs2(spec.frontmatter.due), "day");
|
|
3430
3314
|
return `overdue by ${daysOverdue} day${daysOverdue > 1 ? "s" : ""}`;
|
|
3431
3315
|
}
|
|
3432
3316
|
if (spec.frontmatter.status === "in-progress") {
|
|
3433
3317
|
const updatedAt = spec.frontmatter.updated || spec.frontmatter.updated_at || spec.frontmatter.created || spec.frontmatter.created_at;
|
|
3434
3318
|
if (updatedAt) {
|
|
3435
|
-
const daysSinceUpdate =
|
|
3319
|
+
const daysSinceUpdate = dayjs2().diff(dayjs2(updatedAt), "day");
|
|
3436
3320
|
if (daysSinceUpdate > 7) {
|
|
3437
3321
|
return `in-progress for ${daysSinceUpdate} days`;
|
|
3438
3322
|
}
|
|
@@ -3462,7 +3346,7 @@ async function statsCommand(options) {
|
|
|
3462
3346
|
console.log("No specs found.");
|
|
3463
3347
|
return;
|
|
3464
3348
|
}
|
|
3465
|
-
|
|
3349
|
+
options.full || false;
|
|
3466
3350
|
const showStats = options.full || !options.timeline && !options.velocity;
|
|
3467
3351
|
const showTimeline = options.timeline || options.full;
|
|
3468
3352
|
const showVelocity = options.velocity || options.full;
|
|
@@ -3532,7 +3416,7 @@ async function statsCommand(options) {
|
|
|
3532
3416
|
const criticalInProgress = specs.filter((s) => s.frontmatter.priority === "critical" && s.frontmatter.status === "in-progress").length;
|
|
3533
3417
|
const criticalComplete = specs.filter((s) => s.frontmatter.priority === "critical" && s.frontmatter.status === "complete").length;
|
|
3534
3418
|
const criticalOverdue = specs.filter(
|
|
3535
|
-
(s) => s.frontmatter.priority === "critical" && s.frontmatter.due &&
|
|
3419
|
+
(s) => s.frontmatter.priority === "critical" && s.frontmatter.due && dayjs2(s.frontmatter.due).isBefore(dayjs2(), "day") && s.frontmatter.status !== "complete"
|
|
3536
3420
|
).length;
|
|
3537
3421
|
const parts = [];
|
|
3538
3422
|
if (criticalPlanned > 0) parts.push(chalk16.dim(`${criticalPlanned} planned`));
|
|
@@ -3546,7 +3430,7 @@ async function statsCommand(options) {
|
|
|
3546
3430
|
const highInProgress = specs.filter((s) => s.frontmatter.priority === "high" && s.frontmatter.status === "in-progress").length;
|
|
3547
3431
|
const highComplete = specs.filter((s) => s.frontmatter.priority === "high" && s.frontmatter.status === "complete").length;
|
|
3548
3432
|
const highOverdue = specs.filter(
|
|
3549
|
-
(s) => s.frontmatter.priority === "high" && s.frontmatter.due &&
|
|
3433
|
+
(s) => s.frontmatter.priority === "high" && s.frontmatter.due && dayjs2(s.frontmatter.due).isBefore(dayjs2(), "day") && s.frontmatter.status !== "complete"
|
|
3550
3434
|
).length;
|
|
3551
3435
|
const parts = [];
|
|
3552
3436
|
if (highPlanned > 0) parts.push(chalk16.dim(`${highPlanned} planned`));
|
|
@@ -3708,18 +3592,18 @@ async function statsCommand(options) {
|
|
|
3708
3592
|
}
|
|
3709
3593
|
if (showTimeline) {
|
|
3710
3594
|
const days = 30;
|
|
3711
|
-
const today =
|
|
3595
|
+
const today = dayjs2();
|
|
3712
3596
|
const startDate = today.subtract(days, "day");
|
|
3713
3597
|
const createdByDate = {};
|
|
3714
3598
|
const completedByDate = {};
|
|
3715
3599
|
for (const spec of specs) {
|
|
3716
|
-
const created =
|
|
3600
|
+
const created = dayjs2(spec.frontmatter.created);
|
|
3717
3601
|
if (created.isAfter(startDate)) {
|
|
3718
3602
|
const dateKey = created.format("YYYY-MM-DD");
|
|
3719
3603
|
createdByDate[dateKey] = (createdByDate[dateKey] || 0) + 1;
|
|
3720
3604
|
}
|
|
3721
3605
|
if (spec.frontmatter.completed) {
|
|
3722
|
-
const completed =
|
|
3606
|
+
const completed = dayjs2(spec.frontmatter.completed);
|
|
3723
3607
|
if (completed.isAfter(startDate)) {
|
|
3724
3608
|
const dateKey = completed.format("YYYY-MM-DD");
|
|
3725
3609
|
completedByDate[dateKey] = (completedByDate[dateKey] || 0) + 1;
|
|
@@ -3838,9 +3722,6 @@ async function statsCommand(options) {
|
|
|
3838
3722
|
}
|
|
3839
3723
|
}
|
|
3840
3724
|
}
|
|
3841
|
-
|
|
3842
|
-
// src/commands/search.ts
|
|
3843
|
-
import chalk17 from "chalk";
|
|
3844
3725
|
async function searchCommand(query, options) {
|
|
3845
3726
|
await autoCheckIfEnabled();
|
|
3846
3727
|
const filter = {};
|
|
@@ -3884,32 +3765,32 @@ async function searchCommand(query, options) {
|
|
|
3884
3765
|
}
|
|
3885
3766
|
if (results.length === 0) {
|
|
3886
3767
|
console.log("");
|
|
3887
|
-
console.log(
|
|
3768
|
+
console.log(chalk16.yellow(`\u{1F50D} No specs found matching "${sanitizeUserInput(query)}"`));
|
|
3888
3769
|
if (Object.keys(filter).length > 0) {
|
|
3889
3770
|
const filters = [];
|
|
3890
3771
|
if (options.status) filters.push(`status=${sanitizeUserInput(options.status)}`);
|
|
3891
3772
|
if (options.tag) filters.push(`tag=${sanitizeUserInput(options.tag)}`);
|
|
3892
3773
|
if (options.priority) filters.push(`priority=${sanitizeUserInput(options.priority)}`);
|
|
3893
3774
|
if (options.assignee) filters.push(`assignee=${sanitizeUserInput(options.assignee)}`);
|
|
3894
|
-
console.log(
|
|
3775
|
+
console.log(chalk16.gray(`With filters: ${filters.join(", ")}`));
|
|
3895
3776
|
}
|
|
3896
3777
|
console.log("");
|
|
3897
3778
|
return;
|
|
3898
3779
|
}
|
|
3899
3780
|
console.log("");
|
|
3900
|
-
console.log(
|
|
3781
|
+
console.log(chalk16.green(`\u{1F50D} Found ${results.length} spec${results.length === 1 ? "" : "s"} matching "${sanitizeUserInput(query)}"`));
|
|
3901
3782
|
if (Object.keys(filter).length > 0) {
|
|
3902
3783
|
const filters = [];
|
|
3903
3784
|
if (options.status) filters.push(`status=${sanitizeUserInput(options.status)}`);
|
|
3904
3785
|
if (options.tag) filters.push(`tag=${sanitizeUserInput(options.tag)}`);
|
|
3905
3786
|
if (options.priority) filters.push(`priority=${sanitizeUserInput(options.priority)}`);
|
|
3906
3787
|
if (options.assignee) filters.push(`assignee=${sanitizeUserInput(options.assignee)}`);
|
|
3907
|
-
console.log(
|
|
3788
|
+
console.log(chalk16.gray(`With filters: ${filters.join(", ")}`));
|
|
3908
3789
|
}
|
|
3909
3790
|
console.log("");
|
|
3910
3791
|
for (const result of results) {
|
|
3911
3792
|
const { spec, matches } = result;
|
|
3912
|
-
console.log(
|
|
3793
|
+
console.log(chalk16.cyan(`${spec.frontmatter.status === "in-progress" ? "\u{1F528}" : spec.frontmatter.status === "complete" ? "\u2705" : "\u{1F4C5}"} ${sanitizeUserInput(spec.path)}`));
|
|
3913
3794
|
const meta = [];
|
|
3914
3795
|
if (spec.frontmatter.priority) {
|
|
3915
3796
|
const priorityEmoji = spec.frontmatter.priority === "critical" ? "\u{1F534}" : spec.frontmatter.priority === "high" ? "\u{1F7E1}" : spec.frontmatter.priority === "medium" ? "\u{1F7E0}" : "\u{1F7E2}";
|
|
@@ -3919,34 +3800,30 @@ async function searchCommand(query, options) {
|
|
|
3919
3800
|
meta.push(`[${spec.frontmatter.tags.map((tag) => sanitizeUserInput(tag)).join(", ")}]`);
|
|
3920
3801
|
}
|
|
3921
3802
|
if (meta.length > 0) {
|
|
3922
|
-
console.log(
|
|
3803
|
+
console.log(chalk16.gray(` ${meta.join(" \u2022 ")}`));
|
|
3923
3804
|
}
|
|
3924
3805
|
const maxMatches = 3;
|
|
3925
3806
|
for (let i = 0; i < Math.min(matches.length, maxMatches); i++) {
|
|
3926
|
-
console.log(` ${
|
|
3807
|
+
console.log(` ${chalk16.gray("Match:")} ${matches[i].trim()}`);
|
|
3927
3808
|
}
|
|
3928
3809
|
if (matches.length > maxMatches) {
|
|
3929
|
-
console.log(
|
|
3810
|
+
console.log(chalk16.gray(` ... and ${matches.length - maxMatches} more match${matches.length - maxMatches === 1 ? "" : "es"}`));
|
|
3930
3811
|
}
|
|
3931
3812
|
console.log("");
|
|
3932
3813
|
}
|
|
3933
3814
|
}
|
|
3934
3815
|
function highlightMatch(text, query) {
|
|
3935
3816
|
const regex = new RegExp(`(${escapeRegex(query)})`, "gi");
|
|
3936
|
-
return text.replace(regex,
|
|
3817
|
+
return text.replace(regex, chalk16.yellow("$1"));
|
|
3937
3818
|
}
|
|
3938
3819
|
function escapeRegex(str) {
|
|
3939
3820
|
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3940
3821
|
}
|
|
3941
|
-
|
|
3942
|
-
// src/commands/deps.ts
|
|
3943
|
-
import chalk18 from "chalk";
|
|
3944
|
-
import * as path18 from "path";
|
|
3945
3822
|
async function depsCommand(specPath, options) {
|
|
3946
3823
|
await autoCheckIfEnabled();
|
|
3947
3824
|
const config = await loadConfig();
|
|
3948
3825
|
const cwd = process.cwd();
|
|
3949
|
-
const specsDir =
|
|
3826
|
+
const specsDir = path2.join(cwd, config.specsDir);
|
|
3950
3827
|
const resolvedPath = await resolveSpecPath(specPath, cwd, specsDir);
|
|
3951
3828
|
if (!resolvedPath) {
|
|
3952
3829
|
throw new Error(`Spec not found: ${sanitizeUserInput(specPath)}`);
|
|
@@ -3975,16 +3852,16 @@ async function depsCommand(specPath, options) {
|
|
|
3975
3852
|
return;
|
|
3976
3853
|
}
|
|
3977
3854
|
console.log("");
|
|
3978
|
-
console.log(
|
|
3855
|
+
console.log(chalk16.green(`\u{1F4E6} Dependencies for ${chalk16.cyan(sanitizeUserInput(spec.path))}`));
|
|
3979
3856
|
console.log("");
|
|
3980
3857
|
const hasAnyRelationships = dependsOn.length > 0 || blocks.length > 0 || relatedSpecs.length > 0;
|
|
3981
3858
|
if (!hasAnyRelationships) {
|
|
3982
|
-
console.log(
|
|
3859
|
+
console.log(chalk16.gray(" No dependencies or relationships"));
|
|
3983
3860
|
console.log("");
|
|
3984
3861
|
return;
|
|
3985
3862
|
}
|
|
3986
3863
|
if (dependsOn.length > 0) {
|
|
3987
|
-
console.log(
|
|
3864
|
+
console.log(chalk16.bold("Depends On:"));
|
|
3988
3865
|
for (const dep of dependsOn) {
|
|
3989
3866
|
const status = getStatusIndicator(dep.frontmatter.status);
|
|
3990
3867
|
console.log(` \u2192 ${sanitizeUserInput(dep.path)} ${status}`);
|
|
@@ -3992,7 +3869,7 @@ async function depsCommand(specPath, options) {
|
|
|
3992
3869
|
console.log("");
|
|
3993
3870
|
}
|
|
3994
3871
|
if (blocks.length > 0) {
|
|
3995
|
-
console.log(
|
|
3872
|
+
console.log(chalk16.bold("Required By:"));
|
|
3996
3873
|
for (const blocked of blocks) {
|
|
3997
3874
|
const status = getStatusIndicator(blocked.frontmatter.status);
|
|
3998
3875
|
console.log(` \u2190 ${sanitizeUserInput(blocked.path)} ${status}`);
|
|
@@ -4000,7 +3877,7 @@ async function depsCommand(specPath, options) {
|
|
|
4000
3877
|
console.log("");
|
|
4001
3878
|
}
|
|
4002
3879
|
if (relatedSpecs.length > 0) {
|
|
4003
|
-
console.log(
|
|
3880
|
+
console.log(chalk16.bold("Related Specs:"));
|
|
4004
3881
|
for (const rel of relatedSpecs) {
|
|
4005
3882
|
const status = getStatusIndicator(rel.frontmatter.status);
|
|
4006
3883
|
console.log(` \u27F7 ${sanitizeUserInput(rel.path)} ${status}`);
|
|
@@ -4008,7 +3885,7 @@ async function depsCommand(specPath, options) {
|
|
|
4008
3885
|
console.log("");
|
|
4009
3886
|
}
|
|
4010
3887
|
if (options.graph || dependsOn.length > 0) {
|
|
4011
|
-
console.log(
|
|
3888
|
+
console.log(chalk16.bold("Dependency Chain:"));
|
|
4012
3889
|
const chain = buildDependencyChain(spec, specMap, options.depth || 3);
|
|
4013
3890
|
displayChain(chain, 0);
|
|
4014
3891
|
console.log("");
|
|
@@ -4022,8 +3899,8 @@ function findDependencies(spec, specMap) {
|
|
|
4022
3899
|
if (dep) {
|
|
4023
3900
|
deps.push(dep);
|
|
4024
3901
|
} else {
|
|
4025
|
-
for (const [
|
|
4026
|
-
if (
|
|
3902
|
+
for (const [path21, s] of specMap.entries()) {
|
|
3903
|
+
if (path21.includes(depPath)) {
|
|
4027
3904
|
deps.push(s);
|
|
4028
3905
|
break;
|
|
4029
3906
|
}
|
|
@@ -4055,8 +3932,8 @@ function findRelated(spec, specMap) {
|
|
|
4055
3932
|
if (rel) {
|
|
4056
3933
|
related.push(rel);
|
|
4057
3934
|
} else {
|
|
4058
|
-
for (const [
|
|
4059
|
-
if (
|
|
3935
|
+
for (const [path21, s] of specMap.entries()) {
|
|
3936
|
+
if (path21.includes(relPath)) {
|
|
4060
3937
|
related.push(s);
|
|
4061
3938
|
break;
|
|
4062
3939
|
}
|
|
@@ -4114,7 +3991,7 @@ function buildDependencyChain(spec, specMap, maxDepth, currentDepth = 0, visited
|
|
|
4114
3991
|
function displayChain(node, level) {
|
|
4115
3992
|
const indent = " ".repeat(level);
|
|
4116
3993
|
const status = getStatusIndicator(node.spec.frontmatter.status);
|
|
4117
|
-
const name = level === 0 ?
|
|
3994
|
+
const name = level === 0 ? chalk16.cyan(node.spec.path) : node.spec.path;
|
|
4118
3995
|
console.log(`${indent}${name} ${status}`);
|
|
4119
3996
|
for (const dep of node.dependencies) {
|
|
4120
3997
|
const prefix = " ".repeat(level) + "\u2514\u2500 ";
|
|
@@ -4125,10 +4002,6 @@ function displayChain(node, level) {
|
|
|
4125
4002
|
}
|
|
4126
4003
|
}
|
|
4127
4004
|
}
|
|
4128
|
-
|
|
4129
|
-
// src/commands/timeline.ts
|
|
4130
|
-
import chalk19 from "chalk";
|
|
4131
|
-
import dayjs5 from "dayjs";
|
|
4132
4005
|
async function timelineCommand(options) {
|
|
4133
4006
|
await autoCheckIfEnabled();
|
|
4134
4007
|
const createBar = (count, maxCount, width, char = "\u2501") => {
|
|
@@ -4143,13 +4016,13 @@ async function timelineCommand(options) {
|
|
|
4143
4016
|
console.log("No specs found.");
|
|
4144
4017
|
return;
|
|
4145
4018
|
}
|
|
4146
|
-
const today =
|
|
4019
|
+
const today = dayjs2();
|
|
4147
4020
|
const startDate = today.subtract(days, "day");
|
|
4148
4021
|
const createdByDate = {};
|
|
4149
4022
|
const completedByDate = {};
|
|
4150
4023
|
const createdByMonth = {};
|
|
4151
4024
|
for (const spec of specs) {
|
|
4152
|
-
const created =
|
|
4025
|
+
const created = dayjs2(spec.frontmatter.created);
|
|
4153
4026
|
if (created.isAfter(startDate)) {
|
|
4154
4027
|
const dateKey = created.format("YYYY-MM-DD");
|
|
4155
4028
|
createdByDate[dateKey] = (createdByDate[dateKey] || 0) + 1;
|
|
@@ -4157,26 +4030,26 @@ async function timelineCommand(options) {
|
|
|
4157
4030
|
const monthKey = created.format("MMM YYYY");
|
|
4158
4031
|
createdByMonth[monthKey] = (createdByMonth[monthKey] || 0) + 1;
|
|
4159
4032
|
if (spec.frontmatter.completed) {
|
|
4160
|
-
const completed =
|
|
4033
|
+
const completed = dayjs2(spec.frontmatter.completed);
|
|
4161
4034
|
if (completed.isAfter(startDate)) {
|
|
4162
4035
|
const dateKey = completed.format("YYYY-MM-DD");
|
|
4163
4036
|
completedByDate[dateKey] = (completedByDate[dateKey] || 0) + 1;
|
|
4164
4037
|
}
|
|
4165
4038
|
}
|
|
4166
4039
|
}
|
|
4167
|
-
console.log(
|
|
4040
|
+
console.log(chalk16.bold.cyan("\u{1F4C8} Spec Timeline"));
|
|
4168
4041
|
console.log("");
|
|
4169
4042
|
const allDates = /* @__PURE__ */ new Set([...Object.keys(createdByDate), ...Object.keys(completedByDate)]);
|
|
4170
4043
|
const sortedDates = Array.from(allDates).sort();
|
|
4171
4044
|
if (sortedDates.length > 0) {
|
|
4172
|
-
console.log(
|
|
4045
|
+
console.log(chalk16.bold(`\u{1F4C5} Activity (Last ${days} Days)`));
|
|
4173
4046
|
console.log("");
|
|
4174
4047
|
const labelWidth2 = 15;
|
|
4175
4048
|
const barWidth = 20;
|
|
4176
4049
|
const specsWidth = 3;
|
|
4177
4050
|
const colWidth = barWidth + specsWidth;
|
|
4178
|
-
console.log(` ${"Date".padEnd(labelWidth2)} ${
|
|
4179
|
-
console.log(` ${
|
|
4051
|
+
console.log(` ${"Date".padEnd(labelWidth2)} ${chalk16.cyan("Created".padEnd(colWidth))} ${chalk16.green("Completed".padEnd(colWidth))}`);
|
|
4052
|
+
console.log(` ${chalk16.dim("\u2500".repeat(labelWidth2))} ${chalk16.dim("\u2500".repeat(colWidth))} ${chalk16.dim("\u2500".repeat(colWidth))}`);
|
|
4180
4053
|
const maxCount = Math.max(...Object.values(createdByDate), ...Object.values(completedByDate));
|
|
4181
4054
|
for (const date of sortedDates) {
|
|
4182
4055
|
const created = createdByDate[date] || 0;
|
|
@@ -4185,61 +4058,61 @@ async function timelineCommand(options) {
|
|
|
4185
4058
|
const completedBar = createBar(completed, maxCount, barWidth);
|
|
4186
4059
|
const createdCol = `${createdBar.padEnd(barWidth)}${created.toString().padStart(specsWidth)}`;
|
|
4187
4060
|
const completedCol = `${completedBar.padEnd(barWidth)}${completed.toString().padStart(specsWidth)}`;
|
|
4188
|
-
console.log(` ${
|
|
4061
|
+
console.log(` ${chalk16.dim(date.padEnd(labelWidth2))} ${chalk16.cyan(createdCol)} ${chalk16.green(completedCol)}`);
|
|
4189
4062
|
}
|
|
4190
4063
|
console.log("");
|
|
4191
4064
|
}
|
|
4192
4065
|
const sortedMonths = Object.entries(createdByMonth).sort((a, b) => {
|
|
4193
|
-
const dateA =
|
|
4194
|
-
const dateB =
|
|
4066
|
+
const dateA = dayjs2(a[0], "MMM YYYY");
|
|
4067
|
+
const dateB = dayjs2(b[0], "MMM YYYY");
|
|
4195
4068
|
return dateB.diff(dateA);
|
|
4196
4069
|
}).slice(0, 6);
|
|
4197
4070
|
if (sortedMonths.length > 0) {
|
|
4198
|
-
console.log(
|
|
4071
|
+
console.log(chalk16.bold("\u{1F4CA} Monthly Overview"));
|
|
4199
4072
|
console.log("");
|
|
4200
4073
|
const labelWidth2 = 15;
|
|
4201
4074
|
const barWidth = 20;
|
|
4202
4075
|
const specsWidth = 3;
|
|
4203
4076
|
const colWidth = barWidth + specsWidth;
|
|
4204
|
-
console.log(` ${"Month".padEnd(labelWidth2)} ${
|
|
4205
|
-
console.log(` ${
|
|
4077
|
+
console.log(` ${"Month".padEnd(labelWidth2)} ${chalk16.magenta("Specs".padEnd(colWidth))}`);
|
|
4078
|
+
console.log(` ${chalk16.dim("\u2500".repeat(labelWidth2))} ${chalk16.dim("\u2500".repeat(colWidth))}`);
|
|
4206
4079
|
const maxCount = Math.max(...sortedMonths.map(([, count]) => count));
|
|
4207
4080
|
for (const [month, count] of sortedMonths) {
|
|
4208
4081
|
const bar = createBar(count, maxCount, barWidth);
|
|
4209
|
-
console.log(` ${month.padEnd(labelWidth2)} ${
|
|
4082
|
+
console.log(` ${month.padEnd(labelWidth2)} ${chalk16.magenta(bar.padEnd(barWidth))}${chalk16.magenta(count.toString().padStart(specsWidth))}`);
|
|
4210
4083
|
}
|
|
4211
4084
|
console.log("");
|
|
4212
4085
|
}
|
|
4213
4086
|
const last7Days = specs.filter((s) => {
|
|
4214
4087
|
if (!s.frontmatter.completed) return false;
|
|
4215
|
-
const completed =
|
|
4088
|
+
const completed = dayjs2(s.frontmatter.completed);
|
|
4216
4089
|
return completed.isAfter(today.subtract(7, "day"));
|
|
4217
4090
|
}).length;
|
|
4218
4091
|
const last30Days = specs.filter((s) => {
|
|
4219
4092
|
if (!s.frontmatter.completed) return false;
|
|
4220
|
-
const completed =
|
|
4093
|
+
const completed = dayjs2(s.frontmatter.completed);
|
|
4221
4094
|
return completed.isAfter(today.subtract(30, "day"));
|
|
4222
4095
|
}).length;
|
|
4223
|
-
console.log(
|
|
4096
|
+
console.log(chalk16.bold("\u2705 Completion Rate"));
|
|
4224
4097
|
console.log("");
|
|
4225
4098
|
const labelWidth = 15;
|
|
4226
4099
|
const valueWidth = 5;
|
|
4227
4100
|
console.log(` ${"Period".padEnd(labelWidth)} ${"Specs".padStart(valueWidth)}`);
|
|
4228
|
-
console.log(` ${
|
|
4229
|
-
console.log(` ${"Last 7 days".padEnd(labelWidth)} ${
|
|
4230
|
-
console.log(` ${"Last 30 days".padEnd(labelWidth)} ${
|
|
4101
|
+
console.log(` ${chalk16.dim("\u2500".repeat(labelWidth))} ${chalk16.dim("\u2500".repeat(valueWidth))}`);
|
|
4102
|
+
console.log(` ${"Last 7 days".padEnd(labelWidth)} ${chalk16.green(last7Days.toString().padStart(valueWidth))}`);
|
|
4103
|
+
console.log(` ${"Last 30 days".padEnd(labelWidth)} ${chalk16.green(last30Days.toString().padStart(valueWidth))}`);
|
|
4231
4104
|
console.log("");
|
|
4232
4105
|
if (options.byTag) {
|
|
4233
4106
|
const tagStats = {};
|
|
4234
4107
|
for (const spec of specs) {
|
|
4235
|
-
const created =
|
|
4108
|
+
const created = dayjs2(spec.frontmatter.created);
|
|
4236
4109
|
const isInRange = created.isAfter(startDate);
|
|
4237
4110
|
if (isInRange && spec.frontmatter.tags) {
|
|
4238
4111
|
for (const tag of spec.frontmatter.tags) {
|
|
4239
4112
|
if (!tagStats[tag]) tagStats[tag] = { created: 0, completed: 0 };
|
|
4240
4113
|
tagStats[tag].created++;
|
|
4241
4114
|
if (spec.frontmatter.completed) {
|
|
4242
|
-
const completed =
|
|
4115
|
+
const completed = dayjs2(spec.frontmatter.completed);
|
|
4243
4116
|
if (completed.isAfter(startDate)) {
|
|
4244
4117
|
tagStats[tag].completed++;
|
|
4245
4118
|
}
|
|
@@ -4249,9 +4122,9 @@ async function timelineCommand(options) {
|
|
|
4249
4122
|
}
|
|
4250
4123
|
const sortedTags = Object.entries(tagStats).sort((a, b) => b[1].created - a[1].created).slice(0, 10);
|
|
4251
4124
|
if (sortedTags.length > 0) {
|
|
4252
|
-
console.log(
|
|
4125
|
+
console.log(chalk16.bold("\u{1F3F7}\uFE0F By Tag"));
|
|
4253
4126
|
for (const [tag, stats] of sortedTags) {
|
|
4254
|
-
console.log(` ${
|
|
4127
|
+
console.log(` ${chalk16.dim("#")}${tag.padEnd(20)} ${chalk16.cyan(stats.created)} created \xB7 ${chalk16.green(stats.completed)} completed`);
|
|
4255
4128
|
}
|
|
4256
4129
|
console.log("");
|
|
4257
4130
|
}
|
|
@@ -4260,14 +4133,14 @@ async function timelineCommand(options) {
|
|
|
4260
4133
|
const assigneeStats = {};
|
|
4261
4134
|
for (const spec of specs) {
|
|
4262
4135
|
if (!spec.frontmatter.assignee) continue;
|
|
4263
|
-
const created =
|
|
4136
|
+
const created = dayjs2(spec.frontmatter.created);
|
|
4264
4137
|
const isInRange = created.isAfter(startDate);
|
|
4265
4138
|
if (isInRange) {
|
|
4266
4139
|
const assignee = spec.frontmatter.assignee;
|
|
4267
4140
|
if (!assigneeStats[assignee]) assigneeStats[assignee] = { created: 0, completed: 0 };
|
|
4268
4141
|
assigneeStats[assignee].created++;
|
|
4269
4142
|
if (spec.frontmatter.completed) {
|
|
4270
|
-
const completed =
|
|
4143
|
+
const completed = dayjs2(spec.frontmatter.completed);
|
|
4271
4144
|
if (completed.isAfter(startDate)) {
|
|
4272
4145
|
assigneeStats[assignee].completed++;
|
|
4273
4146
|
}
|
|
@@ -4276,18 +4149,14 @@ async function timelineCommand(options) {
|
|
|
4276
4149
|
}
|
|
4277
4150
|
const sortedAssignees = Object.entries(assigneeStats).sort((a, b) => b[1].created - a[1].created);
|
|
4278
4151
|
if (sortedAssignees.length > 0) {
|
|
4279
|
-
console.log(
|
|
4152
|
+
console.log(chalk16.bold("\u{1F464} By Assignee"));
|
|
4280
4153
|
for (const [assignee, stats] of sortedAssignees) {
|
|
4281
|
-
console.log(` ${
|
|
4154
|
+
console.log(` ${chalk16.dim("@")}${assignee.padEnd(20)} ${chalk16.cyan(stats.created)} created \xB7 ${chalk16.green(stats.completed)} completed`);
|
|
4282
4155
|
}
|
|
4283
4156
|
console.log("");
|
|
4284
4157
|
}
|
|
4285
4158
|
}
|
|
4286
4159
|
}
|
|
4287
|
-
|
|
4288
|
-
// src/commands/gantt.ts
|
|
4289
|
-
import chalk20 from "chalk";
|
|
4290
|
-
import dayjs6 from "dayjs";
|
|
4291
4160
|
var SPEC_COLUMN_WIDTH = 43;
|
|
4292
4161
|
var COLUMN_SEPARATOR = " ";
|
|
4293
4162
|
var SPEC_INDENT = " ";
|
|
@@ -4300,10 +4169,10 @@ var STATUS_CONFIG2 = {
|
|
|
4300
4169
|
archived: { emoji: "\u{1F4E6}", color: "gray" }
|
|
4301
4170
|
};
|
|
4302
4171
|
var PRIORITY_CONFIG3 = {
|
|
4303
|
-
critical: { emoji: "\u{1F534}", label: "CRITICAL", colorFn:
|
|
4304
|
-
high: { emoji: "\u{1F7E0}", label: "HIGH", colorFn:
|
|
4305
|
-
medium: { emoji: "\u{1F7E1}", label: "MEDIUM", colorFn:
|
|
4306
|
-
low: { emoji: "\u{1F7E2}", label: "LOW", colorFn:
|
|
4172
|
+
critical: { emoji: "\u{1F534}", label: "CRITICAL", colorFn: chalk16.red },
|
|
4173
|
+
high: { emoji: "\u{1F7E0}", label: "HIGH", colorFn: chalk16.hex("#FFA500") },
|
|
4174
|
+
medium: { emoji: "\u{1F7E1}", label: "MEDIUM", colorFn: chalk16.yellow },
|
|
4175
|
+
low: { emoji: "\u{1F7E2}", label: "LOW", colorFn: chalk16.green }
|
|
4307
4176
|
};
|
|
4308
4177
|
async function ganttCommand(options) {
|
|
4309
4178
|
await autoCheckIfEnabled();
|
|
@@ -4326,8 +4195,8 @@ async function ganttCommand(options) {
|
|
|
4326
4195
|
return spec.frontmatter.status !== "archived";
|
|
4327
4196
|
});
|
|
4328
4197
|
if (relevantSpecs.length === 0) {
|
|
4329
|
-
console.log(
|
|
4330
|
-
console.log(
|
|
4198
|
+
console.log(chalk16.dim("No active specs found."));
|
|
4199
|
+
console.log(chalk16.dim("Tip: Use --show-complete to include completed specs."));
|
|
4331
4200
|
return;
|
|
4332
4201
|
}
|
|
4333
4202
|
const groupedSpecs = {
|
|
@@ -4336,12 +4205,9 @@ async function ganttCommand(options) {
|
|
|
4336
4205
|
medium: [],
|
|
4337
4206
|
low: []
|
|
4338
4207
|
};
|
|
4339
|
-
const noPrioritySpecs = [];
|
|
4340
4208
|
for (const spec of relevantSpecs) {
|
|
4341
4209
|
if (spec.frontmatter.priority && spec.frontmatter.priority in groupedSpecs) {
|
|
4342
4210
|
groupedSpecs[spec.frontmatter.priority].push(spec);
|
|
4343
|
-
} else {
|
|
4344
|
-
noPrioritySpecs.push(spec);
|
|
4345
4211
|
}
|
|
4346
4212
|
}
|
|
4347
4213
|
const sortSpecs = (specs2) => {
|
|
@@ -4353,20 +4219,20 @@ async function ganttCommand(options) {
|
|
|
4353
4219
|
if (a.frontmatter.due && !b.frontmatter.due) return -1;
|
|
4354
4220
|
if (!a.frontmatter.due && b.frontmatter.due) return 1;
|
|
4355
4221
|
if (a.frontmatter.due && b.frontmatter.due) {
|
|
4356
|
-
return
|
|
4222
|
+
return dayjs2(a.frontmatter.due).diff(dayjs2(b.frontmatter.due));
|
|
4357
4223
|
}
|
|
4358
4224
|
return 0;
|
|
4359
4225
|
});
|
|
4360
4226
|
};
|
|
4361
|
-
const today =
|
|
4227
|
+
const today = dayjs2();
|
|
4362
4228
|
const startDate = today.startOf("week");
|
|
4363
4229
|
const endDate = startDate.add(weeks, "week");
|
|
4364
4230
|
const inProgress = relevantSpecs.filter((s) => s.frontmatter.status === "in-progress").length;
|
|
4365
4231
|
const planned = relevantSpecs.filter((s) => s.frontmatter.status === "planned").length;
|
|
4366
4232
|
const overdue = relevantSpecs.filter(
|
|
4367
|
-
(s) => s.frontmatter.due &&
|
|
4233
|
+
(s) => s.frontmatter.due && dayjs2(s.frontmatter.due).isBefore(today) && s.frontmatter.status !== "complete"
|
|
4368
4234
|
).length;
|
|
4369
|
-
console.log(
|
|
4235
|
+
console.log(chalk16.bold.cyan(`\u{1F4C5} Gantt Chart (${weeks} weeks from ${startDate.format("MMM D, YYYY")})`));
|
|
4370
4236
|
console.log("");
|
|
4371
4237
|
const specHeader = "Spec".padEnd(SPEC_COLUMN_WIDTH);
|
|
4372
4238
|
const timelineHeader = "Timeline";
|
|
@@ -4378,17 +4244,17 @@ async function ganttCommand(options) {
|
|
|
4378
4244
|
calendarDates.push(dateStr);
|
|
4379
4245
|
}
|
|
4380
4246
|
const dateRow = " ".repeat(SPEC_COLUMN_WIDTH) + COLUMN_SEPARATOR + calendarDates.join("");
|
|
4381
|
-
console.log(
|
|
4247
|
+
console.log(chalk16.dim(dateRow));
|
|
4382
4248
|
const specSeparator = "\u2500".repeat(SPEC_COLUMN_WIDTH);
|
|
4383
4249
|
const timelineSeparator = "\u2500".repeat(timelineColumnWidth);
|
|
4384
|
-
console.log(
|
|
4250
|
+
console.log(chalk16.dim(specSeparator + COLUMN_SEPARATOR + timelineSeparator));
|
|
4385
4251
|
const todayWeekOffset = today.diff(startDate, "week");
|
|
4386
4252
|
const todayMarkerPos = todayWeekOffset * 8;
|
|
4387
4253
|
let todayMarker = " ".repeat(SPEC_COLUMN_WIDTH) + COLUMN_SEPARATOR;
|
|
4388
4254
|
if (todayMarkerPos >= 0 && todayMarkerPos < timelineColumnWidth) {
|
|
4389
4255
|
todayMarker += " ".repeat(todayMarkerPos) + "\u2502 Today";
|
|
4390
4256
|
}
|
|
4391
|
-
console.log(
|
|
4257
|
+
console.log(chalk16.dim(todayMarker));
|
|
4392
4258
|
console.log("");
|
|
4393
4259
|
const priorities = ["critical", "high", "medium", "low"];
|
|
4394
4260
|
for (const priority of priorities) {
|
|
@@ -4406,13 +4272,12 @@ async function ganttCommand(options) {
|
|
|
4406
4272
|
const summaryParts = [];
|
|
4407
4273
|
if (inProgress > 0) summaryParts.push(`${inProgress} in-progress`);
|
|
4408
4274
|
if (planned > 0) summaryParts.push(`${planned} planned`);
|
|
4409
|
-
if (overdue > 0) summaryParts.push(
|
|
4410
|
-
console.log(
|
|
4411
|
-
console.log(
|
|
4275
|
+
if (overdue > 0) summaryParts.push(chalk16.red(`${overdue} overdue`));
|
|
4276
|
+
console.log(chalk16.bold("Summary: ") + summaryParts.join(" \xB7 "));
|
|
4277
|
+
console.log(chalk16.dim('\u{1F4A1} Tip: Add "due: YYYY-MM-DD" to frontmatter for timeline planning'));
|
|
4412
4278
|
}
|
|
4413
4279
|
function renderSpecRow(spec, startDate, endDate, weeks, today) {
|
|
4414
4280
|
const statusConfig = STATUS_CONFIG2[spec.frontmatter.status];
|
|
4415
|
-
const timelineColumnWidth = weeks * 8;
|
|
4416
4281
|
const emoji = statusConfig.emoji;
|
|
4417
4282
|
const maxNameLength = SPEC_COLUMN_WIDTH - 2;
|
|
4418
4283
|
let specName = spec.name;
|
|
@@ -4422,7 +4287,7 @@ function renderSpecRow(spec, startDate, endDate, weeks, today) {
|
|
|
4422
4287
|
const specColumn = `${SPEC_INDENT}${emoji} ${specName}`.padEnd(SPEC_COLUMN_WIDTH);
|
|
4423
4288
|
let timelineColumn;
|
|
4424
4289
|
if (!spec.frontmatter.due) {
|
|
4425
|
-
timelineColumn =
|
|
4290
|
+
timelineColumn = chalk16.dim("(no due date set)");
|
|
4426
4291
|
} else {
|
|
4427
4292
|
timelineColumn = renderTimelineBar(spec, startDate, endDate, weeks, today);
|
|
4428
4293
|
}
|
|
@@ -4431,7 +4296,7 @@ function renderSpecRow(spec, startDate, endDate, weeks, today) {
|
|
|
4431
4296
|
function renderTimelineBar(spec, startDate, endDate, weeks, today) {
|
|
4432
4297
|
const charsPerWeek = 8;
|
|
4433
4298
|
const totalChars = weeks * charsPerWeek;
|
|
4434
|
-
const due =
|
|
4299
|
+
const due = dayjs2(spec.frontmatter.due);
|
|
4435
4300
|
const specStart = today;
|
|
4436
4301
|
const startDaysFromStart = specStart.diff(startDate, "day");
|
|
4437
4302
|
const dueDaysFromStart = due.diff(startDate, "day");
|
|
@@ -4445,13 +4310,13 @@ function renderTimelineBar(spec, startDate, endDate, weeks, today) {
|
|
|
4445
4310
|
result += " ".repeat(barStart);
|
|
4446
4311
|
}
|
|
4447
4312
|
if (spec.frontmatter.status === "complete") {
|
|
4448
|
-
result +=
|
|
4313
|
+
result += chalk16.green(FILLED_BAR_CHAR.repeat(barLength));
|
|
4449
4314
|
} else if (spec.frontmatter.status === "in-progress") {
|
|
4450
4315
|
const halfLength = Math.floor(barLength / 2);
|
|
4451
|
-
result +=
|
|
4452
|
-
result +=
|
|
4316
|
+
result += chalk16.yellow(FILLED_BAR_CHAR.repeat(halfLength));
|
|
4317
|
+
result += chalk16.dim(EMPTY_BAR_CHAR.repeat(barLength - halfLength));
|
|
4453
4318
|
} else {
|
|
4454
|
-
result +=
|
|
4319
|
+
result += chalk16.dim(EMPTY_BAR_CHAR.repeat(barLength));
|
|
4455
4320
|
}
|
|
4456
4321
|
const trailingSpace = totalChars - barEnd;
|
|
4457
4322
|
if (trailingSpace > 0) {
|
|
@@ -4459,18 +4324,157 @@ function renderTimelineBar(spec, startDate, endDate, weeks, today) {
|
|
|
4459
4324
|
}
|
|
4460
4325
|
return result;
|
|
4461
4326
|
}
|
|
4462
|
-
|
|
4463
|
-
|
|
4464
|
-
|
|
4465
|
-
|
|
4466
|
-
|
|
4467
|
-
|
|
4468
|
-
|
|
4469
|
-
|
|
4327
|
+
async function tokensCommand(specPath, options = {}) {
|
|
4328
|
+
await autoCheckIfEnabled();
|
|
4329
|
+
const counter = new TokenCounter();
|
|
4330
|
+
try {
|
|
4331
|
+
const config = await loadConfig();
|
|
4332
|
+
const cwd = process.cwd();
|
|
4333
|
+
const specsDir = path2.join(cwd, config.specsDir);
|
|
4334
|
+
const resolvedPath = await resolveSpecPath(specPath, cwd, specsDir);
|
|
4335
|
+
if (!resolvedPath) {
|
|
4336
|
+
throw new Error(`Spec not found: ${sanitizeUserInput(specPath)}`);
|
|
4337
|
+
}
|
|
4338
|
+
const specName = path2.basename(resolvedPath);
|
|
4339
|
+
const result = await counter.countSpec(resolvedPath, {
|
|
4340
|
+
detailed: options.detailed,
|
|
4341
|
+
includeSubSpecs: options.includeSubSpecs
|
|
4342
|
+
});
|
|
4343
|
+
if (options.json) {
|
|
4344
|
+
console.log(JSON.stringify({
|
|
4345
|
+
spec: specName,
|
|
4346
|
+
path: resolvedPath,
|
|
4347
|
+
...result
|
|
4348
|
+
}, null, 2));
|
|
4349
|
+
return;
|
|
4350
|
+
}
|
|
4351
|
+
console.log(chalk16.bold.cyan(`\u{1F4CA} Token Count: ${specName}`));
|
|
4352
|
+
console.log("");
|
|
4353
|
+
const indicators = counter.getPerformanceIndicators(result.total);
|
|
4354
|
+
const levelEmoji = indicators.level === "excellent" ? "\u2705" : indicators.level === "good" ? "\u{1F44D}" : indicators.level === "warning" ? "\u26A0\uFE0F" : "\u{1F534}";
|
|
4355
|
+
console.log(` Total: ${chalk16.cyan(result.total.toLocaleString())} tokens ${levelEmoji}`);
|
|
4356
|
+
console.log("");
|
|
4357
|
+
if (result.files.length > 1 || options.detailed) {
|
|
4358
|
+
console.log(chalk16.bold("Files:"));
|
|
4359
|
+
console.log("");
|
|
4360
|
+
for (const file of result.files) {
|
|
4361
|
+
const lineInfo = file.lines ? chalk16.dim(` (${file.lines} lines)`) : "";
|
|
4362
|
+
console.log(` ${file.path.padEnd(25)} ${chalk16.cyan(file.tokens.toLocaleString().padStart(6))} tokens${lineInfo}`);
|
|
4363
|
+
}
|
|
4364
|
+
console.log("");
|
|
4365
|
+
}
|
|
4366
|
+
if (options.detailed && result.breakdown) {
|
|
4367
|
+
const b = result.breakdown;
|
|
4368
|
+
const total = b.code + b.prose + b.tables + b.frontmatter;
|
|
4369
|
+
console.log(chalk16.bold("Content Breakdown:"));
|
|
4370
|
+
console.log("");
|
|
4371
|
+
console.log(` Prose ${chalk16.cyan(b.prose.toLocaleString().padStart(6))} tokens ${chalk16.dim(`(${Math.round(b.prose / total * 100)}%)`)}`);
|
|
4372
|
+
console.log(` Code ${chalk16.cyan(b.code.toLocaleString().padStart(6))} tokens ${chalk16.dim(`(${Math.round(b.code / total * 100)}%)`)}`);
|
|
4373
|
+
console.log(` Tables ${chalk16.cyan(b.tables.toLocaleString().padStart(6))} tokens ${chalk16.dim(`(${Math.round(b.tables / total * 100)}%)`)}`);
|
|
4374
|
+
console.log(` Frontmatter ${chalk16.cyan(b.frontmatter.toLocaleString().padStart(6))} tokens ${chalk16.dim(`(${Math.round(b.frontmatter / total * 100)}%)`)}`);
|
|
4375
|
+
console.log("");
|
|
4376
|
+
}
|
|
4377
|
+
console.log(chalk16.bold("Performance Indicators:"));
|
|
4378
|
+
console.log("");
|
|
4379
|
+
const costColor = indicators.costMultiplier < 2 ? chalk16.green : indicators.costMultiplier < 4 ? chalk16.yellow : chalk16.red;
|
|
4380
|
+
const effectivenessColor = indicators.effectiveness >= 95 ? chalk16.green : indicators.effectiveness >= 85 ? chalk16.yellow : chalk16.red;
|
|
4381
|
+
console.log(` Cost multiplier: ${costColor(`${indicators.costMultiplier}x`)} ${chalk16.dim("vs 1,200 token baseline")}`);
|
|
4382
|
+
console.log(` AI effectiveness: ${effectivenessColor(`~${indicators.effectiveness}%`)} ${chalk16.dim("(hypothesis)")}`);
|
|
4383
|
+
console.log(` Context Economy: ${levelEmoji} ${indicators.recommendation}`);
|
|
4384
|
+
console.log("");
|
|
4385
|
+
if (!options.includeSubSpecs && result.files.length === 1) {
|
|
4386
|
+
console.log(chalk16.dim("\u{1F4A1} Use `--include-sub-specs` to count all sub-spec files"));
|
|
4387
|
+
}
|
|
4388
|
+
} finally {
|
|
4389
|
+
counter.dispose();
|
|
4390
|
+
}
|
|
4391
|
+
}
|
|
4392
|
+
async function tokensAllCommand(options = {}) {
|
|
4393
|
+
await autoCheckIfEnabled();
|
|
4394
|
+
const specs = await withSpinner(
|
|
4395
|
+
"Loading specs...",
|
|
4396
|
+
() => loadAllSpecs({ includeArchived: false })
|
|
4397
|
+
);
|
|
4398
|
+
if (specs.length === 0) {
|
|
4399
|
+
console.log("No specs found.");
|
|
4400
|
+
return;
|
|
4401
|
+
}
|
|
4402
|
+
const counter = new TokenCounter();
|
|
4403
|
+
const results = [];
|
|
4404
|
+
try {
|
|
4405
|
+
for (const spec of specs) {
|
|
4406
|
+
const result = await counter.countSpec(spec.fullPath, {
|
|
4407
|
+
includeSubSpecs: options.includeSubSpecs
|
|
4408
|
+
});
|
|
4409
|
+
const indicators = counter.getPerformanceIndicators(result.total);
|
|
4410
|
+
const totalLines = result.files.reduce((sum, f) => sum + (f.lines || 0), 0);
|
|
4411
|
+
results.push({
|
|
4412
|
+
name: spec.name,
|
|
4413
|
+
path: spec.fullPath,
|
|
4414
|
+
tokens: result.total,
|
|
4415
|
+
lines: totalLines,
|
|
4416
|
+
level: indicators.level
|
|
4417
|
+
});
|
|
4418
|
+
}
|
|
4419
|
+
} finally {
|
|
4420
|
+
counter.dispose();
|
|
4421
|
+
}
|
|
4422
|
+
const sortBy = options.sortBy || "tokens";
|
|
4423
|
+
results.sort((a, b) => {
|
|
4424
|
+
if (sortBy === "tokens") return b.tokens - a.tokens;
|
|
4425
|
+
if (sortBy === "lines") return b.lines - a.lines;
|
|
4426
|
+
return a.name.localeCompare(b.name);
|
|
4427
|
+
});
|
|
4428
|
+
if (options.json) {
|
|
4429
|
+
console.log(JSON.stringify(results, null, 2));
|
|
4430
|
+
return;
|
|
4431
|
+
}
|
|
4432
|
+
console.log(chalk16.bold.cyan("\u{1F4CA} Token Counts"));
|
|
4433
|
+
console.log("");
|
|
4434
|
+
console.log(chalk16.dim(`Sorted by: ${sortBy}`));
|
|
4435
|
+
console.log("");
|
|
4436
|
+
const totalTokens = results.reduce((sum, r) => sum + r.tokens, 0);
|
|
4437
|
+
const avgTokens = Math.round(totalTokens / results.length);
|
|
4438
|
+
const warningCount = results.filter((r) => r.level === "warning" || r.level === "problem").length;
|
|
4439
|
+
console.log(chalk16.bold("Summary:"));
|
|
4440
|
+
console.log("");
|
|
4441
|
+
console.log(` Total specs: ${chalk16.cyan(results.length)}`);
|
|
4442
|
+
console.log(` Total tokens: ${chalk16.cyan(totalTokens.toLocaleString())}`);
|
|
4443
|
+
console.log(` Average tokens: ${chalk16.cyan(avgTokens.toLocaleString())}`);
|
|
4444
|
+
if (warningCount > 0) {
|
|
4445
|
+
console.log(` Needs review: ${chalk16.yellow(warningCount)} specs ${chalk16.dim("(\u26A0\uFE0F or \u{1F534})")}`);
|
|
4446
|
+
}
|
|
4447
|
+
console.log("");
|
|
4448
|
+
const nameCol = 35;
|
|
4449
|
+
const tokensCol = 10;
|
|
4450
|
+
const linesCol = 8;
|
|
4451
|
+
console.log(chalk16.bold(
|
|
4452
|
+
"Spec".padEnd(nameCol) + "Tokens".padStart(tokensCol) + "Lines".padStart(linesCol) + " Status"
|
|
4453
|
+
));
|
|
4454
|
+
console.log(chalk16.dim("\u2500".repeat(nameCol + tokensCol + linesCol + 10)));
|
|
4455
|
+
const displayCount = options.all ? results.length : Math.min(20, results.length);
|
|
4456
|
+
for (let i = 0; i < displayCount; i++) {
|
|
4457
|
+
const r = results[i];
|
|
4458
|
+
const emoji = r.level === "excellent" ? "\u2705" : r.level === "good" ? "\u{1F44D}" : r.level === "warning" ? "\u26A0\uFE0F" : "\u{1F534}";
|
|
4459
|
+
const tokensColor = r.level === "excellent" || r.level === "good" ? chalk16.cyan : r.level === "warning" ? chalk16.yellow : chalk16.red;
|
|
4460
|
+
const name = r.name.length > nameCol - 2 ? r.name.substring(0, nameCol - 3) + "\u2026" : r.name;
|
|
4461
|
+
console.log(
|
|
4462
|
+
name.padEnd(nameCol) + tokensColor(r.tokens.toLocaleString().padStart(tokensCol)) + chalk16.dim(r.lines.toString().padStart(linesCol)) + ` ${emoji}`
|
|
4463
|
+
);
|
|
4464
|
+
}
|
|
4465
|
+
if (results.length > displayCount) {
|
|
4466
|
+
console.log("");
|
|
4467
|
+
console.log(chalk16.dim(`... and ${results.length - displayCount} more specs`));
|
|
4468
|
+
console.log(chalk16.dim(`Use --all to show all specs`));
|
|
4469
|
+
}
|
|
4470
|
+
console.log("");
|
|
4471
|
+
console.log(chalk16.dim("Legend: \u2705 excellent (<2K) | \u{1F44D} good (<3.5K) | \u26A0\uFE0F warning (<5K) | \u{1F534} problem (>5K)"));
|
|
4472
|
+
console.log("");
|
|
4473
|
+
}
|
|
4470
4474
|
marked.use(markedTerminal());
|
|
4471
4475
|
async function readSpecContent(specPath, cwd = process.cwd()) {
|
|
4472
4476
|
const config = await loadConfig(cwd);
|
|
4473
|
-
const specsDir =
|
|
4477
|
+
const specsDir = path2.join(cwd, config.specsDir);
|
|
4474
4478
|
let resolvedPath = null;
|
|
4475
4479
|
let targetFile = null;
|
|
4476
4480
|
const pathParts = specPath.split("/").filter((p) => p);
|
|
@@ -4479,9 +4483,9 @@ async function readSpecContent(specPath, cwd = process.cwd()) {
|
|
|
4479
4483
|
const filePart = pathParts[pathParts.length - 1];
|
|
4480
4484
|
resolvedPath = await resolveSpecPath(specPart, cwd, specsDir);
|
|
4481
4485
|
if (resolvedPath) {
|
|
4482
|
-
targetFile =
|
|
4486
|
+
targetFile = path2.join(resolvedPath, filePart);
|
|
4483
4487
|
try {
|
|
4484
|
-
await
|
|
4488
|
+
await fs9.access(targetFile);
|
|
4485
4489
|
} catch {
|
|
4486
4490
|
return null;
|
|
4487
4491
|
}
|
|
@@ -4500,8 +4504,8 @@ async function readSpecContent(specPath, cwd = process.cwd()) {
|
|
|
4500
4504
|
if (!targetFile) {
|
|
4501
4505
|
return null;
|
|
4502
4506
|
}
|
|
4503
|
-
const rawContent = await
|
|
4504
|
-
const fileName =
|
|
4507
|
+
const rawContent = await fs9.readFile(targetFile, "utf-8");
|
|
4508
|
+
const fileName = path2.basename(targetFile);
|
|
4505
4509
|
const isSubSpec = fileName !== config.structure.defaultFile;
|
|
4506
4510
|
let frontmatter = null;
|
|
4507
4511
|
if (!isSubSpec) {
|
|
@@ -4530,7 +4534,7 @@ async function readSpecContent(specPath, cwd = process.cwd()) {
|
|
|
4530
4534
|
}
|
|
4531
4535
|
}
|
|
4532
4536
|
const content = lines.slice(contentStartIndex).join("\n").trim();
|
|
4533
|
-
const specName =
|
|
4537
|
+
const specName = path2.basename(resolvedPath);
|
|
4534
4538
|
const displayName = isSubSpec ? `${specName}/${fileName}` : specName;
|
|
4535
4539
|
return {
|
|
4536
4540
|
frontmatter,
|
|
@@ -4551,7 +4555,7 @@ function formatFrontmatter(frontmatter) {
|
|
|
4551
4555
|
archived: "\u{1F4E6}"
|
|
4552
4556
|
};
|
|
4553
4557
|
const statusEmoji = statusEmojis[frontmatter.status] || "\u{1F4C4}";
|
|
4554
|
-
lines.push(
|
|
4558
|
+
lines.push(chalk16.bold(`${statusEmoji} Status: `) + chalk16.cyan(frontmatter.status));
|
|
4555
4559
|
if (frontmatter.priority) {
|
|
4556
4560
|
const priorityEmojis = {
|
|
4557
4561
|
low: "\u{1F7E2}",
|
|
@@ -4560,25 +4564,25 @@ function formatFrontmatter(frontmatter) {
|
|
|
4560
4564
|
critical: "\u{1F534}"
|
|
4561
4565
|
};
|
|
4562
4566
|
const priorityEmoji = priorityEmojis[frontmatter.priority] || "";
|
|
4563
|
-
lines.push(
|
|
4567
|
+
lines.push(chalk16.bold(`${priorityEmoji} Priority: `) + chalk16.yellow(frontmatter.priority));
|
|
4564
4568
|
}
|
|
4565
4569
|
if (frontmatter.created) {
|
|
4566
|
-
lines.push(
|
|
4570
|
+
lines.push(chalk16.bold("\u{1F4C6} Created: ") + chalk16.gray(String(frontmatter.created)));
|
|
4567
4571
|
}
|
|
4568
4572
|
if (frontmatter.tags && frontmatter.tags.length > 0) {
|
|
4569
|
-
const tagStr = frontmatter.tags.map((tag) =>
|
|
4570
|
-
lines.push(
|
|
4573
|
+
const tagStr = frontmatter.tags.map((tag) => chalk16.blue(`#${tag}`)).join(" ");
|
|
4574
|
+
lines.push(chalk16.bold("\u{1F3F7}\uFE0F Tags: ") + tagStr);
|
|
4571
4575
|
}
|
|
4572
4576
|
if (frontmatter.assignee) {
|
|
4573
|
-
lines.push(
|
|
4577
|
+
lines.push(chalk16.bold("\u{1F464} Assignee: ") + chalk16.green(frontmatter.assignee));
|
|
4574
4578
|
}
|
|
4575
4579
|
const standardFields = ["status", "priority", "created", "tags", "assignee"];
|
|
4576
4580
|
const customFields = Object.entries(frontmatter).filter(([key]) => !standardFields.includes(key)).filter(([_, value]) => value !== void 0 && value !== null);
|
|
4577
4581
|
if (customFields.length > 0) {
|
|
4578
4582
|
lines.push("");
|
|
4579
|
-
lines.push(
|
|
4583
|
+
lines.push(chalk16.bold("Custom Fields:"));
|
|
4580
4584
|
for (const [key, value] of customFields) {
|
|
4581
|
-
lines.push(` ${
|
|
4585
|
+
lines.push(` ${chalk16.gray(key)}: ${chalk16.white(String(value))}`);
|
|
4582
4586
|
}
|
|
4583
4587
|
}
|
|
4584
4588
|
return lines.join("\n");
|
|
@@ -4586,11 +4590,11 @@ function formatFrontmatter(frontmatter) {
|
|
|
4586
4590
|
function displayFormattedSpec(spec) {
|
|
4587
4591
|
const output = [];
|
|
4588
4592
|
output.push("");
|
|
4589
|
-
output.push(
|
|
4593
|
+
output.push(chalk16.bold.cyan(`\u2501\u2501\u2501 ${spec.name} \u2501\u2501\u2501`));
|
|
4590
4594
|
output.push("");
|
|
4591
4595
|
output.push(formatFrontmatter(spec.frontmatter));
|
|
4592
4596
|
output.push("");
|
|
4593
|
-
output.push(
|
|
4597
|
+
output.push(chalk16.gray("\u2500".repeat(60)));
|
|
4594
4598
|
output.push("");
|
|
4595
4599
|
return output.join("\n");
|
|
4596
4600
|
}
|
|
@@ -4620,7 +4624,7 @@ async function viewCommand(specPath, options = {}) {
|
|
|
4620
4624
|
async function openCommand(specPath, options = {}) {
|
|
4621
4625
|
const cwd = process.cwd();
|
|
4622
4626
|
const config = await loadConfig(cwd);
|
|
4623
|
-
const specsDir =
|
|
4627
|
+
const specsDir = path2.join(cwd, config.specsDir);
|
|
4624
4628
|
let resolvedPath = null;
|
|
4625
4629
|
let targetFile = null;
|
|
4626
4630
|
const pathParts = specPath.split("/").filter((p) => p);
|
|
@@ -4629,9 +4633,9 @@ async function openCommand(specPath, options = {}) {
|
|
|
4629
4633
|
const filePart = pathParts[pathParts.length - 1];
|
|
4630
4634
|
resolvedPath = await resolveSpecPath(specPart, cwd, specsDir);
|
|
4631
4635
|
if (resolvedPath) {
|
|
4632
|
-
targetFile =
|
|
4636
|
+
targetFile = path2.join(resolvedPath, filePart);
|
|
4633
4637
|
try {
|
|
4634
|
-
await
|
|
4638
|
+
await fs9.access(targetFile);
|
|
4635
4639
|
} catch {
|
|
4636
4640
|
targetFile = null;
|
|
4637
4641
|
}
|
|
@@ -4649,7 +4653,6 @@ async function openCommand(specPath, options = {}) {
|
|
|
4649
4653
|
} else if (!targetFile) {
|
|
4650
4654
|
throw new Error(`Sub-spec file not found: ${specPath}`);
|
|
4651
4655
|
}
|
|
4652
|
-
const specFile = targetFile;
|
|
4653
4656
|
let editor = options.editor;
|
|
4654
4657
|
if (!editor) {
|
|
4655
4658
|
editor = process.env.VISUAL || process.env.EDITOR;
|
|
@@ -4664,7 +4667,7 @@ async function openCommand(specPath, options = {}) {
|
|
|
4664
4667
|
editor = "xdg-open";
|
|
4665
4668
|
}
|
|
4666
4669
|
}
|
|
4667
|
-
console.log(
|
|
4670
|
+
console.log(chalk16.gray(`Opening ${targetFile} with ${editor}...`));
|
|
4668
4671
|
const child = spawn(editor, [targetFile], {
|
|
4669
4672
|
stdio: "inherit",
|
|
4670
4673
|
shell: true
|
|
@@ -4694,9 +4697,6 @@ async function openCommand(specPath, options = {}) {
|
|
|
4694
4697
|
});
|
|
4695
4698
|
}
|
|
4696
4699
|
}
|
|
4697
|
-
|
|
4698
|
-
// src/commands/mcp.ts
|
|
4699
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4700
4700
|
async function mcpCommand() {
|
|
4701
4701
|
try {
|
|
4702
4702
|
const server = await createMcpServer();
|
|
@@ -4708,15 +4708,10 @@ async function mcpCommand() {
|
|
|
4708
4708
|
process.exit(1);
|
|
4709
4709
|
}
|
|
4710
4710
|
}
|
|
4711
|
-
|
|
4712
|
-
|
|
4713
|
-
import { readFileSync } from "fs";
|
|
4714
|
-
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
4715
|
-
import { dirname as dirname3, join as join18 } from "path";
|
|
4716
|
-
var __filename2 = fileURLToPath2(import.meta.url);
|
|
4717
|
-
var __dirname3 = dirname3(__filename2);
|
|
4711
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
4712
|
+
var __dirname2 = dirname(__filename);
|
|
4718
4713
|
var packageJson = JSON.parse(
|
|
4719
|
-
readFileSync(
|
|
4714
|
+
readFileSync(join(__dirname2, "../package.json"), "utf-8")
|
|
4720
4715
|
);
|
|
4721
4716
|
function formatErrorMessage(prefix, error) {
|
|
4722
4717
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
@@ -5626,31 +5621,6 @@ Please search for this topic and show me the dependencies between related specs.
|
|
|
5626
5621
|
return server;
|
|
5627
5622
|
}
|
|
5628
5623
|
|
|
5629
|
-
export {
|
|
5630
|
-
|
|
5631
|
-
|
|
5632
|
-
archiveSpec,
|
|
5633
|
-
listSpecs,
|
|
5634
|
-
updateSpec,
|
|
5635
|
-
backfillTimestamps,
|
|
5636
|
-
listTemplates,
|
|
5637
|
-
showTemplate,
|
|
5638
|
-
addTemplate,
|
|
5639
|
-
removeTemplate,
|
|
5640
|
-
copyTemplate,
|
|
5641
|
-
initProject,
|
|
5642
|
-
filesCommand,
|
|
5643
|
-
validateCommand,
|
|
5644
|
-
migrateCommand,
|
|
5645
|
-
boardCommand,
|
|
5646
|
-
statsCommand,
|
|
5647
|
-
searchCommand,
|
|
5648
|
-
depsCommand,
|
|
5649
|
-
timelineCommand,
|
|
5650
|
-
ganttCommand,
|
|
5651
|
-
viewCommand,
|
|
5652
|
-
openCommand,
|
|
5653
|
-
createMcpServer,
|
|
5654
|
-
mcpCommand
|
|
5655
|
-
};
|
|
5656
|
-
//# sourceMappingURL=chunk-J7ZSZ5VJ.js.map
|
|
5624
|
+
export { addTemplate, archiveSpec, backfillTimestamps, boardCommand, checkSpecs, copyTemplate, createMcpServer, createSpec, depsCommand, filesCommand, ganttCommand, initProject, listSpecs, listTemplates, mcpCommand, migrateCommand, openCommand, removeTemplate, searchCommand, showTemplate, statsCommand, timelineCommand, tokensAllCommand, tokensCommand, updateSpec, validateCommand, viewCommand };
|
|
5625
|
+
//# sourceMappingURL=chunk-ER23B6KS.js.map
|
|
5626
|
+
//# sourceMappingURL=chunk-ER23B6KS.js.map
|