xcorecli 2.1.0__tar.gz → 2.2.0__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 (75) hide show
  1. {xcorecli-2.1.0 → xcorecli-2.2.0}/PKG-INFO +1 -1
  2. {xcorecli-2.1.0 → xcorecli-2.2.0}/pyproject.toml +1 -1
  3. {xcorecli-2.1.0 → xcorecli-2.2.0}/uv.lock +1 -1
  4. {xcorecli-2.1.0 → xcorecli-2.2.0}/xcli/plugin/install_commands.py +38 -2
  5. {xcorecli-2.1.0 → xcorecli-2.2.0}/xcli/plugin/marketplace_commands.py +41 -0
  6. xcorecli-2.2.0/xcli/plugin/shared.py +107 -0
  7. xcorecli-2.1.0/xcli/plugin/shared.py +0 -59
  8. {xcorecli-2.1.0 → xcorecli-2.2.0}/.github/workflows/publish.yml +0 -0
  9. {xcorecli-2.1.0 → xcorecli-2.2.0}/.gitignore +0 -0
  10. {xcorecli-2.1.0 → xcorecli-2.2.0}/.python-version +0 -0
  11. {xcorecli-2.1.0 → xcorecli-2.2.0}/.vscode/configurationCache.log +0 -0
  12. {xcorecli-2.1.0 → xcorecli-2.2.0}/.vscode/dryrun.log +0 -0
  13. {xcorecli-2.1.0 → xcorecli-2.2.0}/.vscode/settings.json +0 -0
  14. {xcorecli-2.1.0 → xcorecli-2.2.0}/.vscode/targets.log +0 -0
  15. {xcorecli-2.1.0 → xcorecli-2.2.0}/README.md +0 -0
  16. {xcorecli-2.1.0 → xcorecli-2.2.0}/docs/commands/health.md +0 -0
  17. {xcorecli-2.1.0 → xcorecli-2.2.0}/docs/commands/init.md +0 -0
  18. {xcorecli-2.1.0 → xcorecli-2.2.0}/docs/commands/login.md +0 -0
  19. {xcorecli-2.1.0 → xcorecli-2.2.0}/docs/config/index.md +0 -0
  20. {xcorecli-2.1.0 → xcorecli-2.2.0}/docs/getting-started/auth.md +0 -0
  21. {xcorecli-2.1.0 → xcorecli-2.2.0}/docs/getting-started/configuration.md +0 -0
  22. {xcorecli-2.1.0 → xcorecli-2.2.0}/docs/getting-started/install.md +0 -0
  23. {xcorecli-2.1.0 → xcorecli-2.2.0}/docs/index.md +0 -0
  24. {xcorecli-2.1.0 → xcorecli-2.2.0}/docs/manager/index.md +0 -0
  25. {xcorecli-2.1.0 → xcorecli-2.2.0}/docs/manager/monitoring.md +0 -0
  26. {xcorecli-2.1.0 → xcorecli-2.2.0}/docs/manager/services.md +0 -0
  27. {xcorecli-2.1.0 → xcorecli-2.2.0}/docs/migration/index.md +0 -0
  28. {xcorecli-2.1.0 → xcorecli-2.2.0}/docs/plugin/index.md +0 -0
  29. {xcorecli-2.1.0 → xcorecli-2.2.0}/docs/plugin/install.md +0 -0
  30. {xcorecli-2.1.0 → xcorecli-2.2.0}/docs/plugin/local.md +0 -0
  31. {xcorecli-2.1.0 → xcorecli-2.2.0}/docs/plugin/marketplace.md +0 -0
  32. {xcorecli-2.1.0 → xcorecli-2.2.0}/docs/plugin/runtime.md +0 -0
  33. {xcorecli-2.1.0 → xcorecli-2.2.0}/docs/plugin/security.md +0 -0
  34. {xcorecli-2.1.0 → xcorecli-2.2.0}/docs/plugin/update.md +0 -0
  35. {xcorecli-2.1.0 → xcorecli-2.2.0}/docs/reference.md +0 -0
  36. {xcorecli-2.1.0 → xcorecli-2.2.0}/docs/sandbox/index.md +0 -0
  37. {xcorecli-2.1.0 → xcorecli-2.2.0}/docs/service/index.md +0 -0
  38. {xcorecli-2.1.0 → xcorecli-2.2.0}/docs/worker/index.md +0 -0
  39. {xcorecli-2.1.0 → xcorecli-2.2.0}/docs/worker/process.md +0 -0
  40. {xcorecli-2.1.0 → xcorecli-2.2.0}/makefile +0 -0
  41. {xcorecli-2.1.0 → xcorecli-2.2.0}/mkdocs.yml +0 -0
  42. {xcorecli-2.1.0 → xcorecli-2.2.0}/xcli/__init__.py +0 -0
  43. {xcorecli-2.1.0 → xcorecli-2.2.0}/xcli/_credentials.py +0 -0
  44. {xcorecli-2.1.0 → xcorecli-2.2.0}/xcli/_login.py +0 -0
  45. {xcorecli-2.1.0 → xcorecli-2.2.0}/xcli/_run.py +0 -0
  46. {xcorecli-2.1.0 → xcorecli-2.2.0}/xcli/_xcore.py +0 -0
  47. {xcorecli-2.1.0 → xcorecli-2.2.0}/xcli/config/__init__.py +0 -0
  48. {xcorecli-2.1.0 → xcorecli-2.2.0}/xcli/config/cli.py +0 -0
  49. {xcorecli-2.1.0 → xcorecli-2.2.0}/xcli/config/runtime.py +0 -0
  50. {xcorecli-2.1.0 → xcorecli-2.2.0}/xcli/init/__init__.py +0 -0
  51. {xcorecli-2.1.0 → xcorecli-2.2.0}/xcli/init/manager.py +0 -0
  52. {xcorecli-2.1.0 → xcorecli-2.2.0}/xcli/init/upgrade.py +0 -0
  53. {xcorecli-2.1.0 → xcorecli-2.2.0}/xcli/main.py +0 -0
  54. {xcorecli-2.1.0 → xcorecli-2.2.0}/xcli/manager/__init__.py +0 -0
  55. {xcorecli-2.1.0 → xcorecli-2.2.0}/xcli/manager/cli.py +0 -0
  56. {xcorecli-2.1.0 → xcorecli-2.2.0}/xcli/migrations/__init__.py +0 -0
  57. {xcorecli-2.1.0 → xcorecli-2.2.0}/xcli/migrations/cli.py +0 -0
  58. {xcorecli-2.1.0 → xcorecli-2.2.0}/xcli/migrations/runtime.py +0 -0
  59. {xcorecli-2.1.0 → xcorecli-2.2.0}/xcli/plugin/__init__.py +0 -0
  60. {xcorecli-2.1.0 → xcorecli-2.2.0}/xcli/plugin/cli.py +0 -0
  61. {xcorecli-2.1.0 → xcorecli-2.2.0}/xcli/plugin/local_commands.py +0 -0
  62. {xcorecli-2.1.0 → xcorecli-2.2.0}/xcli/plugin/runtime_commands.py +0 -0
  63. {xcorecli-2.1.0 → xcorecli-2.2.0}/xcli/plugin/scaffold.py +0 -0
  64. {xcorecli-2.1.0 → xcorecli-2.2.0}/xcli/plugin/security_commands.py +0 -0
  65. {xcorecli-2.1.0 → xcorecli-2.2.0}/xcli/plugin/update_commands.py +0 -0
  66. {xcorecli-2.1.0 → xcorecli-2.2.0}/xcli/sandbox/__init__.py +0 -0
  67. {xcorecli-2.1.0 → xcorecli-2.2.0}/xcli/sandbox/cli.py +0 -0
  68. {xcorecli-2.1.0 → xcorecli-2.2.0}/xcli/service/__init__.py +0 -0
  69. {xcorecli-2.1.0 → xcorecli-2.2.0}/xcli/service/cli.py +0 -0
  70. {xcorecli-2.1.0 → xcorecli-2.2.0}/xcli/service/install_commands.py +0 -0
  71. {xcorecli-2.1.0 → xcorecli-2.2.0}/xcli/service/marketplace_commands.py +0 -0
  72. {xcorecli-2.1.0 → xcorecli-2.2.0}/xcli/service/shared.py +0 -0
  73. {xcorecli-2.1.0 → xcorecli-2.2.0}/xcli/worker/__init__.py +0 -0
  74. {xcorecli-2.1.0 → xcorecli-2.2.0}/xcli/worker/cli.py +0 -0
  75. {xcorecli-2.1.0 → xcorecli-2.2.0}/xcli/worker/worker.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: xcorecli
3
- Version: 2.1.0
3
+ Version: 2.2.0
4
4
  Summary: CLI for xcore — configuration, plugins, monitoring
5
5
  Requires-Python: >=3.12
6
6
  Requires-Dist: alembic>=1.18.0
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "xcorecli"
7
- version = "2.1.0"
7
+ version = "2.2.0"
8
8
  description = "CLI for xcore — configuration, plugins, monitoring"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -782,7 +782,7 @@ wheels = [
782
782
 
783
783
  [[package]]
784
784
  name = "xcorecli"
785
- version = "2.0.0"
785
+ version = "2.2.0"
786
786
  source = { editable = "." }
787
787
  dependencies = [
788
788
  { name = "alembic" },
@@ -10,7 +10,7 @@ from typer import Typer
10
10
 
11
11
  from xcli._credentials import get_api_key, get_signing_key
12
12
 
13
- from .shared import console, marketplace_install_url, plugins_dir
13
+ from .shared import console, marketplace_install_url, parse_x_repo, plugins_dir, record_install
14
14
 
15
15
 
16
16
  def _verify_signature(zip_bytes: bytes, x_signature: str, signing_key: str) -> bool:
@@ -127,6 +127,20 @@ def _marketplace_install(name: str, version: str) -> None:
127
127
 
128
128
  console.print(f'[green]✓[/green] [cyan]{x_plugin}[/cyan] installed in [dim]{dest}[/dim]')
129
129
 
130
+ resolved_version = x_plugin.rsplit('@', 1)[-1] if '@' in x_plugin else version
131
+ repository, ref = parse_x_repo(resp.headers.get('X-Repo'))
132
+ record_install(
133
+ name,
134
+ {
135
+ 'source': 'marketplace',
136
+ 'slug': name,
137
+ 'kind': 'plugin',
138
+ 'version': resolved_version,
139
+ 'repository': repository,
140
+ 'ref': ref,
141
+ },
142
+ )
143
+
130
144
 
131
145
  def _install_from_url(name: str, url: str, source_type: str) -> None:
132
146
  import io
@@ -139,7 +153,9 @@ def _install_from_url(name: str, url: str, source_type: str) -> None:
139
153
  shutil.rmtree(dest)
140
154
  dest.mkdir(parents=True, exist_ok=True)
141
155
 
142
- if source_type == 'git' or url.endswith('.git'):
156
+ resolved_ref: Optional[str] = None
157
+ used_git = source_type == 'git' or url.endswith('.git')
158
+ if used_git:
143
159
  import subprocess
144
160
 
145
161
  console.print(f' Cloning [dim]{url}[/dim]...')
@@ -147,6 +163,14 @@ def _install_from_url(name: str, url: str, source_type: str) -> None:
147
163
  if result.returncode != 0:
148
164
  console.print(f'[red]git clone failed:[/red] {result.stderr.decode().strip()}')
149
165
  raise typer.Exit(1)
166
+ # --depth=1 leaves HEAD at whatever the clone resolved (default
167
+ # branch tip, since no ref was requested) — pin the registry entry
168
+ # to that exact commit rather than a mutable branch name, so a
169
+ # later `xcore-agent build` re-resolving from it gets the same code.
170
+ head = subprocess.run(
171
+ ['git', '-C', str(dest), 'rev-parse', 'HEAD'], capture_output=True, text=True
172
+ )
173
+ resolved_ref = head.stdout.strip() if head.returncode == 0 else None
150
174
  else:
151
175
  import urllib.request
152
176
 
@@ -176,6 +200,18 @@ def _install_from_url(name: str, url: str, source_type: str) -> None:
176
200
 
177
201
  console.print(f'[green]✓[/green] [cyan]{name}[/cyan] installed in [dim]{dest}[/dim]')
178
202
 
203
+ record_install(
204
+ name,
205
+ {
206
+ 'source': 'git' if used_git else 'zip',
207
+ 'slug': name,
208
+ 'kind': 'plugin',
209
+ 'version': None,
210
+ 'repository': url if used_git else None,
211
+ 'ref': resolved_ref,
212
+ },
213
+ )
214
+
179
215
 
180
216
  # Module-level (not a closure inside register()) so the exact same function
181
217
  # object can be registered a second time as the top-level `xcli install`
@@ -6,6 +6,8 @@ import httpx
6
6
  import typer
7
7
  from typer import Typer
8
8
 
9
+ from xcli._credentials import get_api_key
10
+
9
11
  from .shared import console, marketplace_api_base
10
12
 
11
13
  # Contrat réel du backend (app/marketplace/src/routes/plugins.py) — pas le
@@ -90,6 +92,45 @@ def register(app: Typer) -> None:
90
92
  return
91
93
  _table(items, f'Search results — {query}')
92
94
 
95
+ @app.command('mine')
96
+ def mine() -> None:
97
+ """List YOUR plugins on the marketplace — public and private alike.
98
+
99
+ `browse`/`search` above only ever see public plugins: they call
100
+ GET /plugins unauthenticated, and the server only widens that to a
101
+ caller's own private plugins for a JWT session — which `xcli` never
102
+ holds (even after `xcli login`, which saves an API key + signing
103
+ key via a device-code flow, never a browser session token). This
104
+ command instead calls GET /plugins/mine with X-API-Key — the same
105
+ auth `xcli plugin install` already uses successfully to fetch a
106
+ private plugin you own, now also available for listing them.
107
+ """
108
+ api_key = get_api_key()
109
+ if not api_key:
110
+ console.print('[red]API key missing.[/red] Run: [cyan]xcli login[/cyan] or [cyan]xcli config set api-key xdk_...[/cyan]')
111
+ raise typer.Exit(1)
112
+
113
+ url = f'{marketplace_api_base()}{_LIST_PATH}/mine'
114
+ with console.status('Fetching your plugins...'):
115
+ try:
116
+ resp = httpx.get(url, headers={'X-API-Key': api_key}, timeout=15, follow_redirects=True)
117
+ resp.raise_for_status()
118
+ except httpx.HTTPStatusError as exc:
119
+ if exc.response.status_code == 401:
120
+ console.print('[red]Invalid or revoked API key.[/red] Run: [cyan]xcli login[/cyan]')
121
+ else:
122
+ console.print(f'[red]HTTP {exc.response.status_code}:[/red] {exc.response.text[:200]}')
123
+ raise typer.Exit(1)
124
+ except httpx.RequestError as exc:
125
+ console.print(f'[red]Network error:[/red] {exc}')
126
+ raise typer.Exit(1)
127
+
128
+ items = resp.json() or []
129
+ if not items:
130
+ console.print('[yellow]No plugins yet.[/yellow] Publish one: [dim]xcli plugin marketplace info <slug>[/dim]')
131
+ return
132
+ _table(items, f'Your Plugins ({len(items)})')
133
+
93
134
  @app.command('info')
94
135
  def info(slug: str) -> None:
95
136
  """Show full details for a marketplace plugin before installing."""
@@ -0,0 +1,107 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import re
5
+ from datetime import datetime, timezone
6
+ from pathlib import Path
7
+ from typing import Any, Optional
8
+
9
+ from rich.console import Console
10
+ from rich.tree import Tree
11
+
12
+ from xcli.config.runtime import load_raw_config, plugins_directory
13
+
14
+ console = Console()
15
+ name_re = re.compile(r'^[a-z][a-z0-9_]*$')
16
+
17
+ _DEFAULT_MARKETPLACE_API = 'https://marketplace.xcorehub.dev'
18
+
19
+
20
+ def marketplace_api_base() -> str:
21
+ """Return the marketplace API base URL from integration.yaml, with fallback.
22
+
23
+ `integration.yaml`'s `marketplace.url` is conventionally a bare domain
24
+ (`marketplace.xcorehub.dev`, no scheme — see the backend's own
25
+ integration.yaml) since it's also used to build the CORS allow-list.
26
+ Used directly as an HTTP base URL that would produce a scheme-less,
27
+ unusable URL for httpx — always ensure a scheme here rather than assume
28
+ every caller of this config value remembers to add one.
29
+ """
30
+ try:
31
+ cfg = load_raw_config(required=False)
32
+ marketplace = cfg.get('marketplace', {})
33
+ url = str(marketplace.get('api_url') or marketplace.get('url') or _DEFAULT_MARKETPLACE_API).rstrip('/')
34
+ except Exception:
35
+ url = _DEFAULT_MARKETPLACE_API
36
+ if not url.startswith(('http://', 'https://')):
37
+ url = f'https://{url}'
38
+ return url
39
+
40
+
41
+ def marketplace_install_url(name: str, version: str) -> str:
42
+ return f'{marketplace_api_base()}/app/marketplace/plugins/{name}/install?version={version}'
43
+
44
+
45
+ def plugins_dir() -> Path:
46
+ return plugins_directory()
47
+
48
+
49
+ def registry_path() -> Path:
50
+ """`.xcore-registry.json` — a sibling of every installed plugin
51
+ directory, one entry per plugin name, recording how it got there
52
+ (marketplace/git/zip + repository + pinned ref when known). Read by
53
+ xcore-agent's packer at build time (see packer.builder._read_registry_
54
+ source) as a fallback for a plugin with no explicit `source:` of its
55
+ own in plugin.yaml — this is what lets `xcore-agent build` resolve a
56
+ plugin from its real origin at deploy time automatically, instead of
57
+ an operator hand-writing `source:` after every `xcli plugin install`."""
58
+ return plugins_dir() / ".xcore-registry.json"
59
+
60
+
61
+ def record_install(name: str, entry: dict[str, Any]) -> None:
62
+ """Add/replace this plugin's entry in the registry. Best-effort: a
63
+ write failure here must never fail the install itself (the plugin is
64
+ already on disk and usable — the registry only makes a later `xcore-
65
+ agent build` smarter, it's not required for the plugin to work)."""
66
+ path = registry_path()
67
+ try:
68
+ registry = json.loads(path.read_text()) if path.is_file() else {}
69
+ if not isinstance(registry, dict):
70
+ registry = {}
71
+ except (OSError, ValueError):
72
+ registry = {}
73
+ registry[name] = {**entry, "installed_at": datetime.now(timezone.utc).isoformat()}
74
+ try:
75
+ path.parent.mkdir(parents=True, exist_ok=True)
76
+ path.write_text(json.dumps(registry, indent=2, sort_keys=True) + "\n")
77
+ except OSError as exc:
78
+ console.print(f"[dim yellow]Note: couldn't update {path.name}: {exc}[/dim yellow]")
79
+
80
+
81
+ def parse_x_repo(header_value: Optional[str]) -> tuple[Optional[str], Optional[str]]:
82
+ """Parse the marketplace install endpoint's `X-Repo: owner/repo@ref`
83
+ response header into (repository_url, ref). Returns (None, None) if the
84
+ header is absent or malformed — callers must treat that as 'unknown
85
+ origin', never guess."""
86
+ if not header_value or "@" not in header_value:
87
+ return None, None
88
+ repo_part, _, ref_part = header_value.rpartition("@")
89
+ if not repo_part or not ref_part:
90
+ return None, None
91
+ return f"https://github.com/{repo_part}", ref_part
92
+
93
+
94
+ def print_tree(root: Path, created: list[Path]) -> None:
95
+ tree = Tree(f'[bold green]{root.name}/[/bold green]')
96
+ dirs: dict[str, Tree] = {}
97
+ for path in sorted(created):
98
+ rel = path.relative_to(root)
99
+ parts = rel.parts
100
+ current = tree
101
+ for i, part in enumerate(parts[:-1]):
102
+ key = '/'.join(parts[: i + 1])
103
+ if key not in dirs:
104
+ dirs[key] = current.add(f'[cyan]{part}/[/cyan]')
105
+ current = dirs[key]
106
+ current.add(f'[white]{parts[-1]}[/white]')
107
+ console.print(tree)
@@ -1,59 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import re
4
- from pathlib import Path
5
-
6
- from rich.console import Console
7
- from rich.tree import Tree
8
-
9
- from xcli.config.runtime import load_raw_config, plugins_directory
10
-
11
- console = Console()
12
- name_re = re.compile(r'^[a-z][a-z0-9_]*$')
13
-
14
- _DEFAULT_MARKETPLACE_API = 'https://marketplace.xcorehub.dev'
15
-
16
-
17
- def marketplace_api_base() -> str:
18
- """Return the marketplace API base URL from integration.yaml, with fallback.
19
-
20
- `integration.yaml`'s `marketplace.url` is conventionally a bare domain
21
- (`marketplace.xcorehub.dev`, no scheme — see the backend's own
22
- integration.yaml) since it's also used to build the CORS allow-list.
23
- Used directly as an HTTP base URL that would produce a scheme-less,
24
- unusable URL for httpx — always ensure a scheme here rather than assume
25
- every caller of this config value remembers to add one.
26
- """
27
- try:
28
- cfg = load_raw_config(required=False)
29
- marketplace = cfg.get('marketplace', {})
30
- url = str(marketplace.get('api_url') or marketplace.get('url') or _DEFAULT_MARKETPLACE_API).rstrip('/')
31
- except Exception:
32
- url = _DEFAULT_MARKETPLACE_API
33
- if not url.startswith(('http://', 'https://')):
34
- url = f'https://{url}'
35
- return url
36
-
37
-
38
- def marketplace_install_url(name: str, version: str) -> str:
39
- return f'{marketplace_api_base()}/app/marketplace/plugins/{name}/install?version={version}'
40
-
41
-
42
- def plugins_dir() -> Path:
43
- return plugins_directory()
44
-
45
-
46
- def print_tree(root: Path, created: list[Path]) -> None:
47
- tree = Tree(f'[bold green]{root.name}/[/bold green]')
48
- dirs: dict[str, Tree] = {}
49
- for path in sorted(created):
50
- rel = path.relative_to(root)
51
- parts = rel.parts
52
- current = tree
53
- for i, part in enumerate(parts[:-1]):
54
- key = '/'.join(parts[: i + 1])
55
- if key not in dirs:
56
- dirs[key] = current.add(f'[cyan]{part}/[/cyan]')
57
- current = dirs[key]
58
- current.add(f'[white]{parts[-1]}[/white]')
59
- console.print(tree)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes