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 +0 -0
- xcli/_run.py +14 -0
- xcli/_xcore.py +56 -0
- xcli/init/__init__.py +0 -0
- xcli/init/cli.py +11 -0
- xcli/init/manager.py +141 -0
- xcli/init/upgrade.py +198 -0
- xcli/main.py +64 -0
- xcli/manager/__init__.py +0 -0
- xcli/manager/cli.py +537 -0
- xcli/marketplace/__init__.py +0 -0
- xcli/marketplace/cli.py +44 -0
- xcli/migrations/__init__.py +3 -0
- xcli/migrations/cli.py +292 -0
- xcli/migrations/runtime.py +198 -0
- xcli/plugin/__init__.py +0 -0
- xcli/plugin/cli.py +538 -0
- xcli/plugin/scaffold.py +213 -0
- xcli/sandbox/__init__.py +0 -0
- xcli/sandbox/cli.py +124 -0
- xcli/worker/__init__.py +0 -0
- xcli/worker/cli.py +569 -0
- xcli/worker/worker.py +565 -0
- xcorecli-1.0.0.dist-info/METADATA +11 -0
- xcorecli-1.0.0.dist-info/RECORD +27 -0
- xcorecli-1.0.0.dist-info/WHEEL +4 -0
- xcorecli-1.0.0.dist-info/entry_points.txt +3 -0
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
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)
|
xcli/manager/__init__.py
ADDED
|
File without changes
|