kctl-vendure 0.6.2__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.
@@ -0,0 +1,3 @@
1
+ """kctl-vendure: Kodemeio Vendure e-commerce CLI."""
2
+
3
+ __version__ = "0.6.2"
@@ -0,0 +1,5 @@
1
+ """Allow running as: python -m kctl_vendure."""
2
+
3
+ from kctl_vendure.cli import _run
4
+
5
+ _run()
kctl_vendure/cli.py ADDED
@@ -0,0 +1,101 @@
1
+ """Main CLI entry point for kctl-vendure."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Annotated
6
+
7
+ import typer
8
+ from kctl_lib import KctlError, handle_cli_error
9
+
10
+ from kctl_vendure import __version__
11
+ from kctl_vendure.commands.assets import app as assets_app
12
+ from kctl_vendure.commands.config_cmd import app as config_app
13
+ from kctl_vendure.commands.customers import app as customers_app
14
+ from kctl_vendure.commands.doctor import app as doctor_app
15
+ from kctl_vendure.commands.orders import app as orders_app
16
+ from kctl_vendure.commands.payments import app as payments_app
17
+ from kctl_vendure.commands.products import app as products_app
18
+ from kctl_vendure.core.callbacks import AppContext
19
+ from kctl_lib.self_update import notify_if_outdated
20
+ from kctl_lib.tui import add_tui_command
21
+
22
+
23
+ def version_callback(value: bool) -> None:
24
+ if value:
25
+ typer.echo(f"kctl-vendure {__version__}")
26
+ raise typer.Exit()
27
+
28
+
29
+ app = typer.Typer(
30
+ name="kctl-vendure",
31
+ help="Kodemeio Vendure CLI - manage your Vendure e-commerce instance.",
32
+ no_args_is_help=True,
33
+ rich_markup_mode="rich",
34
+ pretty_exceptions_enable=False,
35
+ )
36
+
37
+
38
+ @app.callback()
39
+ def main(
40
+ ctx: typer.Context,
41
+ json_output: Annotated[bool, typer.Option("--json", help="Output as JSON (shortcut for --format json)")] = False,
42
+ quiet: Annotated[bool, typer.Option("--quiet", "-q", help="Suppress info messages")] = False,
43
+ output_format: Annotated[
44
+ str, typer.Option("--format", "-f", help="Output format: pretty, json, csv, yaml")
45
+ ] = "pretty",
46
+ no_header: Annotated[bool, typer.Option("--no-header", help="Omit headers in CSV output")] = False,
47
+ debug: Annotated[bool, typer.Option("--debug", help="Enable debug logging")] = False,
48
+ profile: Annotated[str | None, typer.Option("--profile", "-p", help="Config profile name")] = None,
49
+ url: Annotated[str | None, typer.Option("--url", help="API URL override")] = None,
50
+ username: Annotated[str | None, typer.Option("--username", "-u", help="Admin username override")] = None,
51
+ password: Annotated[str | None, typer.Option("--password", help="Admin password override")] = None,
52
+ version: Annotated[
53
+ bool, typer.Option("--version", "-V", callback=version_callback, is_eager=True, help="Show version")
54
+ ] = False,
55
+ ) -> None:
56
+ """Kodemeio Vendure e-commerce CLI."""
57
+ import os
58
+
59
+ if debug:
60
+ os.environ["KCTL_DEBUG"] = "1"
61
+
62
+ effective_format = "json" if json_output else output_format
63
+
64
+ ctx.ensure_object(dict)
65
+ ctx.obj = AppContext(
66
+ json_mode=json_output or effective_format == "json",
67
+ quiet=quiet,
68
+ format=effective_format,
69
+ no_header=no_header,
70
+ debug=debug,
71
+ profile=profile,
72
+ url_override=url,
73
+ username_override=username,
74
+ password_override=password,
75
+ )
76
+ notify_if_outdated(ctx.obj.output, "kctl-vendure", __version__)
77
+
78
+
79
+ # ---------------------------------------------------------------------------
80
+ # Command groups
81
+ # ---------------------------------------------------------------------------
82
+ app.add_typer(config_app, name="config")
83
+ app.add_typer(products_app, name="products")
84
+ app.add_typer(orders_app, name="orders")
85
+ app.add_typer(customers_app, name="customers")
86
+ app.add_typer(assets_app, name="assets")
87
+ app.add_typer(payments_app, name="payments")
88
+ app.add_typer(doctor_app, name="doctor")
89
+ add_tui_command(app, service_key="vendure", version=__version__)
90
+
91
+
92
+ def _run() -> None:
93
+ """Entry point with error handling."""
94
+ try:
95
+ app()
96
+ except KctlError as e:
97
+ handle_cli_error(e)
98
+
99
+
100
+ if __name__ == "__main__":
101
+ _run()
File without changes
@@ -0,0 +1,90 @@
1
+ """Asset management commands for Vendure e-commerce."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Annotated
6
+
7
+ import typer
8
+
9
+ from kctl_vendure.core.callbacks import AppContext
10
+
11
+ app = typer.Typer(help="Manage Vendure assets.")
12
+
13
+ # ---------------------------------------------------------------------------
14
+ # GraphQL Operations
15
+ # ---------------------------------------------------------------------------
16
+
17
+ ASSETS_LIST_QUERY = """
18
+ query GetAssets($options: AssetListOptions) {
19
+ assets(options: $options) {
20
+ items {
21
+ id
22
+ name
23
+ type
24
+ fileSize
25
+ mimeType
26
+ source
27
+ preview
28
+ }
29
+ totalItems
30
+ }
31
+ }
32
+ """
33
+
34
+ DELETE_ASSET_MUTATION = """
35
+ mutation DeleteAsset($input: DeleteAssetInput!) {
36
+ deleteAsset(input: $input) {
37
+ result
38
+ message
39
+ }
40
+ }
41
+ """
42
+
43
+
44
+ @app.command("list")
45
+ def list_cmd(
46
+ ctx: typer.Context,
47
+ limit: Annotated[int, typer.Option("--limit", "-l", help="Number of assets to return")] = 10,
48
+ skip: Annotated[int, typer.Option("--skip", help="Number of assets to skip")] = 0,
49
+ ) -> None:
50
+ """List assets."""
51
+ c: AppContext = ctx.obj
52
+ out = c.output
53
+ variables = {"options": {"take": limit, "skip": skip}}
54
+ data = c.client.query(ASSETS_LIST_QUERY, variables)
55
+ assets_data = data.get("assets", {})
56
+ items = assets_data.get("items", [])
57
+ total = assets_data.get("totalItems", 0)
58
+
59
+ if c.json_mode:
60
+ out.raw_json({"items": items, "totalItems": total})
61
+ else:
62
+ out.info(f"Assets ({total} total)")
63
+ out.table(items, columns=["id", "name", "type", "fileSize", "mimeType"], title="Assets")
64
+
65
+
66
+ @app.command()
67
+ def delete(
68
+ ctx: typer.Context,
69
+ asset_id: Annotated[str, typer.Argument(help="Asset ID")],
70
+ force: Annotated[bool, typer.Option("--force", "-f", help="Skip confirmation and force delete")] = False,
71
+ ) -> None:
72
+ """Delete an asset."""
73
+ c: AppContext = ctx.obj
74
+ out = c.output
75
+
76
+ if not force:
77
+ typer.confirm(f"Delete asset {asset_id}?", abort=True)
78
+
79
+ variables = {"input": {"assetId": asset_id, "force": force}}
80
+ data = c.client.mutate(DELETE_ASSET_MUTATION, variables)
81
+ result = data.get("deleteAsset", {})
82
+
83
+ if c.json_mode:
84
+ out.raw_json(result)
85
+ else:
86
+ if result.get("result") == "DELETED":
87
+ out.success(f"Asset {asset_id} deleted")
88
+ else:
89
+ out.error(f"Delete failed: {result.get('message', 'Unknown error')}")
90
+ raise typer.Exit(1)
@@ -0,0 +1,108 @@
1
+ """Configuration management commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Annotated
6
+
7
+ import typer
8
+
9
+ from kctl_vendure.core.callbacks import AppContext
10
+ from kctl_vendure.core.config import (
11
+ CONFIG_FILE,
12
+ SERVICE_KEY,
13
+ ServiceConfig,
14
+ get_all_services_in_profile,
15
+ get_default_profile,
16
+ get_profile_names,
17
+ resolve_active_profile_name,
18
+ set_default_profile,
19
+ set_service_config,
20
+ )
21
+
22
+ app = typer.Typer(help="Manage CLI configuration and profiles.")
23
+
24
+
25
+ def _mask(val: str) -> str:
26
+ if not val:
27
+ return "[dim]not set[/dim]"
28
+ return f"{val[:4]}{'*' * max(0, len(val) - 8)}{val[-4:]}" if len(val) > 10 else "****"
29
+
30
+
31
+ @app.command()
32
+ def init(
33
+ ctx: typer.Context,
34
+ url: Annotated[str | None, typer.Option("--url")] = None,
35
+ username: Annotated[str | None, typer.Option("--username", "-u")] = None,
36
+ password: Annotated[str | None, typer.Option("--password")] = None,
37
+ name: Annotated[str | None, typer.Option("--name", "-n")] = None,
38
+ ) -> None:
39
+ """Initialize CLI configuration."""
40
+ c: AppContext = ctx.obj
41
+ out = c.output
42
+ profile_name = name or typer.prompt("Profile name", default="kodemeio")
43
+ api_url = url or typer.prompt("Vendure URL (e.g. https://shop.example.com)")
44
+ admin_user = username or typer.prompt("Admin username", default="superadmin")
45
+ admin_pass = password or typer.prompt("Admin password", hide_input=True)
46
+
47
+ svc = ServiceConfig(url=api_url, admin_username=admin_user, admin_password=admin_pass)
48
+ set_service_config(profile_name, svc)
49
+ if len(get_profile_names()) <= 1:
50
+ set_default_profile(profile_name)
51
+ out.success(f"Configuration saved to {CONFIG_FILE}")
52
+ out.kv("Profile", profile_name)
53
+ out.kv("URL", api_url)
54
+ out.kv("Username", admin_user)
55
+ out.kv("Password", _mask(admin_pass))
56
+
57
+
58
+ @app.command()
59
+ def show(ctx: typer.Context) -> None:
60
+ """Show configuration (passwords masked)."""
61
+ c: AppContext = ctx.obj
62
+ out = c.output
63
+ default = get_default_profile()
64
+ sections = [
65
+ (
66
+ "General",
67
+ [
68
+ ("Config file", str(CONFIG_FILE)),
69
+ ("Default profile", default),
70
+ ("Service key", SERVICE_KEY),
71
+ ],
72
+ )
73
+ ]
74
+ for pname in get_profile_names():
75
+ marker = " [green](default)[/green]" if pname == default else ""
76
+ services = get_all_services_in_profile(pname)
77
+ kvs = []
78
+ for svc_name, svc_data in services.items():
79
+ if not isinstance(svc_data, dict):
80
+ continue
81
+ indicator = "[green]●[/green]" if svc_name == SERVICE_KEY else "[dim]○[/dim]"
82
+ kvs.append(
83
+ (
84
+ f"{indicator} {svc_name}",
85
+ f"{svc_data.get('url', '')} user: {svc_data.get('admin_username', '')} pass: {_mask(svc_data.get('admin_password', ''))}",
86
+ )
87
+ )
88
+ sections.append((f"Profile: {pname}{marker}", kvs or [("(empty)", "")]))
89
+ out.detail("Configuration", sections)
90
+
91
+
92
+ @app.command()
93
+ def test(ctx: typer.Context) -> None:
94
+ """Test API connection."""
95
+ c: AppContext = ctx.obj
96
+ out = c.output
97
+ active = resolve_active_profile_name(c.profile)
98
+ out.info(f"Testing profile '{active}' -> {SERVICE_KEY}")
99
+ try:
100
+ status = c.client.check_health()
101
+ if status == 200:
102
+ out.success("Connected to Vendure API")
103
+ else:
104
+ out.error(f"Connection failed: HTTP {status}")
105
+ raise typer.Exit(1)
106
+ except Exception as e:
107
+ out.error(f"Connection failed: {e}")
108
+ raise typer.Exit(1) from e
@@ -0,0 +1,104 @@
1
+ """Customer management commands for Vendure e-commerce."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Annotated
6
+
7
+ import typer
8
+
9
+ from kctl_vendure.core.callbacks import AppContext
10
+
11
+ app = typer.Typer(help="Manage Vendure customers.")
12
+
13
+ # ---------------------------------------------------------------------------
14
+ # GraphQL Operations
15
+ # ---------------------------------------------------------------------------
16
+
17
+ CUSTOMERS_LIST_QUERY = """
18
+ query GetCustomers($options: CustomerListOptions) {
19
+ customers(options: $options) {
20
+ items {
21
+ id
22
+ firstName
23
+ lastName
24
+ emailAddress
25
+ createdAt
26
+ }
27
+ totalItems
28
+ }
29
+ }
30
+ """
31
+
32
+ CUSTOMER_GET_QUERY = """
33
+ query GetCustomer($id: ID!) {
34
+ customer(id: $id) {
35
+ id
36
+ firstName
37
+ lastName
38
+ emailAddress
39
+ phoneNumber
40
+ createdAt
41
+ orders {
42
+ items {
43
+ id
44
+ code
45
+ state
46
+ totalWithTax
47
+ }
48
+ totalItems
49
+ }
50
+ }
51
+ }
52
+ """
53
+
54
+
55
+ @app.command("list")
56
+ def list_cmd(
57
+ ctx: typer.Context,
58
+ limit: Annotated[int, typer.Option("--limit", "-l", help="Number of customers to return")] = 10,
59
+ skip: Annotated[int, typer.Option("--skip", help="Number of customers to skip")] = 0,
60
+ ) -> None:
61
+ """List customers."""
62
+ c: AppContext = ctx.obj
63
+ out = c.output
64
+ variables = {"options": {"take": limit, "skip": skip}}
65
+ data = c.client.query(CUSTOMERS_LIST_QUERY, variables)
66
+ customers_data = data.get("customers", {})
67
+ items = customers_data.get("items", [])
68
+ total = customers_data.get("totalItems", 0)
69
+
70
+ if c.json_mode:
71
+ out.raw_json({"items": items, "totalItems": total})
72
+ else:
73
+ out.info(f"Customers ({total} total)")
74
+ out.table(items, columns=["id", "firstName", "lastName", "emailAddress", "createdAt"], title="Customers")
75
+
76
+
77
+ @app.command()
78
+ def get(
79
+ ctx: typer.Context,
80
+ customer_id: Annotated[str, typer.Argument(help="Customer ID")],
81
+ ) -> None:
82
+ """Get a single customer with order history."""
83
+ c: AppContext = ctx.obj
84
+ out = c.output
85
+ data = c.client.query(CUSTOMER_GET_QUERY, {"id": customer_id})
86
+ customer = data.get("customer")
87
+
88
+ if not customer:
89
+ out.error(f"Customer {customer_id} not found")
90
+ raise typer.Exit(1)
91
+
92
+ if c.json_mode:
93
+ out.raw_json(customer)
94
+ else:
95
+ out.kv("ID", customer["id"])
96
+ out.kv("Name", f"{customer['firstName']} {customer['lastName']}")
97
+ out.kv("Email", customer["emailAddress"])
98
+ out.kv("Phone", customer.get("phoneNumber", ""))
99
+ out.kv("Created", customer.get("createdAt", ""))
100
+ orders = customer.get("orders", {})
101
+ order_items = orders.get("items", [])
102
+ if order_items:
103
+ out.info(f"Orders ({orders.get('totalItems', 0)} total)")
104
+ out.table(order_items, columns=["id", "code", "state", "totalWithTax"], title="Orders")
@@ -0,0 +1,83 @@
1
+ """Doctor diagnostic checks for kctl-vendure."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+ import typer
8
+ from kctl_lib.doctor_base import CheckResult, DoctorCheck, run_doctor
9
+
10
+ from kctl_vendure.core.callbacks import AppContext
11
+
12
+
13
+ @dataclass
14
+ class APIConnectivityCheck:
15
+ """Check that the configured API endpoint is reachable."""
16
+
17
+ name: str = "API Connectivity"
18
+
19
+ def run(self) -> CheckResult:
20
+ try:
21
+ from kctl_vendure.core.config import get_service_config, resolve_active_profile_name
22
+
23
+ profile = resolve_active_profile_name()
24
+ cfg = get_service_config(profile)
25
+ url = cfg.url or ""
26
+ if not url:
27
+ return CheckResult(
28
+ name=self.name,
29
+ status="fail",
30
+ message="No URL configured",
31
+ fix_command="kctl-vendure config init",
32
+ )
33
+ return CheckResult(name=self.name, status="ok", message=f"URL: {url}")
34
+ except Exception as e:
35
+ return CheckResult(name=self.name, status="warn", message=str(e))
36
+
37
+
38
+ @dataclass
39
+ class AuthCheck:
40
+ """Check that authentication credentials are configured."""
41
+
42
+ name: str = "Authentication"
43
+
44
+ def run(self) -> CheckResult:
45
+ try:
46
+ from kctl_vendure.core.config import get_service_config, resolve_active_profile_name
47
+
48
+ profile = resolve_active_profile_name()
49
+ cfg = get_service_config(profile)
50
+ username = cfg.admin_username or ""
51
+ password = cfg.admin_password or ""
52
+ if not username or not password:
53
+ return CheckResult(
54
+ name=self.name,
55
+ status="fail",
56
+ message="Admin credentials not configured",
57
+ fix_command="kctl-vendure config init",
58
+ )
59
+ masked = password[:4] + "****" + password[-4:] if len(password) > 8 else "****"
60
+ return CheckResult(name=self.name, status="ok", message=f"User: {username}, password: {masked}")
61
+ except Exception as e:
62
+ return CheckResult(name=self.name, status="warn", message=str(e))
63
+
64
+
65
+ app = typer.Typer(help="Run diagnostic checks.", no_args_is_help=False, invoke_without_command=True)
66
+
67
+
68
+ @app.callback(invoke_without_command=True)
69
+ def doctor(ctx: typer.Context) -> None:
70
+ """Run all diagnostic checks."""
71
+ if ctx.invoked_subcommand is not None:
72
+ return
73
+ actx: AppContext = ctx.obj
74
+ out = actx.output
75
+
76
+ checks: list[DoctorCheck] = [
77
+ APIConnectivityCheck(),
78
+ AuthCheck(),
79
+ ]
80
+
81
+ all_passed = run_doctor(checks, out) # type: ignore[arg-type]
82
+ if not all_passed:
83
+ raise typer.Exit(code=1)