lean-spec 0.2.9-dev.20251205030455 → 0.2.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +120 -0
- package/dist/{chunk-BJHJ6IUO.js → chunk-KTNU4LUR.js} +1096 -827
- package/dist/chunk-KTNU4LUR.js.map +1 -0
- package/dist/cli.js +1 -1
- package/dist/mcp-server.js +1 -1
- package/package.json +2 -2
- package/dist/chunk-BJHJ6IUO.js.map +0 -1
|
@@ -6,12 +6,12 @@ import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mc
|
|
|
6
6
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
7
7
|
import { readFileSync, existsSync } from 'fs';
|
|
8
8
|
import { fileURLToPath } from 'url';
|
|
9
|
-
import * as
|
|
9
|
+
import * as path13 from 'path';
|
|
10
10
|
import { dirname, join, resolve } from 'path';
|
|
11
11
|
import { z } from 'zod';
|
|
12
|
-
import * as
|
|
12
|
+
import * as fs9 from 'fs/promises';
|
|
13
13
|
import { readFile, writeFile } from 'fs/promises';
|
|
14
|
-
import
|
|
14
|
+
import chalk16 from 'chalk';
|
|
15
15
|
import { Command } from 'commander';
|
|
16
16
|
import { spawn, execSync } from 'child_process';
|
|
17
17
|
import matter from 'gray-matter';
|
|
@@ -31,12 +31,12 @@ function checkCommand() {
|
|
|
31
31
|
async function checkSpecs(options = {}) {
|
|
32
32
|
const config = await loadConfig();
|
|
33
33
|
const cwd = process.cwd();
|
|
34
|
-
|
|
34
|
+
path13.join(cwd, config.specsDir);
|
|
35
35
|
const specs = await loadAllSpecs();
|
|
36
36
|
const sequenceMap = /* @__PURE__ */ new Map();
|
|
37
37
|
const specPattern = createSpecDirPattern();
|
|
38
38
|
for (const spec of specs) {
|
|
39
|
-
const specName =
|
|
39
|
+
const specName = path13.basename(spec.path);
|
|
40
40
|
const match = specName.match(specPattern);
|
|
41
41
|
if (match) {
|
|
42
42
|
const seq = parseInt(match[1], 10);
|
|
@@ -54,7 +54,7 @@ async function checkSpecs(options = {}) {
|
|
|
54
54
|
if (options.json) {
|
|
55
55
|
console.log(JSON.stringify({ conflicts: [], hasConflicts: false }, null, 2));
|
|
56
56
|
} else {
|
|
57
|
-
console.log(
|
|
57
|
+
console.log(chalk16.green("\u2713 No sequence conflicts detected"));
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
60
|
return true;
|
|
@@ -73,23 +73,23 @@ async function checkSpecs(options = {}) {
|
|
|
73
73
|
if (!options.silent) {
|
|
74
74
|
if (!options.quiet) {
|
|
75
75
|
console.log("");
|
|
76
|
-
console.log(
|
|
76
|
+
console.log(chalk16.yellow("\u26A0\uFE0F Sequence conflicts detected:\n"));
|
|
77
77
|
for (const [seq, paths] of conflicts) {
|
|
78
|
-
console.log(
|
|
78
|
+
console.log(chalk16.red(` Sequence ${String(seq).padStart(config.structure.sequenceDigits, "0")}:`));
|
|
79
79
|
for (const p of paths) {
|
|
80
|
-
console.log(
|
|
80
|
+
console.log(chalk16.gray(` - ${sanitizeUserInput(p)}`));
|
|
81
81
|
}
|
|
82
82
|
console.log("");
|
|
83
83
|
}
|
|
84
|
-
console.log(
|
|
85
|
-
console.log(
|
|
84
|
+
console.log(chalk16.cyan("Tip: Use date prefix to prevent conflicts:"));
|
|
85
|
+
console.log(chalk16.gray(' Edit .lean-spec/config.json \u2192 structure.prefix: "{YYYYMMDD}-"'));
|
|
86
86
|
console.log("");
|
|
87
|
-
console.log(
|
|
87
|
+
console.log(chalk16.cyan("Or rename folders manually to resolve."));
|
|
88
88
|
console.log("");
|
|
89
89
|
} else {
|
|
90
90
|
console.log("");
|
|
91
|
-
console.log(
|
|
92
|
-
console.log(
|
|
91
|
+
console.log(chalk16.yellow(`\u26A0\uFE0F Conflict warning: ${conflicts.length} sequence conflict(s) detected`));
|
|
92
|
+
console.log(chalk16.gray("Run: lean-spec check"));
|
|
93
93
|
console.log("");
|
|
94
94
|
}
|
|
95
95
|
}
|
|
@@ -149,7 +149,7 @@ async function updateSpec(specPath, updates, options = {}) {
|
|
|
149
149
|
await autoCheckIfEnabled();
|
|
150
150
|
const cwd = options.cwd ?? process.cwd();
|
|
151
151
|
const config = await loadConfig(cwd);
|
|
152
|
-
const specsDir =
|
|
152
|
+
const specsDir = path13.join(cwd, config.specsDir);
|
|
153
153
|
const resolvedPath = await resolveSpecPath(specPath, cwd, specsDir);
|
|
154
154
|
if (!resolvedPath) {
|
|
155
155
|
throw new Error(`Spec not found: ${sanitizeUserInput(specPath)}. Tried: ${sanitizeUserInput(specPath)}, specs/${sanitizeUserInput(specPath)}, and searching in date directories`);
|
|
@@ -171,12 +171,12 @@ async function updateSpec(specPath, updates, options = {}) {
|
|
|
171
171
|
});
|
|
172
172
|
}
|
|
173
173
|
await updateFrontmatter(specFile, allUpdates);
|
|
174
|
-
console.log(
|
|
174
|
+
console.log(chalk16.green(`\u2713 Updated: ${sanitizeUserInput(path13.relative(cwd, resolvedPath))}`));
|
|
175
175
|
const updatedFields = Object.keys(updates).filter((k) => k !== "customFields");
|
|
176
176
|
if (updates.customFields) {
|
|
177
177
|
updatedFields.push(...Object.keys(updates.customFields));
|
|
178
178
|
}
|
|
179
|
-
console.log(
|
|
179
|
+
console.log(chalk16.gray(` Fields: ${updatedFields.join(", ")}`));
|
|
180
180
|
}
|
|
181
181
|
|
|
182
182
|
// src/commands/agent.ts
|
|
@@ -251,10 +251,10 @@ async function isAgentAvailable(agentConfig) {
|
|
|
251
251
|
}
|
|
252
252
|
const command = agentConfig.command;
|
|
253
253
|
if (!command) return false;
|
|
254
|
-
return new Promise((
|
|
254
|
+
return new Promise((resolve3) => {
|
|
255
255
|
const child = spawn("which", [command], { stdio: "pipe" });
|
|
256
|
-
child.on("close", (code) =>
|
|
257
|
-
child.on("error", () =>
|
|
256
|
+
child.on("close", (code) => resolve3(code === 0));
|
|
257
|
+
child.on("error", () => resolve3(false));
|
|
258
258
|
});
|
|
259
259
|
}
|
|
260
260
|
async function loadSpecContent(specPath) {
|
|
@@ -265,11 +265,11 @@ async function loadSpecContent(specPath) {
|
|
|
265
265
|
const specDir = spec.fullPath;
|
|
266
266
|
let content = "";
|
|
267
267
|
try {
|
|
268
|
-
const files = await
|
|
268
|
+
const files = await fs9.readdir(specDir);
|
|
269
269
|
const mdFiles = files.filter((f) => f.endsWith(".md"));
|
|
270
270
|
for (const file of mdFiles) {
|
|
271
|
-
const filePath =
|
|
272
|
-
const fileContent = await
|
|
271
|
+
const filePath = path13.join(specDir, file);
|
|
272
|
+
const fileContent = await fs9.readFile(filePath, "utf-8");
|
|
273
273
|
content += `
|
|
274
274
|
|
|
275
275
|
### ${file}
|
|
@@ -277,15 +277,15 @@ async function loadSpecContent(specPath) {
|
|
|
277
277
|
${fileContent}`;
|
|
278
278
|
}
|
|
279
279
|
} catch {
|
|
280
|
-
content = await
|
|
280
|
+
content = await fs9.readFile(spec.filePath, "utf-8");
|
|
281
281
|
}
|
|
282
282
|
return content;
|
|
283
283
|
}
|
|
284
284
|
async function createWorktree(specPath, specName, cwd) {
|
|
285
|
-
const worktreePath =
|
|
285
|
+
const worktreePath = path13.join(cwd, ".worktrees", `spec-${specName}`);
|
|
286
286
|
const branchName = `feature/${specName}`;
|
|
287
|
-
await
|
|
288
|
-
return new Promise((
|
|
287
|
+
await fs9.mkdir(path13.join(cwd, ".worktrees"), { recursive: true });
|
|
288
|
+
return new Promise((resolve3, reject) => {
|
|
289
289
|
const child = spawn("git", ["worktree", "add", worktreePath, "-b", branchName], {
|
|
290
290
|
cwd,
|
|
291
291
|
stdio: "pipe"
|
|
@@ -296,7 +296,7 @@ async function createWorktree(specPath, specName, cwd) {
|
|
|
296
296
|
});
|
|
297
297
|
child.on("close", (code) => {
|
|
298
298
|
if (code === 0) {
|
|
299
|
-
|
|
299
|
+
resolve3(worktreePath);
|
|
300
300
|
} else {
|
|
301
301
|
if (stderr.includes("already exists")) {
|
|
302
302
|
const child2 = spawn("git", ["worktree", "add", worktreePath, branchName], {
|
|
@@ -305,7 +305,7 @@ async function createWorktree(specPath, specName, cwd) {
|
|
|
305
305
|
});
|
|
306
306
|
child2.on("close", (code2) => {
|
|
307
307
|
if (code2 === 0) {
|
|
308
|
-
|
|
308
|
+
resolve3(worktreePath);
|
|
309
309
|
} else {
|
|
310
310
|
reject(new Error(`Failed to create worktree: ${stderr}`));
|
|
311
311
|
}
|
|
@@ -333,17 +333,17 @@ async function runCliAgent(specPath, agentConfig, worktreePath) {
|
|
|
333
333
|
return child;
|
|
334
334
|
}
|
|
335
335
|
async function runCloudAgent(specPath, _agentConfig) {
|
|
336
|
-
console.log(
|
|
337
|
-
console.log(
|
|
336
|
+
console.log(chalk16.yellow("Cloud agent integration requires GitHub API configuration."));
|
|
337
|
+
console.log(chalk16.gray("The spec content has been prepared for manual dispatch."));
|
|
338
338
|
const content = await loadSpecContent(specPath);
|
|
339
339
|
console.log("");
|
|
340
|
-
console.log(
|
|
340
|
+
console.log(chalk16.cyan("=== Spec Content for Cloud Agent ==="));
|
|
341
341
|
console.log(content.substring(0, 500) + (content.length > 500 ? "..." : ""));
|
|
342
342
|
console.log("");
|
|
343
|
-
console.log(
|
|
344
|
-
console.log(
|
|
345
|
-
console.log(
|
|
346
|
-
console.log(
|
|
343
|
+
console.log(chalk16.gray("To use GitHub Coding Agent:"));
|
|
344
|
+
console.log(chalk16.gray(" 1. Create a GitHub Issue with the spec content"));
|
|
345
|
+
console.log(chalk16.gray(" 2. Assign to the GitHub Coding Agent"));
|
|
346
|
+
console.log(chalk16.gray(" 3. The agent will create a PR automatically"));
|
|
347
347
|
}
|
|
348
348
|
function agentCommand() {
|
|
349
349
|
const cmd = new Command("agent").description("Dispatch specs to AI coding agents for automated implementation");
|
|
@@ -367,50 +367,50 @@ function agentCommand() {
|
|
|
367
367
|
async function runAgent(specs, options = {}) {
|
|
368
368
|
const config = await loadConfig();
|
|
369
369
|
const cwd = process.cwd();
|
|
370
|
-
const specsDir =
|
|
370
|
+
const specsDir = path13.join(cwd, config.specsDir);
|
|
371
371
|
const agentName = options.agent || await getDefaultAgent(config);
|
|
372
372
|
const agentConfig = await getAgentConfig(agentName, config);
|
|
373
373
|
if (!agentConfig) {
|
|
374
|
-
console.error(
|
|
375
|
-
console.log(
|
|
374
|
+
console.error(chalk16.red(`Unknown agent: ${agentName}`));
|
|
375
|
+
console.log(chalk16.gray("Available agents: claude, copilot, aider, gemini, gh-coding"));
|
|
376
376
|
process.exit(1);
|
|
377
377
|
}
|
|
378
378
|
const available = await isAgentAvailable(agentConfig);
|
|
379
379
|
if (!available && agentConfig.type === "cli" && !options.dryRun) {
|
|
380
|
-
console.error(
|
|
381
|
-
console.log(
|
|
380
|
+
console.error(chalk16.red(`Agent not found: ${agentConfig.command}`));
|
|
381
|
+
console.log(chalk16.gray(`Make sure ${agentConfig.command} is installed and in your PATH.`));
|
|
382
382
|
process.exit(1);
|
|
383
383
|
}
|
|
384
384
|
console.log("");
|
|
385
|
-
console.log(
|
|
385
|
+
console.log(chalk16.green(`\u{1F916} Dispatching to ${chalk16.cyan(agentName)} agent`));
|
|
386
386
|
console.log("");
|
|
387
387
|
const resolvedSpecs = [];
|
|
388
388
|
for (const spec of specs) {
|
|
389
389
|
const resolved = await resolveSpecPath(spec, cwd, specsDir);
|
|
390
390
|
if (!resolved) {
|
|
391
|
-
console.error(
|
|
391
|
+
console.error(chalk16.red(`Spec not found: ${sanitizeUserInput(spec)}`));
|
|
392
392
|
process.exit(1);
|
|
393
393
|
}
|
|
394
394
|
resolvedSpecs.push(resolved);
|
|
395
395
|
}
|
|
396
|
-
console.log(
|
|
396
|
+
console.log(chalk16.bold("Specs to process:"));
|
|
397
397
|
for (const specPath of resolvedSpecs) {
|
|
398
398
|
const spec = await getSpec(specPath);
|
|
399
399
|
if (spec) {
|
|
400
400
|
const status = getStatusIndicator(spec.frontmatter.status);
|
|
401
401
|
console.log(` \u2022 ${sanitizeUserInput(spec.name)} ${status}`);
|
|
402
402
|
} else {
|
|
403
|
-
console.log(` \u2022 ${sanitizeUserInput(
|
|
403
|
+
console.log(` \u2022 ${sanitizeUserInput(path13.basename(specPath))}`);
|
|
404
404
|
}
|
|
405
405
|
}
|
|
406
406
|
console.log("");
|
|
407
407
|
if (options.dryRun) {
|
|
408
|
-
console.log(
|
|
408
|
+
console.log(chalk16.yellow("Dry run mode - no actions will be taken"));
|
|
409
409
|
console.log("");
|
|
410
|
-
console.log(
|
|
410
|
+
console.log(chalk16.cyan("Would execute:"));
|
|
411
411
|
for (const specPath of resolvedSpecs) {
|
|
412
412
|
const spec = await getSpec(specPath);
|
|
413
|
-
const specName = spec?.name ||
|
|
413
|
+
const specName = spec?.name || path13.basename(specPath);
|
|
414
414
|
console.log(` 1. Update ${specName} status to in-progress`);
|
|
415
415
|
if (options.parallel) {
|
|
416
416
|
console.log(` 2. Create worktree at .worktrees/spec-${specName}`);
|
|
@@ -422,23 +422,23 @@ async function runAgent(specs, options = {}) {
|
|
|
422
422
|
}
|
|
423
423
|
for (const specPath of resolvedSpecs) {
|
|
424
424
|
const spec = await getSpec(specPath);
|
|
425
|
-
const specName = spec?.name ||
|
|
426
|
-
console.log(
|
|
425
|
+
const specName = spec?.name || path13.basename(specPath);
|
|
426
|
+
console.log(chalk16.bold(`Processing: ${specName}`));
|
|
427
427
|
if (options.statusUpdate !== false) {
|
|
428
428
|
try {
|
|
429
429
|
await updateSpec(specName, { status: "in-progress" });
|
|
430
|
-
console.log(
|
|
430
|
+
console.log(chalk16.green(` \u2713 Updated status to in-progress`));
|
|
431
431
|
} catch (error) {
|
|
432
|
-
console.log(
|
|
432
|
+
console.log(chalk16.yellow(` \u26A0 Could not update status: ${error.message}`));
|
|
433
433
|
}
|
|
434
434
|
}
|
|
435
435
|
let worktreePath;
|
|
436
436
|
if (options.parallel) {
|
|
437
437
|
try {
|
|
438
438
|
worktreePath = await createWorktree(specPath, specName, cwd);
|
|
439
|
-
console.log(
|
|
439
|
+
console.log(chalk16.green(` \u2713 Created worktree at ${worktreePath}`));
|
|
440
440
|
} catch (error) {
|
|
441
|
-
console.log(
|
|
441
|
+
console.log(chalk16.yellow(` \u26A0 Could not create worktree: ${error.message}`));
|
|
442
442
|
}
|
|
443
443
|
}
|
|
444
444
|
const session = {
|
|
@@ -449,7 +449,7 @@ async function runAgent(specs, options = {}) {
|
|
|
449
449
|
worktree: worktreePath
|
|
450
450
|
};
|
|
451
451
|
if (agentConfig.type === "cli") {
|
|
452
|
-
console.log(
|
|
452
|
+
console.log(chalk16.cyan(` \u2192 Launching ${agentName}...`));
|
|
453
453
|
try {
|
|
454
454
|
const child = await runCliAgent(specPath, agentConfig, worktreePath);
|
|
455
455
|
session.pid = child.pid;
|
|
@@ -468,12 +468,12 @@ async function runAgent(specs, options = {}) {
|
|
|
468
468
|
sess.error = error.message;
|
|
469
469
|
}
|
|
470
470
|
});
|
|
471
|
-
console.log(
|
|
471
|
+
console.log(chalk16.green(` \u2713 Agent launched (PID: ${child.pid})`));
|
|
472
472
|
} catch (error) {
|
|
473
473
|
session.status = "failed";
|
|
474
474
|
session.error = error.message;
|
|
475
475
|
activeSessions.set(specName, session);
|
|
476
|
-
console.error(
|
|
476
|
+
console.error(chalk16.red(` \u2717 Failed to launch agent: ${error.message}`));
|
|
477
477
|
}
|
|
478
478
|
} else {
|
|
479
479
|
activeSessions.set(specName, session);
|
|
@@ -481,8 +481,8 @@ async function runAgent(specs, options = {}) {
|
|
|
481
481
|
}
|
|
482
482
|
console.log("");
|
|
483
483
|
}
|
|
484
|
-
console.log(
|
|
485
|
-
console.log(
|
|
484
|
+
console.log(chalk16.green("\u2728 Agent dispatch complete"));
|
|
485
|
+
console.log(chalk16.gray("Use `lean-spec agent status` to check progress"));
|
|
486
486
|
}
|
|
487
487
|
async function showAgentStatus(spec, options = {}) {
|
|
488
488
|
if (spec) {
|
|
@@ -491,7 +491,7 @@ async function showAgentStatus(spec, options = {}) {
|
|
|
491
491
|
if (options.json) {
|
|
492
492
|
console.log(JSON.stringify({ error: `No active session for spec: ${spec}` }));
|
|
493
493
|
} else {
|
|
494
|
-
console.log(
|
|
494
|
+
console.log(chalk16.yellow(`No active session for spec: ${spec}`));
|
|
495
495
|
}
|
|
496
496
|
return;
|
|
497
497
|
}
|
|
@@ -499,7 +499,7 @@ async function showAgentStatus(spec, options = {}) {
|
|
|
499
499
|
console.log(JSON.stringify(session, null, 2));
|
|
500
500
|
} else {
|
|
501
501
|
console.log("");
|
|
502
|
-
console.log(
|
|
502
|
+
console.log(chalk16.cyan(`Agent Session: ${spec}`));
|
|
503
503
|
console.log(` Agent: ${session.agent}`);
|
|
504
504
|
console.log(` Status: ${getSessionStatusIndicator(session.status)}`);
|
|
505
505
|
console.log(` Started: ${session.startedAt}`);
|
|
@@ -510,7 +510,7 @@ async function showAgentStatus(spec, options = {}) {
|
|
|
510
510
|
console.log(` PID: ${session.pid}`);
|
|
511
511
|
}
|
|
512
512
|
if (session.error) {
|
|
513
|
-
console.log(` Error: ${
|
|
513
|
+
console.log(` Error: ${chalk16.red(session.error)}`);
|
|
514
514
|
}
|
|
515
515
|
console.log("");
|
|
516
516
|
}
|
|
@@ -523,17 +523,17 @@ async function showAgentStatus(spec, options = {}) {
|
|
|
523
523
|
return;
|
|
524
524
|
}
|
|
525
525
|
if (sessions.length === 0) {
|
|
526
|
-
console.log(
|
|
526
|
+
console.log(chalk16.gray("No active agent sessions"));
|
|
527
527
|
return;
|
|
528
528
|
}
|
|
529
529
|
console.log("");
|
|
530
|
-
console.log(
|
|
530
|
+
console.log(chalk16.green("=== Agent Sessions ==="));
|
|
531
531
|
console.log("");
|
|
532
532
|
for (const [specName, session] of sessions) {
|
|
533
|
-
console.log(`${
|
|
533
|
+
console.log(`${chalk16.bold(specName)}`);
|
|
534
534
|
console.log(` Agent: ${session.agent} | Status: ${getSessionStatusIndicator(session.status)}`);
|
|
535
535
|
if (session.worktree) {
|
|
536
|
-
console.log(` Worktree: ${
|
|
536
|
+
console.log(` Worktree: ${chalk16.dim(session.worktree)}`);
|
|
537
537
|
}
|
|
538
538
|
}
|
|
539
539
|
console.log("");
|
|
@@ -541,13 +541,13 @@ async function showAgentStatus(spec, options = {}) {
|
|
|
541
541
|
function getSessionStatusIndicator(status) {
|
|
542
542
|
switch (status) {
|
|
543
543
|
case "running":
|
|
544
|
-
return
|
|
544
|
+
return chalk16.blue("\u{1F504} Running");
|
|
545
545
|
case "completed":
|
|
546
|
-
return
|
|
546
|
+
return chalk16.green("\u2705 Completed");
|
|
547
547
|
case "failed":
|
|
548
|
-
return
|
|
548
|
+
return chalk16.red("\u274C Failed");
|
|
549
549
|
case "pending":
|
|
550
|
-
return
|
|
550
|
+
return chalk16.yellow("\u23F3 Pending");
|
|
551
551
|
default:
|
|
552
552
|
return status;
|
|
553
553
|
}
|
|
@@ -584,42 +584,42 @@ async function listAgents(options = {}) {
|
|
|
584
584
|
return;
|
|
585
585
|
}
|
|
586
586
|
console.log("");
|
|
587
|
-
console.log(
|
|
587
|
+
console.log(chalk16.green("=== Available AI Agents ==="));
|
|
588
588
|
console.log("");
|
|
589
|
-
console.log(
|
|
589
|
+
console.log(chalk16.bold("CLI-based (local):"));
|
|
590
590
|
for (const [name, info] of Object.entries(agents)) {
|
|
591
591
|
if (info.type === "cli") {
|
|
592
|
-
const defaultMarker = info.isDefault ?
|
|
593
|
-
const availableMarker = info.available ?
|
|
594
|
-
console.log(` ${availableMarker} ${name}${defaultMarker} ${
|
|
592
|
+
const defaultMarker = info.isDefault ? chalk16.green(" (default)") : "";
|
|
593
|
+
const availableMarker = info.available ? chalk16.green("\u2713") : chalk16.red("\u2717");
|
|
594
|
+
console.log(` ${availableMarker} ${name}${defaultMarker} ${chalk16.dim(`(${info.command})`)}`);
|
|
595
595
|
}
|
|
596
596
|
}
|
|
597
597
|
console.log("");
|
|
598
|
-
console.log(
|
|
598
|
+
console.log(chalk16.bold("Cloud-based:"));
|
|
599
599
|
for (const [name, info] of Object.entries(agents)) {
|
|
600
600
|
if (info.type === "cloud") {
|
|
601
|
-
const defaultMarker = info.isDefault ?
|
|
601
|
+
const defaultMarker = info.isDefault ? chalk16.green(" (default)") : "";
|
|
602
602
|
console.log(` \u2022 ${name}${defaultMarker}`);
|
|
603
603
|
}
|
|
604
604
|
}
|
|
605
605
|
console.log("");
|
|
606
|
-
console.log(
|
|
607
|
-
console.log(
|
|
606
|
+
console.log(chalk16.gray("Set default: lean-spec agent config <agent>"));
|
|
607
|
+
console.log(chalk16.gray("Run agent: lean-spec agent run <spec> --agent <agent>"));
|
|
608
608
|
console.log("");
|
|
609
609
|
}
|
|
610
610
|
async function setDefaultAgent(agent) {
|
|
611
611
|
const config = await loadConfig();
|
|
612
612
|
const agentConfig = await getAgentConfig(agent, config);
|
|
613
613
|
if (!agentConfig) {
|
|
614
|
-
console.error(
|
|
615
|
-
console.log(
|
|
614
|
+
console.error(chalk16.red(`Unknown agent: ${agent}`));
|
|
615
|
+
console.log(chalk16.gray("Available agents: claude, copilot, aider, gemini, gh-coding"));
|
|
616
616
|
process.exit(1);
|
|
617
617
|
}
|
|
618
618
|
const configWithAgents = config;
|
|
619
619
|
configWithAgents.agents = configWithAgents.agents || {};
|
|
620
620
|
configWithAgents.agents.default = agent;
|
|
621
621
|
await saveConfig(configWithAgents);
|
|
622
|
-
console.log(
|
|
622
|
+
console.log(chalk16.green(`\u2713 Default agent set to: ${agent}`));
|
|
623
623
|
}
|
|
624
624
|
|
|
625
625
|
// src/mcp/helpers.ts
|
|
@@ -912,8 +912,8 @@ async function getGitInfo() {
|
|
|
912
912
|
}
|
|
913
913
|
async function getProjectName(cwd = process.cwd()) {
|
|
914
914
|
try {
|
|
915
|
-
const packageJsonPath =
|
|
916
|
-
const content = await
|
|
915
|
+
const packageJsonPath = path13.join(cwd, "package.json");
|
|
916
|
+
const content = await fs9.readFile(packageJsonPath, "utf-8");
|
|
917
917
|
const packageJson2 = JSON.parse(content);
|
|
918
918
|
return packageJson2.name || null;
|
|
919
919
|
} catch {
|
|
@@ -1007,7 +1007,7 @@ async function linkSpec(specPath, options) {
|
|
|
1007
1007
|
await autoCheckIfEnabled();
|
|
1008
1008
|
const config = await loadConfig();
|
|
1009
1009
|
const cwd = process.cwd();
|
|
1010
|
-
const specsDir =
|
|
1010
|
+
const specsDir = path13.join(cwd, config.specsDir);
|
|
1011
1011
|
const resolvedPath = await resolveSpecPath(specPath, cwd, specsDir);
|
|
1012
1012
|
if (!resolvedPath) {
|
|
1013
1013
|
throw new Error(`Spec not found: ${sanitizeUserInput(specPath)}`);
|
|
@@ -1019,7 +1019,7 @@ async function linkSpec(specPath, options) {
|
|
|
1019
1019
|
const allSpecs = await loadAllSpecs({ includeArchived: true });
|
|
1020
1020
|
const specMap = new Map(allSpecs.map((s) => [s.path, s]));
|
|
1021
1021
|
const dependsOnSpecs = options.dependsOn ? options.dependsOn.split(",").map((s) => s.trim()) : [];
|
|
1022
|
-
const targetSpecName =
|
|
1022
|
+
const targetSpecName = path13.basename(resolvedPath);
|
|
1023
1023
|
const resolvedDependencies = /* @__PURE__ */ new Map();
|
|
1024
1024
|
for (const depSpec of dependsOnSpecs) {
|
|
1025
1025
|
if (depSpec === targetSpecName || depSpec === specPath) {
|
|
@@ -1032,7 +1032,7 @@ async function linkSpec(specPath, options) {
|
|
|
1032
1032
|
if (depResolvedPath === resolvedPath) {
|
|
1033
1033
|
throw new Error(`Cannot link spec to itself: ${sanitizeUserInput(depSpec)}`);
|
|
1034
1034
|
}
|
|
1035
|
-
const depSpecName =
|
|
1035
|
+
const depSpecName = path13.basename(depResolvedPath);
|
|
1036
1036
|
resolvedDependencies.set(depSpec, depSpecName);
|
|
1037
1037
|
}
|
|
1038
1038
|
const { parseFrontmatter: parseFrontmatter2 } = await import('./frontmatter-6ZBAGOEU.js');
|
|
@@ -1048,31 +1048,31 @@ async function linkSpec(specPath, options) {
|
|
|
1048
1048
|
}
|
|
1049
1049
|
}
|
|
1050
1050
|
if (added === 0) {
|
|
1051
|
-
console.log(
|
|
1051
|
+
console.log(chalk16.gray(`\u2139 Dependencies already exist, no changes made`));
|
|
1052
1052
|
return;
|
|
1053
1053
|
}
|
|
1054
1054
|
const cycles = detectCycles(targetSpecName, newDependsOn, specMap);
|
|
1055
1055
|
if (cycles.length > 0) {
|
|
1056
|
-
console.log(
|
|
1056
|
+
console.log(chalk16.yellow(`\u26A0\uFE0F Dependency cycle detected: ${cycles.join(" \u2192 ")}`));
|
|
1057
1057
|
}
|
|
1058
1058
|
await updateFrontmatter(specFile, { depends_on: newDependsOn });
|
|
1059
|
-
console.log(
|
|
1060
|
-
console.log(
|
|
1059
|
+
console.log(chalk16.green(`\u2713 Added dependencies: ${dependsOnSpecs.join(", ")}`));
|
|
1060
|
+
console.log(chalk16.gray(` Updated: ${sanitizeUserInput(path13.relative(cwd, resolvedPath))}`));
|
|
1061
1061
|
}
|
|
1062
|
-
function detectCycles(startSpec, dependsOn, specMap, visited = /* @__PURE__ */ new Set(),
|
|
1062
|
+
function detectCycles(startSpec, dependsOn, specMap, visited = /* @__PURE__ */ new Set(), path26 = []) {
|
|
1063
1063
|
if (visited.has(startSpec)) {
|
|
1064
|
-
const cycleStart =
|
|
1064
|
+
const cycleStart = path26.indexOf(startSpec);
|
|
1065
1065
|
if (cycleStart !== -1) {
|
|
1066
|
-
return [...
|
|
1066
|
+
return [...path26.slice(cycleStart), startSpec];
|
|
1067
1067
|
}
|
|
1068
1068
|
return [];
|
|
1069
1069
|
}
|
|
1070
1070
|
visited.add(startSpec);
|
|
1071
|
-
|
|
1071
|
+
path26.push(startSpec);
|
|
1072
1072
|
for (const dep of dependsOn) {
|
|
1073
1073
|
const depSpec = specMap.get(dep);
|
|
1074
1074
|
if (depSpec && depSpec.frontmatter.depends_on) {
|
|
1075
|
-
const cycle = detectCycles(dep, depSpec.frontmatter.depends_on, specMap, new Set(visited), [...
|
|
1075
|
+
const cycle = detectCycles(dep, depSpec.frontmatter.depends_on, specMap, new Set(visited), [...path26]);
|
|
1076
1076
|
if (cycle.length > 0) {
|
|
1077
1077
|
return cycle;
|
|
1078
1078
|
}
|
|
@@ -1102,8 +1102,8 @@ function createCommand() {
|
|
|
1102
1102
|
async function createSpec(name, options = {}) {
|
|
1103
1103
|
const config = await loadConfig();
|
|
1104
1104
|
const cwd = process.cwd();
|
|
1105
|
-
const specsDir =
|
|
1106
|
-
await
|
|
1105
|
+
const specsDir = path13.join(cwd, config.specsDir);
|
|
1106
|
+
await fs9.mkdir(specsDir, { recursive: true });
|
|
1107
1107
|
const seq = await getGlobalNextSeq(specsDir, config.structure.sequenceDigits);
|
|
1108
1108
|
let specRelativePath;
|
|
1109
1109
|
if (config.structure.pattern === "flat") {
|
|
@@ -1123,18 +1123,18 @@ async function createSpec(name, options = {}) {
|
|
|
1123
1123
|
} else {
|
|
1124
1124
|
throw new Error(`Unknown pattern: ${config.structure.pattern}`);
|
|
1125
1125
|
}
|
|
1126
|
-
const specDir =
|
|
1127
|
-
const specFile =
|
|
1126
|
+
const specDir = path13.join(specsDir, specRelativePath);
|
|
1127
|
+
const specFile = path13.join(specDir, config.structure.defaultFile);
|
|
1128
1128
|
try {
|
|
1129
|
-
await
|
|
1129
|
+
await fs9.access(specDir);
|
|
1130
1130
|
throw new Error(`Spec already exists: ${sanitizeUserInput(specDir)}`);
|
|
1131
1131
|
} catch (error) {
|
|
1132
1132
|
if (error.code === "ENOENT") ; else {
|
|
1133
1133
|
throw error;
|
|
1134
1134
|
}
|
|
1135
1135
|
}
|
|
1136
|
-
await
|
|
1137
|
-
const templatesDir =
|
|
1136
|
+
await fs9.mkdir(specDir, { recursive: true });
|
|
1137
|
+
const templatesDir = path13.join(cwd, ".lean-spec", "templates");
|
|
1138
1138
|
let templateName;
|
|
1139
1139
|
let templateDir = null;
|
|
1140
1140
|
if (options.template) {
|
|
@@ -1147,24 +1147,24 @@ async function createSpec(name, options = {}) {
|
|
|
1147
1147
|
} else {
|
|
1148
1148
|
templateName = config.template || "spec-template.md";
|
|
1149
1149
|
}
|
|
1150
|
-
let templatePath =
|
|
1150
|
+
let templatePath = path13.join(templatesDir, templateName);
|
|
1151
1151
|
try {
|
|
1152
|
-
const
|
|
1153
|
-
if (
|
|
1152
|
+
const stat6 = await fs9.stat(templatePath);
|
|
1153
|
+
if (stat6.isDirectory()) {
|
|
1154
1154
|
templateDir = templatePath;
|
|
1155
|
-
templatePath =
|
|
1156
|
-
await
|
|
1155
|
+
templatePath = path13.join(templateDir, "README.md");
|
|
1156
|
+
await fs9.access(templatePath);
|
|
1157
1157
|
}
|
|
1158
1158
|
} catch {
|
|
1159
|
-
const legacyPath =
|
|
1159
|
+
const legacyPath = path13.join(templatesDir, "spec-template.md");
|
|
1160
1160
|
try {
|
|
1161
|
-
await
|
|
1161
|
+
await fs9.access(legacyPath);
|
|
1162
1162
|
templatePath = legacyPath;
|
|
1163
1163
|
templateName = "spec-template.md";
|
|
1164
1164
|
} catch {
|
|
1165
|
-
const readmePath =
|
|
1165
|
+
const readmePath = path13.join(templatesDir, "README.md");
|
|
1166
1166
|
try {
|
|
1167
|
-
await
|
|
1167
|
+
await fs9.access(readmePath);
|
|
1168
1168
|
templatePath = readmePath;
|
|
1169
1169
|
templateName = "README.md";
|
|
1170
1170
|
} catch {
|
|
@@ -1175,7 +1175,7 @@ async function createSpec(name, options = {}) {
|
|
|
1175
1175
|
let content;
|
|
1176
1176
|
let varContext;
|
|
1177
1177
|
try {
|
|
1178
|
-
const template = await
|
|
1178
|
+
const template = await fs9.readFile(templatePath, "utf-8");
|
|
1179
1179
|
const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
1180
1180
|
const title = options.title || name;
|
|
1181
1181
|
varContext = await buildVariableContext(config, { name: title, date });
|
|
@@ -1219,41 +1219,41 @@ ${options.description}`
|
|
|
1219
1219
|
} catch (error) {
|
|
1220
1220
|
throw new Error(`Template not found: ${templatePath}. Run: lean-spec init`);
|
|
1221
1221
|
}
|
|
1222
|
-
await
|
|
1222
|
+
await fs9.writeFile(specFile, content, "utf-8");
|
|
1223
1223
|
try {
|
|
1224
1224
|
let additionalFiles = [];
|
|
1225
1225
|
if (templateDir) {
|
|
1226
|
-
const templateFiles = await
|
|
1226
|
+
const templateFiles = await fs9.readdir(templateDir);
|
|
1227
1227
|
additionalFiles = templateFiles.filter(
|
|
1228
1228
|
(f) => f.endsWith(".md") && f !== "README.md"
|
|
1229
1229
|
);
|
|
1230
1230
|
}
|
|
1231
1231
|
if (additionalFiles.length > 0) {
|
|
1232
1232
|
for (const file of additionalFiles) {
|
|
1233
|
-
const srcPath =
|
|
1234
|
-
const destPath =
|
|
1235
|
-
let fileContent = await
|
|
1233
|
+
const srcPath = path13.join(templateDir, file);
|
|
1234
|
+
const destPath = path13.join(specDir, file);
|
|
1235
|
+
let fileContent = await fs9.readFile(srcPath, "utf-8");
|
|
1236
1236
|
fileContent = resolveVariables(fileContent, varContext);
|
|
1237
|
-
await
|
|
1237
|
+
await fs9.writeFile(destPath, fileContent, "utf-8");
|
|
1238
1238
|
}
|
|
1239
|
-
console.log(
|
|
1240
|
-
console.log(
|
|
1239
|
+
console.log(chalk16.green(`\u2713 Created: ${sanitizeUserInput(specDir)}/`));
|
|
1240
|
+
console.log(chalk16.gray(` Files: ${config.structure.defaultFile}, ${additionalFiles.join(", ")}`));
|
|
1241
1241
|
} else {
|
|
1242
|
-
console.log(
|
|
1243
|
-
console.log(
|
|
1242
|
+
console.log(chalk16.green(`\u2713 Created: ${sanitizeUserInput(specDir)}/`));
|
|
1243
|
+
console.log(chalk16.gray(` Edit: ${sanitizeUserInput(specFile)}`));
|
|
1244
1244
|
}
|
|
1245
1245
|
} catch (error) {
|
|
1246
|
-
console.log(
|
|
1247
|
-
console.log(
|
|
1246
|
+
console.log(chalk16.green(`\u2713 Created: ${sanitizeUserInput(specDir)}/`));
|
|
1247
|
+
console.log(chalk16.gray(` Edit: ${sanitizeUserInput(specFile)}`));
|
|
1248
1248
|
}
|
|
1249
1249
|
if (options.dependsOn && options.dependsOn.length > 0) {
|
|
1250
|
-
const newSpecName =
|
|
1250
|
+
const newSpecName = path13.basename(specDir);
|
|
1251
1251
|
try {
|
|
1252
1252
|
await linkSpec(newSpecName, {
|
|
1253
1253
|
dependsOn: options.dependsOn.join(",")
|
|
1254
1254
|
});
|
|
1255
1255
|
} catch (error) {
|
|
1256
|
-
console.log(
|
|
1256
|
+
console.log(chalk16.yellow(`\u26A0\uFE0F Warning: Failed to add relationships: ${error.message}`));
|
|
1257
1257
|
}
|
|
1258
1258
|
}
|
|
1259
1259
|
await autoCheckIfEnabled();
|
|
@@ -1267,7 +1267,7 @@ async function archiveSpec(specPath) {
|
|
|
1267
1267
|
await autoCheckIfEnabled();
|
|
1268
1268
|
const config = await loadConfig();
|
|
1269
1269
|
const cwd = process.cwd();
|
|
1270
|
-
const specsDir =
|
|
1270
|
+
const specsDir = path13.join(cwd, config.specsDir);
|
|
1271
1271
|
const resolvedPath = await resolveSpecPath(specPath, cwd, specsDir);
|
|
1272
1272
|
if (!resolvedPath) {
|
|
1273
1273
|
throw new Error(`Spec not found: ${sanitizeUserInput(specPath)}`);
|
|
@@ -1276,12 +1276,12 @@ async function archiveSpec(specPath) {
|
|
|
1276
1276
|
if (specFile) {
|
|
1277
1277
|
await updateFrontmatter(specFile, { status: "archived" });
|
|
1278
1278
|
}
|
|
1279
|
-
const archiveDir =
|
|
1280
|
-
await
|
|
1281
|
-
const specName =
|
|
1282
|
-
const archivePath =
|
|
1283
|
-
await
|
|
1284
|
-
console.log(
|
|
1279
|
+
const archiveDir = path13.join(specsDir, "archived");
|
|
1280
|
+
await fs9.mkdir(archiveDir, { recursive: true });
|
|
1281
|
+
const specName = path13.basename(resolvedPath);
|
|
1282
|
+
const archivePath = path13.join(archiveDir, specName);
|
|
1283
|
+
await fs9.rename(resolvedPath, archivePath);
|
|
1284
|
+
console.log(chalk16.green(`\u2713 Archived: ${sanitizeUserInput(archivePath)}`));
|
|
1285
1285
|
}
|
|
1286
1286
|
|
|
1287
1287
|
// src/utils/pattern-detection.ts
|
|
@@ -1330,9 +1330,9 @@ async function listSpecs(options = {}) {
|
|
|
1330
1330
|
await autoCheckIfEnabled();
|
|
1331
1331
|
const config = await loadConfig();
|
|
1332
1332
|
const cwd = process.cwd();
|
|
1333
|
-
const specsDir =
|
|
1333
|
+
const specsDir = path13.join(cwd, config.specsDir);
|
|
1334
1334
|
try {
|
|
1335
|
-
await
|
|
1335
|
+
await fs9.access(specsDir);
|
|
1336
1336
|
} catch {
|
|
1337
1337
|
console.log("");
|
|
1338
1338
|
console.log("No specs directory found. Initialize with: lean-spec init");
|
|
@@ -1360,7 +1360,7 @@ async function listSpecs(options = {}) {
|
|
|
1360
1360
|
if (options.json) {
|
|
1361
1361
|
console.log(JSON.stringify({ specs: [], total: 0 }, null, 2));
|
|
1362
1362
|
} else {
|
|
1363
|
-
console.log(
|
|
1363
|
+
console.log(chalk16.dim("No specs found."));
|
|
1364
1364
|
}
|
|
1365
1365
|
return;
|
|
1366
1366
|
}
|
|
@@ -1383,7 +1383,7 @@ async function listSpecs(options = {}) {
|
|
|
1383
1383
|
console.log(JSON.stringify(jsonOutput, null, 2));
|
|
1384
1384
|
return;
|
|
1385
1385
|
}
|
|
1386
|
-
console.log(
|
|
1386
|
+
console.log(chalk16.bold.cyan("\u{1F4C4} Spec List"));
|
|
1387
1387
|
const filterParts = [];
|
|
1388
1388
|
if (options.status) {
|
|
1389
1389
|
const statusStr = Array.isArray(options.status) ? options.status.join(",") : options.status;
|
|
@@ -1396,7 +1396,7 @@ async function listSpecs(options = {}) {
|
|
|
1396
1396
|
}
|
|
1397
1397
|
if (options.assignee) filterParts.push(`assignee=${options.assignee}`);
|
|
1398
1398
|
if (filterParts.length > 0) {
|
|
1399
|
-
console.log(
|
|
1399
|
+
console.log(chalk16.dim(`Filtered by: ${filterParts.join(", ")}`));
|
|
1400
1400
|
}
|
|
1401
1401
|
console.log("");
|
|
1402
1402
|
const patternInfo = detectPatternType(config);
|
|
@@ -1406,7 +1406,7 @@ async function listSpecs(options = {}) {
|
|
|
1406
1406
|
renderFlatList(specs);
|
|
1407
1407
|
}
|
|
1408
1408
|
console.log("");
|
|
1409
|
-
console.log(
|
|
1409
|
+
console.log(chalk16.bold(`Total: ${chalk16.green(specs.length)} spec${specs.length !== 1 ? "s" : ""}`));
|
|
1410
1410
|
}
|
|
1411
1411
|
function renderFlatList(specs) {
|
|
1412
1412
|
for (const spec of specs) {
|
|
@@ -1414,25 +1414,25 @@ function renderFlatList(specs) {
|
|
|
1414
1414
|
const priorityEmoji = getPriorityEmoji(spec.frontmatter.priority);
|
|
1415
1415
|
let assigneeStr = "";
|
|
1416
1416
|
if (spec.frontmatter.assignee) {
|
|
1417
|
-
assigneeStr = " " +
|
|
1417
|
+
assigneeStr = " " + chalk16.cyan(`@${sanitizeUserInput(spec.frontmatter.assignee)}`);
|
|
1418
1418
|
}
|
|
1419
1419
|
let tagsStr = "";
|
|
1420
1420
|
if (spec.frontmatter.tags?.length) {
|
|
1421
1421
|
const tags = Array.isArray(spec.frontmatter.tags) ? spec.frontmatter.tags : [];
|
|
1422
1422
|
if (tags.length > 0) {
|
|
1423
1423
|
const tagStr = tags.map((tag) => `#${sanitizeUserInput(tag)}`).join(" ");
|
|
1424
|
-
tagsStr = " " +
|
|
1424
|
+
tagsStr = " " + chalk16.dim(chalk16.magenta(tagStr));
|
|
1425
1425
|
}
|
|
1426
1426
|
}
|
|
1427
1427
|
let subSpecStr = "";
|
|
1428
1428
|
if (spec.subFiles) {
|
|
1429
1429
|
const docCount = spec.subFiles.filter((f) => f.type === "document").length;
|
|
1430
1430
|
if (docCount > 0) {
|
|
1431
|
-
subSpecStr = " " +
|
|
1431
|
+
subSpecStr = " " + chalk16.dim(chalk16.yellow(`(+${docCount} sub-spec${docCount > 1 ? "s" : ""})`));
|
|
1432
1432
|
}
|
|
1433
1433
|
}
|
|
1434
1434
|
const priorityPrefix = priorityEmoji ? `${priorityEmoji} ` : "";
|
|
1435
|
-
console.log(`${priorityPrefix}${statusEmoji} ${
|
|
1435
|
+
console.log(`${priorityPrefix}${statusEmoji} ${chalk16.cyan(sanitizeUserInput(spec.path))}${assigneeStr}${tagsStr}${subSpecStr}`);
|
|
1436
1436
|
}
|
|
1437
1437
|
}
|
|
1438
1438
|
function renderGroupedList(specs, groupExtractor) {
|
|
@@ -1461,7 +1461,7 @@ function renderGroupedList(specs, groupExtractor) {
|
|
|
1461
1461
|
const groupName = sortedGroups[i];
|
|
1462
1462
|
const groupSpecs = groups.get(groupName);
|
|
1463
1463
|
const groupEmoji = /^\d{8}$/.test(groupName) ? "\u{1F4C5}" : groupName.startsWith("milestone") ? "\u{1F3AF}" : "\u{1F4C1}";
|
|
1464
|
-
console.log(`${
|
|
1464
|
+
console.log(`${chalk16.bold.cyan(`${groupEmoji} ${groupName}/`)} ${chalk16.dim(`(${groupSpecs.length})`)}`);
|
|
1465
1465
|
console.log("");
|
|
1466
1466
|
for (const spec of groupSpecs) {
|
|
1467
1467
|
const statusEmoji = getStatusEmoji(spec.frontmatter.status);
|
|
@@ -1469,25 +1469,25 @@ function renderGroupedList(specs, groupExtractor) {
|
|
|
1469
1469
|
const displayPath = spec.path.includes("/") ? spec.path.split("/").slice(1).join("/") : spec.path;
|
|
1470
1470
|
let assigneeStr = "";
|
|
1471
1471
|
if (spec.frontmatter.assignee) {
|
|
1472
|
-
assigneeStr = " " +
|
|
1472
|
+
assigneeStr = " " + chalk16.cyan(`@${sanitizeUserInput(spec.frontmatter.assignee)}`);
|
|
1473
1473
|
}
|
|
1474
1474
|
let tagsStr = "";
|
|
1475
1475
|
if (spec.frontmatter.tags?.length) {
|
|
1476
1476
|
const tags = Array.isArray(spec.frontmatter.tags) ? spec.frontmatter.tags : [];
|
|
1477
1477
|
if (tags.length > 0) {
|
|
1478
1478
|
const tagStr = tags.map((tag) => `#${sanitizeUserInput(tag)}`).join(" ");
|
|
1479
|
-
tagsStr = " " +
|
|
1479
|
+
tagsStr = " " + chalk16.dim(chalk16.magenta(tagStr));
|
|
1480
1480
|
}
|
|
1481
1481
|
}
|
|
1482
1482
|
let subSpecStr = "";
|
|
1483
1483
|
if (spec.subFiles) {
|
|
1484
1484
|
const docCount = spec.subFiles.filter((f) => f.type === "document").length;
|
|
1485
1485
|
if (docCount > 0) {
|
|
1486
|
-
subSpecStr = " " +
|
|
1486
|
+
subSpecStr = " " + chalk16.dim(chalk16.yellow(`(+${docCount} sub-spec${docCount > 1 ? "s" : ""})`));
|
|
1487
1487
|
}
|
|
1488
1488
|
}
|
|
1489
1489
|
const priorityPrefix = priorityEmoji ? `${priorityEmoji} ` : "";
|
|
1490
|
-
console.log(` ${priorityPrefix}${statusEmoji} ${
|
|
1490
|
+
console.log(` ${priorityPrefix}${statusEmoji} ${chalk16.cyan(sanitizeUserInput(displayPath))}${assigneeStr}${tagsStr}${subSpecStr}`);
|
|
1491
1491
|
}
|
|
1492
1492
|
if (i < sortedGroups.length - 1) {
|
|
1493
1493
|
console.log("");
|
|
@@ -1507,7 +1507,7 @@ async function unlinkSpec(specPath, options) {
|
|
|
1507
1507
|
await autoCheckIfEnabled();
|
|
1508
1508
|
const config = await loadConfig();
|
|
1509
1509
|
const cwd = process.cwd();
|
|
1510
|
-
const specsDir =
|
|
1510
|
+
const specsDir = path13.join(cwd, config.specsDir);
|
|
1511
1511
|
const resolvedPath = await resolveSpecPath(specPath, cwd, specsDir);
|
|
1512
1512
|
if (!resolvedPath) {
|
|
1513
1513
|
throw new Error(`Spec not found: ${sanitizeUserInput(specPath)}`);
|
|
@@ -1530,7 +1530,7 @@ async function unlinkSpec(specPath, options) {
|
|
|
1530
1530
|
for (const spec of toRemove) {
|
|
1531
1531
|
const resolvedSpecPath = await resolveSpecPath(spec, cwd, specsDir);
|
|
1532
1532
|
if (resolvedSpecPath) {
|
|
1533
|
-
resolvedToRemove.add(
|
|
1533
|
+
resolvedToRemove.add(path13.basename(resolvedSpecPath));
|
|
1534
1534
|
} else {
|
|
1535
1535
|
resolvedToRemove.add(spec);
|
|
1536
1536
|
}
|
|
@@ -1539,12 +1539,12 @@ async function unlinkSpec(specPath, options) {
|
|
|
1539
1539
|
removedCount = currentDependsOn.length - newDependsOn.length;
|
|
1540
1540
|
}
|
|
1541
1541
|
if (removedCount === 0) {
|
|
1542
|
-
console.log(
|
|
1542
|
+
console.log(chalk16.gray(`\u2139 No matching dependencies found to remove`));
|
|
1543
1543
|
return;
|
|
1544
1544
|
}
|
|
1545
1545
|
await updateFrontmatter(specFile, { depends_on: newDependsOn });
|
|
1546
|
-
console.log(
|
|
1547
|
-
console.log(
|
|
1546
|
+
console.log(chalk16.green(`\u2713 Removed ${removedCount} dependencies`));
|
|
1547
|
+
console.log(chalk16.gray(` Updated: ${sanitizeUserInput(path13.relative(cwd, resolvedPath))}`));
|
|
1548
1548
|
}
|
|
1549
1549
|
function templatesCommand() {
|
|
1550
1550
|
const cmd = new Command("templates").description("Manage spec templates");
|
|
@@ -1570,131 +1570,131 @@ function templatesCommand() {
|
|
|
1570
1570
|
}
|
|
1571
1571
|
async function listTemplates(cwd = process.cwd()) {
|
|
1572
1572
|
const config = await loadConfig(cwd);
|
|
1573
|
-
const templatesDir =
|
|
1573
|
+
const templatesDir = path13.join(cwd, ".lean-spec", "templates");
|
|
1574
1574
|
console.log("");
|
|
1575
|
-
console.log(
|
|
1575
|
+
console.log(chalk16.green("=== Project Templates ==="));
|
|
1576
1576
|
console.log("");
|
|
1577
1577
|
try {
|
|
1578
|
-
await
|
|
1578
|
+
await fs9.access(templatesDir);
|
|
1579
1579
|
} catch {
|
|
1580
|
-
console.log(
|
|
1581
|
-
console.log(
|
|
1580
|
+
console.log(chalk16.yellow("No templates directory found."));
|
|
1581
|
+
console.log(chalk16.gray("Run: lean-spec init"));
|
|
1582
1582
|
console.log("");
|
|
1583
1583
|
return;
|
|
1584
1584
|
}
|
|
1585
|
-
const entries = await
|
|
1585
|
+
const entries = await fs9.readdir(templatesDir, { withFileTypes: true });
|
|
1586
1586
|
const templateFiles = entries.filter((e) => e.isFile() && e.name.endsWith(".md"));
|
|
1587
1587
|
const templateDirs = entries.filter((e) => e.isDirectory());
|
|
1588
1588
|
if (templateFiles.length === 0 && templateDirs.length === 0) {
|
|
1589
|
-
console.log(
|
|
1589
|
+
console.log(chalk16.yellow("No templates found."));
|
|
1590
1590
|
console.log("");
|
|
1591
1591
|
return;
|
|
1592
1592
|
}
|
|
1593
1593
|
if (config.templates && Object.keys(config.templates).length > 0) {
|
|
1594
|
-
console.log(
|
|
1594
|
+
console.log(chalk16.cyan("Registered:"));
|
|
1595
1595
|
for (const [name, file] of Object.entries(config.templates)) {
|
|
1596
1596
|
const isDefault = config.template === file;
|
|
1597
|
-
const marker = isDefault ?
|
|
1598
|
-
const templatePath =
|
|
1597
|
+
const marker = isDefault ? chalk16.green("\u2713 (default)") : "";
|
|
1598
|
+
const templatePath = path13.join(templatesDir, file);
|
|
1599
1599
|
try {
|
|
1600
|
-
const
|
|
1601
|
-
if (
|
|
1602
|
-
const dirFiles = await
|
|
1600
|
+
const stat6 = await fs9.stat(templatePath);
|
|
1601
|
+
if (stat6.isDirectory()) {
|
|
1602
|
+
const dirFiles = await fs9.readdir(templatePath);
|
|
1603
1603
|
const mdFiles = dirFiles.filter((f) => f.endsWith(".md"));
|
|
1604
|
-
console.log(` ${
|
|
1605
|
-
console.log(
|
|
1604
|
+
console.log(` ${chalk16.bold(name)}: ${file}/ ${marker}`);
|
|
1605
|
+
console.log(chalk16.gray(` Files: ${mdFiles.join(", ")}`));
|
|
1606
1606
|
} else {
|
|
1607
|
-
console.log(` ${
|
|
1607
|
+
console.log(` ${chalk16.bold(name)}: ${file} ${marker}`);
|
|
1608
1608
|
}
|
|
1609
1609
|
} catch {
|
|
1610
|
-
console.log(` ${
|
|
1610
|
+
console.log(` ${chalk16.bold(name)}: ${file} ${marker} ${chalk16.red("(missing)")}`);
|
|
1611
1611
|
}
|
|
1612
1612
|
}
|
|
1613
1613
|
console.log("");
|
|
1614
1614
|
}
|
|
1615
1615
|
if (templateFiles.length > 0) {
|
|
1616
|
-
console.log(
|
|
1616
|
+
console.log(chalk16.cyan("Available files:"));
|
|
1617
1617
|
for (const entry of templateFiles) {
|
|
1618
|
-
const filePath =
|
|
1619
|
-
const
|
|
1620
|
-
const sizeKB = (
|
|
1618
|
+
const filePath = path13.join(templatesDir, entry.name);
|
|
1619
|
+
const stat6 = await fs9.stat(filePath);
|
|
1620
|
+
const sizeKB = (stat6.size / 1024).toFixed(1);
|
|
1621
1621
|
console.log(` ${entry.name} (${sizeKB} KB)`);
|
|
1622
1622
|
}
|
|
1623
1623
|
console.log("");
|
|
1624
1624
|
}
|
|
1625
1625
|
if (templateDirs.length > 0) {
|
|
1626
|
-
console.log(
|
|
1626
|
+
console.log(chalk16.cyan("Available directories (multi-file templates):"));
|
|
1627
1627
|
for (const entry of templateDirs) {
|
|
1628
|
-
const dirPath =
|
|
1629
|
-
const dirFiles = await
|
|
1628
|
+
const dirPath = path13.join(templatesDir, entry.name);
|
|
1629
|
+
const dirFiles = await fs9.readdir(dirPath);
|
|
1630
1630
|
const mdFiles = dirFiles.filter((f) => f.endsWith(".md"));
|
|
1631
1631
|
console.log(` ${entry.name}/ (${mdFiles.length} files: ${mdFiles.join(", ")})`);
|
|
1632
1632
|
}
|
|
1633
1633
|
console.log("");
|
|
1634
1634
|
}
|
|
1635
|
-
console.log(
|
|
1635
|
+
console.log(chalk16.gray("Use templates with: lean-spec create <name> --template=<template-name>"));
|
|
1636
1636
|
console.log("");
|
|
1637
1637
|
}
|
|
1638
1638
|
async function showTemplate(templateName, cwd = process.cwd()) {
|
|
1639
1639
|
const config = await loadConfig(cwd);
|
|
1640
1640
|
if (!config.templates?.[templateName]) {
|
|
1641
|
-
console.error(
|
|
1642
|
-
console.error(
|
|
1641
|
+
console.error(chalk16.red(`Template not found: ${templateName}`));
|
|
1642
|
+
console.error(chalk16.gray(`Available: ${Object.keys(config.templates || {}).join(", ")}`));
|
|
1643
1643
|
process.exit(1);
|
|
1644
1644
|
}
|
|
1645
|
-
const templatesDir =
|
|
1645
|
+
const templatesDir = path13.join(cwd, ".lean-spec", "templates");
|
|
1646
1646
|
const templateFile = config.templates[templateName];
|
|
1647
|
-
const templatePath =
|
|
1647
|
+
const templatePath = path13.join(templatesDir, templateFile);
|
|
1648
1648
|
try {
|
|
1649
|
-
const
|
|
1650
|
-
if (
|
|
1649
|
+
const stat6 = await fs9.stat(templatePath);
|
|
1650
|
+
if (stat6.isDirectory()) {
|
|
1651
1651
|
console.log("");
|
|
1652
|
-
console.log(
|
|
1652
|
+
console.log(chalk16.cyan(`=== Template: ${templateName} (${templateFile}/) ===`));
|
|
1653
1653
|
console.log("");
|
|
1654
|
-
const files = await
|
|
1654
|
+
const files = await fs9.readdir(templatePath);
|
|
1655
1655
|
const mdFiles = files.filter((f) => f.endsWith(".md"));
|
|
1656
1656
|
for (const file of mdFiles) {
|
|
1657
|
-
const filePath =
|
|
1658
|
-
const content = await
|
|
1659
|
-
console.log(
|
|
1657
|
+
const filePath = path13.join(templatePath, file);
|
|
1658
|
+
const content = await fs9.readFile(filePath, "utf-8");
|
|
1659
|
+
console.log(chalk16.yellow(`--- ${file} ---`));
|
|
1660
1660
|
console.log(content);
|
|
1661
1661
|
console.log("");
|
|
1662
1662
|
}
|
|
1663
1663
|
} else {
|
|
1664
|
-
const content = await
|
|
1664
|
+
const content = await fs9.readFile(templatePath, "utf-8");
|
|
1665
1665
|
console.log("");
|
|
1666
|
-
console.log(
|
|
1666
|
+
console.log(chalk16.cyan(`=== Template: ${templateName} (${templateFile}) ===`));
|
|
1667
1667
|
console.log("");
|
|
1668
1668
|
console.log(content);
|
|
1669
1669
|
console.log("");
|
|
1670
1670
|
}
|
|
1671
1671
|
} catch (error) {
|
|
1672
|
-
console.error(
|
|
1672
|
+
console.error(chalk16.red(`Error reading template: ${templateFile}`));
|
|
1673
1673
|
console.error(error);
|
|
1674
1674
|
process.exit(1);
|
|
1675
1675
|
}
|
|
1676
1676
|
}
|
|
1677
1677
|
async function addTemplate(name, file, cwd = process.cwd()) {
|
|
1678
1678
|
const config = await loadConfig(cwd);
|
|
1679
|
-
const templatesDir =
|
|
1680
|
-
const templatePath =
|
|
1679
|
+
const templatesDir = path13.join(cwd, ".lean-spec", "templates");
|
|
1680
|
+
const templatePath = path13.join(templatesDir, file);
|
|
1681
1681
|
try {
|
|
1682
|
-
const
|
|
1683
|
-
if (
|
|
1684
|
-
const mainFile =
|
|
1682
|
+
const stat6 = await fs9.stat(templatePath);
|
|
1683
|
+
if (stat6.isDirectory()) {
|
|
1684
|
+
const mainFile = path13.join(templatePath, "README.md");
|
|
1685
1685
|
try {
|
|
1686
|
-
await
|
|
1686
|
+
await fs9.access(mainFile);
|
|
1687
1687
|
} catch {
|
|
1688
|
-
console.error(
|
|
1689
|
-
console.error(
|
|
1688
|
+
console.error(chalk16.red(`Directory template must contain README.md: ${file}/`));
|
|
1689
|
+
console.error(chalk16.gray(`Expected at: ${mainFile}`));
|
|
1690
1690
|
process.exit(1);
|
|
1691
1691
|
}
|
|
1692
1692
|
}
|
|
1693
1693
|
} catch {
|
|
1694
|
-
console.error(
|
|
1695
|
-
console.error(
|
|
1694
|
+
console.error(chalk16.red(`Template not found: ${file}`));
|
|
1695
|
+
console.error(chalk16.gray(`Expected at: ${templatePath}`));
|
|
1696
1696
|
console.error(
|
|
1697
|
-
|
|
1697
|
+
chalk16.yellow("Create the file/directory first or use: lean-spec templates copy <source> <target>")
|
|
1698
1698
|
);
|
|
1699
1699
|
process.exit(1);
|
|
1700
1700
|
}
|
|
@@ -1702,60 +1702,60 @@ async function addTemplate(name, file, cwd = process.cwd()) {
|
|
|
1702
1702
|
config.templates = {};
|
|
1703
1703
|
}
|
|
1704
1704
|
if (config.templates[name]) {
|
|
1705
|
-
console.log(
|
|
1705
|
+
console.log(chalk16.yellow(`Warning: Template '${name}' already exists, updating...`));
|
|
1706
1706
|
}
|
|
1707
1707
|
config.templates[name] = file;
|
|
1708
1708
|
await saveConfig(config, cwd);
|
|
1709
|
-
console.log(
|
|
1710
|
-
console.log(
|
|
1709
|
+
console.log(chalk16.green(`\u2713 Added template: ${name} \u2192 ${file}`));
|
|
1710
|
+
console.log(chalk16.gray(` Use with: lean-spec create <spec-name> --template=${name}`));
|
|
1711
1711
|
}
|
|
1712
1712
|
async function removeTemplate(name, cwd = process.cwd()) {
|
|
1713
1713
|
const config = await loadConfig(cwd);
|
|
1714
1714
|
if (!config.templates?.[name]) {
|
|
1715
|
-
console.error(
|
|
1716
|
-
console.error(
|
|
1715
|
+
console.error(chalk16.red(`Template not found: ${name}`));
|
|
1716
|
+
console.error(chalk16.gray(`Available: ${Object.keys(config.templates || {}).join(", ")}`));
|
|
1717
1717
|
process.exit(1);
|
|
1718
1718
|
}
|
|
1719
1719
|
if (name === "default") {
|
|
1720
|
-
console.error(
|
|
1720
|
+
console.error(chalk16.red("Cannot remove default template"));
|
|
1721
1721
|
process.exit(1);
|
|
1722
1722
|
}
|
|
1723
1723
|
const file = config.templates[name];
|
|
1724
1724
|
delete config.templates[name];
|
|
1725
1725
|
await saveConfig(config, cwd);
|
|
1726
|
-
console.log(
|
|
1727
|
-
console.log(
|
|
1726
|
+
console.log(chalk16.green(`\u2713 Removed template: ${name}`));
|
|
1727
|
+
console.log(chalk16.gray(` Note: Template file ${file} still exists in .lean-spec/templates/`));
|
|
1728
1728
|
}
|
|
1729
1729
|
async function copyTemplate(source, target, cwd = process.cwd()) {
|
|
1730
1730
|
const config = await loadConfig(cwd);
|
|
1731
|
-
const templatesDir =
|
|
1731
|
+
const templatesDir = path13.join(cwd, ".lean-spec", "templates");
|
|
1732
1732
|
let sourceFile;
|
|
1733
1733
|
if (config.templates?.[source]) {
|
|
1734
1734
|
sourceFile = config.templates[source];
|
|
1735
1735
|
} else {
|
|
1736
1736
|
sourceFile = source;
|
|
1737
1737
|
}
|
|
1738
|
-
const sourcePath =
|
|
1738
|
+
const sourcePath = path13.join(templatesDir, sourceFile);
|
|
1739
1739
|
try {
|
|
1740
|
-
await
|
|
1740
|
+
await fs9.access(sourcePath);
|
|
1741
1741
|
} catch {
|
|
1742
|
-
console.error(
|
|
1743
|
-
console.error(
|
|
1742
|
+
console.error(chalk16.red(`Source template not found: ${source}`));
|
|
1743
|
+
console.error(chalk16.gray(`Expected at: ${sourcePath}`));
|
|
1744
1744
|
process.exit(1);
|
|
1745
1745
|
}
|
|
1746
1746
|
const targetFile = target.endsWith(".md") ? target : `${target}.md`;
|
|
1747
|
-
const targetPath =
|
|
1748
|
-
await
|
|
1749
|
-
console.log(
|
|
1747
|
+
const targetPath = path13.join(templatesDir, targetFile);
|
|
1748
|
+
await fs9.copyFile(sourcePath, targetPath);
|
|
1749
|
+
console.log(chalk16.green(`\u2713 Copied: ${sourceFile} \u2192 ${targetFile}`));
|
|
1750
1750
|
if (!config.templates) {
|
|
1751
1751
|
config.templates = {};
|
|
1752
1752
|
}
|
|
1753
1753
|
const templateName = target.replace(/\.md$/, "");
|
|
1754
1754
|
config.templates[templateName] = targetFile;
|
|
1755
1755
|
await saveConfig(config, cwd);
|
|
1756
|
-
console.log(
|
|
1757
|
-
console.log(
|
|
1758
|
-
console.log(
|
|
1756
|
+
console.log(chalk16.green(`\u2713 Registered template: ${templateName}`));
|
|
1757
|
+
console.log(chalk16.gray(` Edit: ${targetPath}`));
|
|
1758
|
+
console.log(chalk16.gray(` Use with: lean-spec create <spec-name> --template=${templateName}`));
|
|
1759
1759
|
}
|
|
1760
1760
|
var AI_TOOL_CONFIGS = {
|
|
1761
1761
|
aider: {
|
|
@@ -1776,7 +1776,7 @@ var AI_TOOL_CONFIGS = {
|
|
|
1776
1776
|
claude: {
|
|
1777
1777
|
file: "CLAUDE.md",
|
|
1778
1778
|
description: "Claude Code (CLAUDE.md)",
|
|
1779
|
-
default:
|
|
1779
|
+
default: false,
|
|
1780
1780
|
usesSymlink: true,
|
|
1781
1781
|
detection: {
|
|
1782
1782
|
commands: ["claude"],
|
|
@@ -1891,7 +1891,7 @@ async function configDirExists(dirName) {
|
|
|
1891
1891
|
const homeDir = process.env.HOME || process.env.USERPROFILE || "";
|
|
1892
1892
|
if (!homeDir) return false;
|
|
1893
1893
|
try {
|
|
1894
|
-
await
|
|
1894
|
+
await fs9.access(path13.join(homeDir, dirName));
|
|
1895
1895
|
return true;
|
|
1896
1896
|
} catch {
|
|
1897
1897
|
return false;
|
|
@@ -1904,13 +1904,13 @@ async function extensionInstalled(extensionId) {
|
|
|
1904
1904
|
const homeDir = process.env.HOME || process.env.USERPROFILE || "";
|
|
1905
1905
|
if (!homeDir) return false;
|
|
1906
1906
|
const extensionDirs = [
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1907
|
+
path13.join(homeDir, ".vscode", "extensions"),
|
|
1908
|
+
path13.join(homeDir, ".vscode-server", "extensions"),
|
|
1909
|
+
path13.join(homeDir, ".cursor", "extensions")
|
|
1910
1910
|
];
|
|
1911
1911
|
for (const extDir of extensionDirs) {
|
|
1912
1912
|
try {
|
|
1913
|
-
const entries = await
|
|
1913
|
+
const entries = await fs9.readdir(extDir);
|
|
1914
1914
|
if (entries.some((e) => e.toLowerCase().startsWith(extensionId.toLowerCase()))) {
|
|
1915
1915
|
return true;
|
|
1916
1916
|
}
|
|
@@ -1990,10 +1990,10 @@ async function createAgentToolSymlinks(cwd, selectedTools) {
|
|
|
1990
1990
|
}
|
|
1991
1991
|
}
|
|
1992
1992
|
for (const file of filesToCreate) {
|
|
1993
|
-
const targetPath =
|
|
1993
|
+
const targetPath = path13.join(cwd, file);
|
|
1994
1994
|
try {
|
|
1995
1995
|
try {
|
|
1996
|
-
await
|
|
1996
|
+
await fs9.access(targetPath);
|
|
1997
1997
|
results.push({ file, skipped: true });
|
|
1998
1998
|
continue;
|
|
1999
1999
|
} catch {
|
|
@@ -2008,10 +2008,10 @@ async function createAgentToolSymlinks(cwd, selectedTools) {
|
|
|
2008
2008
|
|
|
2009
2009
|
See AGENTS.md for the full LeanSpec AI agent instructions.
|
|
2010
2010
|
`;
|
|
2011
|
-
await
|
|
2011
|
+
await fs9.writeFile(targetPath, windowsContent, "utf-8");
|
|
2012
2012
|
results.push({ file, created: true, error: "created as copy (Windows)" });
|
|
2013
2013
|
} else {
|
|
2014
|
-
await
|
|
2014
|
+
await fs9.symlink("AGENTS.md", targetPath);
|
|
2015
2015
|
results.push({ file, created: true });
|
|
2016
2016
|
}
|
|
2017
2017
|
} catch (error) {
|
|
@@ -2032,7 +2032,7 @@ async function detectExistingSystemPrompts(cwd) {
|
|
|
2032
2032
|
const found = [];
|
|
2033
2033
|
for (const file of commonFiles) {
|
|
2034
2034
|
try {
|
|
2035
|
-
await
|
|
2035
|
+
await fs9.access(path13.join(cwd, file));
|
|
2036
2036
|
found.push(file);
|
|
2037
2037
|
} catch {
|
|
2038
2038
|
}
|
|
@@ -2041,20 +2041,20 @@ async function detectExistingSystemPrompts(cwd) {
|
|
|
2041
2041
|
}
|
|
2042
2042
|
async function handleExistingFiles(action, existingFiles, templateDir, cwd, variables = {}) {
|
|
2043
2043
|
for (const file of existingFiles) {
|
|
2044
|
-
const filePath =
|
|
2045
|
-
const templateFilePath =
|
|
2044
|
+
const filePath = path13.join(cwd, file);
|
|
2045
|
+
const templateFilePath = path13.join(templateDir, file);
|
|
2046
2046
|
try {
|
|
2047
|
-
await
|
|
2047
|
+
await fs9.access(templateFilePath);
|
|
2048
2048
|
} catch {
|
|
2049
2049
|
continue;
|
|
2050
2050
|
}
|
|
2051
2051
|
if (action === "merge-ai" && file === "AGENTS.md") {
|
|
2052
|
-
const existing = await
|
|
2053
|
-
let template = await
|
|
2052
|
+
const existing = await fs9.readFile(filePath, "utf-8");
|
|
2053
|
+
let template = await fs9.readFile(templateFilePath, "utf-8");
|
|
2054
2054
|
for (const [key, value] of Object.entries(variables)) {
|
|
2055
2055
|
template = template.replace(new RegExp(`\\{${key}\\}`, "g"), value);
|
|
2056
2056
|
}
|
|
2057
|
-
const promptPath =
|
|
2057
|
+
const promptPath = path13.join(cwd, ".lean-spec", "MERGE-AGENTS-PROMPT.md");
|
|
2058
2058
|
const aiPrompt = `# AI Prompt: Consolidate AGENTS.md
|
|
2059
2059
|
|
|
2060
2060
|
## Task
|
|
@@ -2086,20 +2086,20 @@ Create a single consolidated AGENTS.md that:
|
|
|
2086
2086
|
- Maintains clear structure and readability
|
|
2087
2087
|
- Removes any duplicate or conflicting guidance
|
|
2088
2088
|
`;
|
|
2089
|
-
await
|
|
2090
|
-
await
|
|
2091
|
-
console.log(
|
|
2092
|
-
console.log(
|
|
2089
|
+
await fs9.mkdir(path13.dirname(promptPath), { recursive: true });
|
|
2090
|
+
await fs9.writeFile(promptPath, aiPrompt, "utf-8");
|
|
2091
|
+
console.log(chalk16.green(`\u2713 Created AI consolidation prompt`));
|
|
2092
|
+
console.log(chalk16.cyan(` \u2192 ${promptPath}`));
|
|
2093
2093
|
console.log("");
|
|
2094
|
-
console.log(
|
|
2095
|
-
console.log(
|
|
2096
|
-
console.log(
|
|
2097
|
-
console.log(
|
|
2098
|
-
console.log(
|
|
2094
|
+
console.log(chalk16.yellow("\u{1F4DD} Next steps:"));
|
|
2095
|
+
console.log(chalk16.gray(" 1. Open .lean-spec/MERGE-AGENTS-PROMPT.md"));
|
|
2096
|
+
console.log(chalk16.gray(" 2. Send it to your AI coding assistant (GitHub Copilot, Cursor, etc.)"));
|
|
2097
|
+
console.log(chalk16.gray(" 3. Let AI create the consolidated AGENTS.md"));
|
|
2098
|
+
console.log(chalk16.gray(" 4. Review and commit the result"));
|
|
2099
2099
|
console.log("");
|
|
2100
2100
|
} else if (action === "merge-append" && file === "AGENTS.md") {
|
|
2101
|
-
const existing = await
|
|
2102
|
-
let template = await
|
|
2101
|
+
const existing = await fs9.readFile(filePath, "utf-8");
|
|
2102
|
+
let template = await fs9.readFile(templateFilePath, "utf-8");
|
|
2103
2103
|
for (const [key, value] of Object.entries(variables)) {
|
|
2104
2104
|
template = template.replace(new RegExp(`\\{${key}\\}`, "g"), value);
|
|
2105
2105
|
}
|
|
@@ -2110,29 +2110,29 @@ Create a single consolidated AGENTS.md that:
|
|
|
2110
2110
|
## LeanSpec Integration
|
|
2111
2111
|
|
|
2112
2112
|
${template.split("\n").slice(1).join("\n")}`;
|
|
2113
|
-
await
|
|
2114
|
-
console.log(
|
|
2115
|
-
console.log(
|
|
2113
|
+
await fs9.writeFile(filePath, merged, "utf-8");
|
|
2114
|
+
console.log(chalk16.green(`\u2713 Appended LeanSpec section to ${file}`));
|
|
2115
|
+
console.log(chalk16.yellow(" \u26A0 Note: May be verbose. Consider consolidating later."));
|
|
2116
2116
|
} else if (action === "overwrite") {
|
|
2117
2117
|
const backupPath = `${filePath}.backup`;
|
|
2118
|
-
await
|
|
2119
|
-
console.log(
|
|
2120
|
-
let content = await
|
|
2118
|
+
await fs9.rename(filePath, backupPath);
|
|
2119
|
+
console.log(chalk16.yellow(`\u2713 Backed up ${file} \u2192 ${file}.backup`));
|
|
2120
|
+
let content = await fs9.readFile(templateFilePath, "utf-8");
|
|
2121
2121
|
for (const [key, value] of Object.entries(variables)) {
|
|
2122
2122
|
content = content.replace(new RegExp(`\\{${key}\\}`, "g"), value);
|
|
2123
2123
|
}
|
|
2124
|
-
await
|
|
2125
|
-
console.log(
|
|
2126
|
-
console.log(
|
|
2124
|
+
await fs9.writeFile(filePath, content, "utf-8");
|
|
2125
|
+
console.log(chalk16.green(`\u2713 Created new ${file}`));
|
|
2126
|
+
console.log(chalk16.gray(` \u{1F4A1} Your original content is preserved in ${file}.backup`));
|
|
2127
2127
|
}
|
|
2128
2128
|
}
|
|
2129
2129
|
}
|
|
2130
2130
|
async function copyDirectory(src, dest, skipFiles = [], variables = {}) {
|
|
2131
|
-
await
|
|
2132
|
-
const entries = await
|
|
2131
|
+
await fs9.mkdir(dest, { recursive: true });
|
|
2132
|
+
const entries = await fs9.readdir(src, { withFileTypes: true });
|
|
2133
2133
|
for (const entry of entries) {
|
|
2134
|
-
const srcPath =
|
|
2135
|
-
const destPath =
|
|
2134
|
+
const srcPath = path13.join(src, entry.name);
|
|
2135
|
+
const destPath = path13.join(dest, entry.name);
|
|
2136
2136
|
if (skipFiles.includes(entry.name)) {
|
|
2137
2137
|
continue;
|
|
2138
2138
|
}
|
|
@@ -2140,28 +2140,28 @@ async function copyDirectory(src, dest, skipFiles = [], variables = {}) {
|
|
|
2140
2140
|
await copyDirectory(srcPath, destPath, skipFiles, variables);
|
|
2141
2141
|
} else {
|
|
2142
2142
|
try {
|
|
2143
|
-
await
|
|
2143
|
+
await fs9.access(destPath);
|
|
2144
2144
|
} catch {
|
|
2145
|
-
let content = await
|
|
2145
|
+
let content = await fs9.readFile(srcPath, "utf-8");
|
|
2146
2146
|
for (const [key, value] of Object.entries(variables)) {
|
|
2147
2147
|
content = content.replace(new RegExp(`\\{${key}\\}`, "g"), value);
|
|
2148
2148
|
}
|
|
2149
|
-
await
|
|
2149
|
+
await fs9.writeFile(destPath, content, "utf-8");
|
|
2150
2150
|
}
|
|
2151
2151
|
}
|
|
2152
2152
|
}
|
|
2153
2153
|
}
|
|
2154
2154
|
async function getProjectName2(cwd) {
|
|
2155
2155
|
try {
|
|
2156
|
-
const packageJsonPath =
|
|
2157
|
-
const content = await
|
|
2156
|
+
const packageJsonPath = path13.join(cwd, "package.json");
|
|
2157
|
+
const content = await fs9.readFile(packageJsonPath, "utf-8");
|
|
2158
2158
|
const pkg = JSON.parse(content);
|
|
2159
2159
|
if (pkg.name) {
|
|
2160
2160
|
return pkg.name;
|
|
2161
2161
|
}
|
|
2162
2162
|
} catch {
|
|
2163
2163
|
}
|
|
2164
|
-
return
|
|
2164
|
+
return path13.basename(cwd);
|
|
2165
2165
|
}
|
|
2166
2166
|
function buildMergeCommand(cli, promptPath) {
|
|
2167
2167
|
const prompt = `Follow the instructions in ${promptPath} to consolidate AGENTS.md. Read the prompt file, then edit AGENTS.md with the merged content.`;
|
|
@@ -2197,7 +2197,7 @@ async function executeMergeWithAI(cwd, promptPath, tool, timeoutMs = 12e4) {
|
|
|
2197
2197
|
return arg;
|
|
2198
2198
|
});
|
|
2199
2199
|
const fullCommand = `${command} ${quotedArgs.join(" ")}`;
|
|
2200
|
-
return new Promise((
|
|
2200
|
+
return new Promise((resolve3) => {
|
|
2201
2201
|
let output = "";
|
|
2202
2202
|
let errorOutput = "";
|
|
2203
2203
|
let timedOut = false;
|
|
@@ -2221,19 +2221,19 @@ async function executeMergeWithAI(cwd, promptPath, tool, timeoutMs = 12e4) {
|
|
|
2221
2221
|
child.on("close", (code) => {
|
|
2222
2222
|
clearTimeout(timeout);
|
|
2223
2223
|
if (timedOut) {
|
|
2224
|
-
|
|
2224
|
+
resolve3({
|
|
2225
2225
|
success: false,
|
|
2226
2226
|
output,
|
|
2227
2227
|
error: "Command timed out",
|
|
2228
2228
|
timedOut: true
|
|
2229
2229
|
});
|
|
2230
2230
|
} else if (code === 0) {
|
|
2231
|
-
|
|
2231
|
+
resolve3({
|
|
2232
2232
|
success: true,
|
|
2233
2233
|
output
|
|
2234
2234
|
});
|
|
2235
2235
|
} else {
|
|
2236
|
-
|
|
2236
|
+
resolve3({
|
|
2237
2237
|
success: false,
|
|
2238
2238
|
output,
|
|
2239
2239
|
error: errorOutput || `Process exited with code ${code}`
|
|
@@ -2242,7 +2242,7 @@ async function executeMergeWithAI(cwd, promptPath, tool, timeoutMs = 12e4) {
|
|
|
2242
2242
|
});
|
|
2243
2243
|
child.on("error", (err) => {
|
|
2244
2244
|
clearTimeout(timeout);
|
|
2245
|
-
|
|
2245
|
+
resolve3({
|
|
2246
2246
|
success: false,
|
|
2247
2247
|
error: err.message
|
|
2248
2248
|
});
|
|
@@ -2279,6 +2279,225 @@ function getDisplayCommand(tool, promptPath) {
|
|
|
2279
2279
|
});
|
|
2280
2280
|
return `${command} ${displayArgs.join(" ")}`;
|
|
2281
2281
|
}
|
|
2282
|
+
var MCP_TOOL_CONFIGS = {
|
|
2283
|
+
"claude": {
|
|
2284
|
+
id: "claude",
|
|
2285
|
+
name: "Claude Code",
|
|
2286
|
+
description: "Claude Code (.mcp.json)",
|
|
2287
|
+
configPath: ".mcp.json",
|
|
2288
|
+
configLocation: "workspace",
|
|
2289
|
+
usesProjectVariable: true,
|
|
2290
|
+
detection: {
|
|
2291
|
+
directories: [".claude"],
|
|
2292
|
+
files: ["CLAUDE.md", ".claude.json"]
|
|
2293
|
+
}
|
|
2294
|
+
},
|
|
2295
|
+
vscode: {
|
|
2296
|
+
id: "vscode",
|
|
2297
|
+
name: "VS Code",
|
|
2298
|
+
description: "VS Code with GitHub Copilot (.vscode/mcp.json)",
|
|
2299
|
+
configPath: ".vscode/mcp.json",
|
|
2300
|
+
configLocation: "workspace",
|
|
2301
|
+
usesProjectVariable: false,
|
|
2302
|
+
detection: {
|
|
2303
|
+
directories: [".vscode"]
|
|
2304
|
+
}
|
|
2305
|
+
},
|
|
2306
|
+
cursor: {
|
|
2307
|
+
id: "cursor",
|
|
2308
|
+
name: "Cursor",
|
|
2309
|
+
description: "Cursor (.cursor/mcp.json)",
|
|
2310
|
+
configPath: ".cursor/mcp.json",
|
|
2311
|
+
configLocation: "workspace",
|
|
2312
|
+
usesProjectVariable: false,
|
|
2313
|
+
detection: {
|
|
2314
|
+
directories: [".cursor"],
|
|
2315
|
+
files: [".cursorrules"]
|
|
2316
|
+
}
|
|
2317
|
+
},
|
|
2318
|
+
windsurf: {
|
|
2319
|
+
id: "windsurf",
|
|
2320
|
+
name: "Windsurf",
|
|
2321
|
+
description: "Windsurf (.windsurf/mcp.json)",
|
|
2322
|
+
configPath: ".windsurf/mcp.json",
|
|
2323
|
+
configLocation: "workspace",
|
|
2324
|
+
usesProjectVariable: false,
|
|
2325
|
+
detection: {
|
|
2326
|
+
directories: [".windsurf"],
|
|
2327
|
+
files: [".windsurfrules"]
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
};
|
|
2331
|
+
async function detectMcpTools(cwd) {
|
|
2332
|
+
const results = [];
|
|
2333
|
+
for (const [key, config] of Object.entries(MCP_TOOL_CONFIGS)) {
|
|
2334
|
+
const reasons = [];
|
|
2335
|
+
const detection = config.detection;
|
|
2336
|
+
if (!detection) {
|
|
2337
|
+
results.push({ tool: key, detected: false, reasons: [] });
|
|
2338
|
+
continue;
|
|
2339
|
+
}
|
|
2340
|
+
if (detection.directories) {
|
|
2341
|
+
for (const dir of detection.directories) {
|
|
2342
|
+
const dirPath = path13.join(cwd, dir);
|
|
2343
|
+
try {
|
|
2344
|
+
const stat6 = await fs9.stat(dirPath);
|
|
2345
|
+
if (stat6.isDirectory()) {
|
|
2346
|
+
reasons.push(`${dir}/ directory found`);
|
|
2347
|
+
}
|
|
2348
|
+
} catch {
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2351
|
+
}
|
|
2352
|
+
if (detection.files) {
|
|
2353
|
+
for (const file of detection.files) {
|
|
2354
|
+
const filePath = path13.join(cwd, file);
|
|
2355
|
+
try {
|
|
2356
|
+
await fs9.access(filePath);
|
|
2357
|
+
reasons.push(`${file} found`);
|
|
2358
|
+
} catch {
|
|
2359
|
+
}
|
|
2360
|
+
}
|
|
2361
|
+
}
|
|
2362
|
+
results.push({
|
|
2363
|
+
tool: key,
|
|
2364
|
+
detected: reasons.length > 0,
|
|
2365
|
+
reasons
|
|
2366
|
+
});
|
|
2367
|
+
}
|
|
2368
|
+
return results;
|
|
2369
|
+
}
|
|
2370
|
+
function generateMcpConfig(projectPath, tool) {
|
|
2371
|
+
const config = MCP_TOOL_CONFIGS[tool];
|
|
2372
|
+
const projectArg = config.usesProjectVariable ? "${workspaceFolder}" : projectPath;
|
|
2373
|
+
return {
|
|
2374
|
+
command: "npx",
|
|
2375
|
+
args: ["-y", "@leanspec/mcp", "--project", projectArg]
|
|
2376
|
+
};
|
|
2377
|
+
}
|
|
2378
|
+
function generateMcpConfigFile(projectPath, tool) {
|
|
2379
|
+
return {
|
|
2380
|
+
mcpServers: {
|
|
2381
|
+
"lean-spec": generateMcpConfig(projectPath, tool)
|
|
2382
|
+
}
|
|
2383
|
+
};
|
|
2384
|
+
}
|
|
2385
|
+
async function readMcpConfig(configPath) {
|
|
2386
|
+
try {
|
|
2387
|
+
const content = await fs9.readFile(configPath, "utf-8");
|
|
2388
|
+
return JSON.parse(content);
|
|
2389
|
+
} catch {
|
|
2390
|
+
return null;
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
async function writeMcpConfig(configPath, config) {
|
|
2394
|
+
const dir = path13.dirname(configPath);
|
|
2395
|
+
await fs9.mkdir(dir, { recursive: true });
|
|
2396
|
+
await fs9.writeFile(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
2397
|
+
}
|
|
2398
|
+
async function createMcpConfig(cwd, tool) {
|
|
2399
|
+
const toolConfig = MCP_TOOL_CONFIGS[tool];
|
|
2400
|
+
const configPath = path13.join(cwd, toolConfig.configPath);
|
|
2401
|
+
const absoluteProjectPath = path13.resolve(cwd);
|
|
2402
|
+
try {
|
|
2403
|
+
const existingConfig = await readMcpConfig(configPath);
|
|
2404
|
+
if (existingConfig) {
|
|
2405
|
+
if (existingConfig.mcpServers?.["lean-spec"]) {
|
|
2406
|
+
return {
|
|
2407
|
+
tool,
|
|
2408
|
+
configPath: toolConfig.configPath,
|
|
2409
|
+
skipped: true
|
|
2410
|
+
};
|
|
2411
|
+
}
|
|
2412
|
+
const newServerConfig = generateMcpConfig(absoluteProjectPath, tool);
|
|
2413
|
+
existingConfig.mcpServers = existingConfig.mcpServers || {};
|
|
2414
|
+
existingConfig.mcpServers["lean-spec"] = newServerConfig;
|
|
2415
|
+
await writeMcpConfig(configPath, existingConfig);
|
|
2416
|
+
return {
|
|
2417
|
+
tool,
|
|
2418
|
+
configPath: toolConfig.configPath,
|
|
2419
|
+
merged: true
|
|
2420
|
+
};
|
|
2421
|
+
}
|
|
2422
|
+
const newConfig = generateMcpConfigFile(absoluteProjectPath, tool);
|
|
2423
|
+
await writeMcpConfig(configPath, newConfig);
|
|
2424
|
+
return {
|
|
2425
|
+
tool,
|
|
2426
|
+
configPath: toolConfig.configPath,
|
|
2427
|
+
created: true
|
|
2428
|
+
};
|
|
2429
|
+
} catch (error) {
|
|
2430
|
+
return {
|
|
2431
|
+
tool,
|
|
2432
|
+
configPath: toolConfig.configPath,
|
|
2433
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
2434
|
+
};
|
|
2435
|
+
}
|
|
2436
|
+
}
|
|
2437
|
+
async function createMcpConfigs(cwd, tools) {
|
|
2438
|
+
const results = [];
|
|
2439
|
+
for (const tool of tools) {
|
|
2440
|
+
const result = await createMcpConfig(cwd, tool);
|
|
2441
|
+
results.push(result);
|
|
2442
|
+
}
|
|
2443
|
+
return results;
|
|
2444
|
+
}
|
|
2445
|
+
function printMcpConfigResults(results) {
|
|
2446
|
+
for (const result of results) {
|
|
2447
|
+
const config = MCP_TOOL_CONFIGS[result.tool];
|
|
2448
|
+
if (result.created) {
|
|
2449
|
+
console.log(chalk16.green(`\u2713 ${config.name}: Created ${result.configPath}`));
|
|
2450
|
+
} else if (result.merged) {
|
|
2451
|
+
console.log(chalk16.green(`\u2713 ${config.name}: Added lean-spec to ${result.configPath}`));
|
|
2452
|
+
} else if (result.skipped) {
|
|
2453
|
+
console.log(chalk16.yellow(`\u26A0 ${config.name}: Already configured in ${result.configPath}`));
|
|
2454
|
+
} else if (result.error) {
|
|
2455
|
+
console.log(chalk16.red(`\u2717 ${config.name}: ${result.error}`));
|
|
2456
|
+
}
|
|
2457
|
+
}
|
|
2458
|
+
}
|
|
2459
|
+
async function getDefaultMcpToolSelection(cwd) {
|
|
2460
|
+
const detectionResults = await detectMcpTools(cwd);
|
|
2461
|
+
const detectedTools = detectionResults.filter((r) => r.detected).map((r) => r.tool);
|
|
2462
|
+
if (detectedTools.length > 0) {
|
|
2463
|
+
return { defaults: detectedTools, detected: detectionResults };
|
|
2464
|
+
}
|
|
2465
|
+
return { defaults: [], detected: detectionResults };
|
|
2466
|
+
}
|
|
2467
|
+
function parseMcpConfigFlag(value) {
|
|
2468
|
+
if (value === "all") {
|
|
2469
|
+
return "all";
|
|
2470
|
+
}
|
|
2471
|
+
if (value === "none") {
|
|
2472
|
+
return "none";
|
|
2473
|
+
}
|
|
2474
|
+
const tools = value.split(",").map((t) => t.trim().toLowerCase());
|
|
2475
|
+
const validTools = [];
|
|
2476
|
+
for (const tool of tools) {
|
|
2477
|
+
const normalized = normalizeToolName(tool);
|
|
2478
|
+
if (normalized && isValidMcpTool(normalized)) {
|
|
2479
|
+
validTools.push(normalized);
|
|
2480
|
+
}
|
|
2481
|
+
}
|
|
2482
|
+
return validTools;
|
|
2483
|
+
}
|
|
2484
|
+
function normalizeToolName(tool) {
|
|
2485
|
+
const aliases = {
|
|
2486
|
+
"claude": "claude",
|
|
2487
|
+
"claude-code": "claude",
|
|
2488
|
+
"claudecode": "claude",
|
|
2489
|
+
"vscode": "vscode",
|
|
2490
|
+
"vs-code": "vscode",
|
|
2491
|
+
"code": "vscode",
|
|
2492
|
+
"copilot": "vscode",
|
|
2493
|
+
"cursor": "cursor",
|
|
2494
|
+
"windsurf": "windsurf"
|
|
2495
|
+
};
|
|
2496
|
+
return aliases[tool] || null;
|
|
2497
|
+
}
|
|
2498
|
+
function isValidMcpTool(tool) {
|
|
2499
|
+
return tool in MCP_TOOL_CONFIGS;
|
|
2500
|
+
}
|
|
2282
2501
|
|
|
2283
2502
|
// src/utils/examples.ts
|
|
2284
2503
|
var EXAMPLES = {
|
|
@@ -2327,9 +2546,9 @@ function exampleExists(name) {
|
|
|
2327
2546
|
}
|
|
2328
2547
|
|
|
2329
2548
|
// src/commands/init.ts
|
|
2330
|
-
var __dirname =
|
|
2331
|
-
var TEMPLATES_DIR =
|
|
2332
|
-
var EXAMPLES_DIR =
|
|
2549
|
+
var __dirname = path13.dirname(fileURLToPath(import.meta.url));
|
|
2550
|
+
var TEMPLATES_DIR = path13.join(__dirname, "..", "templates");
|
|
2551
|
+
var EXAMPLES_DIR = path13.join(TEMPLATES_DIR, "examples");
|
|
2333
2552
|
async function attemptAutoMerge(cwd, promptPath, autoExecute) {
|
|
2334
2553
|
const cliTools = await getCliCapableDetectedTools();
|
|
2335
2554
|
if (cliTools.length === 0) {
|
|
@@ -2338,12 +2557,12 @@ async function attemptAutoMerge(cwd, promptPath, autoExecute) {
|
|
|
2338
2557
|
const tool = cliTools[0];
|
|
2339
2558
|
const displayCmd = getDisplayCommand(tool.tool, promptPath);
|
|
2340
2559
|
console.log("");
|
|
2341
|
-
console.log(
|
|
2560
|
+
console.log(chalk16.cyan(`\u{1F50D} Detected AI CLI: ${tool.config.description}`));
|
|
2342
2561
|
for (const reason of tool.reasons) {
|
|
2343
|
-
console.log(
|
|
2562
|
+
console.log(chalk16.gray(` \u2514\u2500 ${reason}`));
|
|
2344
2563
|
}
|
|
2345
2564
|
console.log("");
|
|
2346
|
-
console.log(
|
|
2565
|
+
console.log(chalk16.gray(`Command: ${displayCmd}`));
|
|
2347
2566
|
console.log("");
|
|
2348
2567
|
let shouldExecute = autoExecute;
|
|
2349
2568
|
if (!autoExecute) {
|
|
@@ -2353,52 +2572,52 @@ async function attemptAutoMerge(cwd, promptPath, autoExecute) {
|
|
|
2353
2572
|
});
|
|
2354
2573
|
}
|
|
2355
2574
|
if (!shouldExecute) {
|
|
2356
|
-
console.log(
|
|
2575
|
+
console.log(chalk16.gray("Skipping auto-merge. Run the command above manually to merge."));
|
|
2357
2576
|
return false;
|
|
2358
2577
|
}
|
|
2359
2578
|
console.log("");
|
|
2360
|
-
console.log(
|
|
2361
|
-
console.log(
|
|
2579
|
+
console.log(chalk16.cyan("\u{1F916} Running AI-assisted merge..."));
|
|
2580
|
+
console.log(chalk16.gray(" (This may take a moment)"));
|
|
2362
2581
|
console.log("");
|
|
2363
2582
|
const result = await executeMergeWithAI(cwd, promptPath, tool.tool);
|
|
2364
2583
|
if (result.success) {
|
|
2365
2584
|
console.log("");
|
|
2366
|
-
console.log(
|
|
2367
|
-
console.log(
|
|
2585
|
+
console.log(chalk16.green("\u2713 AGENTS.md merged successfully!"));
|
|
2586
|
+
console.log(chalk16.gray(" Review changes: git diff AGENTS.md"));
|
|
2368
2587
|
return true;
|
|
2369
2588
|
} else if (result.timedOut) {
|
|
2370
2589
|
console.log("");
|
|
2371
|
-
console.log(
|
|
2372
|
-
console.log(
|
|
2590
|
+
console.log(chalk16.yellow("\u26A0 Merge timed out. Try running the command manually:"));
|
|
2591
|
+
console.log(chalk16.gray(` ${displayCmd}`));
|
|
2373
2592
|
return false;
|
|
2374
2593
|
} else {
|
|
2375
2594
|
console.log("");
|
|
2376
|
-
console.log(
|
|
2377
|
-
console.log(
|
|
2378
|
-
console.log(
|
|
2595
|
+
console.log(chalk16.yellow(`\u26A0 Auto-merge encountered an issue: ${result.error}`));
|
|
2596
|
+
console.log(chalk16.gray(" Try running the command manually:"));
|
|
2597
|
+
console.log(chalk16.gray(` ${displayCmd}`));
|
|
2379
2598
|
return false;
|
|
2380
2599
|
}
|
|
2381
2600
|
}
|
|
2382
2601
|
async function handleReinitialize(cwd, skipPrompts, forceReinit) {
|
|
2383
|
-
const specsDir =
|
|
2602
|
+
const specsDir = path13.join(cwd, "specs");
|
|
2384
2603
|
let specCount = 0;
|
|
2385
2604
|
try {
|
|
2386
|
-
const entries = await
|
|
2605
|
+
const entries = await fs9.readdir(specsDir, { withFileTypes: true });
|
|
2387
2606
|
specCount = entries.filter((e) => e.isDirectory()).length;
|
|
2388
2607
|
} catch {
|
|
2389
2608
|
}
|
|
2390
2609
|
console.log("");
|
|
2391
|
-
console.log(
|
|
2610
|
+
console.log(chalk16.yellow("\u26A0 LeanSpec is already initialized in this directory."));
|
|
2392
2611
|
if (specCount > 0) {
|
|
2393
|
-
console.log(
|
|
2612
|
+
console.log(chalk16.cyan(` Found ${specCount} spec${specCount > 1 ? "s" : ""} in specs/`));
|
|
2394
2613
|
}
|
|
2395
2614
|
console.log("");
|
|
2396
2615
|
if (forceReinit) {
|
|
2397
|
-
console.log(
|
|
2616
|
+
console.log(chalk16.gray("Force flag detected. Resetting configuration..."));
|
|
2398
2617
|
return "reset-config";
|
|
2399
2618
|
}
|
|
2400
2619
|
if (skipPrompts) {
|
|
2401
|
-
console.log(
|
|
2620
|
+
console.log(chalk16.gray("Using safe upgrade (preserving all existing files)"));
|
|
2402
2621
|
return "upgrade";
|
|
2403
2622
|
}
|
|
2404
2623
|
const strategy = await select({
|
|
@@ -2432,15 +2651,15 @@ async function handleReinitialize(cwd, skipPrompts, forceReinit) {
|
|
|
2432
2651
|
warnings.push(`${specCount} spec${specCount > 1 ? "s" : ""} in specs/`);
|
|
2433
2652
|
}
|
|
2434
2653
|
try {
|
|
2435
|
-
await
|
|
2654
|
+
await fs9.access(path13.join(cwd, "AGENTS.md"));
|
|
2436
2655
|
warnings.push("AGENTS.md");
|
|
2437
2656
|
} catch {
|
|
2438
2657
|
}
|
|
2439
2658
|
if (warnings.length > 0) {
|
|
2440
2659
|
console.log("");
|
|
2441
|
-
console.log(
|
|
2660
|
+
console.log(chalk16.red("\u26A0 This will permanently delete:"));
|
|
2442
2661
|
for (const warning of warnings) {
|
|
2443
|
-
console.log(
|
|
2662
|
+
console.log(chalk16.red(` - ${warning}`));
|
|
2444
2663
|
}
|
|
2445
2664
|
console.log("");
|
|
2446
2665
|
const confirmed = await confirm({
|
|
@@ -2448,22 +2667,22 @@ async function handleReinitialize(cwd, skipPrompts, forceReinit) {
|
|
|
2448
2667
|
default: false
|
|
2449
2668
|
});
|
|
2450
2669
|
if (!confirmed) {
|
|
2451
|
-
console.log(
|
|
2670
|
+
console.log(chalk16.gray("Cancelled."));
|
|
2452
2671
|
return "cancel";
|
|
2453
2672
|
}
|
|
2454
2673
|
}
|
|
2455
|
-
console.log(
|
|
2456
|
-
await
|
|
2457
|
-
console.log(
|
|
2674
|
+
console.log(chalk16.gray("Performing full reset..."));
|
|
2675
|
+
await fs9.rm(path13.join(cwd, ".lean-spec"), { recursive: true, force: true });
|
|
2676
|
+
console.log(chalk16.gray(" Removed .lean-spec/"));
|
|
2458
2677
|
try {
|
|
2459
|
-
await
|
|
2460
|
-
console.log(
|
|
2678
|
+
await fs9.rm(specsDir, { recursive: true, force: true });
|
|
2679
|
+
console.log(chalk16.gray(" Removed specs/"));
|
|
2461
2680
|
} catch {
|
|
2462
2681
|
}
|
|
2463
2682
|
for (const file of ["AGENTS.md", "CLAUDE.md", "GEMINI.md"]) {
|
|
2464
2683
|
try {
|
|
2465
|
-
await
|
|
2466
|
-
console.log(
|
|
2684
|
+
await fs9.rm(path13.join(cwd, file), { force: true });
|
|
2685
|
+
console.log(chalk16.gray(` Removed ${file}`));
|
|
2467
2686
|
} catch {
|
|
2468
2687
|
}
|
|
2469
2688
|
}
|
|
@@ -2471,22 +2690,22 @@ async function handleReinitialize(cwd, skipPrompts, forceReinit) {
|
|
|
2471
2690
|
return strategy;
|
|
2472
2691
|
}
|
|
2473
2692
|
async function upgradeConfig(cwd) {
|
|
2474
|
-
const configPath =
|
|
2693
|
+
const configPath = path13.join(cwd, ".lean-spec", "config.json");
|
|
2475
2694
|
let existingConfig;
|
|
2476
2695
|
try {
|
|
2477
|
-
const content = await
|
|
2696
|
+
const content = await fs9.readFile(configPath, "utf-8");
|
|
2478
2697
|
existingConfig = JSON.parse(content);
|
|
2479
2698
|
} catch {
|
|
2480
|
-
console.error(
|
|
2699
|
+
console.error(chalk16.red("Error reading existing config"));
|
|
2481
2700
|
process.exit(1);
|
|
2482
2701
|
}
|
|
2483
|
-
const templateConfigPath =
|
|
2702
|
+
const templateConfigPath = path13.join(TEMPLATES_DIR, "standard", "config.json");
|
|
2484
2703
|
let templateConfig;
|
|
2485
2704
|
try {
|
|
2486
|
-
const content = await
|
|
2705
|
+
const content = await fs9.readFile(templateConfigPath, "utf-8");
|
|
2487
2706
|
templateConfig = JSON.parse(content).config;
|
|
2488
2707
|
} catch {
|
|
2489
|
-
console.error(
|
|
2708
|
+
console.error(chalk16.red("Error reading template config"));
|
|
2490
2709
|
process.exit(1);
|
|
2491
2710
|
}
|
|
2492
2711
|
const upgradedConfig = {
|
|
@@ -2498,70 +2717,70 @@ async function upgradeConfig(cwd) {
|
|
|
2498
2717
|
...existingConfig.structure
|
|
2499
2718
|
}
|
|
2500
2719
|
};
|
|
2501
|
-
const templatesDir =
|
|
2720
|
+
const templatesDir = path13.join(cwd, ".lean-spec", "templates");
|
|
2502
2721
|
try {
|
|
2503
|
-
await
|
|
2722
|
+
await fs9.mkdir(templatesDir, { recursive: true });
|
|
2504
2723
|
} catch {
|
|
2505
2724
|
}
|
|
2506
2725
|
const templateFiles = ["spec-template.md"];
|
|
2507
2726
|
let templatesUpdated = false;
|
|
2508
2727
|
for (const file of templateFiles) {
|
|
2509
|
-
const destPath =
|
|
2728
|
+
const destPath = path13.join(templatesDir, file);
|
|
2510
2729
|
try {
|
|
2511
|
-
await
|
|
2730
|
+
await fs9.access(destPath);
|
|
2512
2731
|
} catch {
|
|
2513
|
-
const srcPath =
|
|
2732
|
+
const srcPath = path13.join(TEMPLATES_DIR, "standard", "files", "README.md");
|
|
2514
2733
|
try {
|
|
2515
|
-
await
|
|
2734
|
+
await fs9.copyFile(srcPath, destPath);
|
|
2516
2735
|
templatesUpdated = true;
|
|
2517
|
-
console.log(
|
|
2736
|
+
console.log(chalk16.green(`\u2713 Added missing template: ${file}`));
|
|
2518
2737
|
} catch {
|
|
2519
2738
|
}
|
|
2520
2739
|
}
|
|
2521
2740
|
}
|
|
2522
|
-
const agentsPath =
|
|
2741
|
+
const agentsPath = path13.join(cwd, "AGENTS.md");
|
|
2523
2742
|
let agentsCreated = false;
|
|
2524
2743
|
let agentsPreserved = false;
|
|
2525
2744
|
try {
|
|
2526
|
-
await
|
|
2745
|
+
await fs9.access(agentsPath);
|
|
2527
2746
|
agentsPreserved = true;
|
|
2528
2747
|
} catch {
|
|
2529
|
-
const templateDir =
|
|
2530
|
-
const agentsSourcePath =
|
|
2748
|
+
const templateDir = path13.join(TEMPLATES_DIR, "standard");
|
|
2749
|
+
const agentsSourcePath = path13.join(templateDir, "AGENTS.md");
|
|
2531
2750
|
try {
|
|
2532
2751
|
const projectName = await getProjectName2(cwd);
|
|
2533
|
-
let agentsContent = await
|
|
2752
|
+
let agentsContent = await fs9.readFile(agentsSourcePath, "utf-8");
|
|
2534
2753
|
agentsContent = agentsContent.replace(/\{project_name\}/g, projectName);
|
|
2535
|
-
await
|
|
2754
|
+
await fs9.writeFile(agentsPath, agentsContent, "utf-8");
|
|
2536
2755
|
agentsCreated = true;
|
|
2537
|
-
console.log(
|
|
2756
|
+
console.log(chalk16.green("\u2713 Created missing AGENTS.md"));
|
|
2538
2757
|
} catch (error) {
|
|
2539
|
-
console.log(
|
|
2758
|
+
console.log(chalk16.yellow(`\u26A0 Could not create AGENTS.md: ${error}`));
|
|
2540
2759
|
}
|
|
2541
2760
|
}
|
|
2542
2761
|
await saveConfig(upgradedConfig, cwd);
|
|
2543
2762
|
console.log("");
|
|
2544
|
-
console.log(
|
|
2763
|
+
console.log(chalk16.green("\u2713 Configuration upgraded!"));
|
|
2545
2764
|
console.log("");
|
|
2546
|
-
console.log(
|
|
2547
|
-
console.log(
|
|
2765
|
+
console.log(chalk16.gray("What was updated:"));
|
|
2766
|
+
console.log(chalk16.gray(" - Config merged with latest defaults"));
|
|
2548
2767
|
if (templatesUpdated) {
|
|
2549
|
-
console.log(
|
|
2768
|
+
console.log(chalk16.gray(" - Missing templates added"));
|
|
2550
2769
|
}
|
|
2551
2770
|
if (agentsCreated) {
|
|
2552
|
-
console.log(
|
|
2771
|
+
console.log(chalk16.gray(" - AGENTS.md created (was missing)"));
|
|
2553
2772
|
}
|
|
2554
2773
|
console.log("");
|
|
2555
|
-
console.log(
|
|
2556
|
-
console.log(
|
|
2774
|
+
console.log(chalk16.gray("What was preserved:"));
|
|
2775
|
+
console.log(chalk16.gray(" - Your specs/ directory"));
|
|
2557
2776
|
if (agentsPreserved) {
|
|
2558
|
-
console.log(
|
|
2777
|
+
console.log(chalk16.gray(" - Your AGENTS.md"));
|
|
2559
2778
|
}
|
|
2560
|
-
console.log(
|
|
2779
|
+
console.log(chalk16.gray(" - Your custom settings"));
|
|
2561
2780
|
console.log("");
|
|
2562
2781
|
}
|
|
2563
2782
|
function initCommand() {
|
|
2564
|
-
return new Command("init").description("Initialize LeanSpec in current directory").option("-y, --yes", "Skip prompts and use defaults (quick start with standard template)").option("-f, --force", "Force re-initialization (resets config, keeps specs)").option("--template <name>", "Use specific template (standard or detailed)").option("--example [name]", "Scaffold an example project for tutorials (interactive if no name provided)").option("--name <dirname>", "Custom directory name for example project").option("--list", "List available example projects").option("--agent-tools <tools>", 'AI tools to create symlinks for (comma-separated: claude,gemini,copilot or "all" or "none")').action(async (options) => {
|
|
2783
|
+
return new Command("init").description("Initialize LeanSpec in current directory").option("-y, --yes", "Skip prompts and use defaults (quick start with standard template)").option("-f, --force", "Force re-initialization (resets config, keeps specs)").option("--template <name>", "Use specific template (standard or detailed)").option("--example [name]", "Scaffold an example project for tutorials (interactive if no name provided)").option("--name <dirname>", "Custom directory name for example project").option("--list", "List available example projects").option("--agent-tools <tools>", 'AI tools to create symlinks for (comma-separated: claude,gemini,copilot or "all" or "none")').option("--mcp-config <tools>", 'Configure MCP server for AI tools (comma-separated: claude-code,vscode,cursor,windsurf or "all" or "none")').action(async (options) => {
|
|
2565
2784
|
if (options.list) {
|
|
2566
2785
|
await listExamples();
|
|
2567
2786
|
return;
|
|
@@ -2570,15 +2789,15 @@ function initCommand() {
|
|
|
2570
2789
|
await scaffoldExample(options.example, options.name);
|
|
2571
2790
|
return;
|
|
2572
2791
|
}
|
|
2573
|
-
await initProject(options.yes, options.template, options.agentTools, options.force);
|
|
2792
|
+
await initProject(options.yes, options.template, options.agentTools, options.force, options.mcpConfig);
|
|
2574
2793
|
});
|
|
2575
2794
|
}
|
|
2576
|
-
async function initProject(skipPrompts = false, templateOption, agentToolsOption, forceReinit = false) {
|
|
2795
|
+
async function initProject(skipPrompts = false, templateOption, agentToolsOption, forceReinit = false, mcpConfigOption) {
|
|
2577
2796
|
const cwd = process.cwd();
|
|
2578
|
-
const configPath =
|
|
2797
|
+
const configPath = path13.join(cwd, ".lean-spec", "config.json");
|
|
2579
2798
|
let isAlreadyInitialized = false;
|
|
2580
2799
|
try {
|
|
2581
|
-
await
|
|
2800
|
+
await fs9.access(configPath);
|
|
2582
2801
|
isAlreadyInitialized = true;
|
|
2583
2802
|
} catch {
|
|
2584
2803
|
}
|
|
@@ -2592,16 +2811,17 @@ async function initProject(skipPrompts = false, templateOption, agentToolsOption
|
|
|
2592
2811
|
return;
|
|
2593
2812
|
}
|
|
2594
2813
|
if (strategy === "reset-config") {
|
|
2595
|
-
await
|
|
2596
|
-
console.log(
|
|
2814
|
+
await fs9.rm(path13.join(cwd, ".lean-spec"), { recursive: true, force: true });
|
|
2815
|
+
console.log(chalk16.gray("Removed .lean-spec/ configuration"));
|
|
2597
2816
|
}
|
|
2598
2817
|
}
|
|
2599
2818
|
console.log("");
|
|
2600
|
-
console.log(
|
|
2819
|
+
console.log(chalk16.green("Welcome to LeanSpec!"));
|
|
2601
2820
|
console.log("");
|
|
2602
2821
|
let setupMode = "quick";
|
|
2603
2822
|
let templateName = templateOption || "standard";
|
|
2604
2823
|
let selectedAgentTools = [];
|
|
2824
|
+
let selectedMcpTools = [];
|
|
2605
2825
|
if (agentToolsOption) {
|
|
2606
2826
|
if (agentToolsOption === "all") {
|
|
2607
2827
|
selectedAgentTools = Object.keys(AI_TOOL_CONFIGS);
|
|
@@ -2611,11 +2831,24 @@ async function initProject(skipPrompts = false, templateOption, agentToolsOption
|
|
|
2611
2831
|
selectedAgentTools = agentToolsOption.split(",").map((t) => t.trim());
|
|
2612
2832
|
}
|
|
2613
2833
|
}
|
|
2834
|
+
if (mcpConfigOption) {
|
|
2835
|
+
const parsed = parseMcpConfigFlag(mcpConfigOption);
|
|
2836
|
+
if (parsed === "all") {
|
|
2837
|
+
selectedMcpTools = Object.keys(MCP_TOOL_CONFIGS);
|
|
2838
|
+
} else if (parsed === "none") {
|
|
2839
|
+
selectedMcpTools = [];
|
|
2840
|
+
} else {
|
|
2841
|
+
selectedMcpTools = parsed;
|
|
2842
|
+
}
|
|
2843
|
+
}
|
|
2614
2844
|
if (skipPrompts) {
|
|
2615
|
-
console.log(
|
|
2845
|
+
console.log(chalk16.gray("Using defaults: quick start with standard template"));
|
|
2616
2846
|
if (!agentToolsOption) {
|
|
2617
2847
|
selectedAgentTools = ["copilot"];
|
|
2618
2848
|
}
|
|
2849
|
+
if (!mcpConfigOption) {
|
|
2850
|
+
selectedMcpTools = ["claude"];
|
|
2851
|
+
}
|
|
2619
2852
|
console.log("");
|
|
2620
2853
|
} else if (!templateOption) {
|
|
2621
2854
|
setupMode = await select({
|
|
@@ -2654,24 +2887,24 @@ async function initProject(skipPrompts = false, templateOption, agentToolsOption
|
|
|
2654
2887
|
}
|
|
2655
2888
|
}
|
|
2656
2889
|
if (templateName === "minimal") {
|
|
2657
|
-
console.log(
|
|
2658
|
-
console.log(
|
|
2890
|
+
console.log(chalk16.yellow('\u26A0 The "minimal" template has been removed.'));
|
|
2891
|
+
console.log(chalk16.gray(' Using "standard" template instead (same lightweight approach).'));
|
|
2659
2892
|
console.log("");
|
|
2660
2893
|
templateName = "standard";
|
|
2661
2894
|
} else if (templateName === "enterprise") {
|
|
2662
|
-
console.log(
|
|
2663
|
-
console.log(
|
|
2895
|
+
console.log(chalk16.yellow('\u26A0 The "enterprise" template has been renamed to "detailed".'));
|
|
2896
|
+
console.log(chalk16.gray(' Using "detailed" template (sub-spec structure for complex specs).'));
|
|
2664
2897
|
console.log("");
|
|
2665
2898
|
templateName = "detailed";
|
|
2666
2899
|
}
|
|
2667
|
-
const templateDir =
|
|
2668
|
-
const templateConfigPath =
|
|
2900
|
+
const templateDir = path13.join(TEMPLATES_DIR, templateName);
|
|
2901
|
+
const templateConfigPath = path13.join(templateDir, "config.json");
|
|
2669
2902
|
let templateConfig;
|
|
2670
2903
|
try {
|
|
2671
|
-
const content = await
|
|
2904
|
+
const content = await fs9.readFile(templateConfigPath, "utf-8");
|
|
2672
2905
|
templateConfig = JSON.parse(content).config;
|
|
2673
2906
|
} catch {
|
|
2674
|
-
console.error(
|
|
2907
|
+
console.error(chalk16.red(`Error: Template not found: ${templateName}`));
|
|
2675
2908
|
process.exit(1);
|
|
2676
2909
|
}
|
|
2677
2910
|
let patternChoice = "simple";
|
|
@@ -2714,9 +2947,9 @@ async function initProject(skipPrompts = false, templateOption, agentToolsOption
|
|
|
2714
2947
|
templateConfig.structure.prefix = "{YYYYMMDD}-";
|
|
2715
2948
|
} else if (patternChoice === "custom") {
|
|
2716
2949
|
console.log("");
|
|
2717
|
-
console.log(
|
|
2718
|
-
console.log(
|
|
2719
|
-
console.log(
|
|
2950
|
+
console.log(chalk16.yellow("\u26A0 Custom pattern input is not yet implemented."));
|
|
2951
|
+
console.log(chalk16.gray(" You can manually edit .lean-spec/config.json after initialization."));
|
|
2952
|
+
console.log(chalk16.gray(" Using simple pattern for now."));
|
|
2720
2953
|
console.log("");
|
|
2721
2954
|
templateConfig.structure.pattern = "flat";
|
|
2722
2955
|
templateConfig.structure.prefix = "";
|
|
@@ -2726,12 +2959,12 @@ async function initProject(skipPrompts = false, templateOption, agentToolsOption
|
|
|
2726
2959
|
const anyDetected = detectionResults.some((r) => r.detected);
|
|
2727
2960
|
if (anyDetected) {
|
|
2728
2961
|
console.log("");
|
|
2729
|
-
console.log(
|
|
2962
|
+
console.log(chalk16.cyan("\u{1F50D} Detected AI tools:"));
|
|
2730
2963
|
for (const result of detectionResults) {
|
|
2731
2964
|
if (result.detected) {
|
|
2732
|
-
console.log(
|
|
2965
|
+
console.log(chalk16.gray(` ${AI_TOOL_CONFIGS[result.tool].description}`));
|
|
2733
2966
|
for (const reason of result.reasons) {
|
|
2734
|
-
console.log(
|
|
2967
|
+
console.log(chalk16.gray(` \u2514\u2500 ${reason}`));
|
|
2735
2968
|
}
|
|
2736
2969
|
}
|
|
2737
2970
|
}
|
|
@@ -2744,8 +2977,8 @@ async function initProject(skipPrompts = false, templateOption, agentToolsOption
|
|
|
2744
2977
|
}));
|
|
2745
2978
|
if (symlinkTools.length > 0) {
|
|
2746
2979
|
console.log("");
|
|
2747
|
-
console.log(
|
|
2748
|
-
console.log(
|
|
2980
|
+
console.log(chalk16.gray("AGENTS.md will be created as the primary instruction file."));
|
|
2981
|
+
console.log(chalk16.gray("Some AI tools (Claude Code, Gemini CLI) use their own filenames."));
|
|
2749
2982
|
console.log("");
|
|
2750
2983
|
const symlinkSelection = await checkbox({
|
|
2751
2984
|
message: "Create symlinks for additional AI tools?",
|
|
@@ -2755,56 +2988,86 @@ async function initProject(skipPrompts = false, templateOption, agentToolsOption
|
|
|
2755
2988
|
} else {
|
|
2756
2989
|
selectedAgentTools = [];
|
|
2757
2990
|
}
|
|
2991
|
+
if (!mcpConfigOption) {
|
|
2992
|
+
const { defaults: mcpDefaults, detected: mcpDetectionResults } = await getDefaultMcpToolSelection(cwd);
|
|
2993
|
+
const anyMcpDetected = mcpDetectionResults.some((r) => r.detected);
|
|
2994
|
+
if (anyMcpDetected) {
|
|
2995
|
+
console.log("");
|
|
2996
|
+
console.log(chalk16.cyan("\u{1F50D} Detected MCP-capable tools:"));
|
|
2997
|
+
for (const result of mcpDetectionResults) {
|
|
2998
|
+
if (result.detected) {
|
|
2999
|
+
console.log(chalk16.gray(` ${MCP_TOOL_CONFIGS[result.tool].description}`));
|
|
3000
|
+
for (const reason of result.reasons) {
|
|
3001
|
+
console.log(chalk16.gray(` \u2514\u2500 ${reason}`));
|
|
3002
|
+
}
|
|
3003
|
+
}
|
|
3004
|
+
}
|
|
3005
|
+
}
|
|
3006
|
+
console.log("");
|
|
3007
|
+
console.log(chalk16.gray("MCP (Model Context Protocol) provides richer AI integration than CLI alone."));
|
|
3008
|
+
console.log(chalk16.gray("Configure MCP server to enable structured data exchange with your AI tools."));
|
|
3009
|
+
console.log("");
|
|
3010
|
+
const mcpToolChoices = Object.entries(MCP_TOOL_CONFIGS).map(([key, config]) => ({
|
|
3011
|
+
name: config.description,
|
|
3012
|
+
value: key,
|
|
3013
|
+
checked: mcpDefaults.includes(key)
|
|
3014
|
+
}));
|
|
3015
|
+
const mcpSelection = await checkbox({
|
|
3016
|
+
message: "Configure MCP server for which tools?",
|
|
3017
|
+
choices: mcpToolChoices
|
|
3018
|
+
});
|
|
3019
|
+
selectedMcpTools = mcpSelection;
|
|
3020
|
+
}
|
|
2758
3021
|
}
|
|
2759
|
-
const templatesDir =
|
|
3022
|
+
const templatesDir = path13.join(cwd, ".lean-spec", "templates");
|
|
2760
3023
|
try {
|
|
2761
|
-
await
|
|
3024
|
+
await fs9.mkdir(templatesDir, { recursive: true });
|
|
2762
3025
|
} catch (error) {
|
|
2763
|
-
console.error(
|
|
3026
|
+
console.error(chalk16.red("Error creating templates directory:"), error);
|
|
2764
3027
|
process.exit(1);
|
|
2765
3028
|
}
|
|
2766
|
-
const templateFilesDir =
|
|
3029
|
+
const templateFilesDir = path13.join(templateDir, "files");
|
|
2767
3030
|
try {
|
|
2768
|
-
const files = await
|
|
3031
|
+
const files = await fs9.readdir(templateFilesDir);
|
|
2769
3032
|
if (templateName === "standard") {
|
|
2770
|
-
const readmePath =
|
|
2771
|
-
const targetSpecPath =
|
|
2772
|
-
await
|
|
2773
|
-
console.log(
|
|
3033
|
+
const readmePath = path13.join(templateFilesDir, "README.md");
|
|
3034
|
+
const targetSpecPath = path13.join(templatesDir, "spec-template.md");
|
|
3035
|
+
await fs9.copyFile(readmePath, targetSpecPath);
|
|
3036
|
+
console.log(chalk16.green("\u2713 Created .lean-spec/templates/spec-template.md"));
|
|
2774
3037
|
templateConfig.template = "spec-template.md";
|
|
2775
3038
|
templateConfig.templates = {
|
|
2776
3039
|
default: "spec-template.md"
|
|
2777
3040
|
};
|
|
2778
3041
|
} else if (templateName === "detailed") {
|
|
2779
3042
|
for (const file of files) {
|
|
2780
|
-
const srcPath =
|
|
2781
|
-
const destPath =
|
|
2782
|
-
await
|
|
3043
|
+
const srcPath = path13.join(templateFilesDir, file);
|
|
3044
|
+
const destPath = path13.join(templatesDir, file);
|
|
3045
|
+
await fs9.copyFile(srcPath, destPath);
|
|
2783
3046
|
}
|
|
2784
|
-
console.log(
|
|
2785
|
-
console.log(
|
|
3047
|
+
console.log(chalk16.green(`\u2713 Created .lean-spec/templates/ with ${files.length} files`));
|
|
3048
|
+
console.log(chalk16.gray(` Files: ${files.join(", ")}`));
|
|
2786
3049
|
templateConfig.template = "README.md";
|
|
2787
3050
|
templateConfig.templates = {
|
|
2788
3051
|
default: "README.md"
|
|
2789
3052
|
};
|
|
2790
3053
|
}
|
|
2791
3054
|
} catch (error) {
|
|
2792
|
-
console.error(
|
|
3055
|
+
console.error(chalk16.red("Error copying template files:"), error);
|
|
2793
3056
|
process.exit(1);
|
|
2794
3057
|
}
|
|
2795
3058
|
await saveConfig(templateConfig, cwd);
|
|
2796
|
-
console.log(
|
|
3059
|
+
console.log(chalk16.green("\u2713 Created .lean-spec/config.json"));
|
|
2797
3060
|
const existingFiles = await detectExistingSystemPrompts(cwd);
|
|
2798
3061
|
let skipFiles = [];
|
|
2799
3062
|
let mergeCompleted = false;
|
|
2800
3063
|
if (existingFiles.length > 0) {
|
|
2801
3064
|
console.log("");
|
|
2802
|
-
console.log(
|
|
3065
|
+
console.log(chalk16.yellow(`Found existing: ${existingFiles.join(", ")}`));
|
|
2803
3066
|
if (skipPrompts) {
|
|
2804
|
-
console.log(
|
|
3067
|
+
console.log(chalk16.gray("Using AI-Assisted Merge for existing AGENTS.md"));
|
|
2805
3068
|
const projectName2 = await getProjectName2(cwd);
|
|
2806
3069
|
await handleExistingFiles("merge-ai", existingFiles, templateDir, cwd, { project_name: projectName2 });
|
|
2807
|
-
const promptPath =
|
|
3070
|
+
const promptPath = path13.join(cwd, ".lean-spec", "MERGE-AGENTS-PROMPT.md");
|
|
2808
3071
|
mergeCompleted = await attemptAutoMerge(
|
|
2809
3072
|
cwd,
|
|
2810
3073
|
promptPath,
|
|
@@ -2842,7 +3105,7 @@ async function initProject(skipPrompts = false, templateOption, agentToolsOption
|
|
|
2842
3105
|
if (action === "skip") {
|
|
2843
3106
|
skipFiles = existingFiles;
|
|
2844
3107
|
} else if (action === "merge-ai") {
|
|
2845
|
-
const promptPath =
|
|
3108
|
+
const promptPath = path13.join(cwd, ".lean-spec", "MERGE-AGENTS-PROMPT.md");
|
|
2846
3109
|
mergeCompleted = await attemptAutoMerge(
|
|
2847
3110
|
cwd,
|
|
2848
3111
|
promptPath,
|
|
@@ -2854,75 +3117,81 @@ async function initProject(skipPrompts = false, templateOption, agentToolsOption
|
|
|
2854
3117
|
}
|
|
2855
3118
|
const projectName = await getProjectName2(cwd);
|
|
2856
3119
|
if (!skipFiles.includes("AGENTS.md") && !mergeCompleted) {
|
|
2857
|
-
const agentsSourcePath =
|
|
2858
|
-
const agentsTargetPath =
|
|
3120
|
+
const agentsSourcePath = path13.join(templateDir, "AGENTS.md");
|
|
3121
|
+
const agentsTargetPath = path13.join(cwd, "AGENTS.md");
|
|
2859
3122
|
try {
|
|
2860
|
-
let agentsContent = await
|
|
3123
|
+
let agentsContent = await fs9.readFile(agentsSourcePath, "utf-8");
|
|
2861
3124
|
agentsContent = agentsContent.replace(/\{project_name\}/g, projectName);
|
|
2862
|
-
await
|
|
2863
|
-
console.log(
|
|
3125
|
+
await fs9.writeFile(agentsTargetPath, agentsContent, "utf-8");
|
|
3126
|
+
console.log(chalk16.green("\u2713 Created AGENTS.md"));
|
|
2864
3127
|
} catch (error) {
|
|
2865
|
-
console.error(
|
|
3128
|
+
console.error(chalk16.red("Error copying AGENTS.md:"), error);
|
|
2866
3129
|
process.exit(1);
|
|
2867
3130
|
}
|
|
2868
3131
|
if (selectedAgentTools.length > 0) {
|
|
2869
3132
|
const symlinkResults = await createAgentToolSymlinks(cwd, selectedAgentTools);
|
|
2870
3133
|
for (const result of symlinkResults) {
|
|
2871
3134
|
if (result.created) {
|
|
2872
|
-
console.log(
|
|
3135
|
+
console.log(chalk16.green(`\u2713 Created ${result.file} \u2192 AGENTS.md`));
|
|
2873
3136
|
} else if (result.skipped) {
|
|
2874
|
-
console.log(
|
|
3137
|
+
console.log(chalk16.yellow(`\u26A0 Skipped ${result.file} (already exists)`));
|
|
2875
3138
|
} else if (result.error) {
|
|
2876
|
-
console.log(
|
|
3139
|
+
console.log(chalk16.yellow(`\u26A0 Could not create ${result.file}: ${result.error}`));
|
|
2877
3140
|
}
|
|
2878
3141
|
}
|
|
2879
3142
|
}
|
|
2880
3143
|
}
|
|
2881
|
-
const filesDir =
|
|
3144
|
+
const filesDir = path13.join(templateDir, "files");
|
|
2882
3145
|
try {
|
|
2883
|
-
const filesToCopy = await
|
|
3146
|
+
const filesToCopy = await fs9.readdir(filesDir);
|
|
2884
3147
|
const hasOtherFiles = filesToCopy.some((f) => !f.match(/\.(md)$/i) || !["README.md", "DESIGN.md", "PLAN.md", "TEST.md"].includes(f));
|
|
2885
3148
|
if (hasOtherFiles) {
|
|
2886
3149
|
await copyDirectory(filesDir, cwd, [...skipFiles, "README.md", "DESIGN.md", "PLAN.md", "TEST.md"], { project_name: projectName });
|
|
2887
3150
|
}
|
|
2888
|
-
console.log(
|
|
3151
|
+
console.log(chalk16.green("\u2713 Initialized project structure"));
|
|
2889
3152
|
} catch (error) {
|
|
2890
|
-
console.error(
|
|
3153
|
+
console.error(chalk16.red("Error copying template files:"), error);
|
|
2891
3154
|
process.exit(1);
|
|
2892
3155
|
}
|
|
2893
|
-
const specsDir =
|
|
3156
|
+
const specsDir = path13.join(cwd, "specs");
|
|
2894
3157
|
try {
|
|
2895
|
-
await
|
|
2896
|
-
console.log(
|
|
3158
|
+
await fs9.mkdir(specsDir, { recursive: true });
|
|
3159
|
+
console.log(chalk16.green("\u2713 Created specs/ directory"));
|
|
2897
3160
|
} catch (error) {
|
|
2898
|
-
console.error(
|
|
3161
|
+
console.error(chalk16.red("Error creating specs directory:"), error);
|
|
2899
3162
|
process.exit(1);
|
|
2900
3163
|
}
|
|
3164
|
+
if (selectedMcpTools.length > 0) {
|
|
3165
|
+
console.log("");
|
|
3166
|
+
console.log(chalk16.cyan("Configuring MCP server..."));
|
|
3167
|
+
const mcpResults = await createMcpConfigs(cwd, selectedMcpTools);
|
|
3168
|
+
printMcpConfigResults(mcpResults);
|
|
3169
|
+
}
|
|
2901
3170
|
console.log("");
|
|
2902
|
-
console.log(
|
|
3171
|
+
console.log(chalk16.green("\u2713 LeanSpec initialized!"));
|
|
2903
3172
|
console.log("");
|
|
2904
|
-
console.log(
|
|
3173
|
+
console.log(chalk16.cyan("You're ready to go!") + chalk16.gray(" Ask your AI to create a spec for your next feature."));
|
|
2905
3174
|
console.log("");
|
|
2906
|
-
console.log(
|
|
2907
|
-
console.log(
|
|
3175
|
+
console.log(chalk16.gray('Example: "Create a spec for user authentication"'));
|
|
3176
|
+
console.log(chalk16.gray("Learn more: https://lean-spec.dev/docs/guide/getting-started"));
|
|
2908
3177
|
console.log("");
|
|
2909
3178
|
}
|
|
2910
3179
|
async function listExamples() {
|
|
2911
3180
|
const examples = getExamplesList();
|
|
2912
3181
|
console.log("");
|
|
2913
|
-
console.log(
|
|
3182
|
+
console.log(chalk16.bold("Available Examples:"));
|
|
2914
3183
|
console.log("");
|
|
2915
3184
|
for (const example of examples) {
|
|
2916
|
-
const difficultyColor = example.difficulty === "beginner" ?
|
|
2917
|
-
console.log(
|
|
3185
|
+
const difficultyColor = example.difficulty === "beginner" ? chalk16.green : example.difficulty === "intermediate" ? chalk16.yellow : chalk16.red;
|
|
3186
|
+
console.log(chalk16.cyan(` ${example.name}`));
|
|
2918
3187
|
console.log(` ${example.description}`);
|
|
2919
3188
|
console.log(` ${difficultyColor(example.difficulty)} \u2022 ${example.tech.join(", ")} \u2022 ~${example.lines} lines`);
|
|
2920
|
-
console.log(` Tutorial: ${
|
|
3189
|
+
console.log(` Tutorial: ${chalk16.gray(example.tutorialUrl)}`);
|
|
2921
3190
|
console.log("");
|
|
2922
3191
|
}
|
|
2923
3192
|
console.log("Usage:");
|
|
2924
|
-
console.log(
|
|
2925
|
-
console.log(
|
|
3193
|
+
console.log(chalk16.gray(" lean-spec init --example <name>"));
|
|
3194
|
+
console.log(chalk16.gray(" lean-spec init --example dark-theme"));
|
|
2926
3195
|
console.log("");
|
|
2927
3196
|
}
|
|
2928
3197
|
async function scaffoldExample(exampleName, customName) {
|
|
@@ -2930,7 +3199,7 @@ async function scaffoldExample(exampleName, customName) {
|
|
|
2930
3199
|
exampleName = await selectExample();
|
|
2931
3200
|
}
|
|
2932
3201
|
if (!exampleExists(exampleName)) {
|
|
2933
|
-
console.error(
|
|
3202
|
+
console.error(chalk16.red(`Error: Example "${exampleName}" not found.`));
|
|
2934
3203
|
console.log("");
|
|
2935
3204
|
console.log("Available examples:");
|
|
2936
3205
|
getExamplesList().forEach((ex) => {
|
|
@@ -2942,63 +3211,63 @@ async function scaffoldExample(exampleName, customName) {
|
|
|
2942
3211
|
}
|
|
2943
3212
|
const example = getExample(exampleName);
|
|
2944
3213
|
const targetDirName = customName || exampleName;
|
|
2945
|
-
const targetPath =
|
|
3214
|
+
const targetPath = path13.join(process.cwd(), targetDirName);
|
|
2946
3215
|
try {
|
|
2947
|
-
const files = await
|
|
3216
|
+
const files = await fs9.readdir(targetPath);
|
|
2948
3217
|
const nonGitFiles = files.filter((f) => f !== ".git");
|
|
2949
3218
|
if (nonGitFiles.length > 0) {
|
|
2950
|
-
console.error(
|
|
2951
|
-
console.log(
|
|
3219
|
+
console.error(chalk16.red(`Error: Directory "${targetDirName}" already exists and is not empty.`));
|
|
3220
|
+
console.log(chalk16.gray("Choose a different name with --name option."));
|
|
2952
3221
|
process.exit(1);
|
|
2953
3222
|
}
|
|
2954
3223
|
} catch {
|
|
2955
3224
|
}
|
|
2956
3225
|
console.log("");
|
|
2957
|
-
console.log(
|
|
2958
|
-
console.log(
|
|
3226
|
+
console.log(chalk16.green(`Setting up example: ${example.title}`));
|
|
3227
|
+
console.log(chalk16.gray(example.description));
|
|
2959
3228
|
console.log("");
|
|
2960
|
-
await
|
|
2961
|
-
console.log(
|
|
2962
|
-
const examplePath =
|
|
3229
|
+
await fs9.mkdir(targetPath, { recursive: true });
|
|
3230
|
+
console.log(chalk16.green(`\u2713 Created directory: ${targetDirName}/`));
|
|
3231
|
+
const examplePath = path13.join(EXAMPLES_DIR, exampleName);
|
|
2963
3232
|
await copyDirectoryRecursive(examplePath, targetPath);
|
|
2964
|
-
console.log(
|
|
3233
|
+
console.log(chalk16.green("\u2713 Copied example project"));
|
|
2965
3234
|
const originalCwd = process.cwd();
|
|
2966
3235
|
try {
|
|
2967
3236
|
process.chdir(targetPath);
|
|
2968
|
-
console.log(
|
|
3237
|
+
console.log(chalk16.gray("Initializing LeanSpec..."));
|
|
2969
3238
|
await initProject(true);
|
|
2970
|
-
console.log(
|
|
3239
|
+
console.log(chalk16.green("\u2713 Initialized LeanSpec"));
|
|
2971
3240
|
} catch (error) {
|
|
2972
|
-
console.error(
|
|
3241
|
+
console.error(chalk16.red("Error initializing LeanSpec:"), error);
|
|
2973
3242
|
process.exit(1);
|
|
2974
3243
|
} finally {
|
|
2975
3244
|
process.chdir(originalCwd);
|
|
2976
3245
|
}
|
|
2977
3246
|
const packageManager = await detectPackageManager();
|
|
2978
|
-
console.log(
|
|
3247
|
+
console.log(chalk16.gray(`Installing dependencies with ${packageManager}...`));
|
|
2979
3248
|
try {
|
|
2980
3249
|
const { execSync: execSync3 } = await import('child_process');
|
|
2981
3250
|
execSync3(`${packageManager} install`, {
|
|
2982
3251
|
cwd: targetPath,
|
|
2983
3252
|
stdio: "inherit"
|
|
2984
3253
|
});
|
|
2985
|
-
console.log(
|
|
3254
|
+
console.log(chalk16.green("\u2713 Installed dependencies"));
|
|
2986
3255
|
} catch (error) {
|
|
2987
|
-
console.log(
|
|
2988
|
-
console.log(
|
|
3256
|
+
console.log(chalk16.yellow("\u26A0 Failed to install dependencies automatically"));
|
|
3257
|
+
console.log(chalk16.gray(` Run: cd ${targetDirName} && ${packageManager} install`));
|
|
2989
3258
|
}
|
|
2990
3259
|
console.log("");
|
|
2991
|
-
console.log(
|
|
3260
|
+
console.log(chalk16.green("\u2713 Example project ready!"));
|
|
2992
3261
|
console.log("");
|
|
2993
|
-
console.log(
|
|
2994
|
-
console.log(
|
|
2995
|
-
console.log(
|
|
3262
|
+
console.log(chalk16.gray("Created:"));
|
|
3263
|
+
console.log(chalk16.gray(` - Application code (${example.tech.join(", ")})`));
|
|
3264
|
+
console.log(chalk16.gray(" - LeanSpec files (AGENTS.md, .lean-spec/, specs/)"));
|
|
2996
3265
|
console.log("");
|
|
2997
3266
|
console.log("Next steps:");
|
|
2998
|
-
console.log(
|
|
2999
|
-
console.log(
|
|
3000
|
-
console.log(
|
|
3001
|
-
console.log(
|
|
3267
|
+
console.log(chalk16.cyan(` 1. cd ${targetDirName}`));
|
|
3268
|
+
console.log(chalk16.cyan(" 2. Open this project in your editor"));
|
|
3269
|
+
console.log(chalk16.cyan(` 3. Follow the tutorial: ${example.tutorialUrl}`));
|
|
3270
|
+
console.log(chalk16.cyan(` 4. Ask your AI: "Help me with this tutorial using LeanSpec"`));
|
|
3002
3271
|
console.log("");
|
|
3003
3272
|
}
|
|
3004
3273
|
async function selectExample() {
|
|
@@ -3017,27 +3286,27 @@ async function selectExample() {
|
|
|
3017
3286
|
return choice;
|
|
3018
3287
|
}
|
|
3019
3288
|
async function copyDirectoryRecursive(src, dest) {
|
|
3020
|
-
const entries = await
|
|
3289
|
+
const entries = await fs9.readdir(src, { withFileTypes: true });
|
|
3021
3290
|
for (const entry of entries) {
|
|
3022
|
-
const srcPath =
|
|
3023
|
-
const destPath =
|
|
3291
|
+
const srcPath = path13.join(src, entry.name);
|
|
3292
|
+
const destPath = path13.join(dest, entry.name);
|
|
3024
3293
|
if (entry.isDirectory()) {
|
|
3025
|
-
await
|
|
3294
|
+
await fs9.mkdir(destPath, { recursive: true });
|
|
3026
3295
|
await copyDirectoryRecursive(srcPath, destPath);
|
|
3027
3296
|
} else {
|
|
3028
|
-
await
|
|
3297
|
+
await fs9.copyFile(srcPath, destPath);
|
|
3029
3298
|
}
|
|
3030
3299
|
}
|
|
3031
3300
|
}
|
|
3032
3301
|
async function detectPackageManager() {
|
|
3033
3302
|
const cwd = process.cwd();
|
|
3034
3303
|
try {
|
|
3035
|
-
await
|
|
3304
|
+
await fs9.access(path13.join(cwd, "..", "pnpm-lock.yaml"));
|
|
3036
3305
|
return "pnpm";
|
|
3037
3306
|
} catch {
|
|
3038
3307
|
}
|
|
3039
3308
|
try {
|
|
3040
|
-
await
|
|
3309
|
+
await fs9.access(path13.join(cwd, "..", "yarn.lock"));
|
|
3041
3310
|
return "yarn";
|
|
3042
3311
|
} catch {
|
|
3043
3312
|
}
|
|
@@ -3055,7 +3324,7 @@ async function showFiles(specPath, options = {}) {
|
|
|
3055
3324
|
await autoCheckIfEnabled();
|
|
3056
3325
|
const config = await loadConfig();
|
|
3057
3326
|
const cwd = process.cwd();
|
|
3058
|
-
const specsDir =
|
|
3327
|
+
const specsDir = path13.join(cwd, config.specsDir);
|
|
3059
3328
|
const resolvedPath = await resolveSpecPath(specPath, cwd, specsDir);
|
|
3060
3329
|
if (!resolvedPath) {
|
|
3061
3330
|
throw new Error(`Spec not found: ${sanitizeUserInput(specPath)}. Try using the full path or spec name (e.g., 001-my-spec)`);
|
|
@@ -3066,8 +3335,8 @@ async function showFiles(specPath, options = {}) {
|
|
|
3066
3335
|
}
|
|
3067
3336
|
const subFiles = await loadSubFiles(spec.fullPath);
|
|
3068
3337
|
if (options.json) {
|
|
3069
|
-
const readmeStat2 = await
|
|
3070
|
-
const readmeContent2 = await
|
|
3338
|
+
const readmeStat2 = await fs9.stat(spec.filePath);
|
|
3339
|
+
const readmeContent2 = await fs9.readFile(spec.filePath, "utf-8");
|
|
3071
3340
|
const readmeTokens2 = await countTokens({ content: readmeContent2 });
|
|
3072
3341
|
const jsonOutput = {
|
|
3073
3342
|
spec: spec.name,
|
|
@@ -3091,14 +3360,14 @@ async function showFiles(specPath, options = {}) {
|
|
|
3091
3360
|
return;
|
|
3092
3361
|
}
|
|
3093
3362
|
console.log("");
|
|
3094
|
-
console.log(
|
|
3363
|
+
console.log(chalk16.cyan(`\u{1F4C4} Files in ${sanitizeUserInput(spec.name)}`));
|
|
3095
3364
|
console.log("");
|
|
3096
|
-
console.log(
|
|
3097
|
-
const readmeStat = await
|
|
3365
|
+
console.log(chalk16.green("Required:"));
|
|
3366
|
+
const readmeStat = await fs9.stat(spec.filePath);
|
|
3098
3367
|
const readmeSize = formatSize(readmeStat.size);
|
|
3099
|
-
const readmeContent = await
|
|
3368
|
+
const readmeContent = await fs9.readFile(spec.filePath, "utf-8");
|
|
3100
3369
|
const readmeTokens = await countTokens({ content: readmeContent });
|
|
3101
|
-
console.log(
|
|
3370
|
+
console.log(chalk16.green(` \u2713 README.md (${readmeSize}, ~${readmeTokens.total.toLocaleString()} tokens) Main spec`));
|
|
3102
3371
|
console.log("");
|
|
3103
3372
|
let filteredFiles = subFiles;
|
|
3104
3373
|
if (options.type === "docs") {
|
|
@@ -3107,27 +3376,27 @@ async function showFiles(specPath, options = {}) {
|
|
|
3107
3376
|
filteredFiles = subFiles.filter((f) => f.type === "asset");
|
|
3108
3377
|
}
|
|
3109
3378
|
if (filteredFiles.length === 0) {
|
|
3110
|
-
console.log(
|
|
3379
|
+
console.log(chalk16.gray("No additional files"));
|
|
3111
3380
|
console.log("");
|
|
3112
3381
|
return;
|
|
3113
3382
|
}
|
|
3114
3383
|
const documents = filteredFiles.filter((f) => f.type === "document");
|
|
3115
3384
|
const assets = filteredFiles.filter((f) => f.type === "asset");
|
|
3116
3385
|
if (documents.length > 0 && (!options.type || options.type === "docs")) {
|
|
3117
|
-
console.log(
|
|
3386
|
+
console.log(chalk16.cyan("Documents:"));
|
|
3118
3387
|
for (const file of documents) {
|
|
3119
3388
|
const size = formatSize(file.size);
|
|
3120
|
-
const content = await
|
|
3389
|
+
const content = await fs9.readFile(file.path, "utf-8");
|
|
3121
3390
|
const tokenCount = await countTokens({ content });
|
|
3122
|
-
console.log(
|
|
3391
|
+
console.log(chalk16.cyan(` \u2713 ${sanitizeUserInput(file.name).padEnd(20)} (${size}, ~${tokenCount.total.toLocaleString()} tokens)`));
|
|
3123
3392
|
}
|
|
3124
3393
|
console.log("");
|
|
3125
3394
|
}
|
|
3126
3395
|
if (assets.length > 0 && (!options.type || options.type === "assets")) {
|
|
3127
|
-
console.log(
|
|
3396
|
+
console.log(chalk16.yellow("Assets:"));
|
|
3128
3397
|
for (const file of assets) {
|
|
3129
3398
|
const size = formatSize(file.size);
|
|
3130
|
-
console.log(
|
|
3399
|
+
console.log(chalk16.yellow(` \u2713 ${sanitizeUserInput(file.name).padEnd(20)} (${size})`));
|
|
3131
3400
|
}
|
|
3132
3401
|
console.log("");
|
|
3133
3402
|
}
|
|
@@ -3135,7 +3404,7 @@ async function showFiles(specPath, options = {}) {
|
|
|
3135
3404
|
const totalSize = formatSize(
|
|
3136
3405
|
readmeStat.size + filteredFiles.reduce((sum, f) => sum + f.size, 0)
|
|
3137
3406
|
);
|
|
3138
|
-
console.log(
|
|
3407
|
+
console.log(chalk16.gray(`Total: ${totalFiles} files, ${totalSize}`));
|
|
3139
3408
|
console.log("");
|
|
3140
3409
|
}
|
|
3141
3410
|
function formatSize(bytes) {
|
|
@@ -3155,31 +3424,31 @@ function examplesCommand() {
|
|
|
3155
3424
|
async function listExamples2() {
|
|
3156
3425
|
const examples = getExamplesList();
|
|
3157
3426
|
console.log("");
|
|
3158
|
-
console.log(
|
|
3427
|
+
console.log(chalk16.bold("LeanSpec Example Projects"));
|
|
3159
3428
|
console.log("");
|
|
3160
3429
|
console.log("Scaffold complete example projects to follow tutorials:");
|
|
3161
3430
|
console.log("");
|
|
3162
3431
|
for (const example of examples) {
|
|
3163
|
-
const difficultyColor = example.difficulty === "beginner" ?
|
|
3432
|
+
const difficultyColor = example.difficulty === "beginner" ? chalk16.green : example.difficulty === "intermediate" ? chalk16.yellow : chalk16.red;
|
|
3164
3433
|
const difficultyStars = example.difficulty === "beginner" ? "\u2605\u2606\u2606" : example.difficulty === "intermediate" ? "\u2605\u2605\u2606" : "\u2605\u2605\u2605";
|
|
3165
|
-
console.log(
|
|
3166
|
-
console.log(` ${
|
|
3434
|
+
console.log(chalk16.cyan.bold(` ${example.title}`));
|
|
3435
|
+
console.log(` ${chalk16.gray(example.name)}`);
|
|
3167
3436
|
console.log(` ${example.description}`);
|
|
3168
3437
|
console.log(` ${difficultyColor(difficultyStars + " " + example.difficulty)} \u2022 ${example.tech.join(", ")} \u2022 ~${example.lines} lines`);
|
|
3169
|
-
console.log(` ${
|
|
3170
|
-
console.log(` ${
|
|
3438
|
+
console.log(` ${chalk16.gray("Tutorial:")} ${example.tutorial}`);
|
|
3439
|
+
console.log(` ${chalk16.gray(example.tutorialUrl)}`);
|
|
3171
3440
|
console.log("");
|
|
3172
3441
|
}
|
|
3173
|
-
console.log(
|
|
3442
|
+
console.log(chalk16.bold("Usage:"));
|
|
3174
3443
|
console.log("");
|
|
3175
3444
|
console.log(" # Scaffold an example");
|
|
3176
|
-
console.log(
|
|
3445
|
+
console.log(chalk16.cyan(" lean-spec init --example <name>"));
|
|
3177
3446
|
console.log("");
|
|
3178
3447
|
console.log(" # Interactive selection");
|
|
3179
|
-
console.log(
|
|
3448
|
+
console.log(chalk16.cyan(" lean-spec init --example"));
|
|
3180
3449
|
console.log("");
|
|
3181
3450
|
console.log(" # Custom directory name");
|
|
3182
|
-
console.log(
|
|
3451
|
+
console.log(chalk16.cyan(" lean-spec init --example dark-theme --name my-demo"));
|
|
3183
3452
|
console.log("");
|
|
3184
3453
|
}
|
|
3185
3454
|
function migrateCommand(inputPath, options = {}) {
|
|
@@ -3204,7 +3473,7 @@ function migrateCommand(inputPath, options = {}) {
|
|
|
3204
3473
|
async function migrateSpecs(inputPath, options = {}) {
|
|
3205
3474
|
const config = await loadConfig();
|
|
3206
3475
|
try {
|
|
3207
|
-
const stats = await
|
|
3476
|
+
const stats = await fs9.stat(inputPath);
|
|
3208
3477
|
if (!stats.isDirectory()) {
|
|
3209
3478
|
console.error("\x1B[31m\u274C Error:\x1B[0m Input path must be a directory");
|
|
3210
3479
|
process.exit(1);
|
|
@@ -3239,16 +3508,16 @@ async function migrateSpecs(inputPath, options = {}) {
|
|
|
3239
3508
|
async function scanDocuments(dirPath) {
|
|
3240
3509
|
const documents = [];
|
|
3241
3510
|
async function scanRecursive(currentPath) {
|
|
3242
|
-
const entries = await
|
|
3511
|
+
const entries = await fs9.readdir(currentPath, { withFileTypes: true });
|
|
3243
3512
|
for (const entry of entries) {
|
|
3244
|
-
const fullPath =
|
|
3513
|
+
const fullPath = path13.join(currentPath, entry.name);
|
|
3245
3514
|
if (entry.isDirectory()) {
|
|
3246
3515
|
if (!entry.name.startsWith(".") && entry.name !== "node_modules") {
|
|
3247
3516
|
await scanRecursive(fullPath);
|
|
3248
3517
|
}
|
|
3249
3518
|
} else if (entry.isFile()) {
|
|
3250
3519
|
if (entry.name.endsWith(".md") || entry.name.endsWith(".markdown")) {
|
|
3251
|
-
const stats = await
|
|
3520
|
+
const stats = await fs9.stat(fullPath);
|
|
3252
3521
|
documents.push({
|
|
3253
3522
|
path: fullPath,
|
|
3254
3523
|
name: entry.name,
|
|
@@ -3275,7 +3544,7 @@ async function detectSourceFormat(inputPath, documents) {
|
|
|
3275
3544
|
return "generic";
|
|
3276
3545
|
}
|
|
3277
3546
|
async function migrateAuto(inputPath, documents, format, config, options) {
|
|
3278
|
-
const specsDir =
|
|
3547
|
+
const specsDir = path13.join(process.cwd(), config.specsDir || "specs");
|
|
3279
3548
|
const startTime = Date.now();
|
|
3280
3549
|
console.log("\u2550".repeat(70));
|
|
3281
3550
|
console.log("\x1B[1m\x1B[36m\u{1F680} Auto Migration\x1B[0m");
|
|
@@ -3285,13 +3554,13 @@ async function migrateAuto(inputPath, documents, format, config, options) {
|
|
|
3285
3554
|
console.log("\x1B[33m\u26A0\uFE0F DRY RUN - No changes will be made\x1B[0m\n");
|
|
3286
3555
|
}
|
|
3287
3556
|
if (!options.dryRun) {
|
|
3288
|
-
await
|
|
3557
|
+
await fs9.mkdir(specsDir, { recursive: true });
|
|
3289
3558
|
}
|
|
3290
3559
|
let migratedCount = 0;
|
|
3291
3560
|
let skippedCount = 0;
|
|
3292
3561
|
let nextSeq = 1;
|
|
3293
3562
|
try {
|
|
3294
|
-
const existingSpecs = await
|
|
3563
|
+
const existingSpecs = await fs9.readdir(specsDir);
|
|
3295
3564
|
const seqNumbers = existingSpecs.map((name) => {
|
|
3296
3565
|
const match = name.match(/^(\d+)-/);
|
|
3297
3566
|
return match ? parseInt(match[1], 10) : 0;
|
|
@@ -3305,17 +3574,17 @@ async function migrateAuto(inputPath, documents, format, config, options) {
|
|
|
3305
3574
|
console.log("\x1B[36mMigrating spec-kit format...\x1B[0m\n");
|
|
3306
3575
|
const specMdFiles = documents.filter((d) => d.name === "spec.md");
|
|
3307
3576
|
for (const doc of specMdFiles) {
|
|
3308
|
-
const sourceDir =
|
|
3309
|
-
const dirName =
|
|
3577
|
+
const sourceDir = path13.dirname(doc.path);
|
|
3578
|
+
const dirName = path13.basename(sourceDir);
|
|
3310
3579
|
const hasSeq = /^\d{3}-/.test(dirName);
|
|
3311
3580
|
const targetDirName = hasSeq ? dirName : `${String(nextSeq).padStart(3, "0")}-${dirName}`;
|
|
3312
|
-
const targetDir =
|
|
3581
|
+
const targetDir = path13.join(specsDir, targetDirName);
|
|
3313
3582
|
if (!options.dryRun) {
|
|
3314
3583
|
await copyDirectory2(sourceDir, targetDir);
|
|
3315
|
-
const oldPath =
|
|
3316
|
-
const newPath =
|
|
3584
|
+
const oldPath = path13.join(targetDir, "spec.md");
|
|
3585
|
+
const newPath = path13.join(targetDir, "README.md");
|
|
3317
3586
|
try {
|
|
3318
|
-
await
|
|
3587
|
+
await fs9.rename(oldPath, newPath);
|
|
3319
3588
|
} catch {
|
|
3320
3589
|
}
|
|
3321
3590
|
}
|
|
@@ -3327,8 +3596,8 @@ async function migrateAuto(inputPath, documents, format, config, options) {
|
|
|
3327
3596
|
console.log("\x1B[36mMigrating OpenSpec format...\x1B[0m\n");
|
|
3328
3597
|
const folders = /* @__PURE__ */ new Map();
|
|
3329
3598
|
for (const doc of documents) {
|
|
3330
|
-
const parentDir =
|
|
3331
|
-
const folderName =
|
|
3599
|
+
const parentDir = path13.dirname(doc.path);
|
|
3600
|
+
const folderName = path13.basename(parentDir);
|
|
3332
3601
|
if (!folders.has(folderName)) {
|
|
3333
3602
|
folders.set(folderName, []);
|
|
3334
3603
|
}
|
|
@@ -3339,13 +3608,13 @@ async function migrateAuto(inputPath, documents, format, config, options) {
|
|
|
3339
3608
|
continue;
|
|
3340
3609
|
}
|
|
3341
3610
|
const targetDirName = `${String(nextSeq).padStart(3, "0")}-${folderName}`;
|
|
3342
|
-
const targetDir =
|
|
3611
|
+
const targetDir = path13.join(specsDir, targetDirName);
|
|
3343
3612
|
if (!options.dryRun) {
|
|
3344
|
-
await
|
|
3613
|
+
await fs9.mkdir(targetDir, { recursive: true });
|
|
3345
3614
|
for (const doc of docs) {
|
|
3346
3615
|
const targetName = doc.name === "spec.md" ? "README.md" : doc.name;
|
|
3347
|
-
const targetPath =
|
|
3348
|
-
await
|
|
3616
|
+
const targetPath = path13.join(targetDir, targetName);
|
|
3617
|
+
await fs9.copyFile(doc.path, targetPath);
|
|
3349
3618
|
}
|
|
3350
3619
|
}
|
|
3351
3620
|
console.log(` \x1B[32m\u2713\x1B[0m ${folderName} \u2192 ${targetDirName}/`);
|
|
@@ -3355,18 +3624,18 @@ async function migrateAuto(inputPath, documents, format, config, options) {
|
|
|
3355
3624
|
} else {
|
|
3356
3625
|
console.log("\x1B[36mMigrating generic markdown files...\x1B[0m\n");
|
|
3357
3626
|
for (const doc of documents) {
|
|
3358
|
-
const baseName =
|
|
3627
|
+
const baseName = path13.basename(doc.name, path13.extname(doc.name)).replace(/^\d+-/, "").replace(/^[_-]+/, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/-+$/, "");
|
|
3359
3628
|
if (!baseName) {
|
|
3360
3629
|
console.log(` \x1B[33m\u26A0\x1B[0m Skipped: ${doc.name} (invalid name)`);
|
|
3361
3630
|
skippedCount++;
|
|
3362
3631
|
continue;
|
|
3363
3632
|
}
|
|
3364
3633
|
const targetDirName = `${String(nextSeq).padStart(3, "0")}-${baseName}`;
|
|
3365
|
-
const targetDir =
|
|
3366
|
-
const targetPath =
|
|
3634
|
+
const targetDir = path13.join(specsDir, targetDirName);
|
|
3635
|
+
const targetPath = path13.join(targetDir, "README.md");
|
|
3367
3636
|
if (!options.dryRun) {
|
|
3368
|
-
await
|
|
3369
|
-
await
|
|
3637
|
+
await fs9.mkdir(targetDir, { recursive: true });
|
|
3638
|
+
await fs9.copyFile(doc.path, targetPath);
|
|
3370
3639
|
}
|
|
3371
3640
|
console.log(` \x1B[32m\u2713\x1B[0m ${doc.name} \u2192 ${targetDirName}/README.md`);
|
|
3372
3641
|
migratedCount++;
|
|
@@ -3410,15 +3679,15 @@ async function migrateAuto(inputPath, documents, format, config, options) {
|
|
|
3410
3679
|
console.log(" lean-spec validate # Check for issues");
|
|
3411
3680
|
}
|
|
3412
3681
|
async function copyDirectory2(src, dest) {
|
|
3413
|
-
await
|
|
3414
|
-
const entries = await
|
|
3682
|
+
await fs9.mkdir(dest, { recursive: true });
|
|
3683
|
+
const entries = await fs9.readdir(src, { withFileTypes: true });
|
|
3415
3684
|
for (const entry of entries) {
|
|
3416
|
-
const srcPath =
|
|
3417
|
-
const destPath =
|
|
3685
|
+
const srcPath = path13.join(src, entry.name);
|
|
3686
|
+
const destPath = path13.join(dest, entry.name);
|
|
3418
3687
|
if (entry.isDirectory()) {
|
|
3419
3688
|
await copyDirectory2(srcPath, destPath);
|
|
3420
3689
|
} else {
|
|
3421
|
-
await
|
|
3690
|
+
await fs9.copyFile(srcPath, destPath);
|
|
3422
3691
|
}
|
|
3423
3692
|
}
|
|
3424
3693
|
}
|
|
@@ -3776,7 +4045,7 @@ async function showBoard(options) {
|
|
|
3776
4045
|
if (options.json) {
|
|
3777
4046
|
console.log(JSON.stringify({ columns: {}, total: 0 }, null, 2));
|
|
3778
4047
|
} else {
|
|
3779
|
-
console.log(
|
|
4048
|
+
console.log(chalk16.dim("No specs found."));
|
|
3780
4049
|
}
|
|
3781
4050
|
return;
|
|
3782
4051
|
}
|
|
@@ -3815,12 +4084,12 @@ async function showBoard(options) {
|
|
|
3815
4084
|
console.log(JSON.stringify(jsonOutput, null, 2));
|
|
3816
4085
|
return;
|
|
3817
4086
|
}
|
|
3818
|
-
console.log(
|
|
4087
|
+
console.log(chalk16.bold.cyan("\u{1F4CB} Spec Kanban Board"));
|
|
3819
4088
|
if (options.tag || options.assignee) {
|
|
3820
4089
|
const filterParts = [];
|
|
3821
4090
|
if (options.tag) filterParts.push(`tag=${options.tag}`);
|
|
3822
4091
|
if (options.assignee) filterParts.push(`assignee=${options.assignee}`);
|
|
3823
|
-
console.log(
|
|
4092
|
+
console.log(chalk16.dim(`Filtered by: ${filterParts.join(", ")}`));
|
|
3824
4093
|
}
|
|
3825
4094
|
console.log("");
|
|
3826
4095
|
if (!options.simple) {
|
|
@@ -3835,12 +4104,12 @@ async function showBoard(options) {
|
|
|
3835
4104
|
const padding = boxWidth - 2 - visibleLength;
|
|
3836
4105
|
return content + " ".repeat(Math.max(0, padding));
|
|
3837
4106
|
};
|
|
3838
|
-
console.log(
|
|
3839
|
-
const headerLine =
|
|
3840
|
-
console.log(
|
|
3841
|
-
const percentageColor = completionMetrics.score >= 70 ?
|
|
4107
|
+
console.log(chalk16.dim(topBorder));
|
|
4108
|
+
const headerLine = chalk16.bold(" Project Overview");
|
|
4109
|
+
console.log(chalk16.dim("\u2551") + padLine(headerLine) + chalk16.dim("\u2551"));
|
|
4110
|
+
const percentageColor = completionMetrics.score >= 70 ? chalk16.green : completionMetrics.score >= 40 ? chalk16.yellow : chalk16.red;
|
|
3842
4111
|
const line1 = ` ${completionMetrics.totalSpecs} total \xB7 ${completionMetrics.activeSpecs} active \xB7 ${completionMetrics.completeSpecs} complete ${percentageColor("(" + completionMetrics.score + "%)")}`;
|
|
3843
|
-
console.log(
|
|
4112
|
+
console.log(chalk16.dim("\u2551") + padLine(line1) + chalk16.dim("\u2551"));
|
|
3844
4113
|
if (completionMetrics.criticalIssues.length > 0 || completionMetrics.warnings.length > 0) {
|
|
3845
4114
|
const alerts = [];
|
|
3846
4115
|
if (completionMetrics.criticalIssues.length > 0) {
|
|
@@ -3849,27 +4118,27 @@ async function showBoard(options) {
|
|
|
3849
4118
|
if (completionMetrics.warnings.length > 0) {
|
|
3850
4119
|
alerts.push(`${completionMetrics.warnings.length} specs WIP > 7 days`);
|
|
3851
4120
|
}
|
|
3852
|
-
const alertLine = ` ${
|
|
3853
|
-
console.log(
|
|
4121
|
+
const alertLine = ` ${chalk16.yellow("\u26A0\uFE0F " + alerts.join(" \xB7 "))}`;
|
|
4122
|
+
console.log(chalk16.dim("\u2551") + padLine(alertLine) + chalk16.dim("\u2551"));
|
|
3854
4123
|
}
|
|
3855
|
-
const velocityLine = ` ${
|
|
3856
|
-
console.log(
|
|
3857
|
-
console.log(
|
|
4124
|
+
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`;
|
|
4125
|
+
console.log(chalk16.dim("\u2551") + padLine(velocityLine) + chalk16.dim("\u2551"));
|
|
4126
|
+
console.log(chalk16.dim(bottomBorder));
|
|
3858
4127
|
console.log("");
|
|
3859
4128
|
if (options.completionOnly) {
|
|
3860
4129
|
return;
|
|
3861
4130
|
}
|
|
3862
4131
|
}
|
|
3863
4132
|
renderColumn(STATUS_CONFIG.planned.label, STATUS_CONFIG.planned.emoji, columns.planned, true, STATUS_CONFIG.planned.colorFn);
|
|
3864
|
-
console.log(
|
|
4133
|
+
console.log(chalk16.dim("\u2501".repeat(70)));
|
|
3865
4134
|
console.log("");
|
|
3866
4135
|
renderColumn(STATUS_CONFIG["in-progress"].label, STATUS_CONFIG["in-progress"].emoji, columns["in-progress"], true, STATUS_CONFIG["in-progress"].colorFn);
|
|
3867
|
-
console.log(
|
|
4136
|
+
console.log(chalk16.dim("\u2501".repeat(70)));
|
|
3868
4137
|
console.log("");
|
|
3869
4138
|
renderColumn(STATUS_CONFIG.complete.label, STATUS_CONFIG.complete.emoji, columns.complete, options.complete || false, STATUS_CONFIG.complete.colorFn);
|
|
3870
4139
|
}
|
|
3871
4140
|
function renderColumn(title, emoji, specs, expanded, colorFn) {
|
|
3872
|
-
console.log(`${emoji} ${colorFn(
|
|
4141
|
+
console.log(`${emoji} ${colorFn(chalk16.bold(`${title} (${specs.length})`))}`);
|
|
3873
4142
|
console.log("");
|
|
3874
4143
|
if (expanded && specs.length > 0) {
|
|
3875
4144
|
const priorityGroups = {
|
|
@@ -3894,30 +4163,30 @@ function renderColumn(title, emoji, specs, expanded, colorFn) {
|
|
|
3894
4163
|
firstGroup = false;
|
|
3895
4164
|
const priorityLabel = priority === "none" ? "No Priority" : priority.charAt(0).toUpperCase() + priority.slice(1);
|
|
3896
4165
|
const priorityEmoji = priority === "none" ? "\u26AA" : PRIORITY_CONFIG[priority].emoji;
|
|
3897
|
-
const priorityColor = priority === "none" ?
|
|
3898
|
-
console.log(` ${priorityColor(`${priorityEmoji} ${
|
|
4166
|
+
const priorityColor = priority === "none" ? chalk16.dim : PRIORITY_CONFIG[priority].colorFn;
|
|
4167
|
+
console.log(` ${priorityColor(`${priorityEmoji} ${chalk16.bold(priorityLabel)} ${chalk16.dim(`(${groupSpecs.length})`)}`)}`);
|
|
3899
4168
|
for (const spec of groupSpecs) {
|
|
3900
4169
|
let assigneeStr = "";
|
|
3901
4170
|
if (spec.frontmatter.assignee) {
|
|
3902
|
-
assigneeStr = " " +
|
|
4171
|
+
assigneeStr = " " + chalk16.cyan(`@${sanitizeUserInput(spec.frontmatter.assignee)}`);
|
|
3903
4172
|
}
|
|
3904
4173
|
let tagsStr = "";
|
|
3905
4174
|
if (spec.frontmatter.tags?.length) {
|
|
3906
4175
|
const tags = Array.isArray(spec.frontmatter.tags) ? spec.frontmatter.tags : [];
|
|
3907
4176
|
if (tags.length > 0) {
|
|
3908
4177
|
const tagStr = tags.map((tag) => `#${sanitizeUserInput(tag)}`).join(" ");
|
|
3909
|
-
tagsStr = " " +
|
|
4178
|
+
tagsStr = " " + chalk16.dim(chalk16.magenta(tagStr));
|
|
3910
4179
|
}
|
|
3911
4180
|
}
|
|
3912
|
-
console.log(` ${
|
|
4181
|
+
console.log(` ${chalk16.cyan(sanitizeUserInput(spec.path))}${assigneeStr}${tagsStr}`);
|
|
3913
4182
|
}
|
|
3914
4183
|
}
|
|
3915
4184
|
console.log("");
|
|
3916
4185
|
} else if (!expanded && specs.length > 0) {
|
|
3917
|
-
console.log(` ${
|
|
4186
|
+
console.log(` ${chalk16.dim("(collapsed, use --complete to expand)")}`);
|
|
3918
4187
|
console.log("");
|
|
3919
4188
|
} else {
|
|
3920
|
-
console.log(` ${
|
|
4189
|
+
console.log(` ${chalk16.dim("(empty)")}`);
|
|
3921
4190
|
console.log("");
|
|
3922
4191
|
}
|
|
3923
4192
|
}
|
|
@@ -4086,26 +4355,26 @@ async function showStats(options) {
|
|
|
4086
4355
|
console.log(JSON.stringify(data, null, 2));
|
|
4087
4356
|
return;
|
|
4088
4357
|
}
|
|
4089
|
-
console.log(
|
|
4358
|
+
console.log(chalk16.bold.cyan("\u{1F4CA} Spec Stats"));
|
|
4090
4359
|
console.log("");
|
|
4091
4360
|
if (options.tag || options.assignee) {
|
|
4092
4361
|
const filterParts = [];
|
|
4093
4362
|
if (options.tag) filterParts.push(`tag=${options.tag}`);
|
|
4094
4363
|
if (options.assignee) filterParts.push(`assignee=${options.assignee}`);
|
|
4095
|
-
console.log(
|
|
4364
|
+
console.log(chalk16.dim(`Filtered by: ${filterParts.join(", ")}`));
|
|
4096
4365
|
console.log("");
|
|
4097
4366
|
}
|
|
4098
4367
|
if (showSimplified) {
|
|
4099
|
-
console.log(
|
|
4368
|
+
console.log(chalk16.bold("\u{1F4C8} Overview"));
|
|
4100
4369
|
console.log("");
|
|
4101
4370
|
const completionStatus = getCompletionStatus(completionMetrics.score);
|
|
4102
|
-
const completionColor = completionStatus.color === "green" ?
|
|
4103
|
-
console.log(` Total Specs ${
|
|
4104
|
-
console.log(` Active (Planned+WIP) ${
|
|
4105
|
-
console.log(` Complete ${
|
|
4371
|
+
const completionColor = completionStatus.color === "green" ? chalk16.green : completionStatus.color === "yellow" ? chalk16.yellow : chalk16.red;
|
|
4372
|
+
console.log(` Total Specs ${chalk16.cyan(completionMetrics.totalSpecs)}`);
|
|
4373
|
+
console.log(` Active (Planned+WIP) ${chalk16.yellow(completionMetrics.activeSpecs)}`);
|
|
4374
|
+
console.log(` Complete ${chalk16.green(completionMetrics.completeSpecs)}`);
|
|
4106
4375
|
console.log(` Completion Rate ${completionColor(`${completionMetrics.score}% ${completionStatus.emoji}`)}`);
|
|
4107
4376
|
console.log("");
|
|
4108
|
-
console.log(
|
|
4377
|
+
console.log(chalk16.bold("\u{1F4CA} Status"));
|
|
4109
4378
|
console.log("");
|
|
4110
4379
|
const labelWidth2 = 15;
|
|
4111
4380
|
const barWidth2 = 20;
|
|
@@ -4114,19 +4383,19 @@ async function showStats(options) {
|
|
|
4114
4383
|
const width = Math.round(count / maxCount * barWidth2);
|
|
4115
4384
|
const filledWidth = Math.min(width, barWidth2);
|
|
4116
4385
|
const emptyWidth = barWidth2 - filledWidth;
|
|
4117
|
-
return char.repeat(filledWidth) +
|
|
4386
|
+
return char.repeat(filledWidth) + chalk16.dim("\u2591").repeat(emptyWidth);
|
|
4118
4387
|
};
|
|
4119
|
-
console.log(` \u{1F4C5} ${"Planned".padEnd(labelWidth2)} ${
|
|
4120
|
-
console.log(` \u23F3 ${"In Progress".padEnd(labelWidth2)} ${
|
|
4121
|
-
console.log(` \u2705 ${"Complete".padEnd(labelWidth2)} ${
|
|
4388
|
+
console.log(` \u{1F4C5} ${"Planned".padEnd(labelWidth2)} ${chalk16.cyan(createBar2(statusCounts.planned, maxStatusCount))} ${chalk16.cyan(statusCounts.planned)}`);
|
|
4389
|
+
console.log(` \u23F3 ${"In Progress".padEnd(labelWidth2)} ${chalk16.yellow(createBar2(statusCounts["in-progress"], maxStatusCount))} ${chalk16.yellow(statusCounts["in-progress"])}`);
|
|
4390
|
+
console.log(` \u2705 ${"Complete".padEnd(labelWidth2)} ${chalk16.green(createBar2(statusCounts.complete, maxStatusCount))} ${chalk16.green(statusCounts.complete)}`);
|
|
4122
4391
|
if (statusCounts.archived > 0) {
|
|
4123
|
-
console.log(` \u{1F4E6} ${"Archived".padEnd(labelWidth2)} ${
|
|
4392
|
+
console.log(` \u{1F4E6} ${"Archived".padEnd(labelWidth2)} ${chalk16.dim(createBar2(statusCounts.archived, maxStatusCount))} ${chalk16.dim(statusCounts.archived)}`);
|
|
4124
4393
|
}
|
|
4125
4394
|
console.log("");
|
|
4126
4395
|
const criticalCount = priorityCounts.critical || 0;
|
|
4127
4396
|
const highCount = priorityCounts.high || 0;
|
|
4128
4397
|
if (criticalCount > 0 || highCount > 0) {
|
|
4129
|
-
console.log(
|
|
4398
|
+
console.log(chalk16.bold("\u{1F3AF} Priority Focus"));
|
|
4130
4399
|
console.log("");
|
|
4131
4400
|
if (criticalCount > 0) {
|
|
4132
4401
|
const criticalPlanned = specs.filter((s) => s.frontmatter.priority === "critical" && s.frontmatter.status === "planned").length;
|
|
@@ -4136,11 +4405,11 @@ async function showStats(options) {
|
|
|
4136
4405
|
(s) => s.frontmatter.priority === "critical" && s.frontmatter.due && dayjs2(s.frontmatter.due).isBefore(dayjs2(), "day") && s.frontmatter.status !== "complete"
|
|
4137
4406
|
).length;
|
|
4138
4407
|
const parts = [];
|
|
4139
|
-
if (criticalPlanned > 0) parts.push(
|
|
4408
|
+
if (criticalPlanned > 0) parts.push(chalk16.dim(`${criticalPlanned} planned`));
|
|
4140
4409
|
if (criticalInProgress > 0) parts.push(`${criticalInProgress} in-progress`);
|
|
4141
|
-
if (criticalComplete > 0) parts.push(
|
|
4142
|
-
if (criticalOverdue > 0) parts.push(
|
|
4143
|
-
console.log(` \u{1F534} Critical ${
|
|
4410
|
+
if (criticalComplete > 0) parts.push(chalk16.green(`${criticalComplete} complete`));
|
|
4411
|
+
if (criticalOverdue > 0) parts.push(chalk16.red(`${criticalOverdue} overdue!`));
|
|
4412
|
+
console.log(` \u{1F534} Critical ${chalk16.red(criticalCount)} specs${parts.length > 0 ? ` (${parts.join(", ")})` : ""}`);
|
|
4144
4413
|
}
|
|
4145
4414
|
if (highCount > 0) {
|
|
4146
4415
|
const highPlanned = specs.filter((s) => s.frontmatter.priority === "high" && s.frontmatter.status === "planned").length;
|
|
@@ -4150,52 +4419,52 @@ async function showStats(options) {
|
|
|
4150
4419
|
(s) => s.frontmatter.priority === "high" && s.frontmatter.due && dayjs2(s.frontmatter.due).isBefore(dayjs2(), "day") && s.frontmatter.status !== "complete"
|
|
4151
4420
|
).length;
|
|
4152
4421
|
const parts = [];
|
|
4153
|
-
if (highPlanned > 0) parts.push(
|
|
4422
|
+
if (highPlanned > 0) parts.push(chalk16.dim(`${highPlanned} planned`));
|
|
4154
4423
|
if (highInProgress > 0) parts.push(`${highInProgress} in-progress`);
|
|
4155
|
-
if (highComplete > 0) parts.push(
|
|
4156
|
-
if (highOverdue > 0) parts.push(
|
|
4157
|
-
console.log(` \u{1F7E0} High ${
|
|
4424
|
+
if (highComplete > 0) parts.push(chalk16.green(`${highComplete} complete`));
|
|
4425
|
+
if (highOverdue > 0) parts.push(chalk16.yellow(`${highOverdue} overdue`));
|
|
4426
|
+
console.log(` \u{1F7E0} High ${chalk16.hex("#FFA500")(highCount)} specs${parts.length > 0 ? ` (${parts.join(", ")})` : ""}`);
|
|
4158
4427
|
}
|
|
4159
4428
|
console.log("");
|
|
4160
4429
|
}
|
|
4161
4430
|
if (insights.length > 0) {
|
|
4162
|
-
console.log(
|
|
4431
|
+
console.log(chalk16.bold.yellow("\u26A0\uFE0F Needs Attention"));
|
|
4163
4432
|
console.log("");
|
|
4164
4433
|
for (const insight of insights) {
|
|
4165
|
-
const color = insight.severity === "critical" ?
|
|
4434
|
+
const color = insight.severity === "critical" ? chalk16.red : insight.severity === "warning" ? chalk16.yellow : chalk16.cyan;
|
|
4166
4435
|
console.log(` ${color("\u2022")} ${insight.message}`);
|
|
4167
4436
|
for (const specPath of insight.specs.slice(0, 3)) {
|
|
4168
4437
|
const spec = specs.find((s) => s.path === specPath);
|
|
4169
4438
|
const details = spec ? getSpecInsightDetails(spec) : null;
|
|
4170
|
-
console.log(` ${
|
|
4439
|
+
console.log(` ${chalk16.dim(specPath)}${details ? chalk16.dim(` (${details})`) : ""}`);
|
|
4171
4440
|
}
|
|
4172
4441
|
if (insight.specs.length > 3) {
|
|
4173
|
-
console.log(` ${
|
|
4442
|
+
console.log(` ${chalk16.dim(`...and ${insight.specs.length - 3} more`)}`);
|
|
4174
4443
|
}
|
|
4175
4444
|
}
|
|
4176
4445
|
console.log("");
|
|
4177
4446
|
} else if (completionMetrics.activeSpecs === 0 && completionMetrics.completeSpecs > 0) {
|
|
4178
|
-
console.log(
|
|
4447
|
+
console.log(chalk16.bold.green("\u{1F389} All Specs Complete!"));
|
|
4179
4448
|
console.log("");
|
|
4180
|
-
console.log(` ${
|
|
4449
|
+
console.log(` ${chalk16.dim("Great work! All active specs are complete.")}`);
|
|
4181
4450
|
console.log("");
|
|
4182
4451
|
} else if (completionMetrics.activeSpecs > 0) {
|
|
4183
|
-
console.log(
|
|
4452
|
+
console.log(chalk16.bold.green("\u2728 All Clear!"));
|
|
4184
4453
|
console.log("");
|
|
4185
|
-
console.log(` ${
|
|
4454
|
+
console.log(` ${chalk16.dim("No critical issues detected. Keep up the good work!")}`);
|
|
4186
4455
|
console.log("");
|
|
4187
4456
|
}
|
|
4188
|
-
console.log(
|
|
4457
|
+
console.log(chalk16.bold("\u{1F680} Velocity Summary"));
|
|
4189
4458
|
console.log("");
|
|
4190
|
-
const cycleTimeStatus = velocityMetrics.cycleTime.average <= 7 ?
|
|
4191
|
-
const throughputTrend = velocityMetrics.throughput.trend === "up" ?
|
|
4192
|
-
console.log(` Avg Cycle Time ${
|
|
4193
|
-
console.log(` Throughput ${
|
|
4194
|
-
console.log(` WIP ${
|
|
4459
|
+
const cycleTimeStatus = velocityMetrics.cycleTime.average <= 7 ? chalk16.green("\u2713") : chalk16.yellow("\u26A0");
|
|
4460
|
+
const throughputTrend = velocityMetrics.throughput.trend === "up" ? chalk16.green("\u2191") : velocityMetrics.throughput.trend === "down" ? chalk16.red("\u2193") : chalk16.yellow("\u2192");
|
|
4461
|
+
console.log(` Avg Cycle Time ${chalk16.cyan(velocityMetrics.cycleTime.average.toFixed(1))} days ${cycleTimeStatus}${velocityMetrics.cycleTime.average <= 7 ? chalk16.dim(" (target: 7d)") : ""}`);
|
|
4462
|
+
console.log(` Throughput ${chalk16.cyan((velocityMetrics.throughput.perWeek / 7 * 7).toFixed(1))}/week ${throughputTrend}`);
|
|
4463
|
+
console.log(` WIP ${chalk16.yellow(velocityMetrics.wip.current)} specs`);
|
|
4195
4464
|
console.log("");
|
|
4196
|
-
console.log(
|
|
4197
|
-
console.log(
|
|
4198
|
-
console.log(
|
|
4465
|
+
console.log(chalk16.dim("\u{1F4A1} Use `lean-spec stats --full` for detailed analytics"));
|
|
4466
|
+
console.log(chalk16.dim(" Use `lean-spec stats --velocity` for velocity breakdown"));
|
|
4467
|
+
console.log(chalk16.dim(" Use `lean-spec stats --timeline` for activity timeline"));
|
|
4199
4468
|
console.log("");
|
|
4200
4469
|
return;
|
|
4201
4470
|
}
|
|
@@ -4211,97 +4480,97 @@ async function showStats(options) {
|
|
|
4211
4480
|
(sum, count) => sum + count,
|
|
4212
4481
|
0
|
|
4213
4482
|
);
|
|
4214
|
-
console.log(
|
|
4483
|
+
console.log(chalk16.bold("\u{1F4C8} Overview"));
|
|
4215
4484
|
console.log("");
|
|
4216
4485
|
console.log(
|
|
4217
4486
|
` ${"Metric".padEnd(labelWidth)} ${"Value".padStart(valueWidth)}`
|
|
4218
4487
|
);
|
|
4219
4488
|
console.log(
|
|
4220
|
-
` ${
|
|
4489
|
+
` ${chalk16.dim("\u2500".repeat(labelWidth))} ${chalk16.dim("\u2500".repeat(valueWidth))}`
|
|
4221
4490
|
);
|
|
4222
4491
|
console.log(
|
|
4223
|
-
` ${"Total Specs".padEnd(labelWidth)} ${
|
|
4492
|
+
` ${"Total Specs".padEnd(labelWidth)} ${chalk16.green(specs.length.toString().padStart(valueWidth))}`
|
|
4224
4493
|
);
|
|
4225
4494
|
console.log(
|
|
4226
|
-
` ${"With Priority".padEnd(labelWidth)} ${
|
|
4495
|
+
` ${"With Priority".padEnd(labelWidth)} ${chalk16.cyan(totalWithPriority.toString().padStart(valueWidth))}`
|
|
4227
4496
|
);
|
|
4228
4497
|
console.log(
|
|
4229
|
-
` ${"Unique Tags".padEnd(labelWidth)} ${
|
|
4498
|
+
` ${"Unique Tags".padEnd(labelWidth)} ${chalk16.magenta(Object.keys(tagCounts).length.toString().padStart(valueWidth))}`
|
|
4230
4499
|
);
|
|
4231
4500
|
console.log("");
|
|
4232
|
-
console.log(
|
|
4501
|
+
console.log(chalk16.bold("\u{1F4CA} Status Distribution"));
|
|
4233
4502
|
console.log("");
|
|
4234
4503
|
const maxStatusCount = Math.max(...Object.values(statusCounts));
|
|
4235
4504
|
const colWidth = barWidth + 3;
|
|
4236
4505
|
console.log(
|
|
4237
|
-
` ${"Status".padEnd(labelWidth)} ${
|
|
4506
|
+
` ${"Status".padEnd(labelWidth)} ${chalk16.cyan("Count".padEnd(colWidth))}`
|
|
4238
4507
|
);
|
|
4239
4508
|
console.log(
|
|
4240
|
-
` ${
|
|
4509
|
+
` ${chalk16.dim("\u2500".repeat(labelWidth))} ${chalk16.dim("\u2500".repeat(colWidth))}`
|
|
4241
4510
|
);
|
|
4242
4511
|
console.log(
|
|
4243
|
-
` \u{1F4CB} ${"Planned".padEnd(labelWidth - 3)} ${
|
|
4512
|
+
` \u{1F4CB} ${"Planned".padEnd(labelWidth - 3)} ${chalk16.cyan(createBar(statusCounts.planned, maxStatusCount).padEnd(barWidth))}${chalk16.cyan(statusCounts.planned.toString().padStart(3))}`
|
|
4244
4513
|
);
|
|
4245
4514
|
console.log(
|
|
4246
|
-
` \u23F3 ${"In Progress".padEnd(labelWidth - 3)} ${
|
|
4515
|
+
` \u23F3 ${"In Progress".padEnd(labelWidth - 3)} ${chalk16.yellow(createBar(statusCounts["in-progress"], maxStatusCount).padEnd(barWidth))}${chalk16.yellow(statusCounts["in-progress"].toString().padStart(3))}`
|
|
4247
4516
|
);
|
|
4248
4517
|
console.log(
|
|
4249
|
-
` \u2705 ${"Complete".padEnd(labelWidth - 3)} ${
|
|
4518
|
+
` \u2705 ${"Complete".padEnd(labelWidth - 3)} ${chalk16.green(createBar(statusCounts.complete, maxStatusCount).padEnd(barWidth))}${chalk16.green(statusCounts.complete.toString().padStart(3))}`
|
|
4250
4519
|
);
|
|
4251
4520
|
console.log(
|
|
4252
|
-
` \u{1F4E6} ${"Archived".padEnd(labelWidth - 3)} ${
|
|
4521
|
+
` \u{1F4E6} ${"Archived".padEnd(labelWidth - 3)} ${chalk16.dim(createBar(statusCounts.archived, maxStatusCount).padEnd(barWidth))}${chalk16.dim(statusCounts.archived.toString().padStart(3))}`
|
|
4253
4522
|
);
|
|
4254
4523
|
console.log("");
|
|
4255
4524
|
if (totalWithPriority > 0) {
|
|
4256
|
-
console.log(
|
|
4525
|
+
console.log(chalk16.bold("\u{1F3AF} Priority Breakdown"));
|
|
4257
4526
|
console.log("");
|
|
4258
4527
|
const maxPriorityCount = Math.max(
|
|
4259
4528
|
...Object.values(priorityCounts).filter((c) => c > 0)
|
|
4260
4529
|
);
|
|
4261
4530
|
console.log(
|
|
4262
|
-
` ${"Priority".padEnd(labelWidth)} ${
|
|
4531
|
+
` ${"Priority".padEnd(labelWidth)} ${chalk16.cyan("Count".padEnd(colWidth))}`
|
|
4263
4532
|
);
|
|
4264
4533
|
console.log(
|
|
4265
|
-
` ${
|
|
4534
|
+
` ${chalk16.dim("\u2500".repeat(labelWidth))} ${chalk16.dim("\u2500".repeat(colWidth))}`
|
|
4266
4535
|
);
|
|
4267
4536
|
if (priorityCounts.critical > 0) {
|
|
4268
4537
|
console.log(
|
|
4269
|
-
` \u{1F534} ${"Critical".padEnd(labelWidth - 3)} ${
|
|
4538
|
+
` \u{1F534} ${"Critical".padEnd(labelWidth - 3)} ${chalk16.red(createBar(priorityCounts.critical, maxPriorityCount).padEnd(barWidth))}${chalk16.red(priorityCounts.critical.toString().padStart(3))}`
|
|
4270
4539
|
);
|
|
4271
4540
|
}
|
|
4272
4541
|
if (priorityCounts.high > 0) {
|
|
4273
4542
|
console.log(
|
|
4274
|
-
` \u{1F7E0} ${"High".padEnd(labelWidth - 3)} ${
|
|
4543
|
+
` \u{1F7E0} ${"High".padEnd(labelWidth - 3)} ${chalk16.hex("#FFA500")(createBar(priorityCounts.high, maxPriorityCount).padEnd(barWidth))}${chalk16.hex("#FFA500")(priorityCounts.high.toString().padStart(3))}`
|
|
4275
4544
|
);
|
|
4276
4545
|
}
|
|
4277
4546
|
if (priorityCounts.medium > 0) {
|
|
4278
4547
|
console.log(
|
|
4279
|
-
` \u{1F7E1} ${"Medium".padEnd(labelWidth - 3)} ${
|
|
4548
|
+
` \u{1F7E1} ${"Medium".padEnd(labelWidth - 3)} ${chalk16.yellow(createBar(priorityCounts.medium, maxPriorityCount).padEnd(barWidth))}${chalk16.yellow(priorityCounts.medium.toString().padStart(3))}`
|
|
4280
4549
|
);
|
|
4281
4550
|
}
|
|
4282
4551
|
if (priorityCounts.low > 0) {
|
|
4283
4552
|
console.log(
|
|
4284
|
-
` \u{1F7E2} ${"Low".padEnd(labelWidth - 3)} ${
|
|
4553
|
+
` \u{1F7E2} ${"Low".padEnd(labelWidth - 3)} ${chalk16.green(createBar(priorityCounts.low, maxPriorityCount).padEnd(barWidth))}${chalk16.green(priorityCounts.low.toString().padStart(3))}`
|
|
4285
4554
|
);
|
|
4286
4555
|
}
|
|
4287
4556
|
console.log("");
|
|
4288
4557
|
}
|
|
4289
4558
|
const topTags = Object.entries(tagCounts).sort((a, b) => b[1] - a[1]).slice(0, 5);
|
|
4290
4559
|
if (topTags.length > 0) {
|
|
4291
|
-
console.log(
|
|
4560
|
+
console.log(chalk16.bold("\u{1F3F7}\uFE0F Popular Tags"));
|
|
4292
4561
|
console.log("");
|
|
4293
4562
|
const maxTagCount = Math.max(...topTags.map(([, count]) => count));
|
|
4294
4563
|
console.log(
|
|
4295
|
-
` ${"Tag".padEnd(labelWidth)} ${
|
|
4564
|
+
` ${"Tag".padEnd(labelWidth)} ${chalk16.magenta("Count".padEnd(colWidth))}`
|
|
4296
4565
|
);
|
|
4297
4566
|
console.log(
|
|
4298
|
-
` ${
|
|
4567
|
+
` ${chalk16.dim("\u2500".repeat(labelWidth))} ${chalk16.dim("\u2500".repeat(colWidth))}`
|
|
4299
4568
|
);
|
|
4300
4569
|
for (const [tag, count] of topTags) {
|
|
4301
4570
|
const truncatedTag = tag.length > labelWidth ? tag.substring(0, labelWidth - 1) + "\u2026" : tag;
|
|
4302
4571
|
const bar = createBar(count, maxTagCount);
|
|
4303
4572
|
console.log(
|
|
4304
|
-
` ${truncatedTag.padEnd(labelWidth)} ${
|
|
4573
|
+
` ${truncatedTag.padEnd(labelWidth)} ${chalk16.magenta(bar.padEnd(barWidth))}${chalk16.magenta(count.toString().padStart(3))}`
|
|
4305
4574
|
);
|
|
4306
4575
|
}
|
|
4307
4576
|
console.log("");
|
|
@@ -4333,14 +4602,14 @@ async function showStats(options) {
|
|
|
4333
4602
|
]);
|
|
4334
4603
|
const sortedDates = Array.from(allDates).sort();
|
|
4335
4604
|
if (sortedDates.length > 0) {
|
|
4336
|
-
console.log(
|
|
4605
|
+
console.log(chalk16.bold(`\u{1F4C5} Activity (Last ${days} Days)`));
|
|
4337
4606
|
console.log("");
|
|
4338
4607
|
const colWidth = barWidth + 3;
|
|
4339
4608
|
console.log(
|
|
4340
|
-
` ${"Date".padEnd(15)} ${
|
|
4609
|
+
` ${"Date".padEnd(15)} ${chalk16.cyan("Created".padEnd(colWidth))} ${chalk16.green("Completed".padEnd(colWidth))}`
|
|
4341
4610
|
);
|
|
4342
4611
|
console.log(
|
|
4343
|
-
` ${
|
|
4612
|
+
` ${chalk16.dim("\u2500".repeat(15))} ${chalk16.dim("\u2500".repeat(colWidth))} ${chalk16.dim("\u2500".repeat(colWidth))}`
|
|
4344
4613
|
);
|
|
4345
4614
|
const maxCount = Math.max(
|
|
4346
4615
|
...Object.values(createdByDate),
|
|
@@ -4354,85 +4623,85 @@ async function showStats(options) {
|
|
|
4354
4623
|
const createdCol = `${createdBar.padEnd(barWidth)}${created.toString().padStart(3)}`;
|
|
4355
4624
|
const completedCol = `${completedBar.padEnd(barWidth)}${completed.toString().padStart(3)}`;
|
|
4356
4625
|
console.log(
|
|
4357
|
-
` ${
|
|
4626
|
+
` ${chalk16.dim(date.padEnd(15))} ${chalk16.cyan(createdCol)} ${chalk16.green(completedCol)}`
|
|
4358
4627
|
);
|
|
4359
4628
|
}
|
|
4360
4629
|
console.log("");
|
|
4361
4630
|
}
|
|
4362
4631
|
}
|
|
4363
4632
|
if (showVelocity) {
|
|
4364
|
-
console.log(
|
|
4633
|
+
console.log(chalk16.bold("\u{1F680} Velocity Metrics"));
|
|
4365
4634
|
console.log("");
|
|
4366
|
-
console.log(
|
|
4635
|
+
console.log(chalk16.bold("\u23F1\uFE0F Cycle Time (Created \u2192 Completed)"));
|
|
4367
4636
|
console.log("");
|
|
4368
4637
|
console.log(
|
|
4369
4638
|
` ${"Metric".padEnd(labelWidth)} ${"Days".padStart(valueWidth)}`
|
|
4370
4639
|
);
|
|
4371
4640
|
console.log(
|
|
4372
|
-
` ${
|
|
4641
|
+
` ${chalk16.dim("\u2500".repeat(labelWidth))} ${chalk16.dim("\u2500".repeat(valueWidth))}`
|
|
4373
4642
|
);
|
|
4374
4643
|
console.log(
|
|
4375
|
-
` ${"Average".padEnd(labelWidth)} ${
|
|
4644
|
+
` ${"Average".padEnd(labelWidth)} ${chalk16.cyan(velocityMetrics.cycleTime.average.toFixed(1).padStart(valueWidth))}`
|
|
4376
4645
|
);
|
|
4377
4646
|
console.log(
|
|
4378
|
-
` ${"Median".padEnd(labelWidth)} ${
|
|
4647
|
+
` ${"Median".padEnd(labelWidth)} ${chalk16.cyan(velocityMetrics.cycleTime.median.toFixed(1).padStart(valueWidth))}`
|
|
4379
4648
|
);
|
|
4380
4649
|
console.log(
|
|
4381
|
-
` ${"90th Percentile".padEnd(labelWidth)} ${
|
|
4650
|
+
` ${"90th Percentile".padEnd(labelWidth)} ${chalk16.yellow(velocityMetrics.cycleTime.p90.toFixed(1).padStart(valueWidth))}`
|
|
4382
4651
|
);
|
|
4383
4652
|
console.log("");
|
|
4384
|
-
console.log(
|
|
4653
|
+
console.log(chalk16.bold("\u{1F4E6} Throughput"));
|
|
4385
4654
|
console.log("");
|
|
4386
4655
|
console.log(
|
|
4387
4656
|
` ${"Period".padEnd(labelWidth)} ${"Specs".padStart(valueWidth)}`
|
|
4388
4657
|
);
|
|
4389
4658
|
console.log(
|
|
4390
|
-
` ${
|
|
4659
|
+
` ${chalk16.dim("\u2500".repeat(labelWidth))} ${chalk16.dim("\u2500".repeat(valueWidth))}`
|
|
4391
4660
|
);
|
|
4392
4661
|
console.log(
|
|
4393
|
-
` ${"Last 7 days".padEnd(labelWidth)} ${
|
|
4662
|
+
` ${"Last 7 days".padEnd(labelWidth)} ${chalk16.green(velocityMetrics.throughput.perWeek.toString().padStart(valueWidth))}`
|
|
4394
4663
|
);
|
|
4395
4664
|
console.log(
|
|
4396
|
-
` ${"Last 30 days".padEnd(labelWidth)} ${
|
|
4665
|
+
` ${"Last 30 days".padEnd(labelWidth)} ${chalk16.green(velocityMetrics.throughput.perMonth.toString().padStart(valueWidth))}`
|
|
4397
4666
|
);
|
|
4398
|
-
const trendColor = velocityMetrics.throughput.trend === "up" ?
|
|
4667
|
+
const trendColor = velocityMetrics.throughput.trend === "up" ? chalk16.green : velocityMetrics.throughput.trend === "down" ? chalk16.red : chalk16.yellow;
|
|
4399
4668
|
const trendSymbol = velocityMetrics.throughput.trend === "up" ? "\u2191" : velocityMetrics.throughput.trend === "down" ? "\u2193" : "\u2192";
|
|
4400
4669
|
console.log(
|
|
4401
4670
|
` ${"Trend".padEnd(labelWidth)} ${trendColor(trendSymbol + " " + velocityMetrics.throughput.trend.padStart(valueWidth - 2))}`
|
|
4402
4671
|
);
|
|
4403
4672
|
console.log("");
|
|
4404
|
-
console.log(
|
|
4673
|
+
console.log(chalk16.bold("\u{1F504} Work In Progress"));
|
|
4405
4674
|
console.log("");
|
|
4406
4675
|
console.log(
|
|
4407
4676
|
` ${"Metric".padEnd(labelWidth)} ${"Specs".padStart(valueWidth)}`
|
|
4408
4677
|
);
|
|
4409
4678
|
console.log(
|
|
4410
|
-
` ${
|
|
4679
|
+
` ${chalk16.dim("\u2500".repeat(labelWidth))} ${chalk16.dim("\u2500".repeat(valueWidth))}`
|
|
4411
4680
|
);
|
|
4412
4681
|
console.log(
|
|
4413
|
-
` ${"Current".padEnd(labelWidth)} ${
|
|
4682
|
+
` ${"Current".padEnd(labelWidth)} ${chalk16.yellow(velocityMetrics.wip.current.toString().padStart(valueWidth))}`
|
|
4414
4683
|
);
|
|
4415
4684
|
console.log(
|
|
4416
|
-
` ${"30-day Average".padEnd(labelWidth)} ${
|
|
4685
|
+
` ${"30-day Average".padEnd(labelWidth)} ${chalk16.cyan(velocityMetrics.wip.average.toFixed(1).padStart(valueWidth))}`
|
|
4417
4686
|
);
|
|
4418
4687
|
console.log("");
|
|
4419
4688
|
if (velocityMetrics.leadTime.plannedToInProgress > 0 || velocityMetrics.leadTime.inProgressToComplete > 0) {
|
|
4420
|
-
console.log(
|
|
4689
|
+
console.log(chalk16.bold("\u{1F500} Lead Time by Stage"));
|
|
4421
4690
|
console.log("");
|
|
4422
4691
|
console.log(
|
|
4423
4692
|
` ${"Stage".padEnd(labelWidth)} ${"Days".padStart(valueWidth)}`
|
|
4424
4693
|
);
|
|
4425
4694
|
console.log(
|
|
4426
|
-
` ${
|
|
4695
|
+
` ${chalk16.dim("\u2500".repeat(labelWidth))} ${chalk16.dim("\u2500".repeat(valueWidth))}`
|
|
4427
4696
|
);
|
|
4428
4697
|
if (velocityMetrics.leadTime.plannedToInProgress > 0) {
|
|
4429
4698
|
console.log(
|
|
4430
|
-
` ${"Planned \u2192 In Progress".padEnd(labelWidth)} ${
|
|
4699
|
+
` ${"Planned \u2192 In Progress".padEnd(labelWidth)} ${chalk16.cyan(velocityMetrics.leadTime.plannedToInProgress.toFixed(1).padStart(valueWidth))}`
|
|
4431
4700
|
);
|
|
4432
4701
|
}
|
|
4433
4702
|
if (velocityMetrics.leadTime.inProgressToComplete > 0) {
|
|
4434
4703
|
console.log(
|
|
4435
|
-
` ${"In Progress \u2192 Complete".padEnd(labelWidth)} ${
|
|
4704
|
+
` ${"In Progress \u2192 Complete".padEnd(labelWidth)} ${chalk16.green(velocityMetrics.leadTime.inProgressToComplete.toFixed(1).padStart(valueWidth))}`
|
|
4436
4705
|
);
|
|
4437
4706
|
}
|
|
4438
4707
|
console.log("");
|
|
@@ -4443,16 +4712,16 @@ function searchCommand() {
|
|
|
4443
4712
|
return new Command("search").description("Full-text search with advanced query syntax").argument("[query]", "Search query (supports AND, OR, NOT, field:value, fuzzy~)").option("--status <status>", "Filter by status").option("--tag <tag>", "Filter by tag").option("--priority <priority>", "Filter by priority").option("--assignee <name>", "Filter by assignee").option("--field <name=value...>", "Filter by custom field (can specify multiple)").option("--json", "Output as JSON").option("--help-syntax", "Show advanced query syntax help").action(async (query, options) => {
|
|
4444
4713
|
if (options.helpSyntax) {
|
|
4445
4714
|
console.log("");
|
|
4446
|
-
console.log(
|
|
4447
|
-
console.log(
|
|
4715
|
+
console.log(chalk16.cyan("Advanced Search Syntax"));
|
|
4716
|
+
console.log(chalk16.gray("\u2500".repeat(60)));
|
|
4448
4717
|
console.log("");
|
|
4449
4718
|
console.log(getSearchSyntaxHelp());
|
|
4450
4719
|
console.log("");
|
|
4451
4720
|
return;
|
|
4452
4721
|
}
|
|
4453
4722
|
if (!query) {
|
|
4454
|
-
console.log(
|
|
4455
|
-
console.log(
|
|
4723
|
+
console.log(chalk16.yellow("Usage: lean-spec search <query>"));
|
|
4724
|
+
console.log(chalk16.gray("Use --help-syntax for advanced query syntax"));
|
|
4456
4725
|
return;
|
|
4457
4726
|
}
|
|
4458
4727
|
const customFields = parseCustomFieldOptions(options.field);
|
|
@@ -4525,34 +4794,34 @@ async function performSearch(query, options) {
|
|
|
4525
4794
|
}
|
|
4526
4795
|
if (results.length === 0) {
|
|
4527
4796
|
console.log("");
|
|
4528
|
-
console.log(
|
|
4797
|
+
console.log(chalk16.yellow(`\u{1F50D} No specs found matching "${sanitizeUserInput(query)}"`));
|
|
4529
4798
|
if (Object.keys(filter).length > 0) {
|
|
4530
4799
|
const filters = [];
|
|
4531
4800
|
if (options.status) filters.push(`status=${sanitizeUserInput(options.status)}`);
|
|
4532
4801
|
if (options.tag) filters.push(`tag=${sanitizeUserInput(options.tag)}`);
|
|
4533
4802
|
if (options.priority) filters.push(`priority=${sanitizeUserInput(options.priority)}`);
|
|
4534
4803
|
if (options.assignee) filters.push(`assignee=${sanitizeUserInput(options.assignee)}`);
|
|
4535
|
-
console.log(
|
|
4804
|
+
console.log(chalk16.gray(`With filters: ${filters.join(", ")}`));
|
|
4536
4805
|
}
|
|
4537
4806
|
console.log("");
|
|
4538
4807
|
return;
|
|
4539
4808
|
}
|
|
4540
4809
|
console.log("");
|
|
4541
|
-
console.log(
|
|
4542
|
-
console.log(
|
|
4810
|
+
console.log(chalk16.green(`\u{1F50D} Found ${results.length} spec${results.length === 1 ? "" : "s"} matching "${sanitizeUserInput(query)}"`));
|
|
4811
|
+
console.log(chalk16.gray(` Searched ${metadata.specsSearched} specs in ${metadata.searchTime}ms`));
|
|
4543
4812
|
if (Object.keys(filter).length > 0) {
|
|
4544
4813
|
const filters = [];
|
|
4545
4814
|
if (options.status) filters.push(`status=${sanitizeUserInput(options.status)}`);
|
|
4546
4815
|
if (options.tag) filters.push(`tag=${sanitizeUserInput(options.tag)}`);
|
|
4547
4816
|
if (options.priority) filters.push(`priority=${sanitizeUserInput(options.priority)}`);
|
|
4548
4817
|
if (options.assignee) filters.push(`assignee=${sanitizeUserInput(options.assignee)}`);
|
|
4549
|
-
console.log(
|
|
4818
|
+
console.log(chalk16.gray(` With filters: ${filters.join(", ")}`));
|
|
4550
4819
|
}
|
|
4551
4820
|
console.log("");
|
|
4552
4821
|
for (const result of results) {
|
|
4553
4822
|
const { spec, matches, score, totalMatches } = result;
|
|
4554
4823
|
const statusEmoji = spec.status === "in-progress" ? "\u{1F528}" : spec.status === "complete" ? "\u2705" : "\u{1F4C5}";
|
|
4555
|
-
console.log(
|
|
4824
|
+
console.log(chalk16.cyan(`${statusEmoji} ${sanitizeUserInput(spec.path)} ${chalk16.gray(`(${score}% match)`)}`));
|
|
4556
4825
|
const meta = [];
|
|
4557
4826
|
if (spec.priority) {
|
|
4558
4827
|
const priorityEmoji = spec.priority === "critical" ? "\u{1F534}" : spec.priority === "high" ? "\u{1F7E1}" : spec.priority === "medium" ? "\u{1F7E0}" : "\u{1F7E2}";
|
|
@@ -4562,30 +4831,30 @@ async function performSearch(query, options) {
|
|
|
4562
4831
|
meta.push(`[${spec.tags.map((tag) => sanitizeUserInput(tag)).join(", ")}]`);
|
|
4563
4832
|
}
|
|
4564
4833
|
if (meta.length > 0) {
|
|
4565
|
-
console.log(
|
|
4834
|
+
console.log(chalk16.gray(` ${meta.join(" \u2022 ")}`));
|
|
4566
4835
|
}
|
|
4567
4836
|
const titleMatch = matches.find((m) => m.field === "title");
|
|
4568
4837
|
if (titleMatch) {
|
|
4569
|
-
console.log(` ${
|
|
4838
|
+
console.log(` ${chalk16.bold("Title:")} ${highlightMatches(titleMatch.text, titleMatch.highlights)}`);
|
|
4570
4839
|
}
|
|
4571
4840
|
const descMatch = matches.find((m) => m.field === "description");
|
|
4572
4841
|
if (descMatch) {
|
|
4573
|
-
console.log(` ${
|
|
4842
|
+
console.log(` ${chalk16.bold("Description:")} ${highlightMatches(descMatch.text, descMatch.highlights)}`);
|
|
4574
4843
|
}
|
|
4575
4844
|
const tagMatches = matches.filter((m) => m.field === "tags");
|
|
4576
4845
|
if (tagMatches.length > 0) {
|
|
4577
|
-
console.log(` ${
|
|
4846
|
+
console.log(` ${chalk16.bold("Tags:")} ${tagMatches.map((m) => highlightMatches(m.text, m.highlights)).join(", ")}`);
|
|
4578
4847
|
}
|
|
4579
4848
|
const contentMatches = matches.filter((m) => m.field === "content");
|
|
4580
4849
|
if (contentMatches.length > 0) {
|
|
4581
|
-
console.log(` ${
|
|
4850
|
+
console.log(` ${chalk16.bold("Content matches:")}`);
|
|
4582
4851
|
for (const match of contentMatches) {
|
|
4583
|
-
const lineInfo = match.lineNumber ?
|
|
4852
|
+
const lineInfo = match.lineNumber ? chalk16.gray(`[L${match.lineNumber}]`) : "";
|
|
4584
4853
|
console.log(` ${lineInfo} ${highlightMatches(match.text, match.highlights)}`);
|
|
4585
4854
|
}
|
|
4586
4855
|
}
|
|
4587
4856
|
if (totalMatches > matches.length) {
|
|
4588
|
-
console.log(
|
|
4857
|
+
console.log(chalk16.gray(` ... and ${totalMatches - matches.length} more match${totalMatches - matches.length === 1 ? "" : "es"}`));
|
|
4589
4858
|
}
|
|
4590
4859
|
console.log("");
|
|
4591
4860
|
}
|
|
@@ -4596,7 +4865,7 @@ function highlightMatches(text, highlights) {
|
|
|
4596
4865
|
let lastEnd = 0;
|
|
4597
4866
|
for (const [start, end] of highlights) {
|
|
4598
4867
|
result += text.substring(lastEnd, start);
|
|
4599
|
-
result +=
|
|
4868
|
+
result += chalk16.yellow(text.substring(start, end));
|
|
4600
4869
|
lastEnd = end;
|
|
4601
4870
|
}
|
|
4602
4871
|
result += text.substring(lastEnd);
|
|
@@ -4614,7 +4883,7 @@ async function showDeps(specPath, options = {}) {
|
|
|
4614
4883
|
await autoCheckIfEnabled();
|
|
4615
4884
|
const config = await loadConfig();
|
|
4616
4885
|
const cwd = process.cwd();
|
|
4617
|
-
const specsDir =
|
|
4886
|
+
const specsDir = path13.join(cwd, config.specsDir);
|
|
4618
4887
|
const resolvedPath = await resolveSpecPath(specPath, cwd, specsDir);
|
|
4619
4888
|
if (!resolvedPath) {
|
|
4620
4889
|
throw new Error(`Spec not found: ${sanitizeUserInput(specPath)}`);
|
|
@@ -4672,17 +4941,17 @@ async function showDeps(specPath, options = {}) {
|
|
|
4672
4941
|
return;
|
|
4673
4942
|
}
|
|
4674
4943
|
console.log("");
|
|
4675
|
-
console.log(
|
|
4944
|
+
console.log(chalk16.green(`\u{1F4E6} Dependencies for ${chalk16.cyan(sanitizeUserInput(spec.path))}`));
|
|
4676
4945
|
console.log("");
|
|
4677
4946
|
const hasAnyDependencies = dependsOn.length > 0 || requiredBy.length > 0;
|
|
4678
4947
|
if (!hasAnyDependencies) {
|
|
4679
|
-
console.log(
|
|
4948
|
+
console.log(chalk16.gray(" No dependencies"));
|
|
4680
4949
|
console.log("");
|
|
4681
4950
|
return;
|
|
4682
4951
|
}
|
|
4683
4952
|
if ((mode === "complete" || mode === "upstream" || mode === "impact") && dependsOn.length > 0) {
|
|
4684
4953
|
const label = mode === "complete" ? "Depends On" : mode === "upstream" ? "Upstream Dependencies" : "Upstream (Impact)";
|
|
4685
|
-
console.log(
|
|
4954
|
+
console.log(chalk16.bold(`${label}:`));
|
|
4686
4955
|
for (const dep of dependsOn) {
|
|
4687
4956
|
const status = getStatusIndicator(dep.frontmatter.status);
|
|
4688
4957
|
console.log(` \u2192 ${sanitizeUserInput(dep.path)} ${status}`);
|
|
@@ -4691,7 +4960,7 @@ async function showDeps(specPath, options = {}) {
|
|
|
4691
4960
|
}
|
|
4692
4961
|
if ((mode === "complete" || mode === "downstream" || mode === "impact") && requiredBy.length > 0) {
|
|
4693
4962
|
const label = mode === "complete" ? "Required By" : mode === "downstream" ? "Downstream Dependents" : "Downstream (Impact)";
|
|
4694
|
-
console.log(
|
|
4963
|
+
console.log(chalk16.bold(`${label}:`));
|
|
4695
4964
|
for (const blocked of requiredBy) {
|
|
4696
4965
|
const status = getStatusIndicator(blocked.frontmatter.status);
|
|
4697
4966
|
console.log(` \u2190 ${sanitizeUserInput(blocked.path)} ${status}`);
|
|
@@ -4699,15 +4968,15 @@ async function showDeps(specPath, options = {}) {
|
|
|
4699
4968
|
console.log("");
|
|
4700
4969
|
}
|
|
4701
4970
|
if (mode === "complete" && (options.graph || dependsOn.length > 0)) {
|
|
4702
|
-
console.log(
|
|
4971
|
+
console.log(chalk16.bold("Dependency Chain:"));
|
|
4703
4972
|
const chain = buildDependencyChain(spec, specMap, options.depth || 3);
|
|
4704
4973
|
displayChain(chain, 0);
|
|
4705
4974
|
console.log("");
|
|
4706
4975
|
}
|
|
4707
4976
|
if (mode === "impact") {
|
|
4708
4977
|
const total = dependsOn.length + requiredBy.length;
|
|
4709
|
-
console.log(
|
|
4710
|
-
console.log(` Changing this spec affects ${
|
|
4978
|
+
console.log(chalk16.bold(`Impact Summary:`));
|
|
4979
|
+
console.log(` Changing this spec affects ${chalk16.yellow(total)} specs total`);
|
|
4711
4980
|
console.log(` Upstream: ${dependsOn.length} | Downstream: ${requiredBy.length}`);
|
|
4712
4981
|
console.log("");
|
|
4713
4982
|
}
|
|
@@ -4737,7 +5006,7 @@ function buildDependencyChain(spec, specMap, maxDepth, currentDepth = 0, visited
|
|
|
4737
5006
|
function displayChain(node, level) {
|
|
4738
5007
|
const indent = " ".repeat(level);
|
|
4739
5008
|
const status = getStatusIndicator(node.spec.frontmatter.status);
|
|
4740
|
-
const name = level === 0 ?
|
|
5009
|
+
const name = level === 0 ? chalk16.cyan(node.spec.path) : node.spec.path;
|
|
4741
5010
|
console.log(`${indent}${name} ${status}`);
|
|
4742
5011
|
for (const dep of node.dependencies) {
|
|
4743
5012
|
const prefix = " ".repeat(level) + "\u2514\u2500 ";
|
|
@@ -4798,19 +5067,19 @@ async function showTimeline(options) {
|
|
|
4798
5067
|
console.log(JSON.stringify(jsonOutput, null, 2));
|
|
4799
5068
|
return;
|
|
4800
5069
|
}
|
|
4801
|
-
console.log(
|
|
5070
|
+
console.log(chalk16.bold.cyan("\u{1F4C8} Spec Timeline"));
|
|
4802
5071
|
console.log("");
|
|
4803
5072
|
const allDates = /* @__PURE__ */ new Set([...Object.keys(createdByDate), ...Object.keys(completedByDate)]);
|
|
4804
5073
|
const sortedDates = Array.from(allDates).sort();
|
|
4805
5074
|
if (sortedDates.length > 0) {
|
|
4806
|
-
console.log(
|
|
5075
|
+
console.log(chalk16.bold(`\u{1F4C5} Activity (Last ${days} Days)`));
|
|
4807
5076
|
console.log("");
|
|
4808
5077
|
const labelWidth2 = 15;
|
|
4809
5078
|
const barWidth = 20;
|
|
4810
5079
|
const specsWidth = 3;
|
|
4811
5080
|
const colWidth = barWidth + specsWidth;
|
|
4812
|
-
console.log(` ${"Date".padEnd(labelWidth2)} ${
|
|
4813
|
-
console.log(` ${
|
|
5081
|
+
console.log(` ${"Date".padEnd(labelWidth2)} ${chalk16.cyan("Created".padEnd(colWidth))} ${chalk16.green("Completed".padEnd(colWidth))}`);
|
|
5082
|
+
console.log(` ${chalk16.dim("\u2500".repeat(labelWidth2))} ${chalk16.dim("\u2500".repeat(colWidth))} ${chalk16.dim("\u2500".repeat(colWidth))}`);
|
|
4814
5083
|
const maxCount = Math.max(...Object.values(createdByDate), ...Object.values(completedByDate));
|
|
4815
5084
|
for (const date of sortedDates) {
|
|
4816
5085
|
const created = createdByDate[date] || 0;
|
|
@@ -4819,7 +5088,7 @@ async function showTimeline(options) {
|
|
|
4819
5088
|
const completedBar = createBar(completed, maxCount, barWidth);
|
|
4820
5089
|
const createdCol = `${createdBar.padEnd(barWidth)}${created.toString().padStart(specsWidth)}`;
|
|
4821
5090
|
const completedCol = `${completedBar.padEnd(barWidth)}${completed.toString().padStart(specsWidth)}`;
|
|
4822
|
-
console.log(` ${
|
|
5091
|
+
console.log(` ${chalk16.dim(date.padEnd(labelWidth2))} ${chalk16.cyan(createdCol)} ${chalk16.green(completedCol)}`);
|
|
4823
5092
|
}
|
|
4824
5093
|
console.log("");
|
|
4825
5094
|
}
|
|
@@ -4829,18 +5098,18 @@ async function showTimeline(options) {
|
|
|
4829
5098
|
return dateB.diff(dateA);
|
|
4830
5099
|
}).slice(0, 6);
|
|
4831
5100
|
if (sortedMonths.length > 0) {
|
|
4832
|
-
console.log(
|
|
5101
|
+
console.log(chalk16.bold("\u{1F4CA} Monthly Overview"));
|
|
4833
5102
|
console.log("");
|
|
4834
5103
|
const labelWidth2 = 15;
|
|
4835
5104
|
const barWidth = 20;
|
|
4836
5105
|
const specsWidth = 3;
|
|
4837
5106
|
const colWidth = barWidth + specsWidth;
|
|
4838
|
-
console.log(` ${"Month".padEnd(labelWidth2)} ${
|
|
4839
|
-
console.log(` ${
|
|
5107
|
+
console.log(` ${"Month".padEnd(labelWidth2)} ${chalk16.magenta("Specs".padEnd(colWidth))}`);
|
|
5108
|
+
console.log(` ${chalk16.dim("\u2500".repeat(labelWidth2))} ${chalk16.dim("\u2500".repeat(colWidth))}`);
|
|
4840
5109
|
const maxCount = Math.max(...sortedMonths.map(([, count]) => count));
|
|
4841
5110
|
for (const [month, count] of sortedMonths) {
|
|
4842
5111
|
const bar = createBar(count, maxCount, barWidth);
|
|
4843
|
-
console.log(` ${month.padEnd(labelWidth2)} ${
|
|
5112
|
+
console.log(` ${month.padEnd(labelWidth2)} ${chalk16.magenta(bar.padEnd(barWidth))}${chalk16.magenta(count.toString().padStart(specsWidth))}`);
|
|
4844
5113
|
}
|
|
4845
5114
|
console.log("");
|
|
4846
5115
|
}
|
|
@@ -4854,14 +5123,14 @@ async function showTimeline(options) {
|
|
|
4854
5123
|
const completed = dayjs2(s.frontmatter.completed);
|
|
4855
5124
|
return completed.isAfter(today.subtract(30, "day"));
|
|
4856
5125
|
}).length;
|
|
4857
|
-
console.log(
|
|
5126
|
+
console.log(chalk16.bold("\u2705 Completion Rate"));
|
|
4858
5127
|
console.log("");
|
|
4859
5128
|
const labelWidth = 15;
|
|
4860
5129
|
const valueWidth = 5;
|
|
4861
5130
|
console.log(` ${"Period".padEnd(labelWidth)} ${"Specs".padStart(valueWidth)}`);
|
|
4862
|
-
console.log(` ${
|
|
4863
|
-
console.log(` ${"Last 7 days".padEnd(labelWidth)} ${
|
|
4864
|
-
console.log(` ${"Last 30 days".padEnd(labelWidth)} ${
|
|
5131
|
+
console.log(` ${chalk16.dim("\u2500".repeat(labelWidth))} ${chalk16.dim("\u2500".repeat(valueWidth))}`);
|
|
5132
|
+
console.log(` ${"Last 7 days".padEnd(labelWidth)} ${chalk16.green(last7Days.toString().padStart(valueWidth))}`);
|
|
5133
|
+
console.log(` ${"Last 30 days".padEnd(labelWidth)} ${chalk16.green(last30Days.toString().padStart(valueWidth))}`);
|
|
4865
5134
|
console.log("");
|
|
4866
5135
|
if (options.byTag) {
|
|
4867
5136
|
const tagStats = {};
|
|
@@ -4883,9 +5152,9 @@ async function showTimeline(options) {
|
|
|
4883
5152
|
}
|
|
4884
5153
|
const sortedTags = Object.entries(tagStats).sort((a, b) => b[1].created - a[1].created).slice(0, 10);
|
|
4885
5154
|
if (sortedTags.length > 0) {
|
|
4886
|
-
console.log(
|
|
5155
|
+
console.log(chalk16.bold("\u{1F3F7}\uFE0F By Tag"));
|
|
4887
5156
|
for (const [tag, stats] of sortedTags) {
|
|
4888
|
-
console.log(` ${
|
|
5157
|
+
console.log(` ${chalk16.dim("#")}${tag.padEnd(20)} ${chalk16.cyan(stats.created)} created \xB7 ${chalk16.green(stats.completed)} completed`);
|
|
4889
5158
|
}
|
|
4890
5159
|
console.log("");
|
|
4891
5160
|
}
|
|
@@ -4910,9 +5179,9 @@ async function showTimeline(options) {
|
|
|
4910
5179
|
}
|
|
4911
5180
|
const sortedAssignees = Object.entries(assigneeStats).sort((a, b) => b[1].created - a[1].created);
|
|
4912
5181
|
if (sortedAssignees.length > 0) {
|
|
4913
|
-
console.log(
|
|
5182
|
+
console.log(chalk16.bold("\u{1F464} By Assignee"));
|
|
4914
5183
|
for (const [assignee, stats] of sortedAssignees) {
|
|
4915
|
-
console.log(` ${
|
|
5184
|
+
console.log(` ${chalk16.dim("@")}${assignee.padEnd(20)} ${chalk16.cyan(stats.created)} created \xB7 ${chalk16.green(stats.completed)} completed`);
|
|
4916
5185
|
}
|
|
4917
5186
|
console.log("");
|
|
4918
5187
|
}
|
|
@@ -4930,10 +5199,10 @@ var STATUS_CONFIG2 = {
|
|
|
4930
5199
|
archived: { emoji: "\u{1F4E6}", color: "gray" }
|
|
4931
5200
|
};
|
|
4932
5201
|
var PRIORITY_CONFIG3 = {
|
|
4933
|
-
critical: { emoji: "\u{1F534}", label: "CRITICAL", colorFn:
|
|
4934
|
-
high: { emoji: "\u{1F7E0}", label: "HIGH", colorFn:
|
|
4935
|
-
medium: { emoji: "\u{1F7E1}", label: "MEDIUM", colorFn:
|
|
4936
|
-
low: { emoji: "\u{1F7E2}", label: "LOW", colorFn:
|
|
5202
|
+
critical: { emoji: "\u{1F534}", label: "CRITICAL", colorFn: chalk16.red },
|
|
5203
|
+
high: { emoji: "\u{1F7E0}", label: "HIGH", colorFn: chalk16.hex("#FFA500") },
|
|
5204
|
+
medium: { emoji: "\u{1F7E1}", label: "MEDIUM", colorFn: chalk16.yellow },
|
|
5205
|
+
low: { emoji: "\u{1F7E2}", label: "LOW", colorFn: chalk16.green }
|
|
4937
5206
|
};
|
|
4938
5207
|
function ganttCommand() {
|
|
4939
5208
|
return new Command("gantt").description("Show timeline with dependencies").option("--weeks <n>", "Show N weeks (default: 4)", parseInt).option("--show-complete", "Include completed specs").option("--critical-path", "Highlight critical path").option("--json", "Output as JSON").action(async (options) => {
|
|
@@ -4964,8 +5233,8 @@ async function showGantt(options) {
|
|
|
4964
5233
|
if (options.json) {
|
|
4965
5234
|
console.log(JSON.stringify({ specs: [], weeks }, null, 2));
|
|
4966
5235
|
} else {
|
|
4967
|
-
console.log(
|
|
4968
|
-
console.log(
|
|
5236
|
+
console.log(chalk16.dim("No active specs found."));
|
|
5237
|
+
console.log(chalk16.dim("Tip: Use --show-complete to include completed specs."));
|
|
4969
5238
|
}
|
|
4970
5239
|
return;
|
|
4971
5240
|
}
|
|
@@ -5018,7 +5287,7 @@ async function showGantt(options) {
|
|
|
5018
5287
|
const overdue = relevantSpecs.filter(
|
|
5019
5288
|
(s) => s.frontmatter.due && dayjs2(s.frontmatter.due).isBefore(today) && s.frontmatter.status !== "complete"
|
|
5020
5289
|
).length;
|
|
5021
|
-
console.log(
|
|
5290
|
+
console.log(chalk16.bold.cyan(`\u{1F4C5} Gantt Chart (${weeks} weeks from ${startDate.format("MMM D, YYYY")})`));
|
|
5022
5291
|
console.log("");
|
|
5023
5292
|
const specHeader = "Spec".padEnd(SPEC_COLUMN_WIDTH);
|
|
5024
5293
|
const timelineHeader = "Timeline";
|
|
@@ -5030,17 +5299,17 @@ async function showGantt(options) {
|
|
|
5030
5299
|
calendarDates.push(dateStr);
|
|
5031
5300
|
}
|
|
5032
5301
|
const dateRow = " ".repeat(SPEC_COLUMN_WIDTH) + COLUMN_SEPARATOR + calendarDates.join("");
|
|
5033
|
-
console.log(
|
|
5302
|
+
console.log(chalk16.dim(dateRow));
|
|
5034
5303
|
const specSeparator = "\u2500".repeat(SPEC_COLUMN_WIDTH);
|
|
5035
5304
|
const timelineSeparator = "\u2500".repeat(timelineColumnWidth);
|
|
5036
|
-
console.log(
|
|
5305
|
+
console.log(chalk16.dim(specSeparator + COLUMN_SEPARATOR + timelineSeparator));
|
|
5037
5306
|
const todayWeekOffset = today.diff(startDate, "week");
|
|
5038
5307
|
const todayMarkerPos = todayWeekOffset * 8;
|
|
5039
5308
|
let todayMarker = " ".repeat(SPEC_COLUMN_WIDTH) + COLUMN_SEPARATOR;
|
|
5040
5309
|
if (todayMarkerPos >= 0 && todayMarkerPos < timelineColumnWidth) {
|
|
5041
5310
|
todayMarker += " ".repeat(todayMarkerPos) + "\u2502 Today";
|
|
5042
5311
|
}
|
|
5043
|
-
console.log(
|
|
5312
|
+
console.log(chalk16.dim(todayMarker));
|
|
5044
5313
|
console.log("");
|
|
5045
5314
|
const priorities = ["critical", "high", "medium", "low"];
|
|
5046
5315
|
for (const priority of priorities) {
|
|
@@ -5058,9 +5327,9 @@ async function showGantt(options) {
|
|
|
5058
5327
|
const summaryParts = [];
|
|
5059
5328
|
if (inProgress > 0) summaryParts.push(`${inProgress} in-progress`);
|
|
5060
5329
|
if (planned > 0) summaryParts.push(`${planned} planned`);
|
|
5061
|
-
if (overdue > 0) summaryParts.push(
|
|
5062
|
-
console.log(
|
|
5063
|
-
console.log(
|
|
5330
|
+
if (overdue > 0) summaryParts.push(chalk16.red(`${overdue} overdue`));
|
|
5331
|
+
console.log(chalk16.bold("Summary: ") + summaryParts.join(" \xB7 "));
|
|
5332
|
+
console.log(chalk16.dim('\u{1F4A1} Tip: Add "due: YYYY-MM-DD" to frontmatter for timeline planning'));
|
|
5064
5333
|
}
|
|
5065
5334
|
function renderSpecRow(spec, startDate, endDate, weeks, today) {
|
|
5066
5335
|
const statusConfig = STATUS_CONFIG2[spec.frontmatter.status];
|
|
@@ -5073,7 +5342,7 @@ function renderSpecRow(spec, startDate, endDate, weeks, today) {
|
|
|
5073
5342
|
const specColumn = `${SPEC_INDENT}${emoji} ${specName}`.padEnd(SPEC_COLUMN_WIDTH);
|
|
5074
5343
|
let timelineColumn;
|
|
5075
5344
|
if (!spec.frontmatter.due) {
|
|
5076
|
-
timelineColumn =
|
|
5345
|
+
timelineColumn = chalk16.dim("(no due date set)");
|
|
5077
5346
|
} else {
|
|
5078
5347
|
timelineColumn = renderTimelineBar(spec, startDate, endDate, weeks, today);
|
|
5079
5348
|
}
|
|
@@ -5096,13 +5365,13 @@ function renderTimelineBar(spec, startDate, endDate, weeks, today) {
|
|
|
5096
5365
|
result += " ".repeat(barStart);
|
|
5097
5366
|
}
|
|
5098
5367
|
if (spec.frontmatter.status === "complete") {
|
|
5099
|
-
result +=
|
|
5368
|
+
result += chalk16.green(FILLED_BAR_CHAR.repeat(barLength));
|
|
5100
5369
|
} else if (spec.frontmatter.status === "in-progress") {
|
|
5101
5370
|
const halfLength = Math.floor(barLength / 2);
|
|
5102
|
-
result +=
|
|
5103
|
-
result +=
|
|
5371
|
+
result += chalk16.yellow(FILLED_BAR_CHAR.repeat(halfLength));
|
|
5372
|
+
result += chalk16.dim(EMPTY_BAR_CHAR.repeat(barLength - halfLength));
|
|
5104
5373
|
} else {
|
|
5105
|
-
result +=
|
|
5374
|
+
result += chalk16.dim(EMPTY_BAR_CHAR.repeat(barLength));
|
|
5106
5375
|
}
|
|
5107
5376
|
const trailingSpace = totalChars - barEnd;
|
|
5108
5377
|
if (trailingSpace > 0) {
|
|
@@ -5128,12 +5397,12 @@ async function countSpecTokens(specPath, options = {}) {
|
|
|
5128
5397
|
try {
|
|
5129
5398
|
const config = await loadConfig();
|
|
5130
5399
|
const cwd = process.cwd();
|
|
5131
|
-
const specsDir =
|
|
5400
|
+
const specsDir = path13.join(cwd, config.specsDir);
|
|
5132
5401
|
const resolvedPath = await resolveSpecPath(specPath, cwd, specsDir);
|
|
5133
5402
|
if (!resolvedPath) {
|
|
5134
5403
|
throw new Error(`Spec not found: ${sanitizeUserInput(specPath)}`);
|
|
5135
5404
|
}
|
|
5136
|
-
const specName =
|
|
5405
|
+
const specName = path13.basename(resolvedPath);
|
|
5137
5406
|
const result = await counter.countSpec(resolvedPath, {
|
|
5138
5407
|
detailed: options.detailed,
|
|
5139
5408
|
includeSubSpecs: options.includeSubSpecs
|
|
@@ -5146,42 +5415,42 @@ async function countSpecTokens(specPath, options = {}) {
|
|
|
5146
5415
|
}, null, 2));
|
|
5147
5416
|
return;
|
|
5148
5417
|
}
|
|
5149
|
-
console.log(
|
|
5418
|
+
console.log(chalk16.bold.cyan(`\u{1F4CA} Token Count: ${specName}`));
|
|
5150
5419
|
console.log("");
|
|
5151
5420
|
const indicators = counter.getPerformanceIndicators(result.total);
|
|
5152
5421
|
const levelEmoji = indicators.level === "excellent" ? "\u2705" : indicators.level === "good" ? "\u{1F44D}" : indicators.level === "warning" ? "\u26A0\uFE0F" : "\u{1F534}";
|
|
5153
|
-
console.log(` Total: ${
|
|
5422
|
+
console.log(` Total: ${chalk16.cyan(result.total.toLocaleString())} tokens ${levelEmoji}`);
|
|
5154
5423
|
console.log("");
|
|
5155
5424
|
if (result.files.length > 1 || options.detailed) {
|
|
5156
|
-
console.log(
|
|
5425
|
+
console.log(chalk16.bold("Files:"));
|
|
5157
5426
|
console.log("");
|
|
5158
5427
|
for (const file of result.files) {
|
|
5159
|
-
const lineInfo = file.lines ?
|
|
5160
|
-
console.log(` ${file.path.padEnd(25)} ${
|
|
5428
|
+
const lineInfo = file.lines ? chalk16.dim(` (${file.lines} lines)`) : "";
|
|
5429
|
+
console.log(` ${file.path.padEnd(25)} ${chalk16.cyan(file.tokens.toLocaleString().padStart(6))} tokens${lineInfo}`);
|
|
5161
5430
|
}
|
|
5162
5431
|
console.log("");
|
|
5163
5432
|
}
|
|
5164
5433
|
if (options.detailed && result.breakdown) {
|
|
5165
5434
|
const b = result.breakdown;
|
|
5166
5435
|
const total = b.code + b.prose + b.tables + b.frontmatter;
|
|
5167
|
-
console.log(
|
|
5436
|
+
console.log(chalk16.bold("Content Breakdown:"));
|
|
5168
5437
|
console.log("");
|
|
5169
|
-
console.log(` Prose ${
|
|
5170
|
-
console.log(` Code ${
|
|
5171
|
-
console.log(` Tables ${
|
|
5172
|
-
console.log(` Frontmatter ${
|
|
5438
|
+
console.log(` Prose ${chalk16.cyan(b.prose.toLocaleString().padStart(6))} tokens ${chalk16.dim(`(${Math.round(b.prose / total * 100)}%)`)}`);
|
|
5439
|
+
console.log(` Code ${chalk16.cyan(b.code.toLocaleString().padStart(6))} tokens ${chalk16.dim(`(${Math.round(b.code / total * 100)}%)`)}`);
|
|
5440
|
+
console.log(` Tables ${chalk16.cyan(b.tables.toLocaleString().padStart(6))} tokens ${chalk16.dim(`(${Math.round(b.tables / total * 100)}%)`)}`);
|
|
5441
|
+
console.log(` Frontmatter ${chalk16.cyan(b.frontmatter.toLocaleString().padStart(6))} tokens ${chalk16.dim(`(${Math.round(b.frontmatter / total * 100)}%)`)}`);
|
|
5173
5442
|
console.log("");
|
|
5174
5443
|
}
|
|
5175
|
-
console.log(
|
|
5444
|
+
console.log(chalk16.bold("Performance Indicators:"));
|
|
5176
5445
|
console.log("");
|
|
5177
|
-
const costColor = indicators.costMultiplier < 2 ?
|
|
5178
|
-
const effectivenessColor = indicators.effectiveness >= 95 ?
|
|
5179
|
-
console.log(` Cost multiplier: ${costColor(`${indicators.costMultiplier}x`)} ${
|
|
5180
|
-
console.log(` AI effectiveness: ${effectivenessColor(`~${indicators.effectiveness}%`)} ${
|
|
5446
|
+
const costColor = indicators.costMultiplier < 2 ? chalk16.green : indicators.costMultiplier < 4 ? chalk16.yellow : chalk16.red;
|
|
5447
|
+
const effectivenessColor = indicators.effectiveness >= 95 ? chalk16.green : indicators.effectiveness >= 85 ? chalk16.yellow : chalk16.red;
|
|
5448
|
+
console.log(` Cost multiplier: ${costColor(`${indicators.costMultiplier}x`)} ${chalk16.dim("vs 1,200 token baseline")}`);
|
|
5449
|
+
console.log(` AI effectiveness: ${effectivenessColor(`~${indicators.effectiveness}%`)} ${chalk16.dim("(hypothesis)")}`);
|
|
5181
5450
|
console.log(` Context Economy: ${levelEmoji} ${indicators.recommendation}`);
|
|
5182
5451
|
console.log("");
|
|
5183
5452
|
if (!options.includeSubSpecs && result.files.length === 1) {
|
|
5184
|
-
console.log(
|
|
5453
|
+
console.log(chalk16.dim("\u{1F4A1} Use `--include-sub-specs` to count all sub-spec files"));
|
|
5185
5454
|
}
|
|
5186
5455
|
} finally {
|
|
5187
5456
|
counter.dispose();
|
|
@@ -5227,46 +5496,46 @@ async function tokensAllCommand(options = {}) {
|
|
|
5227
5496
|
console.log(JSON.stringify(results, null, 2));
|
|
5228
5497
|
return;
|
|
5229
5498
|
}
|
|
5230
|
-
console.log(
|
|
5499
|
+
console.log(chalk16.bold.cyan("\u{1F4CA} Token Counts"));
|
|
5231
5500
|
console.log("");
|
|
5232
|
-
console.log(
|
|
5501
|
+
console.log(chalk16.dim(`Sorted by: ${sortBy}`));
|
|
5233
5502
|
console.log("");
|
|
5234
5503
|
const totalTokens = results.reduce((sum, r) => sum + r.tokens, 0);
|
|
5235
5504
|
const avgTokens = Math.round(totalTokens / results.length);
|
|
5236
5505
|
const warningCount = results.filter((r) => r.level === "warning" || r.level === "problem").length;
|
|
5237
|
-
console.log(
|
|
5506
|
+
console.log(chalk16.bold("Summary:"));
|
|
5238
5507
|
console.log("");
|
|
5239
|
-
console.log(` Total specs: ${
|
|
5240
|
-
console.log(` Total tokens: ${
|
|
5241
|
-
console.log(` Average tokens: ${
|
|
5508
|
+
console.log(` Total specs: ${chalk16.cyan(results.length)}`);
|
|
5509
|
+
console.log(` Total tokens: ${chalk16.cyan(totalTokens.toLocaleString())}`);
|
|
5510
|
+
console.log(` Average tokens: ${chalk16.cyan(avgTokens.toLocaleString())}`);
|
|
5242
5511
|
if (warningCount > 0) {
|
|
5243
|
-
console.log(` Needs review: ${
|
|
5512
|
+
console.log(` Needs review: ${chalk16.yellow(warningCount)} specs ${chalk16.dim("(\u26A0\uFE0F or \u{1F534})")}`);
|
|
5244
5513
|
}
|
|
5245
5514
|
console.log("");
|
|
5246
5515
|
const nameCol = 35;
|
|
5247
5516
|
const tokensCol = 10;
|
|
5248
5517
|
const linesCol = 8;
|
|
5249
|
-
console.log(
|
|
5518
|
+
console.log(chalk16.bold(
|
|
5250
5519
|
"Spec".padEnd(nameCol) + "Tokens".padStart(tokensCol) + "Lines".padStart(linesCol) + " Status"
|
|
5251
5520
|
));
|
|
5252
|
-
console.log(
|
|
5521
|
+
console.log(chalk16.dim("\u2500".repeat(nameCol + tokensCol + linesCol + 10)));
|
|
5253
5522
|
const displayCount = options.all ? results.length : Math.min(20, results.length);
|
|
5254
5523
|
for (let i = 0; i < displayCount; i++) {
|
|
5255
5524
|
const r = results[i];
|
|
5256
5525
|
const emoji = r.level === "excellent" ? "\u2705" : r.level === "good" ? "\u{1F44D}" : r.level === "warning" ? "\u26A0\uFE0F" : "\u{1F534}";
|
|
5257
|
-
const tokensColor = r.level === "excellent" || r.level === "good" ?
|
|
5526
|
+
const tokensColor = r.level === "excellent" || r.level === "good" ? chalk16.cyan : r.level === "warning" ? chalk16.yellow : chalk16.red;
|
|
5258
5527
|
const name = r.name.length > nameCol - 2 ? r.name.substring(0, nameCol - 3) + "\u2026" : r.name;
|
|
5259
5528
|
console.log(
|
|
5260
|
-
name.padEnd(nameCol) + tokensColor(r.tokens.toLocaleString().padStart(tokensCol)) +
|
|
5529
|
+
name.padEnd(nameCol) + tokensColor(r.tokens.toLocaleString().padStart(tokensCol)) + chalk16.dim(r.lines.toString().padStart(linesCol)) + ` ${emoji}`
|
|
5261
5530
|
);
|
|
5262
5531
|
}
|
|
5263
5532
|
if (results.length > displayCount) {
|
|
5264
5533
|
console.log("");
|
|
5265
|
-
console.log(
|
|
5266
|
-
console.log(
|
|
5534
|
+
console.log(chalk16.dim(`... and ${results.length - displayCount} more specs`));
|
|
5535
|
+
console.log(chalk16.dim(`Use --all to show all specs`));
|
|
5267
5536
|
}
|
|
5268
5537
|
console.log("");
|
|
5269
|
-
console.log(
|
|
5538
|
+
console.log(chalk16.dim("Legend: \u2705 excellent (<2K) | \u{1F44D} good (<3.5K) | \u26A0\uFE0F warning (<5K) | \u{1F534} problem (>5K)"));
|
|
5270
5539
|
console.log("");
|
|
5271
5540
|
}
|
|
5272
5541
|
function analyzeCommand() {
|
|
@@ -5280,13 +5549,13 @@ async function analyzeSpec(specPath, options = {}) {
|
|
|
5280
5549
|
try {
|
|
5281
5550
|
const config = await loadConfig();
|
|
5282
5551
|
const cwd = process.cwd();
|
|
5283
|
-
const specsDir =
|
|
5552
|
+
const specsDir = path13.join(cwd, config.specsDir);
|
|
5284
5553
|
const resolvedPath = await resolveSpecPath(specPath, cwd, specsDir);
|
|
5285
5554
|
if (!resolvedPath) {
|
|
5286
5555
|
throw new Error(`Spec not found: ${sanitizeUserInput(specPath)}`);
|
|
5287
5556
|
}
|
|
5288
|
-
const specName =
|
|
5289
|
-
const readmePath =
|
|
5557
|
+
const specName = path13.basename(resolvedPath);
|
|
5558
|
+
const readmePath = path13.join(resolvedPath, "README.md");
|
|
5290
5559
|
const content = await readFile(readmePath, "utf-8");
|
|
5291
5560
|
const structure = analyzeMarkdownStructure(content);
|
|
5292
5561
|
const tokenResult = await counter.countSpec(resolvedPath, {
|
|
@@ -5389,44 +5658,44 @@ function getThresholdLimit(level) {
|
|
|
5389
5658
|
}
|
|
5390
5659
|
}
|
|
5391
5660
|
function displayAnalysis(result, verbose) {
|
|
5392
|
-
console.log(
|
|
5661
|
+
console.log(chalk16.bold.cyan(`\u{1F4CA} Spec Analysis: ${result.spec}`));
|
|
5393
5662
|
console.log("");
|
|
5394
5663
|
const statusEmoji = result.threshold.status === "excellent" ? "\u2705" : result.threshold.status === "good" ? "\u{1F44D}" : result.threshold.status === "warning" ? "\u26A0\uFE0F" : "\u{1F534}";
|
|
5395
|
-
const tokenColor = result.threshold.status === "excellent" || result.threshold.status === "good" ?
|
|
5396
|
-
console.log(
|
|
5397
|
-
console.log(
|
|
5398
|
-
console.log(
|
|
5664
|
+
const tokenColor = result.threshold.status === "excellent" || result.threshold.status === "good" ? chalk16.cyan : result.threshold.status === "warning" ? chalk16.yellow : chalk16.red;
|
|
5665
|
+
console.log(chalk16.bold("Token Count:"), tokenColor(result.metrics.tokens.toLocaleString()), "tokens", statusEmoji);
|
|
5666
|
+
console.log(chalk16.dim(` Threshold: ${result.threshold.limit.toLocaleString()} tokens`));
|
|
5667
|
+
console.log(chalk16.dim(` Status: ${result.threshold.message}`));
|
|
5399
5668
|
console.log("");
|
|
5400
|
-
console.log(
|
|
5401
|
-
console.log(` Lines: ${
|
|
5402
|
-
console.log(` Sections: ${
|
|
5403
|
-
console.log(` Code blocks: ${
|
|
5404
|
-
console.log(` Max nesting: ${
|
|
5669
|
+
console.log(chalk16.bold("Structure:"));
|
|
5670
|
+
console.log(` Lines: ${chalk16.cyan(result.metrics.lines.toLocaleString())}`);
|
|
5671
|
+
console.log(` Sections: ${chalk16.cyan(result.metrics.sections.total)} (H1:${result.metrics.sections.h1}, H2:${result.metrics.sections.h2}, H3:${result.metrics.sections.h3}, H4:${result.metrics.sections.h4})`);
|
|
5672
|
+
console.log(` Code blocks: ${chalk16.cyan(result.metrics.codeBlocks)}`);
|
|
5673
|
+
console.log(` Max nesting: ${chalk16.cyan(result.metrics.maxNesting)} levels`);
|
|
5405
5674
|
console.log("");
|
|
5406
5675
|
if (verbose && result.structure.length > 0) {
|
|
5407
5676
|
const topSections = result.structure.filter((s) => s.level <= 2).sort((a, b) => b.tokens - a.tokens).slice(0, 5);
|
|
5408
|
-
console.log(
|
|
5677
|
+
console.log(chalk16.bold("Top Sections by Size:"));
|
|
5409
5678
|
console.log("");
|
|
5410
5679
|
for (let i = 0; i < topSections.length; i++) {
|
|
5411
5680
|
const s = topSections[i];
|
|
5412
5681
|
const percentage = Math.round(s.tokens / result.metrics.tokens * 100);
|
|
5413
5682
|
const indent = " ".repeat(s.level - 1);
|
|
5414
5683
|
console.log(` ${i + 1}. ${indent}${s.section}`);
|
|
5415
|
-
console.log(` ${
|
|
5416
|
-
console.log(
|
|
5684
|
+
console.log(` ${chalk16.cyan(s.tokens.toLocaleString())} tokens / ${s.lineRange[1] - s.lineRange[0] + 1} lines ${chalk16.dim(`(${percentage}%)`)}`);
|
|
5685
|
+
console.log(chalk16.dim(` Lines ${s.lineRange[0]}-${s.lineRange[1]}`));
|
|
5417
5686
|
}
|
|
5418
5687
|
console.log("");
|
|
5419
5688
|
}
|
|
5420
|
-
const actionColor = result.recommendation.action === "none" ?
|
|
5421
|
-
console.log(
|
|
5422
|
-
console.log(
|
|
5423
|
-
console.log(
|
|
5689
|
+
const actionColor = result.recommendation.action === "none" ? chalk16.green : result.recommendation.action === "compact" ? chalk16.yellow : result.recommendation.action === "split" ? chalk16.red : chalk16.blue;
|
|
5690
|
+
console.log(chalk16.bold("Recommendation:"), actionColor(result.recommendation.action.toUpperCase()));
|
|
5691
|
+
console.log(chalk16.dim(` ${result.recommendation.reason}`));
|
|
5692
|
+
console.log(chalk16.dim(` Confidence: ${result.recommendation.confidence}`));
|
|
5424
5693
|
console.log("");
|
|
5425
5694
|
if (result.recommendation.action === "split") {
|
|
5426
|
-
console.log(
|
|
5427
|
-
console.log(
|
|
5695
|
+
console.log(chalk16.dim("\u{1F4A1} Use `lean-spec split` to partition into sub-specs"));
|
|
5696
|
+
console.log(chalk16.dim("\u{1F4A1} Consider splitting by H2 sections (concerns)"));
|
|
5428
5697
|
} else if (result.recommendation.action === "compact") {
|
|
5429
|
-
console.log(
|
|
5698
|
+
console.log(chalk16.dim("\u{1F4A1} Use `lean-spec compact` to remove redundancy"));
|
|
5430
5699
|
}
|
|
5431
5700
|
console.log("");
|
|
5432
5701
|
}
|
|
@@ -5458,13 +5727,13 @@ async function splitSpec(specPath, options) {
|
|
|
5458
5727
|
}
|
|
5459
5728
|
const config = await loadConfig();
|
|
5460
5729
|
const cwd = process.cwd();
|
|
5461
|
-
const specsDir =
|
|
5730
|
+
const specsDir = path13.join(cwd, config.specsDir);
|
|
5462
5731
|
const resolvedPath = await resolveSpecPath(specPath, cwd, specsDir);
|
|
5463
5732
|
if (!resolvedPath) {
|
|
5464
5733
|
throw new Error(`Spec not found: ${sanitizeUserInput(specPath)}`);
|
|
5465
5734
|
}
|
|
5466
|
-
const specName =
|
|
5467
|
-
const readmePath =
|
|
5735
|
+
const specName = path13.basename(resolvedPath);
|
|
5736
|
+
const readmePath = path13.join(resolvedPath, "README.md");
|
|
5468
5737
|
const content = await readFile(readmePath, "utf-8");
|
|
5469
5738
|
const parsedOutputs = parseOutputSpecs(options.outputs);
|
|
5470
5739
|
validateNoOverlaps(parsedOutputs);
|
|
@@ -5487,7 +5756,7 @@ async function splitSpec(specPath, options) {
|
|
|
5487
5756
|
await executeSplit(resolvedPath, specName, content, extractions, options);
|
|
5488
5757
|
} catch (error) {
|
|
5489
5758
|
if (error instanceof Error) {
|
|
5490
|
-
console.error(
|
|
5759
|
+
console.error(chalk16.red(`Error: ${error.message}`));
|
|
5491
5760
|
}
|
|
5492
5761
|
throw error;
|
|
5493
5762
|
}
|
|
@@ -5525,30 +5794,30 @@ function validateNoOverlaps(outputs) {
|
|
|
5525
5794
|
}
|
|
5526
5795
|
}
|
|
5527
5796
|
async function displayDryRun(specName, extractions) {
|
|
5528
|
-
console.log(
|
|
5797
|
+
console.log(chalk16.bold.cyan(`\u{1F4CB} Split Preview: ${specName}`));
|
|
5529
5798
|
console.log("");
|
|
5530
|
-
console.log(
|
|
5799
|
+
console.log(chalk16.bold("Would create:"));
|
|
5531
5800
|
console.log("");
|
|
5532
5801
|
for (const ext of extractions) {
|
|
5533
|
-
console.log(` ${
|
|
5802
|
+
console.log(` ${chalk16.cyan(ext.file)}`);
|
|
5534
5803
|
console.log(` Lines: ${ext.lines}`);
|
|
5535
5804
|
const previewLines = ext.content.split("\n").slice(0, 3);
|
|
5536
|
-
console.log(
|
|
5805
|
+
console.log(chalk16.dim(" Preview:"));
|
|
5537
5806
|
for (const line of previewLines) {
|
|
5538
|
-
console.log(
|
|
5807
|
+
console.log(chalk16.dim(` ${line.substring(0, 60)}${line.length > 60 ? "..." : ""}`));
|
|
5539
5808
|
}
|
|
5540
5809
|
console.log("");
|
|
5541
5810
|
}
|
|
5542
|
-
console.log(
|
|
5543
|
-
console.log(
|
|
5811
|
+
console.log(chalk16.dim("No files modified (dry run)"));
|
|
5812
|
+
console.log(chalk16.dim("Run without --dry-run to apply changes"));
|
|
5544
5813
|
console.log("");
|
|
5545
5814
|
}
|
|
5546
5815
|
async function executeSplit(specPath, specName, originalContent, extractions, options) {
|
|
5547
|
-
console.log(
|
|
5816
|
+
console.log(chalk16.bold.cyan(`\u2702\uFE0F Splitting: ${specName}`));
|
|
5548
5817
|
console.log("");
|
|
5549
5818
|
const frontmatter = parseFrontmatterFromString(originalContent);
|
|
5550
5819
|
for (const ext of extractions) {
|
|
5551
|
-
const outputPath =
|
|
5820
|
+
const outputPath = path13.join(specPath, ext.file);
|
|
5552
5821
|
let finalContent = ext.content;
|
|
5553
5822
|
if (ext.file === "README.md" && frontmatter) {
|
|
5554
5823
|
const { content: contentWithFrontmatter } = createUpdatedFrontmatter(
|
|
@@ -5558,21 +5827,21 @@ async function executeSplit(specPath, specName, originalContent, extractions, op
|
|
|
5558
5827
|
finalContent = contentWithFrontmatter;
|
|
5559
5828
|
}
|
|
5560
5829
|
await writeFile(outputPath, finalContent, "utf-8");
|
|
5561
|
-
console.log(
|
|
5830
|
+
console.log(chalk16.green(`\u2713 Created ${ext.file} (${ext.lines} lines)`));
|
|
5562
5831
|
}
|
|
5563
5832
|
if (options.updateRefs) {
|
|
5564
|
-
const readmePath =
|
|
5833
|
+
const readmePath = path13.join(specPath, "README.md");
|
|
5565
5834
|
const readmeContent = await readFile(readmePath, "utf-8");
|
|
5566
5835
|
const updatedReadme = await addSubSpecLinks(
|
|
5567
5836
|
readmeContent,
|
|
5568
5837
|
extractions.map((e) => e.file).filter((f) => f !== "README.md")
|
|
5569
5838
|
);
|
|
5570
5839
|
await writeFile(readmePath, updatedReadme, "utf-8");
|
|
5571
|
-
console.log(
|
|
5840
|
+
console.log(chalk16.green(`\u2713 Updated README.md with sub-spec links`));
|
|
5572
5841
|
}
|
|
5573
5842
|
console.log("");
|
|
5574
|
-
console.log(
|
|
5575
|
-
console.log(
|
|
5843
|
+
console.log(chalk16.bold.green("Split complete!"));
|
|
5844
|
+
console.log(chalk16.dim(`Created ${extractions.length} files in ${specName}`));
|
|
5576
5845
|
console.log("");
|
|
5577
5846
|
}
|
|
5578
5847
|
async function addSubSpecLinks(content, subSpecs) {
|
|
@@ -5640,13 +5909,13 @@ async function compactSpec(specPath, options) {
|
|
|
5640
5909
|
}
|
|
5641
5910
|
const config = await loadConfig();
|
|
5642
5911
|
const cwd = process.cwd();
|
|
5643
|
-
const specsDir =
|
|
5912
|
+
const specsDir = path13.join(cwd, config.specsDir);
|
|
5644
5913
|
const resolvedPath = await resolveSpecPath(specPath, cwd, specsDir);
|
|
5645
5914
|
if (!resolvedPath) {
|
|
5646
5915
|
throw new Error(`Spec not found: ${sanitizeUserInput(specPath)}`);
|
|
5647
5916
|
}
|
|
5648
|
-
const specName =
|
|
5649
|
-
const readmePath =
|
|
5917
|
+
const specName = path13.basename(resolvedPath);
|
|
5918
|
+
const readmePath = path13.join(resolvedPath, "README.md");
|
|
5650
5919
|
const content = await readFile(readmePath, "utf-8");
|
|
5651
5920
|
const parsedRemoves = parseRemoveSpecs(options.removes);
|
|
5652
5921
|
validateNoOverlaps2(parsedRemoves);
|
|
@@ -5657,7 +5926,7 @@ async function compactSpec(specPath, options) {
|
|
|
5657
5926
|
await executeCompact(readmePath, specName, content, parsedRemoves);
|
|
5658
5927
|
} catch (error) {
|
|
5659
5928
|
if (error instanceof Error) {
|
|
5660
|
-
console.error(
|
|
5929
|
+
console.error(chalk16.red(`Error: ${error.message}`));
|
|
5661
5930
|
}
|
|
5662
5931
|
throw error;
|
|
5663
5932
|
}
|
|
@@ -5696,9 +5965,9 @@ function validateNoOverlaps2(removes) {
|
|
|
5696
5965
|
}
|
|
5697
5966
|
}
|
|
5698
5967
|
async function displayDryRun2(specName, content, removes) {
|
|
5699
|
-
console.log(
|
|
5968
|
+
console.log(chalk16.bold.cyan(`\u{1F4CB} Compact Preview: ${specName}`));
|
|
5700
5969
|
console.log("");
|
|
5701
|
-
console.log(
|
|
5970
|
+
console.log(chalk16.bold("Would remove:"));
|
|
5702
5971
|
console.log("");
|
|
5703
5972
|
let totalLines = 0;
|
|
5704
5973
|
for (const remove of removes) {
|
|
@@ -5707,29 +5976,29 @@ async function displayDryRun2(specName, content, removes) {
|
|
|
5707
5976
|
const removedContent = extractLines(content, remove.startLine, remove.endLine);
|
|
5708
5977
|
const previewLines = removedContent.split("\n").slice(0, 3);
|
|
5709
5978
|
console.log(` Lines ${remove.startLine}-${remove.endLine} (${lineCount} lines)`);
|
|
5710
|
-
console.log(
|
|
5979
|
+
console.log(chalk16.dim(" Preview:"));
|
|
5711
5980
|
for (const line of previewLines) {
|
|
5712
|
-
console.log(
|
|
5981
|
+
console.log(chalk16.dim(` ${line.substring(0, 60)}${line.length > 60 ? "..." : ""}`));
|
|
5713
5982
|
}
|
|
5714
5983
|
if (removedContent.split("\n").length > 3) {
|
|
5715
|
-
console.log(
|
|
5984
|
+
console.log(chalk16.dim(` ... (${removedContent.split("\n").length - 3} more lines)`));
|
|
5716
5985
|
}
|
|
5717
5986
|
console.log("");
|
|
5718
5987
|
}
|
|
5719
5988
|
const originalLines = countLines(content);
|
|
5720
5989
|
const remainingLines = originalLines - totalLines;
|
|
5721
5990
|
const percentage = Math.round(totalLines / originalLines * 100);
|
|
5722
|
-
console.log(
|
|
5723
|
-
console.log(` Original lines: ${
|
|
5724
|
-
console.log(` Removing: ${
|
|
5725
|
-
console.log(` Remaining lines: ${
|
|
5991
|
+
console.log(chalk16.bold("Summary:"));
|
|
5992
|
+
console.log(` Original lines: ${chalk16.cyan(originalLines)}`);
|
|
5993
|
+
console.log(` Removing: ${chalk16.yellow(totalLines)} lines (${percentage}%)`);
|
|
5994
|
+
console.log(` Remaining lines: ${chalk16.cyan(remainingLines)}`);
|
|
5726
5995
|
console.log("");
|
|
5727
|
-
console.log(
|
|
5728
|
-
console.log(
|
|
5996
|
+
console.log(chalk16.dim("No files modified (dry run)"));
|
|
5997
|
+
console.log(chalk16.dim("Run without --dry-run to apply changes"));
|
|
5729
5998
|
console.log("");
|
|
5730
5999
|
}
|
|
5731
6000
|
async function executeCompact(readmePath, specName, content, removes) {
|
|
5732
|
-
console.log(
|
|
6001
|
+
console.log(chalk16.bold.cyan(`\u{1F5DC}\uFE0F Compacting: ${specName}`));
|
|
5733
6002
|
console.log("");
|
|
5734
6003
|
const sorted = [...removes].sort((a, b) => b.startLine - a.startLine);
|
|
5735
6004
|
let updatedContent = content;
|
|
@@ -5738,22 +6007,22 @@ async function executeCompact(readmePath, specName, content, removes) {
|
|
|
5738
6007
|
const lineCount = remove.endLine - remove.startLine + 1;
|
|
5739
6008
|
updatedContent = removeLines(updatedContent, remove.startLine, remove.endLine);
|
|
5740
6009
|
totalRemoved += lineCount;
|
|
5741
|
-
console.log(
|
|
6010
|
+
console.log(chalk16.green(`\u2713 Removed lines ${remove.startLine}-${remove.endLine} (${lineCount} lines)`));
|
|
5742
6011
|
}
|
|
5743
6012
|
await writeFile(readmePath, updatedContent, "utf-8");
|
|
5744
6013
|
const originalLines = countLines(content);
|
|
5745
6014
|
const finalLines = countLines(updatedContent);
|
|
5746
6015
|
const percentage = Math.round(totalRemoved / originalLines * 100);
|
|
5747
6016
|
console.log("");
|
|
5748
|
-
console.log(
|
|
5749
|
-
console.log(
|
|
5750
|
-
console.log(
|
|
6017
|
+
console.log(chalk16.bold.green("Compaction complete!"));
|
|
6018
|
+
console.log(chalk16.dim(`Removed ${totalRemoved} lines (${percentage}%)`));
|
|
6019
|
+
console.log(chalk16.dim(`${originalLines} \u2192 ${finalLines} lines`));
|
|
5751
6020
|
console.log("");
|
|
5752
6021
|
}
|
|
5753
6022
|
marked.use(markedTerminal());
|
|
5754
6023
|
async function readSpecContent(specPath, cwd = process.cwd()) {
|
|
5755
6024
|
const config = await loadConfig(cwd);
|
|
5756
|
-
const specsDir =
|
|
6025
|
+
const specsDir = path13.join(cwd, config.specsDir);
|
|
5757
6026
|
let resolvedPath = null;
|
|
5758
6027
|
let targetFile = null;
|
|
5759
6028
|
const pathParts = specPath.split("/").filter((p) => p);
|
|
@@ -5762,9 +6031,9 @@ async function readSpecContent(specPath, cwd = process.cwd()) {
|
|
|
5762
6031
|
const filePart = pathParts[pathParts.length - 1];
|
|
5763
6032
|
resolvedPath = await resolveSpecPath(specPart, cwd, specsDir);
|
|
5764
6033
|
if (resolvedPath) {
|
|
5765
|
-
targetFile =
|
|
6034
|
+
targetFile = path13.join(resolvedPath, filePart);
|
|
5766
6035
|
try {
|
|
5767
|
-
await
|
|
6036
|
+
await fs9.access(targetFile);
|
|
5768
6037
|
} catch {
|
|
5769
6038
|
return null;
|
|
5770
6039
|
}
|
|
@@ -5783,8 +6052,8 @@ async function readSpecContent(specPath, cwd = process.cwd()) {
|
|
|
5783
6052
|
if (!targetFile) {
|
|
5784
6053
|
return null;
|
|
5785
6054
|
}
|
|
5786
|
-
const rawContent = await
|
|
5787
|
-
const fileName =
|
|
6055
|
+
const rawContent = await fs9.readFile(targetFile, "utf-8");
|
|
6056
|
+
const fileName = path13.basename(targetFile);
|
|
5788
6057
|
const isSubSpec = fileName !== config.structure.defaultFile;
|
|
5789
6058
|
let frontmatter = null;
|
|
5790
6059
|
if (!isSubSpec) {
|
|
@@ -5813,7 +6082,7 @@ async function readSpecContent(specPath, cwd = process.cwd()) {
|
|
|
5813
6082
|
}
|
|
5814
6083
|
}
|
|
5815
6084
|
const content = lines.slice(contentStartIndex).join("\n").trim();
|
|
5816
|
-
const specName =
|
|
6085
|
+
const specName = path13.basename(resolvedPath);
|
|
5817
6086
|
const displayName = isSubSpec ? `${specName}/${fileName}` : specName;
|
|
5818
6087
|
return {
|
|
5819
6088
|
frontmatter,
|
|
@@ -5836,7 +6105,7 @@ function formatFrontmatter(frontmatter) {
|
|
|
5836
6105
|
archived: "\u{1F4E6}"
|
|
5837
6106
|
};
|
|
5838
6107
|
const statusEmoji = statusEmojis[frontmatter.status] || "\u{1F4C4}";
|
|
5839
|
-
lines.push(
|
|
6108
|
+
lines.push(chalk16.bold(`${statusEmoji} Status: `) + chalk16.cyan(frontmatter.status));
|
|
5840
6109
|
if (frontmatter.priority) {
|
|
5841
6110
|
const priorityEmojis = {
|
|
5842
6111
|
low: "\u{1F7E2}",
|
|
@@ -5845,25 +6114,25 @@ function formatFrontmatter(frontmatter) {
|
|
|
5845
6114
|
critical: "\u{1F534}"
|
|
5846
6115
|
};
|
|
5847
6116
|
const priorityEmoji = priorityEmojis[frontmatter.priority] || "";
|
|
5848
|
-
lines.push(
|
|
6117
|
+
lines.push(chalk16.bold(`${priorityEmoji} Priority: `) + chalk16.yellow(frontmatter.priority));
|
|
5849
6118
|
}
|
|
5850
6119
|
if (frontmatter.created) {
|
|
5851
|
-
lines.push(
|
|
6120
|
+
lines.push(chalk16.bold("\u{1F4C6} Created: ") + chalk16.gray(String(frontmatter.created)));
|
|
5852
6121
|
}
|
|
5853
6122
|
if (frontmatter.tags && frontmatter.tags.length > 0) {
|
|
5854
|
-
const tagStr = frontmatter.tags.map((tag) =>
|
|
5855
|
-
lines.push(
|
|
6123
|
+
const tagStr = frontmatter.tags.map((tag) => chalk16.blue(`#${tag}`)).join(" ");
|
|
6124
|
+
lines.push(chalk16.bold("\u{1F3F7}\uFE0F Tags: ") + tagStr);
|
|
5856
6125
|
}
|
|
5857
6126
|
if (frontmatter.assignee) {
|
|
5858
|
-
lines.push(
|
|
6127
|
+
lines.push(chalk16.bold("\u{1F464} Assignee: ") + chalk16.green(frontmatter.assignee));
|
|
5859
6128
|
}
|
|
5860
6129
|
const standardFields = ["status", "priority", "created", "tags", "assignee"];
|
|
5861
6130
|
const customFields = Object.entries(frontmatter).filter(([key]) => !standardFields.includes(key)).filter(([_, value]) => value !== void 0 && value !== null);
|
|
5862
6131
|
if (customFields.length > 0) {
|
|
5863
6132
|
lines.push("");
|
|
5864
|
-
lines.push(
|
|
6133
|
+
lines.push(chalk16.bold("Custom Fields:"));
|
|
5865
6134
|
for (const [key, value] of customFields) {
|
|
5866
|
-
lines.push(` ${
|
|
6135
|
+
lines.push(` ${chalk16.gray(key)}: ${chalk16.white(String(value))}`);
|
|
5867
6136
|
}
|
|
5868
6137
|
}
|
|
5869
6138
|
return lines.join("\n");
|
|
@@ -5871,11 +6140,11 @@ function formatFrontmatter(frontmatter) {
|
|
|
5871
6140
|
function displayFormattedSpec(spec) {
|
|
5872
6141
|
const output = [];
|
|
5873
6142
|
output.push("");
|
|
5874
|
-
output.push(
|
|
6143
|
+
output.push(chalk16.bold.cyan(`\u2501\u2501\u2501 ${spec.name} \u2501\u2501\u2501`));
|
|
5875
6144
|
output.push("");
|
|
5876
6145
|
output.push(formatFrontmatter(spec.frontmatter));
|
|
5877
6146
|
output.push("");
|
|
5878
|
-
output.push(
|
|
6147
|
+
output.push(chalk16.gray("\u2500".repeat(60)));
|
|
5879
6148
|
output.push("");
|
|
5880
6149
|
return output.join("\n");
|
|
5881
6150
|
}
|
|
@@ -5937,7 +6206,7 @@ function openCommand(specPath, options = {}) {
|
|
|
5937
6206
|
async function openSpec(specPath, options = {}) {
|
|
5938
6207
|
const cwd = process.cwd();
|
|
5939
6208
|
const config = await loadConfig(cwd);
|
|
5940
|
-
const specsDir =
|
|
6209
|
+
const specsDir = path13.join(cwd, config.specsDir);
|
|
5941
6210
|
let resolvedPath = null;
|
|
5942
6211
|
let targetFile = null;
|
|
5943
6212
|
const pathParts = specPath.split("/").filter((p) => p);
|
|
@@ -5946,9 +6215,9 @@ async function openSpec(specPath, options = {}) {
|
|
|
5946
6215
|
const filePart = pathParts[pathParts.length - 1];
|
|
5947
6216
|
resolvedPath = await resolveSpecPath(specPart, cwd, specsDir);
|
|
5948
6217
|
if (resolvedPath) {
|
|
5949
|
-
targetFile =
|
|
6218
|
+
targetFile = path13.join(resolvedPath, filePart);
|
|
5950
6219
|
try {
|
|
5951
|
-
await
|
|
6220
|
+
await fs9.access(targetFile);
|
|
5952
6221
|
} catch {
|
|
5953
6222
|
targetFile = null;
|
|
5954
6223
|
}
|
|
@@ -5980,7 +6249,7 @@ async function openSpec(specPath, options = {}) {
|
|
|
5980
6249
|
editor = "xdg-open";
|
|
5981
6250
|
}
|
|
5982
6251
|
}
|
|
5983
|
-
console.log(
|
|
6252
|
+
console.log(chalk16.gray(`Opening ${targetFile} with ${editor}...`));
|
|
5984
6253
|
const child = spawn(editor, [targetFile], {
|
|
5985
6254
|
stdio: "inherit",
|
|
5986
6255
|
shell: true
|
|
@@ -5988,21 +6257,21 @@ async function openSpec(specPath, options = {}) {
|
|
|
5988
6257
|
const guiEditors = ["open", "start", "xdg-open", "code", "atom", "subl"];
|
|
5989
6258
|
const editorCommand = editor.trim().split(" ")[0];
|
|
5990
6259
|
if (editorCommand && guiEditors.includes(editorCommand)) {
|
|
5991
|
-
return new Promise((
|
|
6260
|
+
return new Promise((resolve3, reject) => {
|
|
5992
6261
|
child.on("error", (error) => {
|
|
5993
6262
|
reject(new Error(`Error opening editor: ${error.message}`));
|
|
5994
6263
|
});
|
|
5995
6264
|
child.unref();
|
|
5996
|
-
|
|
6265
|
+
resolve3();
|
|
5997
6266
|
});
|
|
5998
6267
|
} else {
|
|
5999
|
-
return new Promise((
|
|
6268
|
+
return new Promise((resolve3, reject) => {
|
|
6000
6269
|
child.on("error", (error) => {
|
|
6001
6270
|
reject(new Error(`Error opening editor: ${error.message}`));
|
|
6002
6271
|
});
|
|
6003
6272
|
child.on("close", (code) => {
|
|
6004
6273
|
if (code === 0) {
|
|
6005
|
-
|
|
6274
|
+
resolve3();
|
|
6006
6275
|
} else {
|
|
6007
6276
|
reject(new Error(`Editor exited with code ${code}`));
|
|
6008
6277
|
}
|
|
@@ -6062,8 +6331,8 @@ function uiCommand() {
|
|
|
6062
6331
|
async function startUi(options) {
|
|
6063
6332
|
const portNum = parseInt(options.port, 10);
|
|
6064
6333
|
if (isNaN(portNum) || portNum < 1 || portNum > 65535) {
|
|
6065
|
-
console.error(
|
|
6066
|
-
console.log(
|
|
6334
|
+
console.error(chalk16.red(`\u2717 Invalid port number: ${options.port}`));
|
|
6335
|
+
console.log(chalk16.dim("Port must be between 1 and 65535"));
|
|
6067
6336
|
throw new Error(`Invalid port: ${options.port}`);
|
|
6068
6337
|
}
|
|
6069
6338
|
const cwd = process.cwd();
|
|
@@ -6077,24 +6346,24 @@ async function startUi(options) {
|
|
|
6077
6346
|
specsDir = join(cwd, config.specsDir);
|
|
6078
6347
|
}
|
|
6079
6348
|
if (!existsSync(specsDir)) {
|
|
6080
|
-
console.error(
|
|
6081
|
-
console.log(
|
|
6349
|
+
console.error(chalk16.red(`\u2717 Specs directory not found: ${specsDir}`));
|
|
6350
|
+
console.log(chalk16.dim("\nRun `lean-spec init` to initialize LeanSpec in this directory."));
|
|
6082
6351
|
throw new Error(`Specs directory not found: ${specsDir}`);
|
|
6083
6352
|
}
|
|
6084
6353
|
} else {
|
|
6085
|
-
console.log(
|
|
6354
|
+
console.log(chalk16.cyan("\u2192 Multi-project mode enabled"));
|
|
6086
6355
|
if (options.addProject) {
|
|
6087
|
-
console.log(
|
|
6356
|
+
console.log(chalk16.dim(` Adding project: ${options.addProject}`));
|
|
6088
6357
|
}
|
|
6089
6358
|
if (options.discover) {
|
|
6090
|
-
console.log(
|
|
6359
|
+
console.log(chalk16.dim(` Will discover projects in: ${options.discover}`));
|
|
6091
6360
|
}
|
|
6092
6361
|
}
|
|
6093
6362
|
if (options.dev) {
|
|
6094
6363
|
const isLeanSpecMonorepo = checkIsLeanSpecMonorepo(cwd);
|
|
6095
6364
|
if (!isLeanSpecMonorepo) {
|
|
6096
|
-
console.error(
|
|
6097
|
-
console.log(
|
|
6365
|
+
console.error(chalk16.red(`\u2717 Development mode only works in the LeanSpec monorepo`));
|
|
6366
|
+
console.log(chalk16.dim("Remove --dev flag to use production mode"));
|
|
6098
6367
|
throw new Error("Not in LeanSpec monorepo");
|
|
6099
6368
|
}
|
|
6100
6369
|
const localUiDir = join(cwd, "packages/ui");
|
|
@@ -6116,15 +6385,15 @@ function checkIsLeanSpecMonorepo(cwd) {
|
|
|
6116
6385
|
}
|
|
6117
6386
|
}
|
|
6118
6387
|
async function runLocalWeb(uiDir, specsDir, specsMode, options) {
|
|
6119
|
-
console.log(
|
|
6388
|
+
console.log(chalk16.dim("\u2192 Detected LeanSpec monorepo, using local ui package\n"));
|
|
6120
6389
|
const repoRoot = resolve(uiDir, "..", "..");
|
|
6121
6390
|
const packageManager = detectPackageManager2(repoRoot);
|
|
6122
6391
|
if (options.dryRun) {
|
|
6123
|
-
console.log(
|
|
6124
|
-
console.log(
|
|
6125
|
-
console.log(
|
|
6392
|
+
console.log(chalk16.cyan("Would run:"));
|
|
6393
|
+
console.log(chalk16.dim(` cd ${uiDir}`));
|
|
6394
|
+
console.log(chalk16.dim(` SPECS_MODE=${specsMode} SPECS_DIR=${specsDir} PORT=${options.port} ${packageManager} run dev`));
|
|
6126
6395
|
if (options.open) {
|
|
6127
|
-
console.log(
|
|
6396
|
+
console.log(chalk16.dim(` open http://localhost:${options.port}`));
|
|
6128
6397
|
}
|
|
6129
6398
|
return;
|
|
6130
6399
|
}
|
|
@@ -6143,11 +6412,11 @@ async function runLocalWeb(uiDir, specsDir, specsMode, options) {
|
|
|
6143
6412
|
});
|
|
6144
6413
|
const readyTimeout = setTimeout(async () => {
|
|
6145
6414
|
spinner.succeed("Web UI running");
|
|
6146
|
-
console.log(
|
|
6415
|
+
console.log(chalk16.green(`
|
|
6147
6416
|
\u2728 LeanSpec UI: http://localhost:${options.port}
|
|
6148
6417
|
`));
|
|
6149
6418
|
if (options.multiProject) {
|
|
6150
|
-
console.log(
|
|
6419
|
+
console.log(chalk16.cyan("Multi-project mode is active"));
|
|
6151
6420
|
if (options.addProject) {
|
|
6152
6421
|
try {
|
|
6153
6422
|
const res = await fetch(`http://localhost:${options.port}/api/projects`, {
|
|
@@ -6156,16 +6425,16 @@ async function runLocalWeb(uiDir, specsDir, specsMode, options) {
|
|
|
6156
6425
|
body: JSON.stringify({ path: options.addProject })
|
|
6157
6426
|
});
|
|
6158
6427
|
if (res.ok) {
|
|
6159
|
-
console.log(
|
|
6428
|
+
console.log(chalk16.green(` \u2713 Added project: ${options.addProject}`));
|
|
6160
6429
|
} else {
|
|
6161
|
-
console.error(
|
|
6430
|
+
console.error(chalk16.red(` \u2717 Failed to add project: ${options.addProject}`));
|
|
6162
6431
|
}
|
|
6163
6432
|
} catch (e) {
|
|
6164
|
-
console.error(
|
|
6433
|
+
console.error(chalk16.red(` \u2717 Failed to connect to server for adding project`));
|
|
6165
6434
|
}
|
|
6166
6435
|
}
|
|
6167
6436
|
if (options.discover) {
|
|
6168
|
-
console.log(
|
|
6437
|
+
console.log(chalk16.dim(` Discovering projects in: ${options.discover}...`));
|
|
6169
6438
|
try {
|
|
6170
6439
|
const res = await fetch(`http://localhost:${options.port}/api/local-projects/discover`, {
|
|
6171
6440
|
method: "POST",
|
|
@@ -6175,7 +6444,7 @@ async function runLocalWeb(uiDir, specsDir, specsMode, options) {
|
|
|
6175
6444
|
if (res.ok) {
|
|
6176
6445
|
const data = await res.json();
|
|
6177
6446
|
const discovered = data.discovered || [];
|
|
6178
|
-
console.log(
|
|
6447
|
+
console.log(chalk16.green(` \u2713 Found ${discovered.length} projects`));
|
|
6179
6448
|
for (const p of discovered) {
|
|
6180
6449
|
const addRes = await fetch(`http://localhost:${options.port}/api/projects`, {
|
|
6181
6450
|
method: "POST",
|
|
@@ -6183,27 +6452,27 @@ async function runLocalWeb(uiDir, specsDir, specsMode, options) {
|
|
|
6183
6452
|
body: JSON.stringify({ path: p.path })
|
|
6184
6453
|
});
|
|
6185
6454
|
if (addRes.ok) {
|
|
6186
|
-
console.log(
|
|
6455
|
+
console.log(chalk16.dim(` + Added: ${p.name} (${p.path})`));
|
|
6187
6456
|
}
|
|
6188
6457
|
}
|
|
6189
6458
|
} else {
|
|
6190
|
-
console.error(
|
|
6459
|
+
console.error(chalk16.red(` \u2717 Failed to discover projects`));
|
|
6191
6460
|
}
|
|
6192
6461
|
} catch (e) {
|
|
6193
|
-
console.error(
|
|
6462
|
+
console.error(chalk16.red(` \u2717 Failed to connect to server for discovery`));
|
|
6194
6463
|
}
|
|
6195
6464
|
}
|
|
6196
6465
|
}
|
|
6197
|
-
console.log(
|
|
6466
|
+
console.log(chalk16.dim("\nPress Ctrl+C to stop\n"));
|
|
6198
6467
|
if (options.open) {
|
|
6199
6468
|
try {
|
|
6200
6469
|
const openModule = await import('open');
|
|
6201
6470
|
const open = openModule.default;
|
|
6202
6471
|
await open(`http://localhost:${options.port}`);
|
|
6203
6472
|
} catch (error) {
|
|
6204
|
-
console.log(
|
|
6205
|
-
console.log(
|
|
6206
|
-
console.error(
|
|
6473
|
+
console.log(chalk16.yellow("\u26A0 Could not open browser automatically"));
|
|
6474
|
+
console.log(chalk16.dim("Please visit the URL above manually\n"));
|
|
6475
|
+
console.error(chalk16.dim(`Debug: ${error instanceof Error ? error.message : String(error)}`));
|
|
6207
6476
|
}
|
|
6208
6477
|
}
|
|
6209
6478
|
}, 3e3);
|
|
@@ -6220,7 +6489,7 @@ async function runLocalWeb(uiDir, specsDir, specsMode, options) {
|
|
|
6220
6489
|
}
|
|
6221
6490
|
} catch (err) {
|
|
6222
6491
|
}
|
|
6223
|
-
console.log(
|
|
6492
|
+
console.log(chalk16.dim("\n\u2713 Web UI stopped"));
|
|
6224
6493
|
process.exit(0);
|
|
6225
6494
|
};
|
|
6226
6495
|
process.once("SIGINT", () => shutdown("SIGINT"));
|
|
@@ -6234,7 +6503,7 @@ async function runLocalWeb(uiDir, specsDir, specsMode, options) {
|
|
|
6234
6503
|
spinner.stop();
|
|
6235
6504
|
if (code !== 0 && code !== null) {
|
|
6236
6505
|
spinner.fail("Web UI failed to start");
|
|
6237
|
-
console.error(
|
|
6506
|
+
console.error(chalk16.red(`
|
|
6238
6507
|
Process exited with code ${code}`));
|
|
6239
6508
|
process.exit(code);
|
|
6240
6509
|
}
|
|
@@ -6242,12 +6511,12 @@ Process exited with code ${code}`));
|
|
|
6242
6511
|
});
|
|
6243
6512
|
}
|
|
6244
6513
|
async function runPublishedUI(cwd, specsDir, specsMode, options) {
|
|
6245
|
-
console.log(
|
|
6514
|
+
console.log(chalk16.dim("\u2192 Using published @leanspec/ui package\n"));
|
|
6246
6515
|
const packageManager = detectPackageManager2(cwd);
|
|
6247
6516
|
const { command, args, preview } = buildUiRunner(packageManager, specsDir, specsMode, options.port, options.open, options.multiProject);
|
|
6248
6517
|
if (options.dryRun) {
|
|
6249
|
-
console.log(
|
|
6250
|
-
console.log(
|
|
6518
|
+
console.log(chalk16.cyan("Would run:"));
|
|
6519
|
+
console.log(chalk16.dim(` ${preview}`));
|
|
6251
6520
|
return;
|
|
6252
6521
|
}
|
|
6253
6522
|
const child = spawn(command, args, {
|
|
@@ -6266,7 +6535,7 @@ async function runPublishedUI(cwd, specsDir, specsMode, options) {
|
|
|
6266
6535
|
}
|
|
6267
6536
|
} catch (err) {
|
|
6268
6537
|
}
|
|
6269
|
-
console.log(
|
|
6538
|
+
console.log(chalk16.dim("\n\u2713 Web UI stopped"));
|
|
6270
6539
|
process.exit(0);
|
|
6271
6540
|
};
|
|
6272
6541
|
process.once("SIGINT", () => shutdownPublished("SIGINT"));
|
|
@@ -6280,14 +6549,14 @@ async function runPublishedUI(cwd, specsDir, specsMode, options) {
|
|
|
6280
6549
|
process.exit(0);
|
|
6281
6550
|
return;
|
|
6282
6551
|
}
|
|
6283
|
-
console.error(
|
|
6552
|
+
console.error(chalk16.red(`
|
|
6284
6553
|
@leanspec/ui exited with code ${code}`));
|
|
6285
|
-
console.log(
|
|
6554
|
+
console.log(chalk16.dim("Make sure npm can download @leanspec/ui (https://www.npmjs.com/package/@leanspec/ui)."));
|
|
6286
6555
|
process.exit(code);
|
|
6287
6556
|
});
|
|
6288
6557
|
child.on("error", (error) => {
|
|
6289
|
-
console.error(
|
|
6290
|
-
console.log(
|
|
6558
|
+
console.error(chalk16.red(`Failed to launch @leanspec/ui: ${error instanceof Error ? error.message : String(error)}`));
|
|
6559
|
+
console.log(chalk16.dim("You can also run it manually with `npx @leanspec/ui --specs <dir>`"));
|
|
6291
6560
|
process.exit(1);
|
|
6292
6561
|
});
|
|
6293
6562
|
}
|
|
@@ -6590,7 +6859,7 @@ function createTool() {
|
|
|
6590
6859
|
async function getDepsData(specPath, mode = "complete") {
|
|
6591
6860
|
const config = await loadConfig();
|
|
6592
6861
|
const cwd = process.cwd();
|
|
6593
|
-
const specsDir =
|
|
6862
|
+
const specsDir = path13.join(cwd, config.specsDir);
|
|
6594
6863
|
const resolvedPath = await resolveSpecPath(specPath, cwd, specsDir);
|
|
6595
6864
|
if (!resolvedPath) {
|
|
6596
6865
|
throw new Error(`Spec not found: ${specPath}`);
|
|
@@ -7139,7 +7408,7 @@ function tokensTool() {
|
|
|
7139
7408
|
try {
|
|
7140
7409
|
const config = await loadConfig();
|
|
7141
7410
|
const cwd = process.cwd();
|
|
7142
|
-
const specsDir =
|
|
7411
|
+
const specsDir = path13.join(cwd, config.specsDir);
|
|
7143
7412
|
const resolvedPath = await resolveSpecPath(input.specPath, cwd, specsDir);
|
|
7144
7413
|
if (!resolvedPath) {
|
|
7145
7414
|
return {
|
|
@@ -7150,7 +7419,7 @@ function tokensTool() {
|
|
|
7150
7419
|
isError: true
|
|
7151
7420
|
};
|
|
7152
7421
|
}
|
|
7153
|
-
const specName =
|
|
7422
|
+
const specName = path13.basename(resolvedPath);
|
|
7154
7423
|
const result = await counter.countSpec(resolvedPath, {
|
|
7155
7424
|
detailed: input.detailed,
|
|
7156
7425
|
includeSubSpecs: input.includeSubSpecs
|
|
@@ -7941,5 +8210,5 @@ if (import.meta.url === `file://${process.argv[1]}`) {
|
|
|
7941
8210
|
}
|
|
7942
8211
|
|
|
7943
8212
|
export { agentCommand, analyzeCommand, archiveCommand, boardCommand, checkCommand, compactCommand, createCommand, createMcpServer, depsCommand, examplesCommand, filesCommand, ganttCommand, initCommand, linkCommand, listCommand, mcpCommand, migrateCommand, openCommand, searchCommand, splitCommand, statsCommand, templatesCommand, timelineCommand, tokensCommand, uiCommand, unlinkCommand, updateCommand, viewCommand };
|
|
7944
|
-
//# sourceMappingURL=chunk-
|
|
7945
|
-
//# sourceMappingURL=chunk-
|
|
8213
|
+
//# sourceMappingURL=chunk-KTNU4LUR.js.map
|
|
8214
|
+
//# sourceMappingURL=chunk-KTNU4LUR.js.map
|