wayscloud-cli 0.1.0__tar.gz → 0.1.1__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 (32) hide show
  1. wayscloud_cli-0.1.1/PKG-INFO +147 -0
  2. wayscloud_cli-0.1.1/README.md +123 -0
  3. wayscloud_cli-0.1.1/pyproject.toml +36 -0
  4. {wayscloud_cli-0.1.0 → wayscloud_cli-0.1.1}/setup.cfg +4 -0
  5. wayscloud_cli-0.1.1/tests/test_smoke.py +146 -0
  6. {wayscloud_cli-0.1.0 → wayscloud_cli-0.1.1}/wayscloud_cli/__init__.py +1 -1
  7. {wayscloud_cli-0.1.0 → wayscloud_cli-0.1.1}/wayscloud_cli/__main__.py +20 -6
  8. wayscloud_cli-0.1.1/wayscloud_cli/commands/__init__.py +0 -0
  9. wayscloud_cli-0.1.1/wayscloud_cli/commands/app.py +136 -0
  10. wayscloud_cli-0.1.1/wayscloud_cli/commands/db.py +103 -0
  11. wayscloud_cli-0.1.1/wayscloud_cli/commands/dns.py +171 -0
  12. wayscloud_cli-0.1.1/wayscloud_cli/commands/iot.py +165 -0
  13. {wayscloud_cli-0.1.0 → wayscloud_cli-0.1.1}/wayscloud_cli/commands/login.py +25 -35
  14. wayscloud_cli-0.1.1/wayscloud_cli/commands/redis.py +127 -0
  15. wayscloud_cli-0.1.1/wayscloud_cli/commands/storage.py +87 -0
  16. {wayscloud_cli-0.1.0 → wayscloud_cli-0.1.1}/wayscloud_cli/commands/vps.py +116 -104
  17. wayscloud_cli-0.1.1/wayscloud_cli/sdk.py +52 -0
  18. wayscloud_cli-0.1.1/wayscloud_cli.egg-info/PKG-INFO +147 -0
  19. {wayscloud_cli-0.1.0 → wayscloud_cli-0.1.1}/wayscloud_cli.egg-info/SOURCES.txt +9 -1
  20. {wayscloud_cli-0.1.0 → wayscloud_cli-0.1.1}/wayscloud_cli.egg-info/requires.txt +1 -1
  21. {wayscloud_cli-0.1.0 → wayscloud_cli-0.1.1}/wayscloud_cli.egg-info/top_level.txt +1 -0
  22. wayscloud_cli-0.1.0/PKG-INFO +0 -39
  23. wayscloud_cli-0.1.0/README.md +0 -23
  24. wayscloud_cli-0.1.0/pyproject.toml +0 -28
  25. wayscloud_cli-0.1.0/wayscloud_cli/client.py +0 -94
  26. wayscloud_cli-0.1.0/wayscloud_cli.egg-info/PKG-INFO +0 -39
  27. {wayscloud_cli-0.1.0/wayscloud_cli/commands → wayscloud_cli-0.1.1/tests}/__init__.py +0 -0
  28. {wayscloud_cli-0.1.0 → wayscloud_cli-0.1.1}/wayscloud_cli/commands/shell.py +0 -0
  29. {wayscloud_cli-0.1.0 → wayscloud_cli-0.1.1}/wayscloud_cli/config.py +0 -0
  30. {wayscloud_cli-0.1.0 → wayscloud_cli-0.1.1}/wayscloud_cli/output.py +0 -0
  31. {wayscloud_cli-0.1.0 → wayscloud_cli-0.1.1}/wayscloud_cli.egg-info/dependency_links.txt +0 -0
  32. {wayscloud_cli-0.1.0 → wayscloud_cli-0.1.1}/wayscloud_cli.egg-info/entry_points.txt +0 -0
@@ -0,0 +1,147 @@
1
+ Metadata-Version: 2.4
2
+ Name: wayscloud-cli
3
+ Version: 0.1.1
4
+ Summary: Command-line interface for WAYSCloud — manage VPS, DNS, databases, storage, apps, IoT, and more
5
+ License: MIT
6
+ Project-URL: Homepage, https://wayscloud.services
7
+ Project-URL: Documentation, https://docs.wayscloud.services/cli
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Intended Audience :: System Administrators
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.9
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Operating System :: OS Independent
18
+ Requires-Python: >=3.9
19
+ Description-Content-Type: text/markdown
20
+ Requires-Dist: wayscloud>=0.1.0
21
+ Requires-Dist: typer>=0.9
22
+ Requires-Dist: rich>=13.0
23
+ Requires-Dist: websockets>=12.0
24
+
25
+ # WAYSCloud CLI
26
+
27
+ Command-line interface for [WAYSCloud](https://wayscloud.services). Built on the [Python SDK](https://pypi.org/project/wayscloud/).
28
+
29
+ ## Installation
30
+
31
+ ```bash
32
+ pip install wayscloud-cli
33
+ ```
34
+
35
+ ## Authentication
36
+
37
+ ```bash
38
+ cloud login --token wayscloud_pat_...
39
+ cloud whoami
40
+ cloud logout
41
+ ```
42
+
43
+ Priority: `--token` flag > `WAYSCLOUD_TOKEN` env var > `~/.wayscloud/credentials` file.
44
+
45
+ ## Commands
46
+
47
+ ### VPS
48
+
49
+ ```bash
50
+ cloud vps list
51
+ cloud vps create --hostname web01 --plan vps-medium --region no --os ubuntu-24.04
52
+ cloud vps info <id>
53
+ cloud vps delete <id> --confirm
54
+ cloud vps start <id>
55
+ cloud vps stop <id>
56
+ cloud vps plans
57
+ cloud vps os-templates
58
+ ```
59
+
60
+ ### DNS
61
+
62
+ ```bash
63
+ cloud dns zones
64
+ cloud dns zones-create example.com
65
+ cloud dns records example.com
66
+ cloud dns records-create example.com --type A --name www --value 192.0.2.1
67
+ cloud dns records-delete example.com <record-id> --confirm
68
+ ```
69
+
70
+ ### Database
71
+
72
+ ```bash
73
+ cloud db list
74
+ cloud db create mydb --type postgresql --tier standard
75
+ cloud db info postgresql mydb
76
+ cloud db delete postgresql mydb --confirm
77
+ ```
78
+
79
+ ### Redis
80
+
81
+ ```bash
82
+ cloud redis list
83
+ cloud redis create myredis --plan redis-starter --region no
84
+ cloud redis info <id>
85
+ cloud redis delete <id> --confirm
86
+ cloud redis plans
87
+ ```
88
+
89
+ ### Storage
90
+
91
+ ```bash
92
+ cloud storage buckets
93
+ cloud storage buckets-create my-bucket
94
+ cloud storage buckets-delete my-bucket --confirm
95
+ cloud storage credentials
96
+ ```
97
+
98
+ ### Apps
99
+
100
+ ```bash
101
+ cloud app list
102
+ cloud app create my-app --plan app-basic --region eu
103
+ cloud app deploy <id> --image ghcr.io/org/app:latest
104
+ cloud app start <id>
105
+ cloud app stop <id>
106
+ cloud app delete <id> --confirm
107
+ ```
108
+
109
+ ### IoT
110
+
111
+ ```bash
112
+ cloud iot devices
113
+ cloud iot devices-create --device-id sensor-01 --name "Temperature Sensor"
114
+ cloud iot devices-info <device-id>
115
+ cloud iot devices-delete <device-id> --confirm
116
+ cloud iot groups
117
+ cloud iot groups-create --name "Floor 2"
118
+ ```
119
+
120
+ ### Shell
121
+
122
+ ```bash
123
+ cloud shell connect
124
+ ```
125
+
126
+ ## Output formats
127
+
128
+ ```bash
129
+ # Default: formatted tables
130
+ cloud vps list
131
+
132
+ # JSON (for scripting)
133
+ cloud vps list --json
134
+ ```
135
+
136
+ ## Requirements
137
+
138
+ - Python 3.9+
139
+ - wayscloud (SDK)
140
+
141
+ ## Documentation
142
+
143
+ Full reference: [docs.wayscloud.services/cli](https://docs.wayscloud.services/cli)
144
+
145
+ ## License
146
+
147
+ MIT
@@ -0,0 +1,123 @@
1
+ # WAYSCloud CLI
2
+
3
+ Command-line interface for [WAYSCloud](https://wayscloud.services). Built on the [Python SDK](https://pypi.org/project/wayscloud/).
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install wayscloud-cli
9
+ ```
10
+
11
+ ## Authentication
12
+
13
+ ```bash
14
+ cloud login --token wayscloud_pat_...
15
+ cloud whoami
16
+ cloud logout
17
+ ```
18
+
19
+ Priority: `--token` flag > `WAYSCLOUD_TOKEN` env var > `~/.wayscloud/credentials` file.
20
+
21
+ ## Commands
22
+
23
+ ### VPS
24
+
25
+ ```bash
26
+ cloud vps list
27
+ cloud vps create --hostname web01 --plan vps-medium --region no --os ubuntu-24.04
28
+ cloud vps info <id>
29
+ cloud vps delete <id> --confirm
30
+ cloud vps start <id>
31
+ cloud vps stop <id>
32
+ cloud vps plans
33
+ cloud vps os-templates
34
+ ```
35
+
36
+ ### DNS
37
+
38
+ ```bash
39
+ cloud dns zones
40
+ cloud dns zones-create example.com
41
+ cloud dns records example.com
42
+ cloud dns records-create example.com --type A --name www --value 192.0.2.1
43
+ cloud dns records-delete example.com <record-id> --confirm
44
+ ```
45
+
46
+ ### Database
47
+
48
+ ```bash
49
+ cloud db list
50
+ cloud db create mydb --type postgresql --tier standard
51
+ cloud db info postgresql mydb
52
+ cloud db delete postgresql mydb --confirm
53
+ ```
54
+
55
+ ### Redis
56
+
57
+ ```bash
58
+ cloud redis list
59
+ cloud redis create myredis --plan redis-starter --region no
60
+ cloud redis info <id>
61
+ cloud redis delete <id> --confirm
62
+ cloud redis plans
63
+ ```
64
+
65
+ ### Storage
66
+
67
+ ```bash
68
+ cloud storage buckets
69
+ cloud storage buckets-create my-bucket
70
+ cloud storage buckets-delete my-bucket --confirm
71
+ cloud storage credentials
72
+ ```
73
+
74
+ ### Apps
75
+
76
+ ```bash
77
+ cloud app list
78
+ cloud app create my-app --plan app-basic --region eu
79
+ cloud app deploy <id> --image ghcr.io/org/app:latest
80
+ cloud app start <id>
81
+ cloud app stop <id>
82
+ cloud app delete <id> --confirm
83
+ ```
84
+
85
+ ### IoT
86
+
87
+ ```bash
88
+ cloud iot devices
89
+ cloud iot devices-create --device-id sensor-01 --name "Temperature Sensor"
90
+ cloud iot devices-info <device-id>
91
+ cloud iot devices-delete <device-id> --confirm
92
+ cloud iot groups
93
+ cloud iot groups-create --name "Floor 2"
94
+ ```
95
+
96
+ ### Shell
97
+
98
+ ```bash
99
+ cloud shell connect
100
+ ```
101
+
102
+ ## Output formats
103
+
104
+ ```bash
105
+ # Default: formatted tables
106
+ cloud vps list
107
+
108
+ # JSON (for scripting)
109
+ cloud vps list --json
110
+ ```
111
+
112
+ ## Requirements
113
+
114
+ - Python 3.9+
115
+ - wayscloud (SDK)
116
+
117
+ ## Documentation
118
+
119
+ Full reference: [docs.wayscloud.services/cli](https://docs.wayscloud.services/cli)
120
+
121
+ ## License
122
+
123
+ MIT
@@ -0,0 +1,36 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "wayscloud-cli"
7
+ version = "0.1.1"
8
+ description = "Command-line interface for WAYSCloud — manage VPS, DNS, databases, storage, apps, IoT, and more"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = {text = "MIT"}
12
+ classifiers = [
13
+ "Development Status :: 4 - Beta",
14
+ "Intended Audience :: Developers",
15
+ "Intended Audience :: System Administrators",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.9",
19
+ "Programming Language :: Python :: 3.10",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Operating System :: OS Independent",
23
+ ]
24
+ dependencies = [
25
+ "wayscloud>=0.1.0",
26
+ "typer>=0.9",
27
+ "rich>=13.0",
28
+ "websockets>=12.0",
29
+ ]
30
+
31
+ [project.urls]
32
+ Homepage = "https://wayscloud.services"
33
+ Documentation = "https://docs.wayscloud.services/cli"
34
+
35
+ [project.scripts]
36
+ cloud = "wayscloud_cli.__main__:main"
@@ -5,6 +5,10 @@ version = 0.1.0
5
5
  [options]
6
6
  packages = find:
7
7
 
8
+ [options.entry_points]
9
+ console_scripts =
10
+ cloud = wayscloud_cli.__main__:main
11
+
8
12
  [egg_info]
9
13
  tag_build =
10
14
  tag_date = 0
@@ -0,0 +1,146 @@
1
+ """CLI smoke tests — verify app structure, command registration, flags, and token resolution."""
2
+
3
+ import json
4
+ import os
5
+ from unittest.mock import patch
6
+
7
+ from typer.testing import CliRunner
8
+
9
+ from wayscloud_cli.__main__ import app
10
+ from wayscloud_cli import __version__
11
+ from wayscloud_cli.config import resolve_token
12
+ from wayscloud_cli.output import set_json_mode, is_json_mode
13
+
14
+ runner = CliRunner()
15
+
16
+
17
+ # ── App structure ───────────────────────────────────────────────
18
+
19
+ def test_app_has_help():
20
+ result = runner.invoke(app, ["--help"])
21
+ assert result.exit_code == 0
22
+ assert "WAYSCloud CLI" in result.output
23
+
24
+
25
+ def test_version_flag():
26
+ result = runner.invoke(app, ["--version"])
27
+ assert result.exit_code == 0
28
+ assert __version__ in result.output
29
+
30
+
31
+ # ── Command groups registered ───────────────────────────────────
32
+
33
+ EXPECTED_GROUPS = ["auth", "vps", "dns", "storage", "db", "redis", "app", "iot", "shell"]
34
+
35
+
36
+ def test_all_command_groups_registered():
37
+ result = runner.invoke(app, ["--help"])
38
+ for group in EXPECTED_GROUPS:
39
+ assert group in result.output, f"Command group '{group}' not in help output"
40
+
41
+
42
+ def test_each_group_has_help():
43
+ for group in EXPECTED_GROUPS:
44
+ result = runner.invoke(app, [group, "--help"])
45
+ assert result.exit_code == 0, f"{group} --help failed with exit {result.exit_code}"
46
+
47
+
48
+ # ── Top-level shortcuts ─────────────────────────────────────────
49
+
50
+ def test_login_shortcut_exists():
51
+ result = runner.invoke(app, ["login", "--help"])
52
+ assert result.exit_code == 0
53
+ assert "token" in result.output.lower()
54
+
55
+
56
+ def test_whoami_shortcut_exists():
57
+ result = runner.invoke(app, ["whoami", "--help"])
58
+ assert result.exit_code == 0
59
+
60
+
61
+ def test_logout_shortcut_exists():
62
+ result = runner.invoke(app, ["logout", "--help"])
63
+ assert result.exit_code == 0
64
+
65
+
66
+ # ── Token resolution ────────────────────────────────────────────
67
+
68
+ def test_explicit_token_wins(monkeypatch):
69
+ monkeypatch.setenv("WAYSCLOUD_TOKEN", "env_token")
70
+ assert resolve_token("explicit_token") == "explicit_token"
71
+
72
+
73
+ def test_env_var_used_when_no_explicit(monkeypatch):
74
+ monkeypatch.setenv("WAYSCLOUD_TOKEN", "env_token")
75
+ assert resolve_token(None) == "env_token"
76
+
77
+
78
+ def test_returns_none_when_nothing(monkeypatch, tmp_path):
79
+ monkeypatch.delenv("WAYSCLOUD_TOKEN", raising=False)
80
+ # Point credentials file to nonexistent path
81
+ monkeypatch.setattr("wayscloud_cli.config.CREDENTIALS_FILE", tmp_path / "nope")
82
+ assert resolve_token(None) is None
83
+
84
+
85
+ def test_reads_credentials_file(monkeypatch, tmp_path):
86
+ monkeypatch.delenv("WAYSCLOUD_TOKEN", raising=False)
87
+ cred_file = tmp_path / "credentials"
88
+ cred_file.write_text(json.dumps({"version": 1, "token": "file_token"}))
89
+ monkeypatch.setattr("wayscloud_cli.config.CREDENTIALS_FILE", cred_file)
90
+ assert resolve_token(None) == "file_token"
91
+
92
+
93
+ # ── Output modes ────────────────────────────────────────────────
94
+
95
+ def test_json_mode_toggle():
96
+ set_json_mode(False)
97
+ assert not is_json_mode()
98
+ set_json_mode(True)
99
+ assert is_json_mode()
100
+ set_json_mode(False)
101
+
102
+
103
+ def test_json_flag_accepted():
104
+ result = runner.invoke(app, ["--json", "--help"])
105
+ assert result.exit_code == 0
106
+
107
+
108
+ # ── VPS commands exist with correct args ────────────────────────
109
+
110
+ def test_vps_list_help():
111
+ result = runner.invoke(app, ["vps", "list", "--help"])
112
+ assert result.exit_code == 0
113
+ assert "--token" in result.output
114
+
115
+
116
+ def test_vps_create_requires_flags():
117
+ result = runner.invoke(app, ["vps", "create", "--help"])
118
+ assert result.exit_code == 0
119
+ for flag in ["--hostname", "--plan", "--region", "--os"]:
120
+ assert flag in result.output, f"Missing {flag} in vps create"
121
+
122
+
123
+ # ── DNS commands ────────────────────────────────────────────────
124
+
125
+ def test_dns_records_create_has_type_flag():
126
+ result = runner.invoke(app, ["dns", "records-create", "--help"])
127
+ assert result.exit_code == 0
128
+ assert "--type" in result.output
129
+ assert "--value" in result.output
130
+
131
+
132
+ # ── DB commands ─────────────────────────────────────────────────
133
+
134
+ def test_db_create_has_type_flag():
135
+ result = runner.invoke(app, ["db", "create", "--help"])
136
+ assert result.exit_code == 0
137
+ assert "--type" in result.output
138
+
139
+
140
+ # ── IoT commands ────────────────────────────────────────────────
141
+
142
+ def test_iot_devices_create_has_required_flags():
143
+ result = runner.invoke(app, ["iot", "devices-create", "--help"])
144
+ assert result.exit_code == 0
145
+ assert "--device-id" in result.output
146
+ assert "--name" in result.output
@@ -1,2 +1,2 @@
1
1
  """WAYSCloud CLI — cloud command."""
2
- __version__ = "0.1.0"
2
+ __version__ = "0.1.1"
@@ -3,15 +3,23 @@ WAYSCloud CLI entry point.
3
3
 
4
4
  Binary name: cloud (C21)
5
5
  Package name: wayscloud-cli (C21)
6
+
7
+ Principle: CLI = provisioning + control, NOT observability.
6
8
  """
7
9
 
8
10
  import typer
9
11
 
10
12
  from . import __version__
11
13
  from .output import set_json_mode, set_no_color
12
- from .commands.login import app as login_app
14
+ from .commands.login import app as login_app, login, logout, whoami
13
15
  from .commands.vps import app as vps_app
14
16
  from .commands.shell import app as shell_app
17
+ from .commands.dns import app as dns_app
18
+ from .commands.storage import app as storage_app
19
+ from .commands.db import app as db_app
20
+ from .commands.redis import app as redis_app
21
+ from .commands.app import app as app_platform_app
22
+ from .commands.iot import app as iot_app
15
23
 
16
24
  app = typer.Typer(
17
25
  name="cloud",
@@ -43,14 +51,20 @@ def global_options(
43
51
 
44
52
 
45
53
  # Register command groups
46
- app.add_typer(login_app, name="auth", help="Login, logout, whoami")
54
+ app.add_typer(login_app, name="auth", help="Authentication")
47
55
  app.add_typer(vps_app, name="vps", help="Virtual Private Servers")
48
- app.add_typer(shell_app, name="shell", help="Interactive CloudShell")
56
+ app.add_typer(dns_app, name="dns", help="DNS zones and records")
57
+ app.add_typer(storage_app, name="storage", help="Object storage (S3)")
58
+ app.add_typer(db_app, name="db", help="Managed databases")
59
+ app.add_typer(redis_app, name="redis", help="Managed Redis")
60
+ app.add_typer(app_platform_app, name="app", help="App platform")
61
+ app.add_typer(iot_app, name="iot", help="IoT platform")
62
+ app.add_typer(shell_app, name="shell", help="CloudShell")
49
63
 
50
64
  # Top-level shortcuts for login/logout/whoami
51
- app.command("login")(login_app.registered_commands[0].callback)
52
- app.command("logout")(login_app.registered_commands[1].callback)
53
- app.command("whoami")(login_app.registered_commands[2].callback)
65
+ app.command("login")(login)
66
+ app.command("logout")(logout)
67
+ app.command("whoami")(whoami)
54
68
 
55
69
 
56
70
  def main():
File without changes
@@ -0,0 +1,136 @@
1
+ """
2
+ cloud app commands — App Platform 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="App platform")
12
+
13
+
14
+ @app.command("list")
15
+ def list_apps(
16
+ token: Optional[str] = typer.Option(None, "--token", help="Override token"),
17
+ ):
18
+ """List your apps."""
19
+ c = get_client(token)
20
+ data = sdk_call(c.apps.list)
21
+ apps_list = data if isinstance(data, list) else data.get("apps", []) if isinstance(data, dict) else []
22
+
23
+ if is_json_mode():
24
+ print_json(apps_list)
25
+ return
26
+
27
+ if not apps_list:
28
+ print("No apps found.")
29
+ return
30
+
31
+ rows = [{"id": str(a.get("id", ""))[:14], "name": a.get("name", ""),
32
+ "status": a.get("status", ""), "plan": a.get("plan", a.get("plan_code", "")),
33
+ "url": a.get("default_url", ""), "created_at": a.get("created_at", "")}
34
+ for a in apps_list]
35
+
36
+ print_table(rows, [
37
+ ("id", "ID", 14), ("name", "Name", 20), ("status", "Status", 10),
38
+ ("plan", "Plan", 16), ("url", "URL", 30), ("created_at", "Created", 20),
39
+ ])
40
+
41
+
42
+ @app.command("create")
43
+ def create_app(
44
+ name: str = typer.Argument(..., help="App name"),
45
+ plan: str = typer.Option("app-basic", "--plan", help="Plan code"),
46
+ region: str = typer.Option("no", "--region", help="Region"),
47
+ port: int = typer.Option(8080, "--port", help="Application port"),
48
+ token: Optional[str] = typer.Option(None, "--token", help="Override token"),
49
+ ):
50
+ """Create a new app."""
51
+ c = get_client(token)
52
+ data = sdk_call(c.apps.create, name, plan=plan, region=region, port=port)
53
+ if is_json_mode():
54
+ print_json(data)
55
+ else:
56
+ print(f"App created: {data.get('name', name)}")
57
+ print_object(data, [("id", "ID"), ("name", "Name"), ("status", "Status"),
58
+ ("plan", "Plan"), ("default_url", "URL")])
59
+
60
+
61
+ @app.command("info")
62
+ def info_app(
63
+ app_id: str = typer.Argument(..., help="App ID"),
64
+ token: Optional[str] = typer.Option(None, "--token", help="Override token"),
65
+ ):
66
+ """Show app details."""
67
+ c = get_client(token)
68
+ data = sdk_call(c.apps.get, app_id)
69
+ print_object(data, [
70
+ ("id", "ID"), ("name", "Name"), ("status", "Status"), ("plan", "Plan"),
71
+ ("region", "Region"), ("port", "Port"), ("default_url", "URL"),
72
+ ("image_uri", "Image"), ("created_at", "Created"),
73
+ ])
74
+
75
+
76
+ @app.command("delete")
77
+ def delete_app(
78
+ app_id: str = typer.Argument(..., help="App ID"),
79
+ token: Optional[str] = typer.Option(None, "--token", help="Override token"),
80
+ confirm: bool = typer.Option(False, "--confirm", help="Skip confirmation"),
81
+ ):
82
+ """Delete an app (permanent)."""
83
+ if not confirm:
84
+ print(f"This will permanently delete app {app_id} and all its data.")
85
+ print("Use --confirm to proceed.")
86
+ raise typer.Exit(code=1)
87
+
88
+ c = get_client(token)
89
+ data = sdk_call(c.apps.delete, app_id)
90
+ if is_json_mode():
91
+ print_json(data)
92
+ else:
93
+ print(f"App {app_id}: deleted")
94
+
95
+
96
+ @app.command("deploy")
97
+ def deploy_app(
98
+ app_id: str = typer.Argument(..., help="App ID"),
99
+ image: str = typer.Option(..., "--image", help="Container image URI"),
100
+ token: Optional[str] = typer.Option(None, "--token", help="Override token"),
101
+ ):
102
+ """Deploy an app from a container image."""
103
+ c = get_client(token)
104
+ data = sdk_call(c.apps.deploy, app_id, image)
105
+ if is_json_mode():
106
+ print_json(data)
107
+ else:
108
+ print(f"App {app_id}: deploying image {image}")
109
+
110
+
111
+ @app.command("start")
112
+ def start_app(
113
+ app_id: str = typer.Argument(..., help="App ID"),
114
+ token: Optional[str] = typer.Option(None, "--token", help="Override token"),
115
+ ):
116
+ """Start an app."""
117
+ c = get_client(token)
118
+ data = sdk_call(c.apps.start, app_id)
119
+ if is_json_mode():
120
+ print_json(data)
121
+ else:
122
+ print(f"App {app_id}: {data.get('message', 'starting')}")
123
+
124
+
125
+ @app.command("stop")
126
+ def stop_app(
127
+ app_id: str = typer.Argument(..., help="App ID"),
128
+ token: Optional[str] = typer.Option(None, "--token", help="Override token"),
129
+ ):
130
+ """Stop an app."""
131
+ c = get_client(token)
132
+ data = sdk_call(c.apps.stop, app_id)
133
+ if is_json_mode():
134
+ print_json(data)
135
+ else:
136
+ print(f"App {app_id}: {data.get('message', 'stopping')}")