lean-spec 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -39,7 +39,7 @@ var DEFAULT_CONFIG = {
39
39
  }
40
40
  };
41
41
  async function loadConfig(cwd = process.cwd()) {
42
- const configPath = path.join(cwd, ".lspec", "config.json");
42
+ const configPath = path.join(cwd, ".lean-spec", "config.json");
43
43
  try {
44
44
  const content = await fs.readFile(configPath, "utf-8");
45
45
  const userConfig = JSON.parse(content);
@@ -51,7 +51,7 @@ async function loadConfig(cwd = process.cwd()) {
51
51
  }
52
52
  }
53
53
  async function saveConfig(config, cwd = process.cwd()) {
54
- const configDir = path.join(cwd, ".lspec");
54
+ const configDir = path.join(cwd, ".lean-spec");
55
55
  const configPath = path.join(configDir, "config.json");
56
56
  await fs.mkdir(configDir, { recursive: true });
57
57
  await fs.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
@@ -141,13 +141,13 @@ async function loadSubFiles(specDir, options = {}) {
141
141
  if (entry.name === "README.md") continue;
142
142
  if (entry.isDirectory()) continue;
143
143
  const filePath = path2.join(specDir, entry.name);
144
- const stat4 = await fs2.stat(filePath);
144
+ const stat5 = await fs2.stat(filePath);
145
145
  const ext = path2.extname(entry.name).toLowerCase();
146
146
  const isDocument = ext === ".md";
147
147
  const subFile = {
148
148
  name: entry.name,
149
149
  path: filePath,
150
- size: stat4.size,
150
+ size: stat5.size,
151
151
  type: isDocument ? "document" : "asset"
152
152
  };
153
153
  if (isDocument && options.includeContent) {
@@ -616,14 +616,14 @@ async function checkSpecs(options = {}) {
616
616
  console.log("");
617
617
  }
618
618
  console.log(chalk3.cyan("Tip: Use date prefix to prevent conflicts:"));
619
- console.log(chalk3.gray(' Edit .lspec/config.json \u2192 structure.prefix: "{YYYYMMDD}-"'));
619
+ console.log(chalk3.gray(' Edit .lean-spec/config.json \u2192 structure.prefix: "{YYYYMMDD}-"'));
620
620
  console.log("");
621
621
  console.log(chalk3.cyan("Or rename folders manually to resolve."));
622
622
  console.log("");
623
623
  } else {
624
624
  console.log("");
625
625
  console.log(chalk3.yellow(`\u26A0\uFE0F Conflict warning: ${conflicts.length} sequence conflict(s) detected`));
626
- console.log(chalk3.gray("Run: lspec check"));
626
+ console.log(chalk3.gray("Run: lean-spec check"));
627
627
  console.log("");
628
628
  }
629
629
  }
@@ -677,7 +677,7 @@ async function createSpec(name, options = {}) {
677
677
  }
678
678
  }
679
679
  await fs5.mkdir(specDir, { recursive: true });
680
- const templatesDir = path6.join(cwd, ".lspec", "templates");
680
+ const templatesDir = path6.join(cwd, ".lean-spec", "templates");
681
681
  let templateName;
682
682
  if (options.template) {
683
683
  if (config.templates?.[options.template]) {
@@ -734,7 +734,7 @@ ${options.description}`
734
734
  );
735
735
  }
736
736
  } catch (error) {
737
- throw new Error(`Template not found: ${templatePath}. Run: lspec init`);
737
+ throw new Error(`Template not found: ${templatePath}. Run: lean-spec init`);
738
738
  }
739
739
  await fs5.writeFile(specFile, content, "utf-8");
740
740
  console.log(chalk4.green(`\u2713 Created: ${sanitizeUserInput(specDir)}/`));
@@ -755,6 +755,10 @@ async function archiveSpec(specPath) {
755
755
  if (!resolvedPath) {
756
756
  throw new Error(`Spec not found: ${sanitizeUserInput(specPath)}`);
757
757
  }
758
+ const specFile = await getSpecFile(resolvedPath, config.structure.defaultFile);
759
+ if (specFile) {
760
+ await updateFrontmatter(specFile, { status: "archived" });
761
+ }
758
762
  const archiveDir = path7.join(specsDir, "archived");
759
763
  await fs6.mkdir(archiveDir, { recursive: true });
760
764
  const specName = path7.basename(resolvedPath);
@@ -870,7 +874,7 @@ async function listSpecs(options = {}) {
870
874
  await fs7.access(specsDir);
871
875
  } catch {
872
876
  console.log("");
873
- console.log("No specs directory found. Initialize with: lspec init");
877
+ console.log("No specs directory found. Initialize with: lean-spec init");
874
878
  console.log("");
875
879
  return;
876
880
  }
@@ -1351,7 +1355,7 @@ import * as path11 from "path";
1351
1355
  import chalk9 from "chalk";
1352
1356
  async function listTemplates(cwd = process.cwd()) {
1353
1357
  const config = await loadConfig(cwd);
1354
- const templatesDir = path11.join(cwd, ".lspec", "templates");
1358
+ const templatesDir = path11.join(cwd, ".lean-spec", "templates");
1355
1359
  console.log("");
1356
1360
  console.log(chalk9.green("=== Project Templates ==="));
1357
1361
  console.log("");
@@ -1359,7 +1363,7 @@ async function listTemplates(cwd = process.cwd()) {
1359
1363
  await fs8.access(templatesDir);
1360
1364
  } catch {
1361
1365
  console.log(chalk9.yellow("No templates directory found."));
1362
- console.log(chalk9.gray("Run: lspec init"));
1366
+ console.log(chalk9.gray("Run: lean-spec init"));
1363
1367
  console.log("");
1364
1368
  return;
1365
1369
  }
@@ -1382,12 +1386,12 @@ async function listTemplates(cwd = process.cwd()) {
1382
1386
  console.log(chalk9.cyan("Available files:"));
1383
1387
  for (const file of templateFiles) {
1384
1388
  const filePath = path11.join(templatesDir, file);
1385
- const stat4 = await fs8.stat(filePath);
1386
- const sizeKB = (stat4.size / 1024).toFixed(1);
1389
+ const stat5 = await fs8.stat(filePath);
1390
+ const sizeKB = (stat5.size / 1024).toFixed(1);
1387
1391
  console.log(` ${file} (${sizeKB} KB)`);
1388
1392
  }
1389
1393
  console.log("");
1390
- console.log(chalk9.gray("Use templates with: lspec create <name> --template=<template-name>"));
1394
+ console.log(chalk9.gray("Use templates with: lean-spec create <name> --template=<template-name>"));
1391
1395
  console.log("");
1392
1396
  }
1393
1397
  async function showTemplate(templateName, cwd = process.cwd()) {
@@ -1397,7 +1401,7 @@ async function showTemplate(templateName, cwd = process.cwd()) {
1397
1401
  console.error(chalk9.gray(`Available: ${Object.keys(config.templates || {}).join(", ")}`));
1398
1402
  process.exit(1);
1399
1403
  }
1400
- const templatesDir = path11.join(cwd, ".lspec", "templates");
1404
+ const templatesDir = path11.join(cwd, ".lean-spec", "templates");
1401
1405
  const templateFile = config.templates[templateName];
1402
1406
  const templatePath = path11.join(templatesDir, templateFile);
1403
1407
  try {
@@ -1415,7 +1419,7 @@ async function showTemplate(templateName, cwd = process.cwd()) {
1415
1419
  }
1416
1420
  async function addTemplate(name, file, cwd = process.cwd()) {
1417
1421
  const config = await loadConfig(cwd);
1418
- const templatesDir = path11.join(cwd, ".lspec", "templates");
1422
+ const templatesDir = path11.join(cwd, ".lean-spec", "templates");
1419
1423
  const templatePath = path11.join(templatesDir, file);
1420
1424
  try {
1421
1425
  await fs8.access(templatePath);
@@ -1423,7 +1427,7 @@ async function addTemplate(name, file, cwd = process.cwd()) {
1423
1427
  console.error(chalk9.red(`Template file not found: ${file}`));
1424
1428
  console.error(chalk9.gray(`Expected at: ${templatePath}`));
1425
1429
  console.error(
1426
- chalk9.yellow("Create the file first or use: lspec templates copy <source> <target>")
1430
+ chalk9.yellow("Create the file first or use: lean-spec templates copy <source> <target>")
1427
1431
  );
1428
1432
  process.exit(1);
1429
1433
  }
@@ -1436,7 +1440,7 @@ async function addTemplate(name, file, cwd = process.cwd()) {
1436
1440
  config.templates[name] = file;
1437
1441
  await saveConfig(config, cwd);
1438
1442
  console.log(chalk9.green(`\u2713 Added template: ${name} \u2192 ${file}`));
1439
- console.log(chalk9.gray(` Use with: lspec create <spec-name> --template=${name}`));
1443
+ console.log(chalk9.gray(` Use with: lean-spec create <spec-name> --template=${name}`));
1440
1444
  }
1441
1445
  async function removeTemplate(name, cwd = process.cwd()) {
1442
1446
  const config = await loadConfig(cwd);
@@ -1453,11 +1457,11 @@ async function removeTemplate(name, cwd = process.cwd()) {
1453
1457
  delete config.templates[name];
1454
1458
  await saveConfig(config, cwd);
1455
1459
  console.log(chalk9.green(`\u2713 Removed template: ${name}`));
1456
- console.log(chalk9.gray(` Note: Template file ${file} still exists in .lspec/templates/`));
1460
+ console.log(chalk9.gray(` Note: Template file ${file} still exists in .lean-spec/templates/`));
1457
1461
  }
1458
1462
  async function copyTemplate(source, target, cwd = process.cwd()) {
1459
1463
  const config = await loadConfig(cwd);
1460
- const templatesDir = path11.join(cwd, ".lspec", "templates");
1464
+ const templatesDir = path11.join(cwd, ".lean-spec", "templates");
1461
1465
  let sourceFile;
1462
1466
  if (config.templates?.[source]) {
1463
1467
  sourceFile = config.templates[source];
@@ -1484,7 +1488,7 @@ async function copyTemplate(source, target, cwd = process.cwd()) {
1484
1488
  await saveConfig(config, cwd);
1485
1489
  console.log(chalk9.green(`\u2713 Registered template: ${templateName}`));
1486
1490
  console.log(chalk9.gray(` Edit: ${targetPath}`));
1487
- console.log(chalk9.gray(` Use with: lspec create <spec-name> --template=${templateName}`));
1491
+ console.log(chalk9.gray(` Use with: lean-spec create <spec-name> --template=${templateName}`));
1488
1492
  }
1489
1493
 
1490
1494
  // src/commands/init.ts
@@ -1523,7 +1527,56 @@ async function handleExistingFiles(action, existingFiles, templateDir, cwd, vari
1523
1527
  } catch {
1524
1528
  continue;
1525
1529
  }
1526
- if (action === "merge" && file === "AGENTS.md") {
1530
+ if (action === "merge-ai" && file === "AGENTS.md") {
1531
+ const existing = await fs9.readFile(filePath, "utf-8");
1532
+ let template = await fs9.readFile(templateFilePath, "utf-8");
1533
+ for (const [key, value] of Object.entries(variables)) {
1534
+ template = template.replace(new RegExp(`\\{${key}\\}`, "g"), value);
1535
+ }
1536
+ const promptPath = path12.join(cwd, ".lean-spec", "MERGE-AGENTS-PROMPT.md");
1537
+ const aiPrompt = `# AI Prompt: Consolidate AGENTS.md
1538
+
1539
+ ## Task
1540
+ Consolidate the existing AGENTS.md with LeanSpec instructions into a single, coherent document.
1541
+
1542
+ ## Instructions
1543
+ 1. Read both documents below
1544
+ 2. Merge them intelligently:
1545
+ - Preserve ALL existing project-specific information (workflows, SOPs, architecture, conventions)
1546
+ - Integrate LeanSpec sections where they fit naturally
1547
+ - Remove redundancy and ensure coherent flow
1548
+ - Keep the tone and style consistent
1549
+ 3. Replace the existing AGENTS.md with the consolidated version
1550
+
1551
+ ## Existing AGENTS.md
1552
+ \`\`\`markdown
1553
+ ${existing}
1554
+ \`\`\`
1555
+
1556
+ ## LeanSpec Instructions to Integrate
1557
+ \`\`\`markdown
1558
+ ${template}
1559
+ \`\`\`
1560
+
1561
+ ## Output
1562
+ Create a single consolidated AGENTS.md that:
1563
+ - Keeps all existing project context and workflows
1564
+ - Adds LeanSpec commands and principles where appropriate
1565
+ - Maintains clear structure and readability
1566
+ - Removes any duplicate or conflicting guidance
1567
+ `;
1568
+ await fs9.mkdir(path12.dirname(promptPath), { recursive: true });
1569
+ await fs9.writeFile(promptPath, aiPrompt, "utf-8");
1570
+ console.log(chalk10.green(`\u2713 Created AI consolidation prompt`));
1571
+ console.log(chalk10.cyan(` \u2192 ${promptPath}`));
1572
+ console.log("");
1573
+ console.log(chalk10.yellow("\u{1F4DD} Next steps:"));
1574
+ console.log(chalk10.gray(" 1. Open .lean-spec/MERGE-AGENTS-PROMPT.md"));
1575
+ console.log(chalk10.gray(" 2. Send it to your AI coding assistant (GitHub Copilot, Cursor, etc.)"));
1576
+ console.log(chalk10.gray(" 3. Let AI create the consolidated AGENTS.md"));
1577
+ console.log(chalk10.gray(" 4. Review and commit the result"));
1578
+ console.log("");
1579
+ } else if (action === "merge-append" && file === "AGENTS.md") {
1527
1580
  const existing = await fs9.readFile(filePath, "utf-8");
1528
1581
  let template = await fs9.readFile(templateFilePath, "utf-8");
1529
1582
  for (const [key, value] of Object.entries(variables)) {
@@ -1537,8 +1590,9 @@ async function handleExistingFiles(action, existingFiles, templateDir, cwd, vari
1537
1590
 
1538
1591
  ${template.split("\n").slice(1).join("\n")}`;
1539
1592
  await fs9.writeFile(filePath, merged, "utf-8");
1540
- console.log(chalk10.green(`\u2713 Merged LeanSpec section into ${file}`));
1541
- } else if (action === "backup") {
1593
+ console.log(chalk10.green(`\u2713 Appended LeanSpec section to ${file}`));
1594
+ console.log(chalk10.yellow(" \u26A0 Note: May be verbose. Consider consolidating later."));
1595
+ } else if (action === "overwrite") {
1542
1596
  const backupPath = `${filePath}.backup`;
1543
1597
  await fs9.rename(filePath, backupPath);
1544
1598
  console.log(chalk10.yellow(`\u2713 Backed up ${file} \u2192 ${file}.backup`));
@@ -1548,6 +1602,7 @@ ${template.split("\n").slice(1).join("\n")}`;
1548
1602
  }
1549
1603
  await fs9.writeFile(filePath, content, "utf-8");
1550
1604
  console.log(chalk10.green(`\u2713 Created new ${file}`));
1605
+ console.log(chalk10.gray(` \u{1F4A1} Your original content is preserved in ${file}.backup`));
1551
1606
  }
1552
1607
  }
1553
1608
  }
@@ -1594,9 +1649,9 @@ var TEMPLATES_DIR = path13.join(__dirname2, "..", "templates");
1594
1649
  async function initProject() {
1595
1650
  const cwd = process.cwd();
1596
1651
  try {
1597
- await fs10.access(path13.join(cwd, ".lspec", "config.json"));
1598
- console.log(chalk11.yellow("LeanSpec already initialized in this directory."));
1599
- console.log(chalk11.gray("To reinitialize, delete .lspec/ directory first."));
1652
+ await fs10.access(path13.join(cwd, ".lean-spec", "config.json"));
1653
+ console.log(chalk11.yellow("\u26A0 LeanSpec already initialized in this directory."));
1654
+ console.log(chalk11.gray("To reinitialize, delete .lean-spec/ directory first."));
1600
1655
  return;
1601
1656
  } catch {
1602
1657
  }
@@ -1615,12 +1670,13 @@ async function initProject() {
1615
1670
  name: "Choose template",
1616
1671
  value: "template",
1617
1672
  description: "Pick from: minimal, standard, enterprise"
1618
- },
1619
- {
1620
- name: "Customize everything",
1621
- value: "custom",
1622
- description: "Full control over structure and settings"
1623
1673
  }
1674
+ // TODO: Re-enable when custom setup mode is implemented
1675
+ // {
1676
+ // name: 'Customize everything',
1677
+ // value: 'custom',
1678
+ // description: 'Full control over structure and settings',
1679
+ // },
1624
1680
  ]
1625
1681
  });
1626
1682
  let templateName = "standard";
@@ -1637,8 +1693,6 @@ async function initProject() {
1637
1693
  }
1638
1694
  ]
1639
1695
  });
1640
- } else if (setupMode === "custom") {
1641
- console.log(chalk11.yellow("Full customization coming soon. Using standard for now."));
1642
1696
  }
1643
1697
  const templateDir = path13.join(TEMPLATES_DIR, templateName);
1644
1698
  const templateConfigPath = path13.join(templateDir, "config.json");
@@ -1650,31 +1704,34 @@ async function initProject() {
1650
1704
  console.error(chalk11.red(`Error: Template not found: ${templateName}`));
1651
1705
  process.exit(1);
1652
1706
  }
1653
- const patternChoice = await select({
1654
- message: "Select folder pattern:",
1655
- choices: [
1656
- {
1657
- name: "Simple: 001-my-spec/",
1658
- value: "simple",
1659
- description: "Global sequential numbering (recommended)"
1660
- },
1661
- {
1662
- name: "Date-grouped: 20251105/001-my-spec/",
1663
- value: "date-grouped",
1664
- description: "Group specs by creation date (good for teams)"
1665
- },
1666
- {
1667
- name: "Flat with date: 20251105-001-my-spec/",
1668
- value: "date-prefix",
1669
- description: "Date prefix with global numbering"
1670
- },
1671
- {
1672
- name: "Custom pattern",
1673
- value: "custom",
1674
- description: "Enter your own pattern"
1675
- }
1676
- ]
1677
- });
1707
+ let patternChoice = "simple";
1708
+ if (setupMode !== "quick") {
1709
+ patternChoice = await select({
1710
+ message: "Select folder pattern:",
1711
+ choices: [
1712
+ {
1713
+ name: "Simple: 001-my-spec/",
1714
+ value: "simple",
1715
+ description: "Global sequential numbering (recommended)"
1716
+ },
1717
+ {
1718
+ name: "Date-grouped: 20251105/001-my-spec/",
1719
+ value: "date-grouped",
1720
+ description: "Group specs by creation date (good for teams)"
1721
+ },
1722
+ {
1723
+ name: "Flat with date: 20251105-001-my-spec/",
1724
+ value: "date-prefix",
1725
+ description: "Date prefix with global numbering"
1726
+ },
1727
+ {
1728
+ name: "Custom pattern",
1729
+ value: "custom",
1730
+ description: "Enter your own pattern"
1731
+ }
1732
+ ]
1733
+ });
1734
+ }
1678
1735
  if (patternChoice === "simple") {
1679
1736
  templateConfig.structure.pattern = "flat";
1680
1737
  templateConfig.structure.prefix = "";
@@ -1688,13 +1745,13 @@ async function initProject() {
1688
1745
  } else if (patternChoice === "custom") {
1689
1746
  console.log("");
1690
1747
  console.log(chalk11.yellow("\u26A0 Custom pattern input is not yet implemented."));
1691
- console.log(chalk11.gray(" You can manually edit .lspec/config.json after initialization."));
1748
+ console.log(chalk11.gray(" You can manually edit .lean-spec/config.json after initialization."));
1692
1749
  console.log(chalk11.gray(" Using simple pattern for now."));
1693
1750
  console.log("");
1694
1751
  templateConfig.structure.pattern = "flat";
1695
1752
  templateConfig.structure.prefix = "";
1696
1753
  }
1697
- const templatesDir = path13.join(cwd, ".lspec", "templates");
1754
+ const templatesDir = path13.join(cwd, ".lean-spec", "templates");
1698
1755
  try {
1699
1756
  await fs10.mkdir(templatesDir, { recursive: true });
1700
1757
  } catch (error) {
@@ -1705,7 +1762,7 @@ async function initProject() {
1705
1762
  const targetSpecPath = path13.join(templatesDir, "spec-template.md");
1706
1763
  try {
1707
1764
  await fs10.copyFile(templateSpecPath, targetSpecPath);
1708
- console.log(chalk11.green("\u2713 Created .lspec/templates/spec-template.md"));
1765
+ console.log(chalk11.green("\u2713 Created .lean-spec/templates/spec-template.md"));
1709
1766
  } catch (error) {
1710
1767
  console.error(chalk11.red("Error copying template:"), error);
1711
1768
  process.exit(1);
@@ -1715,29 +1772,34 @@ async function initProject() {
1715
1772
  default: "spec-template.md"
1716
1773
  };
1717
1774
  await saveConfig(templateConfig, cwd);
1718
- console.log(chalk11.green("\u2713 Created .lspec/config.json"));
1775
+ console.log(chalk11.green("\u2713 Created .lean-spec/config.json"));
1719
1776
  const existingFiles = await detectExistingSystemPrompts(cwd);
1720
1777
  let skipFiles = [];
1721
1778
  if (existingFiles.length > 0) {
1722
1779
  console.log("");
1723
1780
  console.log(chalk11.yellow(`Found existing: ${existingFiles.join(", ")}`));
1724
1781
  const action = await select({
1725
- message: "How would you like to proceed?",
1782
+ message: "How would you like to handle existing AGENTS.md?",
1726
1783
  choices: [
1727
1784
  {
1728
- name: "Merge - Add LeanSpec section to existing files",
1729
- value: "merge",
1730
- description: "Appends LeanSpec guidance to your existing AGENTS.md"
1785
+ name: "AI-Assisted Merge (recommended)",
1786
+ value: "merge-ai",
1787
+ description: "Creates prompt for AI to intelligently consolidate both files"
1788
+ },
1789
+ {
1790
+ name: "Simple Append",
1791
+ value: "merge-append",
1792
+ description: "Quickly appends LeanSpec section (may be verbose)"
1731
1793
  },
1732
1794
  {
1733
- name: "Backup - Save existing and create new",
1734
- value: "backup",
1735
- description: "Renames existing files to .backup and creates fresh ones"
1795
+ name: "Replace with LeanSpec",
1796
+ value: "overwrite",
1797
+ description: "Backs up existing, creates fresh AGENTS.md from template"
1736
1798
  },
1737
1799
  {
1738
- name: "Skip - Keep existing files as-is",
1800
+ name: "Keep Existing Only",
1739
1801
  value: "skip",
1740
- description: "Only adds .lspec config and specs/ directory"
1802
+ description: "Skips AGENTS.md, only adds .lean-spec config and specs/"
1741
1803
  }
1742
1804
  ]
1743
1805
  });
@@ -1762,7 +1824,7 @@ async function initProject() {
1762
1824
  console.log("Next steps:");
1763
1825
  console.log(chalk11.gray(" - Review and customize AGENTS.md"));
1764
1826
  console.log(chalk11.gray(" - Check out example spec in specs/"));
1765
- console.log(chalk11.gray(" - Create your first spec: lspec create my-feature"));
1827
+ console.log(chalk11.gray(" - Create your first spec: lean-spec create my-feature"));
1766
1828
  console.log("");
1767
1829
  }
1768
1830
 
@@ -2728,6 +2790,220 @@ async function validateCommand(options = {}) {
2728
2790
  return !hasErrors;
2729
2791
  }
2730
2792
 
2793
+ // src/commands/migrate.ts
2794
+ import * as fs13 from "fs/promises";
2795
+ import * as path17 from "path";
2796
+ async function migrateCommand(inputPath, options = {}) {
2797
+ const config = await loadConfig();
2798
+ try {
2799
+ const stats = await fs13.stat(inputPath);
2800
+ if (!stats.isDirectory()) {
2801
+ console.error("\x1B[31m\u274C Error:\x1B[0m Input path must be a directory");
2802
+ process.exit(1);
2803
+ }
2804
+ } catch (error) {
2805
+ console.error(`\x1B[31m\u274C Error:\x1B[0m Path not found: ${inputPath}`);
2806
+ process.exit(1);
2807
+ }
2808
+ console.log(`\x1B[36mScanning:\x1B[0m ${inputPath}
2809
+ `);
2810
+ const documents = await scanDocuments(inputPath);
2811
+ if (documents.length === 0) {
2812
+ console.error(`\x1B[31m\u274C Error:\x1B[0m No documents found in ${inputPath}`);
2813
+ console.error(" Check path and try again");
2814
+ process.exit(1);
2815
+ }
2816
+ console.log(`\x1B[32m\u2713\x1B[0m Found ${documents.length} document${documents.length === 1 ? "" : "s"}
2817
+ `);
2818
+ if (options.aiProvider) {
2819
+ await migrateWithAI(inputPath, documents, options);
2820
+ } else {
2821
+ await outputManualInstructions(inputPath, documents, config);
2822
+ }
2823
+ }
2824
+ async function scanDocuments(dirPath) {
2825
+ const documents = [];
2826
+ async function scanRecursive(currentPath) {
2827
+ const entries = await fs13.readdir(currentPath, { withFileTypes: true });
2828
+ for (const entry of entries) {
2829
+ const fullPath = path17.join(currentPath, entry.name);
2830
+ if (entry.isDirectory()) {
2831
+ if (!entry.name.startsWith(".") && entry.name !== "node_modules") {
2832
+ await scanRecursive(fullPath);
2833
+ }
2834
+ } else if (entry.isFile()) {
2835
+ if (entry.name.endsWith(".md") || entry.name.endsWith(".markdown")) {
2836
+ const stats = await fs13.stat(fullPath);
2837
+ documents.push({
2838
+ path: fullPath,
2839
+ name: entry.name,
2840
+ size: stats.size
2841
+ });
2842
+ }
2843
+ }
2844
+ }
2845
+ }
2846
+ await scanRecursive(dirPath);
2847
+ return documents;
2848
+ }
2849
+ async function outputManualInstructions(inputPath, documents, config) {
2850
+ const specsDir = config.specsDir || "specs";
2851
+ console.log("\u2550".repeat(70));
2852
+ console.log("\x1B[1m\x1B[36m\u{1F4CB} LeanSpec Migration Instructions\x1B[0m");
2853
+ console.log("\u2550".repeat(70));
2854
+ console.log();
2855
+ console.log("\x1B[1mSource Location:\x1B[0m");
2856
+ console.log(` ${inputPath} (${documents.length} documents found)`);
2857
+ console.log();
2858
+ console.log("\x1B[1mMigration Prompt:\x1B[0m");
2859
+ console.log(" Copy this prompt to your AI assistant (Copilot, Claude, ChatGPT, etc.):");
2860
+ console.log();
2861
+ console.log("\u2500".repeat(70));
2862
+ console.log();
2863
+ console.log("You are helping migrate specification documents to LeanSpec format.");
2864
+ console.log();
2865
+ console.log(`\x1B[1mSource:\x1B[0m ${inputPath}`);
2866
+ console.log();
2867
+ console.log("\x1B[1mYour Task:\x1B[0m");
2868
+ console.log("1. Analyze the source documents to understand their format and structure");
2869
+ console.log("2. For each document, extract:");
2870
+ console.log(" - Title/name");
2871
+ console.log(" - Status (map to: planned, in-progress, complete, archived)");
2872
+ console.log(" - Creation date");
2873
+ console.log(" - Priority (if present)");
2874
+ console.log(" - Main content sections");
2875
+ console.log(" - Relationships to other documents");
2876
+ console.log();
2877
+ console.log("3. Migrate each document by running these commands:");
2878
+ console.log();
2879
+ console.log(" # Create spec");
2880
+ console.log(" lean-spec create <name>");
2881
+ console.log();
2882
+ console.log(" # Set metadata (NEVER edit frontmatter manually)");
2883
+ console.log(" lean-spec update <name> --status <status>");
2884
+ console.log(" lean-spec update <name> --priority <priority>");
2885
+ console.log(" lean-spec update <name> --tags <tag1,tag2>");
2886
+ console.log();
2887
+ console.log(" # Edit content with your preferred tool");
2888
+ console.log(" # Map original sections to LeanSpec structure:");
2889
+ console.log(" # - Overview: Problem statement and context");
2890
+ console.log(" # - Design: Technical approach and decisions");
2891
+ console.log(" # - Plan: Implementation steps (if applicable)");
2892
+ console.log(" # - Test: Validation criteria (if applicable)");
2893
+ console.log(" # - Notes: Additional context, trade-offs, alternatives");
2894
+ console.log();
2895
+ console.log("4. After migration, run:");
2896
+ console.log();
2897
+ console.log(" lean-spec validate # Check for issues");
2898
+ console.log(" lean-spec board # Verify migration");
2899
+ console.log();
2900
+ console.log("\x1B[1mImportant Rules:\x1B[0m");
2901
+ console.log("- Preserve decision rationale and context");
2902
+ console.log("- Map status appropriately to LeanSpec states");
2903
+ console.log("- Link related specs using `related` field (manual frontmatter edit)");
2904
+ console.log("- Follow LeanSpec first principles: clarity over completeness");
2905
+ console.log("- Keep specs under 400 lines (split if needed)");
2906
+ console.log();
2907
+ console.log("\u2500".repeat(70));
2908
+ console.log();
2909
+ console.log("\x1B[36m\u2139\x1B[0m \x1B[1mTip:\x1B[0m For AI-assisted migration, use:");
2910
+ console.log(" \x1B[90mlean-spec migrate <path> --with copilot\x1B[0m");
2911
+ console.log();
2912
+ }
2913
+ async function migrateWithAI(inputPath, documents, options) {
2914
+ const provider = options.aiProvider;
2915
+ console.log(`\x1B[36m\u{1F916} AI-Assisted Migration:\x1B[0m ${provider}
2916
+ `);
2917
+ const tool = await verifyAITool(provider);
2918
+ if (!tool.installed) {
2919
+ console.error(`\x1B[31m\u274C ${tool.name} CLI not found\x1B[0m`);
2920
+ console.error(` Install: ${tool.installCmd}`);
2921
+ console.error(" Or run without --with flag for manual instructions");
2922
+ process.exit(1);
2923
+ }
2924
+ if (!tool.compatible) {
2925
+ console.error(`\x1B[31m\u274C ${tool.name} version ${tool.version} too old\x1B[0m`);
2926
+ console.error(` Required: >=${tool.minVersion}`);
2927
+ console.error(` Update: ${tool.updateCmd}`);
2928
+ process.exit(1);
2929
+ }
2930
+ console.log(`\x1B[32m\u2713\x1B[0m ${tool.name} CLI verified (v${tool.version})
2931
+ `);
2932
+ console.log("\x1B[33m\u26A0 AI-assisted migration is not yet fully implemented\x1B[0m");
2933
+ console.log(" This feature will automatically execute migration via AI CLI tools.");
2934
+ console.log();
2935
+ console.log(" For now, use manual mode (without --with flag) to get migration instructions.");
2936
+ console.log();
2937
+ }
2938
+ async function verifyAITool(provider) {
2939
+ const tools = {
2940
+ copilot: {
2941
+ name: "GitHub Copilot CLI",
2942
+ cliCommand: "github-copilot-cli",
2943
+ installCmd: "npm install -g @githubnext/github-copilot-cli",
2944
+ updateCmd: "npm update -g @githubnext/github-copilot-cli",
2945
+ versionCmd: "github-copilot-cli --version",
2946
+ minVersion: "0.1.0"
2947
+ },
2948
+ claude: {
2949
+ name: "Claude CLI",
2950
+ cliCommand: "claude",
2951
+ installCmd: "pip install claude-cli",
2952
+ updateCmd: "pip install --upgrade claude-cli",
2953
+ versionCmd: "claude --version",
2954
+ minVersion: "1.0.0"
2955
+ },
2956
+ gemini: {
2957
+ name: "Gemini CLI",
2958
+ cliCommand: "gemini-cli",
2959
+ installCmd: "npm install -g @google/gemini-cli",
2960
+ updateCmd: "npm update -g @google/gemini-cli",
2961
+ versionCmd: "gemini-cli --version",
2962
+ minVersion: "1.0.0"
2963
+ }
2964
+ };
2965
+ const toolDef = tools[provider];
2966
+ let installed = false;
2967
+ let version;
2968
+ try {
2969
+ const { execSync: execSync3 } = await import("child_process");
2970
+ execSync3(`which ${toolDef.cliCommand}`, { stdio: "ignore" });
2971
+ installed = true;
2972
+ try {
2973
+ const versionOutput = execSync3(toolDef.versionCmd, {
2974
+ encoding: "utf-8",
2975
+ stdio: ["ignore", "pipe", "ignore"]
2976
+ });
2977
+ const versionMatch = versionOutput.match(/(\d+\.\d+\.\d+)/);
2978
+ if (versionMatch) {
2979
+ version = versionMatch[1];
2980
+ }
2981
+ } catch {
2982
+ version = "unknown";
2983
+ }
2984
+ } catch {
2985
+ installed = false;
2986
+ }
2987
+ const compatible = installed && (version === "unknown" || version !== void 0 && satisfiesVersion(version, toolDef.minVersion));
2988
+ return {
2989
+ ...toolDef,
2990
+ installed,
2991
+ version,
2992
+ compatible
2993
+ };
2994
+ }
2995
+ function satisfiesVersion(version, minVersion) {
2996
+ const vParts = version.split(".").map(Number);
2997
+ const minParts = minVersion.split(".").map(Number);
2998
+ for (let i = 0; i < 3; i++) {
2999
+ const v = vParts[i] || 0;
3000
+ const min = minParts[i] || 0;
3001
+ if (v > min) return true;
3002
+ if (v < min) return false;
3003
+ }
3004
+ return true;
3005
+ }
3006
+
2731
3007
  // src/commands/board.ts
2732
3008
  import chalk15 from "chalk";
2733
3009
 
@@ -3035,7 +3311,7 @@ function renderColumn(title, emoji, specs, expanded, colorFn) {
3035
3311
  }
3036
3312
  console.log("");
3037
3313
  } else if (!expanded && specs.length > 0) {
3038
- console.log(` ${chalk15.dim("(collapsed, use --show-complete to expand)")}`);
3314
+ console.log(` ${chalk15.dim("(collapsed, use --complete to expand)")}`);
3039
3315
  console.log("");
3040
3316
  } else {
3041
3317
  console.log(` ${chalk15.dim("(empty)")}`);
@@ -3316,9 +3592,9 @@ async function statsCommand(options) {
3316
3592
  console.log(` Throughput ${chalk16.cyan((velocityMetrics.throughput.perWeek / 7 * 7).toFixed(1))}/week ${throughputTrend}`);
3317
3593
  console.log(` WIP ${chalk16.yellow(velocityMetrics.wip.current)} specs`);
3318
3594
  console.log("");
3319
- console.log(chalk16.dim("\u{1F4A1} Use `lspec stats --full` for detailed analytics"));
3320
- console.log(chalk16.dim(" Use `lspec stats --velocity` for velocity breakdown"));
3321
- console.log(chalk16.dim(" Use `lspec stats --timeline` for activity timeline"));
3595
+ console.log(chalk16.dim("\u{1F4A1} Use `lean-spec stats --full` for detailed analytics"));
3596
+ console.log(chalk16.dim(" Use `lean-spec stats --velocity` for velocity breakdown"));
3597
+ console.log(chalk16.dim(" Use `lean-spec stats --timeline` for activity timeline"));
3322
3598
  console.log("");
3323
3599
  return;
3324
3600
  }
@@ -3665,12 +3941,12 @@ function escapeRegex(str) {
3665
3941
 
3666
3942
  // src/commands/deps.ts
3667
3943
  import chalk18 from "chalk";
3668
- import * as path17 from "path";
3944
+ import * as path18 from "path";
3669
3945
  async function depsCommand(specPath, options) {
3670
3946
  await autoCheckIfEnabled();
3671
3947
  const config = await loadConfig();
3672
3948
  const cwd = process.cwd();
3673
- const specsDir = path17.join(cwd, config.specsDir);
3949
+ const specsDir = path18.join(cwd, config.specsDir);
3674
3950
  const resolvedPath = await resolveSpecPath(specPath, cwd, specsDir);
3675
3951
  if (!resolvedPath) {
3676
3952
  throw new Error(`Spec not found: ${sanitizeUserInput(specPath)}`);
@@ -3701,26 +3977,28 @@ async function depsCommand(specPath, options) {
3701
3977
  console.log("");
3702
3978
  console.log(chalk18.green(`\u{1F4E6} Dependencies for ${chalk18.cyan(sanitizeUserInput(spec.path))}`));
3703
3979
  console.log("");
3704
- console.log(chalk18.bold("Depends On:"));
3980
+ const hasAnyRelationships = dependsOn.length > 0 || blocks.length > 0 || relatedSpecs.length > 0;
3981
+ if (!hasAnyRelationships) {
3982
+ console.log(chalk18.gray(" No dependencies or relationships"));
3983
+ console.log("");
3984
+ return;
3985
+ }
3705
3986
  if (dependsOn.length > 0) {
3987
+ console.log(chalk18.bold("Depends On:"));
3706
3988
  for (const dep of dependsOn) {
3707
3989
  const status = getStatusIndicator(dep.frontmatter.status);
3708
3990
  console.log(` \u2192 ${sanitizeUserInput(dep.path)} ${status}`);
3709
3991
  }
3710
- } else {
3711
- console.log(chalk18.gray(" (none)"));
3992
+ console.log("");
3712
3993
  }
3713
- console.log("");
3714
- console.log(chalk18.bold("Blocks:"));
3715
3994
  if (blocks.length > 0) {
3995
+ console.log(chalk18.bold("Required By:"));
3716
3996
  for (const blocked of blocks) {
3717
3997
  const status = getStatusIndicator(blocked.frontmatter.status);
3718
3998
  console.log(` \u2190 ${sanitizeUserInput(blocked.path)} ${status}`);
3719
3999
  }
3720
- } else {
3721
- console.log(chalk18.gray(" (none)"));
4000
+ console.log("");
3722
4001
  }
3723
- console.log("");
3724
4002
  if (relatedSpecs.length > 0) {
3725
4003
  console.log(chalk18.bold("Related Specs:"));
3726
4004
  for (const rel of relatedSpecs) {
@@ -4183,8 +4461,8 @@ function renderTimelineBar(spec, startDate, endDate, weeks, today) {
4183
4461
  }
4184
4462
 
4185
4463
  // src/commands/viewer.ts
4186
- import * as fs13 from "fs/promises";
4187
- import * as path18 from "path";
4464
+ import * as fs14 from "fs/promises";
4465
+ import * as path19 from "path";
4188
4466
  import chalk21 from "chalk";
4189
4467
  import { marked } from "marked";
4190
4468
  import { markedTerminal } from "marked-terminal";
@@ -4192,7 +4470,7 @@ import { spawn } from "child_process";
4192
4470
  marked.use(markedTerminal());
4193
4471
  async function readSpecContent(specPath, cwd = process.cwd()) {
4194
4472
  const config = await loadConfig(cwd);
4195
- const specsDir = path18.join(cwd, config.specsDir);
4473
+ const specsDir = path19.join(cwd, config.specsDir);
4196
4474
  let resolvedPath = null;
4197
4475
  let targetFile = null;
4198
4476
  const pathParts = specPath.split("/").filter((p) => p);
@@ -4201,9 +4479,9 @@ async function readSpecContent(specPath, cwd = process.cwd()) {
4201
4479
  const filePart = pathParts[pathParts.length - 1];
4202
4480
  resolvedPath = await resolveSpecPath(specPart, cwd, specsDir);
4203
4481
  if (resolvedPath) {
4204
- targetFile = path18.join(resolvedPath, filePart);
4482
+ targetFile = path19.join(resolvedPath, filePart);
4205
4483
  try {
4206
- await fs13.access(targetFile);
4484
+ await fs14.access(targetFile);
4207
4485
  } catch {
4208
4486
  return null;
4209
4487
  }
@@ -4222,8 +4500,8 @@ async function readSpecContent(specPath, cwd = process.cwd()) {
4222
4500
  if (!targetFile) {
4223
4501
  return null;
4224
4502
  }
4225
- const rawContent = await fs13.readFile(targetFile, "utf-8");
4226
- const fileName = path18.basename(targetFile);
4503
+ const rawContent = await fs14.readFile(targetFile, "utf-8");
4504
+ const fileName = path19.basename(targetFile);
4227
4505
  const isSubSpec = fileName !== config.structure.defaultFile;
4228
4506
  let frontmatter = null;
4229
4507
  if (!isSubSpec) {
@@ -4252,7 +4530,7 @@ async function readSpecContent(specPath, cwd = process.cwd()) {
4252
4530
  }
4253
4531
  }
4254
4532
  const content = lines.slice(contentStartIndex).join("\n").trim();
4255
- const specName = path18.basename(resolvedPath);
4533
+ const specName = path19.basename(resolvedPath);
4256
4534
  const displayName = isSubSpec ? `${specName}/${fileName}` : specName;
4257
4535
  return {
4258
4536
  frontmatter,
@@ -4319,7 +4597,7 @@ function displayFormattedSpec(spec) {
4319
4597
  async function viewCommand(specPath, options = {}) {
4320
4598
  const spec = await readSpecContent(specPath, process.cwd());
4321
4599
  if (!spec) {
4322
- throw new Error(`Spec not found: ${specPath}. Try: lspec list`);
4600
+ throw new Error(`Spec not found: ${specPath}. Try: lean-spec list`);
4323
4601
  }
4324
4602
  if (options.json) {
4325
4603
  const jsonOutput = {
@@ -4342,7 +4620,7 @@ async function viewCommand(specPath, options = {}) {
4342
4620
  async function openCommand(specPath, options = {}) {
4343
4621
  const cwd = process.cwd();
4344
4622
  const config = await loadConfig(cwd);
4345
- const specsDir = path18.join(cwd, config.specsDir);
4623
+ const specsDir = path19.join(cwd, config.specsDir);
4346
4624
  let resolvedPath = null;
4347
4625
  let targetFile = null;
4348
4626
  const pathParts = specPath.split("/").filter((p) => p);
@@ -4351,9 +4629,9 @@ async function openCommand(specPath, options = {}) {
4351
4629
  const filePart = pathParts[pathParts.length - 1];
4352
4630
  resolvedPath = await resolveSpecPath(specPart, cwd, specsDir);
4353
4631
  if (resolvedPath) {
4354
- targetFile = path18.join(resolvedPath, filePart);
4632
+ targetFile = path19.join(resolvedPath, filePart);
4355
4633
  try {
4356
- await fs13.access(targetFile);
4634
+ await fs14.access(targetFile);
4357
4635
  } catch {
4358
4636
  targetFile = null;
4359
4637
  }
@@ -4432,7 +4710,6 @@ async function mcpCommand() {
4432
4710
  }
4433
4711
 
4434
4712
  // src/mcp-server.ts
4435
- import * as path19 from "path";
4436
4713
  function formatErrorMessage(prefix, error) {
4437
4714
  const errorMsg = error instanceof Error ? error.message : String(error);
4438
4715
  return `${prefix}: ${errorMsg}`;
@@ -4496,20 +4773,22 @@ async function searchSpecsData(query, options) {
4496
4773
  return results;
4497
4774
  }
4498
4775
  async function readSpecData(specPath) {
4499
- const config = await loadConfig();
4500
4776
  const cwd = process.cwd();
4501
- const specsDir = path19.join(cwd, config.specsDir);
4502
- const resolvedPath = await resolveSpecPath(specPath, cwd, specsDir);
4503
- if (!resolvedPath) {
4504
- throw new Error(`Spec not found: ${specPath}`);
4505
- }
4506
- const specInfo = await getSpec(resolvedPath);
4507
- if (!specInfo) {
4777
+ const specContent = await readSpecContent(specPath, cwd);
4778
+ if (!specContent) {
4508
4779
  throw new Error(`Spec not found: ${specPath}`);
4509
4780
  }
4510
4781
  return {
4511
- spec: specToData(specInfo),
4512
- content: specInfo.content || ""
4782
+ spec: {
4783
+ name: specContent.name,
4784
+ path: specContent.path,
4785
+ status: specContent.frontmatter.status,
4786
+ created: String(specContent.frontmatter.created),
4787
+ priority: specContent.frontmatter.priority,
4788
+ tags: specContent.frontmatter.tags,
4789
+ assignee: specContent.frontmatter.assignee
4790
+ },
4791
+ content: specContent.content
4513
4792
  };
4514
4793
  }
4515
4794
  async function getStatsData() {
@@ -4665,7 +4944,7 @@ async function createMcpServer() {
4665
4944
  title: "View Spec",
4666
4945
  description: "Read the complete content of a specification. Use this to understand spec details, review design decisions, or check implementation status. Returns metadata and full content.",
4667
4946
  inputSchema: {
4668
- specPath: z.string().describe('The spec to view. Can be: spec name (e.g., "unified-dashboard"), sequence number (e.g., "045" or "45"), or full folder name (e.g., "045-unified-dashboard").'),
4947
+ specPath: z.string().describe('The spec to view. Can be: spec name (e.g., "unified-dashboard"), sequence number (e.g., "045" or "45"), full folder name (e.g., "045-unified-dashboard"), or sub-spec file (e.g., "045/DESIGN.md" or "unified-dashboard/TESTING.md").'),
4669
4948
  raw: z.boolean().optional().describe("Output raw markdown instead of formatted"),
4670
4949
  json: z.boolean().optional().describe("Output as JSON instead of formatted")
4671
4950
  },
@@ -5354,6 +5633,7 @@ export {
5354
5633
  initProject,
5355
5634
  filesCommand,
5356
5635
  validateCommand,
5636
+ migrateCommand,
5357
5637
  boardCommand,
5358
5638
  statsCommand,
5359
5639
  searchCommand,
@@ -5365,4 +5645,4 @@ export {
5365
5645
  createMcpServer,
5366
5646
  mcpCommand
5367
5647
  };
5368
- //# sourceMappingURL=chunk-GLXYUS7F.js.map
5648
+ //# sourceMappingURL=chunk-S6TGAL75.js.map