@@ -1695,309 +1695,6 @@ def check():
16951695 if not any (agent_results .values ()):
16961696 console .print ("[dim]Tip: Install an AI assistant for the best experience[/dim]" )
16971697
1698-
1699- @app .command ()
1700- def doctor ():
1701- """Diagnose a Specify project and report health issues."""
1702- show_banner ()
1703- console .print ("[bold]Running project diagnostics...[/bold]\n " )
1704-
1705- project_root = Path .cwd ()
1706- issues = [] # (severity, message) tuples: "error", "warning", "info"
1707-
1708- # ── 1. Project structure ──────────────────────────────────────────
1709- tracker = StepTracker ("Project Structure" )
1710-
1711- specify_dir = project_root / ".specify"
1712- tracker .add ("specify_dir" , ".specify/ directory" )
1713- if specify_dir .is_dir ():
1714- tracker .complete ("specify_dir" , "found" )
1715- else :
1716- tracker .error ("specify_dir" , "missing" )
1717- issues .append (("error" , "No .specify/ directory — run 'specify init --here' to initialize" ))
1718-
1719- specs_dir = project_root / "specs"
1720- tracker .add ("specs_dir" , "specs/ directory" )
1721- if specs_dir .is_dir ():
1722- tracker .complete ("specs_dir" , "found" )
1723- else :
1724- tracker .skip ("specs_dir" , "not created yet" )
1725- issues .append (("info" , "No specs/ directory — created when you run /speckit.specify" ))
1726-
1727- scripts_dir = project_root / "scripts"
1728- tracker .add ("scripts_dir" , "scripts/ directory" )
1729- if scripts_dir .is_dir ():
1730- tracker .complete ("scripts_dir" , "found" )
1731- else :
1732- tracker .error ("scripts_dir" , "missing" )
1733- issues .append (("error" , "No scripts/ directory — project may not be initialized" ))
1734-
1735- templates_dir = project_root / "templates"
1736- tracker .add ("templates_dir" , "templates/ directory" )
1737- if templates_dir .is_dir ():
1738- tracker .complete ("templates_dir" , "found" )
1739- else :
1740- tracker .error ("templates_dir" , "missing" )
1741- issues .append (("error" , "No templates/ directory — project may not be initialized" ))
1742-
1743- memory_dir = project_root / "memory"
1744- tracker .add ("memory_dir" , "memory/ directory" )
1745- if memory_dir .is_dir ():
1746- tracker .complete ("memory_dir" , "found" )
1747- constitution = memory_dir / "constitution.md"
1748- tracker .add ("constitution" , "memory/constitution.md" )
1749- if constitution .is_file ():
1750- tracker .complete ("constitution" , "found" )
1751- else :
1752- tracker .error ("constitution" , "missing" )
1753- issues .append (("warning" , "No constitution.md in memory/ — project governance rules are missing" ))
1754- else :
1755- tracker .error ("memory_dir" , "missing" )
1756- issues .append (("error" , "No memory/ directory — project may not be initialized" ))
1757-
1758- console .print (tracker .render ())
1759- console .print ()
1760-
1761- # ── 2. AI agent detection ─────────────────────────────────────────
1762- agent_tracker = StepTracker ("AI Agent Configuration" )
1763- detected_agents = []
1764-
1765- for agent_key , agent_config in AGENT_CONFIG .items ():
1766- if agent_key == "generic" :
1767- continue
1768- agent_folder = agent_config ["folder" ]
1769- if agent_folder and (project_root / agent_folder ).is_dir ():
1770- detected_agents .append (agent_key )
1771- agent_tracker .add (agent_key , agent_config ["name" ])
1772- commands_dir = project_root / agent_folder / agent_config ["commands_subdir" ]
1773- if commands_dir .is_dir () and any (commands_dir .iterdir ()):
1774- agent_tracker .complete (agent_key , f"commands in { agent_folder } { agent_config ['commands_subdir' ]} /" )
1775- else :
1776- agent_tracker .error (agent_key , f"folder exists but no commands in { agent_config ['commands_subdir' ]} /" )
1777- issues .append (("warning" , f"Agent '{ agent_config ['name' ]} ' folder exists but commands directory is empty" ))
1778-
1779- if not detected_agents :
1780- agent_tracker .add ("none" , "No AI agent configured" )
1781- agent_tracker .skip ("none" , "run 'specify init --here --ai <agent>' to set up" )
1782- issues .append (("info" , "No AI agent folder detected — this is fine if you use IDE-based agents" ))
1783-
1784- console .print (agent_tracker .render ())
1785- console .print ()
1786-
1787- # ── 3. Feature specs ──────────────────────────────────────────────
1788- feature_tracker = StepTracker ("Feature Specifications" )
1789-
1790- if specs_dir .is_dir ():
1791- feature_dirs = sorted (
1792- [d for d in specs_dir .iterdir () if d .is_dir ()],
1793- key = lambda d : d .name ,
1794- )
1795- if not feature_dirs :
1796- feature_tracker .add ("empty" , "No feature directories" )
1797- feature_tracker .skip ("empty" , "run /speckit.specify to create one" )
1798- else :
1799- for fdir in feature_dirs :
1800- key = fdir .name
1801- feature_tracker .add (key , key )
1802-
1803- spec_file = fdir / "spec.md"
1804- plan_file = fdir / "plan.md"
1805- tasks_file = fdir / "tasks.md"
1806-
1807- artifacts = []
1808- missing = []
1809- for name , path in [("spec" , spec_file ), ("plan" , plan_file ), ("tasks" , tasks_file )]:
1810- if path .is_file ():
1811- artifacts .append (name )
1812- else :
1813- missing .append (name )
1814-
1815- if missing :
1816- detail = f"{ ', ' .join (artifacts )} present; missing { ', ' .join (missing )} "
1817- if "spec" in missing :
1818- feature_tracker .error (key , detail )
1819- issues .append (("error" , f"Feature '{ key } ' is missing spec.md" ))
1820- else :
1821- feature_tracker .complete (key , detail )
1822- for m in missing :
1823- issues .append (("info" , f"Feature '{ key } ' has no { m } .md — run /speckit.{ m } to generate" ))
1824- else :
1825- feature_tracker .complete (key , "spec, plan, tasks all present" )
1826- else :
1827- feature_tracker .add ("none" , "No specs/ directory" )
1828- feature_tracker .skip ("none" , "not applicable" )
1829-
1830- console .print (feature_tracker .render ())
1831- console .print ()
1832-
1833- # ── 4. Scripts health ─────────────────────────────────────────────
1834- script_tracker = StepTracker ("Scripts" )
1835-
1836- bash_dir = project_root / "scripts" / "bash"
1837- ps_dir = project_root / "scripts" / "powershell"
1838-
1839- expected_scripts = ["common" , "check-prerequisites" , "create-new-feature" , "setup-plan" , "update-agent-context" ]
1840-
1841- if bash_dir .is_dir ():
1842- for name in expected_scripts :
1843- key = f"sh_{ name } "
1844- script_path = bash_dir / f"{ name } .sh"
1845- script_tracker .add (key , f"bash/{ name } .sh" )
1846- if script_path .is_file ():
1847- if os .name != "nt" and not os .access (script_path , os .X_OK ):
1848- script_tracker .error (key , "not executable" )
1849- issues .append (("warning" , f"scripts/bash/{ name } .sh is not executable — run chmod +x" ))
1850- else :
1851- script_tracker .complete (key , "ok" )
1852- else :
1853- script_tracker .error (key , "missing" )
1854- issues .append (("error" , f"scripts/bash/{ name } .sh is missing" ))
1855- else :
1856- script_tracker .add ("no_bash" , "scripts/bash/" )
1857- script_tracker .skip ("no_bash" , "not found" )
1858-
1859- if ps_dir .is_dir ():
1860- for name in expected_scripts :
1861- key = f"ps_{ name } "
1862- script_path = ps_dir / f"{ name } .ps1"
1863- script_tracker .add (key , f"powershell/{ name } .ps1" )
1864- if script_path .is_file ():
1865- script_tracker .complete (key , "ok" )
1866- else :
1867- script_tracker .error (key , "missing" )
1868- issues .append (("error" , f"scripts/powershell/{ name } .ps1 is missing" ))
1869- else :
1870- script_tracker .add ("no_ps" , "scripts/powershell/" )
1871- script_tracker .skip ("no_ps" , "not found" )
1872-
1873- console .print (script_tracker .render ())
1874- console .print ()
1875-
1876- # ── 5. Extensions health ──────────────────────────────────────────
1877- ext_tracker = StepTracker ("Extensions" )
1878-
1879- extensions_yml = specify_dir / "extensions.yml" if specify_dir .is_dir () else None
1880- registry_json = specify_dir / "extensions" / "registry.json" if specify_dir .is_dir () else None
1881-
1882- if extensions_yml and extensions_yml .is_file ():
1883- ext_tracker .add ("config" , "extensions.yml" )
1884- try :
1885- with open (extensions_yml ) as f :
1886- ext_config = yaml .safe_load (f )
1887- if ext_config and isinstance (ext_config , dict ):
1888- ext_tracker .complete ("config" , "valid YAML" )
1889- hooks = ext_config .get ("hooks" , {})
1890- if hooks :
1891- hook_count = sum (len (v ) if isinstance (v , list ) else 0 for v in hooks .values ())
1892- ext_tracker .add ("hooks" , "Hook registrations" )
1893- ext_tracker .complete ("hooks" , f"{ hook_count } hook(s) registered" )
1894- else :
1895- ext_tracker .complete ("config" , "empty or no hooks" )
1896- except Exception as e :
1897- ext_tracker .error ("config" , f"invalid YAML: { e } " )
1898- issues .append (("warning" , f"extensions.yml has invalid YAML: { e } " ))
1899- else :
1900- ext_tracker .add ("config" , "extensions.yml" )
1901- ext_tracker .skip ("config" , "no extensions configured" )
1902-
1903- if registry_json and registry_json .is_file ():
1904- ext_tracker .add ("registry" , "Extension registry" )
1905- try :
1906- with open (registry_json ) as f :
1907- registry = json .load (f )
1908- installed = [k for k , v in registry .items () if isinstance (v , dict )]
1909- enabled = [k for k , v in registry .items () if isinstance (v , dict ) and v .get ("enabled" , True )]
1910- ext_tracker .complete ("registry" , f"{ len (installed )} installed, { len (enabled )} enabled" )
1911- except Exception as e :
1912- ext_tracker .error ("registry" , f"corrupt: { e } " )
1913- issues .append (("error" , f"Extension registry is corrupt: { e } " ))
1914- else :
1915- ext_tracker .add ("registry" , "Extension registry" )
1916- ext_tracker .skip ("registry" , "no extensions installed" )
1917-
1918- console .print (ext_tracker .render ())
1919- console .print ()
1920-
1921- # ── 6. Git status ─────────────────────────────────────────────────
1922- git_tracker = StepTracker ("Git Repository" )
1923- git_tracker .add ("git" , "Git repository" )
1924-
1925- git_ok = shutil .which ("git" ) is not None
1926- in_git_repo = False
1927- if git_ok :
1928- try :
1929- result = subprocess .run (
1930- ["git" , "rev-parse" , "--is-inside-work-tree" ],
1931- capture_output = True , text = True , cwd = str (project_root )
1932- )
1933- in_git_repo = result .returncode == 0
1934- except Exception :
1935- pass
1936-
1937- if in_git_repo :
1938- git_tracker .complete ("git" , "inside git repository" )
1939- try :
1940- branch = subprocess .run (
1941- ["git" , "rev-parse" , "--abbrev-ref" , "HEAD" ],
1942- capture_output = True , text = True , cwd = str (project_root )
1943- ).stdout .strip ()
1944- git_tracker .add ("branch" , "Current branch" )
1945- git_tracker .complete ("branch" , branch )
1946- except Exception :
1947- pass
1948- elif git_ok :
1949- git_tracker .skip ("git" , "not a git repository" )
1950- issues .append (("info" , "Not inside a git repository — git features like branching won't work" ))
1951- else :
1952- git_tracker .skip ("git" , "git not installed" )
1953- issues .append (("info" , "Git is not installed — branching and version control unavailable" ))
1954-
1955- console .print (git_tracker .render ())
1956- console .print ()
1957-
1958- # ── Summary ───────────────────────────────────────────────────────
1959- errors = [msg for sev , msg in issues if sev == "error" ]
1960- warnings = [msg for sev , msg in issues if sev == "warning" ]
1961- infos = [msg for sev , msg in issues if sev == "info" ]
1962-
1963- if not issues :
1964- console .print (Panel (
1965- "[bold green]All checks passed — project looks healthy![/bold green]" ,
1966- border_style = "green" ,
1967- padding = (1 , 2 ),
1968- ))
1969- else :
1970- summary_lines = []
1971-
1972- if errors :
1973- summary_lines .append (f"[bold red]{ len (errors )} error(s)[/bold red]" )
1974- for msg in errors :
1975- summary_lines .append (f" [red]●[/red] { msg } " )
1976- summary_lines .append ("" )
1977-
1978- if warnings :
1979- summary_lines .append (f"[bold yellow]{ len (warnings )} warning(s)[/bold yellow]" )
1980- for msg in warnings :
1981- summary_lines .append (f" [yellow]●[/yellow] { msg } " )
1982- summary_lines .append ("" )
1983-
1984- if infos :
1985- summary_lines .append (f"[bold blue]{ len (infos )} note(s)[/bold blue]" )
1986- for msg in infos :
1987- summary_lines .append (f" [blue]○[/blue] { msg } " )
1988-
1989- border = "red" if errors else "yellow" if warnings else "blue"
1990- console .print (Panel (
1991- "\n " .join (summary_lines ),
1992- title = "Diagnostic Summary" ,
1993- border_style = border ,
1994- padding = (1 , 2 ),
1995- ))
1996-
1997- if errors :
1998- raise typer .Exit (1 )
1999-
2000-
20011698@app .command ()
20021699def version ():
20031700 """Display version and system information."""
0 commit comments