tscode-kg 0.2.0__py3-none-any.whl

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,86 @@
1
+ """
2
+ cli/cmd_build.py — tscodekg build command.
3
+
4
+ Builds the SQLite graph and sqlite-vec vector index from a TypeScript/JS repo.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from pathlib import Path
10
+
11
+ import click
12
+ from rich.console import Console
13
+
14
+ console = Console()
15
+
16
+
17
+ @click.command("build")
18
+ @click.option("--repo", default=".", show_default=True, help="Repository root directory.")
19
+ @click.option(
20
+ "--db",
21
+ default=None,
22
+ help="SQLite database path (default: <repo>/.tscodekg/graph.sqlite).",
23
+ )
24
+ @click.option(
25
+ "--vectors",
26
+ default=None,
27
+ help="sqlite-vec store path (default: <repo>/.tscodekg/vectors.sqlite).",
28
+ )
29
+ @click.option("--wipe", is_flag=True, default=False, help="Clear existing data before building.")
30
+ @click.option(
31
+ "--graph-only",
32
+ is_flag=True,
33
+ default=False,
34
+ help="Build SQLite graph only; skip vector index.",
35
+ )
36
+ @click.option(
37
+ "--index-only",
38
+ is_flag=True,
39
+ default=False,
40
+ help="Build vector index only; graph must already exist.",
41
+ )
42
+ def build(
43
+ repo: str,
44
+ db: str | None,
45
+ vectors: str | None,
46
+ wipe: bool,
47
+ graph_only: bool,
48
+ index_only: bool,
49
+ ) -> None:
50
+ """Build the TypeScript/JS knowledge graph for a repository."""
51
+ from tscode_kg.kg import TypeScriptKG # pylint: disable=import-outside-toplevel
52
+
53
+ repo_path = Path(repo).resolve()
54
+ if not repo_path.is_dir():
55
+ console.print(f"[red]Error:[/red] Repository not found: {repo_path}")
56
+ raise SystemExit(1)
57
+
58
+ kg = TypeScriptKG(
59
+ repo_root=repo_path,
60
+ db_path=db,
61
+ vectors_path=vectors,
62
+ )
63
+
64
+ console.print("[bold]TypeScriptKG build[/bold]")
65
+ console.print(f" repo : {repo_path}")
66
+ console.print(f" db : {kg.db_path}")
67
+ console.print(f" vectors : {kg.vectors_path}")
68
+ console.print(f" wipe : {wipe}")
69
+ console.print()
70
+
71
+ try:
72
+ if index_only:
73
+ console.print("[cyan]Building vector index...[/cyan]")
74
+ stats = kg.build_index(wipe=wipe)
75
+ elif graph_only:
76
+ console.print("[cyan]Building SQLite graph...[/cyan]")
77
+ stats = kg.build_graph(wipe=wipe)
78
+ else:
79
+ console.print("[cyan]Building graph + vector index...[/cyan]")
80
+ stats = kg.build(wipe=wipe)
81
+
82
+ console.print("[green]Done.[/green]")
83
+ console.print(str(stats))
84
+ except Exception as exc: # pylint: disable=broad-except
85
+ console.print(f"[red]Build failed:[/red] {exc}")
86
+ raise SystemExit(1) from exc
@@ -0,0 +1,124 @@
1
+ """
2
+ cli/cmd_centrality.py — tscodekg centrality command.
3
+
4
+ Structural Importance Ranking (SIR) over the TypeScriptKG graph.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ from pathlib import Path
11
+
12
+ import click
13
+
14
+
15
+ @click.command("centrality")
16
+ @click.option(
17
+ "--db",
18
+ type=click.Path(path_type=Path, dir_okay=False),
19
+ default=Path(".tscodekg/graph.sqlite"),
20
+ show_default=True,
21
+ help="Path to the TypeScriptKG SQLite graph.",
22
+ )
23
+ @click.option(
24
+ "--kind",
25
+ "kinds",
26
+ multiple=True,
27
+ type=click.Choice(["module", "class", "interface", "function", "method"], case_sensitive=False),
28
+ help="Restrict output to one or more node kinds.",
29
+ )
30
+ @click.option("--top", type=int, default=25, show_default=True, help="Maximum rows to show.")
31
+ @click.option(
32
+ "--group-by",
33
+ type=click.Choice(["node", "module"], case_sensitive=False),
34
+ default="node",
35
+ show_default=True,
36
+ help="Return node-level or module-level rankings.",
37
+ )
38
+ @click.option("--json", "as_json", is_flag=True, help="Emit JSON instead of a table.")
39
+ @click.option("--write-db", is_flag=True, help="Persist node-level scores into centrality_scores.")
40
+ def centrality(
41
+ db: Path,
42
+ kinds: tuple[str, ...],
43
+ top: int,
44
+ group_by: str,
45
+ as_json: bool,
46
+ write_db: bool,
47
+ ) -> None:
48
+ """Compute Structural Importance Ranking over the resolved TypeScriptKG graph."""
49
+ from tscode_kg.centrality import ( # pylint: disable=import-outside-toplevel
50
+ StructuralImportanceRanker,
51
+ aggregate_module_scores,
52
+ )
53
+
54
+ ranker = StructuralImportanceRanker(db)
55
+ records = ranker.compute(kinds=set(kinds) if kinds else None, top=None)
56
+
57
+ if write_db:
58
+ ranker.write_scores(records)
59
+
60
+ if group_by == "module":
61
+ payload = aggregate_module_scores(records)[:top]
62
+ if as_json:
63
+ click.echo(json.dumps(payload, indent=2))
64
+ return
65
+ _print_module_table(payload)
66
+ return
67
+
68
+ node_payload = records[:top]
69
+ if as_json:
70
+ click.echo(json.dumps([_record_to_dict(r) for r in node_payload], indent=2))
71
+ return
72
+ _print_node_table(node_payload)
73
+
74
+
75
+ def _record_to_dict(record) -> dict:
76
+ return {
77
+ "node_id": record.node_id,
78
+ "kind": record.kind,
79
+ "name": record.name,
80
+ "module_path": record.module_path,
81
+ "score": record.score,
82
+ "rank": record.rank,
83
+ "inbound_count": record.inbound_count,
84
+ "cross_module_inbound": record.cross_module_inbound,
85
+ "rel_breakdown": record.rel_breakdown,
86
+ "top_contributors": record.top_contributors,
87
+ }
88
+
89
+
90
+ def _print_node_table(records) -> None:
91
+ headers = ("Rank", "Score", "Kind", "Name", "Inbound", "XMod", "Module")
92
+ rows = [
93
+ (
94
+ r.rank,
95
+ f"{r.score:.6f}",
96
+ r.kind,
97
+ r.name,
98
+ r.inbound_count,
99
+ r.cross_module_inbound,
100
+ r.module_path or "",
101
+ )
102
+ for r in records
103
+ ]
104
+ click.echo(_format_table(headers, rows))
105
+
106
+
107
+ def _print_module_table(payload) -> None:
108
+ headers = ("Rank", "Score", "Members", "Module")
109
+ rows = [
110
+ (row["rank"], f"{row['score']:.6f}", row["member_count"], row["module_path"])
111
+ for row in payload
112
+ ]
113
+ click.echo(_format_table(headers, rows))
114
+
115
+
116
+ def _format_table(headers, rows) -> str:
117
+ widths = [len(str(h)) for h in headers]
118
+ for row in rows:
119
+ for i, value in enumerate(row):
120
+ widths[i] = max(widths[i], len(str(value)))
121
+ fmt = " ".join(f"{{:{w}}}" for w in widths)
122
+ lines = [fmt.format(*headers), fmt.format(*["-" * w for w in widths])]
123
+ lines.extend(fmt.format(*[str(v) for v in row]) for row in rows)
124
+ return "\n".join(lines)
@@ -0,0 +1,58 @@
1
+ """
2
+ cli/cmd_explain.py — tscodekg explain command.
3
+
4
+ explain — get a natural-language explanation of a code node by its ID
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from pathlib import Path
10
+
11
+ import click
12
+
13
+
14
+ @click.command("explain")
15
+ @click.argument("node_id", metavar="NODE_ID")
16
+ @click.option("--repo", default=".", show_default=True, help="Repository root.")
17
+ @click.option(
18
+ "--db",
19
+ default=None,
20
+ type=click.Path(),
21
+ help="SQLite knowledge graph path (default: <repo>/.tscodekg/graph.sqlite).",
22
+ )
23
+ @click.option(
24
+ "--out",
25
+ type=click.Path(),
26
+ default=None,
27
+ help="Output file path (default: stdout).",
28
+ )
29
+ def explain(node_id: str, repo: str, db: str | None, out: str | None) -> None:
30
+ """Get a natural-language explanation of a code node.
31
+
32
+ NODE_ID is the stable identifier of a node, e.g.:
33
+ fn:src/utils/helpers.ts:formatDate
34
+ """
35
+ from tscode_kg.explain import render_explain # pylint: disable=import-outside-toplevel
36
+ from tscode_kg.kg import TypeScriptKG # pylint: disable=import-outside-toplevel
37
+
38
+ kg = TypeScriptKG(repo_root=Path(repo).resolve(), db_path=db)
39
+
40
+ # Fail fast for scripting: distinguish missing-node from rendered output.
41
+ if kg.node(node_id) is None:
42
+ click.echo(f"[ERROR] Node not found: {node_id}", err=True)
43
+ kg.close()
44
+ raise SystemExit(1)
45
+
46
+ markdown_output = render_explain(
47
+ kg,
48
+ node_id,
49
+ snippets_hint="tscodekg pack",
50
+ )
51
+
52
+ if out:
53
+ Path(out).write_text(markdown_output)
54
+ click.echo(f"[OK] Explanation written to {out}")
55
+ else:
56
+ click.echo(markdown_output)
57
+
58
+ kg.close()
@@ -0,0 +1,43 @@
1
+ """
2
+ cli/cmd_framework_nodes.py — tscodekg framework-nodes command.
3
+
4
+ Framework-like hub module detection over the TypeScriptKG graph.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from pathlib import Path
10
+
11
+ import click
12
+
13
+
14
+ @click.command("framework-nodes")
15
+ @click.option(
16
+ "--db",
17
+ type=click.Path(path_type=Path, dir_okay=False),
18
+ default=Path(".tscodekg/graph.sqlite"),
19
+ show_default=True,
20
+ help="Path to the TypeScriptKG SQLite graph.",
21
+ )
22
+ @click.option("--top", type=int, default=25, show_default=True, help="Number of top modules.")
23
+ def framework_nodes(db: Path, top: int) -> None:
24
+ """Show top framework-like modules (high SIR + high connectivity)."""
25
+ from tscode_kg.bridge import ( # pylint: disable=import-outside-toplevel
26
+ compute_bridge_centrality,
27
+ )
28
+ from tscode_kg.centrality import ( # pylint: disable=import-outside-toplevel
29
+ StructuralImportanceRanker,
30
+ )
31
+ from tscode_kg.framework_detector import ( # pylint: disable=import-outside-toplevel
32
+ detect_framework_nodes,
33
+ )
34
+
35
+ # Both metrics must be persisted before detection can combine them.
36
+ ranker = StructuralImportanceRanker(str(db))
37
+ ranker.write_scores(ranker.compute(), metric="sir_pagerank")
38
+ compute_bridge_centrality(kind="module", include_imports=True, top=top, db_path=str(db))
39
+
40
+ nodes = detect_framework_nodes(limit=top, db_path=str(db))
41
+ click.echo(f"Top {top} framework-like modules:")
42
+ for node_id, score, label in nodes:
43
+ click.echo(f"{label:50s} {score:.5f} ({node_id})")
@@ -0,0 +1,125 @@
1
+ """
2
+ cli/cmd_hooks.py — tscodekg install-hooks command.
3
+
4
+ install-hooks — install the pre-commit snapshot hook into .git/hooks/
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import stat
10
+ from pathlib import Path
11
+
12
+ import click
13
+
14
+ # ---------------------------------------------------------------------------
15
+ # Hook script content (embedded so this module is self-contained when
16
+ # installed as a package in any repo, not just tscode_kg itself)
17
+ # ---------------------------------------------------------------------------
18
+
19
+ _PRE_COMMIT_HOOK = """\
20
+ #!/usr/bin/env bash
21
+ # TypeScriptKG pre-commit hook — keeps the local index in sync and captures
22
+ # metrics snapshots BEFORE quality checks run.
23
+ # Installed by: tscodekg install-hooks
24
+ # Skip with: TSCODEKG_SKIP_SNAPSHOT=1 git commit ...
25
+ set -euo pipefail
26
+
27
+ [ "${TSCODEKG_SKIP_SNAPSHOT:-0}" = "1" ] && exit 0
28
+
29
+ REPO_ROOT="$(git rev-parse --show-toplevel)"
30
+ cd "$REPO_ROOT"
31
+
32
+ # Resolve the tscodekg binary: prefer the repo's .venv, fall back to PATH.
33
+ if [ -x "$REPO_ROOT/.venv/bin/tscodekg" ]; then
34
+ TSCODEKG="$REPO_ROOT/.venv/bin/tscodekg"
35
+ elif command -v tscodekg &>/dev/null; then
36
+ TSCODEKG="tscodekg"
37
+ else
38
+ echo "[tscodekg] binary not found — skipping snapshot hook" >&2
39
+ exit 0
40
+ fi
41
+
42
+ # Capture the tree hash of the staged index NOW — before any tool modifies files.
43
+ TREE_HASH=$(git write-tree)
44
+ BRANCH=$(git rev-parse --abbrev-ref HEAD)
45
+
46
+ # Rebuild the local index to keep it in sync with staged content.
47
+ "$TSCODEKG" build --repo "$REPO_ROOT" || exit 1
48
+
49
+ # Snapshot TypeScriptKG (version auto-detected from installed package).
50
+ "$TSCODEKG" snapshot save \\
51
+ --repo . \\
52
+ --tree-hash "$TREE_HASH" \\
53
+ --branch "$BRANCH" \\
54
+ || { echo "[tscodekg] snapshot skipped (run 'tscodekg build' to initialize)" >&2; }
55
+
56
+ # Stage the snapshot directory so it is included in the commit.
57
+ git add .tscodekg/snapshots/ 2>/dev/null || true
58
+
59
+ # Run pre-commit framework checks (ruff, ty, detect-secrets, etc.) AFTER
60
+ # snapshots are captured and staged. Delegates to .pre-commit-config.yaml so
61
+ # quality checks stay in one place.
62
+ PRECOMMIT="$REPO_ROOT/.venv/bin/pre-commit"
63
+ if [ -x "$PRECOMMIT" ]; then
64
+ "$PRECOMMIT" run || exit 1
65
+ elif command -v pre-commit &>/dev/null; then
66
+ pre-commit run || exit 1
67
+ fi
68
+
69
+ exit 0
70
+ """
71
+
72
+
73
+ @click.command("install-hooks")
74
+ @click.option(
75
+ "--repo",
76
+ default=".",
77
+ type=click.Path(exists=True),
78
+ show_default=True,
79
+ help="Repository root.",
80
+ )
81
+ @click.option(
82
+ "--force",
83
+ is_flag=True,
84
+ help="Overwrite an existing pre-commit hook.",
85
+ )
86
+ def install_hooks(repo: str, force: bool) -> None:
87
+ """Install the TypeScriptKG pre-commit git hook.
88
+
89
+ After installation, before each commit:
90
+
91
+ \b
92
+ 1. Rebuilds the local TypeScriptKG index (full wipe)
93
+ 2. Captures a metrics snapshot keyed by tree hash
94
+ 3. Stages the snapshot directory atomically
95
+ 4. Runs the pre-commit framework checks
96
+
97
+ This keeps the index in sync and ensures snapshots reflect the state of
98
+ the knowledge graph at commit time.
99
+
100
+ Example:
101
+ tscodekg install-hooks --repo .
102
+ """
103
+ repo_root = Path(repo).resolve()
104
+ git_dir = repo_root / ".git"
105
+
106
+ if not git_dir.is_dir():
107
+ click.echo(f"Error: {repo_root} is not a git repository.", err=True)
108
+ raise SystemExit(1)
109
+
110
+ hooks_dir = git_dir / "hooks"
111
+ hooks_dir.mkdir(exist_ok=True)
112
+ hook_path = hooks_dir / "pre-commit"
113
+
114
+ if hook_path.exists() and not force:
115
+ click.echo(f"Hook already exists: {hook_path}")
116
+ click.echo("Use --force to overwrite.")
117
+ raise SystemExit(1)
118
+
119
+ hook_path.write_text(_PRE_COMMIT_HOOK)
120
+ mode = hook_path.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
121
+ hook_path.chmod(mode)
122
+
123
+ click.echo(f"OK Installed pre-commit hook: {hook_path}")
124
+ click.echo(" Snapshots will be captured automatically before each commit.")
125
+ click.echo(" Run 'tscodekg build' first if you haven't built the graph yet.")
@@ -0,0 +1,234 @@
1
+ """
2
+ cli/cmd_init.py — tscodekg init command.
3
+
4
+ init — download model, build graph, install hooks, capture snapshot
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import stat
10
+ import subprocess
11
+ import time
12
+ import tomllib
13
+ from pathlib import Path
14
+
15
+ import click
16
+
17
+ from tscode_kg.cli.cmd_hooks import _PRE_COMMIT_HOOK
18
+
19
+
20
+ def _has_tscodekg_config(repo_root: Path) -> bool:
21
+ """Check whether pyproject.toml already has a [tool.tscodekg] section.
22
+
23
+ :param repo_root: Repository root directory.
24
+ :return: True when the section exists.
25
+ """
26
+ pyproject = repo_root / "pyproject.toml"
27
+ if not pyproject.exists():
28
+ return False
29
+ try:
30
+ with open(pyproject, "rb") as f:
31
+ data = tomllib.load(f)
32
+ return "tscodekg" in data.get("tool", {})
33
+ except (OSError, ValueError):
34
+ return False
35
+
36
+
37
+ def _scaffold_tscodekg_config(repo_root: Path) -> bool:
38
+ """Append a minimal [tool.tscodekg] section to pyproject.toml if missing.
39
+
40
+ Detects the most likely source directory (``src``, ``lib``, or ``app``)
41
+ and sets it as the include list.
42
+
43
+ :param repo_root: Repository root directory.
44
+ :return: True if the section was added, False if skipped.
45
+ """
46
+ pyproject = repo_root / "pyproject.toml"
47
+ if not pyproject.exists():
48
+ return False
49
+
50
+ candidates = ["src", "lib", "app"]
51
+ include_dirs: list[str] = [d for d in candidates if (repo_root / d).is_dir()]
52
+
53
+ lines = [
54
+ "",
55
+ "[tool.tscodekg]",
56
+ "# Directories to include in the knowledge graph build and analysis.",
57
+ "# When unset, all directories are indexed.",
58
+ ]
59
+ if include_dirs:
60
+ include_str = ", ".join(f'"{d}"' for d in include_dirs)
61
+ lines.append(f"include = [{include_str}]")
62
+ else:
63
+ lines.append("# include = []")
64
+
65
+ pyproject.open("a").write("\n".join(lines) + "\n")
66
+ return True
67
+
68
+
69
+ @click.command("init")
70
+ @click.option(
71
+ "--repo",
72
+ default=".",
73
+ type=click.Path(exists=True),
74
+ show_default=True,
75
+ help="Repository root.",
76
+ )
77
+ @click.option(
78
+ "--model",
79
+ default=None,
80
+ help="SentenceTransformer model name (default: shared kg_utils default).",
81
+ )
82
+ @click.option("--skip-hooks", is_flag=True, help="Don't install the pre-commit git hook.")
83
+ @click.option("--skip-snapshot", is_flag=True, help="Don't capture an initial snapshot.")
84
+ @click.option("--force", is_flag=True, help="Overwrite existing graph data and hook.")
85
+ def init(
86
+ repo: str,
87
+ model: str | None,
88
+ skip_hooks: bool,
89
+ skip_snapshot: bool,
90
+ force: bool,
91
+ ) -> None:
92
+ """Initialize TypeScriptKG in a repository.
93
+
94
+ Downloads the embedding model, builds the knowledge graph (SQLite +
95
+ sqlite-vec), optionally installs the pre-commit hook, and captures an
96
+ initial snapshot. Designed to be idempotent — safe to run more than once.
97
+
98
+ Example::
99
+
100
+ tscodekg init --repo .
101
+ """
102
+ from kg_utils.semantic import ( # pylint: disable=import-outside-toplevel
103
+ DEFAULT_MODEL,
104
+ _local_model_path,
105
+ )
106
+
107
+ from tscode_kg.kg import TypeScriptKG # pylint: disable=import-outside-toplevel
108
+
109
+ repo_root = Path(repo).resolve()
110
+ model = model or DEFAULT_MODEL
111
+ t_total = time.monotonic()
112
+
113
+ click.echo()
114
+ click.echo(" TypeScriptKG Init")
115
+ click.echo(f" repo {repo_root}")
116
+ click.echo()
117
+
118
+ # ------------------------------------------------------------------
119
+ # Step 0: Scaffold [tool.tscodekg] in pyproject.toml if missing
120
+ # ------------------------------------------------------------------
121
+ if not _has_tscodekg_config(repo_root):
122
+ if _scaffold_tscodekg_config(repo_root):
123
+ click.echo(" [0/4] Added [tool.tscodekg] section to pyproject.toml")
124
+ else:
125
+ click.echo(" [0/4] No pyproject.toml found — skipping config scaffold")
126
+ else:
127
+ click.echo(" [0/4] [tool.tscodekg] config already present")
128
+
129
+ # ------------------------------------------------------------------
130
+ # Step 1: Download the embedding model
131
+ # ------------------------------------------------------------------
132
+ click.echo()
133
+ local_path = _local_model_path(model)
134
+
135
+ if local_path.exists() and not force:
136
+ click.echo(f" [1/4] Model already cached at {local_path}")
137
+ else:
138
+ click.echo(f" [1/4] Downloading embedding model '{model}'...")
139
+ from sentence_transformers import ( # pylint: disable=import-outside-toplevel
140
+ SentenceTransformer,
141
+ )
142
+
143
+ st_model = SentenceTransformer(model)
144
+ local_path.mkdir(parents=True, exist_ok=True)
145
+ st_model.save(str(local_path))
146
+ click.echo(f" OK: model saved to {local_path}")
147
+
148
+ # ------------------------------------------------------------------
149
+ # Step 2: Build the knowledge graph (full wipe)
150
+ # ------------------------------------------------------------------
151
+ click.echo()
152
+ click.echo(" [2/4] Building knowledge graph...")
153
+ kg = TypeScriptKG(repo_root=repo_root, model=model)
154
+ try:
155
+ stats = kg.build(wipe=True)
156
+ click.echo(f" OK: {stats}")
157
+ finally:
158
+ kg.close()
159
+
160
+ # ------------------------------------------------------------------
161
+ # Step 3: Install pre-commit hook
162
+ # ------------------------------------------------------------------
163
+ click.echo()
164
+ if skip_hooks:
165
+ click.echo(" [3/4] Skipping hook installation (--skip-hooks)")
166
+ else:
167
+ git_dir = repo_root / ".git"
168
+ if not git_dir.is_dir():
169
+ click.echo(" [3/4] Not a git repository — skipping hook installation")
170
+ else:
171
+ hooks_dir = git_dir / "hooks"
172
+ hooks_dir.mkdir(exist_ok=True)
173
+ hook_path = hooks_dir / "pre-commit"
174
+
175
+ if hook_path.exists() and not force:
176
+ click.echo(f" [3/4] Hook already exists: {hook_path}")
177
+ click.echo(" Use --force to overwrite.")
178
+ else:
179
+ hook_path.write_text(_PRE_COMMIT_HOOK)
180
+ mode = hook_path.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
181
+ hook_path.chmod(mode)
182
+ click.echo(f" [3/4] OK: installed pre-commit hook at {hook_path}")
183
+
184
+ # ------------------------------------------------------------------
185
+ # Step 4: Capture initial snapshot
186
+ # ------------------------------------------------------------------
187
+ click.echo()
188
+ if skip_snapshot:
189
+ click.echo(" [4/4] Skipping initial snapshot (--skip-snapshot)")
190
+ else:
191
+ try:
192
+ tree_hash = (
193
+ subprocess.check_output(
194
+ ["git", "rev-parse", "HEAD"],
195
+ cwd=str(repo_root),
196
+ stderr=subprocess.DEVNULL,
197
+ )
198
+ .decode()
199
+ .strip()
200
+ )
201
+ branch = (
202
+ subprocess.check_output(
203
+ ["git", "rev-parse", "--abbrev-ref", "HEAD"],
204
+ cwd=str(repo_root),
205
+ stderr=subprocess.DEVNULL,
206
+ )
207
+ .decode()
208
+ .strip()
209
+ )
210
+ except (subprocess.CalledProcessError, FileNotFoundError):
211
+ tree_hash = ""
212
+ branch = None
213
+
214
+ from tscode_kg.cli.cmd_snapshot import ( # pylint: disable=import-outside-toplevel
215
+ capture_snapshot,
216
+ )
217
+
218
+ try:
219
+ capture_snapshot(
220
+ version="",
221
+ repo=str(repo_root),
222
+ db=None,
223
+ snapshots_dir=None,
224
+ branch=branch,
225
+ tree_hash=tree_hash,
226
+ )
227
+ click.echo(" [4/4] OK: initial snapshot captured")
228
+ except Exception as exc: # noqa: BLE001
229
+ click.echo(f" [4/4] Snapshot skipped: {exc}")
230
+
231
+ elapsed = time.monotonic() - t_total
232
+ click.echo()
233
+ click.echo(f" Done in {elapsed:.1f}s — TypeScriptKG is ready.")
234
+ click.echo(' Try: tscodekg query "authentication middleware"')