lava-cmd 0.1.0__tar.gz
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.
- lava_cmd-0.1.0/.github/workflows/publish.yml +26 -0
- lava_cmd-0.1.0/.gitignore +20 -0
- lava_cmd-0.1.0/PKG-INFO +13 -0
- lava_cmd-0.1.0/lava/__init__.py +3 -0
- lava_cmd-0.1.0/lava/_app.py +17 -0
- lava_cmd-0.1.0/lava/_helpers.py +68 -0
- lava_cmd-0.1.0/lava/_tui_editor.py +54 -0
- lava_cmd-0.1.0/lava/commands/__init__.py +0 -0
- lava_cmd-0.1.0/lava/commands/config_cmds.py +310 -0
- lava_cmd-0.1.0/lava/commands/graph_cmds.py +307 -0
- lava_cmd-0.1.0/lava/commands/nav.py +218 -0
- lava_cmd-0.1.0/lava/commands/notes.py +526 -0
- lava_cmd-0.1.0/lava/commands/vault_info.py +227 -0
- lava_cmd-0.1.0/lava/config.py +134 -0
- lava_cmd-0.1.0/lava/editor.py +53 -0
- lava_cmd-0.1.0/lava/graph.py +133 -0
- lava_cmd-0.1.0/lava/main.py +29 -0
- lava_cmd-0.1.0/lava/search.py +77 -0
- lava_cmd-0.1.0/lava/ui.py +77 -0
- lava_cmd-0.1.0/lava/vault.py +199 -0
- lava_cmd-0.1.0/pyproject.toml +25 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
publish:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v4
|
|
13
|
+
|
|
14
|
+
- uses: actions/setup-python@v5
|
|
15
|
+
with:
|
|
16
|
+
python-version: "3.11"
|
|
17
|
+
|
|
18
|
+
- name: Build
|
|
19
|
+
run: |
|
|
20
|
+
pip install build
|
|
21
|
+
python -m build
|
|
22
|
+
|
|
23
|
+
- name: Publish
|
|
24
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
25
|
+
with:
|
|
26
|
+
password: ${{ secrets.PYPI_API_TOKEN }}
|
lava_cmd-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lava-cmd
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A CLI toolkit for interacting with Obsidian vaults
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Requires-Dist: networkx
|
|
7
|
+
Requires-Dist: platformdirs
|
|
8
|
+
Requires-Dist: python-frontmatter
|
|
9
|
+
Requires-Dist: rank-bm25
|
|
10
|
+
Requires-Dist: rich
|
|
11
|
+
Requires-Dist: textual
|
|
12
|
+
Requires-Dist: toml
|
|
13
|
+
Requires-Dist: typer
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""lava — app singleton, console, and shared constants."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
|
|
8
|
+
from lava import __version__ as _LAVA_VERSION
|
|
9
|
+
|
|
10
|
+
app = typer.Typer(
|
|
11
|
+
name="lava",
|
|
12
|
+
help="A CLI toolkit for interacting with Obsidian vaults.",
|
|
13
|
+
add_completion=False,
|
|
14
|
+
no_args_is_help=True,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
console = Console()
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""lava — shared helpers used across multiple command modules."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from rich.prompt import Prompt
|
|
8
|
+
|
|
9
|
+
from lava import config as cfg
|
|
10
|
+
from lava import vault as vlt
|
|
11
|
+
from lava import ui
|
|
12
|
+
from lava._app import console
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _cwd(vault_path: Path) -> Path:
|
|
16
|
+
"""Return the current working directory within the vault."""
|
|
17
|
+
rel = cfg.get_vault_cwd()
|
|
18
|
+
if not rel:
|
|
19
|
+
return vault_path
|
|
20
|
+
candidate = vault_path / rel
|
|
21
|
+
if candidate.exists() and candidate.is_dir():
|
|
22
|
+
return candidate
|
|
23
|
+
# cwd no longer valid (folder deleted), reset to root
|
|
24
|
+
cfg.set_vault_cwd("")
|
|
25
|
+
return vault_path
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _cwd_label(vault_path: Path) -> str:
|
|
29
|
+
cwd = _cwd(vault_path)
|
|
30
|
+
if cwd == vault_path:
|
|
31
|
+
return f"[bold orange1]{vault_path.name}[/bold orange1] [dim]/[/dim]"
|
|
32
|
+
rel = cwd.relative_to(vault_path)
|
|
33
|
+
parts = rel.parts
|
|
34
|
+
trail = "/".join(f"[orange1]{p}[/orange1]" for p in parts)
|
|
35
|
+
return f"[dim]{vault_path.name}/[/dim]{trail} [dim]/[/dim]"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _pick_move_destination(vault_path: Path, source: Path) -> str | None:
|
|
39
|
+
"""Interactive folder picker + optional rename."""
|
|
40
|
+
folders = vlt.list_folders(vault_path)
|
|
41
|
+
|
|
42
|
+
console.print(f"\n[bold orange1]Move:[/bold orange1] [white]{source.stem}[/white]\n")
|
|
43
|
+
console.print(f" [dim] 0.[/dim] [white]/ (vault root)[/white]")
|
|
44
|
+
for i, folder in enumerate(folders, 1):
|
|
45
|
+
rel = folder.relative_to(vault_path)
|
|
46
|
+
console.print(f" [dim]{i:2}.[/dim] [white]{rel}[/white]")
|
|
47
|
+
|
|
48
|
+
choice = Prompt.ask("\nDestination folder", default="0")
|
|
49
|
+
try:
|
|
50
|
+
idx = int(choice)
|
|
51
|
+
except ValueError:
|
|
52
|
+
ui.print_error("Invalid choice.")
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
if idx == 0:
|
|
56
|
+
folder_path = vault_path
|
|
57
|
+
elif 1 <= idx <= len(folders):
|
|
58
|
+
folder_path = folders[idx - 1]
|
|
59
|
+
else:
|
|
60
|
+
ui.print_error("Invalid choice.")
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
new_name = Prompt.ask("New name", default=source.stem)
|
|
64
|
+
|
|
65
|
+
rel_folder = folder_path.relative_to(vault_path)
|
|
66
|
+
if str(rel_folder) == ".":
|
|
67
|
+
return new_name
|
|
68
|
+
return str(rel_folder / new_name)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Textual-based TUI editor for lava."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from textual.app import App, ComposeResult
|
|
8
|
+
from textual.binding import Binding
|
|
9
|
+
from textual.widgets import Footer, Header, TextArea
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class LavaEditorApp(App):
|
|
13
|
+
"""A minimal TUI editor for editing vault notes."""
|
|
14
|
+
|
|
15
|
+
TITLE = "lava editor"
|
|
16
|
+
BINDINGS = [
|
|
17
|
+
Binding("ctrl+s", "save", "Save", show=True),
|
|
18
|
+
Binding("ctrl+q", "save_quit", "Save & Quit", show=True),
|
|
19
|
+
Binding("ctrl+c", "quit_no_save", "Quit without saving", show=True),
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
def __init__(self, path: Path) -> None:
|
|
23
|
+
super().__init__()
|
|
24
|
+
self._path = path
|
|
25
|
+
self._content = path.read_text(encoding="utf-8") if path.exists() else ""
|
|
26
|
+
self._saved = False
|
|
27
|
+
|
|
28
|
+
def compose(self) -> ComposeResult:
|
|
29
|
+
yield Header()
|
|
30
|
+
yield TextArea(self._content, id="editor", language="markdown")
|
|
31
|
+
yield Footer()
|
|
32
|
+
|
|
33
|
+
def on_mount(self) -> None:
|
|
34
|
+
self.title = f"lava — {self._path.name}"
|
|
35
|
+
self.query_one("#editor", TextArea).focus()
|
|
36
|
+
|
|
37
|
+
def action_save(self) -> None:
|
|
38
|
+
content = self.query_one("#editor", TextArea).text
|
|
39
|
+
self._path.write_text(content, encoding="utf-8")
|
|
40
|
+
self._saved = True
|
|
41
|
+
self.notify(f"Saved {self._path.name}", severity="information")
|
|
42
|
+
|
|
43
|
+
def action_save_quit(self) -> None:
|
|
44
|
+
self.action_save()
|
|
45
|
+
self.exit()
|
|
46
|
+
|
|
47
|
+
def action_quit_no_save(self) -> None:
|
|
48
|
+
self.exit()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def run_tui_editor(path: Path) -> None:
|
|
52
|
+
"""Launch the Textual TUI editor for a file."""
|
|
53
|
+
app = LavaEditorApp(path)
|
|
54
|
+
app.run()
|
|
File without changes
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
"""lava — Configuration commands: dir_app, config_app, version, uninstall."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import subprocess
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Annotated, Optional
|
|
9
|
+
|
|
10
|
+
import typer
|
|
11
|
+
from rich.prompt import Confirm, Prompt
|
|
12
|
+
from rich.table import Table
|
|
13
|
+
|
|
14
|
+
from lava import config as cfg
|
|
15
|
+
from lava import ui
|
|
16
|
+
from lava._app import app, console, _LAVA_VERSION
|
|
17
|
+
from lava.ui import C_PRIMARY
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
dir_app = typer.Typer(
|
|
21
|
+
name="dir",
|
|
22
|
+
help="Manage the active vault directory.",
|
|
23
|
+
invoke_without_command=True,
|
|
24
|
+
no_args_is_help=False,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
config_app = typer.Typer(
|
|
28
|
+
name="config",
|
|
29
|
+
help="View and edit lava configuration.",
|
|
30
|
+
invoke_without_command=True,
|
|
31
|
+
no_args_is_help=False,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dir_app.callback(invoke_without_command=True)
|
|
36
|
+
def dir_callback(
|
|
37
|
+
ctx: typer.Context,
|
|
38
|
+
history: Annotated[bool, typer.Option("--history", help="Show recent vaults")] = False,
|
|
39
|
+
list_vaults: Annotated[bool, typer.Option("--list", help="Show recent vaults (alias for --history)")] = False,
|
|
40
|
+
pick: Annotated[bool, typer.Option("--pick", help="Pick vault from history")] = False,
|
|
41
|
+
clear: Annotated[bool, typer.Option("--clear", help="Unset the active vault path")] = False,
|
|
42
|
+
) -> None:
|
|
43
|
+
"""Manage the active vault directory."""
|
|
44
|
+
if ctx.invoked_subcommand is not None:
|
|
45
|
+
return
|
|
46
|
+
|
|
47
|
+
if clear:
|
|
48
|
+
cfg.set_vault_path("")
|
|
49
|
+
ui.print_success("Vault path cleared.")
|
|
50
|
+
return
|
|
51
|
+
|
|
52
|
+
if history or list_vaults:
|
|
53
|
+
_dir_history()
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
if pick:
|
|
57
|
+
_dir_pick()
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
_dir_set_prompt()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _dir_set_prompt() -> None:
|
|
64
|
+
"""Interactively set the active vault path."""
|
|
65
|
+
current = cfg.get_vault_path()
|
|
66
|
+
if current:
|
|
67
|
+
console.print(f"[dim]Current vault:[/dim] [orange1]{current}[/orange1]")
|
|
68
|
+
else:
|
|
69
|
+
console.print("[dim]No vault set.[/dim]")
|
|
70
|
+
|
|
71
|
+
console.print(
|
|
72
|
+
f"\n [dim]Enter a path, or [/dim][bold white]f[/bold white][dim] to browse folders[/dim]"
|
|
73
|
+
)
|
|
74
|
+
raw = Prompt.ask("[orange1]Vault path[/orange1]", default="").strip()
|
|
75
|
+
|
|
76
|
+
if not raw:
|
|
77
|
+
return
|
|
78
|
+
|
|
79
|
+
if raw.lower() == "f":
|
|
80
|
+
location = _pick_fs_folder()
|
|
81
|
+
if location is None:
|
|
82
|
+
return
|
|
83
|
+
resolved = str(location)
|
|
84
|
+
else:
|
|
85
|
+
resolved = str(Path(raw).expanduser().resolve())
|
|
86
|
+
|
|
87
|
+
if not Path(resolved).exists():
|
|
88
|
+
ui.print_error(f"Path does not exist: {resolved}")
|
|
89
|
+
raise typer.Exit(1)
|
|
90
|
+
|
|
91
|
+
cfg.set_vault_path(resolved)
|
|
92
|
+
cfg.add_to_history(resolved)
|
|
93
|
+
ui.print_success(f"Vault set to: {resolved}")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _dir_history() -> None:
|
|
97
|
+
config = cfg.load_config()
|
|
98
|
+
history = config.get("vault", {}).get("history", [])
|
|
99
|
+
if not history:
|
|
100
|
+
console.print("[dim]No vault history.[/dim]")
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
table = Table(title="Recent Vaults", header_style="bold orange1")
|
|
104
|
+
table.add_column("#", style="dim", width=4)
|
|
105
|
+
table.add_column("Path", style="white")
|
|
106
|
+
current = cfg.get_vault_path()
|
|
107
|
+
for i, p in enumerate(history, 1):
|
|
108
|
+
marker = " [gold1](active)[/gold1]" if p == current else ""
|
|
109
|
+
table.add_row(str(i), p + marker)
|
|
110
|
+
console.print(table)
|
|
111
|
+
|
|
112
|
+
choice = Prompt.ask("Switch to vault # (or Enter to skip)", default="").strip()
|
|
113
|
+
if not choice:
|
|
114
|
+
return
|
|
115
|
+
try:
|
|
116
|
+
idx = int(choice) - 1
|
|
117
|
+
selected = history[idx]
|
|
118
|
+
except (ValueError, IndexError):
|
|
119
|
+
ui.print_error("Invalid selection.")
|
|
120
|
+
return
|
|
121
|
+
cfg.set_vault_path(selected)
|
|
122
|
+
cfg.add_to_history(selected)
|
|
123
|
+
ui.print_success(f"Vault set to: {selected}")
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _dir_pick() -> None:
|
|
127
|
+
config = cfg.load_config()
|
|
128
|
+
history = config.get("vault", {}).get("history", [])
|
|
129
|
+
if not history:
|
|
130
|
+
ui.print_error("No vault history to pick from.")
|
|
131
|
+
raise typer.Exit(1)
|
|
132
|
+
|
|
133
|
+
console.print("[bold orange1]Recent vaults:[/bold orange1]")
|
|
134
|
+
for i, p in enumerate(history, 1):
|
|
135
|
+
console.print(f" [dim]{i}.[/dim] {p}")
|
|
136
|
+
|
|
137
|
+
choice = Prompt.ask("Pick a vault number", default="1")
|
|
138
|
+
try:
|
|
139
|
+
idx = int(choice) - 1
|
|
140
|
+
selected = history[idx]
|
|
141
|
+
except (ValueError, IndexError):
|
|
142
|
+
ui.print_error("Invalid selection.")
|
|
143
|
+
raise typer.Exit(1)
|
|
144
|
+
|
|
145
|
+
cfg.set_vault_path(selected)
|
|
146
|
+
cfg.add_to_history(selected)
|
|
147
|
+
ui.print_success(f"Vault set to: {selected}")
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _pick_fs_folder() -> Path | None:
|
|
151
|
+
"""Navigate the real filesystem level-by-level (folders only) and return chosen path."""
|
|
152
|
+
current = Path.home()
|
|
153
|
+
|
|
154
|
+
while True:
|
|
155
|
+
subdirs = sorted(
|
|
156
|
+
[p for p in current.iterdir() if p.is_dir() and not p.name.startswith(".")],
|
|
157
|
+
key=lambda p: p.name.lower(),
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
console.print(f"\n[bold {C_PRIMARY}]{current}[/bold {C_PRIMARY}]\n")
|
|
161
|
+
console.print(f" [dim] 0.[/dim] [dim]✓ use this folder[/dim]")
|
|
162
|
+
if current != current.anchor:
|
|
163
|
+
console.print(f" [dim] b.[/dim] [dim].. (go up)[/dim]")
|
|
164
|
+
for i, d in enumerate(subdirs, 1):
|
|
165
|
+
console.print(f" [dim]{i:2}.[/dim] [orange1]{d.name}/[/orange1]")
|
|
166
|
+
console.print()
|
|
167
|
+
|
|
168
|
+
choice = Prompt.ask("Pick folder or #", default="").strip().lower()
|
|
169
|
+
if not choice:
|
|
170
|
+
return None
|
|
171
|
+
if choice == "0":
|
|
172
|
+
return current
|
|
173
|
+
if choice == "b" and current != Path(current.anchor):
|
|
174
|
+
current = current.parent
|
|
175
|
+
continue
|
|
176
|
+
try:
|
|
177
|
+
idx = int(choice)
|
|
178
|
+
if 1 <= idx <= len(subdirs):
|
|
179
|
+
current = subdirs[idx - 1]
|
|
180
|
+
else:
|
|
181
|
+
ui.print_error("Invalid choice.")
|
|
182
|
+
except ValueError:
|
|
183
|
+
ui.print_error("Invalid choice.")
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@dir_app.command("init")
|
|
187
|
+
def dir_init(
|
|
188
|
+
path: Annotated[Optional[str], typer.Argument(help="Path for the new vault. Omit to create in current directory.")] = None,
|
|
189
|
+
pick: Annotated[bool, typer.Option("--pick", help="Navigate filesystem to pick location")] = False,
|
|
190
|
+
) -> None:
|
|
191
|
+
"""Create a new vault directory and set it as the active vault."""
|
|
192
|
+
if pick:
|
|
193
|
+
location = _pick_fs_folder()
|
|
194
|
+
if location is None:
|
|
195
|
+
raise typer.Exit(0)
|
|
196
|
+
vault_name = Prompt.ask("[orange1]Vault name[/orange1]").strip()
|
|
197
|
+
if not vault_name:
|
|
198
|
+
raise typer.Exit(0)
|
|
199
|
+
resolved = location / vault_name
|
|
200
|
+
elif path:
|
|
201
|
+
resolved = Path(path).expanduser().resolve()
|
|
202
|
+
else:
|
|
203
|
+
vault_name = Prompt.ask("[orange1]Vault name[/orange1]").strip()
|
|
204
|
+
if not vault_name:
|
|
205
|
+
raise typer.Exit(0)
|
|
206
|
+
resolved = Path.cwd() / vault_name
|
|
207
|
+
|
|
208
|
+
if resolved.exists() and any(resolved.iterdir()):
|
|
209
|
+
ui.print_error(f"Directory already exists and is not empty: {resolved}")
|
|
210
|
+
raise typer.Exit(1)
|
|
211
|
+
|
|
212
|
+
resolved.mkdir(parents=True, exist_ok=True)
|
|
213
|
+
|
|
214
|
+
cfg.set_vault_path(str(resolved))
|
|
215
|
+
cfg.add_to_history(str(resolved))
|
|
216
|
+
|
|
217
|
+
console.print(f"\n[bold {C_PRIMARY}]Vault created:[/bold {C_PRIMARY}] {resolved}")
|
|
218
|
+
console.print(f"[dim]Active vault set. Run [white]lava new[/white] to get started.[/dim]")
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
@dir_app.command("set")
|
|
222
|
+
def dir_set(
|
|
223
|
+
path: Annotated[str, typer.Argument(help="Vault path, or 'current' for cwd")],
|
|
224
|
+
) -> None:
|
|
225
|
+
"""Set the active vault path."""
|
|
226
|
+
if path.lower() == "current":
|
|
227
|
+
resolved = str(Path.cwd())
|
|
228
|
+
else:
|
|
229
|
+
resolved = str(Path(path).expanduser().resolve())
|
|
230
|
+
|
|
231
|
+
if not Path(resolved).exists():
|
|
232
|
+
ui.print_error(f"Path does not exist: {resolved}")
|
|
233
|
+
raise typer.Exit(1)
|
|
234
|
+
|
|
235
|
+
cfg.set_vault_path(resolved)
|
|
236
|
+
cfg.add_to_history(resolved)
|
|
237
|
+
ui.print_success(f"Vault set to: {resolved}")
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
@config_app.callback(invoke_without_command=True)
|
|
241
|
+
def config_callback(
|
|
242
|
+
ctx: typer.Context,
|
|
243
|
+
edit: Annotated[bool, typer.Option("--edit", help="Open config in $EDITOR")] = False,
|
|
244
|
+
) -> None:
|
|
245
|
+
"""View or edit the lava configuration."""
|
|
246
|
+
if ctx.invoked_subcommand is not None:
|
|
247
|
+
return
|
|
248
|
+
|
|
249
|
+
config = cfg.load_config()
|
|
250
|
+
|
|
251
|
+
if edit:
|
|
252
|
+
config_path = cfg.get_config_path()
|
|
253
|
+
editor = config.get("editor", "") or os.environ.get("EDITOR", "")
|
|
254
|
+
if editor:
|
|
255
|
+
subprocess.run([editor, str(config_path)])
|
|
256
|
+
else:
|
|
257
|
+
console.print(f"[orange1]Config file:[/orange1] {config_path}")
|
|
258
|
+
console.print("[dim]Set $EDITOR or lava config set editor <editor> to open it.[/dim]")
|
|
259
|
+
return
|
|
260
|
+
|
|
261
|
+
ui.config_panel(config)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
@config_app.command("set")
|
|
265
|
+
def config_set(
|
|
266
|
+
key: Annotated[Optional[str], typer.Argument(help="Dotted config key (e.g. pagination, editor)")] = None,
|
|
267
|
+
value: Annotated[Optional[str], typer.Argument(help="Value to set")] = None,
|
|
268
|
+
link_folder: Annotated[bool, typer.Option("--link-folder", is_flag=True, help="Set the links folder to the current working directory.")] = False,
|
|
269
|
+
link_folder_path: Annotated[Optional[str], typer.Option("--link-folder-path", help="Set the links folder to a specific path.")] = None,
|
|
270
|
+
) -> None:
|
|
271
|
+
"""Set a config value, or configure the links folder."""
|
|
272
|
+
if link_folder_path is not None:
|
|
273
|
+
cfg.set_dotted_key("links_folder", link_folder_path)
|
|
274
|
+
ui.print_success(f"Links folder set to: {link_folder_path}")
|
|
275
|
+
return
|
|
276
|
+
|
|
277
|
+
if link_folder:
|
|
278
|
+
path_val = cfg.get_vault_cwd() or "."
|
|
279
|
+
cfg.set_dotted_key("links_folder", path_val)
|
|
280
|
+
ui.print_success(f"Links folder set to: {path_val}")
|
|
281
|
+
return
|
|
282
|
+
|
|
283
|
+
if key is None or value is None:
|
|
284
|
+
ui.print_error("Usage: lava config set <key> <value> or lava config set --link-folder")
|
|
285
|
+
raise typer.Exit(1)
|
|
286
|
+
|
|
287
|
+
try:
|
|
288
|
+
cfg.set_dotted_key(key, value)
|
|
289
|
+
ui.print_success(f"Config updated: {key} = {value}")
|
|
290
|
+
except Exception as e:
|
|
291
|
+
ui.print_error(str(e))
|
|
292
|
+
raise typer.Exit(1)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
@app.command("version", rich_help_panel="Configuration")
|
|
296
|
+
def cmd_version() -> None:
|
|
297
|
+
"""Show the installed lava version."""
|
|
298
|
+
console.print(f"lava [orange1]{_LAVA_VERSION}[/orange1]")
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
@app.command("uninstall", rich_help_panel="Other")
|
|
302
|
+
def cmd_uninstall() -> None:
|
|
303
|
+
"""Uninstall lava from the current Python environment."""
|
|
304
|
+
confirmed = Confirm.ask(
|
|
305
|
+
f"Uninstall [orange1]lava {_LAVA_VERSION}[/orange1] from this environment?",
|
|
306
|
+
default=False,
|
|
307
|
+
)
|
|
308
|
+
if not confirmed:
|
|
309
|
+
raise typer.Exit(0)
|
|
310
|
+
subprocess.run(["pip", "uninstall", "lava", "-y"], check=True)
|