linman 0.0.1__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.
apphub/__init__.py ADDED
File without changes
apphub/cli/__init__.py ADDED
File without changes
apphub/cli/banner.py ADDED
@@ -0,0 +1,74 @@
1
+ from rich.align import Align
2
+ from rich.console import Console, Group
3
+ from rich.panel import Panel
4
+ from rich.text import Text
5
+
6
+ # Block-style wordmark — reads clearly at typical terminal widths.
7
+ _LOGO_LINES = [
8
+ r" ██╗ ██╗███╗ ██╗███╗ ███╗ █████╗ ███╗ ██╗",
9
+ r" ██║ ██║████╗ ██║████╗ ████║██╔══██╗████╗ ██║",
10
+ r" ██║ ██║██╔██╗ ██║██╔████╔██║███████║██╔██╗ ██║",
11
+ r" ██║ ██║██║╚██╗██║██║╚██╔╝██║██╔══██║██║╚██╗██║",
12
+ r" ███████╗██║██║ ╚████║██║ ╚═╝ ██║██║ ██║██║ ╚████║",
13
+ r" ╚══════╝╚═╝╚═╝ ╚═══╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝",
14
+ ]
15
+
16
+ # Gradient stops (top → bottom) — cyan → blue → magenta.
17
+ _GRADIENT = [
18
+ "bright_cyan",
19
+ "cyan",
20
+ "deep_sky_blue1",
21
+ "dodger_blue1",
22
+ "medium_purple",
23
+ "medium_orchid",
24
+ ]
25
+
26
+ _TAGLINE = "Linux App Manager · apt · snap · flatpak · appimage"
27
+ _SUB = "one CLI · every format · zero friction"
28
+
29
+
30
+ def render_logo() -> Text:
31
+ """Return a colorized LINMAN wordmark."""
32
+ logo = Text()
33
+ for i, line in enumerate(_LOGO_LINES):
34
+ style = _GRADIENT[i % len(_GRADIENT)]
35
+ logo.append(line + "\n", style=f"bold {style}")
36
+ return logo
37
+
38
+
39
+ def print_banner(
40
+ console: Console | None = None,
41
+ *,
42
+ version: str = "0.1.0",
43
+ show_panel: bool = True,
44
+ ) -> None:
45
+ """Print the linman logo banner to the terminal."""
46
+ console = console or Console()
47
+
48
+ logo = render_logo()
49
+ tagline = Text(_TAGLINE, style="bold white")
50
+ sub = Text(_SUB, style="dim italic")
51
+ ver = Text(f"v{version}", style="dim cyan")
52
+
53
+ body = Group(
54
+ Align.center(logo),
55
+ Align.center(tagline),
56
+ Align.center(sub),
57
+ Align.center(ver),
58
+ )
59
+
60
+ if show_panel:
61
+ console.print(
62
+ Panel(
63
+ body,
64
+ border_style="bright_cyan",
65
+ padding=(1, 2),
66
+ title="[bold bright_magenta]◆[/] [bold white]linman[/]",
67
+ title_align="left",
68
+ subtitle="[dim]unified package hub[/]",
69
+ subtitle_align="right",
70
+ )
71
+ )
72
+ else:
73
+ console.print(body)
74
+ console.print()
apphub/cli/commands.py ADDED
@@ -0,0 +1,276 @@
1
+ import asyncio
2
+ from pathlib import Path
3
+
4
+ import typer
5
+ from rich.console import Console
6
+
7
+ from apphub.cli.formatters import (
8
+ format_app_panel,
9
+ format_app_table,
10
+ format_history_table,
11
+ format_storage_table,
12
+ )
13
+ from apphub.cli.serializers import to_json, to_json_single
14
+ from apphub.core.exceptions import AppNotFoundError
15
+ from apphub.core.hub import AppHubCore
16
+ from apphub.core.models import AppFormat, AppManifest, LifeCycleEvent
17
+
18
+ cli_app = typer.Typer(no_args_is_help=True)
19
+ hub = AppHubCore()
20
+ console = Console()
21
+
22
+
23
+ def _emit_app_list(
24
+ apps: list[AppManifest],
25
+ *,
26
+ title: str,
27
+ count: bool,
28
+ output_json: bool,
29
+ ) -> None:
30
+ if count:
31
+ console.print(f"[bold cyan]{len(apps)}[/bold cyan] application(s) found.")
32
+ return
33
+ if output_json:
34
+ console.print(to_json(apps))
35
+ return
36
+ console.print(format_app_table(apps, title=f"{title} ({len(apps)})"))
37
+
38
+
39
+ def _prompt_select_app(
40
+ results: list[AppManifest], name: str, action: str
41
+ ) -> AppManifest:
42
+ if len(results) == 1:
43
+ return results[0]
44
+
45
+ console.print(f"[yellow]Found multiple applications matching '{name}':[/yellow]")
46
+ for idx, app in enumerate(results, start=1):
47
+ desc = app.description or "No description"
48
+ console.print(
49
+ f"[cyan][{idx}] | {app.name} ({app.format.value}) | {desc}[/cyan]"
50
+ )
51
+
52
+ choice: int = typer.prompt(f"Select an application to {action} (number)", type=int)
53
+ if choice < 1 or choice > len(results):
54
+ console.print("[red]Invalid selection.[/red]")
55
+ raise typer.Exit(1)
56
+ return results[choice - 1]
57
+
58
+
59
+ @cli_app.command(name="list")
60
+ def list_apps(
61
+ query: str | None = typer.Argument(None, help="Filter applications by name"),
62
+ formats: list[AppFormat] | None = typer.Option(
63
+ None, "--format", "-f", help="Filter by format"
64
+ ),
65
+ exclude_defaults: bool = typer.Option(
66
+ False, "--exclude-defaults", "-e", help="Exclude system/default packages"
67
+ ),
68
+ sort_by: str | None = typer.Option(
69
+ None, "--sort", "-s", help="Sort by field: name, version, format"
70
+ ),
71
+ count: bool = typer.Option(
72
+ False, "--count", "-n", help="Print only the number of matching apps"
73
+ ),
74
+ output_json: bool = typer.Option(False, "--json", help="Output as JSON"),
75
+ ):
76
+ """List installed applications across all package managers."""
77
+ apps = asyncio.run(
78
+ hub.list_apps(query=query, formats=formats, exclude_defaults=exclude_defaults)
79
+ )
80
+
81
+ if sort_by:
82
+ apps = sorted(apps, key=lambda a: getattr(a, sort_by, "") or "")
83
+
84
+ _emit_app_list(apps, title="Applications", count=count, output_json=output_json)
85
+
86
+
87
+ @cli_app.command(name="search")
88
+ def search(
89
+ query: str = typer.Argument(..., help="Search applications by name or description"),
90
+ formats: list[AppFormat] | None = typer.Option(
91
+ None, "--format", "-f", help="Filter by format"
92
+ ),
93
+ count: bool = typer.Option(
94
+ False, "--count", "-n", help="Print only the number of matching apps"
95
+ ),
96
+ output_json: bool = typer.Option(False, "--json", help="Output as JSON"),
97
+ ):
98
+ """Search available applications across supported registries."""
99
+ apps = asyncio.run(hub.search(query=query, formats=formats))
100
+ _emit_app_list(apps, title="Search Results", count=count, output_json=output_json)
101
+
102
+
103
+ @cli_app.command(name="inspect")
104
+ def inspect(
105
+ file_path: str = typer.Argument(
106
+ ..., help="Path to the application file or Application Name to be searched"
107
+ ),
108
+ output_json: bool = typer.Option(False, "--json", help="Output as JSON"),
109
+ ):
110
+ """Inspect Local Installable File."""
111
+
112
+ if not Path(file_path).exists():
113
+ console.print(f"[red] No Application Found: '{file_path}'.[/red]")
114
+ raise typer.Exit(1)
115
+
116
+ app_info = asyncio.run(hub.inspect(path=file_path))
117
+
118
+ if output_json:
119
+ console.print(to_json_single(app_info))
120
+ else:
121
+ console.print(format_app_panel(app_info))
122
+
123
+
124
+ @cli_app.command(name="install")
125
+ def install(
126
+ path_or_name: str = typer.Argument(
127
+ ..., help="Path to the application file or Application Name"
128
+ ),
129
+ yes: bool = typer.Option(False, "--yes", "-y", help="Auto confirm installation"),
130
+ launch: bool = typer.Option(
131
+ False, "--launch", "-l", help="Launch after installation"
132
+ ),
133
+ formats: list[AppFormat] | None = typer.Option(
134
+ None, "--format", "-f", help="Optional format lookup for application"
135
+ ),
136
+ ):
137
+ """Install an application (from registery or from file)."""
138
+
139
+ if Path(path_or_name).exists():
140
+ app_info = asyncio.run(hub.inspect(path=path_or_name))
141
+ console.print(format_app_panel(app_info))
142
+ install_target = path_or_name
143
+ install_format = app_info.format
144
+ else:
145
+ results = asyncio.run(hub.search(query=path_or_name, formats=formats))
146
+ if not results:
147
+ console.print(f"[red]No application found matching '{path_or_name}'.[/red]")
148
+ raise typer.Exit(1)
149
+
150
+ app_info = _prompt_select_app(results, path_or_name, "install")
151
+ if len(results) == 1:
152
+ console.print(f"Found this on {app_info.format.value}")
153
+ console.print(format_app_panel(app_info))
154
+ install_target = app_info.name
155
+ install_format = app_info.format
156
+
157
+ if not yes:
158
+ confirm = typer.confirm("Install this application?")
159
+ if not confirm:
160
+ print("Installation cancelled.")
161
+ raise typer.Exit()
162
+
163
+ result = asyncio.run(
164
+ hub.install(
165
+ query_or_path=install_target, install_format=install_format, launch=launch
166
+ )
167
+ )
168
+
169
+ if result:
170
+ console.print("[green]Installation successful[/green]")
171
+
172
+
173
+ @cli_app.command(name="uninstall")
174
+ def uninstall(
175
+ name: str = typer.Argument(..., help="Name of the application to uninstall"),
176
+ yes: bool = typer.Option(False, "--yes", "-y", help="Auto confirm installation"),
177
+ clean_uninstall: bool = typer.Option(
178
+ False,
179
+ "--clean",
180
+ "-c",
181
+ help="Clean Uninstall, remove data associated with application",
182
+ ),
183
+ ):
184
+ """Uninstall an installed application."""
185
+ results = asyncio.run(hub.list_apps(query=name))
186
+ if not results:
187
+ console.print(f"[red]No application found matching '{name}'.[/red]")
188
+ raise typer.Exit(1)
189
+
190
+ if len(results) == 1:
191
+ console.print(f"[bold yellow]Uninstalling {name}...[/bold yellow]")
192
+ app_info = _prompt_select_app(results, name, "uninstall")
193
+ console.print(format_app_panel(app_info))
194
+
195
+ if not yes:
196
+ confirm = typer.confirm("Uninstall this application?")
197
+ if not confirm:
198
+ print("Uninstallation cancelled.")
199
+ raise typer.Exit(1)
200
+
201
+ success = asyncio.run(hub.uninstall(app_info, clean_uninstall))
202
+ if success:
203
+ console.print(f"[green]Successfully uninstalled {name}.[/green]")
204
+ else:
205
+ console.print(
206
+ f"[red]Failed to uninstall {name} or application not found.[/red]"
207
+ )
208
+
209
+
210
+ @cli_app.command(name="info")
211
+ def info(
212
+ name: str = typer.Argument(..., help="Name of the application"),
213
+ output_json: bool = typer.Option(False, "--json", help="Output as JSON"),
214
+ ):
215
+ """Show detailed information about an application."""
216
+ try:
217
+ app_info = asyncio.run(hub.info(query=name))
218
+ except AppNotFoundError:
219
+ console.print(f"[red]No application found matching '{name}'.[/red]")
220
+ raise typer.Exit(1)
221
+
222
+ if output_json:
223
+ console.print(to_json_single(app_info))
224
+ return
225
+ console.print(format_app_panel(app_info))
226
+
227
+
228
+ @cli_app.command(name="storage")
229
+ def storage(
230
+ formats: list[AppFormat] | None = typer.Option(
231
+ None, "--format", "-f", help="Filter by format"
232
+ ),
233
+ top: int | None = typer.Option(None, "--top", "-t", help="Show top N apps by size"),
234
+ output_json: bool = typer.Option(False, "--json", help="Output as JSON"),
235
+ ):
236
+ """Show disk usage by installed applications."""
237
+ apps = asyncio.run(hub.storage(formats=formats, top=top))
238
+
239
+ if output_json:
240
+ console.print(to_json(apps))
241
+ return
242
+
243
+ console.print(format_storage_table(apps, top=None)) # already sliced
244
+
245
+
246
+ @cli_app.command(name="history")
247
+ def history(
248
+ formats: list[AppFormat] | None = typer.Option(
249
+ None, "--format", "-f", help="Filter by format"
250
+ ),
251
+ action_categories: list[LifeCycleEvent] | None = typer.Option(
252
+ None, "--action-categories", "-a", help="Filter by action categories"
253
+ ),
254
+ output_json: bool = typer.Option(False, "--json", help="Output as JSON"),
255
+ order_by: str = typer.Option(
256
+ None, "--sort", "-s", help="Sort by field: name, version, timestamp"
257
+ ),
258
+ descending: bool = typer.Option(False, "--desc", "-d", help="Sort Descending"),
259
+ top: int = typer.Option(None, "--top", "-t", help="Show top N records"),
260
+ ):
261
+ """Show installation/uninstallation history."""
262
+ history_result = asyncio.run(
263
+ hub.history(formats=formats, action_categories=action_categories)
264
+ )
265
+
266
+ if order_by:
267
+ history_result.sort(key=lambda x: getattr(x, order_by, "") or "")
268
+ if descending:
269
+ history_result.reverse()
270
+ if top is not None:
271
+ history_result = history_result[:top]
272
+ if output_json:
273
+ console.print(to_json(history_result))
274
+ return
275
+
276
+ console.print(format_history_table(history_result))
@@ -0,0 +1,136 @@
1
+ from rich import box
2
+ from rich.console import Console
3
+ from rich.panel import Panel
4
+ from rich.table import Table
5
+
6
+ from apphub.core.models import AppManifest, AppRuntime, HistoryRecords, LifeCycleEvent
7
+
8
+ console = Console()
9
+
10
+
11
+ def _format_size(size_bytes: int | None) -> str:
12
+ if size_bytes is None:
13
+ return "—"
14
+ size = float(size_bytes)
15
+ for unit in ("B", "KB", "MB", "GB"):
16
+ if size < 1024:
17
+ return f"{size:.1f} {unit}"
18
+ size /= 1024
19
+
20
+ return f"{size:.1f} TB"
21
+
22
+
23
+ def _styled_table(title: str) -> Table:
24
+ """Shared Rich table chrome for list/history views."""
25
+ return Table(
26
+ title=title,
27
+ box=box.ROUNDED,
28
+ highlight=True,
29
+ show_lines=False,
30
+ header_style="bold cyan",
31
+ )
32
+
33
+
34
+ def format_app_table(apps: list[AppManifest], title: str = "Applications") -> Table:
35
+ """Styled Rich table for a list of AppManifest objects."""
36
+ table = _styled_table(title)
37
+ table.add_column("Name", style="bold white", min_width=20)
38
+ table.add_column("Format", style="yellow", justify="center")
39
+ table.add_column("Version", style="magenta")
40
+ table.add_column("Publisher", style="green")
41
+ table.add_column("Category", style="dim cyan")
42
+
43
+ for app in apps:
44
+ table.add_row(
45
+ app.name,
46
+ app.format.value,
47
+ app.version,
48
+ app.publisher or "—",
49
+ app.category or "—",
50
+ )
51
+ return table
52
+
53
+
54
+ def format_app_panel(app: AppManifest) -> Panel:
55
+ """Rich Panel for `apphub info <name>`."""
56
+ lines = [
57
+ f"[bold]Name[/bold]: {app.name}",
58
+ f"[bold]ID[/bold]: {app.id}",
59
+ f"[bold]Format[/bold]: {app.format.value}",
60
+ f"[bold]Version[/bold]: {app.version}",
61
+ f"[bold]Publisher[/bold]: {app.publisher or '—'}",
62
+ f"[bold]Category[/bold]: {app.category or '—'}",
63
+ f"[bold]Installed[/bold]: {'✓' if app.installed else '✗'}",
64
+ f"[bold]Size[/bold]: {_format_size(app.size_bytes)}",
65
+ ]
66
+ if app.description:
67
+ lines.append(f"\n[bold]Description[/bold]:\n {app.description}")
68
+
69
+ if app.icon:
70
+ lines.append(f"\n[bold]Icon[/bold]: {app.icon}")
71
+
72
+ if app.runtime != AppRuntime.NATIVE.value:
73
+ lines.append(f"\n[bold]Runtime[/bold]: {app.runtime.value}")
74
+
75
+ return Panel(
76
+ "\n".join(lines),
77
+ title=f"[bold cyan]{app.name}[/bold cyan]",
78
+ border_style="cyan",
79
+ padding=(1, 2),
80
+ )
81
+
82
+
83
+ def format_storage_table(apps: list[AppManifest], top: int | None = None) -> Table:
84
+ """Storage breakdown table, sorted by size descending."""
85
+ sized = sorted(
86
+ [a for a in apps if a.size_bytes is not None],
87
+ key=lambda a: a.size_bytes or 0,
88
+ reverse=True,
89
+ )
90
+ unsized = [a for a in apps if a.size_bytes is None]
91
+ ordered = sized + unsized
92
+
93
+ if top is not None:
94
+ ordered = ordered[:top]
95
+
96
+ table = Table(
97
+ title="Storage Usage",
98
+ box=box.ROUNDED,
99
+ header_style="bold cyan",
100
+ show_lines=False,
101
+ )
102
+ table.add_column("Name", style="bold white", min_width=20)
103
+ table.add_column("Format", style="yellow", justify="center")
104
+ table.add_column("Size", style="magenta", justify="right")
105
+
106
+ for app in ordered:
107
+ table.add_row(app.name, app.format.value, _format_size(app.size_bytes))
108
+
109
+ return table
110
+
111
+
112
+ def format_history_table(
113
+ history_records: list[HistoryRecords], title: str = "HistoryRecords"
114
+ ) -> Table:
115
+ """Styled Rich table for a list of HistoryRecords objects."""
116
+
117
+ table = _styled_table(title)
118
+ table.add_column("Name", style="bold white", min_width=20)
119
+ table.add_column("Format", style="yellow", justify="center")
120
+ table.add_column("Timestamp", style="magenta")
121
+ table.add_column("LifeCycle Event", style="red")
122
+ table.add_column("Version", style="green")
123
+
124
+ for record in history_records:
125
+ table.add_row(
126
+ record.app_name,
127
+ record.format.value,
128
+ record.timestamp.strftime("%B %d, %Y, %I:%M %p"),
129
+ record.lifecycle_event,
130
+ (
131
+ f"{record.old_version_id} -> {record.version_id}"
132
+ if record.lifecycle_event == LifeCycleEvent.UPGRADED
133
+ else record.version_id
134
+ ),
135
+ )
136
+ return table
@@ -0,0 +1,14 @@
1
+ import json
2
+
3
+ from apphub.core.models import AppManifest, HistoryRecords
4
+
5
+
6
+ def to_json(apps: list[AppManifest | HistoryRecords], indent: int = 2) -> str:
7
+ return json.dumps(
8
+ [app.model_dump(mode="json") for app in apps],
9
+ indent=indent,
10
+ )
11
+
12
+
13
+ def to_json_single(app: AppManifest, indent: int = 2) -> str:
14
+ return json.dumps(app.model_dump(mode="json"), indent=indent)
File without changes
@@ -0,0 +1,52 @@
1
+ class AppHubError(Exception):
2
+ """Base error for AppHub."""
3
+
4
+ pass
5
+
6
+
7
+ class PluginError(AppHubError):
8
+ """Raised when Plugin need to raise exception"""
9
+
10
+ def __init__(self, plugin_name: str, msg: str):
11
+ super().__init__(msg)
12
+ self.plugin_name = plugin_name
13
+
14
+
15
+ class PluginNotAvailableError(PluginError):
16
+ """Raised when a required backend (e.g. snap, flatpak) is not installed."""
17
+
18
+ def __init__(self, plugin_name: str):
19
+ super().__init__(
20
+ plugin_name=plugin_name,
21
+ msg=f"Plugin backend not available: '{plugin_name}'. Is it installed?",
22
+ )
23
+
24
+
25
+ class AppNotFoundError(AppHubError):
26
+ """Raised when a requested application cannot be found."""
27
+
28
+ def __init__(self, app_name: str):
29
+ super().__init__(f"Application not found: '{app_name}'")
30
+ self.app_name = app_name
31
+
32
+
33
+ class InstallError(AppHubError):
34
+ """Raised when an installation fails."""
35
+
36
+ def __init__(self, app_name: str, reason: str = ""):
37
+ msg = f"Failed to install '{app_name}'"
38
+ if reason:
39
+ msg += f": {reason}"
40
+ super().__init__(msg)
41
+ self.app_name = app_name
42
+
43
+
44
+ class UninstallError(AppHubError):
45
+ """Raised when an uninstallation fails."""
46
+
47
+ def __init__(self, app_name: str, reason: str = ""):
48
+ msg = f"Failed to uninstall '{app_name}'"
49
+ if reason:
50
+ msg += f": {reason}"
51
+ super().__init__(msg)
52
+ self.app_name = app_name