pcd-cli 0.2.1__tar.gz → 0.2.2__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.
- {pcd_cli-0.2.1 → pcd_cli-0.2.2}/PKG-INFO +4 -2
- {pcd_cli-0.2.1 → pcd_cli-0.2.2}/README.txt +3 -1
- {pcd_cli-0.2.1 → pcd_cli-0.2.2}/pyproject.toml +1 -1
- {pcd_cli-0.2.1 → pcd_cli-0.2.2}/src/pcd_cli/cli/__init__.py +2 -1
- {pcd_cli-0.2.1 → pcd_cli-0.2.2}/src/pcd_cli/cli/app.py +8 -4
- {pcd_cli-0.2.1 → pcd_cli-0.2.2}/src/pcd_cli/cli/config.py +2 -2
- {pcd_cli-0.2.1 → pcd_cli-0.2.2}/src/pcd_cli/cli/projects.py +60 -55
- pcd_cli-0.2.2/src/pcd_cli/cli/roots.py +113 -0
- {pcd_cli-0.2.1 → pcd_cli-0.2.2}/LICENSE +0 -0
- {pcd_cli-0.2.1 → pcd_cli-0.2.2}/src/pcd_cli/__init__.py +0 -0
- {pcd_cli-0.2.1 → pcd_cli-0.2.2}/src/pcd_cli/cache.py +0 -0
- {pcd_cli-0.2.1 → pcd_cli-0.2.2}/src/pcd_cli/catalog.py +0 -0
- {pcd_cli-0.2.1 → pcd_cli-0.2.2}/src/pcd_cli/cli/shell.py +0 -0
- {pcd_cli-0.2.1 → pcd_cli-0.2.2}/src/pcd_cli/config.py +0 -0
- {pcd_cli-0.2.1 → pcd_cli-0.2.2}/src/pcd_cli/filesystem.py +0 -0
- {pcd_cli-0.2.1 → pcd_cli-0.2.2}/src/pcd_cli/history.py +0 -0
- {pcd_cli-0.2.1 → pcd_cli-0.2.2}/src/pcd_cli/models.py +0 -0
- {pcd_cli-0.2.1 → pcd_cli-0.2.2}/src/pcd_cli/navigation.py +0 -0
- {pcd_cli-0.2.1 → pcd_cli-0.2.2}/src/pcd_cli/picker.py +0 -0
- {pcd_cli-0.2.1 → pcd_cli-0.2.2}/src/pcd_cli/scanner.py +0 -0
- {pcd_cli-0.2.1 → pcd_cli-0.2.2}/src/pcd_cli/search.py +0 -0
- {pcd_cli-0.2.1 → pcd_cli-0.2.2}/src/pcd_cli/shell_integration.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pcd-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Fast project navigator for Bash, Zsh and Fish
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -69,7 +69,7 @@ COMMANDS
|
|
|
69
69
|
pcd roots List scan roots
|
|
70
70
|
pcd refresh Rescan roots and rebuild the project cache
|
|
71
71
|
|
|
72
|
-
pcd list
|
|
72
|
+
pcd list [options] List and filter known projects
|
|
73
73
|
pcd config path Print the configuration file path
|
|
74
74
|
pcd config show Print the effective configuration
|
|
75
75
|
pcd config edit Edit the user configuration
|
|
@@ -85,6 +85,8 @@ COMMANDS
|
|
|
85
85
|
|
|
86
86
|
* Projects inside registered roots are discovered automatically.
|
|
87
87
|
* You can register multiple scan roots.
|
|
88
|
+
* Filter `pcd list` with `--manual`, `--discovered`, or `--missing`.
|
|
89
|
+
* Use `pcd list --json` for machine-readable output.
|
|
88
90
|
* Set `editor = "nvim"` in the configuration to choose an editor for `pcd config edit`.
|
|
89
91
|
|
|
90
92
|
|
|
@@ -45,7 +45,7 @@ COMMANDS
|
|
|
45
45
|
pcd roots List scan roots
|
|
46
46
|
pcd refresh Rescan roots and rebuild the project cache
|
|
47
47
|
|
|
48
|
-
pcd list
|
|
48
|
+
pcd list [options] List and filter known projects
|
|
49
49
|
pcd config path Print the configuration file path
|
|
50
50
|
pcd config show Print the effective configuration
|
|
51
51
|
pcd config edit Edit the user configuration
|
|
@@ -61,6 +61,8 @@ COMMANDS
|
|
|
61
61
|
|
|
62
62
|
* Projects inside registered roots are discovered automatically.
|
|
63
63
|
* You can register multiple scan roots.
|
|
64
|
+
* Filter `pcd list` with `--manual`, `--discovered`, or `--missing`.
|
|
65
|
+
* Use `pcd list --json` for machine-readable output.
|
|
64
66
|
* Set `editor = "nvim"` in the configuration to choose an editor for `pcd config edit`.
|
|
65
67
|
|
|
66
68
|
|
|
@@ -6,7 +6,8 @@ from pcd_cli.cli.config import (
|
|
|
6
6
|
show_config,
|
|
7
7
|
validate_config,
|
|
8
8
|
)
|
|
9
|
-
from pcd_cli.cli.projects import add,
|
|
9
|
+
from pcd_cli.cli.projects import add, list_projects, project, remove
|
|
10
|
+
from pcd_cli.cli.roots import init, refresh, roots, uninit
|
|
10
11
|
from pcd_cli.cli.shell import (
|
|
11
12
|
install_shell,
|
|
12
13
|
print_shell_integration,
|
|
@@ -8,6 +8,7 @@ import click
|
|
|
8
8
|
from pcd_cli.catalog import ProjectCatalog
|
|
9
9
|
from pcd_cli.cli.config import config_commands
|
|
10
10
|
from pcd_cli.cli.projects import project_commands
|
|
11
|
+
from pcd_cli.cli.roots import root_commands
|
|
11
12
|
from pcd_cli.cli.shell import shell_commands, shell_init
|
|
12
13
|
from pcd_cli.config import InvalidConfigError
|
|
13
14
|
from pcd_cli.models import ExitCode
|
|
@@ -83,11 +84,14 @@ def cli(ctx: click.Context, project_name: str | None) -> None:
|
|
|
83
84
|
navigate_to_project(catalog, project_name, exact_only=True)
|
|
84
85
|
|
|
85
86
|
|
|
86
|
-
for command in
|
|
87
|
+
for command in (
|
|
88
|
+
*project_commands,
|
|
89
|
+
*root_commands,
|
|
90
|
+
config_commands,
|
|
91
|
+
shell_commands,
|
|
92
|
+
shell_init,
|
|
93
|
+
):
|
|
87
94
|
cli.add_command(command)
|
|
88
|
-
cli.add_command(config_commands)
|
|
89
|
-
cli.add_command(shell_commands)
|
|
90
|
-
cli.add_command(shell_init)
|
|
91
95
|
|
|
92
96
|
|
|
93
97
|
def main() -> None:
|
|
@@ -40,8 +40,8 @@ def edit_config(catalog: ProjectCatalog) -> None:
|
|
|
40
40
|
editor = catalog.config.load().editor
|
|
41
41
|
except InvalidConfigError:
|
|
42
42
|
editor = None
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
|
|
44
|
+
editor = editor or os.environ.get("VISUAL") or os.environ.get("EDITOR")
|
|
45
45
|
|
|
46
46
|
if editor is None:
|
|
47
47
|
raise click.UsageError("Set $VISUAL or $EDITOR before running `pcd config edit`")
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import json
|
|
4
|
+
from enum import StrEnum
|
|
3
5
|
from pathlib import Path
|
|
4
6
|
from typing import assert_never, ClassVar, Literal, NamedTuple, TYPE_CHECKING
|
|
5
7
|
|
|
@@ -15,14 +17,18 @@ if TYPE_CHECKING:
|
|
|
15
17
|
from pcd_cli.catalog import ProjectCatalog
|
|
16
18
|
|
|
17
19
|
type ProjectSourceLabel = Literal["manual", "scanned"]
|
|
18
|
-
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ProjectStatus(StrEnum):
|
|
23
|
+
AVAILABLE = "available"
|
|
24
|
+
MISSING = "missing"
|
|
19
25
|
|
|
20
26
|
|
|
21
27
|
class ProjectListRow(NamedTuple):
|
|
22
28
|
name: str
|
|
23
29
|
path: str
|
|
24
30
|
source: ProjectSourceLabel
|
|
25
|
-
status:
|
|
31
|
+
status: ProjectStatus
|
|
26
32
|
|
|
27
33
|
@classmethod
|
|
28
34
|
def from_project(cls, project: Project) -> ProjectListRow:
|
|
@@ -44,11 +50,11 @@ class ProjectListRow(NamedTuple):
|
|
|
44
50
|
assert_never(source)
|
|
45
51
|
|
|
46
52
|
@staticmethod
|
|
47
|
-
def _status_label(path: Path) ->
|
|
53
|
+
def _status_label(path: Path) -> ProjectStatus:
|
|
48
54
|
if path.is_dir():
|
|
49
|
-
return
|
|
55
|
+
return ProjectStatus.AVAILABLE
|
|
50
56
|
else:
|
|
51
|
-
return
|
|
57
|
+
return ProjectStatus.MISSING
|
|
52
58
|
|
|
53
59
|
|
|
54
60
|
class ProjectListTable:
|
|
@@ -59,6 +65,12 @@ class ProjectListTable:
|
|
|
59
65
|
def __init__(self, projects: Sequence[Project]) -> None:
|
|
60
66
|
self._rows = tuple(ProjectListRow.from_project(project) for project in projects)
|
|
61
67
|
|
|
68
|
+
@classmethod
|
|
69
|
+
def from_rows(cls, rows: Sequence[ProjectListRow]) -> ProjectListTable:
|
|
70
|
+
table = cls(())
|
|
71
|
+
table._rows = tuple(rows)
|
|
72
|
+
return table
|
|
73
|
+
|
|
62
74
|
def render(self) -> str:
|
|
63
75
|
if self._rows:
|
|
64
76
|
return self._render_table()
|
|
@@ -87,37 +99,6 @@ def project(catalog: ProjectCatalog, query: str) -> None:
|
|
|
87
99
|
navigate_to_project(catalog, query)
|
|
88
100
|
|
|
89
101
|
|
|
90
|
-
@click.command()
|
|
91
|
-
@click.pass_obj
|
|
92
|
-
def init(catalog: ProjectCatalog) -> None:
|
|
93
|
-
"""Register the current directory as a scan root."""
|
|
94
|
-
current = Path.cwd()
|
|
95
|
-
parent = catalog.find_parent_root(current)
|
|
96
|
-
added = catalog.add_scan_root(current)
|
|
97
|
-
|
|
98
|
-
if not added:
|
|
99
|
-
click.echo(f"Already a root: {format_path(current)}")
|
|
100
|
-
return
|
|
101
|
-
|
|
102
|
-
if parent is not None:
|
|
103
|
-
click.echo(f"Note: root is inside {format_path(parent)}", err=True)
|
|
104
|
-
|
|
105
|
-
click.echo(f"Added root: {format_path(current)}")
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
@click.command()
|
|
109
|
-
@click.pass_obj
|
|
110
|
-
def uninit(catalog: ProjectCatalog) -> None:
|
|
111
|
-
"""Remove the current directory from scan roots."""
|
|
112
|
-
current = Path.cwd()
|
|
113
|
-
if catalog.remove_scan_root(current):
|
|
114
|
-
click.echo(f"Removed root: {format_path(current)}")
|
|
115
|
-
return
|
|
116
|
-
|
|
117
|
-
click.echo("Current directory is not a pcd root.", err=True)
|
|
118
|
-
raise click.exceptions.Exit(ExitCode.ERROR)
|
|
119
|
-
|
|
120
|
-
|
|
121
102
|
@click.command()
|
|
122
103
|
@click.argument("path", type=click.Path(path_type=Path, file_okay=False), required=False)
|
|
123
104
|
@click.option("--name", help="Custom name for a manual project.")
|
|
@@ -187,26 +168,50 @@ def remove(catalog: ProjectCatalog, query: str) -> None:
|
|
|
187
168
|
|
|
188
169
|
|
|
189
170
|
@click.command("list")
|
|
171
|
+
@click.option("--manual", is_flag=True, help="Show manually registered projects.")
|
|
172
|
+
@click.option("--discovered", is_flag=True, help="Show automatically discovered projects.")
|
|
173
|
+
@click.option("--missing", is_flag=True, help="Show projects whose directories are missing.")
|
|
174
|
+
@click.option("--json", "as_json", is_flag=True, help="Print projects as JSON.")
|
|
190
175
|
@click.pass_obj
|
|
191
|
-
def list_projects(
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
176
|
+
def list_projects(
|
|
177
|
+
catalog: ProjectCatalog,
|
|
178
|
+
manual: bool,
|
|
179
|
+
discovered: bool,
|
|
180
|
+
missing: bool,
|
|
181
|
+
as_json: bool,
|
|
182
|
+
) -> None:
|
|
183
|
+
"""List and filter known projects."""
|
|
184
|
+
entries: list[tuple[Project, ProjectListRow]] = []
|
|
185
|
+
for project in catalog.projects():
|
|
186
|
+
source_matches = (
|
|
187
|
+
not (manual or discovered)
|
|
188
|
+
or (manual and project.source is ProjectSource.MANUAL)
|
|
189
|
+
or (discovered and project.source is ProjectSource.DISCOVERED)
|
|
190
|
+
)
|
|
191
|
+
if not source_matches:
|
|
192
|
+
continue
|
|
193
|
+
|
|
194
|
+
row = ProjectListRow.from_project(project)
|
|
195
|
+
if missing and row.status is not ProjectStatus.MISSING:
|
|
196
|
+
continue
|
|
197
|
+
|
|
198
|
+
entries.append((project, row))
|
|
199
|
+
|
|
200
|
+
if as_json:
|
|
201
|
+
items = [
|
|
202
|
+
{
|
|
203
|
+
"name": project.name,
|
|
204
|
+
"path": str(project.display_path),
|
|
205
|
+
"source": project.source.value,
|
|
206
|
+
"status": row.status,
|
|
207
|
+
}
|
|
208
|
+
for project, row in entries
|
|
209
|
+
]
|
|
210
|
+
click.echo(json.dumps(items, indent=2))
|
|
211
|
+
return
|
|
204
212
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
def refresh(catalog: ProjectCatalog) -> None:
|
|
208
|
-
"""Rescan roots and rebuild the project cache."""
|
|
209
|
-
click.echo(f"Found {len(catalog.refresh())} projects.")
|
|
213
|
+
table = ProjectListTable.from_rows(tuple(row for _, row in entries))
|
|
214
|
+
click.echo(table.render())
|
|
210
215
|
|
|
211
216
|
|
|
212
|
-
project_commands = (project,
|
|
217
|
+
project_commands = (project, add, remove, list_projects)
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import ClassVar, Literal, NamedTuple, TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
|
|
8
|
+
from pcd_cli.filesystem import canonical_path, format_path
|
|
9
|
+
from pcd_cli.models import ExitCode, ProjectSource
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from collections.abc import Sequence
|
|
13
|
+
|
|
14
|
+
from pcd_cli.catalog import ProjectCatalog
|
|
15
|
+
from pcd_cli.models import Project
|
|
16
|
+
|
|
17
|
+
type RootStatusLabel = Literal["available", "missing"]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class RootListRow(NamedTuple):
|
|
21
|
+
path: str
|
|
22
|
+
projects: str
|
|
23
|
+
status: RootStatusLabel
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
def from_root(cls, root: Path, projects: Sequence[Project]) -> RootListRow:
|
|
27
|
+
resolved_root = canonical_path(root)
|
|
28
|
+
project_count = sum(
|
|
29
|
+
project.path.is_relative_to(resolved_root)
|
|
30
|
+
for project in projects
|
|
31
|
+
if project.source is ProjectSource.DISCOVERED
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
return cls(
|
|
35
|
+
path=format_path(root),
|
|
36
|
+
projects=str(project_count),
|
|
37
|
+
status="available" if root.is_dir() else "missing",
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class RootListTable:
|
|
42
|
+
HEADERS: ClassVar[tuple[str, str, str]] = ("PATH", "PROJECTS", "STATUS")
|
|
43
|
+
|
|
44
|
+
_rows: tuple[RootListRow, ...]
|
|
45
|
+
|
|
46
|
+
def __init__(self, roots: Sequence[Path], projects: Sequence[Project]) -> None:
|
|
47
|
+
self._rows = tuple(RootListRow.from_root(root, projects) for root in roots)
|
|
48
|
+
|
|
49
|
+
def render(self) -> str:
|
|
50
|
+
if not self._rows:
|
|
51
|
+
return "No roots found."
|
|
52
|
+
|
|
53
|
+
table = (self.HEADERS, *self._rows)
|
|
54
|
+
columns = zip(*table, strict=True)
|
|
55
|
+
widths = tuple(max(len(value) for value in column) for column in columns)
|
|
56
|
+
|
|
57
|
+
return "\n".join(self._render_row(row, widths) for row in table)
|
|
58
|
+
|
|
59
|
+
@staticmethod
|
|
60
|
+
def _render_row(row: tuple[str, ...], widths: tuple[int, ...]) -> str:
|
|
61
|
+
return " ".join(
|
|
62
|
+
value.ljust(width) for value, width in zip(row, widths, strict=True)
|
|
63
|
+
).rstrip()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@click.command()
|
|
67
|
+
@click.pass_obj
|
|
68
|
+
def init(catalog: ProjectCatalog) -> None:
|
|
69
|
+
"""Register the current directory as a scan root."""
|
|
70
|
+
current = Path.cwd()
|
|
71
|
+
parent = catalog.find_parent_root(current)
|
|
72
|
+
added = catalog.add_scan_root(current)
|
|
73
|
+
|
|
74
|
+
if not added:
|
|
75
|
+
click.echo(f"Already a root: {format_path(current)}")
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
if parent is not None:
|
|
79
|
+
click.echo(f"Note: root is inside {format_path(parent)}", err=True)
|
|
80
|
+
|
|
81
|
+
click.echo(f"Added root: {format_path(current)}")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@click.command()
|
|
85
|
+
@click.pass_obj
|
|
86
|
+
def uninit(catalog: ProjectCatalog) -> None:
|
|
87
|
+
"""Remove the current directory from scan roots."""
|
|
88
|
+
current = Path.cwd()
|
|
89
|
+
if catalog.remove_scan_root(current):
|
|
90
|
+
click.echo(f"Removed root: {format_path(current)}")
|
|
91
|
+
return
|
|
92
|
+
|
|
93
|
+
click.echo("Current directory is not a pcd root.", err=True)
|
|
94
|
+
raise click.exceptions.Exit(ExitCode.ERROR)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@click.command()
|
|
98
|
+
@click.pass_obj
|
|
99
|
+
def roots(catalog: ProjectCatalog) -> None:
|
|
100
|
+
"""List registered scan roots with project counts and status."""
|
|
101
|
+
settings = catalog.config.load()
|
|
102
|
+
table = RootListTable(settings.roots, catalog.projects())
|
|
103
|
+
click.echo(table.render())
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@click.command()
|
|
107
|
+
@click.pass_obj
|
|
108
|
+
def refresh(catalog: ProjectCatalog) -> None:
|
|
109
|
+
"""Rescan roots and rebuild the project cache."""
|
|
110
|
+
click.echo(f"Found {len(catalog.refresh())} projects.")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
root_commands = (init, uninit, roots, refresh)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|