ravmem-cli 0.1.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.
- ravmem/cli/__init__.py +0 -0
- ravmem/cli/analyze.py +484 -0
- ravmem/cli/auth.py +675 -0
- ravmem/cli/browse.py +144 -0
- ravmem/cli/config_cmd.py +69 -0
- ravmem/cli/diff.py +122 -0
- ravmem/cli/git_local.py +131 -0
- ravmem/cli/impact.py +106 -0
- ravmem/cli/info.py +82 -0
- ravmem/cli/kg.py +787 -0
- ravmem/cli/main.py +96 -0
- ravmem/cli/remote.py +549 -0
- ravmem/cli/search.py +91 -0
- ravmem/cli/serve.py +41 -0
- ravmem/client/__init__.py +3 -0
- ravmem/client/api.py +279 -0
- ravmem/client/config.py +52 -0
- ravmem/config.py +103 -0
- ravmem/config_loader.py +247 -0
- ravmem_cli-0.1.0.dist-info/METADATA +62 -0
- ravmem_cli-0.1.0.dist-info/RECORD +25 -0
- ravmem_cli-0.1.0.dist-info/WHEEL +5 -0
- ravmem_cli-0.1.0.dist-info/entry_points.txt +2 -0
- ravmem_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- ravmem_cli-0.1.0.dist-info/top_level.txt +1 -0
ravmem/cli/main.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""CLI entry point — registers all subcommands."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def setup_logging(verbose: bool = False) -> None:
|
|
10
|
+
"""Configure root logger based on verbosity or RAVMEM_LOG_LEVEL env var."""
|
|
11
|
+
import os
|
|
12
|
+
env_level = os.environ.get("RAVMEM_LOG_LEVEL", "").upper()
|
|
13
|
+
if verbose:
|
|
14
|
+
level = logging.DEBUG
|
|
15
|
+
elif env_level in ("DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"):
|
|
16
|
+
level = getattr(logging, env_level)
|
|
17
|
+
else:
|
|
18
|
+
level = logging.INFO
|
|
19
|
+
logging.basicConfig(
|
|
20
|
+
level=level,
|
|
21
|
+
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
|
22
|
+
datefmt="%H:%M:%S",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def version_callback(value: bool):
|
|
27
|
+
if value:
|
|
28
|
+
typer.echo("RavMem CLI v0.2.0")
|
|
29
|
+
raise typer.Exit()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
BANNER = r"""
|
|
33
|
+
[bold cyan]
|
|
34
|
+
_____ __ __
|
|
35
|
+
| __ \ | \/ |
|
|
36
|
+
| |__) | __ ___ ___ __ ___ | \ / | ___ _ __ ___
|
|
37
|
+
| _ / / _` \ \ / / '_ ` _ \| |\/| |/ _ \ '_ ` _ \
|
|
38
|
+
| | \ \ (_| |\ V /| | | | | | | | | __/ | | | | |
|
|
39
|
+
|_| \_\__,_| \_/ |_| |_| |_|_| |_|\___|_| |_| |_|
|
|
40
|
+
[/bold cyan]
|
|
41
|
+
[bold]Persistent Memory & Context Engine for AI Agent Teams[/bold]
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
app = typer.Typer(
|
|
45
|
+
name="ravmem",
|
|
46
|
+
help="RavMem — code knowledge graph engine",
|
|
47
|
+
rich_markup_mode="rich",
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@app.callback(invoke_without_command=True)
|
|
52
|
+
def main(
|
|
53
|
+
ctx: typer.Context,
|
|
54
|
+
verbose: bool = typer.Option(False, "--verbose", "-v", help="Enable debug logging"),
|
|
55
|
+
version: Optional[bool] = typer.Option(
|
|
56
|
+
None, "--version", "-V", help="Show version and exit", callback=version_callback, is_eager=True
|
|
57
|
+
),
|
|
58
|
+
):
|
|
59
|
+
"""
|
|
60
|
+
RavMem — The Code Knowledge Graph Engine.
|
|
61
|
+
"""
|
|
62
|
+
setup_logging(verbose)
|
|
63
|
+
if ctx.invoked_subcommand is None and not version:
|
|
64
|
+
from rich.console import Console
|
|
65
|
+
Console().print(BANNER)
|
|
66
|
+
typer.echo(ctx.get_help())
|
|
67
|
+
|
|
68
|
+
from ravmem.cli.analyze import analyze_cmd
|
|
69
|
+
from ravmem.cli.auth import app as auth_app
|
|
70
|
+
from ravmem.cli.config_cmd import app as config_app
|
|
71
|
+
from ravmem.cli.impact import impact_cmd
|
|
72
|
+
from ravmem.cli.kg import app as kg_app
|
|
73
|
+
from ravmem.cli.remote import app as remote_app
|
|
74
|
+
from ravmem.cli.search import search_cmd
|
|
75
|
+
from ravmem.cli.serve import serve_cmd
|
|
76
|
+
from ravmem.cli.info import health_cmd, metrics_cmd
|
|
77
|
+
from ravmem.cli.diff import diff_impact_cmd
|
|
78
|
+
from ravmem.cli.browse import clusters_cmd, symbols_cmd
|
|
79
|
+
|
|
80
|
+
app.command("analyze")(analyze_cmd)
|
|
81
|
+
app.command("search")(search_cmd)
|
|
82
|
+
app.command("impact")(impact_cmd)
|
|
83
|
+
app.command("serve")(serve_cmd)
|
|
84
|
+
app.command("health")(health_cmd)
|
|
85
|
+
app.command("metrics")(metrics_cmd)
|
|
86
|
+
app.command("diff-impact")(diff_impact_cmd)
|
|
87
|
+
app.command("clusters")(clusters_cmd)
|
|
88
|
+
app.command("symbols")(symbols_cmd)
|
|
89
|
+
app.add_typer(auth_app, name="auth")
|
|
90
|
+
app.add_typer(config_app, name="config")
|
|
91
|
+
app.add_typer(remote_app, name="remote")
|
|
92
|
+
app.add_typer(kg_app, name="kg")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
if __name__ == "__main__":
|
|
96
|
+
app()
|
ravmem/cli/remote.py
ADDED
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
"""ravmem remote — manage a remote RavMem server."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from rich.table import Table
|
|
9
|
+
|
|
10
|
+
app = typer.Typer(
|
|
11
|
+
name="remote",
|
|
12
|
+
help="Manage remote RavMem server",
|
|
13
|
+
no_args_is_help=True,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _console():
|
|
18
|
+
"""Lazy import of rich console."""
|
|
19
|
+
from rich.console import Console
|
|
20
|
+
|
|
21
|
+
return Console()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _get_client():
|
|
25
|
+
"""Build a client from saved config or exit with an error."""
|
|
26
|
+
from ravmem.client.config import get_client_from_config
|
|
27
|
+
|
|
28
|
+
client = get_client_from_config()
|
|
29
|
+
if client is None:
|
|
30
|
+
typer.echo(
|
|
31
|
+
"Remote server not configured. Run 'ravmem remote config --url <URL>' first.",
|
|
32
|
+
err=True,
|
|
33
|
+
)
|
|
34
|
+
raise typer.Exit(1)
|
|
35
|
+
return client
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _resolve_project(explicit: str | None) -> str:
|
|
39
|
+
"""Return *explicit* if given, else fall back to config default_project."""
|
|
40
|
+
if explicit:
|
|
41
|
+
return explicit
|
|
42
|
+
from ravmem.client.config import load_config
|
|
43
|
+
|
|
44
|
+
cfg = load_config()
|
|
45
|
+
proj = cfg.get("default_project", "")
|
|
46
|
+
if not proj:
|
|
47
|
+
typer.echo(
|
|
48
|
+
"No --project given and no default_project configured. "
|
|
49
|
+
"Run 'ravmem remote use <project_id>' to set a default.",
|
|
50
|
+
err=True,
|
|
51
|
+
)
|
|
52
|
+
raise typer.Exit(1)
|
|
53
|
+
return proj
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _resolve_branch(explicit: str | None) -> str:
|
|
57
|
+
"""Return *explicit* if given, else fall back to config default_branch."""
|
|
58
|
+
if explicit:
|
|
59
|
+
return explicit
|
|
60
|
+
from ravmem.client.config import load_config
|
|
61
|
+
|
|
62
|
+
cfg = load_config()
|
|
63
|
+
return cfg.get("default_branch", "") or "main"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# ------------------------------------------------------------------
|
|
67
|
+
# Commands
|
|
68
|
+
# ------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@app.command("config")
|
|
72
|
+
def config_cmd(
|
|
73
|
+
url: Optional[str] = typer.Option(None, "--url", help="Server URL"),
|
|
74
|
+
api_key: Optional[str] = typer.Option(None, "--api-key", help="API key"),
|
|
75
|
+
) -> None:
|
|
76
|
+
"""Configure server connection. Saved to ~/.ravmem/client.json"""
|
|
77
|
+
from ravmem.client.config import load_config, save_config
|
|
78
|
+
|
|
79
|
+
cfg = load_config()
|
|
80
|
+
if url is not None:
|
|
81
|
+
cfg["server_url"] = url.rstrip("/")
|
|
82
|
+
if api_key is not None:
|
|
83
|
+
cfg["api_key"] = api_key
|
|
84
|
+
save_config(cfg)
|
|
85
|
+
typer.echo("Configuration saved.")
|
|
86
|
+
typer.echo(f" server_url: {cfg['server_url']}")
|
|
87
|
+
typer.echo(f" api_key: {'***' if cfg['api_key'] else '(none)'}")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@app.command("projects")
|
|
91
|
+
def projects_cmd(
|
|
92
|
+
output_json: bool = typer.Option(False, "--json", help="Output raw JSON"),
|
|
93
|
+
) -> None:
|
|
94
|
+
"""List all projects on the server."""
|
|
95
|
+
client = _get_client()
|
|
96
|
+
try:
|
|
97
|
+
projects = client.list_projects()
|
|
98
|
+
finally:
|
|
99
|
+
client.close()
|
|
100
|
+
|
|
101
|
+
if output_json:
|
|
102
|
+
import json
|
|
103
|
+
typer.echo(json.dumps(projects, indent=2, default=str))
|
|
104
|
+
return
|
|
105
|
+
|
|
106
|
+
if not projects:
|
|
107
|
+
typer.echo("No projects found.")
|
|
108
|
+
return
|
|
109
|
+
|
|
110
|
+
table = Table(title="Projects")
|
|
111
|
+
table.add_column("ID", style="cyan")
|
|
112
|
+
table.add_column("Name")
|
|
113
|
+
table.add_column("Default Branch")
|
|
114
|
+
table.add_column("Repo")
|
|
115
|
+
for p in projects:
|
|
116
|
+
table.add_row(
|
|
117
|
+
p.get("id", ""),
|
|
118
|
+
p.get("name", ""),
|
|
119
|
+
p.get("default_branch", ""),
|
|
120
|
+
p.get("repo_url", "") or p.get("repo_path", ""),
|
|
121
|
+
)
|
|
122
|
+
_console().print(table)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@app.command("add")
|
|
126
|
+
def add_cmd(
|
|
127
|
+
path: Optional[str] = typer.Argument(
|
|
128
|
+
None,
|
|
129
|
+
help="Local path to the repo (upload mode) or server path (mount mode). "
|
|
130
|
+
"Omit when using --git-url.",
|
|
131
|
+
),
|
|
132
|
+
name: Optional[str] = typer.Option(None, "--name", help="Project display name"),
|
|
133
|
+
default_branch: str = typer.Option(
|
|
134
|
+
"main", "--default-branch",
|
|
135
|
+
help="Primary branch (used as base for new-branch copy-from-base optimisation)",
|
|
136
|
+
),
|
|
137
|
+
# Clone mode
|
|
138
|
+
git_url: Optional[str] = typer.Option(
|
|
139
|
+
None, "--git-url",
|
|
140
|
+
help="Git remote URL — server clones this. Selects clone mode.",
|
|
141
|
+
),
|
|
142
|
+
git_username: Optional[str] = typer.Option(
|
|
143
|
+
None, "--git-username",
|
|
144
|
+
help="Git username for private repos (clone mode only).",
|
|
145
|
+
),
|
|
146
|
+
git_token: Optional[str] = typer.Option(
|
|
147
|
+
None, "--git-token",
|
|
148
|
+
help="Git token/password for private repos (clone mode only). "
|
|
149
|
+
"Stored encrypted on the server — never logged.",
|
|
150
|
+
hide_input=True,
|
|
151
|
+
),
|
|
152
|
+
# Mount mode
|
|
153
|
+
mount: bool = typer.Option(
|
|
154
|
+
False, "--mount",
|
|
155
|
+
help="Mount mode: treat PATH as a volume already mounted on the server. "
|
|
156
|
+
"No file upload; admin keeps the mount current.",
|
|
157
|
+
),
|
|
158
|
+
) -> None:
|
|
159
|
+
"""Register a project on the remote server.
|
|
160
|
+
|
|
161
|
+
\b
|
|
162
|
+
Three modes — pick exactly one:
|
|
163
|
+
|
|
164
|
+
Upload (default when PATH is given):
|
|
165
|
+
ravmem remote add /local/repo --name myproject
|
|
166
|
+
|
|
167
|
+
Clone from public GitHub/GitLab:
|
|
168
|
+
ravmem remote add --git-url https://github.com/org/repo --name myproject
|
|
169
|
+
|
|
170
|
+
Clone from private repo:
|
|
171
|
+
ravmem remote add --git-url https://github.com/org/private \\
|
|
172
|
+
--git-username myuser --git-token ghp_xxx --name myproject
|
|
173
|
+
|
|
174
|
+
Mount (server-side volume / symlink):
|
|
175
|
+
ravmem remote add /mounted/repo --name myproject --mount
|
|
176
|
+
"""
|
|
177
|
+
if not git_url and not path:
|
|
178
|
+
typer.echo(
|
|
179
|
+
"✗ Specify a source:\n"
|
|
180
|
+
" Upload: ravmem remote add <PATH>\n"
|
|
181
|
+
" Clone: ravmem remote add --git-url <URL>\n"
|
|
182
|
+
" Mount: ravmem remote add <PATH> --mount",
|
|
183
|
+
err=True,
|
|
184
|
+
)
|
|
185
|
+
raise typer.Exit(1)
|
|
186
|
+
|
|
187
|
+
client = _get_client()
|
|
188
|
+
try:
|
|
189
|
+
result = client.create_project(
|
|
190
|
+
git_url=git_url,
|
|
191
|
+
git_username=git_username,
|
|
192
|
+
git_token=git_token,
|
|
193
|
+
repo_path=path,
|
|
194
|
+
mount=mount,
|
|
195
|
+
name=name,
|
|
196
|
+
default_branch=default_branch,
|
|
197
|
+
)
|
|
198
|
+
finally:
|
|
199
|
+
client.close()
|
|
200
|
+
|
|
201
|
+
mode = result.get("index_mode", "upload")
|
|
202
|
+
typer.echo(f"✓ Project created: {result.get('id', '')}")
|
|
203
|
+
typer.echo(f" name: {result.get('name', '')}")
|
|
204
|
+
typer.echo(f" mode: {mode}")
|
|
205
|
+
typer.echo(f" default branch: {result.get('default_branch', '')}")
|
|
206
|
+
if mode == "upload":
|
|
207
|
+
typer.echo(
|
|
208
|
+
"\n Next: run 'ravmem analyze <PATH> --project "
|
|
209
|
+
+ result.get("id", "<id>")
|
|
210
|
+
+ "' to upload and index."
|
|
211
|
+
)
|
|
212
|
+
elif mode == "clone":
|
|
213
|
+
typer.echo(
|
|
214
|
+
"\n Next: run 'ravmem analyze --project "
|
|
215
|
+
+ result.get("id", "<id>")
|
|
216
|
+
+ "' to trigger the first index."
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
@app.command("use")
|
|
221
|
+
def use_cmd(
|
|
222
|
+
project_id: str = typer.Argument(..., help="Project ID to set as default"),
|
|
223
|
+
) -> None:
|
|
224
|
+
"""Set the default project for remote commands."""
|
|
225
|
+
from ravmem.client.config import load_config, save_config
|
|
226
|
+
|
|
227
|
+
cfg = load_config()
|
|
228
|
+
cfg["default_project"] = project_id
|
|
229
|
+
save_config(cfg)
|
|
230
|
+
typer.echo(f"Default project set to '{project_id}'.")
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
@app.command("branches")
|
|
234
|
+
def branches_cmd(
|
|
235
|
+
project: Optional[str] = typer.Option(None, "--project", "-p", help="Project ID"),
|
|
236
|
+
output_json: bool = typer.Option(False, "--json", help="Output raw JSON"),
|
|
237
|
+
) -> None:
|
|
238
|
+
"""List branches and their index status."""
|
|
239
|
+
project_id = _resolve_project(project)
|
|
240
|
+
client = _get_client()
|
|
241
|
+
try:
|
|
242
|
+
branches = client.list_branches(project_id)
|
|
243
|
+
finally:
|
|
244
|
+
client.close()
|
|
245
|
+
|
|
246
|
+
if output_json:
|
|
247
|
+
import json
|
|
248
|
+
typer.echo(json.dumps(branches, indent=2, default=str))
|
|
249
|
+
return
|
|
250
|
+
|
|
251
|
+
if not branches:
|
|
252
|
+
typer.echo("No branches indexed.")
|
|
253
|
+
return
|
|
254
|
+
|
|
255
|
+
table = Table(title=f"Branches for {project_id}")
|
|
256
|
+
table.add_column("Branch", style="cyan")
|
|
257
|
+
table.add_column("Status")
|
|
258
|
+
table.add_column("Symbols")
|
|
259
|
+
table.add_column("Last Indexed")
|
|
260
|
+
for b in branches:
|
|
261
|
+
table.add_row(
|
|
262
|
+
b.get("branch", ""),
|
|
263
|
+
b.get("status", ""),
|
|
264
|
+
str(b.get("symbol_count", "")),
|
|
265
|
+
b.get("last_indexed", ""),
|
|
266
|
+
)
|
|
267
|
+
_console().print(table)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
@app.command("index")
|
|
271
|
+
def index_cmd(
|
|
272
|
+
project: Optional[str] = typer.Option(None, "--project", "-p", help="Project ID"),
|
|
273
|
+
branch: Optional[str] = typer.Option(None, "--branch", "-b", help="Branch to index"),
|
|
274
|
+
mode: str = typer.Option("auto", "--mode", help="Index mode: full, incremental, auto"),
|
|
275
|
+
wait: bool = typer.Option(False, "--wait", "-w", help="Wait for indexing to complete"),
|
|
276
|
+
) -> None:
|
|
277
|
+
"""Trigger indexing on the remote server."""
|
|
278
|
+
project_id = _resolve_project(project)
|
|
279
|
+
branch_name = _resolve_branch(branch)
|
|
280
|
+
client = _get_client()
|
|
281
|
+
try:
|
|
282
|
+
result = client.index_branch(project_id, branch_name, mode=mode)
|
|
283
|
+
job_id = result.get("job_id")
|
|
284
|
+
typer.echo(f"Indexing started: job_id={job_id}")
|
|
285
|
+
|
|
286
|
+
if wait and job_id is not None:
|
|
287
|
+
typer.echo("Waiting for completion...")
|
|
288
|
+
job = client.poll_job(job_id)
|
|
289
|
+
status = job.get("status", "unknown")
|
|
290
|
+
typer.echo(f"Job {job_id} finished: {status}")
|
|
291
|
+
if status == "failed":
|
|
292
|
+
typer.echo(f" reason: {job.get('failure_reason', 'unknown')}", err=True)
|
|
293
|
+
finally:
|
|
294
|
+
client.close()
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
@app.command("delete")
|
|
298
|
+
def delete_cmd(
|
|
299
|
+
project: Optional[str] = typer.Option(None, "--project", "-p", help="Project ID"),
|
|
300
|
+
branch: Optional[str] = typer.Option(None, "--branch", "-b",
|
|
301
|
+
help="Branch name — if given, deletes only that branch index (not the whole project)"),
|
|
302
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt"),
|
|
303
|
+
) -> None:
|
|
304
|
+
"""Delete a project or a single branch index from the server."""
|
|
305
|
+
client = _get_client()
|
|
306
|
+
project_id = _resolve_project(project)
|
|
307
|
+
try:
|
|
308
|
+
if branch:
|
|
309
|
+
if not yes:
|
|
310
|
+
typer.confirm(
|
|
311
|
+
f"Delete branch index '{branch}' in project '{project_id}'?", abort=True
|
|
312
|
+
)
|
|
313
|
+
result = client.delete_branch_index(project_id, branch)
|
|
314
|
+
typer.echo(f"Branch index '{branch}' deleted from project '{project_id}'.")
|
|
315
|
+
else:
|
|
316
|
+
if not yes:
|
|
317
|
+
typer.confirm(
|
|
318
|
+
f"Delete project '{project_id}' and ALL its indexes? This cannot be undone.",
|
|
319
|
+
abort=True,
|
|
320
|
+
)
|
|
321
|
+
result = client.delete_project(project_id)
|
|
322
|
+
typer.echo(f"Project '{project_id}' deleted.")
|
|
323
|
+
finally:
|
|
324
|
+
client.close()
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
@app.command("status")
|
|
328
|
+
def status_cmd(
|
|
329
|
+
job_id: Optional[int] = typer.Option(None, "--job-id", "-j", help="Job ID to check"),
|
|
330
|
+
project: Optional[str] = typer.Option(None, "--project", "-p", help="Project ID"),
|
|
331
|
+
branch: Optional[str] = typer.Option(None, "--branch", "-b", help="Branch name"),
|
|
332
|
+
output_json: bool = typer.Option(False, "--json", help="Output raw JSON"),
|
|
333
|
+
) -> None:
|
|
334
|
+
"""Check indexing job or branch index status."""
|
|
335
|
+
client = _get_client()
|
|
336
|
+
try:
|
|
337
|
+
if job_id is not None:
|
|
338
|
+
job = client.get_job(job_id)
|
|
339
|
+
if output_json:
|
|
340
|
+
import json
|
|
341
|
+
typer.echo(json.dumps(job, indent=2, default=str))
|
|
342
|
+
return
|
|
343
|
+
typer.echo(f"Job {job_id}:")
|
|
344
|
+
typer.echo(f" status: {job.get('status', '')}")
|
|
345
|
+
typer.echo(f" started: {job.get('started_at', '')}")
|
|
346
|
+
typer.echo(f" finished: {job.get('ended_at', '')}")
|
|
347
|
+
if job.get("failure_reason"):
|
|
348
|
+
typer.echo(f" reason: {job['failure_reason']}")
|
|
349
|
+
return
|
|
350
|
+
|
|
351
|
+
project_id = _resolve_project(project)
|
|
352
|
+
branch_name = _resolve_branch(branch)
|
|
353
|
+
info = client.get_branch_status(project_id, branch_name)
|
|
354
|
+
|
|
355
|
+
if output_json:
|
|
356
|
+
import json
|
|
357
|
+
typer.echo(json.dumps(info, indent=2, default=str))
|
|
358
|
+
return
|
|
359
|
+
|
|
360
|
+
status = info.get("status", "unknown")
|
|
361
|
+
index_mode = info.get("index_mode", "mount")
|
|
362
|
+
last_commit = info.get("last_indexed_commit") or ""
|
|
363
|
+
commit_msg = info.get("last_indexed_commit_message") or ""
|
|
364
|
+
indexed_at = info.get("last_indexed_at") or ""
|
|
365
|
+
symbol_count = info.get("symbol_count", 0)
|
|
366
|
+
edge_count = info.get("edge_count", 0)
|
|
367
|
+
parent_branch = info.get("parent_branch") or ""
|
|
368
|
+
parent_commit = info.get("parent_last_indexed_commit") or ""
|
|
369
|
+
active_job = info.get("active_job")
|
|
370
|
+
|
|
371
|
+
commit_display = f"{last_commit[:7]} \"{commit_msg}\"" if last_commit else "(none)"
|
|
372
|
+
parent_display = (
|
|
373
|
+
f"{parent_branch} (indexed at {parent_commit[:7]})"
|
|
374
|
+
if parent_branch and parent_commit
|
|
375
|
+
else parent_branch or "(none)"
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
col = 24
|
|
379
|
+
typer.echo(f"{'Branch:':<{col}}{branch_name}")
|
|
380
|
+
typer.echo(f"{'Status:':<{col}}{status}")
|
|
381
|
+
typer.echo(f"{'Index mode:':<{col}}{index_mode}")
|
|
382
|
+
typer.echo(f"{'Last indexed commit:':<{col}}{commit_display}")
|
|
383
|
+
typer.echo(f"{'Last indexed at:':<{col}}{indexed_at or '(never)'}")
|
|
384
|
+
typer.echo(f"{'Symbols:':<{col}}{symbol_count:,}")
|
|
385
|
+
typer.echo(f"{'Edges:':<{col}}{edge_count:,}")
|
|
386
|
+
typer.echo(f"{'Parent branch:':<{col}}{parent_display}")
|
|
387
|
+
|
|
388
|
+
if active_job:
|
|
389
|
+
jid = active_job.get("id", "?")
|
|
390
|
+
jstatus = active_job.get("status", "")
|
|
391
|
+
phase = active_job.get("current_phase", "")
|
|
392
|
+
pct = active_job.get("progress_pct", 0)
|
|
393
|
+
typer.echo(f"\nActive job #{jid}: {jstatus} phase={phase} {pct}%")
|
|
394
|
+
|
|
395
|
+
if status == "failed":
|
|
396
|
+
typer.echo("\nWarning: last indexing run failed.", err=True)
|
|
397
|
+
elif status == "stale":
|
|
398
|
+
typer.echo("\nNote: index may be out of date — run `ravmem analyze` to refresh.")
|
|
399
|
+
finally:
|
|
400
|
+
client.close()
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
@app.command("sync")
|
|
404
|
+
def sync_cmd(
|
|
405
|
+
project: Optional[str] = typer.Option(None, "--project", "-p", help="Project ID"),
|
|
406
|
+
) -> None:
|
|
407
|
+
"""Fetch the latest remote commits into the server-side workspace (clone-mode only).
|
|
408
|
+
|
|
409
|
+
This pulls new commits from the remote without re-indexing.
|
|
410
|
+
Run `ravmem analyze --project <id>` afterwards to index the new changes.
|
|
411
|
+
"""
|
|
412
|
+
client = _get_client()
|
|
413
|
+
try:
|
|
414
|
+
project_id = _resolve_project(project)
|
|
415
|
+
typer.echo(f"Syncing workspace for project '{project_id}'…")
|
|
416
|
+
result = client.sync_workspace(project_id)
|
|
417
|
+
|
|
418
|
+
duration_s = result.get("duration_ms", 0) / 1000
|
|
419
|
+
fetched_branches = result.get("fetched_branches", [])
|
|
420
|
+
fetched_commits = result.get("fetched_commits", 0)
|
|
421
|
+
head = result.get("workspace_head", "")
|
|
422
|
+
|
|
423
|
+
head_display = f" (workspace @ {head})" if head else ""
|
|
424
|
+
commits_display = f"{fetched_commits} commit{'s' if fetched_commits != 1 else ''} fetched"
|
|
425
|
+
branches_display = (
|
|
426
|
+
f", {len(fetched_branches)} new branch{'es' if len(fetched_branches) != 1 else ''}"
|
|
427
|
+
f" ({', '.join(fetched_branches)})"
|
|
428
|
+
if fetched_branches else ""
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
typer.echo(
|
|
432
|
+
f"✓ Synced in {duration_s:.1f}s — {commits_display}"
|
|
433
|
+
f"{branches_display}{head_display}"
|
|
434
|
+
)
|
|
435
|
+
if fetched_commits > 0 or fetched_branches:
|
|
436
|
+
typer.echo(
|
|
437
|
+
" Run `ravmem analyze --project <id>` to index the new changes."
|
|
438
|
+
)
|
|
439
|
+
else:
|
|
440
|
+
typer.echo(" Already up to date.")
|
|
441
|
+
except Exception as exc:
|
|
442
|
+
typer.echo(f"✗ Sync failed: {exc}", err=True)
|
|
443
|
+
raise typer.Exit(1)
|
|
444
|
+
finally:
|
|
445
|
+
client.close()
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
@app.command("reset-workspace")
|
|
449
|
+
def reset_workspace_cmd(
|
|
450
|
+
project: Optional[str] = typer.Option(None, "--project", "-p", help="Project ID"),
|
|
451
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt"),
|
|
452
|
+
) -> None:
|
|
453
|
+
"""Delete the server-side workspace and reset all branch index records.
|
|
454
|
+
|
|
455
|
+
Valid for clone and upload mode projects. The workspace directory is
|
|
456
|
+
removed and all branch statuses are set to 'empty', so the next
|
|
457
|
+
`ravmem analyze` or clone will start from scratch.
|
|
458
|
+
|
|
459
|
+
Mount-mode projects cannot be reset — their workspace is managed externally.
|
|
460
|
+
"""
|
|
461
|
+
client = _get_client()
|
|
462
|
+
try:
|
|
463
|
+
project_id = _resolve_project(project)
|
|
464
|
+
if not yes:
|
|
465
|
+
typer.confirm(
|
|
466
|
+
f"Reset workspace for project '{project_id}'? "
|
|
467
|
+
"This removes the server-side checkout and clears all branch indexes.",
|
|
468
|
+
abort=True,
|
|
469
|
+
)
|
|
470
|
+
result = client.reset_workspace(project_id)
|
|
471
|
+
|
|
472
|
+
branches_cleared = result.get("branches_cleared", 0)
|
|
473
|
+
removed = result.get("removed_workspace", False)
|
|
474
|
+
typer.echo(f"✓ Workspace reset for project '{project_id}'.")
|
|
475
|
+
if removed:
|
|
476
|
+
typer.echo(" Workspace directory removed.")
|
|
477
|
+
typer.echo(f" {branches_cleared} branch record{'s' if branches_cleared != 1 else ''} cleared.")
|
|
478
|
+
typer.echo(
|
|
479
|
+
" Next: run `ravmem analyze --project <id>` (upload) or "
|
|
480
|
+
"`ravmem analyze --project <id>` (clone) to re-index."
|
|
481
|
+
)
|
|
482
|
+
except typer.Abort:
|
|
483
|
+
typer.echo("Aborted.")
|
|
484
|
+
except Exception as exc:
|
|
485
|
+
typer.echo(f"✗ Reset failed: {exc}", err=True)
|
|
486
|
+
raise typer.Exit(1)
|
|
487
|
+
finally:
|
|
488
|
+
client.close()
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
@app.command("set-credentials")
|
|
492
|
+
def set_credentials_cmd(
|
|
493
|
+
project: Optional[str] = typer.Option(None, "--project", "-p", help="Project ID"),
|
|
494
|
+
git_username: Optional[str] = typer.Option(None, "--git-username", help="Git username"),
|
|
495
|
+
git_token: Optional[str] = typer.Option(
|
|
496
|
+
None, "--git-token", help="Git token/password (stored encrypted on server)",
|
|
497
|
+
hide_input=True,
|
|
498
|
+
),
|
|
499
|
+
) -> None:
|
|
500
|
+
"""Store or replace encrypted git credentials for a clone-mode project.
|
|
501
|
+
|
|
502
|
+
Credentials are encrypted server-side (requires RAVMEM_SECRET_KEY on the server).
|
|
503
|
+
Use this to update credentials after rotating a token without re-registering the project.
|
|
504
|
+
"""
|
|
505
|
+
project_id = _resolve_project(project)
|
|
506
|
+
if not git_username:
|
|
507
|
+
git_username = typer.prompt("Git username")
|
|
508
|
+
if not git_token:
|
|
509
|
+
git_token = typer.prompt("Git token/password", hide_input=True)
|
|
510
|
+
client = _get_client()
|
|
511
|
+
try:
|
|
512
|
+
client.set_credentials(project_id, git_username, git_token)
|
|
513
|
+
typer.echo(f"✓ Credentials stored for project '{project_id}'.")
|
|
514
|
+
typer.echo(" Token is encrypted at rest. Run `ravmem analyze` to use the updated credentials.")
|
|
515
|
+
except Exception as exc:
|
|
516
|
+
typer.echo(f"✗ Failed to store credentials: {exc}", err=True)
|
|
517
|
+
raise typer.Exit(1)
|
|
518
|
+
finally:
|
|
519
|
+
client.close()
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
@app.command("delete-credentials")
|
|
523
|
+
def delete_credentials_cmd(
|
|
524
|
+
project: Optional[str] = typer.Option(None, "--project", "-p", help="Project ID"),
|
|
525
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt"),
|
|
526
|
+
) -> None:
|
|
527
|
+
"""Remove stored git credentials from a clone-mode project.
|
|
528
|
+
|
|
529
|
+
The project will revert to unauthenticated cloning (public repos only).
|
|
530
|
+
"""
|
|
531
|
+
project_id = _resolve_project(project)
|
|
532
|
+
if not yes:
|
|
533
|
+
typer.confirm(
|
|
534
|
+
f"Remove stored credentials for project '{project_id}'?", abort=True
|
|
535
|
+
)
|
|
536
|
+
client = _get_client()
|
|
537
|
+
try:
|
|
538
|
+
result = client.delete_credentials(project_id)
|
|
539
|
+
if result.get("deleted"):
|
|
540
|
+
typer.echo(f"✓ Credentials deleted for project '{project_id}'.")
|
|
541
|
+
else:
|
|
542
|
+
typer.echo(f" No credentials were stored for project '{project_id}'.")
|
|
543
|
+
except typer.Abort:
|
|
544
|
+
typer.echo("Aborted.")
|
|
545
|
+
except Exception as exc:
|
|
546
|
+
typer.echo(f"✗ Failed to delete credentials: {exc}", err=True)
|
|
547
|
+
raise typer.Exit(1)
|
|
548
|
+
finally:
|
|
549
|
+
client.close()
|