xcorecli 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
xcli/__init__.py ADDED
File without changes
xcli/_run.py ADDED
@@ -0,0 +1,14 @@
1
+ """Shared utilities for calling xcore handlers."""
2
+ import asyncio
3
+ from argparse import Namespace
4
+ from typing import Any, Callable, Coroutine
5
+
6
+
7
+ def ns(**kwargs) -> Namespace:
8
+ """Build an argparse-compatible Namespace with config=None by default."""
9
+ kwargs.setdefault("config", None)
10
+ return Namespace(**kwargs)
11
+
12
+
13
+ def run(coro: Coroutine[Any, Any, Any]) -> None:
14
+ asyncio.run(coro)
xcli/_xcore.py ADDED
@@ -0,0 +1,56 @@
1
+ """
2
+ Shared helpers to boot xcore in standalone mode (no FastAPI/HTTP).
3
+ Used by CLI commands that need direct access to the plugin system.
4
+ """
5
+ from __future__ import annotations
6
+
7
+ import sys
8
+ from pathlib import Path
9
+ from typing import TYPE_CHECKING
10
+
11
+ import yaml
12
+ from rich.console import Console
13
+
14
+ if TYPE_CHECKING:
15
+ from xcore import Xcore
16
+
17
+ console = Console()
18
+
19
+ _CONFIG_CANDIDATES = [
20
+ "integration.yaml", "integration.json",
21
+ "config/integration.yaml", "config/integration.json",
22
+ ]
23
+
24
+
25
+ def _require_config() -> Path:
26
+ for c in _CONFIG_CANDIDATES:
27
+ if Path(c).exists():
28
+ return Path(c)
29
+ console.print("[yellow]⚠[/yellow] No integration.yaml — run [cyan]xcli init[/cyan] first.")
30
+ sys.exit(1)
31
+
32
+
33
+ def load_raw_config() -> dict:
34
+ path = _require_config()
35
+ return yaml.safe_load(path.read_text(encoding="utf-8")) or {}
36
+
37
+
38
+ async def boot() -> "Xcore":
39
+ """Boot xcore in standalone mode (no FastAPI). Reads integration.yaml automatically."""
40
+ from xcore import Xcore
41
+ _require_config()
42
+ xcore = Xcore()
43
+ await xcore.boot()
44
+ return xcore
45
+
46
+
47
+ class _NullEvents:
48
+ """Minimal event bus for sandbox subprocess without a full xcore boot."""
49
+ def emit_sync(self, *a, **kw): pass
50
+ async def emit(self, *a, **kw): pass
51
+ def subscribe(self, *a, **kw): pass
52
+
53
+
54
+ class _NullCtx:
55
+ """Minimal context for SandboxProcessManager without booting xcore."""
56
+ _events = _NullEvents()
xcli/init/__init__.py ADDED
File without changes
xcli/init/cli.py ADDED
@@ -0,0 +1,11 @@
1
+ from typer import Typer
2
+
3
+ from .manager import _app
4
+
5
+ app = Typer()
6
+
7
+
8
+ @app.command()
9
+ def init() -> None:
10
+ """Initialize a new xcore project — generates integration.yaml."""
11
+ _app()
xcli/init/manager.py ADDED
@@ -0,0 +1,141 @@
1
+ from pathlib import Path
2
+
3
+ from rich.console import Console
4
+ from rich.prompt import Prompt
5
+
6
+ console = Console()
7
+
8
+ # ── YAML template (comments preserved) ───────────────────────
9
+
10
+ _TEMPLATE = """\
11
+ app:
12
+ name: {name}
13
+ env: {env}
14
+ debug: {debug}
15
+ secret_key: "{secret_key}"
16
+ plugin_prefix: "/app"
17
+ plugin_tags: []
18
+ server_key: "{server_key}"
19
+ server_key_iterations: 100000
20
+
21
+ fastapi:
22
+ title: "{name}"
23
+ version: "0.1.0"
24
+ docs_url: "/docs"
25
+ redoc_url: "/redoc"
26
+ openapi_url: "/openapi.json"
27
+ redirect_slashes: true
28
+
29
+ server:
30
+ host: "0.0.0.0"
31
+ port: 8000
32
+ workers: 1
33
+ reload: {reload}
34
+ log_level: "{log_level}"
35
+ proxy_headers: true
36
+ forwarded_allow_ips: "*"
37
+
38
+ plugins:
39
+ directory: {plugins_dir}
40
+ secret_key: "{server_key}"
41
+ strict_trusted: false
42
+ interval: 10
43
+ entry_point: src/main.py
44
+
45
+ services:
46
+ databases:
47
+ default:
48
+ type: {db_type}
49
+ url: {db_url}
50
+ echo: false
51
+
52
+ cache:
53
+ backend: memory
54
+ ttl: 300
55
+ max_size: 1000
56
+
57
+ scheduler:
58
+ enabled: false
59
+ backend: memory
60
+ timezone: UTC
61
+
62
+ observability:
63
+ logging:
64
+ level: {log_level}
65
+ format: "%(asctime)s [%(levelname)s] [%(name)s] %(message)s"
66
+ file: log/app.log
67
+ max_bytes: 10485760
68
+ backup_count: 5
69
+ metrics:
70
+ enabled: false
71
+ backend: memory
72
+ prefix: {name}
73
+ tracing:
74
+ enabled: false
75
+ backend: noop
76
+ service_name: {name}
77
+ endpoint: null
78
+
79
+ security:
80
+ allowed_imports:
81
+ - fastapi
82
+ - json
83
+ - re
84
+ - math
85
+ - datetime
86
+ - typing
87
+ - dataclasses
88
+ - enum
89
+ - functools
90
+ - collections
91
+ - hashlib
92
+ - base64
93
+ - asyncio
94
+ - logging
95
+ - uuid
96
+ forbidden_imports: []
97
+ rate_limit_default:
98
+ calls: 100
99
+ period_seconds: 60
100
+ """
101
+
102
+ _DB_URL_DEFAULTS = {
103
+ "sqlite": "sqlite:///./xcore.db",
104
+ "sqlasync": "sqlite+aiosqlite:///./xcore.db",
105
+ "postgresql": "postgresql+asyncpg://user:pass@localhost:5432/dbname",
106
+ "mysql": "mysql+aiomysql://user:pass@localhost:3306/dbname",
107
+ "mongodb": "mongodb://localhost:27017",
108
+ "redis": "redis://localhost:6379/0",
109
+ }
110
+
111
+
112
+ def _app() -> None:
113
+ console.print("\n[bold]xcli init[/bold] [dim]— Enter to keep defaults[/dim]\n")
114
+
115
+ name = Prompt.ask("App name", default="xcore-app")
116
+ env = Prompt.ask("Environment", choices=["development", "production"], default="development")
117
+ secret_key = Prompt.ask("Secret key", default="change-me-in-production", password=True)
118
+ server_key = Prompt.ask("Server key", default="change-me-in-production", password=True)
119
+ db_type = Prompt.ask("Database type", choices=list(_DB_URL_DEFAULTS), default="sqlasync")
120
+ db_url = Prompt.ask("Database URL", default=_DB_URL_DEFAULTS[db_type])
121
+ plugins_dir = Prompt.ask("Plugins dir", default="./app")
122
+
123
+ is_dev = env == "development"
124
+ content = _TEMPLATE.format(
125
+ name=name,
126
+ env=env,
127
+ debug="true" if is_dev else "false",
128
+ secret_key=secret_key,
129
+ server_key=server_key,
130
+ reload="true" if is_dev else "false",
131
+ log_level="DEBUG" if is_dev else "INFO",
132
+ plugins_dir=plugins_dir,
133
+ db_type=db_type,
134
+ db_url=db_url,
135
+ )
136
+
137
+ filename = Prompt.ask("\nSave as", default="integration")
138
+ path = Path(filename.split(".")[0] + ".yaml")
139
+ path.write_text(content, encoding="utf-8")
140
+
141
+ console.print(f"\n[green]✓[/green] [cyan]{path}[/cyan] created.")
xcli/init/upgrade.py ADDED
@@ -0,0 +1,198 @@
1
+ """
2
+ upgrade.py — Migrate an existing integration.yaml to the latest schema.
3
+
4
+ Strategy: deep-merge defaults → existing values win, missing keys are added.
5
+ The original file is backed up as integration.yaml.bak before writing.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from pathlib import Path
11
+
12
+ import yaml
13
+ from rich.console import Console
14
+ from rich.markup import escape
15
+ from rich.table import Table
16
+
17
+ console = Console()
18
+
19
+ # Full default schema — every key that xcli/xcore understands
20
+ _DEFAULTS: dict = {
21
+ "app": {
22
+ "name": "xcore-app",
23
+ "env": "development",
24
+ "debug": False,
25
+ "secret_key": "change-me-in-production",
26
+ "plugin_prefix": "/app",
27
+ "plugin_tags": [],
28
+ "server_key": "change-me-in-production",
29
+ "server_key_iterations": 100000,
30
+ "fastapi": {
31
+ "title": "xcore-app",
32
+ "version": "0.1.0",
33
+ "docs_url": "/docs",
34
+ "redoc_url": "/redoc",
35
+ "openapi_url": "/openapi.json",
36
+ "redirect_slashes": True,
37
+ },
38
+ "server": {
39
+ "host": "0.0.0.0",
40
+ "port": 8000,
41
+ "workers": 1,
42
+ "reload": True,
43
+ "log_level": "DEBUG",
44
+ "proxy_headers": True,
45
+ "forwarded_allow_ips": "*",
46
+ },
47
+ },
48
+ "plugins": {
49
+ "directory": "./app",
50
+ "secret_key": "change-me-in-production",
51
+ "strict_trusted": False,
52
+ "interval": 10,
53
+ "entry_point": "src/main.py",
54
+ },
55
+ "services": {
56
+ "databases": {
57
+ "default": {
58
+ "type": "sqlasync",
59
+ "url": "sqlite+aiosqlite:///./xcore.db",
60
+ "echo": False,
61
+ }
62
+ },
63
+ "cache": {
64
+ "backend": "memory",
65
+ "ttl": 300,
66
+ "max_size": 1000,
67
+ },
68
+ "scheduler": {
69
+ "enabled": False,
70
+ "backend": "memory",
71
+ "timezone": "UTC",
72
+ },
73
+ },
74
+ "observability": {
75
+ "logging": {
76
+ "level": "DEBUG",
77
+ "format": "%(asctime)s [%(levelname)s] [%(name)s] %(message)s",
78
+ "file": "log/app.log",
79
+ "max_bytes": 10485760,
80
+ "backup_count": 5,
81
+ },
82
+ "metrics": {
83
+ "enabled": False,
84
+ "backend": "memory",
85
+ "prefix": "xcore",
86
+ },
87
+ "tracing": {
88
+ "enabled": False,
89
+ "backend": "noop",
90
+ "service_name": "xcore-app",
91
+ "endpoint": None,
92
+ },
93
+ },
94
+ "security": {
95
+ "allowed_imports": [
96
+ "fastapi", "json", "re", "math", "datetime", "typing",
97
+ "dataclasses", "enum", "functools", "collections",
98
+ "hashlib", "base64", "asyncio", "logging", "uuid",
99
+ ],
100
+ "forbidden_imports": [],
101
+ "rate_limit_default": {
102
+ "calls": 100,
103
+ "period_seconds": 60,
104
+ },
105
+ },
106
+ "marketplace": {
107
+ "url": "https://marketplace.xcore.dev",
108
+ "api_key": "",
109
+ "timeout": 10,
110
+ "cache_ttl": 300,
111
+ },
112
+ }
113
+
114
+
115
+ def _deep_merge(defaults: dict, existing: dict) -> tuple[dict, list[str]]:
116
+ """
117
+ Returns (merged, added_keys) where:
118
+ - existing values always win
119
+ - missing keys from defaults are injected
120
+ - added_keys lists the dotted paths of every key that was added
121
+ """
122
+ result = dict(existing)
123
+ added: list[str] = []
124
+
125
+ def _merge(base: dict, src: dict, target: dict, path: str) -> None:
126
+ for key, default_val in base.items():
127
+ full = f"{path}.{key}" if path else key
128
+ if key not in src:
129
+ target[key] = default_val
130
+ added.append(full)
131
+ elif isinstance(default_val, dict) and isinstance(src[key], dict):
132
+ target[key] = dict(src[key])
133
+ _merge(default_val, src[key], target[key], full)
134
+ # else: existing value wins, already in target
135
+
136
+ _merge(defaults, existing, result, "")
137
+ return result, added
138
+
139
+
140
+ _CONFIG_CANDIDATES = [
141
+ "integration.yaml", "integration.json",
142
+ "config/integration.yaml", "config/integration.json",
143
+ ]
144
+
145
+
146
+ def run_upgrade() -> None:
147
+ # Find config file
148
+ config_path: Path | None = None
149
+ for c in _CONFIG_CANDIDATES:
150
+ p = Path(c)
151
+ if p.exists():
152
+ config_path = p
153
+ break
154
+
155
+ if config_path is None:
156
+ console.print("[yellow]⚠[/yellow] No integration.yaml found — run [cyan]xcli init[/cyan] first.")
157
+ return
158
+
159
+ console.print(f"\n[bold]xcli upgrade[/bold] [dim]{config_path}[/dim]\n")
160
+
161
+ existing = yaml.safe_load(config_path.read_text(encoding="utf-8")) or {}
162
+ merged, added = _deep_merge(_DEFAULTS, existing)
163
+
164
+ if not added:
165
+ console.print("[green]✓[/green] Already up to date — no missing keys found.")
166
+ return
167
+
168
+ # Show what will be added
169
+ table = Table(title=f"{len(added)} key(s) will be added")
170
+ table.add_column("Key", style="cyan")
171
+ table.add_column("Default value", style="dim")
172
+
173
+ def _get_nested(d: dict, dotted: str):
174
+ parts = dotted.split(".")
175
+ for p in parts:
176
+ if isinstance(d, dict):
177
+ d = d.get(p)
178
+ else:
179
+ return None
180
+ return d
181
+
182
+ for key in added:
183
+ val = _get_nested(_DEFAULTS, key)
184
+ table.add_row(key, escape(str(val)))
185
+
186
+ console.print(table)
187
+
188
+ # Backup
189
+ bak = config_path.with_suffix(".yaml.bak")
190
+ bak.write_text(config_path.read_text(encoding="utf-8"), encoding="utf-8")
191
+ console.print(f"\n[dim]Backup → {bak}[/dim]")
192
+
193
+ # Write merged config
194
+ config_path.write_text(
195
+ yaml.dump(merged, default_flow_style=False, allow_unicode=True, sort_keys=False),
196
+ encoding="utf-8",
197
+ )
198
+ console.print(f"[green]✓[/green] [cyan]{config_path}[/cyan] upgraded — {len(added)} key(s) added.")
xcli/main.py ADDED
@@ -0,0 +1,64 @@
1
+ from typer import Typer
2
+
3
+ from xcli._run import ns, run
4
+ from xcli.init.manager import _app as _init_wizard
5
+ from xcli.init.upgrade import run_upgrade as _run_upgrade
6
+ from xcli.manager.cli import app as manager_app
7
+ from xcli.migrations.cli import app as migrations_app
8
+ from xcli.plugin.cli import app as plugin_app
9
+ from xcli.sandbox.cli import app as sandbox_app
10
+ from xcli.worker.cli import app as worker_app
11
+
12
+ _CTX = {"help_option_names": ["-h", "--help"]}
13
+
14
+ app = Typer(help="xcli — xcore project manager.", context_settings=_CTX)
15
+
16
+
17
+ # ── init ──────────────────────────────────────────────────────
18
+
19
+ @app.command()
20
+ def init() -> None:
21
+ """Generate integration.yaml for a new xcore project."""
22
+ _init_wizard()
23
+
24
+
25
+ # ── upgrade ───────────────────────────────────────────────────
26
+
27
+ @app.command()
28
+ def upgrade() -> None:
29
+ """Migrate integration.yaml to the latest schema (adds missing keys, keeps existing values)."""
30
+ _run_upgrade()
31
+
32
+
33
+ # ── health ────────────────────────────────────────────────────
34
+
35
+ @app.command()
36
+ def health() -> None:
37
+ """Global health-check of all xcore services."""
38
+ from xcore.cli.plugin_cmd import handle_health
39
+ run(handle_health(ns(json=False)))
40
+
41
+
42
+ # ── services ──────────────────────────────────────────────────
43
+
44
+ @app.command()
45
+ def services() -> None:
46
+ """Show status of all xcore services."""
47
+ from xcore.cli.plugin_cmd import handle_services
48
+ run(handle_services(ns(subcommand="status", json=False)))
49
+
50
+
51
+ # ── sub-apps ──────────────────────────────────────────────────
52
+
53
+ app.add_typer(plugin_app, name="plugin")
54
+ app.add_typer(sandbox_app, name="sandbox")
55
+ app.add_typer(worker_app, name="worker")
56
+ app.add_typer(manager_app, name="manager")
57
+ app.add_typer(migrations_app, name="migration")
58
+
59
+ # ── short aliases (hidden) ────────────────────────────────────
60
+ app.add_typer(plugin_app, name="p", hidden=True)
61
+ app.add_typer(sandbox_app, name="sb", hidden=True)
62
+ app.add_typer(worker_app, name="w", hidden=True)
63
+ app.add_typer(manager_app, name="m", hidden=True)
64
+ app.add_typer(migrations_app, name="mig", hidden=True)
File without changes