Skip to content

Commit 0b34183

Browse files
committed
feat(cli): migrate to async daemon client with watchdog dependency
- Add watchdog>=4.0.0 as project dependency - Replace synchronous client calls with async DaemonClient implementation - Update all CLI commands (compile, search, status, doctor, daemon-status) to use async client methods - Refactor progress callbacks to async functions using asyncio.run() - Move protocol imports from vectorless_code.client to vectorless_code.daemon.protocol - Handle dictionary responses from daemon instead of typed objects - Remove deprecated client.py module which contained old sync implementation
1 parent 0f3cf9c commit 0b34183

7 files changed

Lines changed: 150 additions & 1635 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ dependencies = [
4141
"msgspec>=0.18.0",
4242
"click>=8.0.0",
4343
"fastmcp>=0.1.0",
44+
"watchdog>=4.0.0",
4445
]
4546

4647
[project.optional-dependencies]

src/vectorless_code/cli.py

Lines changed: 101 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from __future__ import annotations
44

5+
import asyncio
56
import functools
67
import logging
78
import os
@@ -15,8 +16,8 @@
1516
from vectorless_code import __version__
1617

1718
logger = logging.getLogger(__name__)
18-
from vectorless_code.client import DaemonStartError, is_daemon_running, start_daemon, stop_daemon
19-
from vectorless_code.protocol import (
19+
from vectorless_code.daemon_client import DaemonStartError, is_daemon_running, start_daemon, stop_daemon
20+
from vectorless_code.daemon.protocol import (
2021
DoctorCheckResult,
2122
IndexingProgress,
2223
ProjectStatusResponse,
@@ -179,28 +180,30 @@ def _run_index_with_progress(project_root: str) -> None:
179180
from rich.live import Live
180181
from rich.spinner import Spinner
181182

182-
from vectorless_code import client as _client
183+
from vectorless_code.daemon_client import DaemonClient
183184

184185
err_console = Console(stderr=True)
185186
last_progress_line: str | None = None
186187

187188
with Live(Spinner("dots", "Indexing..."), console=err_console, transient=True) as live:
188189

189-
def _on_waiting() -> None:
190+
async def _on_progress_async(progress_data: dict) -> None:
191+
nonlocal last_progress_line
192+
progress = IndexingProgress(**progress_data)
193+
last_progress_line = f"Indexing: {_format_progress(progress)}"
194+
live.update(Spinner("dots", last_progress_line))
195+
196+
async def _on_waiting_async() -> None:
190197
live.update(
191198
Spinner(
192199
"dots",
193200
"Another indexing is ongoing, waiting for it to finish...",
194201
)
195202
)
196203

197-
def _on_progress(progress: IndexingProgress) -> None:
198-
nonlocal last_progress_line
199-
last_progress_line = f"Indexing: {_format_progress(progress)}"
200-
live.update(Spinner("dots", last_progress_line))
201-
202204
try:
203-
resp = _client.index(project_root, on_progress=_on_progress, on_waiting=_on_waiting)
205+
client = DaemonClient()
206+
resp = asyncio.run(client.index(project_root, on_progress=_on_progress_async, on_waiting=_on_waiting_async))
204207
except RuntimeError as e:
205208
live.stop()
206209
if isinstance(e, DaemonStartError):
@@ -211,16 +214,17 @@ def _on_progress(progress: IndexingProgress) -> None:
211214
if last_progress_line is not None:
212215
typer.echo(last_progress_line, err=True)
213216

214-
if not resp.success:
215-
typer.echo(f"Indexing failed: {resp.message}", err=True)
217+
if not resp.get('success', False):
218+
typer.echo(f"Indexing failed: {resp.get('message', 'Unknown error')}", err=True)
216219
raise typer.Exit(code=1)
217220

218-
typer.echo(f"Files: {resp.file_count}")
219-
typer.echo(f"Lines: {resp.total_lines}")
220-
typer.echo(f"Size: {resp.total_bytes} bytes")
221-
if resp.languages:
221+
typer.echo(f"Files: {resp.get('file_count', 0)}")
222+
typer.echo(f"Lines: {resp.get('total_lines', 0)}")
223+
typer.echo(f"Size: {resp.get('total_bytes', 0)} bytes")
224+
languages = resp.get('languages', {})
225+
if languages:
222226
typer.echo("Languages:")
223-
for lang, count in sorted(resp.languages.items(), key=lambda x: -x[1]):
227+
for lang, count in sorted(languages.items(), key=lambda x: -x[1]):
224228
typer.echo(f" {lang}: {count}")
225229

226230

@@ -230,32 +234,33 @@ def _search_with_wait_spinner(
230234
doc_ids: list[str] | None = None,
231235
limit: int = 5,
232236
offset: int = 0,
233-
) -> SearchResponse:
237+
) -> dict:
234238
"""Run search, showing a spinner if waiting for load-time indexing."""
235239
from rich.console import Console
236240
from rich.live import Live
237241
from rich.spinner import Spinner
238242

239-
from vectorless_code import client as _client
243+
from vectorless_code.daemon_client import DaemonClient
240244

241245
err_console = Console(stderr=True)
242246

243247
with Live(Spinner("dots", "Searching..."), console=err_console, transient=True) as live:
244248

245-
def _on_waiting() -> None:
249+
async def _on_waiting_async() -> None:
246250
live.update(
247251
Spinner("dots", "Waiting for indexing to complete..."),
248252
refresh=True,
249253
)
250254

251-
resp = _client.search(
255+
client = DaemonClient()
256+
resp = asyncio.run(client.search(
252257
project_root=project_root,
253258
query=query,
254259
doc_ids=doc_ids,
255260
limit=limit,
256261
offset=offset,
257-
on_waiting=_on_waiting,
258-
)
262+
on_waiting=_on_waiting_async,
263+
))
259264

260265
return resp
261266

@@ -308,8 +313,6 @@ def init() -> None:
308313
@_catch_daemon_start_error
309314
def compile_cmd() -> None:
310315
"""Compile the codebase into a searchable index."""
311-
from vectorless_code import client as _client
312-
313316
project_root = str(require_project_root())
314317
logger.info("Compiling project: %s", project_root)
315318
print_project_header(project_root)
@@ -323,36 +326,57 @@ def ask(
323326
question: list[str] = typer.Argument(..., help="Question about the codebase"),
324327
) -> None:
325328
"""Ask a question about the codebase."""
326-
from vectorless_code import client as _client
327-
328329
project_root = str(require_project_root())
329330
query_str = " ".join(question)
330331

331332
logger.info("Querying project %s: %s", project_root, query_str[:100])
332333
print_project_header(project_root)
333334

334-
resp = _search_with_wait_spinner(
335+
resp_dict = _search_with_wait_spinner(
335336
project_root=project_root,
336337
query=query_str,
337338
limit=10,
338339
)
339-
print_search_results(resp)
340+
print_search_results(SearchResponse(**resp_dict))
340341

341342

342343
@app.command()
343344
@_catch_daemon_start_error
344345
def status() -> None:
345346
"""Show compilation status and index statistics."""
346-
from vectorless_code import client as _client
347+
from vectorless_code.daemon_client import DaemonClient
347348

348349
project_root_path = require_project_root()
349350
project_root = str(project_root_path)
350351
print_project_header(project_root)
351352

352353
typer.echo(f"Settings: {settings_path(project_root_path)}")
353354

354-
st = _client.project_status(project_root)
355-
print_index_stats(st)
355+
try:
356+
client = DaemonClient()
357+
resp = asyncio.run(client.project_status(project_root))
358+
359+
# Adapt dictionary response to ProjectStatusResponse or handle directly
360+
# Assuming the daemon returns a dict that can be unpacked or handled
361+
# If strict typing is needed, we might construct ProjectStatusResponse if fields match
362+
# For now, printing basic info based on typical status response
363+
364+
if resp.get('indexed', False):
365+
typer.echo("Status: Indexed")
366+
typer.echo(f"Files: {resp.get('file_count', 0)}")
367+
typer.echo(f"Nodes: {resp.get('node_count', 0)}")
368+
typer.echo(f"Size: {resp.get('total_bytes', 0)} bytes")
369+
if 'last_modified' in resp:
370+
typer.echo(f"Last modified: {resp['last_modified']}")
371+
else:
372+
typer.echo("Status: Not indexed")
373+
typer.echo("Run `vcc compile` to build the index.")
374+
375+
except RuntimeError as e:
376+
if isinstance(e, DaemonStartError):
377+
raise
378+
typer.echo(f"Failed to get status: {e}", err=True)
379+
raise typer.Exit(code=1)
356380

357381

358382
@app.command()
@@ -468,21 +492,32 @@ def doctor() -> None:
468492
_print_section("Daemon")
469493
daemon_ok = False
470494
try:
471-
st = _client.daemon_status()
472-
typer.echo(f" Version: {st.version}")
473-
typer.echo(f" Uptime: {st.uptime_seconds:.1f}s")
474-
typer.echo(f" Loaded projects: {len(st.projects)}")
495+
from vectorless_code.daemon_client import DaemonClient
496+
client = DaemonClient()
497+
st_dict = asyncio.run(client.daemon_status())
498+
# Construct a simple object or access dict keys
499+
# Assuming st_dict has version, uptime_seconds, projects
500+
typer.echo(f" Version: {st_dict.get('version', 'unknown')}")
501+
typer.echo(f" Uptime: {st_dict.get('uptime_seconds', 0):.1f}s")
502+
projects = st_dict.get('projects', [])
503+
typer.echo(f" Loaded projects: {len(projects)}")
475504
daemon_ok = True
476505
except Exception as e:
477506
_print_error(f"Cannot connect to daemon: {e}")
478507

479508
if daemon_ok:
480509
try:
481-
env_resp = _client.daemon_env()
482-
if env_resp.path_mappings:
510+
client = DaemonClient()
511+
env_resp_dict = asyncio.run(client.daemon_env())
512+
path_mappings = env_resp_dict.get('path_mappings', [])
513+
if path_mappings:
483514
typer.echo(" Path mappings:")
484-
for m in env_resp.path_mappings:
485-
typer.echo(f" {m.source}{m.target}")
515+
for m in path_mappings:
516+
# m might be a dict or object depending on daemon protocol serialization
517+
if isinstance(m, dict):
518+
typer.echo(f" {m.get('source')}{m.get('target')}")
519+
else:
520+
typer.echo(f" {m.source}{m.target}")
486521
except Exception as e:
487522
_print_error(f"Failed to get daemon env: {e}")
488523

@@ -495,9 +530,14 @@ def doctor() -> None:
495530

496531
if daemon_ok:
497532
try:
498-
_client.doctor(
533+
client = DaemonClient()
534+
535+
async def _on_result_async(result_data: dict) -> None:
536+
_print_doctor_result(DoctorCheckResult(**result_data))
537+
538+
await client.doctor(
499539
project_root=str(project_root),
500-
on_result=_print_doctor_result,
540+
on_result=_on_result_async,
501541
)
502542
except Exception as e:
503543
_print_error(f"Project checks failed: {e}")
@@ -536,16 +576,26 @@ async def _run_mcp() -> None:
536576
@_catch_daemon_start_error
537577
def daemon_status() -> None:
538578
"""Show daemon status."""
539-
from vectorless_code import client as _client
540-
541-
resp = _client.daemon_status()
542-
typer.echo(f"Daemon version: {resp.version}")
543-
typer.echo(f"Uptime: {resp.uptime_seconds:.1f}s")
544-
if resp.projects:
579+
from vectorless_code.daemon_client import DaemonClient
580+
581+
client = DaemonClient()
582+
resp_dict = asyncio.run(client.daemon_status())
583+
584+
typer.echo(f"Daemon version: {resp_dict.get('version', 'unknown')}")
585+
typer.echo(f"Uptime: {resp_dict.get('uptime_seconds', 0):.1f}s")
586+
projects = resp_dict.get('projects', [])
587+
if projects:
545588
typer.echo("Projects:")
546-
for p in resp.projects:
547-
state = "indexing" if p.indexing else "idle"
548-
typer.echo(f" {p.project_root} [{state}]")
589+
for p in projects:
590+
# p might be dict or object
591+
if isinstance(p, dict):
592+
root = p.get('project_root', 'unknown')
593+
indexing = p.get('indexing', False)
594+
else:
595+
root = p.project_root
596+
indexing = p.indexing
597+
state = "indexing" if indexing else "idle"
598+
typer.echo(f" {root} [{state}]")
549599
else:
550600
typer.echo("No projects loaded.")
551601

0 commit comments

Comments
 (0)