@heytherevibin/skillforge 0.7.0 → 0.10.0

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.
@@ -0,0 +1,160 @@
1
+ """Preflight / health checks for Skillforge (paths, catalog, optional full router load)."""
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import json
6
+ import os
7
+ import sqlite3
8
+ import sys
9
+ from pathlib import Path
10
+
11
+ from app.db_paths import resolve_orchestrator_db
12
+
13
+
14
+ def _bundled_skills_dir() -> Path:
15
+ return Path(os.getenv("SKILLFORGE_BUNDLED_SKILLS", "./skills"))
16
+
17
+
18
+ def _user_skills_dir() -> Path:
19
+ return Path(os.getenv("SKILLFORGE_USER_SKILLS", str(Path.home() / ".skillforge" / "skills")))
20
+
21
+
22
+ def _parse_args(argv: list[str] | None) -> argparse.Namespace:
23
+ p = argparse.ArgumentParser(
24
+ description="Check Skillforge install paths, skill catalog, and optionally load the embedding router."
25
+ )
26
+ p.add_argument(
27
+ "--quick",
28
+ action="store_true",
29
+ help="Skip embedding model load (fast; checks paths + skill file counts only).",
30
+ )
31
+ p.add_argument(
32
+ "--project-root",
33
+ default="",
34
+ help="If set, also checks <root>/.skillforge/ DB path resolution.",
35
+ )
36
+ p.add_argument("--json", action="store_true", help="Machine-readable output on stdout.")
37
+ return p.parse_args(argv)
38
+
39
+
40
+ def _count_skill_md(root: Path) -> int:
41
+ if not root.is_dir():
42
+ return 0
43
+ return sum(1 for _ in root.glob("*/SKILL.md"))
44
+
45
+
46
+ def run_health(*, quick: bool, project_root: str, json_out: bool) -> int:
47
+ checks: list[dict] = []
48
+ failed = False
49
+
50
+ bundled = _bundled_skills_dir()
51
+ user_skills = _user_skills_dir()
52
+ bundled_n = _count_skill_md(bundled)
53
+
54
+ b_ok = bundled.is_dir() and bundled_n > 0
55
+ checks.append({
56
+ "name": "bundled_skills",
57
+ "ok": b_ok,
58
+ "path": str(bundled.resolve()) if bundled.exists() else str(bundled),
59
+ "skill_md_count": bundled_n,
60
+ })
61
+ if not b_ok:
62
+ failed = True
63
+
64
+ user_n = _count_skill_md(user_skills)
65
+ u_ok = True
66
+ u_err: str | None = None
67
+ if not user_skills.is_dir():
68
+ try:
69
+ user_skills.mkdir(parents=True, exist_ok=True)
70
+ except OSError as e:
71
+ u_ok = False
72
+ u_err = str(e)
73
+ failed = True
74
+ checks.append({
75
+ "name": "user_skills",
76
+ "ok": u_ok,
77
+ "path": str(user_skills),
78
+ "skill_md_count": user_n,
79
+ "error": u_err,
80
+ })
81
+
82
+ pr = (project_root or "").strip() or None
83
+ db_path = resolve_orchestrator_db(pr)
84
+ db_ok = True
85
+ db_err: str | None = None
86
+ try:
87
+ db_path.parent.mkdir(parents=True, exist_ok=True)
88
+ con = sqlite3.connect(str(db_path))
89
+ try:
90
+ con.execute("SELECT 1")
91
+ finally:
92
+ con.close()
93
+ except OSError as e:
94
+ db_ok = False
95
+ db_err = str(e)
96
+ failed = True
97
+ checks.append({
98
+ "name": "orchestrator_db",
99
+ "ok": db_ok,
100
+ "path": str(db_path),
101
+ "error": db_err,
102
+ })
103
+
104
+ router_skill_count: int | None = None
105
+ if not quick:
106
+ try:
107
+ from app.main import build_router_and_skills
108
+
109
+ _router, skills = build_router_and_skills(log=not json_out, log_prefix="[skillforge-health]")
110
+ router_skill_count = len(skills)
111
+ if router_skill_count <= 0:
112
+ failed = True
113
+ checks.append({
114
+ "name": "router_load",
115
+ "ok": router_skill_count > 0,
116
+ "skill_count": router_skill_count,
117
+ "error": None if router_skill_count and router_skill_count > 0 else "empty catalog",
118
+ })
119
+ except Exception as e:
120
+ failed = True
121
+ checks.append({
122
+ "name": "router_load",
123
+ "ok": False,
124
+ "skill_count": None,
125
+ "error": str(e),
126
+ })
127
+
128
+ payload = {
129
+ "ok": not failed,
130
+ "quick": quick,
131
+ "checks": checks,
132
+ }
133
+ if json_out:
134
+ print(json.dumps(payload, indent=2))
135
+ else:
136
+ for c in checks:
137
+ sym = "✓" if c.get("ok") else "✗"
138
+ print(f"{sym} {c['name']}", file=sys.stderr)
139
+ if c.get("path"):
140
+ print(f" path: {c['path']}", file=sys.stderr)
141
+ if c.get("skill_md_count") is not None:
142
+ print(f" SKILL.md count: {c['skill_md_count']}", file=sys.stderr)
143
+ if c.get("skill_count") is not None:
144
+ print(f" router skills: {c['skill_count']}", file=sys.stderr)
145
+ if c.get("error"):
146
+ print(f" error: {c['error']}", file=sys.stderr)
147
+ print("health: ok" if not failed else "health: failed", file=sys.stderr)
148
+
149
+ return 0 if not failed else 1
150
+
151
+
152
+ def main(argv: list[str] | None = None) -> None:
153
+ args = _parse_args(argv)
154
+ raise SystemExit(
155
+ run_health(quick=bool(args.quick), project_root=args.project_root, json_out=bool(args.json))
156
+ )
157
+
158
+
159
+ if __name__ == "__main__":
160
+ main()