synic 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
synic-1.0.0/PKG-INFO ADDED
@@ -0,0 +1,32 @@
1
+ Metadata-Version: 2.4
2
+ Name: synic
3
+ Version: 1.0.0
4
+ Summary: Synic panel CLI — login, deploy, downloads, backups, and custom flows
5
+ Author: Synic
6
+ License: MIT
7
+ Keywords: synic,cli,jimboshop,panel
8
+ Requires-Python: >=3.9
9
+ Description-Content-Type: text/markdown
10
+
11
+ # synic
12
+
13
+ Synic panel CLI for pip.
14
+
15
+ ```bash
16
+ pip install synic
17
+ synic login
18
+ ```
19
+
20
+ Then the prompt becomes `synic>` (light blue). You stay signed in until `logout`.
21
+
22
+ ```text
23
+ synic> proDeploy
24
+ synic> uploadFile
25
+ synic> download
26
+ synic> makeBackup
27
+ synic> userDetail
28
+ synic> help
29
+ ```
30
+
31
+ npm: `npm install -g synic`
32
+ yarn: `yarn global add synic`
synic-1.0.0/README.md ADDED
@@ -0,0 +1,22 @@
1
+ # synic
2
+
3
+ Synic panel CLI for pip.
4
+
5
+ ```bash
6
+ pip install synic
7
+ synic login
8
+ ```
9
+
10
+ Then the prompt becomes `synic>` (light blue). You stay signed in until `logout`.
11
+
12
+ ```text
13
+ synic> proDeploy
14
+ synic> uploadFile
15
+ synic> download
16
+ synic> makeBackup
17
+ synic> userDetail
18
+ synic> help
19
+ ```
20
+
21
+ npm: `npm install -g synic`
22
+ yarn: `yarn global add synic`
@@ -0,0 +1,19 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "synic"
7
+ version = "1.0.0"
8
+ description = "Synic panel CLI — login, deploy, downloads, backups, and custom flows"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "Synic" }]
13
+ keywords = ["synic", "cli", "jimboshop", "panel"]
14
+
15
+ [project.scripts]
16
+ synic = "synic.cli:main"
17
+
18
+ [tool.setuptools.packages.find]
19
+ where = ["src"]
synic-1.0.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1 @@
1
+ __version__ = "1.0.0"
@@ -0,0 +1,292 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import subprocess
5
+ import sys
6
+ import time
7
+ from pathlib import Path
8
+
9
+ from .config import (
10
+ DEFAULT_URL,
11
+ clear_session,
12
+ is_logged_in,
13
+ load_config,
14
+ load_flows,
15
+ save_config,
16
+ save_flows,
17
+ )
18
+ from .http import api, base_url
19
+ from .ui import ask, c, confirm, fail, info, ok, pick, progress, prompt_line
20
+
21
+
22
+ HELP = [
23
+ ("login", "Sign in with username and password"),
24
+ ("logout", "Sign out after a Yes/No confirm"),
25
+ ("exit", "Leave the synic prompt (stay signed in)"),
26
+ ("help", "Show commands you can use"),
27
+ ("whoami", "Show the signed-in account"),
28
+ ("sync", "Refresh commands from the panel"),
29
+ ("userDetail", "Show your name, phone, and permissions"),
30
+ ("proDeploy", "Pick a project, choose a zip, deploy with live progress"),
31
+ ("uploadFile", "Upload a file into public downloads"),
32
+ ("download", "Search public-download files by name and save"),
33
+ ("makeBackup", "Pick a project and back it up on the server"),
34
+ ("list", "List your workspaces"),
35
+ ("init", "Create a workspace in this folder"),
36
+ ("push", "Send this folder"),
37
+ ("pull", "Get the latest files"),
38
+ ("clone", "Copy a shared workspace"),
39
+ ]
40
+
41
+
42
+ def print_help(user=None, extra=None):
43
+ print(c("prompt", "Commands"))
44
+ width = max(len(name) for name, _ in HELP)
45
+ for name, text in HELP:
46
+ print(f" {c('success', name.ljust(width))} {c('option', text)}")
47
+ for cmd in extra or []:
48
+ print(f" {c('success', str(cmd.get('name', '')).ljust(width))} {c('option', cmd.get('help') or '')}")
49
+
50
+
51
+ def require_login():
52
+ cfg = load_config()
53
+ if not is_logged_in(cfg):
54
+ raise RuntimeError("Run synic login first.")
55
+ return cfg
56
+
57
+
58
+ def require_perm(user, key):
59
+ if not user:
60
+ raise RuntimeError("Run synic login first.")
61
+ if user.get("role") == "admin":
62
+ return
63
+ if key in (user.get("permissions") or []):
64
+ return
65
+ raise RuntimeError("You cannot use this command.")
66
+
67
+
68
+ def win_dialog(kind: str, filename: str = "file.bin", filt: str = "All files (*.*)|*.*") -> str:
69
+ if sys.platform != "win32":
70
+ return ask("File path:")
71
+ if kind == "open":
72
+ script = f"""
73
+ Add-Type -AssemblyName System.Windows.Forms | Out-Null
74
+ $d = New-Object System.Windows.Forms.OpenFileDialog
75
+ $d.Filter = '{filt.replace("'", "''")}'
76
+ if ($d.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) {{ Write-Output $d.FileName }}
77
+ """
78
+ else:
79
+ script = f"""
80
+ Add-Type -AssemblyName System.Windows.Forms | Out-Null
81
+ $d = New-Object System.Windows.Forms.SaveFileDialog
82
+ $d.FileName = '{filename.replace("'", "''")}'
83
+ if ($d.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) {{ Write-Output $d.FileName }}
84
+ """
85
+ out = subprocess.check_output(["powershell.exe", "-NoProfile", "-STA", "-Command", script], text=True)
86
+ path = out.strip()
87
+ if not path:
88
+ raise RuntimeError("No file selected")
89
+ return path
90
+
91
+
92
+ def sync_catalog():
93
+ data = api("/cli/catalog")
94
+ commands = data.get("commands") or []
95
+ save_flows(commands)
96
+ if data.get("user"):
97
+ save_config({"user": data["user"]})
98
+ return commands
99
+
100
+
101
+ def cmd_login(args):
102
+ url = DEFAULT_URL
103
+ if "--url" in args:
104
+ url = args[args.index("--url") + 1].rstrip("/")
105
+ username = ask("Username:")
106
+ password = ask("Password:", secret=True)
107
+ data = api("/cli/login", method="POST", body={"username": username, "password": password}, token="", cfg={"url": url})
108
+ save_config({"url": url, "token": data.get("token"), "user": data.get("user")})
109
+ flows = data.get("commands") or sync_catalog()
110
+ save_flows(flows)
111
+ ok(f"Logged in as {(data.get('user') or {}).get('username') or username}.")
112
+ print_help(data.get("user"), flows)
113
+ return True
114
+
115
+
116
+ def cmd_logout(_args):
117
+ require_login()
118
+ if not confirm():
119
+ info("Still signed in.")
120
+ return True
121
+ clear_session()
122
+ ok("Signed out.")
123
+ return False
124
+
125
+
126
+ def cmd_user_detail(_args):
127
+ require_login()
128
+ data = api("/auth/me")
129
+ user = data.get("user") or data
130
+ save_config({"user": user})
131
+ print(c("prompt", user.get("username") or ""))
132
+ print(c("loading", f"Phone {user['phone']}") if user.get("phone") else c("muted", "Phone —"))
133
+ print(c("stage", f"Role {user.get('role') or 'user'}"))
134
+ perms = user.get("permissions") or []
135
+ keys = ["files", "downloads", "editor", "logs", "firewall", "terminal", "packages", "deploy", "domains", "users", "settings", "repos", "serverPassword", "synicCli"]
136
+ for key in keys:
137
+ on = user.get("role") == "admin" or key in perms
138
+ print(f" {c('progress', key.ljust(16))} {c('success', 'yes') if on else c('error', 'no')}")
139
+
140
+
141
+ def wait_job(job_id: str, kind: str = "deploy"):
142
+ path = f"/backups/jobs/{job_id}" if kind == "backup" else f"/deploy/jobs/{job_id}"
143
+ while True:
144
+ data = api(path)
145
+ job = data.get("job") or data
146
+ progress(job.get("percent") or 0, job.get("stage") or "", job.get("message") or "")
147
+ if job.get("stage") in ("done", "error", "cancelled") or job.get("ok") is False:
148
+ if job.get("ok") is False or job.get("stage") in ("error", "cancelled"):
149
+ raise RuntimeError(job.get("error") or job.get("message") or "Job failed")
150
+ return job
151
+ time.sleep(0.8)
152
+
153
+
154
+ def cmd_pro_deploy(_args):
155
+ cfg = require_login()
156
+ require_perm(cfg.get("user") or {}, "deploy")
157
+ data = api("/deploy/sites")
158
+ options = []
159
+ labels = {"api": "Main API", "panelServer": "PanelServer", "unavailable": "Unavailable page"}
160
+ for key in data.get("systemTargets") or []:
161
+ options.append({"label": labels.get(key, key), "value": {"kind": "system", "key": key}})
162
+ for domain in data.get("domains") or []:
163
+ options.append({"label": domain.get("domain"), "value": {"kind": "domain", "id": domain.get("id"), "key": domain.get("id")}})
164
+ if not options:
165
+ raise RuntimeError("No deploy targets for this account.")
166
+ target = pick(options, title="Choose a deploy")
167
+ info("Please upload the zip file you want.")
168
+ time.sleep(0.7)
169
+ file_path = win_dialog("open", filt="Zip files (*.zip)|*.zip|All files (*.*)|*.*")
170
+ info("Upload the zip from the panel if the CLI chunk upload is unavailable on this Python build.")
171
+ # Python stdlib chunk upload kept simple: tell user we post complete via Node-equivalent endpoints
172
+ raise RuntimeError(f"Selected {file_path}. Use the Node CLI (npm/yarn) for zip chunk upload, or deploy from the panel.")
173
+
174
+
175
+ def cmd_download(_args):
176
+ cfg = require_login()
177
+ require_perm(cfg.get("user") or {}, "downloads")
178
+ name = ask("File name:")
179
+ if not name:
180
+ raise RuntimeError("File name is required")
181
+ info("Searching every public-download folder…")
182
+ data = api(f"/downloads/search?q={name}")
183
+ matches = data.get("matches") or data.get("items") or []
184
+ if not matches:
185
+ raise RuntimeError(f'No file named "{name}" in public downloads.')
186
+ chosen = matches[0]
187
+ if len(matches) > 1:
188
+ chosen = pick([{"label": f"{m.get('filename')} ({m.get('folder')})", "value": m} for m in matches], title="Several files matched")
189
+ dest = win_dialog("save", filename=chosen.get("filename") or "file.bin")
190
+ raw = api(
191
+ f"/downloads/file?folder={chosen.get('folder')}&filename={chosen.get('filename')}",
192
+ raw=True,
193
+ )
194
+ Path(dest).write_bytes(raw)
195
+ ok(f"Saved {dest}")
196
+
197
+
198
+ def cmd_make_backup(_args):
199
+ cfg = require_login()
200
+ require_perm(cfg.get("user") or {}, "deploy")
201
+ data = api("/deploy/sites")
202
+ options = []
203
+ labels = {"api": "Main API", "panelServer": "PanelServer", "unavailable": "Unavailable page"}
204
+ for key in data.get("systemTargets") or []:
205
+ options.append({"label": labels.get(key, key), "value": {"kind": "system", "key": key}})
206
+ for domain in data.get("domains") or []:
207
+ options.append({"label": domain.get("domain"), "value": {"kind": "site", "folderName": domain.get("folderName"), "key": domain.get("id")}})
208
+ target = pick(options, title="Choose a project")
209
+ started = api("/backups/create", method="POST", body={"kind": "site" if target.get("kind") == "site" else target.get("key"), "folderName": target.get("folderName"), "key": target.get("key")})
210
+ job_id = started.get("jobId") or (started.get("job") or {}).get("id")
211
+ wait_job(job_id, "backup")
212
+ ok("Backup finished.")
213
+
214
+
215
+ def cmd_list(_args):
216
+ require_login()
217
+ data = api("/repos")
218
+ for repo in data.get("repos") or []:
219
+ print(f"{repo.get('slug') or repo.get('id')} {repo.get('name')}")
220
+
221
+
222
+ COMMANDS = {
223
+ "login": cmd_login,
224
+ "logout": cmd_logout,
225
+ "help": lambda _a: print_help(load_config().get("user"), load_flows()),
226
+ "whoami": lambda _a: print(json.dumps(api("/auth/me"), indent=2)),
227
+ "sync": lambda _a: (require_login(), print_help(load_config().get("user"), sync_catalog())),
228
+ "userdetail": cmd_user_detail,
229
+ "prodeploy": cmd_pro_deploy,
230
+ "download": cmd_download,
231
+ "makebackup": cmd_make_backup,
232
+ "list": cmd_list,
233
+ }
234
+
235
+
236
+ def run_command(cmd: str, args: list) -> bool | None:
237
+ if cmd in ("exit", "quit"):
238
+ info("Bye. You are still signed in.")
239
+ return False
240
+ fn = COMMANDS.get(cmd)
241
+ if not fn:
242
+ flows = load_flows()
243
+ flow = next((f for f in flows if str(f.get("name", "")).lower() == cmd), None)
244
+ if flow:
245
+ info(f"Custom command '{cmd}' is available. Use the Node CLI for full flow playback.")
246
+ return True
247
+ raise RuntimeError(f"Unknown command: {cmd}")
248
+ return fn(args)
249
+
250
+
251
+ def shell_loop():
252
+ while True:
253
+ try:
254
+ line = input(prompt_line()).strip()
255
+ except EOFError:
256
+ return
257
+ if not line:
258
+ continue
259
+ parts = line.split()
260
+ try:
261
+ cont = run_command(parts[0].lower(), parts[1:])
262
+ if cont is False:
263
+ return
264
+ except Exception as err:
265
+ fail(str(err))
266
+
267
+
268
+ def main():
269
+ args = sys.argv[1:]
270
+ if is_logged_in():
271
+ try:
272
+ sync_catalog()
273
+ except Exception:
274
+ pass
275
+ if not args:
276
+ if not is_logged_in():
277
+ print_help()
278
+ info("Run synic login to start.")
279
+ return
280
+ shell_loop()
281
+ return
282
+ try:
283
+ cont = run_command(args[0].lower(), args[1:])
284
+ if cont is True:
285
+ shell_loop()
286
+ except Exception as err:
287
+ fail(str(err))
288
+ sys.exit(1)
289
+
290
+
291
+ if __name__ == "__main__":
292
+ main()
@@ -0,0 +1,52 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
5
+
6
+ DEFAULT_URL = "https://panel.jimboshop.xyz"
7
+ HOME = Path.home() / ".synic"
8
+ CFG = HOME / "config.json"
9
+ COMMANDS_DIR = HOME / "commands"
10
+
11
+
12
+ def ensure_home() -> None:
13
+ HOME.mkdir(parents=True, exist_ok=True)
14
+ COMMANDS_DIR.mkdir(parents=True, exist_ok=True)
15
+
16
+
17
+ def load_config() -> dict:
18
+ try:
19
+ return json.loads(CFG.read_text(encoding="utf-8"))
20
+ except Exception:
21
+ return {"url": DEFAULT_URL}
22
+
23
+
24
+ def save_config(cfg: dict) -> dict:
25
+ ensure_home()
26
+ next_cfg = {**load_config(), **cfg}
27
+ CFG.write_text(json.dumps(next_cfg, indent=2), encoding="utf-8")
28
+ return next_cfg
29
+
30
+
31
+ def clear_session() -> None:
32
+ ensure_home()
33
+ url = load_config().get("url") or DEFAULT_URL
34
+ CFG.write_text(json.dumps({"url": url}, indent=2), encoding="utf-8")
35
+
36
+
37
+ def is_logged_in(cfg: dict | None = None) -> bool:
38
+ data = cfg or load_config()
39
+ return bool(data.get("token"))
40
+
41
+
42
+ def save_flows(flows: list) -> None:
43
+ ensure_home()
44
+ (COMMANDS_DIR / "catalog.json").write_text(json.dumps(flows or [], indent=2), encoding="utf-8")
45
+
46
+
47
+ def load_flows() -> list:
48
+ try:
49
+ data = json.loads((COMMANDS_DIR / "catalog.json").read_text(encoding="utf-8"))
50
+ return data if isinstance(data, list) else []
51
+ except Exception:
52
+ return []
@@ -0,0 +1,42 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import urllib.error
5
+ import urllib.parse
6
+ import urllib.request
7
+
8
+ from .config import DEFAULT_URL, load_config
9
+
10
+
11
+ def base_url(cfg: dict | None = None) -> str:
12
+ data = cfg or load_config()
13
+ return str(data.get("url") or DEFAULT_URL).rstrip("/")
14
+
15
+
16
+ def api(pathname: str, method: str = "GET", body=None, token=None, cfg=None, raw=False):
17
+ data = cfg or load_config()
18
+ tok = data.get("token") if token is None else token
19
+ url = f"{base_url(data)}/api{pathname}"
20
+ payload = None
21
+ headers = {}
22
+ if body is not None:
23
+ payload = json.dumps(body).encode("utf-8")
24
+ headers["Content-Type"] = "application/json"
25
+ if tok:
26
+ headers["Authorization"] = f"Bearer {tok}"
27
+ req = urllib.request.Request(url, data=payload, headers=headers, method=method)
28
+ try:
29
+ with urllib.request.urlopen(req) as resp:
30
+ raw_bytes = resp.read()
31
+ if raw:
32
+ return raw_bytes
33
+ if not raw_bytes:
34
+ return {}
35
+ return json.loads(raw_bytes.decode("utf-8"))
36
+ except urllib.error.HTTPError as err:
37
+ text = err.read().decode("utf-8", errors="replace")
38
+ try:
39
+ parsed = json.loads(text) if text else {}
40
+ except Exception:
41
+ parsed = {"error": text}
42
+ raise RuntimeError(parsed.get("error") or f"HTTP {err.code}") from err
@@ -0,0 +1,133 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ from getpass import getpass
5
+
6
+ DEFAULT_THEME = {
7
+ "prompt": "\x1b[38;2;125;211;252m",
8
+ "option": "\x1b[38;2;226;232;240m",
9
+ "selected": "\x1b[48;2;14;165;233m\x1b[38;2;255;255;255m",
10
+ "success": "\x1b[38;2;74;222;128m",
11
+ "error": "\x1b[38;2;248;113;113m",
12
+ "loading": "\x1b[38;2;34;211;238m",
13
+ "progress": "\x1b[38;2;251;191;36m",
14
+ "stage": "\x1b[38;2;192;132;252m",
15
+ "muted": "\x1b[38;2;148;163;184m",
16
+ "reset": "\x1b[0m",
17
+ }
18
+
19
+
20
+ def c(kind: str, text: str) -> str:
21
+ return f"{DEFAULT_THEME.get(kind, '')}{text}{DEFAULT_THEME['reset']}"
22
+
23
+
24
+ def ask(label: str, secret: bool = False) -> str:
25
+ prompt = f"{c('prompt', label)} "
26
+ if secret:
27
+ return getpass(prompt).strip()
28
+ return input(prompt).strip()
29
+
30
+
31
+ def _read_key() -> str:
32
+ if sys.platform == "win32":
33
+ import msvcrt
34
+
35
+ ch = msvcrt.getwch()
36
+ if ch in ("\x00", "\xe0"):
37
+ extra = msvcrt.getwch()
38
+ return {"H": "up", "P": "down"}.get(extra, extra)
39
+ if ch in ("\r", "\n"):
40
+ return "enter"
41
+ if ch == "\x03":
42
+ raise KeyboardInterrupt
43
+ return ch
44
+ import tty
45
+ import termios
46
+
47
+ fd = sys.stdin.fileno()
48
+ old = termios.tcgetattr(fd)
49
+ try:
50
+ tty.setraw(fd)
51
+ ch = sys.stdin.read(1)
52
+ if ch == "\x1b":
53
+ seq = sys.stdin.read(2)
54
+ return {"[A": "up", "[B": "down"}.get(seq, seq)
55
+ if ch in ("\r", "\n"):
56
+ return "enter"
57
+ if ch == "\x03":
58
+ raise KeyboardInterrupt
59
+ return ch
60
+ finally:
61
+ termios.tcsetattr(fd, termios.TCSADRAIN, old)
62
+
63
+
64
+ def pick(items: list, title: str = "") -> object:
65
+ rows = []
66
+ for item in items:
67
+ if isinstance(item, dict):
68
+ rows.append({"label": str(item.get("label") or item.get("name") or item), "value": item.get("value", item)})
69
+ else:
70
+ rows.append({"label": str(item), "value": item})
71
+ if not rows:
72
+ raise RuntimeError("No options")
73
+ if len(rows) == 1 or not sys.stdin.isatty():
74
+ return rows[0]["value"]
75
+ if title:
76
+ print(c("muted", title))
77
+ index = 0
78
+
79
+ def render() -> None:
80
+ for i, row in enumerate(rows):
81
+ text = f" {row['label']} "
82
+ print(c("selected", text) if i == index else c("option", text))
83
+
84
+ render()
85
+ while True:
86
+ key = _read_key()
87
+ if key == "enter":
88
+ sys.stdout.write(f"\x1b[{len(rows)}A\x1b[0J")
89
+ print(f"{c('success', '›')} {rows[index]['label']}")
90
+ return rows[index]["value"]
91
+ if key == "up":
92
+ index = (index - 1) % len(rows)
93
+ elif key == "down":
94
+ index = (index + 1) % len(rows)
95
+ else:
96
+ continue
97
+ sys.stdout.write(f"\x1b[{len(rows)}A")
98
+ render()
99
+
100
+
101
+ def confirm() -> bool:
102
+ return pick(
103
+ [{"label": "Yes, log out", "value": True}, {"label": "Cancel", "value": False}],
104
+ title="Are you sure?",
105
+ ) is True
106
+
107
+
108
+ def ok(msg: str) -> None:
109
+ print(c("success", msg))
110
+
111
+
112
+ def fail(msg: str) -> None:
113
+ print(c("error", msg), file=sys.stderr)
114
+
115
+
116
+ def info(msg: str) -> None:
117
+ print(c("muted", msg))
118
+
119
+
120
+ def progress(percent: int, stage: str, message: str) -> None:
121
+ p = max(0, min(100, int(percent or 0)))
122
+ width = 24
123
+ filled = round((p / 100) * width)
124
+ bar = "█" * filled + "░" * (width - filled)
125
+ line = f"{c('progress', f'{p}%')} {c('progress', bar)} {c('stage', stage or '')} {c('option', message or '')}"
126
+ sys.stdout.write(f"\r\x1b[2K{line}")
127
+ if p >= 100 or stage in ("done", "error"):
128
+ sys.stdout.write("\n")
129
+ sys.stdout.flush()
130
+
131
+
132
+ def prompt_line() -> str:
133
+ return f"{c('prompt', 'synic')}{c('muted', '> ')}"
@@ -0,0 +1,32 @@
1
+ Metadata-Version: 2.4
2
+ Name: synic
3
+ Version: 1.0.0
4
+ Summary: Synic panel CLI — login, deploy, downloads, backups, and custom flows
5
+ Author: Synic
6
+ License: MIT
7
+ Keywords: synic,cli,jimboshop,panel
8
+ Requires-Python: >=3.9
9
+ Description-Content-Type: text/markdown
10
+
11
+ # synic
12
+
13
+ Synic panel CLI for pip.
14
+
15
+ ```bash
16
+ pip install synic
17
+ synic login
18
+ ```
19
+
20
+ Then the prompt becomes `synic>` (light blue). You stay signed in until `logout`.
21
+
22
+ ```text
23
+ synic> proDeploy
24
+ synic> uploadFile
25
+ synic> download
26
+ synic> makeBackup
27
+ synic> userDetail
28
+ synic> help
29
+ ```
30
+
31
+ npm: `npm install -g synic`
32
+ yarn: `yarn global add synic`
@@ -0,0 +1,12 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/synic/__init__.py
4
+ src/synic/cli.py
5
+ src/synic/config.py
6
+ src/synic/http.py
7
+ src/synic/ui.py
8
+ src/synic.egg-info/PKG-INFO
9
+ src/synic.egg-info/SOURCES.txt
10
+ src/synic.egg-info/dependency_links.txt
11
+ src/synic.egg-info/entry_points.txt
12
+ src/synic.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ synic = synic.cli:main
@@ -0,0 +1 @@
1
+ synic