4u-cli 1.0.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.
- 4u_cli-1.0.0.dist-info/METADATA +73 -0
- 4u_cli-1.0.0.dist-info/RECORD +20 -0
- 4u_cli-1.0.0.dist-info/WHEEL +5 -0
- 4u_cli-1.0.0.dist-info/entry_points.txt +2 -0
- 4u_cli-1.0.0.dist-info/top_level.txt +1 -0
- fourutil/__init__.py +3 -0
- fourutil/commands/__init__.py +0 -0
- fourutil/commands/install.py +80 -0
- fourutil/commands/list_tools.py +54 -0
- fourutil/commands/run.py +37 -0
- fourutil/commands/search.py +66 -0
- fourutil/commands/uninstall.py +42 -0
- fourutil/commands/update.py +110 -0
- fourutil/config.py +30 -0
- fourutil/main.py +80 -0
- fourutil/supabase_client.py +17 -0
- fourutil/utils/__init__.py +0 -0
- fourutil/utils/downloader.py +64 -0
- fourutil/utils/manifest.py +65 -0
- fourutil/utils/printer.py +65 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: 4u-cli
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: 4u - A modern CLI package manager for Python tools
|
|
5
|
+
Author: 4u Team
|
|
6
|
+
License: MIT
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Requires-Python: >=3.8
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
Requires-Dist: click>=8.0
|
|
14
|
+
Requires-Dist: rich>=13.0
|
|
15
|
+
Requires-Dist: requests>=2.28
|
|
16
|
+
Requires-Dist: supabase>=2.0
|
|
17
|
+
Dynamic: author
|
|
18
|
+
Dynamic: classifier
|
|
19
|
+
Dynamic: description
|
|
20
|
+
Dynamic: description-content-type
|
|
21
|
+
Dynamic: license
|
|
22
|
+
Dynamic: requires-dist
|
|
23
|
+
Dynamic: requires-python
|
|
24
|
+
Dynamic: summary
|
|
25
|
+
|
|
26
|
+
# 4u CLI
|
|
27
|
+
|
|
28
|
+
> A modern CLI package manager for Python tools ⚡
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install -e .
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Usage
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
# Show help and banner
|
|
40
|
+
4u
|
|
41
|
+
|
|
42
|
+
# Search for tools
|
|
43
|
+
4u search <query>
|
|
44
|
+
|
|
45
|
+
# Install a tool
|
|
46
|
+
4u install <tool_name>
|
|
47
|
+
|
|
48
|
+
# List installed tools
|
|
49
|
+
4u list
|
|
50
|
+
|
|
51
|
+
# Update tools
|
|
52
|
+
4u update # Update all
|
|
53
|
+
4u update <tool> # Update specific tool
|
|
54
|
+
|
|
55
|
+
# Uninstall a tool
|
|
56
|
+
4u uninstall <tool_name>
|
|
57
|
+
|
|
58
|
+
# Run an installed tool directly
|
|
59
|
+
4u <tool_name> [args...]
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Configuration
|
|
63
|
+
|
|
64
|
+
Set environment variables for Supabase connection:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
export FOURU_SUPABASE_URL="https://your-project.supabase.co"
|
|
68
|
+
export FOURU_SUPABASE_KEY="your-anon-key"
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## License
|
|
72
|
+
|
|
73
|
+
MIT
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
fourutil/__init__.py,sha256=9a55rVNAYnHytSKRfRDF96l2by3Q8j4X6ab0aFoFjJY,81
|
|
2
|
+
fourutil/config.py,sha256=UaPqNqiarK6tHaHPwLRcS9Rgyk0AFZa1oNA_NkrJdSQ,954
|
|
3
|
+
fourutil/main.py,sha256=H9zbZ_Tkw4WrToppIXvCMA3oqLk9wbhYt0_fQDhb070,2753
|
|
4
|
+
fourutil/supabase_client.py,sha256=PDd2AV-b1v3mYWtuXmisEoIh2HDoh39V0G2o5RdAZZ8,465
|
|
5
|
+
fourutil/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
fourutil/commands/install.py,sha256=y-4PQvxBrYUW49mJFBD-jM7H17e4HDtQ1C30qOduRJI,2543
|
|
7
|
+
fourutil/commands/list_tools.py,sha256=GTBG9b11mi0PG2DtzzVhm3LQBXvIlqiQwmxcMuBCuqk,1796
|
|
8
|
+
fourutil/commands/run.py,sha256=j1dPYmp3dCvtH50lhwXxQ4qcpXDpO3AVmF5MEUGsXF8,1079
|
|
9
|
+
fourutil/commands/search.py,sha256=QGMIWoHnc9ELnJ0WilEgY9NkOYvZR55lW9L1C2yrMJI,2086
|
|
10
|
+
fourutil/commands/uninstall.py,sha256=XdTEOM9Ya32_wkaCuNdQEwGbRb2SuD36xmMeIulrm_4,1081
|
|
11
|
+
fourutil/commands/update.py,sha256=IQzm2R0_B1uYRLo-O9PQ46iOQToBNEPFyFMx33ETHWY,3392
|
|
12
|
+
fourutil/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
+
fourutil/utils/downloader.py,sha256=kgO4Snilrg9WU0Ez9K6cSeeLUgaYf7ppSCaz58A4WSY,1924
|
|
14
|
+
fourutil/utils/manifest.py,sha256=S9-ix4A029inRjWV2SF-in_a_Z3w-0CBQO4rrVqznYI,1905
|
|
15
|
+
fourutil/utils/printer.py,sha256=8JiCXb7srvevgSopHbNP56XMtBqZrewQR9xQmUUn6bY,2093
|
|
16
|
+
4u_cli-1.0.0.dist-info/METADATA,sha256=OOumaitucieuCPOBOavP_WYU7VMH0Ro2qHbCPdHBO0M,1409
|
|
17
|
+
4u_cli-1.0.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
18
|
+
4u_cli-1.0.0.dist-info/entry_points.txt,sha256=J6qjD48VtxVGpix0AazD1eDzHDSCNoTv-glUGAcrXl0,41
|
|
19
|
+
4u_cli-1.0.0.dist-info/top_level.txt,sha256=BQg2B1X3b4fkmyvhCsTnZpOLlKkxaWvBrWThTD--9hs,9
|
|
20
|
+
4u_cli-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
fourutil
|
fourutil/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""
|
|
2
|
+
أمر التثبيت: 4u install <slug>
|
|
3
|
+
Install command – downloads a tool from Supabase and registers it locally.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
from rich.panel import Panel
|
|
8
|
+
|
|
9
|
+
from fourutil.supabase_client import get_client
|
|
10
|
+
from fourutil.config import TOOLS_DIR, TOOLS_TABLE
|
|
11
|
+
from fourutil.utils.printer import console, print_success, print_error, print_info, print_warning
|
|
12
|
+
from fourutil.utils.manifest import add_tool, get_tool
|
|
13
|
+
from fourutil.utils.downloader import download_file
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@click.command("install")
|
|
17
|
+
@click.argument("slug")
|
|
18
|
+
def install(slug: str) -> None:
|
|
19
|
+
"""Install a tool by its slug name. ⬇️"""
|
|
20
|
+
console.print()
|
|
21
|
+
console.print(f" 🔍 Looking up [bold cyan]{slug}[/bold cyan] …")
|
|
22
|
+
|
|
23
|
+
# --- استعلام Supabase ---
|
|
24
|
+
try:
|
|
25
|
+
client = get_client()
|
|
26
|
+
result = (
|
|
27
|
+
client.table(TOOLS_TABLE)
|
|
28
|
+
.select("*")
|
|
29
|
+
.eq("slug", slug)
|
|
30
|
+
.execute()
|
|
31
|
+
)
|
|
32
|
+
except Exception as exc:
|
|
33
|
+
print_error(f"Failed to query Supabase: {exc}")
|
|
34
|
+
return
|
|
35
|
+
|
|
36
|
+
if not result.data:
|
|
37
|
+
print_error(f"Tool [bold]{slug}[/bold] not found in the registry.")
|
|
38
|
+
console.print(" 💡 Try [bold]4u search <query>[/bold] to find tools.", style="dim")
|
|
39
|
+
return
|
|
40
|
+
|
|
41
|
+
tool = result.data[0]
|
|
42
|
+
remote_version = tool.get("version", "0.0.0")
|
|
43
|
+
file_url = tool.get("file_url", "")
|
|
44
|
+
tool_name = tool.get("name", slug)
|
|
45
|
+
description = tool.get("description", "")
|
|
46
|
+
|
|
47
|
+
# --- هل الأداة مثبتة بالفعل؟ ---
|
|
48
|
+
local = get_tool(slug)
|
|
49
|
+
if local and local.get("version") == remote_version:
|
|
50
|
+
print_info(f"[bold]{tool_name}[/bold] v{remote_version} is already installed and up to date.")
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
if not file_url:
|
|
54
|
+
print_error("No download URL found for this tool.")
|
|
55
|
+
return
|
|
56
|
+
|
|
57
|
+
# --- تحميل الملف ---
|
|
58
|
+
filename = f"{slug}.py"
|
|
59
|
+
dest = TOOLS_DIR / filename
|
|
60
|
+
|
|
61
|
+
console.print(f" ⬇️ Downloading [bold]{tool_name}[/bold] v{remote_version} …")
|
|
62
|
+
ok = download_file(file_url, dest)
|
|
63
|
+
if not ok:
|
|
64
|
+
return
|
|
65
|
+
|
|
66
|
+
# --- تسجيل في manifest ---
|
|
67
|
+
add_tool(slug, remote_version, filename)
|
|
68
|
+
|
|
69
|
+
# --- رسالة نجاح ---
|
|
70
|
+
console.print()
|
|
71
|
+
console.print(
|
|
72
|
+
Panel(
|
|
73
|
+
f"[bold green]{tool_name}[/bold green] v{remote_version} installed successfully!\n"
|
|
74
|
+
f"[dim]{description}[/dim]\n\n"
|
|
75
|
+
f"Run it with: [bold cyan]4u {slug}[/bold cyan]",
|
|
76
|
+
title="✅ Installed",
|
|
77
|
+
border_style="green",
|
|
78
|
+
padding=(1, 2),
|
|
79
|
+
)
|
|
80
|
+
)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""
|
|
2
|
+
أمر العرض: 4u list
|
|
3
|
+
List command – shows all locally installed tools in a rich table.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
from rich.table import Table
|
|
8
|
+
from rich.panel import Panel
|
|
9
|
+
|
|
10
|
+
from fourutil.utils.printer import console, print_info
|
|
11
|
+
from fourutil.utils.manifest import get_all_tools
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@click.command("list")
|
|
15
|
+
def list_tools() -> None:
|
|
16
|
+
"""List all installed tools. 📦"""
|
|
17
|
+
console.print()
|
|
18
|
+
|
|
19
|
+
tools = get_all_tools()
|
|
20
|
+
|
|
21
|
+
if not tools:
|
|
22
|
+
print_info("No tools installed yet.")
|
|
23
|
+
console.print(" 💡 Use [bold cyan]4u search <query>[/bold cyan] to find tools.", style="dim")
|
|
24
|
+
console.print(" 💡 Use [bold cyan]4u install <name>[/bold cyan] to install one.", style="dim")
|
|
25
|
+
return
|
|
26
|
+
|
|
27
|
+
table = Table(
|
|
28
|
+
title="📦 Installed Tools",
|
|
29
|
+
title_style="bold cyan",
|
|
30
|
+
border_style="bright_blue",
|
|
31
|
+
show_lines=True,
|
|
32
|
+
padding=(0, 1),
|
|
33
|
+
)
|
|
34
|
+
table.add_column("#", style="dim", justify="right", width=4)
|
|
35
|
+
table.add_column("Name", style="bold white", min_width=15)
|
|
36
|
+
table.add_column("Version", style="green", justify="center", min_width=10)
|
|
37
|
+
table.add_column("Installed At", style="yellow", min_width=20)
|
|
38
|
+
table.add_column("File", style="dim", min_width=15)
|
|
39
|
+
|
|
40
|
+
for idx, (slug, info) in enumerate(tools.items(), start=1):
|
|
41
|
+
installed_at = info.get("installed_at", "—")
|
|
42
|
+
# تنسيق التاريخ لعرض أجمل
|
|
43
|
+
if "T" in installed_at:
|
|
44
|
+
installed_at = installed_at.replace("T", " ").split(".")[0]
|
|
45
|
+
table.add_row(
|
|
46
|
+
str(idx),
|
|
47
|
+
slug,
|
|
48
|
+
info.get("version", "?"),
|
|
49
|
+
installed_at,
|
|
50
|
+
info.get("file", "—"),
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
console.print(table)
|
|
54
|
+
console.print(f"\n Total: [bold]{len(tools)}[/bold] tool(s) installed.\n", style="dim")
|
fourutil/commands/run.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""
|
|
2
|
+
المشغّل الديناميكي للأدوات المثبتة
|
|
3
|
+
Dynamic tool runner – executes an installed tool as a subprocess.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import sys
|
|
7
|
+
import subprocess
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from fourutil.config import TOOLS_DIR
|
|
11
|
+
from fourutil.utils.printer import print_error, console
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def run_tool(name: str, args: tuple) -> None:
|
|
15
|
+
"""
|
|
16
|
+
تشغيل أداة مثبتة باسمها.
|
|
17
|
+
Runs ~/.4u/commands/<name>.py with the remaining CLI arguments.
|
|
18
|
+
"""
|
|
19
|
+
tool_path = TOOLS_DIR / f"{name}.py"
|
|
20
|
+
|
|
21
|
+
if not tool_path.exists():
|
|
22
|
+
print_error(f"Tool [bold]{name}[/bold] is not installed.")
|
|
23
|
+
console.print(
|
|
24
|
+
f" 💡 Use [bold cyan]4u install {name}[/bold cyan] to install it.",
|
|
25
|
+
style="dim",
|
|
26
|
+
)
|
|
27
|
+
sys.exit(1)
|
|
28
|
+
|
|
29
|
+
cmd = [sys.executable, str(tool_path)] + list(args)
|
|
30
|
+
try:
|
|
31
|
+
result = subprocess.run(cmd)
|
|
32
|
+
sys.exit(result.returncode)
|
|
33
|
+
except KeyboardInterrupt:
|
|
34
|
+
sys.exit(130)
|
|
35
|
+
except Exception as exc:
|
|
36
|
+
print_error(f"Failed to run [bold]{name}[/bold]: {exc}")
|
|
37
|
+
sys.exit(1)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""
|
|
2
|
+
أمر البحث: 4u search <query>
|
|
3
|
+
Search command – queries Supabase for tools matching a keyword.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
from rich.table import Table
|
|
8
|
+
from rich.panel import Panel
|
|
9
|
+
|
|
10
|
+
from fourutil.supabase_client import get_client
|
|
11
|
+
from fourutil.config import TOOLS_TABLE
|
|
12
|
+
from fourutil.utils.printer import console, print_error, print_info
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@click.command("search")
|
|
16
|
+
@click.argument("query")
|
|
17
|
+
def search(query: str) -> None:
|
|
18
|
+
"""Search for tools in the registry. 🔎"""
|
|
19
|
+
console.print()
|
|
20
|
+
console.print(f" 🔎 Searching for [bold cyan]{query}[/bold cyan] …\n")
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
client = get_client()
|
|
24
|
+
# بحث في الاسم أو الوصف باستخدام ilike
|
|
25
|
+
result = (
|
|
26
|
+
client.table(TOOLS_TABLE)
|
|
27
|
+
.select("*")
|
|
28
|
+
.or_(f"name.ilike.%{query}%,description.ilike.%{query}%,slug.ilike.%{query}%")
|
|
29
|
+
.execute()
|
|
30
|
+
)
|
|
31
|
+
except Exception as exc:
|
|
32
|
+
print_error(f"Search failed: {exc}")
|
|
33
|
+
return
|
|
34
|
+
|
|
35
|
+
if not result.data:
|
|
36
|
+
print_info(f"No tools found matching [bold]{query}[/bold].")
|
|
37
|
+
return
|
|
38
|
+
|
|
39
|
+
table = Table(
|
|
40
|
+
title=f"🔎 Search Results for \"{query}\"",
|
|
41
|
+
title_style="bold cyan",
|
|
42
|
+
border_style="bright_blue",
|
|
43
|
+
show_lines=True,
|
|
44
|
+
padding=(0, 1),
|
|
45
|
+
)
|
|
46
|
+
table.add_column("#", style="dim", justify="right", width=4)
|
|
47
|
+
table.add_column("Slug", style="bold white", min_width=12)
|
|
48
|
+
table.add_column("Name", style="cyan", min_width=15)
|
|
49
|
+
table.add_column("Version", style="green", justify="center", min_width=8)
|
|
50
|
+
table.add_column("Description", style="white", min_width=30)
|
|
51
|
+
|
|
52
|
+
for idx, tool in enumerate(result.data, start=1):
|
|
53
|
+
table.add_row(
|
|
54
|
+
str(idx),
|
|
55
|
+
tool.get("slug", "—"),
|
|
56
|
+
tool.get("name", "—"),
|
|
57
|
+
tool.get("version", "—"),
|
|
58
|
+
tool.get("description", "—"),
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
console.print(table)
|
|
62
|
+
console.print(
|
|
63
|
+
f"\n Found [bold]{len(result.data)}[/bold] tool(s). "
|
|
64
|
+
f"Install with [bold cyan]4u install <slug>[/bold cyan]\n",
|
|
65
|
+
style="dim",
|
|
66
|
+
)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""
|
|
2
|
+
أمر الحذف: 4u uninstall <slug>
|
|
3
|
+
Uninstall command – removes a tool's file and manifest entry.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
from rich.panel import Panel
|
|
8
|
+
|
|
9
|
+
from fourutil.config import TOOLS_DIR
|
|
10
|
+
from fourutil.utils.printer import console, print_success, print_error
|
|
11
|
+
from fourutil.utils.manifest import remove_tool, get_tool
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@click.command("uninstall")
|
|
15
|
+
@click.argument("slug")
|
|
16
|
+
def uninstall(slug: str) -> None:
|
|
17
|
+
"""Uninstall a tool by its slug name. 🗑️"""
|
|
18
|
+
console.print()
|
|
19
|
+
|
|
20
|
+
local = get_tool(slug)
|
|
21
|
+
if not local:
|
|
22
|
+
print_error(f"[bold]{slug}[/bold] is not installed.")
|
|
23
|
+
return
|
|
24
|
+
|
|
25
|
+
# --- حذف الملف ---
|
|
26
|
+
filename = local.get("file", f"{slug}.py")
|
|
27
|
+
file_path = TOOLS_DIR / filename
|
|
28
|
+
if file_path.exists():
|
|
29
|
+
file_path.unlink()
|
|
30
|
+
|
|
31
|
+
# --- حذف من manifest ---
|
|
32
|
+
remove_tool(slug)
|
|
33
|
+
|
|
34
|
+
# --- رسالة نجاح ---
|
|
35
|
+
console.print(
|
|
36
|
+
Panel(
|
|
37
|
+
f"[bold]{slug}[/bold] has been uninstalled. 👋",
|
|
38
|
+
title="🗑️ Removed",
|
|
39
|
+
border_style="red",
|
|
40
|
+
padding=(0, 2),
|
|
41
|
+
)
|
|
42
|
+
)
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""
|
|
2
|
+
أمر التحديث: 4u update [slug]
|
|
3
|
+
Update command – checks for newer versions and updates tools.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
from rich.panel import Panel
|
|
8
|
+
|
|
9
|
+
from fourutil.supabase_client import get_client
|
|
10
|
+
from fourutil.config import TOOLS_DIR, TOOLS_TABLE
|
|
11
|
+
from fourutil.utils.printer import console, print_success, print_error, print_info, print_warning
|
|
12
|
+
from fourutil.utils.manifest import add_tool, get_tool, get_all_tools
|
|
13
|
+
from fourutil.utils.downloader import download_file
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _update_single(slug: str) -> bool:
|
|
17
|
+
"""
|
|
18
|
+
تحديث أداة واحدة.
|
|
19
|
+
Returns True if an update was applied, False otherwise.
|
|
20
|
+
"""
|
|
21
|
+
local = get_tool(slug)
|
|
22
|
+
if not local:
|
|
23
|
+
print_error(f"[bold]{slug}[/bold] is not installed.")
|
|
24
|
+
return False
|
|
25
|
+
|
|
26
|
+
local_version = local.get("version", "0.0.0")
|
|
27
|
+
|
|
28
|
+
# --- جلب الإصدار البعيد ---
|
|
29
|
+
try:
|
|
30
|
+
client = get_client()
|
|
31
|
+
result = (
|
|
32
|
+
client.table(TOOLS_TABLE)
|
|
33
|
+
.select("*")
|
|
34
|
+
.eq("slug", slug)
|
|
35
|
+
.execute()
|
|
36
|
+
)
|
|
37
|
+
except Exception as exc:
|
|
38
|
+
print_error(f"Failed to query Supabase for {slug}: {exc}")
|
|
39
|
+
return False
|
|
40
|
+
|
|
41
|
+
if not result.data:
|
|
42
|
+
print_warning(f"[bold]{slug}[/bold] no longer exists in the registry.")
|
|
43
|
+
return False
|
|
44
|
+
|
|
45
|
+
tool = result.data[0]
|
|
46
|
+
remote_version = tool.get("version", "0.0.0")
|
|
47
|
+
file_url = tool.get("file_url", "")
|
|
48
|
+
tool_name = tool.get("name", slug)
|
|
49
|
+
|
|
50
|
+
if remote_version == local_version:
|
|
51
|
+
print_info(f"[bold]{tool_name}[/bold] v{local_version} is already up to date.")
|
|
52
|
+
return False
|
|
53
|
+
|
|
54
|
+
if not file_url:
|
|
55
|
+
print_error(f"No download URL found for [bold]{slug}[/bold].")
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
console.print(f" ⬆️ Updating [bold]{tool_name}[/bold] {local_version} → {remote_version} …")
|
|
59
|
+
filename = f"{slug}.py"
|
|
60
|
+
dest = TOOLS_DIR / filename
|
|
61
|
+
|
|
62
|
+
ok = download_file(file_url, dest)
|
|
63
|
+
if not ok:
|
|
64
|
+
return False
|
|
65
|
+
|
|
66
|
+
add_tool(slug, remote_version, filename)
|
|
67
|
+
print_success(f"[bold]{tool_name}[/bold] updated to v{remote_version}")
|
|
68
|
+
return True
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@click.command("update")
|
|
72
|
+
@click.argument("slug", required=False, default=None)
|
|
73
|
+
def update(slug: str | None) -> None:
|
|
74
|
+
"""Update one or all installed tools. 🔄"""
|
|
75
|
+
console.print()
|
|
76
|
+
|
|
77
|
+
if slug:
|
|
78
|
+
# --- تحديث أداة واحدة ---
|
|
79
|
+
_update_single(slug)
|
|
80
|
+
else:
|
|
81
|
+
# --- تحديث جميع الأدوات ---
|
|
82
|
+
tools = get_all_tools()
|
|
83
|
+
if not tools:
|
|
84
|
+
print_info("No tools installed yet. Use [bold]4u install <name>[/bold].")
|
|
85
|
+
return
|
|
86
|
+
|
|
87
|
+
console.print(" 🔄 Checking updates for all installed tools …\n")
|
|
88
|
+
updated_count = 0
|
|
89
|
+
for tool_slug in tools:
|
|
90
|
+
if _update_single(tool_slug):
|
|
91
|
+
updated_count += 1
|
|
92
|
+
console.print()
|
|
93
|
+
|
|
94
|
+
if updated_count == 0:
|
|
95
|
+
console.print(
|
|
96
|
+
Panel(
|
|
97
|
+
"All tools are already up to date! 🎉",
|
|
98
|
+
border_style="green",
|
|
99
|
+
padding=(0, 2),
|
|
100
|
+
)
|
|
101
|
+
)
|
|
102
|
+
else:
|
|
103
|
+
console.print(
|
|
104
|
+
Panel(
|
|
105
|
+
f"Updated [bold]{updated_count}[/bold] tool(s) successfully!",
|
|
106
|
+
title="✅ Update Complete",
|
|
107
|
+
border_style="green",
|
|
108
|
+
padding=(0, 2),
|
|
109
|
+
)
|
|
110
|
+
)
|
fourutil/config.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""
|
|
2
|
+
تكوين الإعدادات الأساسية لأداة 4u
|
|
3
|
+
Configuration constants for the 4u CLI tool.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
# --- المسارات المحلية (Local Paths) ---
|
|
10
|
+
HOME_DIR = Path.home()
|
|
11
|
+
FOURU_DIR = HOME_DIR / ".4u"
|
|
12
|
+
TOOLS_DIR = FOURU_DIR / "commands"
|
|
13
|
+
MANIFEST_FILE = FOURU_DIR / "manifest.json"
|
|
14
|
+
|
|
15
|
+
# --- إعدادات Supabase ---
|
|
16
|
+
SUPABASE_URL = os.environ.get(
|
|
17
|
+
"FOURU_SUPABASE_URL",
|
|
18
|
+
"https://nvdmkhehhelulfasmpob.supabase.co",
|
|
19
|
+
)
|
|
20
|
+
SUPABASE_KEY = os.environ.get(
|
|
21
|
+
"FOURU_SUPABASE_KEY",
|
|
22
|
+
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im52ZG1raGVoaGVsdWxmYXNtcG9iIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc5MzYzNTM2NiwiZXhwIjoyMTA5MjExMzY2fQ.ZNq5iLFMGMFdBgKmF_jujnEu1LRY-CIRf5548og44uM",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
# --- أسماء الجداول والتخزين (Table & Bucket Names) ---
|
|
26
|
+
TOOLS_TABLE = "tools"
|
|
27
|
+
TOOLS_BUCKET = "tools-bucket"
|
|
28
|
+
|
|
29
|
+
# --- إصدار التطبيق ---
|
|
30
|
+
APP_VERSION = "1.0.0"
|
fourutil/main.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""
|
|
2
|
+
نقطة الدخول الرئيسية لـ 4u CLI
|
|
3
|
+
Main entry point – CLI router with dynamic tool dispatch.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import sys
|
|
7
|
+
import os
|
|
8
|
+
|
|
9
|
+
# ضبط دعم UTF-8 في ويندوز لتفادي UnicodeEncodeError مع الرموز التعبيرية
|
|
10
|
+
if sys.platform == "win32":
|
|
11
|
+
try:
|
|
12
|
+
if sys.stdout.encoding.lower() not in ("utf-8", "utf8"):
|
|
13
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
14
|
+
if sys.stderr.encoding.lower() not in ("utf-8", "utf8"):
|
|
15
|
+
sys.stderr.reconfigure(encoding="utf-8")
|
|
16
|
+
except Exception:
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
import click
|
|
20
|
+
|
|
21
|
+
from fourutil import __version__
|
|
22
|
+
from fourutil.utils.printer import print_banner, console
|
|
23
|
+
from fourutil.commands.install import install
|
|
24
|
+
from fourutil.commands.update import update
|
|
25
|
+
from fourutil.commands.uninstall import uninstall
|
|
26
|
+
from fourutil.commands.list_tools import list_tools
|
|
27
|
+
from fourutil.commands.search import search
|
|
28
|
+
from fourutil.commands.run import run_tool
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# --- الأوامر المدمجة ---
|
|
32
|
+
BUILTIN_COMMANDS = {"install", "update", "uninstall", "list", "search", "--help", "--version", "-h"}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class FourUCLI(click.Group):
|
|
36
|
+
"""
|
|
37
|
+
مجموعة أوامر مخصصة تدعم التوجيه الديناميكي.
|
|
38
|
+
Custom click Group that falls back to running installed tools
|
|
39
|
+
when the sub-command is not a built-in.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def parse_args(self, ctx: click.Context, args: list[str]) -> list[str]:
|
|
43
|
+
"""Override to intercept unknown commands before click raises an error."""
|
|
44
|
+
# إذا لم تكن هناك وسائط، اعرض الشعار
|
|
45
|
+
if not args:
|
|
46
|
+
print_banner()
|
|
47
|
+
ctx.exit(0)
|
|
48
|
+
return [] # لن يصل إلى هنا لكن مطلوب للتوقيع
|
|
49
|
+
|
|
50
|
+
# تحقق مما إذا كان الأمر الأول مدمجاً
|
|
51
|
+
first = args[0]
|
|
52
|
+
if first not in BUILTIN_COMMANDS and not first.startswith("-"):
|
|
53
|
+
# أمر غير مدمج → حاول تشغيله كأداة مثبتة
|
|
54
|
+
run_tool(first, tuple(args[1:]))
|
|
55
|
+
ctx.exit(0)
|
|
56
|
+
return []
|
|
57
|
+
|
|
58
|
+
return super().parse_args(ctx, args)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@click.group(cls=FourUCLI, invoke_without_command=True)
|
|
62
|
+
@click.version_option(version=__version__, prog_name="4u", message="%(prog)s v%(version)s")
|
|
63
|
+
@click.pass_context
|
|
64
|
+
def cli(ctx: click.Context) -> None:
|
|
65
|
+
"""4u — A modern package manager for Python tools."""
|
|
66
|
+
# إذا لم يتم استدعاء أمر فرعي، اعرض الشعار
|
|
67
|
+
if ctx.invoked_subcommand is None:
|
|
68
|
+
print_banner()
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# --- تسجيل الأوامر المدمجة ---
|
|
72
|
+
cli.add_command(install)
|
|
73
|
+
cli.add_command(update)
|
|
74
|
+
cli.add_command(uninstall)
|
|
75
|
+
cli.add_command(list_tools, name="list")
|
|
76
|
+
cli.add_command(search)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
if __name__ == "__main__":
|
|
80
|
+
cli()
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""
|
|
2
|
+
مساعد الاتصال بـ Supabase
|
|
3
|
+
Supabase connection helper – creates a singleton client.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from supabase import create_client, Client
|
|
7
|
+
from fourutil.config import SUPABASE_URL, SUPABASE_KEY
|
|
8
|
+
|
|
9
|
+
_client: Client | None = None
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_client() -> Client:
|
|
13
|
+
"""إرجاع عميل Supabase (Return a Supabase client instance)."""
|
|
14
|
+
global _client
|
|
15
|
+
if _client is None:
|
|
16
|
+
_client = create_client(SUPABASE_URL, SUPABASE_KEY)
|
|
17
|
+
return _client
|
|
File without changes
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""
|
|
2
|
+
مساعد تحميل الملفات
|
|
3
|
+
File download helper – downloads tool scripts from remote URLs.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import requests
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from rich.progress import (
|
|
10
|
+
Progress,
|
|
11
|
+
SpinnerColumn,
|
|
12
|
+
TextColumn,
|
|
13
|
+
BarColumn,
|
|
14
|
+
DownloadColumn,
|
|
15
|
+
TransferSpeedColumn,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
from fourutil.utils.printer import console, print_error
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def download_file(url: str, dest: Path) -> bool:
|
|
22
|
+
"""
|
|
23
|
+
تحميل ملف من URL وحفظه في المسار المحدد.
|
|
24
|
+
Download a file from *url* and save it to *dest*.
|
|
25
|
+
Returns True on success, False on failure.
|
|
26
|
+
"""
|
|
27
|
+
try:
|
|
28
|
+
response = requests.get(url, stream=True, timeout=30)
|
|
29
|
+
response.raise_for_status()
|
|
30
|
+
|
|
31
|
+
total = int(response.headers.get("content-length", 0))
|
|
32
|
+
|
|
33
|
+
# تأكد من وجود المجلد الأب
|
|
34
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
35
|
+
|
|
36
|
+
with Progress(
|
|
37
|
+
SpinnerColumn(),
|
|
38
|
+
TextColumn("[bold blue]Downloading…"),
|
|
39
|
+
BarColumn(bar_width=30),
|
|
40
|
+
DownloadColumn(),
|
|
41
|
+
TransferSpeedColumn(),
|
|
42
|
+
console=console,
|
|
43
|
+
transient=True,
|
|
44
|
+
) as progress:
|
|
45
|
+
task = progress.add_task("download", total=total or None)
|
|
46
|
+
with open(dest, "wb") as f:
|
|
47
|
+
for chunk in response.iter_content(chunk_size=8192):
|
|
48
|
+
f.write(chunk)
|
|
49
|
+
progress.update(task, advance=len(chunk))
|
|
50
|
+
|
|
51
|
+
return True
|
|
52
|
+
|
|
53
|
+
except requests.ConnectionError:
|
|
54
|
+
print_error("Connection error – check your internet connection.")
|
|
55
|
+
return False
|
|
56
|
+
except requests.Timeout:
|
|
57
|
+
print_error("Download timed out. Please try again.")
|
|
58
|
+
return False
|
|
59
|
+
except requests.HTTPError as exc:
|
|
60
|
+
print_error(f"HTTP error: {exc.response.status_code}")
|
|
61
|
+
return False
|
|
62
|
+
except Exception as exc:
|
|
63
|
+
print_error(f"Download failed: {exc}")
|
|
64
|
+
return False
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""
|
|
2
|
+
مدير ملف manifest.json
|
|
3
|
+
Handles reading / writing the local manifest that tracks installed tools.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from fourutil.config import MANIFEST_FILE, FOURU_DIR, TOOLS_DIR
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _ensure_dirs() -> None:
|
|
14
|
+
"""تأكد من وجود المجلدات الضرورية"""
|
|
15
|
+
FOURU_DIR.mkdir(parents=True, exist_ok=True)
|
|
16
|
+
TOOLS_DIR.mkdir(parents=True, exist_ok=True)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def load_manifest() -> dict:
|
|
20
|
+
"""تحميل ملف manifest.json أو إرجاع قالب فارغ"""
|
|
21
|
+
_ensure_dirs()
|
|
22
|
+
if MANIFEST_FILE.exists():
|
|
23
|
+
with open(MANIFEST_FILE, "r", encoding="utf-8") as f:
|
|
24
|
+
return json.load(f)
|
|
25
|
+
return {"installed": {}}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def save_manifest(data: dict) -> None:
|
|
29
|
+
"""حفظ البيانات في manifest.json"""
|
|
30
|
+
_ensure_dirs()
|
|
31
|
+
with open(MANIFEST_FILE, "w", encoding="utf-8") as f:
|
|
32
|
+
json.dump(data, f, indent=2, ensure_ascii=False)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def add_tool(slug: str, version: str, filename: str) -> None:
|
|
36
|
+
"""إضافة أداة إلى manifest"""
|
|
37
|
+
manifest = load_manifest()
|
|
38
|
+
manifest["installed"][slug] = {
|
|
39
|
+
"version": version,
|
|
40
|
+
"installed_at": datetime.now().isoformat(),
|
|
41
|
+
"file": filename,
|
|
42
|
+
}
|
|
43
|
+
save_manifest(manifest)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def remove_tool(slug: str) -> bool:
|
|
47
|
+
"""حذف أداة من manifest — يُرجع True إذا كانت موجودة"""
|
|
48
|
+
manifest = load_manifest()
|
|
49
|
+
if slug in manifest["installed"]:
|
|
50
|
+
del manifest["installed"][slug]
|
|
51
|
+
save_manifest(manifest)
|
|
52
|
+
return True
|
|
53
|
+
return False
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def get_tool(slug: str) -> dict | None:
|
|
57
|
+
"""إرجاع بيانات أداة معينة أو None"""
|
|
58
|
+
manifest = load_manifest()
|
|
59
|
+
return manifest["installed"].get(slug)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def get_all_tools() -> dict:
|
|
63
|
+
"""إرجاع جميع الأدوات المثبتة"""
|
|
64
|
+
manifest = load_manifest()
|
|
65
|
+
return manifest["installed"]
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""
|
|
2
|
+
مساعد الطباعة الملونة في الطرفية
|
|
3
|
+
Colored terminal output helper using rich.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from rich.panel import Panel
|
|
8
|
+
from rich.text import Text
|
|
9
|
+
|
|
10
|
+
console = Console()
|
|
11
|
+
|
|
12
|
+
# --- ثوابت الألوان ---
|
|
13
|
+
SUCCESS = "bold green"
|
|
14
|
+
ERROR = "bold red"
|
|
15
|
+
WARNING = "bold yellow"
|
|
16
|
+
INFO = "bold cyan"
|
|
17
|
+
MUTED = "dim"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def print_success(message: str) -> None:
|
|
21
|
+
"""طباعة رسالة نجاح"""
|
|
22
|
+
console.print(f" ✅ {message}", style=SUCCESS)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def print_error(message: str) -> None:
|
|
26
|
+
"""طباعة رسالة خطأ"""
|
|
27
|
+
console.print(f" ❌ {message}", style=ERROR)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def print_warning(message: str) -> None:
|
|
31
|
+
"""طباعة رسالة تحذير"""
|
|
32
|
+
console.print(f" ⚠️ {message}", style=WARNING)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def print_info(message: str) -> None:
|
|
36
|
+
"""طباعة رسالة معلومات"""
|
|
37
|
+
console.print(f" ℹ️ {message}", style=INFO)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def print_banner() -> None:
|
|
41
|
+
"""عرض شعار 4u عند تشغيله بدون أوامر"""
|
|
42
|
+
logo = Text()
|
|
43
|
+
logo.append(" ██╗ ██╗", style="bold cyan")
|
|
44
|
+
logo.append("██╗ ██╗\n", style="bold magenta")
|
|
45
|
+
logo.append(" ██║ ██║", style="bold cyan")
|
|
46
|
+
logo.append("██║ ██║\n", style="bold magenta")
|
|
47
|
+
logo.append(" ███████║", style="bold cyan")
|
|
48
|
+
logo.append("██║ ██║\n", style="bold magenta")
|
|
49
|
+
logo.append(" ██║", style="bold cyan")
|
|
50
|
+
logo.append("██║ ██║\n", style="bold magenta")
|
|
51
|
+
logo.append(" ██║", style="bold cyan")
|
|
52
|
+
logo.append("╚██████╔╝\n", style="bold magenta")
|
|
53
|
+
logo.append(" ╚═╝", style="bold cyan")
|
|
54
|
+
logo.append(" ╚═════╝ ", style="bold magenta")
|
|
55
|
+
|
|
56
|
+
panel = Panel(
|
|
57
|
+
logo,
|
|
58
|
+
title="[bold white]⚡ 4u Package Manager ⚡[/bold white]",
|
|
59
|
+
subtitle="[dim]v1.0.0 • Type [bold]4u --help[/bold] for commands[/dim]",
|
|
60
|
+
border_style="bright_blue",
|
|
61
|
+
padding=(1, 4),
|
|
62
|
+
)
|
|
63
|
+
console.print()
|
|
64
|
+
console.print(panel)
|
|
65
|
+
console.print()
|