baska 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.
- baska-1.0.0.data/scripts/baska +1312 -0
- baska-1.0.0.dist-info/METADATA +85 -0
- baska-1.0.0.dist-info/RECORD +5 -0
- baska-1.0.0.dist-info/WHEEL +5 -0
- baska-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1312 @@
|
|
|
1
|
+
#!python
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import base64
|
|
6
|
+
import csv
|
|
7
|
+
import getpass
|
|
8
|
+
import hashlib
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import platform as py_platform
|
|
12
|
+
import re
|
|
13
|
+
import shutil
|
|
14
|
+
import subprocess
|
|
15
|
+
import sys
|
|
16
|
+
import tempfile
|
|
17
|
+
import textwrap
|
|
18
|
+
import time
|
|
19
|
+
import urllib.error
|
|
20
|
+
import urllib.parse
|
|
21
|
+
import urllib.request
|
|
22
|
+
from datetime import datetime, timezone
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
VERSION = "1.0.0"
|
|
26
|
+
OWNER = os.getenv("BASKA_OWNER", "baska-pro")
|
|
27
|
+
HUB_REPO = os.getenv("BASKA_HUB_REPO", f"{OWNER}/baska-hub")
|
|
28
|
+
HUB_BRANCH = os.getenv("BASKA_HUB_BRANCH", "main")
|
|
29
|
+
RAW_BASE = f"https://raw.githubusercontent.com/{HUB_REPO}/{HUB_BRANCH}"
|
|
30
|
+
|
|
31
|
+
HOME = Path(os.getenv("BASKA_HOME", str(Path.home() / ".baska"))).expanduser()
|
|
32
|
+
CACHE = HOME / "cache"
|
|
33
|
+
PACKAGES = HOME / "packages"
|
|
34
|
+
STATE_FILE = HOME / "state.json"
|
|
35
|
+
CONFIG_FILE = HOME / "config.json"
|
|
36
|
+
PRIVATE_FILE = HOME / "private_catalog.json"
|
|
37
|
+
TOKEN_FILE = HOME / "auth" / "token"
|
|
38
|
+
CATALOG_FILE = CACHE / "catalog.json"
|
|
39
|
+
COLLECTIONS_FILE = CACHE / "collections.json"
|
|
40
|
+
|
|
41
|
+
for p in (HOME, CACHE, PACKAGES, TOKEN_FILE.parent):
|
|
42
|
+
p.mkdir(parents=True, exist_ok=True)
|
|
43
|
+
|
|
44
|
+
IS_TTY = sys.stdout.isatty()
|
|
45
|
+
C = {
|
|
46
|
+
"reset": "\033[0m" if IS_TTY else "",
|
|
47
|
+
"bold": "\033[1m" if IS_TTY else "",
|
|
48
|
+
"dim": "\033[2m" if IS_TTY else "",
|
|
49
|
+
"cyan": "\033[36m" if IS_TTY else "",
|
|
50
|
+
"green": "\033[32m" if IS_TTY else "",
|
|
51
|
+
"yellow": "\033[33m" if IS_TTY else "",
|
|
52
|
+
"red": "\033[31m" if IS_TTY else "",
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
def color(name: str, text: str) -> str:
|
|
56
|
+
return f"{C[name]}{text}{C['reset']}"
|
|
57
|
+
|
|
58
|
+
def now_iso() -> str:
|
|
59
|
+
return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
|
|
60
|
+
|
|
61
|
+
def load_json(path: Path, default):
|
|
62
|
+
try:
|
|
63
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
64
|
+
except (FileNotFoundError, json.JSONDecodeError):
|
|
65
|
+
return default
|
|
66
|
+
|
|
67
|
+
def save_json(path: Path, data) -> None:
|
|
68
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
69
|
+
tmp = path.with_suffix(path.suffix + ".tmp")
|
|
70
|
+
tmp.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
|
71
|
+
tmp.replace(path)
|
|
72
|
+
|
|
73
|
+
def state() -> dict:
|
|
74
|
+
return load_json(STATE_FILE, {"schema_version": 1, "installed": {}, "notifications": [], "catalog_ids": []})
|
|
75
|
+
|
|
76
|
+
def save_state(data: dict) -> None:
|
|
77
|
+
save_json(STATE_FILE, data)
|
|
78
|
+
|
|
79
|
+
def config() -> dict:
|
|
80
|
+
return load_json(CONFIG_FILE, {
|
|
81
|
+
"schema_version": 1,
|
|
82
|
+
"github_initialized": False,
|
|
83
|
+
"github_user": None,
|
|
84
|
+
"auto_refresh": True,
|
|
85
|
+
"smart_install": True,
|
|
86
|
+
"favorites": [],
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
def save_config(data: dict) -> None:
|
|
90
|
+
save_json(CONFIG_FILE, data)
|
|
91
|
+
|
|
92
|
+
def run(cmd, cwd: Path | None = None, check: bool = True, capture: bool = False,
|
|
93
|
+
env: dict | None = None, shell: bool = False):
|
|
94
|
+
kwargs = {
|
|
95
|
+
"cwd": str(cwd) if cwd else None,
|
|
96
|
+
"check": check,
|
|
97
|
+
"text": True,
|
|
98
|
+
"env": env,
|
|
99
|
+
"shell": shell,
|
|
100
|
+
}
|
|
101
|
+
if capture:
|
|
102
|
+
kwargs["stdout"] = subprocess.PIPE
|
|
103
|
+
kwargs["stderr"] = subprocess.PIPE
|
|
104
|
+
return subprocess.run(cmd, **kwargs)
|
|
105
|
+
|
|
106
|
+
def command_exists(name: str) -> bool:
|
|
107
|
+
return shutil.which(name) is not None
|
|
108
|
+
|
|
109
|
+
def detect_platform() -> str:
|
|
110
|
+
if os.getenv("TERMUX_VERSION") or "com.termux" in os.getenv("PREFIX", ""):
|
|
111
|
+
return "termux"
|
|
112
|
+
sysname = py_platform.system().lower()
|
|
113
|
+
if sysname == "windows":
|
|
114
|
+
return "windows"
|
|
115
|
+
if sysname == "darwin":
|
|
116
|
+
return "macos"
|
|
117
|
+
if sysname == "linux":
|
|
118
|
+
return "linux"
|
|
119
|
+
msystem = os.getenv("MSYSTEM", "").lower()
|
|
120
|
+
if "mingw" in msystem or "msys" in msystem or "cygwin" in msystem:
|
|
121
|
+
return "windows"
|
|
122
|
+
return sysname or "unknown"
|
|
123
|
+
|
|
124
|
+
def detect_arch() -> str:
|
|
125
|
+
machine = py_platform.machine().lower()
|
|
126
|
+
aliases = {
|
|
127
|
+
"amd64": "x86_64",
|
|
128
|
+
"x64": "x86_64",
|
|
129
|
+
"aarch64": "arm64",
|
|
130
|
+
"arm64": "arm64",
|
|
131
|
+
}
|
|
132
|
+
return aliases.get(machine, machine or "unknown")
|
|
133
|
+
|
|
134
|
+
def http_get(url: str, token: str | None = None, timeout: int = 25):
|
|
135
|
+
headers = {
|
|
136
|
+
"Accept": "application/vnd.github+json",
|
|
137
|
+
"User-Agent": f"baska-cli/{VERSION}",
|
|
138
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
139
|
+
}
|
|
140
|
+
if token:
|
|
141
|
+
headers["Authorization"] = f"Bearer {token}"
|
|
142
|
+
req = urllib.request.Request(url, headers=headers)
|
|
143
|
+
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
|
144
|
+
content_type = resp.headers.get("Content-Type", "")
|
|
145
|
+
data = resp.read()
|
|
146
|
+
if "json" in content_type or url.startswith("https://api.github.com/"):
|
|
147
|
+
return json.loads(data.decode("utf-8"))
|
|
148
|
+
return data
|
|
149
|
+
|
|
150
|
+
def download(url: str, target: Path, token: str | None = None) -> Path:
|
|
151
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
152
|
+
headers = {"User-Agent": f"baska-cli/{VERSION}"}
|
|
153
|
+
if token:
|
|
154
|
+
headers["Authorization"] = f"Bearer {token}"
|
|
155
|
+
req = urllib.request.Request(url, headers=headers)
|
|
156
|
+
with urllib.request.urlopen(req, timeout=60) as resp, target.open("wb") as fh:
|
|
157
|
+
shutil.copyfileobj(resp, fh)
|
|
158
|
+
return target
|
|
159
|
+
|
|
160
|
+
def notify(kind: str, message: str) -> None:
|
|
161
|
+
s = state()
|
|
162
|
+
item = {"id": f"{int(time.time()*1000)}", "time": now_iso(), "kind": kind, "message": message, "read": False}
|
|
163
|
+
s.setdefault("notifications", []).insert(0, item)
|
|
164
|
+
s["notifications"] = s["notifications"][:100]
|
|
165
|
+
save_state(s)
|
|
166
|
+
|
|
167
|
+
def refresh(silent: bool = False) -> None:
|
|
168
|
+
old_catalog = load_json(CATALOG_FILE, {"packages": []})
|
|
169
|
+
old_ids = {p.get("id") for p in old_catalog.get("packages", [])}
|
|
170
|
+
try:
|
|
171
|
+
download(f"{RAW_BASE}/registry/catalog.json", CATALOG_FILE)
|
|
172
|
+
download(f"{RAW_BASE}/registry/collections.json", COLLECTIONS_FILE)
|
|
173
|
+
except Exception as exc:
|
|
174
|
+
if not silent:
|
|
175
|
+
print(color("red", f"Gagal refresh: {exc}"))
|
|
176
|
+
if not CATALOG_FILE.exists():
|
|
177
|
+
raise
|
|
178
|
+
return
|
|
179
|
+
|
|
180
|
+
new_catalog = load_json(CATALOG_FILE, {"packages": []})
|
|
181
|
+
new_ids = {p.get("id") for p in new_catalog.get("packages", [])}
|
|
182
|
+
added = [p for p in new_catalog.get("packages", []) if p.get("id") in (new_ids - old_ids)]
|
|
183
|
+
if old_ids and added:
|
|
184
|
+
names = ", ".join(p.get("slug", p.get("id")) for p in added[:5])
|
|
185
|
+
notify("catalog", f"{len(added)} paket/repo baru: {names}")
|
|
186
|
+
s = state()
|
|
187
|
+
s["catalog_ids"] = sorted(x for x in new_ids if x)
|
|
188
|
+
s["last_refresh"] = now_iso()
|
|
189
|
+
save_state(s)
|
|
190
|
+
if not silent:
|
|
191
|
+
print(color("green", "Katalog diperbarui."))
|
|
192
|
+
|
|
193
|
+
def ensure_catalog() -> None:
|
|
194
|
+
cfg = config()
|
|
195
|
+
if not CATALOG_FILE.exists():
|
|
196
|
+
refresh(silent=True)
|
|
197
|
+
elif cfg.get("auto_refresh"):
|
|
198
|
+
try:
|
|
199
|
+
age = time.time() - CATALOG_FILE.stat().st_mtime
|
|
200
|
+
if age > 3600:
|
|
201
|
+
refresh(silent=True)
|
|
202
|
+
except OSError:
|
|
203
|
+
pass
|
|
204
|
+
|
|
205
|
+
def gh_token() -> str | None:
|
|
206
|
+
for key in ("BASKA_GITHUB_TOKEN", "GH_TOKEN"):
|
|
207
|
+
if os.getenv(key):
|
|
208
|
+
return os.getenv(key)
|
|
209
|
+
if command_exists("gh"):
|
|
210
|
+
try:
|
|
211
|
+
p = run(["gh", "auth", "token"], check=True, capture=True)
|
|
212
|
+
token = p.stdout.strip()
|
|
213
|
+
if token:
|
|
214
|
+
return token
|
|
215
|
+
except Exception:
|
|
216
|
+
pass
|
|
217
|
+
if TOKEN_FILE.exists():
|
|
218
|
+
return TOKEN_FILE.read_text(encoding="utf-8").strip() or None
|
|
219
|
+
return None
|
|
220
|
+
|
|
221
|
+
def verify_github_auth(token: str | None = None, silent: bool = False) -> tuple[bool, str | None]:
|
|
222
|
+
token = token or gh_token()
|
|
223
|
+
if not token:
|
|
224
|
+
return False, None
|
|
225
|
+
try:
|
|
226
|
+
user = http_get("https://api.github.com/user", token=token)
|
|
227
|
+
login = user.get("login")
|
|
228
|
+
ok = bool(login and login.lower() == OWNER.lower())
|
|
229
|
+
if not ok and not silent:
|
|
230
|
+
print(color("red", f"Akun GitHub aktif '{login}', tetapi BASKA membutuhkan '{OWNER}'."))
|
|
231
|
+
return ok, login
|
|
232
|
+
except Exception as exc:
|
|
233
|
+
if not silent:
|
|
234
|
+
print(color("red", f"Gagal memverifikasi GitHub: {exc}"))
|
|
235
|
+
return False, None
|
|
236
|
+
|
|
237
|
+
def fetch_private_repositories(token: str) -> list[dict]:
|
|
238
|
+
repos = []
|
|
239
|
+
page = 1
|
|
240
|
+
while True:
|
|
241
|
+
url = (
|
|
242
|
+
"https://api.github.com/user/repos?"
|
|
243
|
+
+ urllib.parse.urlencode({
|
|
244
|
+
"per_page": 100,
|
|
245
|
+
"page": page,
|
|
246
|
+
"affiliation": "owner",
|
|
247
|
+
"visibility": "private",
|
|
248
|
+
"sort": "created",
|
|
249
|
+
"direction": "desc",
|
|
250
|
+
})
|
|
251
|
+
)
|
|
252
|
+
data = http_get(url, token=token)
|
|
253
|
+
if not data:
|
|
254
|
+
break
|
|
255
|
+
repos.extend(
|
|
256
|
+
r for r in data
|
|
257
|
+
if r.get("owner", {}).get("login", "").lower() == OWNER.lower()
|
|
258
|
+
and r.get("private")
|
|
259
|
+
)
|
|
260
|
+
if len(data) < 100:
|
|
261
|
+
break
|
|
262
|
+
page += 1
|
|
263
|
+
repos.sort(key=lambda r: (r.get("created_at") or "", r.get("id", 0)), reverse=True)
|
|
264
|
+
return repos
|
|
265
|
+
|
|
266
|
+
def private_package_from_repo(repo: dict, package_id: str) -> dict:
|
|
267
|
+
name = repo["name"]
|
|
268
|
+
slug = re.sub(r"[^a-z0-9]+", "-", name.lower()).strip("-")
|
|
269
|
+
topics = list(repo.get("topics") or [])
|
|
270
|
+
language = repo.get("language")
|
|
271
|
+
if language:
|
|
272
|
+
topics.append(language.lower())
|
|
273
|
+
return {
|
|
274
|
+
"id": package_id,
|
|
275
|
+
"slug": slug,
|
|
276
|
+
"aliases": [slug.replace("-", "")],
|
|
277
|
+
"name": name,
|
|
278
|
+
"category": "repositories",
|
|
279
|
+
"type": "repo",
|
|
280
|
+
"version": "repo",
|
|
281
|
+
"platforms": ["all"],
|
|
282
|
+
"architectures": ["all"],
|
|
283
|
+
"source": repo["clone_url"],
|
|
284
|
+
"action": "smart",
|
|
285
|
+
"trust": "reviewed",
|
|
286
|
+
"status": "discovered",
|
|
287
|
+
"visibility": "private",
|
|
288
|
+
"repository_id": repo["id"],
|
|
289
|
+
"created_at": repo.get("created_at"),
|
|
290
|
+
"updated_at": repo.get("updated_at"),
|
|
291
|
+
"pushed_at": repo.get("pushed_at"),
|
|
292
|
+
"language": language,
|
|
293
|
+
"tags": sorted(set(topics)),
|
|
294
|
+
"dependencies": ["git"],
|
|
295
|
+
"description": repo.get("description") or f"Private repository {repo['full_name']}.",
|
|
296
|
+
"latest_release": None,
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
def init_github() -> None:
|
|
300
|
+
token = gh_token()
|
|
301
|
+
if not token:
|
|
302
|
+
if not sys.stdin.isatty():
|
|
303
|
+
raise SystemExit("Tidak ada autentikasi GitHub. Gunakan GH_TOKEN/BASKA_GITHUB_TOKEN atau jalankan interaktif.")
|
|
304
|
+
print("Masukkan GitHub Personal Access Token dengan akses read ke repo private.")
|
|
305
|
+
print("Token hanya disimpan lokal di ~/.baska/auth/token dengan permission 600.")
|
|
306
|
+
token = getpass.getpass("GitHub token: ").strip()
|
|
307
|
+
if not token:
|
|
308
|
+
raise SystemExit("Dibatalkan.")
|
|
309
|
+
|
|
310
|
+
ok, login = verify_github_auth(token)
|
|
311
|
+
if not ok:
|
|
312
|
+
raise SystemExit(1)
|
|
313
|
+
|
|
314
|
+
repos = fetch_private_repositories(token)
|
|
315
|
+
old = load_json(PRIVATE_FILE, {"schema_version": 1, "repositories": {}, "packages": []})
|
|
316
|
+
mapping = old.get("repositories", {})
|
|
317
|
+
used = []
|
|
318
|
+
for data in mapping.values():
|
|
319
|
+
m = re.fullmatch(r"P(\d{5})", str(data.get("package_id", "")))
|
|
320
|
+
if m:
|
|
321
|
+
used.append(int(m.group(1)))
|
|
322
|
+
|
|
323
|
+
if not mapping:
|
|
324
|
+
next_down = 50000
|
|
325
|
+
for repo in repos:
|
|
326
|
+
mapping[str(repo["id"])] = {
|
|
327
|
+
"package_id": f"P{next_down:05d}",
|
|
328
|
+
"full_name": repo["full_name"],
|
|
329
|
+
"created_at": repo.get("created_at"),
|
|
330
|
+
}
|
|
331
|
+
next_down -= 1
|
|
332
|
+
else:
|
|
333
|
+
next_up = max(used or [49999]) + 1
|
|
334
|
+
next_down = min(used or [50001]) - 1
|
|
335
|
+
existing_dates = [v.get("created_at") for v in mapping.values() if v.get("created_at")]
|
|
336
|
+
newest_date = max(existing_dates) if existing_dates else ""
|
|
337
|
+
oldest_date = min(existing_dates) if existing_dates else ""
|
|
338
|
+
for repo in repos:
|
|
339
|
+
key = str(repo["id"])
|
|
340
|
+
if key in mapping:
|
|
341
|
+
mapping[key]["full_name"] = repo["full_name"]
|
|
342
|
+
mapping[key]["created_at"] = repo.get("created_at")
|
|
343
|
+
continue
|
|
344
|
+
created = repo.get("created_at") or ""
|
|
345
|
+
if created >= newest_date:
|
|
346
|
+
pid = f"P{next_up:05d}"
|
|
347
|
+
next_up += 1
|
|
348
|
+
elif created <= oldest_date:
|
|
349
|
+
pid = f"P{max(1, next_down):05d}"
|
|
350
|
+
next_down -= 1
|
|
351
|
+
else:
|
|
352
|
+
pid = f"P{next_up:05d}"
|
|
353
|
+
next_up += 1
|
|
354
|
+
mapping[key] = {"package_id": pid, "full_name": repo["full_name"], "created_at": created}
|
|
355
|
+
|
|
356
|
+
packages = [
|
|
357
|
+
private_package_from_repo(repo, mapping[str(repo["id"])]["package_id"])
|
|
358
|
+
for repo in repos
|
|
359
|
+
]
|
|
360
|
+
save_json(PRIVATE_FILE, {
|
|
361
|
+
"schema_version": 1,
|
|
362
|
+
"owner": OWNER,
|
|
363
|
+
"generated_at": now_iso(),
|
|
364
|
+
"repositories": mapping,
|
|
365
|
+
"packages": packages,
|
|
366
|
+
})
|
|
367
|
+
|
|
368
|
+
if not command_exists("gh") and token:
|
|
369
|
+
TOKEN_FILE.write_text(token + "\n", encoding="utf-8")
|
|
370
|
+
try:
|
|
371
|
+
TOKEN_FILE.chmod(0o600)
|
|
372
|
+
except OSError:
|
|
373
|
+
pass
|
|
374
|
+
elif command_exists("gh"):
|
|
375
|
+
try:
|
|
376
|
+
run(["gh", "auth", "setup-git"], check=False)
|
|
377
|
+
except Exception:
|
|
378
|
+
pass
|
|
379
|
+
|
|
380
|
+
cfg = config()
|
|
381
|
+
cfg["github_initialized"] = True
|
|
382
|
+
cfg["github_user"] = login
|
|
383
|
+
save_config(cfg)
|
|
384
|
+
print(color("green", f"GitHub terhubung sebagai {login}."))
|
|
385
|
+
print(f"{len(packages)} repo private tersedia secara lokal dan tidak ditulis ke katalog publik.")
|
|
386
|
+
|
|
387
|
+
def logout_github() -> None:
|
|
388
|
+
cfg = config()
|
|
389
|
+
cfg["github_initialized"] = False
|
|
390
|
+
cfg["github_user"] = None
|
|
391
|
+
save_config(cfg)
|
|
392
|
+
if PRIVATE_FILE.exists():
|
|
393
|
+
PRIVATE_FILE.unlink()
|
|
394
|
+
if TOKEN_FILE.exists():
|
|
395
|
+
TOKEN_FILE.unlink()
|
|
396
|
+
print("Sesi private BASKA dihapus. Login GitHub CLI tidak diubah.")
|
|
397
|
+
|
|
398
|
+
def private_enabled() -> bool:
|
|
399
|
+
cfg = config()
|
|
400
|
+
if not cfg.get("github_initialized") or cfg.get("github_user", "").lower() != OWNER.lower():
|
|
401
|
+
return False
|
|
402
|
+
ok, _ = verify_github_auth(silent=True)
|
|
403
|
+
return ok
|
|
404
|
+
|
|
405
|
+
def catalog_packages(include_private: bool = True) -> list[dict]:
|
|
406
|
+
ensure_catalog()
|
|
407
|
+
public = load_json(CATALOG_FILE, {"packages": []}).get("packages", [])
|
|
408
|
+
packages = list(public)
|
|
409
|
+
if include_private and private_enabled():
|
|
410
|
+
packages.extend(load_json(PRIVATE_FILE, {"packages": []}).get("packages", []))
|
|
411
|
+
return packages
|
|
412
|
+
|
|
413
|
+
def package_sort_key(p: dict):
|
|
414
|
+
created = p.get("created_at") or ""
|
|
415
|
+
pid = str(p.get("id", ""))
|
|
416
|
+
match = re.search(r"(\d+)$", pid)
|
|
417
|
+
num = int(match.group(1)) if match else 0
|
|
418
|
+
return (1 if p.get("type") == "repo" else 0, created, num)
|
|
419
|
+
|
|
420
|
+
def resolve_package(query: str) -> dict | None:
|
|
421
|
+
q = query.lower()
|
|
422
|
+
for p in catalog_packages():
|
|
423
|
+
candidates = [str(p.get("id", "")).lower(), str(p.get("slug", "")).lower()]
|
|
424
|
+
candidates.extend(str(a).lower() for a in p.get("aliases", []))
|
|
425
|
+
if q in candidates:
|
|
426
|
+
return p
|
|
427
|
+
return None
|
|
428
|
+
|
|
429
|
+
def split_target(value: str) -> tuple[str, str | None]:
|
|
430
|
+
if "@" not in value:
|
|
431
|
+
return value, None
|
|
432
|
+
name, ref = value.rsplit("@", 1)
|
|
433
|
+
return name, ref or None
|
|
434
|
+
|
|
435
|
+
def display_packages(packages: list[dict]) -> None:
|
|
436
|
+
if not packages:
|
|
437
|
+
print("Tidak ada paket.")
|
|
438
|
+
return
|
|
439
|
+
width = max(16, min(34, max(len(str(p.get("slug", ""))) for p in packages)))
|
|
440
|
+
print(f"{'ID':<8} {'SLUG':<{width}} {'AKSES':<8} {'STATUS':<15} {'PLATFORM':<16} KETERANGAN")
|
|
441
|
+
print("-" * min(120, 8 + width + 8 + 15 + 16 + 40))
|
|
442
|
+
for p in sorted(packages, key=package_sort_key, reverse=True):
|
|
443
|
+
vis = "PRIVATE" if p.get("visibility") == "private" else "PUBLIC"
|
|
444
|
+
status = p.get("status") or "ready"
|
|
445
|
+
platforms = ",".join(p.get("platforms") or ["all"])
|
|
446
|
+
desc = (p.get("description") or "").replace("\n", " ")
|
|
447
|
+
if len(desc) > 42:
|
|
448
|
+
desc = desc[:39] + "..."
|
|
449
|
+
print(f"{p.get('id',''):<8} {p.get('slug',''):<{width}} {vis:<8} {status:<15} {platforms:<16} {desc}")
|
|
450
|
+
|
|
451
|
+
def list_command(category: str | None = None) -> None:
|
|
452
|
+
pkgs = catalog_packages()
|
|
453
|
+
if category:
|
|
454
|
+
pkgs = [p for p in pkgs if p.get("category") == category or category in p.get("tags", [])]
|
|
455
|
+
display_packages(pkgs)
|
|
456
|
+
|
|
457
|
+
def search_command(term: str) -> None:
|
|
458
|
+
needle = term.lower()
|
|
459
|
+
matches = []
|
|
460
|
+
for p in catalog_packages():
|
|
461
|
+
hay = " ".join([
|
|
462
|
+
str(p.get("id", "")),
|
|
463
|
+
str(p.get("slug", "")),
|
|
464
|
+
str(p.get("name", "")),
|
|
465
|
+
str(p.get("description", "")),
|
|
466
|
+
" ".join(p.get("aliases", [])),
|
|
467
|
+
" ".join(p.get("tags", [])),
|
|
468
|
+
]).lower()
|
|
469
|
+
if needle in hay:
|
|
470
|
+
matches.append(p)
|
|
471
|
+
display_packages(matches)
|
|
472
|
+
|
|
473
|
+
def info_command(query: str) -> None:
|
|
474
|
+
p = resolve_package(query)
|
|
475
|
+
if not p:
|
|
476
|
+
raise SystemExit(f"Paket '{query}' tidak ditemukan.")
|
|
477
|
+
fields = [
|
|
478
|
+
("ID", p.get("id")),
|
|
479
|
+
("Slug", p.get("slug")),
|
|
480
|
+
("Nama", p.get("name")),
|
|
481
|
+
("Akses", p.get("visibility", "public")),
|
|
482
|
+
("Status", p.get("status")),
|
|
483
|
+
("Trust", p.get("trust")),
|
|
484
|
+
("Kategori", p.get("category")),
|
|
485
|
+
("Versi", p.get("version")),
|
|
486
|
+
("Platform", ", ".join(p.get("platforms") or [])),
|
|
487
|
+
("Arsitektur", ", ".join(p.get("architectures") or [])),
|
|
488
|
+
("Bahasa", p.get("language")),
|
|
489
|
+
("Dibuat", p.get("created_at")),
|
|
490
|
+
("Update", p.get("updated_at")),
|
|
491
|
+
("Source", p.get("source")),
|
|
492
|
+
("Action", p.get("action")),
|
|
493
|
+
("Dependencies", ", ".join(p.get("dependencies") or [])),
|
|
494
|
+
("Tags", ", ".join(p.get("tags") or [])),
|
|
495
|
+
("Deskripsi", p.get("description")),
|
|
496
|
+
]
|
|
497
|
+
for key, value in fields:
|
|
498
|
+
if value not in (None, "", []):
|
|
499
|
+
print(f"{key:<13}: {value}")
|
|
500
|
+
release = p.get("latest_release")
|
|
501
|
+
if release:
|
|
502
|
+
print(f"{'Release':<13}: {release.get('tag_name')} ({release.get('published_at') or '-'})")
|
|
503
|
+
|
|
504
|
+
def compatible(package: dict) -> bool:
|
|
505
|
+
supported = set(package.get("platforms") or ["all"])
|
|
506
|
+
current = detect_platform()
|
|
507
|
+
if "all" in supported:
|
|
508
|
+
return True
|
|
509
|
+
if current in supported:
|
|
510
|
+
return True
|
|
511
|
+
if current == "termux" and "linux" in supported:
|
|
512
|
+
return True
|
|
513
|
+
return False
|
|
514
|
+
|
|
515
|
+
def private_git_env(package: dict) -> dict:
|
|
516
|
+
env = os.environ.copy()
|
|
517
|
+
if package.get("visibility") != "private":
|
|
518
|
+
return env
|
|
519
|
+
token = gh_token()
|
|
520
|
+
if not token:
|
|
521
|
+
raise SystemExit("Repo private memerlukan 'baska init' atau autentikasi GitHub CLI.")
|
|
522
|
+
basic = base64.b64encode(f"x-access-token:{token}".encode()).decode()
|
|
523
|
+
env["GIT_CONFIG_COUNT"] = "1"
|
|
524
|
+
env["GIT_CONFIG_KEY_0"] = "http.extraHeader"
|
|
525
|
+
env["GIT_CONFIG_VALUE_0"] = f"Authorization: Basic {basic}"
|
|
526
|
+
env["GIT_TERMINAL_PROMPT"] = "0"
|
|
527
|
+
return env
|
|
528
|
+
|
|
529
|
+
def choose_ref(package: dict, requested_ref: str | None) -> str | None:
|
|
530
|
+
if requested_ref == "latest":
|
|
531
|
+
release = package.get("latest_release") or {}
|
|
532
|
+
return release.get("tag_name")
|
|
533
|
+
if requested_ref:
|
|
534
|
+
return requested_ref
|
|
535
|
+
if package.get("preferred_delivery") == "release":
|
|
536
|
+
release = package.get("latest_release") or {}
|
|
537
|
+
return release.get("tag_name")
|
|
538
|
+
return None
|
|
539
|
+
|
|
540
|
+
def git_clone_or_update(package: dict, requested_ref: str | None = None) -> tuple[Path, str | None]:
|
|
541
|
+
if not command_exists("git"):
|
|
542
|
+
ensure_dependency("git", assume_yes=False)
|
|
543
|
+
slug = package["slug"]
|
|
544
|
+
target = PACKAGES / slug
|
|
545
|
+
env = private_git_env(package)
|
|
546
|
+
source = package["source"]
|
|
547
|
+
chosen_ref = choose_ref(package, requested_ref)
|
|
548
|
+
|
|
549
|
+
if target.exists() and not (target / ".git").exists():
|
|
550
|
+
raise SystemExit(f"{target} sudah ada tetapi bukan Git repository.")
|
|
551
|
+
|
|
552
|
+
if not target.exists():
|
|
553
|
+
cmd = ["git", "clone", source, str(target)]
|
|
554
|
+
run(cmd, env=env)
|
|
555
|
+
else:
|
|
556
|
+
if git_dirty(target):
|
|
557
|
+
raise SystemExit(f"{slug} memiliki perubahan lokal. Commit/stash dahulu sebelum update.")
|
|
558
|
+
run(["git", "fetch", "--all", "--tags", "--prune"], cwd=target, env=env)
|
|
559
|
+
|
|
560
|
+
previous = git_head(target)
|
|
561
|
+
if chosen_ref:
|
|
562
|
+
run(["git", "checkout", "--detach", chosen_ref], cwd=target, env=env)
|
|
563
|
+
else:
|
|
564
|
+
branch = git_default_branch(target, env)
|
|
565
|
+
if branch:
|
|
566
|
+
run(["git", "checkout", branch], cwd=target, env=env, check=False)
|
|
567
|
+
run(["git", "pull", "--ff-only"], cwd=target, env=env, check=False)
|
|
568
|
+
return target, previous
|
|
569
|
+
|
|
570
|
+
def git_head(path: Path) -> str | None:
|
|
571
|
+
try:
|
|
572
|
+
return run(["git", "rev-parse", "HEAD"], cwd=path, capture=True).stdout.strip()
|
|
573
|
+
except Exception:
|
|
574
|
+
return None
|
|
575
|
+
|
|
576
|
+
def git_dirty(path: Path) -> bool:
|
|
577
|
+
try:
|
|
578
|
+
return bool(run(["git", "status", "--porcelain"], cwd=path, capture=True).stdout.strip())
|
|
579
|
+
except Exception:
|
|
580
|
+
return False
|
|
581
|
+
|
|
582
|
+
def git_default_branch(path: Path, env: dict | None = None) -> str | None:
|
|
583
|
+
try:
|
|
584
|
+
out = run(["git", "symbolic-ref", "refs/remotes/origin/HEAD"], cwd=path, capture=True, env=env).stdout.strip()
|
|
585
|
+
return out.rsplit("/", 1)[-1]
|
|
586
|
+
except Exception:
|
|
587
|
+
for candidate in ("main", "master"):
|
|
588
|
+
try:
|
|
589
|
+
run(["git", "show-ref", "--verify", f"refs/remotes/origin/{candidate}"], cwd=path, capture=True)
|
|
590
|
+
return candidate
|
|
591
|
+
except Exception:
|
|
592
|
+
pass
|
|
593
|
+
return None
|
|
594
|
+
|
|
595
|
+
def system_install_command(dep: str) -> list[str] | None:
|
|
596
|
+
plat = detect_platform()
|
|
597
|
+
mapping = {
|
|
598
|
+
"git": {"apt": "git", "dnf": "git", "pacman": "git", "apk": "git", "pkg": "git", "brew": "git", "winget": "Git.Git", "choco": "git"},
|
|
599
|
+
"python": {"apt": "python3 python3-venv python3-pip", "dnf": "python3 python3-pip", "pacman": "python python-pip", "apk": "python3 py3-pip", "pkg": "python", "brew": "python", "winget": "Python.Python.3.12", "choco": "python"},
|
|
600
|
+
"node": {"apt": "nodejs npm", "dnf": "nodejs npm", "pacman": "nodejs npm", "apk": "nodejs npm", "pkg": "nodejs", "brew": "node", "winget": "OpenJS.NodeJS.LTS", "choco": "nodejs-lts"},
|
|
601
|
+
"docker": {"apt": "docker.io docker-compose-v2", "dnf": "docker docker-compose-plugin", "pacman": "docker docker-compose", "apk": "docker docker-cli-compose", "brew": "docker", "winget": "Docker.DockerDesktop", "choco": "docker-desktop"},
|
|
602
|
+
}
|
|
603
|
+
pkg = mapping.get(dep)
|
|
604
|
+
if not pkg:
|
|
605
|
+
return None
|
|
606
|
+
if plat == "termux" and command_exists("pkg"):
|
|
607
|
+
return ["pkg", "install", "-y"] + pkg.get("pkg", dep).split()
|
|
608
|
+
if plat in ("linux", "macos"):
|
|
609
|
+
if command_exists("apt-get"):
|
|
610
|
+
prefix = [] if os.geteuid() == 0 else (["sudo"] if command_exists("sudo") else [])
|
|
611
|
+
return prefix + ["apt-get", "install", "-y"] + pkg["apt"].split()
|
|
612
|
+
if command_exists("dnf"):
|
|
613
|
+
prefix = [] if os.geteuid() == 0 else (["sudo"] if command_exists("sudo") else [])
|
|
614
|
+
return prefix + ["dnf", "install", "-y"] + pkg["dnf"].split()
|
|
615
|
+
if command_exists("pacman"):
|
|
616
|
+
prefix = [] if os.geteuid() == 0 else (["sudo"] if command_exists("sudo") else [])
|
|
617
|
+
return prefix + ["pacman", "-S", "--noconfirm"] + pkg["pacman"].split()
|
|
618
|
+
if command_exists("apk"):
|
|
619
|
+
prefix = [] if os.geteuid() == 0 else (["sudo"] if command_exists("sudo") else [])
|
|
620
|
+
return prefix + ["apk", "add"] + pkg["apk"].split()
|
|
621
|
+
if command_exists("brew"):
|
|
622
|
+
return ["brew", "install"] + pkg["brew"].split()
|
|
623
|
+
if plat == "windows":
|
|
624
|
+
if command_exists("winget"):
|
|
625
|
+
return ["winget", "install", "--id", pkg["winget"], "-e", "--accept-source-agreements", "--accept-package-agreements"]
|
|
626
|
+
if command_exists("choco"):
|
|
627
|
+
return ["choco", "install", "-y", pkg["choco"]]
|
|
628
|
+
return None
|
|
629
|
+
|
|
630
|
+
def dependency_present(dep: str) -> bool:
|
|
631
|
+
checks = {
|
|
632
|
+
"git": ["git"],
|
|
633
|
+
"python": ["python3", "python"],
|
|
634
|
+
"node": ["node"],
|
|
635
|
+
"npm": ["npm"],
|
|
636
|
+
"docker": ["docker"],
|
|
637
|
+
"powershell": ["pwsh", "powershell"],
|
|
638
|
+
}
|
|
639
|
+
return any(command_exists(x) for x in checks.get(dep, [dep]))
|
|
640
|
+
|
|
641
|
+
def confirm(prompt: str, default: bool = False) -> bool:
|
|
642
|
+
if not sys.stdin.isatty():
|
|
643
|
+
return default
|
|
644
|
+
suffix = " [Y/n] " if default else " [y/N] "
|
|
645
|
+
ans = input(prompt + suffix).strip().lower()
|
|
646
|
+
if not ans:
|
|
647
|
+
return default
|
|
648
|
+
return ans in ("y", "yes", "ya")
|
|
649
|
+
|
|
650
|
+
def ensure_dependency(dep: str, assume_yes: bool = False) -> bool:
|
|
651
|
+
if dependency_present(dep):
|
|
652
|
+
return True
|
|
653
|
+
cmd = system_install_command(dep)
|
|
654
|
+
if not cmd:
|
|
655
|
+
print(color("yellow", f"Dependency '{dep}' belum ada dan installer otomatis tidak tersedia."))
|
|
656
|
+
return False
|
|
657
|
+
print(f"Dependency belum ada: {dep}")
|
|
658
|
+
print("Command:", " ".join(cmd))
|
|
659
|
+
if not assume_yes and not confirm(f"Install {dep}?"):
|
|
660
|
+
return False
|
|
661
|
+
try:
|
|
662
|
+
run(cmd)
|
|
663
|
+
return dependency_present(dep)
|
|
664
|
+
except Exception as exc:
|
|
665
|
+
print(color("red", f"Gagal memasang {dep}: {exc}"))
|
|
666
|
+
return False
|
|
667
|
+
|
|
668
|
+
def smart_plan(path: Path) -> list[dict]:
|
|
669
|
+
plan = []
|
|
670
|
+
if (path / "install.sh").is_file():
|
|
671
|
+
plan.append({"kind": "script", "description": "Jalankan install.sh", "deps": [], "cmd": ["sh", "install.sh"]})
|
|
672
|
+
return plan
|
|
673
|
+
if (path / "install.ps1").is_file() and detect_platform() == "windows":
|
|
674
|
+
ps = "pwsh" if command_exists("pwsh") else "powershell"
|
|
675
|
+
plan.append({"kind": "script", "description": "Jalankan install.ps1", "deps": ["powershell"], "cmd": [ps, "-ExecutionPolicy", "Bypass", "-File", "install.ps1"]})
|
|
676
|
+
return plan
|
|
677
|
+
|
|
678
|
+
reqs = sorted(path.glob("requirements*.txt"))
|
|
679
|
+
pyproject = path / "pyproject.toml"
|
|
680
|
+
setup_py = path / "setup.py"
|
|
681
|
+
if reqs or pyproject.exists() or setup_py.exists():
|
|
682
|
+
plan.append({"kind": "python", "description": "Siapkan Python virtualenv dan dependency", "deps": ["python"], "requirements": str(reqs[0].name) if reqs else None, "editable": pyproject.exists() or setup_py.exists()})
|
|
683
|
+
|
|
684
|
+
if (path / "package.json").is_file():
|
|
685
|
+
plan.append({"kind": "node", "description": "Install dependency Node.js", "deps": ["node", "npm"], "cmd": ["npm", "install"]})
|
|
686
|
+
|
|
687
|
+
compose = None
|
|
688
|
+
for name in ("compose.yml", "compose.yaml", "docker-compose.yml", "docker-compose.yaml"):
|
|
689
|
+
if (path / name).is_file():
|
|
690
|
+
compose = name
|
|
691
|
+
break
|
|
692
|
+
if compose:
|
|
693
|
+
plan.append({"kind": "compose", "description": f"Validasi Docker Compose ({compose})", "deps": ["docker"], "cmd": ["docker", "compose", "-f", compose, "config"]})
|
|
694
|
+
elif (path / "Dockerfile").is_file():
|
|
695
|
+
plan.append({"kind": "docker", "description": "Build Docker image", "deps": ["docker"], "cmd": ["docker", "build", "-t", f"baska-{path.name}", "."]})
|
|
696
|
+
return plan
|
|
697
|
+
|
|
698
|
+
def execute_python_plan(path: Path, item: dict) -> None:
|
|
699
|
+
py = shutil.which("python3") or shutil.which("python")
|
|
700
|
+
if not py:
|
|
701
|
+
raise RuntimeError("Python tidak tersedia.")
|
|
702
|
+
venv = path / ".venv"
|
|
703
|
+
if not venv.exists():
|
|
704
|
+
run([py, "-m", "venv", str(venv)], cwd=path)
|
|
705
|
+
if detect_platform() == "windows":
|
|
706
|
+
pip = venv / "Scripts" / "pip.exe"
|
|
707
|
+
else:
|
|
708
|
+
pip = venv / "bin" / "pip"
|
|
709
|
+
run([str(pip), "install", "--upgrade", "pip"], cwd=path)
|
|
710
|
+
if item.get("requirements"):
|
|
711
|
+
run([str(pip), "install", "-r", item["requirements"]], cwd=path)
|
|
712
|
+
if item.get("editable"):
|
|
713
|
+
run([str(pip), "install", "-e", "."], cwd=path)
|
|
714
|
+
|
|
715
|
+
def run_smart_install(package: dict, path: Path, assume_yes: bool = False) -> None:
|
|
716
|
+
plan = smart_plan(path)
|
|
717
|
+
if not plan:
|
|
718
|
+
print(color("yellow", "Tidak ditemukan installer standar; repository berhasil di-clone saja."))
|
|
719
|
+
return
|
|
720
|
+
print(color("cyan", "Smart install plan:"))
|
|
721
|
+
for i, item in enumerate(plan, 1):
|
|
722
|
+
print(f" {i}. {item['description']}")
|
|
723
|
+
trust = package.get("trust", "discovered")
|
|
724
|
+
allowed = assume_yes or trust == "trusted"
|
|
725
|
+
if not allowed:
|
|
726
|
+
allowed = confirm("Jalankan rencana instalasi di atas?")
|
|
727
|
+
if not allowed:
|
|
728
|
+
print("Smart install dilewati. Repository tetap tersedia.")
|
|
729
|
+
return
|
|
730
|
+
for item in plan:
|
|
731
|
+
for dep in item.get("deps", []):
|
|
732
|
+
if not ensure_dependency(dep, assume_yes=assume_yes):
|
|
733
|
+
raise SystemExit(f"Dependency '{dep}' belum tersedia.")
|
|
734
|
+
if item["kind"] == "python":
|
|
735
|
+
execute_python_plan(path, item)
|
|
736
|
+
else:
|
|
737
|
+
run(item["cmd"], cwd=path)
|
|
738
|
+
|
|
739
|
+
def execute_recipe(package: dict, path: Path, assume_yes: bool = False) -> None:
|
|
740
|
+
recipe = package.get("recipe")
|
|
741
|
+
if not recipe:
|
|
742
|
+
action = package.get("action", "")
|
|
743
|
+
if action.startswith("recipe:"):
|
|
744
|
+
recipe = action.split(":", 1)[1]
|
|
745
|
+
if not recipe:
|
|
746
|
+
return
|
|
747
|
+
tmp = CACHE / f"recipe-{package['id']}.sh"
|
|
748
|
+
download(f"{RAW_BASE}/{recipe}", tmp)
|
|
749
|
+
tmp.chmod(0o700)
|
|
750
|
+
env = os.environ.copy()
|
|
751
|
+
env.update({
|
|
752
|
+
"BASKA_PACKAGE_ID": str(package["id"]),
|
|
753
|
+
"BASKA_PACKAGE_SLUG": package["slug"],
|
|
754
|
+
"BASKA_PACKAGE_SOURCE": package["source"],
|
|
755
|
+
"BASKA_PACKAGE_HOME": str(path),
|
|
756
|
+
})
|
|
757
|
+
run(["sh", str(tmp)], env=env)
|
|
758
|
+
|
|
759
|
+
def install_file_package(package: dict) -> None:
|
|
760
|
+
source = package["source"]
|
|
761
|
+
filename = Path(urllib.parse.urlparse(source).path).name or package["slug"]
|
|
762
|
+
if package.get("type") == "apk" and (Path.home() / "storage" / "downloads").is_dir():
|
|
763
|
+
out = Path.home() / "storage" / "downloads" / filename
|
|
764
|
+
else:
|
|
765
|
+
out = PACKAGES / package["slug"] / filename
|
|
766
|
+
download(source, out)
|
|
767
|
+
expected = package.get("sha256")
|
|
768
|
+
if expected:
|
|
769
|
+
actual = hashlib.sha256(out.read_bytes()).hexdigest()
|
|
770
|
+
if actual.lower() != expected.lower():
|
|
771
|
+
out.unlink(missing_ok=True)
|
|
772
|
+
raise SystemExit("SHA-256 tidak cocok. File dihapus.")
|
|
773
|
+
print(color("green", f"Downloaded: {out}"))
|
|
774
|
+
if package.get("action") == "open":
|
|
775
|
+
if command_exists("termux-open"):
|
|
776
|
+
run(["termux-open", str(out)], check=False)
|
|
777
|
+
elif command_exists("xdg-open"):
|
|
778
|
+
run(["xdg-open", str(out)], check=False)
|
|
779
|
+
|
|
780
|
+
def record_install(package: dict, path: Path | None, previous_commit: str | None, ref: str | None) -> None:
|
|
781
|
+
s = state()
|
|
782
|
+
current = git_head(path) if path and (path / ".git").exists() else None
|
|
783
|
+
old = s.setdefault("installed", {}).get(package["slug"], {})
|
|
784
|
+
s["installed"][package["slug"]] = {
|
|
785
|
+
"id": package["id"],
|
|
786
|
+
"slug": package["slug"],
|
|
787
|
+
"path": str(path) if path else None,
|
|
788
|
+
"type": package.get("type"),
|
|
789
|
+
"source": package.get("source"),
|
|
790
|
+
"visibility": package.get("visibility", "public"),
|
|
791
|
+
"installed_at": old.get("installed_at") or now_iso(),
|
|
792
|
+
"updated_at": now_iso(),
|
|
793
|
+
"ref": ref,
|
|
794
|
+
"current_commit": current,
|
|
795
|
+
"previous_commit": previous_commit or old.get("current_commit"),
|
|
796
|
+
"version": package.get("version"),
|
|
797
|
+
}
|
|
798
|
+
save_state(s)
|
|
799
|
+
|
|
800
|
+
def install_command(target: str, assume_yes: bool = False) -> None:
|
|
801
|
+
name, requested_ref = split_target(target)
|
|
802
|
+
package = resolve_package(name)
|
|
803
|
+
if not package:
|
|
804
|
+
raise SystemExit(f"Paket '{name}' tidak ditemukan.")
|
|
805
|
+
if not compatible(package):
|
|
806
|
+
raise SystemExit(f"{package['slug']} tidak kompatibel dengan platform {detect_platform()}.")
|
|
807
|
+
if package.get("visibility") == "private" and not private_enabled():
|
|
808
|
+
raise SystemExit("Repo private hanya tersedia setelah 'baska init' dengan akun GitHub yang benar.")
|
|
809
|
+
|
|
810
|
+
if package.get("type") != "repo":
|
|
811
|
+
install_file_package(package)
|
|
812
|
+
record_install(package, None, None, requested_ref)
|
|
813
|
+
return
|
|
814
|
+
|
|
815
|
+
path, previous = git_clone_or_update(package, requested_ref)
|
|
816
|
+
action = package.get("action", "smart")
|
|
817
|
+
if action.startswith("recipe") or package.get("recipe"):
|
|
818
|
+
execute_recipe(package, path, assume_yes=assume_yes)
|
|
819
|
+
elif action == "smart":
|
|
820
|
+
run_smart_install(package, path, assume_yes=assume_yes)
|
|
821
|
+
else:
|
|
822
|
+
print(color("green", f"Repository tersedia: {path}"))
|
|
823
|
+
record_install(package, path, previous, requested_ref)
|
|
824
|
+
print(color("green", f"Install selesai: {package['slug']} ({package['id']})"))
|
|
825
|
+
|
|
826
|
+
def installed_rows() -> list[dict]:
|
|
827
|
+
s = state()
|
|
828
|
+
return list(s.get("installed", {}).values())
|
|
829
|
+
|
|
830
|
+
def status_command() -> None:
|
|
831
|
+
rows = installed_rows()
|
|
832
|
+
if not rows:
|
|
833
|
+
print("Belum ada paket yang tercatat terpasang.")
|
|
834
|
+
return
|
|
835
|
+
print(f"{'SLUG':<28} {'STATUS':<14} {'REF':<18} PATH")
|
|
836
|
+
print("-" * 100)
|
|
837
|
+
for item in rows:
|
|
838
|
+
path = Path(item["path"]) if item.get("path") else None
|
|
839
|
+
if path and not path.exists():
|
|
840
|
+
st = "MISSING"
|
|
841
|
+
elif path and (path / ".git").exists() and git_dirty(path):
|
|
842
|
+
st = "MODIFIED"
|
|
843
|
+
else:
|
|
844
|
+
st = "OK"
|
|
845
|
+
print(f"{item['slug']:<28} {st:<14} {(item.get('ref') or '-'):<18} {item.get('path') or '-'}")
|
|
846
|
+
|
|
847
|
+
def update_one(query: str, assume_yes: bool = False) -> None:
|
|
848
|
+
package = resolve_package(query)
|
|
849
|
+
if not package:
|
|
850
|
+
raise SystemExit(f"Paket '{query}' tidak ditemukan.")
|
|
851
|
+
s = state()
|
|
852
|
+
installed = s.get("installed", {}).get(package["slug"])
|
|
853
|
+
if not installed:
|
|
854
|
+
return install_command(query, assume_yes=assume_yes)
|
|
855
|
+
if package.get("type") != "repo":
|
|
856
|
+
return install_command(query, assume_yes=assume_yes)
|
|
857
|
+
path = Path(installed["path"])
|
|
858
|
+
if git_dirty(path):
|
|
859
|
+
raise SystemExit(f"{package['slug']} memiliki perubahan lokal; update dibatalkan.")
|
|
860
|
+
old = git_head(path)
|
|
861
|
+
env = private_git_env(package)
|
|
862
|
+
run(["git", "fetch", "--all", "--tags", "--prune"], cwd=path, env=env)
|
|
863
|
+
ref = installed.get("ref")
|
|
864
|
+
if ref:
|
|
865
|
+
chosen = choose_ref(package, ref) or ref
|
|
866
|
+
run(["git", "checkout", "--detach", chosen], cwd=path, env=env)
|
|
867
|
+
else:
|
|
868
|
+
branch = git_default_branch(path, env)
|
|
869
|
+
if branch:
|
|
870
|
+
run(["git", "checkout", branch], cwd=path, env=env, check=False)
|
|
871
|
+
run(["git", "pull", "--ff-only"], cwd=path, env=env)
|
|
872
|
+
new = git_head(path)
|
|
873
|
+
if new != old:
|
|
874
|
+
installed["previous_commit"] = old
|
|
875
|
+
installed["current_commit"] = new
|
|
876
|
+
installed["updated_at"] = now_iso()
|
|
877
|
+
save_state(s)
|
|
878
|
+
notify("update", f"{package['slug']} diperbarui ke {new[:8] if new else 'latest'}")
|
|
879
|
+
if package.get("action") == "smart":
|
|
880
|
+
run_smart_install(package, path, assume_yes=assume_yes)
|
|
881
|
+
print(color("green", f"{package['slug']} diperbarui."))
|
|
882
|
+
else:
|
|
883
|
+
print(f"{package['slug']} sudah terbaru.")
|
|
884
|
+
|
|
885
|
+
def update_all(assume_yes: bool = False) -> None:
|
|
886
|
+
for item in installed_rows():
|
|
887
|
+
try:
|
|
888
|
+
update_one(item["slug"], assume_yes=assume_yes)
|
|
889
|
+
except Exception as exc:
|
|
890
|
+
print(color("red", f"{item['slug']}: {exc}"))
|
|
891
|
+
|
|
892
|
+
def outdated_command() -> None:
|
|
893
|
+
found = 0
|
|
894
|
+
for item in installed_rows():
|
|
895
|
+
path = Path(item["path"]) if item.get("path") else None
|
|
896
|
+
if not path or not (path / ".git").exists():
|
|
897
|
+
continue
|
|
898
|
+
package = resolve_package(item["slug"])
|
|
899
|
+
if not package:
|
|
900
|
+
continue
|
|
901
|
+
if git_dirty(path):
|
|
902
|
+
print(f"{item['slug']}: modified (skip)")
|
|
903
|
+
continue
|
|
904
|
+
env = private_git_env(package)
|
|
905
|
+
try:
|
|
906
|
+
run(["git", "fetch", "--quiet", "origin"], cwd=path, env=env)
|
|
907
|
+
branch = git_default_branch(path, env)
|
|
908
|
+
if not branch:
|
|
909
|
+
continue
|
|
910
|
+
head = git_head(path)
|
|
911
|
+
remote = run(["git", "rev-parse", f"origin/{branch}"], cwd=path, capture=True, env=env).stdout.strip()
|
|
912
|
+
if head != remote:
|
|
913
|
+
print(f"{item['slug']}: UPDATE {head[:8] if head else '-'} -> {remote[:8]}")
|
|
914
|
+
found += 1
|
|
915
|
+
except Exception as exc:
|
|
916
|
+
print(f"{item['slug']}: tidak dapat dicek ({exc})")
|
|
917
|
+
if not found:
|
|
918
|
+
print("Tidak ada update repo yang terdeteksi.")
|
|
919
|
+
else:
|
|
920
|
+
notify("update", f"{found} paket memiliki update.")
|
|
921
|
+
|
|
922
|
+
def remove_command(query: str, assume_yes: bool = False) -> None:
|
|
923
|
+
package = resolve_package(query)
|
|
924
|
+
slug = package["slug"] if package else query
|
|
925
|
+
s = state()
|
|
926
|
+
item = s.get("installed", {}).get(slug)
|
|
927
|
+
if not item:
|
|
928
|
+
raise SystemExit(f"{slug} tidak tercatat terpasang.")
|
|
929
|
+
path = Path(item["path"]) if item.get("path") else None
|
|
930
|
+
if path and path.exists():
|
|
931
|
+
if not assume_yes and not confirm(f"Hapus {path}?"):
|
|
932
|
+
print("Dibatalkan.")
|
|
933
|
+
return
|
|
934
|
+
shutil.rmtree(path)
|
|
935
|
+
del s["installed"][slug]
|
|
936
|
+
save_state(s)
|
|
937
|
+
print(color("green", f"{slug} dihapus dari BASKA."))
|
|
938
|
+
|
|
939
|
+
def rollback_command(query: str, assume_yes: bool = False) -> None:
|
|
940
|
+
package = resolve_package(query)
|
|
941
|
+
slug = package["slug"] if package else query
|
|
942
|
+
s = state()
|
|
943
|
+
item = s.get("installed", {}).get(slug)
|
|
944
|
+
if not item or not item.get("path"):
|
|
945
|
+
raise SystemExit("Paket tidak ditemukan di state instalasi.")
|
|
946
|
+
path = Path(item["path"])
|
|
947
|
+
prev = item.get("previous_commit")
|
|
948
|
+
if not prev:
|
|
949
|
+
raise SystemExit("Tidak ada commit rollback yang tersimpan.")
|
|
950
|
+
if git_dirty(path):
|
|
951
|
+
raise SystemExit("Repository memiliki perubahan lokal; rollback dibatalkan.")
|
|
952
|
+
if not assume_yes and not confirm(f"Rollback {slug} ke {prev[:8]}?"):
|
|
953
|
+
return
|
|
954
|
+
current = git_head(path)
|
|
955
|
+
run(["git", "reset", "--hard", prev], cwd=path)
|
|
956
|
+
item["current_commit"] = prev
|
|
957
|
+
item["previous_commit"] = current
|
|
958
|
+
item["updated_at"] = now_iso()
|
|
959
|
+
save_state(s)
|
|
960
|
+
print(color("green", f"{slug} di-rollback ke {prev[:8]}."))
|
|
961
|
+
|
|
962
|
+
def repair_command(query: str, assume_yes: bool = False) -> None:
|
|
963
|
+
package = resolve_package(query)
|
|
964
|
+
if not package:
|
|
965
|
+
raise SystemExit("Paket tidak ditemukan.")
|
|
966
|
+
s = state()
|
|
967
|
+
item = s.get("installed", {}).get(package["slug"])
|
|
968
|
+
if not item:
|
|
969
|
+
return install_command(query, assume_yes=assume_yes)
|
|
970
|
+
path = Path(item["path"]) if item.get("path") else None
|
|
971
|
+
if not path or not path.exists():
|
|
972
|
+
print("Folder instalasi hilang; memasang ulang.")
|
|
973
|
+
return install_command(query, assume_yes=assume_yes)
|
|
974
|
+
if (path / ".git").exists():
|
|
975
|
+
run(["git", "fsck", "--no-progress"], cwd=path, check=False)
|
|
976
|
+
if package.get("action") == "smart":
|
|
977
|
+
run_smart_install(package, path, assume_yes=assume_yes)
|
|
978
|
+
print(color("green", f"Repair selesai: {package['slug']}"))
|
|
979
|
+
|
|
980
|
+
def versions_command(query: str) -> None:
|
|
981
|
+
package = resolve_package(query)
|
|
982
|
+
if not package or package.get("type") != "repo":
|
|
983
|
+
raise SystemExit("Versions hanya tersedia untuk repository package.")
|
|
984
|
+
env = private_git_env(package)
|
|
985
|
+
p = run(["git", "ls-remote", "--tags", "--refs", package["source"]], capture=True, env=env)
|
|
986
|
+
tags = []
|
|
987
|
+
for line in p.stdout.splitlines():
|
|
988
|
+
if "\trefs/tags/" in line:
|
|
989
|
+
tags.append(line.split("\trefs/tags/", 1)[1])
|
|
990
|
+
release = package.get("latest_release") or {}
|
|
991
|
+
if release.get("tag_name"):
|
|
992
|
+
print(f"Latest release: {release['tag_name']}")
|
|
993
|
+
if not tags:
|
|
994
|
+
print("Tidak ada tag.")
|
|
995
|
+
return
|
|
996
|
+
for tag in tags[-30:][::-1]:
|
|
997
|
+
print(tag)
|
|
998
|
+
|
|
999
|
+
def favorites_command(action: str, value: str | None = None) -> None:
|
|
1000
|
+
cfg = config()
|
|
1001
|
+
fav = cfg.setdefault("favorites", [])
|
|
1002
|
+
if action == "list":
|
|
1003
|
+
display_packages([p for p in catalog_packages() if p.get("slug") in fav])
|
|
1004
|
+
return
|
|
1005
|
+
if not value:
|
|
1006
|
+
raise SystemExit("Slug/ID diperlukan.")
|
|
1007
|
+
p = resolve_package(value)
|
|
1008
|
+
if not p:
|
|
1009
|
+
raise SystemExit("Paket tidak ditemukan.")
|
|
1010
|
+
if action == "add" and p["slug"] not in fav:
|
|
1011
|
+
fav.append(p["slug"])
|
|
1012
|
+
elif action == "remove" and p["slug"] in fav:
|
|
1013
|
+
fav.remove(p["slug"])
|
|
1014
|
+
save_config(cfg)
|
|
1015
|
+
print("Favorites diperbarui.")
|
|
1016
|
+
|
|
1017
|
+
def collections() -> dict:
|
|
1018
|
+
ensure_catalog()
|
|
1019
|
+
return load_json(COLLECTIONS_FILE, {"collections": {}}).get("collections", {})
|
|
1020
|
+
|
|
1021
|
+
def profile_list() -> None:
|
|
1022
|
+
for key, item in collections().items():
|
|
1023
|
+
print(f"{key:<16} {item.get('name', key)} — {item.get('description','')}")
|
|
1024
|
+
|
|
1025
|
+
def profile_show(name: str) -> None:
|
|
1026
|
+
item = collections().get(name)
|
|
1027
|
+
if not item:
|
|
1028
|
+
raise SystemExit("Profile tidak ditemukan.")
|
|
1029
|
+
print(f"{item.get('name', name)}")
|
|
1030
|
+
print(item.get("description", ""))
|
|
1031
|
+
members = []
|
|
1032
|
+
for value in item.get("members", []):
|
|
1033
|
+
p = resolve_package(value)
|
|
1034
|
+
if p:
|
|
1035
|
+
members.append(p)
|
|
1036
|
+
display_packages(members)
|
|
1037
|
+
|
|
1038
|
+
def profile_setup(name: str, assume_yes: bool = False) -> None:
|
|
1039
|
+
item = collections().get(name)
|
|
1040
|
+
if not item:
|
|
1041
|
+
raise SystemExit("Profile tidak ditemukan.")
|
|
1042
|
+
for member in item.get("members", []):
|
|
1043
|
+
try:
|
|
1044
|
+
install_command(member, assume_yes=assume_yes)
|
|
1045
|
+
except Exception as exc:
|
|
1046
|
+
print(color("red", f"{member}: {exc}"))
|
|
1047
|
+
|
|
1048
|
+
def notifications_command(action: str = "list") -> None:
|
|
1049
|
+
s = state()
|
|
1050
|
+
notes = s.setdefault("notifications", [])
|
|
1051
|
+
if action == "clear":
|
|
1052
|
+
s["notifications"] = []
|
|
1053
|
+
save_state(s)
|
|
1054
|
+
print("Notifikasi dibersihkan.")
|
|
1055
|
+
return
|
|
1056
|
+
if not notes:
|
|
1057
|
+
print("Tidak ada notifikasi.")
|
|
1058
|
+
return
|
|
1059
|
+
for n in notes[:30]:
|
|
1060
|
+
mark = "*" if not n.get("read") else " "
|
|
1061
|
+
print(f"{mark} {n.get('time','')} [{n.get('kind','info')}] {n.get('message','')}")
|
|
1062
|
+
n["read"] = True
|
|
1063
|
+
save_state(s)
|
|
1064
|
+
|
|
1065
|
+
def doctor() -> None:
|
|
1066
|
+
ok, login = verify_github_auth(silent=True)
|
|
1067
|
+
checks = {
|
|
1068
|
+
"BASKA": f"v{VERSION}",
|
|
1069
|
+
"Platform": detect_platform(),
|
|
1070
|
+
"Architecture": detect_arch(),
|
|
1071
|
+
"Python": py_platform.python_version(),
|
|
1072
|
+
"Home": str(HOME),
|
|
1073
|
+
"Git": shutil.which("git") or "not found",
|
|
1074
|
+
"Node": shutil.which("node") or "not found",
|
|
1075
|
+
"Docker": shutil.which("docker") or "not found",
|
|
1076
|
+
"GitHub": login if ok else "not initialized/unauthorized",
|
|
1077
|
+
"Catalog": str(CATALOG_FILE) if CATALOG_FILE.exists() else "not cached",
|
|
1078
|
+
}
|
|
1079
|
+
for k, v in checks.items():
|
|
1080
|
+
print(f"{k:<14}: {v}")
|
|
1081
|
+
|
|
1082
|
+
def open_catalog() -> None:
|
|
1083
|
+
url = f"https://{OWNER}.github.io/baska-hub/"
|
|
1084
|
+
if command_exists("termux-open-url"):
|
|
1085
|
+
run(["termux-open-url", url], check=False)
|
|
1086
|
+
elif command_exists("xdg-open"):
|
|
1087
|
+
run(["xdg-open", url], check=False)
|
|
1088
|
+
else:
|
|
1089
|
+
print(url)
|
|
1090
|
+
|
|
1091
|
+
def settings_menu() -> None:
|
|
1092
|
+
cfg = config()
|
|
1093
|
+
while True:
|
|
1094
|
+
print("\nSettings")
|
|
1095
|
+
print(f"1. Auto refresh : {'ON' if cfg.get('auto_refresh') else 'OFF'}")
|
|
1096
|
+
print(f"2. Smart install: {'ON' if cfg.get('smart_install') else 'OFF'}")
|
|
1097
|
+
print("0. Kembali")
|
|
1098
|
+
choice = input("Pilih: ").strip()
|
|
1099
|
+
if choice == "1":
|
|
1100
|
+
cfg["auto_refresh"] = not cfg.get("auto_refresh")
|
|
1101
|
+
elif choice == "2":
|
|
1102
|
+
cfg["smart_install"] = not cfg.get("smart_install")
|
|
1103
|
+
elif choice == "0":
|
|
1104
|
+
break
|
|
1105
|
+
save_config(cfg)
|
|
1106
|
+
|
|
1107
|
+
def clear_screen() -> None:
|
|
1108
|
+
if IS_TTY:
|
|
1109
|
+
print("\033[2J\033[H", end="")
|
|
1110
|
+
|
|
1111
|
+
def dashboard_header() -> None:
|
|
1112
|
+
cfg = config()
|
|
1113
|
+
s = state()
|
|
1114
|
+
unread = sum(1 for n in s.get("notifications", []) if not n.get("read"))
|
|
1115
|
+
auth = cfg.get("github_user") if private_enabled() else "public"
|
|
1116
|
+
print(color("bold", "BASKA HUB"))
|
|
1117
|
+
print(f"v{VERSION} | {detect_platform()}/{detect_arch()} | GitHub: {auth} | Notifikasi: {unread}")
|
|
1118
|
+
print("=" * 72)
|
|
1119
|
+
|
|
1120
|
+
def prompt_package(label: str = "ID/slug") -> str:
|
|
1121
|
+
return input(f"{label}: ").strip()
|
|
1122
|
+
|
|
1123
|
+
def dashboard() -> None:
|
|
1124
|
+
ensure_catalog()
|
|
1125
|
+
while True:
|
|
1126
|
+
clear_screen()
|
|
1127
|
+
dashboard_header()
|
|
1128
|
+
print("""
|
|
1129
|
+
1. Daftar repository & paket
|
|
1130
|
+
2. Cari
|
|
1131
|
+
3. Detail paket
|
|
1132
|
+
4. Install
|
|
1133
|
+
5. Paket terpasang / status
|
|
1134
|
+
6. Cek update
|
|
1135
|
+
7. Update paket
|
|
1136
|
+
8. Update semua
|
|
1137
|
+
9. Remove
|
|
1138
|
+
10. Repair
|
|
1139
|
+
11. Rollback
|
|
1140
|
+
12. Versi / tag
|
|
1141
|
+
13. Profiles & collections
|
|
1142
|
+
14. Favorites
|
|
1143
|
+
15. Notifikasi
|
|
1144
|
+
16. GitHub init / private repo
|
|
1145
|
+
17. Refresh katalog
|
|
1146
|
+
18. Doctor
|
|
1147
|
+
19. Web catalog
|
|
1148
|
+
20. Settings
|
|
1149
|
+
0. Keluar
|
|
1150
|
+
""".strip())
|
|
1151
|
+
choice = input("\nPilih menu: ").strip()
|
|
1152
|
+
try:
|
|
1153
|
+
if choice == "1":
|
|
1154
|
+
list_command()
|
|
1155
|
+
elif choice == "2":
|
|
1156
|
+
search_command(input("Kata pencarian: ").strip())
|
|
1157
|
+
elif choice == "3":
|
|
1158
|
+
info_command(prompt_package())
|
|
1159
|
+
elif choice == "4":
|
|
1160
|
+
install_command(prompt_package("ID/slug[@versi]"))
|
|
1161
|
+
elif choice == "5":
|
|
1162
|
+
status_command()
|
|
1163
|
+
elif choice == "6":
|
|
1164
|
+
outdated_command()
|
|
1165
|
+
elif choice == "7":
|
|
1166
|
+
update_one(prompt_package())
|
|
1167
|
+
elif choice == "8":
|
|
1168
|
+
update_all()
|
|
1169
|
+
elif choice == "9":
|
|
1170
|
+
remove_command(prompt_package())
|
|
1171
|
+
elif choice == "10":
|
|
1172
|
+
repair_command(prompt_package())
|
|
1173
|
+
elif choice == "11":
|
|
1174
|
+
rollback_command(prompt_package())
|
|
1175
|
+
elif choice == "12":
|
|
1176
|
+
versions_command(prompt_package())
|
|
1177
|
+
elif choice == "13":
|
|
1178
|
+
profile_list()
|
|
1179
|
+
sub = input("Profile (kosong untuk kembali): ").strip()
|
|
1180
|
+
if sub:
|
|
1181
|
+
profile_show(sub)
|
|
1182
|
+
if confirm("Install seluruh profile?"):
|
|
1183
|
+
profile_setup(sub)
|
|
1184
|
+
elif choice == "14":
|
|
1185
|
+
print("a=add, r=remove, l=list")
|
|
1186
|
+
a = input("Aksi: ").strip().lower()
|
|
1187
|
+
if a == "l":
|
|
1188
|
+
favorites_command("list")
|
|
1189
|
+
elif a == "a":
|
|
1190
|
+
favorites_command("add", prompt_package())
|
|
1191
|
+
elif a == "r":
|
|
1192
|
+
favorites_command("remove", prompt_package())
|
|
1193
|
+
elif choice == "15":
|
|
1194
|
+
notifications_command()
|
|
1195
|
+
if confirm("Bersihkan semua notifikasi?"):
|
|
1196
|
+
notifications_command("clear")
|
|
1197
|
+
elif choice == "16":
|
|
1198
|
+
if private_enabled():
|
|
1199
|
+
print(f"Terhubung ke {OWNER}.")
|
|
1200
|
+
if confirm("Logout sesi private BASKA?"):
|
|
1201
|
+
logout_github()
|
|
1202
|
+
else:
|
|
1203
|
+
init_github()
|
|
1204
|
+
elif choice == "17":
|
|
1205
|
+
refresh()
|
|
1206
|
+
elif choice == "18":
|
|
1207
|
+
doctor()
|
|
1208
|
+
elif choice == "19":
|
|
1209
|
+
open_catalog()
|
|
1210
|
+
elif choice == "20":
|
|
1211
|
+
settings_menu()
|
|
1212
|
+
elif choice == "0":
|
|
1213
|
+
return
|
|
1214
|
+
else:
|
|
1215
|
+
print("Pilihan tidak dikenal.")
|
|
1216
|
+
except (KeyboardInterrupt, EOFError):
|
|
1217
|
+
print("\nDibatalkan.")
|
|
1218
|
+
except Exception as exc:
|
|
1219
|
+
print(color("red", f"Error: {exc}"))
|
|
1220
|
+
if choice != "0":
|
|
1221
|
+
input("\nEnter untuk kembali ke dashboard...")
|
|
1222
|
+
|
|
1223
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
1224
|
+
parser = argparse.ArgumentParser(prog="baska", description="BASKA Hub package/repository manager")
|
|
1225
|
+
parser.add_argument("--yes", "-y", action="store_true", help="konfirmasi otomatis untuk operasi yang didukung")
|
|
1226
|
+
sub = parser.add_subparsers(dest="command")
|
|
1227
|
+
|
|
1228
|
+
sub.add_parser("dashboard")
|
|
1229
|
+
sub.add_parser("list")
|
|
1230
|
+
p = sub.add_parser("search"); p.add_argument("term")
|
|
1231
|
+
p = sub.add_parser("info"); p.add_argument("package")
|
|
1232
|
+
p = sub.add_parser("install"); p.add_argument("package")
|
|
1233
|
+
p = sub.add_parser("update"); p.add_argument("package", nargs="?")
|
|
1234
|
+
sub.add_parser("update-all")
|
|
1235
|
+
sub.add_parser("outdated")
|
|
1236
|
+
p = sub.add_parser("remove"); p.add_argument("package")
|
|
1237
|
+
p = sub.add_parser("repair"); p.add_argument("package")
|
|
1238
|
+
p = sub.add_parser("rollback"); p.add_argument("package")
|
|
1239
|
+
p = sub.add_parser("versions"); p.add_argument("package")
|
|
1240
|
+
sub.add_parser("status")
|
|
1241
|
+
sub.add_parser("refresh")
|
|
1242
|
+
sub.add_parser("doctor")
|
|
1243
|
+
sub.add_parser("init")
|
|
1244
|
+
sub.add_parser("logout")
|
|
1245
|
+
sub.add_parser("catalog")
|
|
1246
|
+
sub.add_parser("notifications")
|
|
1247
|
+
p = sub.add_parser("favorite"); p.add_argument("action", choices=["add","remove","list"]); p.add_argument("package", nargs="?")
|
|
1248
|
+
p = sub.add_parser("profile"); p.add_argument("action", choices=["list","show","setup"]); p.add_argument("name", nargs="?")
|
|
1249
|
+
sub.add_parser("version")
|
|
1250
|
+
return parser
|
|
1251
|
+
|
|
1252
|
+
def main() -> None:
|
|
1253
|
+
parser = build_parser()
|
|
1254
|
+
args = parser.parse_args()
|
|
1255
|
+
cmd = args.command
|
|
1256
|
+
if cmd is None or cmd == "dashboard":
|
|
1257
|
+
return dashboard()
|
|
1258
|
+
if cmd == "list":
|
|
1259
|
+
return list_command()
|
|
1260
|
+
if cmd == "search":
|
|
1261
|
+
return search_command(args.term)
|
|
1262
|
+
if cmd == "info":
|
|
1263
|
+
return info_command(args.package)
|
|
1264
|
+
if cmd == "install":
|
|
1265
|
+
return install_command(args.package, assume_yes=args.yes)
|
|
1266
|
+
if cmd == "update":
|
|
1267
|
+
if args.package:
|
|
1268
|
+
return update_one(args.package, assume_yes=args.yes)
|
|
1269
|
+
return refresh()
|
|
1270
|
+
if cmd == "update-all":
|
|
1271
|
+
return update_all(assume_yes=args.yes)
|
|
1272
|
+
if cmd == "outdated":
|
|
1273
|
+
return outdated_command()
|
|
1274
|
+
if cmd == "remove":
|
|
1275
|
+
return remove_command(args.package, assume_yes=args.yes)
|
|
1276
|
+
if cmd == "repair":
|
|
1277
|
+
return repair_command(args.package, assume_yes=args.yes)
|
|
1278
|
+
if cmd == "rollback":
|
|
1279
|
+
return rollback_command(args.package, assume_yes=args.yes)
|
|
1280
|
+
if cmd == "versions":
|
|
1281
|
+
return versions_command(args.package)
|
|
1282
|
+
if cmd == "status":
|
|
1283
|
+
return status_command()
|
|
1284
|
+
if cmd == "refresh":
|
|
1285
|
+
return refresh()
|
|
1286
|
+
if cmd == "doctor":
|
|
1287
|
+
return doctor()
|
|
1288
|
+
if cmd == "init":
|
|
1289
|
+
return init_github()
|
|
1290
|
+
if cmd == "logout":
|
|
1291
|
+
return logout_github()
|
|
1292
|
+
if cmd == "catalog":
|
|
1293
|
+
return open_catalog()
|
|
1294
|
+
if cmd == "notifications":
|
|
1295
|
+
return notifications_command()
|
|
1296
|
+
if cmd == "favorite":
|
|
1297
|
+
return favorites_command(args.action, args.package)
|
|
1298
|
+
if cmd == "profile":
|
|
1299
|
+
if args.action == "list":
|
|
1300
|
+
return profile_list()
|
|
1301
|
+
if not args.name:
|
|
1302
|
+
raise SystemExit("Nama profile diperlukan.")
|
|
1303
|
+
if args.action == "show":
|
|
1304
|
+
return profile_show(args.name)
|
|
1305
|
+
return profile_setup(args.name, assume_yes=args.yes)
|
|
1306
|
+
if cmd == "version":
|
|
1307
|
+
print(VERSION)
|
|
1308
|
+
return
|
|
1309
|
+
parser.print_help()
|
|
1310
|
+
|
|
1311
|
+
if __name__ == "__main__":
|
|
1312
|
+
main()
|