sampler-cli 0.2.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.
- sampler_cli-0.2.0/LICENSE +21 -0
- sampler_cli-0.2.0/PKG-INFO +130 -0
- sampler_cli-0.2.0/README.md +93 -0
- sampler_cli-0.2.0/pyproject.toml +64 -0
- sampler_cli-0.2.0/setup.cfg +4 -0
- sampler_cli-0.2.0/src/sampler/__init__.py +3 -0
- sampler_cli-0.2.0/src/sampler/__main__.py +5 -0
- sampler_cli-0.2.0/src/sampler/cli/__init__.py +0 -0
- sampler_cli-0.2.0/src/sampler/cli/main.py +187 -0
- sampler_cli-0.2.0/src/sampler/config.py +77 -0
- sampler_cli-0.2.0/src/sampler/db.py +316 -0
- sampler_cli-0.2.0/src/sampler/indexer/__init__.py +0 -0
- sampler_cli-0.2.0/src/sampler/indexer/builder.py +70 -0
- sampler_cli-0.2.0/src/sampler/indexer/discover.py +53 -0
- sampler_cli-0.2.0/src/sampler/indexer/parsers/__init__.py +0 -0
- sampler_cli-0.2.0/src/sampler/indexer/parsers/base.py +9 -0
- sampler_cli-0.2.0/src/sampler/indexer/parsers/go.py +9 -0
- sampler_cli-0.2.0/src/sampler/indexer/parsers/python.py +139 -0
- sampler_cli-0.2.0/src/sampler/indexer/parsers/typescript.py +9 -0
- sampler_cli-0.2.0/src/sampler/indexer/store.py +47 -0
- sampler_cli-0.2.0/src/sampler/mcp/__init__.py +0 -0
- sampler_cli-0.2.0/src/sampler/mcp/server.py +2 -0
- sampler_cli-0.2.0/src/sampler/models.py +35 -0
- sampler_cli-0.2.0/src/sampler/query/__init__.py +0 -0
- sampler_cli-0.2.0/src/sampler/query/engine.py +16 -0
- sampler_cli-0.2.0/src/sampler/query/semantic.py +4 -0
- sampler_cli-0.2.0/src/sampler_cli.egg-info/PKG-INFO +130 -0
- sampler_cli-0.2.0/src/sampler_cli.egg-info/SOURCES.txt +37 -0
- sampler_cli-0.2.0/src/sampler_cli.egg-info/dependency_links.txt +1 -0
- sampler_cli-0.2.0/src/sampler_cli.egg-info/entry_points.txt +2 -0
- sampler_cli-0.2.0/src/sampler_cli.egg-info/requires.txt +19 -0
- sampler_cli-0.2.0/src/sampler_cli.egg-info/top_level.txt +1 -0
- sampler_cli-0.2.0/tests/test_cli.py +23 -0
- sampler_cli-0.2.0/tests/test_config.py +20 -0
- sampler_cli-0.2.0/tests/test_db.py +22 -0
- sampler_cli-0.2.0/tests/test_discover.py +18 -0
- sampler_cli-0.2.0/tests/test_index_query.py +42 -0
- sampler_cli-0.2.0/tests/test_python_parser.py +26 -0
- sampler_cli-0.2.0/tests/test_smoke.py +10 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Samuel Ignacio Carmona Rodriguez
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sampler-cli
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Token-efficient CLI for indexing and searching code symbols (Python-first, designed for minimal LLM/agent context size)
|
|
5
|
+
Author: Samuel Ignacio Carmona Rodriguez
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/sicr0/sampler-cli
|
|
8
|
+
Project-URL: Repository, https://github.com/sicr0/sampler-cli
|
|
9
|
+
Project-URL: Issues, https://github.com/sicr0/sampler-cli/issues
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
16
|
+
Classifier: Topic :: Software Development :: Code Generators
|
|
17
|
+
Requires-Python: >=3.11
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Requires-Dist: typer>=0.12.0
|
|
21
|
+
Requires-Dist: rich>=13.7.0
|
|
22
|
+
Requires-Dist: tree-sitter>=0.21.0
|
|
23
|
+
Requires-Dist: tree-sitter-python>=0.23.0
|
|
24
|
+
Requires-Dist: gitignore-parser>=0.1.11
|
|
25
|
+
Requires-Dist: pydantic>=2.6.0
|
|
26
|
+
Requires-Dist: pyyaml>=6.0.0
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: pytest>=7.4.0; extra == "dev"
|
|
29
|
+
Requires-Dist: pytest-cov>=4.1.0; extra == "dev"
|
|
30
|
+
Requires-Dist: ruff>=0.5.0; extra == "dev"
|
|
31
|
+
Requires-Dist: mypy>=1.7.0; extra == "dev"
|
|
32
|
+
Provides-Extra: mcp
|
|
33
|
+
Requires-Dist: fastmcp>=0.1.0; extra == "mcp"
|
|
34
|
+
Provides-Extra: semantic
|
|
35
|
+
Requires-Dist: sentence-transformers>=2.2.0; extra == "semantic"
|
|
36
|
+
Dynamic: license-file
|
|
37
|
+
|
|
38
|
+
# Sampler
|
|
39
|
+
|
|
40
|
+
CLI indexer para navegar símbolos y relaciones en codebases multiproyecto.
|
|
41
|
+
|
|
42
|
+
Versión actual: 0.1.2
|
|
43
|
+
|
|
44
|
+
## Requisitos
|
|
45
|
+
|
|
46
|
+
- Python 3.11+
|
|
47
|
+
- `uv` (recomendado)
|
|
48
|
+
- Go (instalado para soporte parser Fase 1)
|
|
49
|
+
|
|
50
|
+
## Instalación de Go (macOS)
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
brew install go
|
|
54
|
+
go version
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Instalación
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pip install sampler-cli
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Para desarrollo (incluye tests, linters):
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
pip install -e '.[dev]'
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Uso rápido
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
pip install sampler-cli
|
|
73
|
+
sampler init
|
|
74
|
+
sampler project add myproj /absolute/path --language python
|
|
75
|
+
sampler project list
|
|
76
|
+
sampler index myproj
|
|
77
|
+
sampler search add --project myproj
|
|
78
|
+
sampler overview /absolute/path/file.py
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
**Demo / LLM use (token-efficient by design):**
|
|
82
|
+
- Default outputs are compact single-line (no tables, short paths, no noise).
|
|
83
|
+
- Ideal for pasting into agents/LLMs with minimal context size.
|
|
84
|
+
- Example: `sampler search worker --project myproj` → `myproj:src/tasks.py:42 function process def process()`
|
|
85
|
+
|
|
86
|
+
## Estado actual
|
|
87
|
+
|
|
88
|
+
Implementado:
|
|
89
|
+
|
|
90
|
+
- Bootstrap inicial de Fase 0
|
|
91
|
+
- Configuración global con archivo `~/.sampler/config.yaml`
|
|
92
|
+
- CRUD de proyectos en config (`add`, `list`, `remove`)
|
|
93
|
+
- Esquema SQLite core + queries de index/search en `src/sampler/db.py`
|
|
94
|
+
- Discovery de archivos por lenguaje con soporte `.gitignore`
|
|
95
|
+
- Parser Python estable basado en AST
|
|
96
|
+
- Indexer real (hash incremental + persistencia)
|
|
97
|
+
- Query engine real (`search`, `overview`)
|
|
98
|
+
- CI básico con GitHub Actions (`pytest -q`)
|
|
99
|
+
- Tests: smoke, config, db, cli, discovery, python_parser, index_query
|
|
100
|
+
|
|
101
|
+
Nota de estabilidad:
|
|
102
|
+
|
|
103
|
+
- Se desactivó uso runtime de tree-sitter en parser Python por crash nativo (`BUS/SEGV`) en indexación real.
|
|
104
|
+
- Se mantiene estrategia AST para estabilidad en producción local.
|
|
105
|
+
|
|
106
|
+
Pendiente inmediato:
|
|
107
|
+
|
|
108
|
+
- Filtros y paginación en búsqueda
|
|
109
|
+
- Comandos `callers`, `usages`, `related`
|
|
110
|
+
- Parsers Go y TypeScript/JavaScript
|
|
111
|
+
|
|
112
|
+
## Estructura clave
|
|
113
|
+
|
|
114
|
+
```text
|
|
115
|
+
src/sampler/cli/main.py # comandos CLI
|
|
116
|
+
src/sampler/config.py # config global YAML
|
|
117
|
+
src/sampler/db.py # capa SQLite
|
|
118
|
+
src/sampler/indexer/builder.py # indexación de proyectos
|
|
119
|
+
src/sampler/indexer/store.py # persistencia de símbolos/relaciones
|
|
120
|
+
src/sampler/indexer/parsers/python.py # parser python estable
|
|
121
|
+
src/sampler/query/engine.py # search/overview
|
|
122
|
+
src/sampler/indexer/discover.py # discovery y filtros
|
|
123
|
+
tests/ # pruebas base
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Ejecutar pruebas
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
pytest -q
|
|
130
|
+
```
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# Sampler
|
|
2
|
+
|
|
3
|
+
CLI indexer para navegar símbolos y relaciones en codebases multiproyecto.
|
|
4
|
+
|
|
5
|
+
Versión actual: 0.1.2
|
|
6
|
+
|
|
7
|
+
## Requisitos
|
|
8
|
+
|
|
9
|
+
- Python 3.11+
|
|
10
|
+
- `uv` (recomendado)
|
|
11
|
+
- Go (instalado para soporte parser Fase 1)
|
|
12
|
+
|
|
13
|
+
## Instalación de Go (macOS)
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
brew install go
|
|
17
|
+
go version
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Instalación
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install sampler-cli
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Para desarrollo (incluye tests, linters):
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install -e '.[dev]'
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Uso rápido
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install sampler-cli
|
|
36
|
+
sampler init
|
|
37
|
+
sampler project add myproj /absolute/path --language python
|
|
38
|
+
sampler project list
|
|
39
|
+
sampler index myproj
|
|
40
|
+
sampler search add --project myproj
|
|
41
|
+
sampler overview /absolute/path/file.py
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
**Demo / LLM use (token-efficient by design):**
|
|
45
|
+
- Default outputs are compact single-line (no tables, short paths, no noise).
|
|
46
|
+
- Ideal for pasting into agents/LLMs with minimal context size.
|
|
47
|
+
- Example: `sampler search worker --project myproj` → `myproj:src/tasks.py:42 function process def process()`
|
|
48
|
+
|
|
49
|
+
## Estado actual
|
|
50
|
+
|
|
51
|
+
Implementado:
|
|
52
|
+
|
|
53
|
+
- Bootstrap inicial de Fase 0
|
|
54
|
+
- Configuración global con archivo `~/.sampler/config.yaml`
|
|
55
|
+
- CRUD de proyectos en config (`add`, `list`, `remove`)
|
|
56
|
+
- Esquema SQLite core + queries de index/search en `src/sampler/db.py`
|
|
57
|
+
- Discovery de archivos por lenguaje con soporte `.gitignore`
|
|
58
|
+
- Parser Python estable basado en AST
|
|
59
|
+
- Indexer real (hash incremental + persistencia)
|
|
60
|
+
- Query engine real (`search`, `overview`)
|
|
61
|
+
- CI básico con GitHub Actions (`pytest -q`)
|
|
62
|
+
- Tests: smoke, config, db, cli, discovery, python_parser, index_query
|
|
63
|
+
|
|
64
|
+
Nota de estabilidad:
|
|
65
|
+
|
|
66
|
+
- Se desactivó uso runtime de tree-sitter en parser Python por crash nativo (`BUS/SEGV`) en indexación real.
|
|
67
|
+
- Se mantiene estrategia AST para estabilidad en producción local.
|
|
68
|
+
|
|
69
|
+
Pendiente inmediato:
|
|
70
|
+
|
|
71
|
+
- Filtros y paginación en búsqueda
|
|
72
|
+
- Comandos `callers`, `usages`, `related`
|
|
73
|
+
- Parsers Go y TypeScript/JavaScript
|
|
74
|
+
|
|
75
|
+
## Estructura clave
|
|
76
|
+
|
|
77
|
+
```text
|
|
78
|
+
src/sampler/cli/main.py # comandos CLI
|
|
79
|
+
src/sampler/config.py # config global YAML
|
|
80
|
+
src/sampler/db.py # capa SQLite
|
|
81
|
+
src/sampler/indexer/builder.py # indexación de proyectos
|
|
82
|
+
src/sampler/indexer/store.py # persistencia de símbolos/relaciones
|
|
83
|
+
src/sampler/indexer/parsers/python.py # parser python estable
|
|
84
|
+
src/sampler/query/engine.py # search/overview
|
|
85
|
+
src/sampler/indexer/discover.py # discovery y filtros
|
|
86
|
+
tests/ # pruebas base
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Ejecutar pruebas
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
pytest -q
|
|
93
|
+
```
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "sampler-cli"
|
|
3
|
+
version = "0.2.0"
|
|
4
|
+
description = "Token-efficient CLI for indexing and searching code symbols (Python-first, designed for minimal LLM/agent context size)"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = { text = "MIT" }
|
|
7
|
+
authors = [
|
|
8
|
+
{ name = "Samuel Ignacio Carmona Rodriguez" },
|
|
9
|
+
]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 4 - Beta",
|
|
12
|
+
"Intended Audience :: Developers",
|
|
13
|
+
"License :: OSI Approved :: MIT License",
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"Programming Language :: Python :: 3.11",
|
|
16
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
17
|
+
"Topic :: Software Development :: Code Generators",
|
|
18
|
+
]
|
|
19
|
+
requires-python = ">=3.11"
|
|
20
|
+
dependencies = [
|
|
21
|
+
"typer>=0.12.0",
|
|
22
|
+
"rich>=13.7.0",
|
|
23
|
+
"tree-sitter>=0.21.0",
|
|
24
|
+
"tree-sitter-python>=0.23.0",
|
|
25
|
+
"gitignore-parser>=0.1.11",
|
|
26
|
+
"pydantic>=2.6.0",
|
|
27
|
+
"pyyaml>=6.0.0",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.optional-dependencies]
|
|
31
|
+
dev = [
|
|
32
|
+
"pytest>=7.4.0",
|
|
33
|
+
"pytest-cov>=4.1.0",
|
|
34
|
+
"ruff>=0.5.0",
|
|
35
|
+
"mypy>=1.7.0",
|
|
36
|
+
]
|
|
37
|
+
mcp = ["fastmcp>=0.1.0"]
|
|
38
|
+
semantic = ["sentence-transformers>=2.2.0"]
|
|
39
|
+
|
|
40
|
+
[project.scripts]
|
|
41
|
+
sampler = "sampler.cli.main:app"
|
|
42
|
+
|
|
43
|
+
[build-system]
|
|
44
|
+
requires = ["setuptools>=68", "wheel"]
|
|
45
|
+
build-backend = "setuptools.build_meta"
|
|
46
|
+
|
|
47
|
+
[tool.setuptools]
|
|
48
|
+
package-dir = {"" = "src"}
|
|
49
|
+
|
|
50
|
+
[tool.setuptools.packages.find]
|
|
51
|
+
where = ["src"]
|
|
52
|
+
|
|
53
|
+
[tool.ruff]
|
|
54
|
+
line-length = 100
|
|
55
|
+
target-version = "py311"
|
|
56
|
+
|
|
57
|
+
[tool.pytest.ini_options]
|
|
58
|
+
pythonpath = ["src"]
|
|
59
|
+
testpaths = ["tests"]
|
|
60
|
+
|
|
61
|
+
[project.urls]
|
|
62
|
+
Homepage = "https://github.com/sicr0/sampler-cli"
|
|
63
|
+
Repository = "https://github.com/sicr0/sampler-cli"
|
|
64
|
+
Issues = "https://github.com/sicr0/sampler-cli/issues"
|
|
File without changes
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
from rich.console import Console
|
|
5
|
+
|
|
6
|
+
from sampler import __version__
|
|
7
|
+
from sampler.config import ConfigManager
|
|
8
|
+
from sampler.db import Database
|
|
9
|
+
from sampler.indexer.builder import IndexBuilder
|
|
10
|
+
from sampler.query.engine import QueryEngine
|
|
11
|
+
|
|
12
|
+
app = typer.Typer(help="Sampler CLI")
|
|
13
|
+
project_app = typer.Typer(help="Project management commands")
|
|
14
|
+
app.add_typer(project_app, name="project")
|
|
15
|
+
console = Console()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _database() -> Database:
|
|
19
|
+
cfg = ConfigManager().load()
|
|
20
|
+
db_path = Path(cfg.cache_dir).expanduser() / "graph.db"
|
|
21
|
+
db = Database(db_path=db_path)
|
|
22
|
+
db.init_schema()
|
|
23
|
+
return db
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _get_project_roots() -> dict[str, Path]:
|
|
27
|
+
"""Map project name -> absolute root path for relative path computation."""
|
|
28
|
+
config = ConfigManager()
|
|
29
|
+
roots: dict[str, Path] = {}
|
|
30
|
+
for p in config.list_projects():
|
|
31
|
+
try:
|
|
32
|
+
roots[p.name] = Path(p.path).expanduser().resolve()
|
|
33
|
+
except Exception:
|
|
34
|
+
pass
|
|
35
|
+
return roots
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _short_path(project_name: str, full_path: str, roots: dict[str, Path]) -> str:
|
|
39
|
+
"""Return shortest useful path for display: relative to project root if possible, else tail or name."""
|
|
40
|
+
root = roots.get(project_name)
|
|
41
|
+
if root:
|
|
42
|
+
try:
|
|
43
|
+
return str(Path(full_path).resolve().relative_to(root))
|
|
44
|
+
except Exception:
|
|
45
|
+
pass
|
|
46
|
+
# Fallback: last 1-2 path segments to keep output short (token friendly)
|
|
47
|
+
p = Path(full_path)
|
|
48
|
+
if len(p.parts) >= 3:
|
|
49
|
+
return "/".join(p.parts[-2:])
|
|
50
|
+
return p.name
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@app.command("version")
|
|
54
|
+
def version() -> None:
|
|
55
|
+
"""Show installed sampler version."""
|
|
56
|
+
console.print(f"sampler {__version__}")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@app.command("init")
|
|
60
|
+
def init() -> None:
|
|
61
|
+
"""Initialize sampler local data directory."""
|
|
62
|
+
config = ConfigManager()
|
|
63
|
+
config.load()
|
|
64
|
+
data_dir = Path.home() / ".sampler"
|
|
65
|
+
console.print(f"Initialized [bold]{data_dir}[/bold]")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@project_app.command("list")
|
|
69
|
+
def project_list() -> None:
|
|
70
|
+
"""List registered projects."""
|
|
71
|
+
config = ConfigManager()
|
|
72
|
+
projects = config.list_projects()
|
|
73
|
+
home = str(Path.home().resolve())
|
|
74
|
+
|
|
75
|
+
for project in projects:
|
|
76
|
+
try:
|
|
77
|
+
pp = Path(project.path).resolve()
|
|
78
|
+
ps = str(pp)
|
|
79
|
+
if ps.startswith(home):
|
|
80
|
+
disp = "~" + ps[len(home):]
|
|
81
|
+
else:
|
|
82
|
+
parts = pp.parts
|
|
83
|
+
disp = "/".join(parts[-2:]) if len(parts) > 2 else ps
|
|
84
|
+
except Exception:
|
|
85
|
+
disp = project.path
|
|
86
|
+
console.print(f"{project.name} {disp}")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@project_app.command("add")
|
|
90
|
+
def project_add(name: str, path: str, language: str = "python") -> None:
|
|
91
|
+
"""Register project in global config."""
|
|
92
|
+
config = ConfigManager()
|
|
93
|
+
try:
|
|
94
|
+
project = config.add_project(name=name, path=path, language=language)
|
|
95
|
+
except ValueError as exc:
|
|
96
|
+
raise typer.BadParameter(str(exc)) from exc
|
|
97
|
+
console.print(f"Added project [bold]{project.name}[/bold]")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@project_app.command("remove")
|
|
101
|
+
def project_remove(name: str) -> None:
|
|
102
|
+
"""Remove project from global config."""
|
|
103
|
+
config = ConfigManager()
|
|
104
|
+
try:
|
|
105
|
+
config.remove_project(name)
|
|
106
|
+
except ValueError as exc:
|
|
107
|
+
raise typer.BadParameter(str(exc)) from exc
|
|
108
|
+
console.print(f"Removed project [bold]{name}[/bold]")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@app.command("search")
|
|
112
|
+
def search(
|
|
113
|
+
query: str,
|
|
114
|
+
project: str | None = typer.Option(None, "--project", "-p"),
|
|
115
|
+
type: str | None = typer.Option(None, "--type", "-t", help="filter e.g. function,class"),
|
|
116
|
+
limit: int = typer.Option(100, "--limit", "-l"),
|
|
117
|
+
) -> None:
|
|
118
|
+
"""Search symbols by name."""
|
|
119
|
+
engine = QueryEngine(db=_database())
|
|
120
|
+
types = [x.strip() for x in type.split(",")] if type else None
|
|
121
|
+
if types:
|
|
122
|
+
exp = set(types)
|
|
123
|
+
for t in list(types):
|
|
124
|
+
if t == "function": exp.add("async function")
|
|
125
|
+
elif t == "method": exp.add("async method")
|
|
126
|
+
types = list(exp)
|
|
127
|
+
rows = engine.search(query=query, project_name=project, types=types, limit=limit)
|
|
128
|
+
roots = _get_project_roots()
|
|
129
|
+
|
|
130
|
+
for r in rows:
|
|
131
|
+
shortf = _short_path(r["project_name"], r["file_path"], roots)
|
|
132
|
+
name = r["qualified_name"] or r["name"]
|
|
133
|
+
sig = r.get("signature") or ""
|
|
134
|
+
line = f"{r['project_name']}:{shortf}:{r['start_line'] or '-'} {r['type']} {name}"
|
|
135
|
+
if sig:
|
|
136
|
+
line += f" {sig}"
|
|
137
|
+
console.print(line)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@app.command("search-all")
|
|
141
|
+
def search_all(
|
|
142
|
+
query: str,
|
|
143
|
+
type: str | None = typer.Option(None, "--type", "-t", help="filter e.g. function,class"),
|
|
144
|
+
limit: int = typer.Option(100, "--limit", "-l"),
|
|
145
|
+
) -> None:
|
|
146
|
+
"""Search symbols across ALL projects."""
|
|
147
|
+
search(query=query, project=None, type=type, limit=limit)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@app.command("index")
|
|
151
|
+
def index(project: str) -> None:
|
|
152
|
+
"""Index selected project."""
|
|
153
|
+
config = ConfigManager()
|
|
154
|
+
project_cfg = config.get_project(project)
|
|
155
|
+
if project_cfg is None:
|
|
156
|
+
raise typer.BadParameter(f"Project '{project}' not found. Use 'sampler project list'.")
|
|
157
|
+
|
|
158
|
+
builder = IndexBuilder(db=_database())
|
|
159
|
+
stats = builder.index_project(
|
|
160
|
+
project_name=project_cfg.name,
|
|
161
|
+
project_path=project_cfg.path,
|
|
162
|
+
language=project_cfg.language,
|
|
163
|
+
)
|
|
164
|
+
console.print(
|
|
165
|
+
"Indexed project "
|
|
166
|
+
f"[bold]{stats['project']}[/bold]: discovered={stats['discovered']} indexed={stats['indexed']} "
|
|
167
|
+
f"skipped={stats['skipped']} failed={stats['failed']}"
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@app.command("overview")
|
|
172
|
+
def overview(filepath: str) -> None:
|
|
173
|
+
"""Show symbols for file."""
|
|
174
|
+
engine = QueryEngine(db=_database())
|
|
175
|
+
rows = engine.overview(filepath=filepath)
|
|
176
|
+
|
|
177
|
+
for r in rows:
|
|
178
|
+
name = r["qualified_name"] or r["name"]
|
|
179
|
+
sig = r.get("signature") or ""
|
|
180
|
+
line = f"{r['start_line'] or '-'}: {r['type']} {name}"
|
|
181
|
+
if sig:
|
|
182
|
+
line += f" {sig}"
|
|
183
|
+
console.print(line)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
if __name__ == "__main__":
|
|
187
|
+
app()
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
import yaml
|
|
4
|
+
from pydantic import BaseModel, Field
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def default_data_dir() -> Path:
|
|
8
|
+
return Path.home() / ".sampler"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ProjectConfig(BaseModel):
|
|
12
|
+
name: str
|
|
13
|
+
path: str
|
|
14
|
+
language: str
|
|
15
|
+
enabled: bool = True
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class GlobalConfig(BaseModel):
|
|
19
|
+
version: int = 1
|
|
20
|
+
cache_dir: str = str(default_data_dir())
|
|
21
|
+
projects: dict[str, ProjectConfig] = Field(default_factory=dict)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ConfigManager:
|
|
25
|
+
def __init__(self, config_path: Path | None = None) -> None:
|
|
26
|
+
self.config_path = config_path or (default_data_dir() / "config.yaml")
|
|
27
|
+
|
|
28
|
+
def load(self) -> GlobalConfig:
|
|
29
|
+
if not self.config_path.exists():
|
|
30
|
+
config = GlobalConfig()
|
|
31
|
+
self.save(config)
|
|
32
|
+
return config
|
|
33
|
+
|
|
34
|
+
with self.config_path.open("r", encoding="utf-8") as f:
|
|
35
|
+
raw = yaml.safe_load(f) or {}
|
|
36
|
+
config = GlobalConfig.model_validate(raw)
|
|
37
|
+
|
|
38
|
+
if config.version != 1:
|
|
39
|
+
config.version = 1
|
|
40
|
+
self.save(config)
|
|
41
|
+
|
|
42
|
+
return config
|
|
43
|
+
|
|
44
|
+
def save(self, config: GlobalConfig) -> None:
|
|
45
|
+
self.config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
46
|
+
payload = config.model_dump(mode="python")
|
|
47
|
+
with self.config_path.open("w", encoding="utf-8") as f:
|
|
48
|
+
yaml.safe_dump(payload, f, sort_keys=False)
|
|
49
|
+
|
|
50
|
+
def add_project(self, name: str, path: str, language: str, enabled: bool = True) -> ProjectConfig:
|
|
51
|
+
config = self.load()
|
|
52
|
+
if name in config.projects:
|
|
53
|
+
raise ValueError(f"Project '{name}' already exists")
|
|
54
|
+
|
|
55
|
+
project_path = str(Path(path).expanduser().resolve())
|
|
56
|
+
if not Path(project_path).exists():
|
|
57
|
+
raise ValueError(f"Project path does not exist: {project_path}")
|
|
58
|
+
|
|
59
|
+
project = ProjectConfig(name=name, path=project_path, language=language, enabled=enabled)
|
|
60
|
+
config.projects[name] = project
|
|
61
|
+
self.save(config)
|
|
62
|
+
return project
|
|
63
|
+
|
|
64
|
+
def remove_project(self, name: str) -> None:
|
|
65
|
+
config = self.load()
|
|
66
|
+
if name not in config.projects:
|
|
67
|
+
raise ValueError(f"Project '{name}' does not exist")
|
|
68
|
+
del config.projects[name]
|
|
69
|
+
self.save(config)
|
|
70
|
+
|
|
71
|
+
def get_project(self, name: str) -> ProjectConfig | None:
|
|
72
|
+
config = self.load()
|
|
73
|
+
return config.projects.get(name)
|
|
74
|
+
|
|
75
|
+
def list_projects(self) -> list[ProjectConfig]:
|
|
76
|
+
config = self.load()
|
|
77
|
+
return list(config.projects.values())
|