nexus-cli 0.3.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.
- nexus/__init__.py +8 -0
- nexus/cli.py +1914 -0
- nexus/integrations/__init__.py +0 -0
- nexus/knowledge/__init__.py +13 -0
- nexus/knowledge/search.py +233 -0
- nexus/knowledge/vault.py +662 -0
- nexus/research/__init__.py +12 -0
- nexus/research/pdf.py +497 -0
- nexus/research/zotero.py +521 -0
- nexus/teaching/__init__.py +14 -0
- nexus/teaching/courses.py +388 -0
- nexus/teaching/quarto.py +385 -0
- nexus/utils/__init__.py +0 -0
- nexus/utils/config.py +157 -0
- nexus/writing/__init__.py +12 -0
- nexus/writing/bibliography.py +339 -0
- nexus/writing/manuscript.py +397 -0
- nexus_cli-0.3.0.dist-info/METADATA +369 -0
- nexus_cli-0.3.0.dist-info/RECORD +21 -0
- nexus_cli-0.3.0.dist-info/WHEEL +4 -0
- nexus_cli-0.3.0.dist-info/entry_points.txt +2 -0
nexus/teaching/quarto.py
ADDED
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
"""Quarto integration for Nexus CLI."""
|
|
2
|
+
|
|
3
|
+
import shutil
|
|
4
|
+
import subprocess
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import yaml
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class QuartoProject:
|
|
13
|
+
"""A Quarto project."""
|
|
14
|
+
|
|
15
|
+
path: str
|
|
16
|
+
name: str
|
|
17
|
+
project_type: str = "default"
|
|
18
|
+
title: str = ""
|
|
19
|
+
output_dir: str = ""
|
|
20
|
+
formats: list[str] = None
|
|
21
|
+
|
|
22
|
+
def __post_init__(self):
|
|
23
|
+
if self.formats is None:
|
|
24
|
+
self.formats = []
|
|
25
|
+
|
|
26
|
+
def to_dict(self) -> dict:
|
|
27
|
+
"""Convert to dictionary."""
|
|
28
|
+
return {
|
|
29
|
+
"path": self.path,
|
|
30
|
+
"name": self.name,
|
|
31
|
+
"project_type": self.project_type,
|
|
32
|
+
"title": self.title,
|
|
33
|
+
"output_dir": self.output_dir,
|
|
34
|
+
"formats": self.formats,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class QuartoBuildResult:
|
|
40
|
+
"""Result of a Quarto build."""
|
|
41
|
+
|
|
42
|
+
success: bool
|
|
43
|
+
output_path: str = ""
|
|
44
|
+
format: str = ""
|
|
45
|
+
error: str = ""
|
|
46
|
+
duration_seconds: float = 0.0
|
|
47
|
+
|
|
48
|
+
def to_dict(self) -> dict:
|
|
49
|
+
"""Convert to dictionary."""
|
|
50
|
+
return {
|
|
51
|
+
"success": self.success,
|
|
52
|
+
"output_path": self.output_path,
|
|
53
|
+
"format": self.format,
|
|
54
|
+
"error": self.error,
|
|
55
|
+
"duration_seconds": self.duration_seconds,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class QuartoManager:
|
|
60
|
+
"""Manage Quarto projects and builds."""
|
|
61
|
+
|
|
62
|
+
def __init__(self):
|
|
63
|
+
"""Initialize Quarto manager."""
|
|
64
|
+
self._quarto_path = shutil.which("quarto")
|
|
65
|
+
|
|
66
|
+
def available(self) -> bool:
|
|
67
|
+
"""Check if Quarto is available."""
|
|
68
|
+
return self._quarto_path is not None
|
|
69
|
+
|
|
70
|
+
def version(self) -> str | None:
|
|
71
|
+
"""Get Quarto version."""
|
|
72
|
+
if not self.available():
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
result = subprocess.run(
|
|
77
|
+
[self._quarto_path, "--version"],
|
|
78
|
+
capture_output=True,
|
|
79
|
+
text=True,
|
|
80
|
+
timeout=10,
|
|
81
|
+
)
|
|
82
|
+
return result.stdout.strip()
|
|
83
|
+
except Exception:
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
def is_quarto_project(self, path: Path) -> bool:
|
|
87
|
+
"""Check if a directory is a Quarto project."""
|
|
88
|
+
path = Path(path).expanduser()
|
|
89
|
+
return (path / "_quarto.yml").exists() or (path / "_quarto.yaml").exists()
|
|
90
|
+
|
|
91
|
+
def load_project(self, path: Path) -> QuartoProject | None:
|
|
92
|
+
"""Load a Quarto project configuration."""
|
|
93
|
+
path = Path(path).expanduser()
|
|
94
|
+
|
|
95
|
+
# Find config file
|
|
96
|
+
config_file = None
|
|
97
|
+
for name in ["_quarto.yml", "_quarto.yaml"]:
|
|
98
|
+
if (path / name).exists():
|
|
99
|
+
config_file = path / name
|
|
100
|
+
break
|
|
101
|
+
|
|
102
|
+
if not config_file:
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
try:
|
|
106
|
+
with open(config_file) as f:
|
|
107
|
+
data = yaml.safe_load(f) or {}
|
|
108
|
+
except Exception:
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
project = data.get("project", {})
|
|
112
|
+
book = data.get("book", {})
|
|
113
|
+
website = data.get("website", {})
|
|
114
|
+
|
|
115
|
+
# Get formats
|
|
116
|
+
formats = []
|
|
117
|
+
if "format" in data:
|
|
118
|
+
fmt = data["format"]
|
|
119
|
+
if isinstance(fmt, dict):
|
|
120
|
+
formats = list(fmt.keys())
|
|
121
|
+
elif isinstance(fmt, str):
|
|
122
|
+
formats = [fmt]
|
|
123
|
+
|
|
124
|
+
return QuartoProject(
|
|
125
|
+
path=str(path),
|
|
126
|
+
name=path.name,
|
|
127
|
+
project_type=project.get("type", "default"),
|
|
128
|
+
title=book.get("title") or website.get("title") or project.get("title", ""),
|
|
129
|
+
output_dir=project.get("output-dir", "_site"),
|
|
130
|
+
formats=formats,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
def render(
|
|
134
|
+
self,
|
|
135
|
+
path: Path,
|
|
136
|
+
output_format: str | None = None,
|
|
137
|
+
to_file: str | None = None,
|
|
138
|
+
) -> QuartoBuildResult:
|
|
139
|
+
"""Render a Quarto document or project.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
path: Path to .qmd file or project directory
|
|
143
|
+
output_format: Output format (html, pdf, revealjs, etc.)
|
|
144
|
+
to_file: Output filename
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
QuartoBuildResult with success status
|
|
148
|
+
"""
|
|
149
|
+
if not self.available():
|
|
150
|
+
return QuartoBuildResult(
|
|
151
|
+
success=False,
|
|
152
|
+
error="Quarto not installed. Run: brew install quarto",
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
path = Path(path).expanduser()
|
|
156
|
+
if not path.exists():
|
|
157
|
+
return QuartoBuildResult(
|
|
158
|
+
success=False,
|
|
159
|
+
error=f"Path not found: {path}",
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
import time
|
|
163
|
+
|
|
164
|
+
start = time.time()
|
|
165
|
+
|
|
166
|
+
# Build command
|
|
167
|
+
cmd = [self._quarto_path, "render", str(path)]
|
|
168
|
+
|
|
169
|
+
if output_format:
|
|
170
|
+
cmd.extend(["--to", output_format])
|
|
171
|
+
|
|
172
|
+
if to_file:
|
|
173
|
+
cmd.extend(["--output", to_file])
|
|
174
|
+
|
|
175
|
+
try:
|
|
176
|
+
result = subprocess.run(
|
|
177
|
+
cmd,
|
|
178
|
+
capture_output=True,
|
|
179
|
+
text=True,
|
|
180
|
+
timeout=300, # 5 minute timeout
|
|
181
|
+
cwd=path.parent if path.is_file() else path,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
duration = time.time() - start
|
|
185
|
+
|
|
186
|
+
if result.returncode == 0:
|
|
187
|
+
# Try to find output file
|
|
188
|
+
output_path = ""
|
|
189
|
+
if path.is_file():
|
|
190
|
+
if output_format == "pdf":
|
|
191
|
+
output_path = str(path.with_suffix(".pdf"))
|
|
192
|
+
elif output_format in ("html", "revealjs"):
|
|
193
|
+
output_path = str(path.with_suffix(".html"))
|
|
194
|
+
else:
|
|
195
|
+
output_path = str(path.parent)
|
|
196
|
+
else:
|
|
197
|
+
project = self.load_project(path)
|
|
198
|
+
if project:
|
|
199
|
+
output_path = str(path / project.output_dir)
|
|
200
|
+
|
|
201
|
+
return QuartoBuildResult(
|
|
202
|
+
success=True,
|
|
203
|
+
output_path=output_path,
|
|
204
|
+
format=output_format or "default",
|
|
205
|
+
duration_seconds=duration,
|
|
206
|
+
)
|
|
207
|
+
else:
|
|
208
|
+
return QuartoBuildResult(
|
|
209
|
+
success=False,
|
|
210
|
+
error=result.stderr or result.stdout,
|
|
211
|
+
duration_seconds=duration,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
except subprocess.TimeoutExpired:
|
|
215
|
+
return QuartoBuildResult(
|
|
216
|
+
success=False,
|
|
217
|
+
error="Build timed out after 5 minutes",
|
|
218
|
+
)
|
|
219
|
+
except Exception as e:
|
|
220
|
+
return QuartoBuildResult(
|
|
221
|
+
success=False,
|
|
222
|
+
error=str(e),
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
def preview(self, path: Path, port: int = 4200) -> dict:
|
|
226
|
+
"""Start Quarto preview server.
|
|
227
|
+
|
|
228
|
+
Note: This starts a background process. Returns info about the server.
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
path: Path to .qmd file or project directory
|
|
232
|
+
port: Port number for preview server
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
Dictionary with server info
|
|
236
|
+
"""
|
|
237
|
+
if not self.available():
|
|
238
|
+
return {
|
|
239
|
+
"success": False,
|
|
240
|
+
"error": "Quarto not installed. Run: brew install quarto",
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
path = Path(path).expanduser()
|
|
244
|
+
if not path.exists():
|
|
245
|
+
return {
|
|
246
|
+
"success": False,
|
|
247
|
+
"error": f"Path not found: {path}",
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
cmd = [self._quarto_path, "preview", str(path), "--port", str(port)]
|
|
251
|
+
|
|
252
|
+
try:
|
|
253
|
+
# Start in background (non-blocking)
|
|
254
|
+
process = subprocess.Popen(
|
|
255
|
+
cmd,
|
|
256
|
+
stdout=subprocess.PIPE,
|
|
257
|
+
stderr=subprocess.PIPE,
|
|
258
|
+
cwd=path.parent if path.is_file() else path,
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
"success": True,
|
|
263
|
+
"pid": process.pid,
|
|
264
|
+
"url": f"http://localhost:{port}",
|
|
265
|
+
"command": " ".join(cmd),
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
except Exception as e:
|
|
269
|
+
return {
|
|
270
|
+
"success": False,
|
|
271
|
+
"error": str(e),
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
def list_formats(self, path: Path) -> list[str]:
|
|
275
|
+
"""List available output formats for a Quarto project."""
|
|
276
|
+
project = self.load_project(path)
|
|
277
|
+
if project:
|
|
278
|
+
return project.formats
|
|
279
|
+
|
|
280
|
+
# Default formats if not a project
|
|
281
|
+
return ["html", "pdf", "docx"]
|
|
282
|
+
|
|
283
|
+
def check_dependencies(self, path: Path) -> dict:
|
|
284
|
+
"""Check if all dependencies are available for a project.
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
path: Path to project
|
|
288
|
+
|
|
289
|
+
Returns:
|
|
290
|
+
Dictionary with dependency status
|
|
291
|
+
"""
|
|
292
|
+
path = Path(path).expanduser()
|
|
293
|
+
deps = {
|
|
294
|
+
"quarto": self.available(),
|
|
295
|
+
"quarto_version": self.version(),
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
# Check for format-specific dependencies
|
|
299
|
+
project = self.load_project(path)
|
|
300
|
+
if project:
|
|
301
|
+
# PDF requires tinytex or latex
|
|
302
|
+
if "pdf" in project.formats or "beamer" in project.formats:
|
|
303
|
+
deps["latex"] = shutil.which("pdflatex") is not None
|
|
304
|
+
deps["tinytex"] = shutil.which("tlmgr") is not None
|
|
305
|
+
|
|
306
|
+
# Typst requires typst
|
|
307
|
+
if "typst" in project.formats:
|
|
308
|
+
deps["typst"] = shutil.which("typst") is not None
|
|
309
|
+
|
|
310
|
+
return deps
|
|
311
|
+
|
|
312
|
+
def clean(self, path: Path) -> dict:
|
|
313
|
+
"""Clean Quarto build artifacts.
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
path: Path to project
|
|
317
|
+
|
|
318
|
+
Returns:
|
|
319
|
+
Dictionary with cleaned items
|
|
320
|
+
"""
|
|
321
|
+
path = Path(path).expanduser()
|
|
322
|
+
cleaned = []
|
|
323
|
+
|
|
324
|
+
# Common build directories to clean
|
|
325
|
+
build_dirs = ["_site", "_book", "_freeze", "_output"]
|
|
326
|
+
|
|
327
|
+
for dirname in build_dirs:
|
|
328
|
+
dir_path = path / dirname
|
|
329
|
+
if dir_path.exists() and dir_path.is_dir():
|
|
330
|
+
try:
|
|
331
|
+
shutil.rmtree(dir_path)
|
|
332
|
+
cleaned.append(str(dir_path))
|
|
333
|
+
except Exception:
|
|
334
|
+
pass
|
|
335
|
+
|
|
336
|
+
# Clean .quarto directory
|
|
337
|
+
quarto_dir = path / ".quarto"
|
|
338
|
+
if quarto_dir.exists():
|
|
339
|
+
try:
|
|
340
|
+
shutil.rmtree(quarto_dir)
|
|
341
|
+
cleaned.append(str(quarto_dir))
|
|
342
|
+
except Exception:
|
|
343
|
+
pass
|
|
344
|
+
|
|
345
|
+
return {
|
|
346
|
+
"success": True,
|
|
347
|
+
"cleaned": cleaned,
|
|
348
|
+
"count": len(cleaned),
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
def export(
|
|
352
|
+
self,
|
|
353
|
+
path: Path,
|
|
354
|
+
output_format: str,
|
|
355
|
+
output_dir: Path | None = None,
|
|
356
|
+
) -> QuartoBuildResult:
|
|
357
|
+
"""Export a document to a specific format.
|
|
358
|
+
|
|
359
|
+
Wrapper around render with better output handling.
|
|
360
|
+
|
|
361
|
+
Args:
|
|
362
|
+
path: Path to .qmd file
|
|
363
|
+
output_format: Target format
|
|
364
|
+
output_dir: Output directory (optional)
|
|
365
|
+
|
|
366
|
+
Returns:
|
|
367
|
+
QuartoBuildResult
|
|
368
|
+
"""
|
|
369
|
+
path = Path(path).expanduser()
|
|
370
|
+
|
|
371
|
+
if output_dir:
|
|
372
|
+
output_dir = Path(output_dir).expanduser()
|
|
373
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
374
|
+
|
|
375
|
+
result = self.render(path, output_format=output_format)
|
|
376
|
+
|
|
377
|
+
# Move output to target directory if specified
|
|
378
|
+
if result.success and output_dir and result.output_path:
|
|
379
|
+
output_path = Path(result.output_path)
|
|
380
|
+
if output_path.exists() and output_path.is_file():
|
|
381
|
+
target = output_dir / output_path.name
|
|
382
|
+
shutil.copy2(output_path, target)
|
|
383
|
+
result.output_path = str(target)
|
|
384
|
+
|
|
385
|
+
return result
|
nexus/utils/__init__.py
ADDED
|
File without changes
|
nexus/utils/config.py
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""Configuration management for Nexus CLI."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import yaml
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
from pydantic_settings import BaseSettings
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ZoteroConfig(BaseModel):
|
|
11
|
+
"""Zotero configuration."""
|
|
12
|
+
|
|
13
|
+
database: str = "~/Zotero/zotero.sqlite"
|
|
14
|
+
storage: str = "~/Zotero/storage"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class VaultConfig(BaseModel):
|
|
18
|
+
"""Obsidian vault configuration."""
|
|
19
|
+
|
|
20
|
+
path: str = "~/Obsidian/Nexus"
|
|
21
|
+
templates: str = "~/Obsidian/Nexus/_SYSTEM/templates"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class PDFConfig(BaseModel):
|
|
25
|
+
"""PDF configuration."""
|
|
26
|
+
|
|
27
|
+
directories: list[str] = [
|
|
28
|
+
"~/Documents/Research/PDFs",
|
|
29
|
+
"~/Documents/Teaching/PDFs",
|
|
30
|
+
]
|
|
31
|
+
index: str = "~/.nexus/pdf-index.db"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class TeachingConfig(BaseModel):
|
|
35
|
+
"""Teaching configuration."""
|
|
36
|
+
|
|
37
|
+
courses_dir: str = "~/projects/teaching"
|
|
38
|
+
materials_dir: str = "~/Documents/Teaching"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class WritingConfig(BaseModel):
|
|
42
|
+
"""Writing configuration."""
|
|
43
|
+
|
|
44
|
+
manuscripts_dir: str = "~/projects/research"
|
|
45
|
+
templates_dir: str = "~/.nexus/templates"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class RConfig(BaseModel):
|
|
49
|
+
"""R configuration."""
|
|
50
|
+
|
|
51
|
+
executable: str = "/usr/local/bin/R"
|
|
52
|
+
packages_dir: str = "~/projects/r-packages"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class OutputConfig(BaseModel):
|
|
56
|
+
"""Output configuration."""
|
|
57
|
+
|
|
58
|
+
format: str = "rich" # rich, json, plain
|
|
59
|
+
color: bool = True
|
|
60
|
+
pager: bool = False
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class IntegrationsConfig(BaseModel):
|
|
64
|
+
"""Integration configuration."""
|
|
65
|
+
|
|
66
|
+
aiterm: bool = True
|
|
67
|
+
obsidian: bool = True
|
|
68
|
+
claude_plugin: bool = True
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class Config(BaseSettings):
|
|
72
|
+
"""Main Nexus configuration."""
|
|
73
|
+
|
|
74
|
+
zotero: ZoteroConfig = ZoteroConfig()
|
|
75
|
+
vault: VaultConfig = VaultConfig()
|
|
76
|
+
pdf: PDFConfig = PDFConfig()
|
|
77
|
+
teaching: TeachingConfig = TeachingConfig()
|
|
78
|
+
writing: WritingConfig = WritingConfig()
|
|
79
|
+
r: RConfig = RConfig()
|
|
80
|
+
output: OutputConfig = OutputConfig()
|
|
81
|
+
integrations: IntegrationsConfig = IntegrationsConfig()
|
|
82
|
+
|
|
83
|
+
class Config:
|
|
84
|
+
env_prefix = "NEXUS_"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def get_config_path() -> Path:
|
|
88
|
+
"""Get the configuration file path."""
|
|
89
|
+
return Path.home() / ".config" / "nexus" / "config.yaml"
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def load_config_from_file(path: Path) -> dict:
|
|
93
|
+
"""Load configuration from a YAML file."""
|
|
94
|
+
if not path.exists():
|
|
95
|
+
return {}
|
|
96
|
+
with open(path) as f:
|
|
97
|
+
return yaml.safe_load(f) or {}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def get_config() -> Config:
|
|
101
|
+
"""Get the current configuration, merging defaults with file config."""
|
|
102
|
+
config_path = get_config_path()
|
|
103
|
+
file_config = load_config_from_file(config_path)
|
|
104
|
+
|
|
105
|
+
# Create config with defaults, then update with file values
|
|
106
|
+
config = Config()
|
|
107
|
+
|
|
108
|
+
if file_config:
|
|
109
|
+
# Update nested configs
|
|
110
|
+
if "zotero" in file_config:
|
|
111
|
+
config.zotero = ZoteroConfig(**file_config["zotero"])
|
|
112
|
+
if "vault" in file_config:
|
|
113
|
+
config.vault = VaultConfig(**file_config["vault"])
|
|
114
|
+
if "pdf" in file_config:
|
|
115
|
+
config.pdf = PDFConfig(**file_config["pdf"])
|
|
116
|
+
if "teaching" in file_config:
|
|
117
|
+
config.teaching = TeachingConfig(**file_config["teaching"])
|
|
118
|
+
if "writing" in file_config:
|
|
119
|
+
config.writing = WritingConfig(**file_config["writing"])
|
|
120
|
+
if "r" in file_config:
|
|
121
|
+
config.r = RConfig(**file_config["r"])
|
|
122
|
+
if "output" in file_config:
|
|
123
|
+
config.output = OutputConfig(**file_config["output"])
|
|
124
|
+
if "integrations" in file_config:
|
|
125
|
+
config.integrations = IntegrationsConfig(**file_config["integrations"])
|
|
126
|
+
|
|
127
|
+
return config
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def save_config(config: Config, path: Path | None = None) -> None:
|
|
131
|
+
"""Save configuration to file."""
|
|
132
|
+
if path is None:
|
|
133
|
+
path = get_config_path()
|
|
134
|
+
|
|
135
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
136
|
+
|
|
137
|
+
config_dict = {
|
|
138
|
+
"zotero": config.zotero.model_dump(),
|
|
139
|
+
"vault": config.vault.model_dump(),
|
|
140
|
+
"pdf": config.pdf.model_dump(),
|
|
141
|
+
"teaching": config.teaching.model_dump(),
|
|
142
|
+
"writing": config.writing.model_dump(),
|
|
143
|
+
"r": config.r.model_dump(),
|
|
144
|
+
"output": config.output.model_dump(),
|
|
145
|
+
"integrations": config.integrations.model_dump(),
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
with open(path, "w") as f:
|
|
149
|
+
yaml.dump(config_dict, f, default_flow_style=False, sort_keys=False)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def init_config() -> Path:
|
|
153
|
+
"""Initialize default configuration file if it doesn't exist."""
|
|
154
|
+
config_path = get_config_path()
|
|
155
|
+
if not config_path.exists():
|
|
156
|
+
save_config(Config())
|
|
157
|
+
return config_path
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Writing domain - Manuscripts, bibliography, LaTeX."""
|
|
2
|
+
|
|
3
|
+
from nexus.writing.bibliography import BibEntry, BibliographyManager
|
|
4
|
+
from nexus.writing.manuscript import Manuscript, ManuscriptManager, ManuscriptStatus
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"Manuscript",
|
|
8
|
+
"ManuscriptManager",
|
|
9
|
+
"ManuscriptStatus",
|
|
10
|
+
"BibEntry",
|
|
11
|
+
"BibliographyManager",
|
|
12
|
+
]
|