wayscloud-cli 0.1.1__tar.gz → 0.2.3__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.
Files changed (30) hide show
  1. wayscloud_cli-0.2.3/LICENSE +21 -0
  2. {wayscloud_cli-0.1.1 → wayscloud_cli-0.2.3}/PKG-INFO +6 -2
  3. {wayscloud_cli-0.1.1 → wayscloud_cli-0.2.3}/pyproject.toml +8 -2
  4. {wayscloud_cli-0.1.1 → wayscloud_cli-0.2.3}/setup.cfg +1 -1
  5. wayscloud_cli-0.2.3/wayscloud_cli/__init__.py +1 -0
  6. wayscloud_cli-0.2.3/wayscloud_cli/commands/vps.py +508 -0
  7. {wayscloud_cli-0.1.1 → wayscloud_cli-0.2.3}/wayscloud_cli.egg-info/PKG-INFO +6 -2
  8. {wayscloud_cli-0.1.1 → wayscloud_cli-0.2.3}/wayscloud_cli.egg-info/SOURCES.txt +1 -1
  9. {wayscloud_cli-0.1.1 → wayscloud_cli-0.2.3}/wayscloud_cli.egg-info/top_level.txt +0 -1
  10. wayscloud_cli-0.1.1/wayscloud_cli/__init__.py +0 -2
  11. wayscloud_cli-0.1.1/wayscloud_cli/commands/__init__.py +0 -0
  12. wayscloud_cli-0.1.1/wayscloud_cli/commands/vps.py +0 -206
  13. {wayscloud_cli-0.1.1 → wayscloud_cli-0.2.3}/README.md +0 -0
  14. {wayscloud_cli-0.1.1 → wayscloud_cli-0.2.3}/tests/test_smoke.py +0 -0
  15. {wayscloud_cli-0.1.1 → wayscloud_cli-0.2.3}/wayscloud_cli/__main__.py +0 -0
  16. {wayscloud_cli-0.1.1/tests → wayscloud_cli-0.2.3/wayscloud_cli/commands}/__init__.py +0 -0
  17. {wayscloud_cli-0.1.1 → wayscloud_cli-0.2.3}/wayscloud_cli/commands/app.py +0 -0
  18. {wayscloud_cli-0.1.1 → wayscloud_cli-0.2.3}/wayscloud_cli/commands/db.py +0 -0
  19. {wayscloud_cli-0.1.1 → wayscloud_cli-0.2.3}/wayscloud_cli/commands/dns.py +0 -0
  20. {wayscloud_cli-0.1.1 → wayscloud_cli-0.2.3}/wayscloud_cli/commands/iot.py +0 -0
  21. {wayscloud_cli-0.1.1 → wayscloud_cli-0.2.3}/wayscloud_cli/commands/login.py +0 -0
  22. {wayscloud_cli-0.1.1 → wayscloud_cli-0.2.3}/wayscloud_cli/commands/redis.py +0 -0
  23. {wayscloud_cli-0.1.1 → wayscloud_cli-0.2.3}/wayscloud_cli/commands/shell.py +0 -0
  24. {wayscloud_cli-0.1.1 → wayscloud_cli-0.2.3}/wayscloud_cli/commands/storage.py +0 -0
  25. {wayscloud_cli-0.1.1 → wayscloud_cli-0.2.3}/wayscloud_cli/config.py +0 -0
  26. {wayscloud_cli-0.1.1 → wayscloud_cli-0.2.3}/wayscloud_cli/output.py +0 -0
  27. {wayscloud_cli-0.1.1 → wayscloud_cli-0.2.3}/wayscloud_cli/sdk.py +0 -0
  28. {wayscloud_cli-0.1.1 → wayscloud_cli-0.2.3}/wayscloud_cli.egg-info/dependency_links.txt +0 -0
  29. {wayscloud_cli-0.1.1 → wayscloud_cli-0.2.3}/wayscloud_cli.egg-info/entry_points.txt +0 -0
  30. {wayscloud_cli-0.1.1 → wayscloud_cli-0.2.3}/wayscloud_cli.egg-info/requires.txt +0 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 WAYSCloud AS
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -1,10 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wayscloud-cli
3
- Version: 0.1.1
3
+ Version: 0.2.3
4
4
  Summary: Command-line interface for WAYSCloud — manage VPS, DNS, databases, storage, apps, IoT, and more
5
5
  License: MIT
6
6
  Project-URL: Homepage, https://wayscloud.services
7
- Project-URL: Documentation, https://docs.wayscloud.services/cli
7
+ Project-URL: Documentation, https://docs.wayscloud.services/integrations/#wayscloud-cli
8
+ Project-URL: Repository, https://github.com/wayscloudas/wayscloud-provision-api
9
+ Project-URL: Issues, https://github.com/wayscloudas/wayscloud-provision-api/issues
8
10
  Classifier: Development Status :: 4 - Beta
9
11
  Classifier: Intended Audience :: Developers
10
12
  Classifier: Intended Audience :: System Administrators
@@ -17,10 +19,12 @@ Classifier: Programming Language :: Python :: 3.12
17
19
  Classifier: Operating System :: OS Independent
18
20
  Requires-Python: >=3.9
19
21
  Description-Content-Type: text/markdown
22
+ License-File: LICENSE
20
23
  Requires-Dist: wayscloud>=0.1.0
21
24
  Requires-Dist: typer>=0.9
22
25
  Requires-Dist: rich>=13.0
23
26
  Requires-Dist: websockets>=12.0
27
+ Dynamic: license-file
24
28
 
25
29
  # WAYSCloud CLI
26
30
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "wayscloud-cli"
7
- version = "0.1.1"
7
+ version = "0.2.3"
8
8
  description = "Command-line interface for WAYSCloud — manage VPS, DNS, databases, storage, apps, IoT, and more"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -30,7 +30,13 @@ dependencies = [
30
30
 
31
31
  [project.urls]
32
32
  Homepage = "https://wayscloud.services"
33
- Documentation = "https://docs.wayscloud.services/cli"
33
+ Documentation = "https://docs.wayscloud.services/integrations/#wayscloud-cli"
34
+ Repository = "https://github.com/wayscloudas/wayscloud-provision-api"
35
+ Issues = "https://github.com/wayscloudas/wayscloud-provision-api/issues"
36
+
37
+ [tool.setuptools.packages.find]
38
+ include = ["wayscloud_cli*"]
39
+ exclude = ["tests*"]
34
40
 
35
41
  [project.scripts]
36
42
  cloud = "wayscloud_cli.__main__:main"
@@ -1,6 +1,6 @@
1
1
  [metadata]
2
2
  name = wayscloud-cli
3
- version = 0.1.0
3
+ version = 0.2.3
4
4
 
5
5
  [options]
6
6
  packages = find:
@@ -0,0 +1 @@
1
+ __version__ = "0.2.3"
@@ -0,0 +1,508 @@
1
+ """
2
+ cloud vps commands — VPS provisioning, control, snapshots, and backups.
3
+ """
4
+
5
+ import typer
6
+ from typing import Optional
7
+
8
+ from ..sdk import get_client, sdk_call
9
+ from ..output import print_table, print_object, print_json, is_json_mode
10
+
11
+ app = typer.Typer(help="Virtual Private Servers")
12
+
13
+ # ── Sub-apps for snapshots and backups ────────────────────────────
14
+ snapshots_app = typer.Typer(help="VPS snapshots (point-in-time disk copies)")
15
+ backups_app = typer.Typer(help="VPS backups (off-site cloud storage)")
16
+ backups_policy_app = typer.Typer(help="Backup policy management")
17
+
18
+ app.add_typer(snapshots_app, name="snapshots")
19
+ app.add_typer(backups_app, name="backups")
20
+ backups_app.add_typer(backups_policy_app, name="policy")
21
+
22
+
23
+ # ══════════════════════════════════════════════════════════════════
24
+ # VPS Instance Commands
25
+ # ══════════════════════════════════════════════════════════════════
26
+
27
+ @app.command("list")
28
+ def list_vps(
29
+ token: Optional[str] = typer.Option(None, "--token", help="Override token"),
30
+ status: Optional[str] = typer.Option(None, help="Filter by status"),
31
+ region: Optional[str] = typer.Option(None, help="Filter by region"),
32
+ ):
33
+ """List your VPS instances."""
34
+ c = get_client(token)
35
+ data = sdk_call(c.vps.list, status=status, region=region)
36
+ instances = data if isinstance(data, list) else data.get("instances", []) if isinstance(data, dict) else []
37
+
38
+ if is_json_mode():
39
+ print_json(instances)
40
+ return
41
+
42
+ if not instances:
43
+ print("No VPS instances found.")
44
+ return
45
+
46
+ rows = []
47
+ for v in instances:
48
+ rows.append({
49
+ "id": str(v.get("id", ""))[:12],
50
+ "hostname": v.get("hostname", v.get("display_name", "")),
51
+ "status": v.get("status", ""),
52
+ "ip": v.get("ipv4_address", v.get("ip_address", "")),
53
+ "plan": v.get("plan_code", ""),
54
+ "region": v.get("region", ""),
55
+ "snap": "yes" if v.get("snapshot_supported") else "no",
56
+ })
57
+
58
+ print_table(rows, [
59
+ ("id", "ID", 14),
60
+ ("hostname", "Hostname", 24),
61
+ ("status", "Status", 12),
62
+ ("ip", "IP", 16),
63
+ ("plan", "Plan", 16),
64
+ ("region", "Region", 8),
65
+ ("snap", "Snap", 5),
66
+ ])
67
+
68
+
69
+ @app.command("create")
70
+ def create_vps(
71
+ hostname: str = typer.Option(..., "--hostname", help="VPS hostname"),
72
+ plan: str = typer.Option(..., "--plan", help="Plan code"),
73
+ region: str = typer.Option(..., "--region", help="Region code"),
74
+ os: str = typer.Option(..., "--os", help="OS template"),
75
+ ssh_key: Optional[list[str]] = typer.Option(None, "--ssh-key", help="SSH key (repeatable)"),
76
+ token: Optional[str] = typer.Option(None, "--token", help="Override token"),
77
+ ):
78
+ """Create a new VPS."""
79
+ c = get_client(token)
80
+ data = sdk_call(c.vps.create, hostname=hostname, plan=plan, region=region,
81
+ os_template=os, ssh_keys=ssh_key or [])
82
+ if is_json_mode():
83
+ print_json(data)
84
+ else:
85
+ print(f"VPS created: {data.get('id', 'unknown')}")
86
+ print_object(data, [("id", "ID"), ("hostname", "Hostname"), ("status", "Status"),
87
+ ("plan_code", "Plan"), ("region", "Region")])
88
+
89
+
90
+ @app.command("info")
91
+ def info_vps(
92
+ vps_id: str = typer.Argument(..., help="VPS ID"),
93
+ token: Optional[str] = typer.Option(None, "--token", help="Override token"),
94
+ ):
95
+ """Show VPS details (status, IPs, plan — all in one)."""
96
+ c = get_client(token)
97
+ data = sdk_call(c.vps.get, vps_id)
98
+
99
+ if is_json_mode():
100
+ print_json(data)
101
+ return
102
+
103
+ print_object(data, [
104
+ ("id", "ID"), ("hostname", "Hostname"), ("status", "Status"),
105
+ ("power_state", "Power"), ("ipv4_address", "IPv4"), ("ipv6_address", "IPv6"),
106
+ ("os_template", "OS"), ("plan_code", "Plan"), ("region", "Region"),
107
+ ("disk_format", "Disk Format"), ("snapshot_supported", "Snapshots"),
108
+ ("created_at", "Created"),
109
+ ])
110
+
111
+
112
+ @app.command("delete")
113
+ def delete_vps(
114
+ vps_id: str = typer.Argument(..., help="VPS ID"),
115
+ token: Optional[str] = typer.Option(None, "--token", help="Override token"),
116
+ confirm: bool = typer.Option(False, "--confirm", help="Skip confirmation"),
117
+ ):
118
+ """Delete a VPS (permanent)."""
119
+ if not confirm:
120
+ print(f"This will permanently delete VPS {vps_id} and all its data.")
121
+ print("Use --confirm to proceed.")
122
+ raise typer.Exit(code=1)
123
+
124
+ c = get_client(token)
125
+ data = sdk_call(c.vps.delete, vps_id)
126
+ if is_json_mode():
127
+ print_json(data)
128
+ else:
129
+ print(f"VPS {vps_id}: deleted")
130
+
131
+
132
+ @app.command("start")
133
+ def start_vps(
134
+ vps_id: str = typer.Argument(..., help="VPS ID"),
135
+ token: Optional[str] = typer.Option(None, "--token", help="Override token"),
136
+ ):
137
+ """Start a VPS."""
138
+ c = get_client(token)
139
+ data = sdk_call(c.vps.start, vps_id)
140
+ if is_json_mode():
141
+ print_json(data)
142
+ else:
143
+ print(f"VPS {vps_id}: {data.get('message', 'starting')}")
144
+
145
+
146
+ @app.command("stop")
147
+ def stop_vps(
148
+ vps_id: str = typer.Argument(..., help="VPS ID"),
149
+ token: Optional[str] = typer.Option(None, "--token", help="Override token"),
150
+ ):
151
+ """Stop a VPS."""
152
+ c = get_client(token)
153
+ data = sdk_call(c.vps.stop, vps_id)
154
+ if is_json_mode():
155
+ print_json(data)
156
+ else:
157
+ print(f"VPS {vps_id}: {data.get('message', 'stopping')}")
158
+
159
+
160
+ @app.command("reboot")
161
+ def reboot_vps(
162
+ vps_id: str = typer.Argument(..., help="VPS ID"),
163
+ token: Optional[str] = typer.Option(None, "--token", help="Override token"),
164
+ ):
165
+ """Reboot a VPS."""
166
+ c = get_client(token)
167
+ data = sdk_call(c.vps.reboot, vps_id)
168
+ if is_json_mode():
169
+ print_json(data)
170
+ else:
171
+ print(f"VPS {vps_id}: {data.get('message', 'rebooting')}")
172
+
173
+
174
+ @app.command("plans")
175
+ def list_plans(
176
+ token: Optional[str] = typer.Option(None, "--token", help="Override token"),
177
+ region: Optional[str] = typer.Option(None, help="Filter by region"),
178
+ ):
179
+ """List available VPS plans."""
180
+ c = get_client(token)
181
+ plans = sdk_call(c.vps.plans, region=region)
182
+ plans = plans if isinstance(plans, list) else []
183
+
184
+ if is_json_mode():
185
+ print_json(plans)
186
+ return
187
+
188
+ rows = []
189
+ for p in plans:
190
+ rows.append({
191
+ "code": p.get("plan_code", ""),
192
+ "vcpu": p.get("vcpu", ""),
193
+ "ram": f"{p.get('ram_mb', 0) // 1024}GB",
194
+ "disk": f"{p.get('disk_gb', 0)}GB",
195
+ "price": f"{p.get('monthly_price', '')} {p.get('currency', '')}",
196
+ })
197
+
198
+ print_table(rows, [
199
+ ("code", "Plan", 24), ("vcpu", "vCPU", 6), ("ram", "RAM", 8),
200
+ ("disk", "Disk", 8), ("price", "Price/mo", 14),
201
+ ])
202
+
203
+
204
+ @app.command("os-templates")
205
+ def vps_os_templates(
206
+ token: Optional[str] = typer.Option(None, "--token", help="Override token"),
207
+ ):
208
+ """List available OS templates."""
209
+ c = get_client(token)
210
+ data = sdk_call(c.vps.os_templates)
211
+ templates = data if isinstance(data, list) else data.get("templates", []) if isinstance(data, dict) else []
212
+
213
+ if is_json_mode():
214
+ print_json(templates)
215
+ return
216
+
217
+ rows = []
218
+ for tpl in templates:
219
+ rows.append({
220
+ "id": tpl.get("id", tpl.get("template_id", "")),
221
+ "name": tpl.get("name", ""),
222
+ "os_family": tpl.get("os_family", ""),
223
+ })
224
+
225
+ print_table(rows, [
226
+ ("id", "Template ID", 24), ("name", "Name", 30), ("os_family", "OS Family", 14),
227
+ ])
228
+
229
+
230
+ # ══════════════════════════════════════════════════════════════════
231
+ # Snapshot Commands (cloud vps snapshots ...)
232
+ # ══════════════════════════════════════════════════════════════════
233
+
234
+ @snapshots_app.command("list")
235
+ def snapshots_list(
236
+ vps_id: str = typer.Argument(..., help="VPS ID"),
237
+ token: Optional[str] = typer.Option(None, "--token", help="Override token"),
238
+ ):
239
+ """List snapshots for a VPS."""
240
+ c = get_client(token)
241
+ snaps = sdk_call(c.vps.snapshots, vps_id)
242
+
243
+ if is_json_mode():
244
+ print_json(snaps)
245
+ return
246
+
247
+ if not snaps:
248
+ print("No snapshots.")
249
+ return
250
+
251
+ rows = []
252
+ for s in snaps:
253
+ rows.append({
254
+ "name": s.get("snapshot_name", ""),
255
+ "status": s.get("status", ""),
256
+ "created": s.get("created_at", "")[:19],
257
+ "desc": (s.get("description") or "")[:30],
258
+ })
259
+
260
+ print_table(rows, [
261
+ ("name", "Name", 24), ("status", "Status", 12),
262
+ ("created", "Created", 20), ("desc", "Description", 32),
263
+ ])
264
+
265
+
266
+ @snapshots_app.command("create")
267
+ def snapshots_create(
268
+ vps_id: str = typer.Argument(..., help="VPS ID"),
269
+ name: str = typer.Option(..., "--name", help="Snapshot name"),
270
+ description: Optional[str] = typer.Option(None, "--description", help="Description"),
271
+ token: Optional[str] = typer.Option(None, "--token", help="Override token"),
272
+ ):
273
+ """Create a point-in-time snapshot. Not a backup — stored on same server."""
274
+ c = get_client(token)
275
+ data = sdk_call(c.vps.create_snapshot, vps_id, name=name, description=description)
276
+ if is_json_mode():
277
+ print_json(data)
278
+ else:
279
+ print(f"Snapshot created: {data.get('snapshot_name', name)} (status: {data.get('status', '?')})")
280
+
281
+
282
+ @snapshots_app.command("delete")
283
+ def snapshots_delete(
284
+ vps_id: str = typer.Argument(..., help="VPS ID"),
285
+ name: str = typer.Option(..., "--name", help="Snapshot name"),
286
+ confirm: bool = typer.Option(False, "--confirm", help="Skip confirmation"),
287
+ token: Optional[str] = typer.Option(None, "--token", help="Override token"),
288
+ ):
289
+ """Delete a snapshot."""
290
+ if not confirm:
291
+ print(f"This will permanently delete snapshot '{name}' from VPS {vps_id}.")
292
+ print("Use --confirm to proceed.")
293
+ raise typer.Exit(code=1)
294
+
295
+ c = get_client(token)
296
+ data = sdk_call(c.vps.delete_snapshot, vps_id, name)
297
+ if is_json_mode():
298
+ print_json(data)
299
+ else:
300
+ print(f"Snapshot '{name}' deleted.")
301
+
302
+
303
+ @snapshots_app.command("rollback")
304
+ def snapshots_rollback(
305
+ vps_id: str = typer.Argument(..., help="VPS ID"),
306
+ name: str = typer.Option(..., "--name", help="Snapshot name to rollback to"),
307
+ confirm: bool = typer.Option(False, "--confirm", help="Skip confirmation"),
308
+ token: Optional[str] = typer.Option(None, "--token", help="Override token"),
309
+ ):
310
+ """Rollback VPS to a snapshot. DESTRUCTIVE — data after snapshot is lost. VPS must be stopped."""
311
+ if not confirm:
312
+ print(f"WARNING: This will rollback VPS {vps_id} to snapshot '{name}'.")
313
+ print("All data written after the snapshot will be PERMANENTLY LOST.")
314
+ print("The VPS must be stopped before rollback.")
315
+ print("Use --confirm to proceed.")
316
+ raise typer.Exit(code=1)
317
+
318
+ c = get_client(token)
319
+ data = sdk_call(c.vps.rollback_snapshot, vps_id, name)
320
+ if is_json_mode():
321
+ print_json(data)
322
+ else:
323
+ print(f"Rollback complete. Start the VPS to resume.")
324
+
325
+
326
+ # ══════════════════════════════════════════════════════════════════
327
+ # Backup Commands (cloud vps backups ...)
328
+ # ══════════════════════════════════════════════════════════════════
329
+
330
+ @backups_app.command("list")
331
+ def backups_list(
332
+ vps_id: str = typer.Argument(..., help="VPS ID"),
333
+ token: Optional[str] = typer.Option(None, "--token", help="Override token"),
334
+ ):
335
+ """List off-site backups for a VPS."""
336
+ c = get_client(token)
337
+ data = sdk_call(c.vps.backups, vps_id)
338
+
339
+ if is_json_mode():
340
+ print_json(data)
341
+ return
342
+
343
+ if not data:
344
+ print("No backups.")
345
+ return
346
+
347
+ rows = []
348
+ for b in data:
349
+ rows.append({
350
+ "id": str(b.get("backup_id", ""))[:12],
351
+ "type": b.get("backup_type", ""),
352
+ "size": f"{b.get('size_gb', 0) or 0:.2f} GB",
353
+ "status": b.get("status", ""),
354
+ "created": b.get("started_at", "")[:19],
355
+ "expires": (b.get("expires_at") or "")[:10],
356
+ })
357
+
358
+ print_table(rows, [
359
+ ("id", "ID", 14), ("type", "Type", 10), ("size", "Size", 10),
360
+ ("status", "Status", 12), ("created", "Created", 20), ("expires", "Expires", 12),
361
+ ])
362
+
363
+
364
+ @backups_app.command("create")
365
+ def backups_create(
366
+ vps_id: str = typer.Argument(..., help="VPS ID"),
367
+ token: Optional[str] = typer.Option(None, "--token", help="Override token"),
368
+ ):
369
+ """Trigger a manual off-site backup. Runs asynchronously."""
370
+ c = get_client(token)
371
+ data = sdk_call(c.vps.create_backup, vps_id)
372
+ if is_json_mode():
373
+ print_json(data)
374
+ else:
375
+ print(f"Backup queued: {data.get('backup_id', '?')} (status: {data.get('status', 'creating')})")
376
+ print("Use 'cloud vps backups list' to check progress.")
377
+
378
+
379
+ @backups_app.command("delete")
380
+ def backups_delete(
381
+ vps_id: str = typer.Argument(..., help="VPS ID"),
382
+ backup_id: str = typer.Option(..., "--backup-id", help="Backup ID"),
383
+ confirm: bool = typer.Option(False, "--confirm", help="Skip confirmation"),
384
+ token: Optional[str] = typer.Option(None, "--token", help="Override token"),
385
+ ):
386
+ """Delete a backup from off-site storage (permanent)."""
387
+ if not confirm:
388
+ print(f"This will permanently delete backup {backup_id}.")
389
+ print("Use --confirm to proceed.")
390
+ raise typer.Exit(code=1)
391
+
392
+ c = get_client(token)
393
+ data = sdk_call(c.vps.delete_backup, vps_id, backup_id)
394
+ if is_json_mode():
395
+ print_json(data)
396
+ else:
397
+ print("Backup deleted.")
398
+
399
+
400
+ @backups_app.command("restore")
401
+ def backups_restore(
402
+ vps_id: str = typer.Argument(..., help="VPS ID"),
403
+ backup_id: str = typer.Option(..., "--backup-id", help="Backup ID"),
404
+ confirm: bool = typer.Option(False, "--confirm", help="Skip confirmation"),
405
+ token: Optional[str] = typer.Option(None, "--token", help="Override token"),
406
+ ):
407
+ """Restore VPS from backup. DESTRUCTIVE — replaces current disk. VPS must be stopped."""
408
+ if not confirm:
409
+ print(f"WARNING: This will replace VPS {vps_id} disk with backup {backup_id}.")
410
+ print("All current data will be PERMANENTLY LOST.")
411
+ print("The VPS must be stopped before restore.")
412
+ print("Use --confirm to proceed.")
413
+ raise typer.Exit(code=1)
414
+
415
+ c = get_client(token)
416
+ # Note: restore currently returns 501 until restore worker is built.
417
+ data = sdk_call(c.vps.restore_backup, vps_id, backup_id)
418
+ if is_json_mode():
419
+ print_json(data)
420
+ else:
421
+ print(data.get("message", "Restore queued."))
422
+
423
+
424
+ @backups_app.command("usage")
425
+ def backups_usage(
426
+ vps_id: str = typer.Argument(..., help="VPS ID"),
427
+ token: Optional[str] = typer.Option(None, "--token", help="Override token"),
428
+ ):
429
+ """Show backup storage usage for a VPS."""
430
+ c = get_client(token)
431
+ data = sdk_call(c.vps.backup_usage, vps_id)
432
+ if is_json_mode():
433
+ print_json(data)
434
+ else:
435
+ print_object(data, [
436
+ ("total_backups", "Backups"), ("total_size_gb", "Total Size (GB)"),
437
+ ])
438
+
439
+
440
+ # ── Backup Policy (cloud vps backups policy ...) ─────────────────
441
+
442
+ @backups_policy_app.command("get")
443
+ def policy_get(
444
+ vps_id: str = typer.Argument(..., help="VPS ID"),
445
+ token: Optional[str] = typer.Option(None, "--token", help="Override token"),
446
+ ):
447
+ """Get backup policy for a VPS."""
448
+ c = get_client(token)
449
+ data = sdk_call(c.vps.backup_policy, vps_id)
450
+ policy = data.get("policy") if isinstance(data, dict) else data
451
+
452
+ if is_json_mode():
453
+ print_json(policy)
454
+ return
455
+
456
+ if not policy:
457
+ print("No backup policy configured.")
458
+ return
459
+
460
+ print_object(policy, [
461
+ ("enabled", "Enabled"), ("frequency", "Frequency"),
462
+ ("time_of_day", "Time (UTC)"), ("day_of_week", "Day of Week"),
463
+ ("retention_days", "Retention (days)"),
464
+ ("next_backup_at", "Next Backup"), ("last_backup_at", "Last Backup"),
465
+ ])
466
+
467
+
468
+ @backups_policy_app.command("set")
469
+ def policy_set(
470
+ vps_id: str = typer.Argument(..., help="VPS ID"),
471
+ frequency: str = typer.Option("daily", "--frequency", help="daily or weekly"),
472
+ time: str = typer.Option("03:00", "--time", help="Time of day (UTC, HH:MM)"),
473
+ retention: int = typer.Option(7, "--retention", help="Days to keep backups (1-90)"),
474
+ day: Optional[int] = typer.Option(None, "--day", help="Day of week (0=Mon, for weekly)"),
475
+ enabled: bool = typer.Option(True, "--enabled/--disabled", help="Enable/disable"),
476
+ token: Optional[str] = typer.Option(None, "--token", help="Override token"),
477
+ ):
478
+ """Set or update backup policy for a VPS."""
479
+ c = get_client(token)
480
+ data = sdk_call(c.vps.set_backup_policy, vps_id,
481
+ enabled=enabled, frequency=frequency,
482
+ time_of_day=time, day_of_week=day,
483
+ retention_days=retention)
484
+ if is_json_mode():
485
+ print_json(data)
486
+ else:
487
+ print(f"Backup policy saved. Next backup: {data.get('next_backup_at', '?')}")
488
+
489
+
490
+ @backups_policy_app.command("remove")
491
+ def policy_remove(
492
+ vps_id: str = typer.Argument(..., help="VPS ID"),
493
+ confirm: bool = typer.Option(False, "--confirm", help="Skip confirmation"),
494
+ token: Optional[str] = typer.Option(None, "--token", help="Override token"),
495
+ ):
496
+ """Remove backup policy (stops automatic backups)."""
497
+ if not confirm:
498
+ print("This will remove the backup policy and stop automatic backups.")
499
+ print("Existing backups will not be deleted.")
500
+ print("Use --confirm to proceed.")
501
+ raise typer.Exit(code=1)
502
+
503
+ c = get_client(token)
504
+ data = sdk_call(c.vps.delete_backup_policy, vps_id)
505
+ if is_json_mode():
506
+ print_json(data)
507
+ else:
508
+ print("Backup policy removed.")
@@ -1,10 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wayscloud-cli
3
- Version: 0.1.1
3
+ Version: 0.2.3
4
4
  Summary: Command-line interface for WAYSCloud — manage VPS, DNS, databases, storage, apps, IoT, and more
5
5
  License: MIT
6
6
  Project-URL: Homepage, https://wayscloud.services
7
- Project-URL: Documentation, https://docs.wayscloud.services/cli
7
+ Project-URL: Documentation, https://docs.wayscloud.services/integrations/#wayscloud-cli
8
+ Project-URL: Repository, https://github.com/wayscloudas/wayscloud-provision-api
9
+ Project-URL: Issues, https://github.com/wayscloudas/wayscloud-provision-api/issues
8
10
  Classifier: Development Status :: 4 - Beta
9
11
  Classifier: Intended Audience :: Developers
10
12
  Classifier: Intended Audience :: System Administrators
@@ -17,10 +19,12 @@ Classifier: Programming Language :: Python :: 3.12
17
19
  Classifier: Operating System :: OS Independent
18
20
  Requires-Python: >=3.9
19
21
  Description-Content-Type: text/markdown
22
+ License-File: LICENSE
20
23
  Requires-Dist: wayscloud>=0.1.0
21
24
  Requires-Dist: typer>=0.9
22
25
  Requires-Dist: rich>=13.0
23
26
  Requires-Dist: websockets>=12.0
27
+ Dynamic: license-file
24
28
 
25
29
  # WAYSCloud CLI
26
30
 
@@ -1,7 +1,7 @@
1
+ LICENSE
1
2
  README.md
2
3
  pyproject.toml
3
4
  setup.cfg
4
- tests/__init__.py
5
5
  tests/test_smoke.py
6
6
  wayscloud_cli/__init__.py
7
7
  wayscloud_cli/__main__.py
@@ -1,2 +0,0 @@
1
- """WAYSCloud CLI — cloud command."""
2
- __version__ = "0.1.1"
File without changes
@@ -1,206 +0,0 @@
1
- """
2
- cloud vps commands — VPS provisioning and control.
3
- """
4
-
5
- import typer
6
- from typing import Optional
7
-
8
- from ..sdk import get_client, sdk_call
9
- from ..output import print_table, print_object, print_json, is_json_mode
10
-
11
- app = typer.Typer(help="Virtual Private Servers")
12
-
13
-
14
- @app.command("list")
15
- def list_vps(
16
- token: Optional[str] = typer.Option(None, "--token", help="Override token"),
17
- status: Optional[str] = typer.Option(None, help="Filter by status"),
18
- region: Optional[str] = typer.Option(None, help="Filter by region"),
19
- ):
20
- """List your VPS instances."""
21
- c = get_client(token)
22
- data = sdk_call(c.vps.list, status=status, region=region)
23
- instances = data if isinstance(data, list) else data.get("instances", []) if isinstance(data, dict) else []
24
-
25
- if is_json_mode():
26
- print_json(instances)
27
- return
28
-
29
- if not instances:
30
- print("No VPS instances found.")
31
- return
32
-
33
- rows = []
34
- for v in instances:
35
- rows.append({
36
- "id": str(v.get("id", ""))[:12],
37
- "hostname": v.get("hostname", v.get("display_name", "")),
38
- "status": v.get("status", ""),
39
- "ip": v.get("ipv4_address", v.get("ip_address", "")),
40
- "plan": v.get("plan_code", ""),
41
- "region": v.get("region", ""),
42
- })
43
-
44
- print_table(rows, [
45
- ("id", "ID", 14),
46
- ("hostname", "Hostname", 24),
47
- ("status", "Status", 12),
48
- ("ip", "IP", 16),
49
- ("plan", "Plan", 16),
50
- ("region", "Region", 8),
51
- ])
52
-
53
-
54
- @app.command("create")
55
- def create_vps(
56
- hostname: str = typer.Option(..., "--hostname", help="VPS hostname"),
57
- plan: str = typer.Option(..., "--plan", help="Plan code"),
58
- region: str = typer.Option(..., "--region", help="Region code"),
59
- os: str = typer.Option(..., "--os", help="OS template"),
60
- ssh_key: Optional[list[str]] = typer.Option(None, "--ssh-key", help="SSH key (repeatable)"),
61
- token: Optional[str] = typer.Option(None, "--token", help="Override token"),
62
- ):
63
- """Create a new VPS."""
64
- c = get_client(token)
65
- data = sdk_call(c.vps.create, hostname=hostname, plan=plan, region=region,
66
- os_template=os, ssh_keys=ssh_key or [])
67
- if is_json_mode():
68
- print_json(data)
69
- else:
70
- print(f"VPS created: {data.get('id', 'unknown')}")
71
- print_object(data, [("id", "ID"), ("hostname", "Hostname"), ("status", "Status"),
72
- ("plan_code", "Plan"), ("region", "Region")])
73
-
74
-
75
- @app.command("info")
76
- def info_vps(
77
- vps_id: str = typer.Argument(..., help="VPS ID"),
78
- token: Optional[str] = typer.Option(None, "--token", help="Override token"),
79
- ):
80
- """Show VPS details (status, IPs, plan — all in one)."""
81
- c = get_client(token)
82
- data = sdk_call(c.vps.get, vps_id)
83
- print_object(data, [
84
- ("id", "ID"), ("hostname", "Hostname"), ("status", "Status"),
85
- ("power_state", "Power"), ("ipv4_address", "IPv4"), ("ipv6_address", "IPv6"),
86
- ("os_template", "OS"), ("plan_code", "Plan"), ("region", "Region"),
87
- ("created_at", "Created"),
88
- ])
89
-
90
-
91
- @app.command("delete")
92
- def delete_vps(
93
- vps_id: str = typer.Argument(..., help="VPS ID"),
94
- token: Optional[str] = typer.Option(None, "--token", help="Override token"),
95
- confirm: bool = typer.Option(False, "--confirm", help="Skip confirmation"),
96
- ):
97
- """Delete a VPS (permanent)."""
98
- if not confirm:
99
- print(f"This will permanently delete VPS {vps_id} and all its data.")
100
- print("Use --confirm to proceed.")
101
- raise typer.Exit(code=1)
102
-
103
- c = get_client(token)
104
- data = sdk_call(c.vps.delete, vps_id)
105
- if is_json_mode():
106
- print_json(data)
107
- else:
108
- print(f"VPS {vps_id}: deleted")
109
-
110
-
111
- @app.command("start")
112
- def start_vps(
113
- vps_id: str = typer.Argument(..., help="VPS ID"),
114
- token: Optional[str] = typer.Option(None, "--token", help="Override token"),
115
- ):
116
- """Start a VPS."""
117
- c = get_client(token)
118
- data = sdk_call(c.vps.start, vps_id)
119
- if is_json_mode():
120
- print_json(data)
121
- else:
122
- print(f"VPS {vps_id}: {data.get('message', 'starting')}")
123
-
124
-
125
- @app.command("stop")
126
- def stop_vps(
127
- vps_id: str = typer.Argument(..., help="VPS ID"),
128
- token: Optional[str] = typer.Option(None, "--token", help="Override token"),
129
- ):
130
- """Stop a VPS."""
131
- c = get_client(token)
132
- data = sdk_call(c.vps.stop, vps_id)
133
- if is_json_mode():
134
- print_json(data)
135
- else:
136
- print(f"VPS {vps_id}: {data.get('message', 'stopping')}")
137
-
138
-
139
- @app.command("reboot")
140
- def reboot_vps(
141
- vps_id: str = typer.Argument(..., help="VPS ID"),
142
- token: Optional[str] = typer.Option(None, "--token", help="Override token"),
143
- ):
144
- """Reboot a VPS."""
145
- c = get_client(token)
146
- data = sdk_call(c.vps.reboot, vps_id)
147
- if is_json_mode():
148
- print_json(data)
149
- else:
150
- print(f"VPS {vps_id}: {data.get('message', 'rebooting')}")
151
-
152
-
153
- @app.command("plans")
154
- def list_plans(
155
- token: Optional[str] = typer.Option(None, "--token", help="Override token"),
156
- region: Optional[str] = typer.Option(None, help="Filter by region"),
157
- ):
158
- """List available VPS plans."""
159
- c = get_client(token)
160
- plans = sdk_call(c.vps.plans, region=region)
161
- plans = plans if isinstance(plans, list) else []
162
-
163
- if is_json_mode():
164
- print_json(plans)
165
- return
166
-
167
- rows = []
168
- for p in plans:
169
- rows.append({
170
- "code": p.get("plan_code", ""),
171
- "vcpu": p.get("vcpu", ""),
172
- "ram": f"{p.get('ram_mb', 0) // 1024}GB",
173
- "disk": f"{p.get('disk_gb', 0)}GB",
174
- "price": f"{p.get('monthly_price', '')} {p.get('currency', '')}",
175
- })
176
-
177
- print_table(rows, [
178
- ("code", "Plan", 24), ("vcpu", "vCPU", 6), ("ram", "RAM", 8),
179
- ("disk", "Disk", 8), ("price", "Price/mo", 14),
180
- ])
181
-
182
-
183
- @app.command("os-templates")
184
- def vps_os_templates(
185
- token: Optional[str] = typer.Option(None, "--token", help="Override token"),
186
- ):
187
- """List available OS templates."""
188
- c = get_client(token)
189
- data = sdk_call(c.vps.os_templates)
190
- templates = data if isinstance(data, list) else data.get("templates", []) if isinstance(data, dict) else []
191
-
192
- if is_json_mode():
193
- print_json(templates)
194
- return
195
-
196
- rows = []
197
- for tpl in templates:
198
- rows.append({
199
- "id": tpl.get("id", tpl.get("template_id", "")),
200
- "name": tpl.get("name", ""),
201
- "os_family": tpl.get("os_family", ""),
202
- })
203
-
204
- print_table(rows, [
205
- ("id", "Template ID", 24), ("name", "Name", 30), ("os_family", "OS Family", 14),
206
- ])
File without changes