getcodexy 1.2.2__tar.gz → 1.4.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.
- {getcodexy-1.2.2 → getcodexy-1.4.0}/PKG-INFO +1 -1
- getcodexy-1.4.0/build_backend.py +51 -0
- {getcodexy-1.2.2 → getcodexy-1.4.0}/pyproject.toml +7 -2
- getcodexy-1.4.0/src/codexy_runtime_tools/_version_data/uv.lock +8 -0
- getcodexy-1.4.0/src/codexy_runtime_tools/activation_transaction.py +189 -0
- {getcodexy-1.2.2 → getcodexy-1.4.0}/src/codexy_runtime_tools/cache.py +21 -6
- getcodexy-1.4.0/src/codexy_runtime_tools/component-manifest.json +106 -0
- getcodexy-1.4.0/src/codexy_runtime_tools/component_cli.py +151 -0
- getcodexy-1.4.0/src/codexy_runtime_tools/component_health.py +156 -0
- getcodexy-1.4.0/src/codexy_runtime_tools/component_inspection.py +209 -0
- getcodexy-1.4.0/src/codexy_runtime_tools/component_integrity.py +207 -0
- getcodexy-1.4.0/src/codexy_runtime_tools/component_inventory_classification.py +132 -0
- getcodexy-1.4.0/src/codexy_runtime_tools/component_inventory_records.py +82 -0
- getcodexy-1.4.0/src/codexy_runtime_tools/component_lifecycle.py +11 -0
- getcodexy-1.4.0/src/codexy_runtime_tools/component_lifecycle_admission.py +120 -0
- getcodexy-1.4.0/src/codexy_runtime_tools/component_lifecycle_interlock.py +38 -0
- getcodexy-1.4.0/src/codexy_runtime_tools/component_lifecycle_operation.py +246 -0
- getcodexy-1.4.0/src/codexy_runtime_tools/component_lifecycle_preflight.py +49 -0
- getcodexy-1.4.0/src/codexy_runtime_tools/component_lifecycle_recovery.py +234 -0
- getcodexy-1.4.0/src/codexy_runtime_tools/component_lifecycle_support.py +33 -0
- getcodexy-1.4.0/src/codexy_runtime_tools/component_lifecycle_terminal.py +44 -0
- getcodexy-1.4.0/src/codexy_runtime_tools/component_manifest.py +247 -0
- getcodexy-1.4.0/src/codexy_runtime_tools/component_manifest_dependencies.py +44 -0
- getcodexy-1.4.0/src/codexy_runtime_tools/component_manifest_values.py +24 -0
- getcodexy-1.4.0/src/codexy_runtime_tools/component_observed_inventory.py +99 -0
- getcodexy-1.4.0/src/codexy_runtime_tools/component_registration_health.py +232 -0
- getcodexy-1.4.0/src/codexy_runtime_tools/component_resolver.py +187 -0
- getcodexy-1.4.0/src/codexy_runtime_tools/component_transaction_durability.py +22 -0
- getcodexy-1.4.0/src/codexy_runtime_tools/component_transaction_identity.py +21 -0
- getcodexy-1.4.0/src/codexy_runtime_tools/component_transaction_receipts.py +50 -0
- getcodexy-1.4.0/src/codexy_runtime_tools/component_transaction_snapshot.py +21 -0
- getcodexy-1.4.0/src/codexy_runtime_tools/component_transaction_state.py +237 -0
- getcodexy-1.4.0/src/codexy_runtime_tools/component_transition_journal.py +206 -0
- getcodexy-1.4.0/src/codexy_runtime_tools/component_transition_model.py +107 -0
- getcodexy-1.4.0/src/codexy_runtime_tools/component_transition_receipt.py +207 -0
- getcodexy-1.4.0/src/codexy_runtime_tools/component_transition_rejections.py +159 -0
- getcodexy-1.4.0/src/codexy_runtime_tools/contract.py +248 -0
- getcodexy-1.4.0/src/codexy_runtime_tools/github_checks.py +170 -0
- getcodexy-1.4.0/src/codexy_runtime_tools/github_pre_session.py +226 -0
- getcodexy-1.4.0/src/codexy_runtime_tools/identity.py +97 -0
- getcodexy-1.4.0/src/codexy_runtime_tools/installer.py +161 -0
- getcodexy-1.4.0/src/codexy_runtime_tools/marketplace_repin.py +167 -0
- getcodexy-1.4.0/src/codexy_runtime_tools/monolith_baseline.py +88 -0
- getcodexy-1.4.0/src/codexy_runtime_tools/monolith_classifier.py +50 -0
- getcodexy-1.4.0/src/codexy_runtime_tools/monolith_migration.py +204 -0
- getcodexy-1.4.0/src/codexy_runtime_tools/monolith_migration_host.py +186 -0
- getcodexy-1.4.0/src/codexy_runtime_tools/monolith_migration_plan.py +58 -0
- getcodexy-1.4.0/src/codexy_runtime_tools/monolith_migration_state.py +183 -0
- {getcodexy-1.2.2 → getcodexy-1.4.0}/src/codexy_runtime_tools/package.py +80 -70
- getcodexy-1.4.0/src/codexy_runtime_tools/package_archive.py +86 -0
- getcodexy-1.4.0/src/codexy_runtime_tools/plugin_resolution.py +154 -0
- getcodexy-1.4.0/src/codexy_runtime_tools/pre_session.py +184 -0
- getcodexy-1.4.0/src/codexy_runtime_tools/runtime.py +223 -0
- getcodexy-1.4.0/src/codexy_runtime_tools/runtime_configuration.py +135 -0
- getcodexy-1.4.0/src/codexy_runtime_tools/source.py +182 -0
- getcodexy-1.4.0/src/codexy_runtime_tools/updater.py +179 -0
- getcodexy-1.4.0/src/codexy_runtime_tools/version_lock.py +58 -0
- getcodexy-1.4.0/uv.lock +8 -0
- getcodexy-1.2.2/src/codexy_runtime_tools/installer.py +0 -107
- getcodexy-1.2.2/src/codexy_runtime_tools/runtime.py +0 -169
- {getcodexy-1.2.2 → getcodexy-1.4.0}/src/codexy_runtime_tools/__init__.py +0 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Materialize getcodexy's canonical lockfile as uv_build package data."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import uv_build
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
_ROOT = Path(__file__).resolve().parent
|
|
12
|
+
_CANONICAL_LOCK = _ROOT / "uv.lock"
|
|
13
|
+
_PACKAGED_LOCK = _ROOT / "src/codexy_runtime_tools/_version_data/uv.lock"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _materialize_version_lock() -> None:
|
|
17
|
+
contents = _CANONICAL_LOCK.read_bytes()
|
|
18
|
+
if not contents:
|
|
19
|
+
raise ValueError("packages/getcodexy/uv.lock must not be empty")
|
|
20
|
+
_PACKAGED_LOCK.parent.mkdir(parents=True, exist_ok=True)
|
|
21
|
+
_PACKAGED_LOCK.write_bytes(contents)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def build_sdist(sdist_directory: str, config_settings: Any = None) -> str:
|
|
25
|
+
_materialize_version_lock()
|
|
26
|
+
return uv_build.build_sdist(sdist_directory, config_settings)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def build_wheel(
|
|
30
|
+
wheel_directory: str,
|
|
31
|
+
config_settings: Any = None,
|
|
32
|
+
metadata_directory: str | None = None,
|
|
33
|
+
) -> str:
|
|
34
|
+
_materialize_version_lock()
|
|
35
|
+
return uv_build.build_wheel(wheel_directory, config_settings, metadata_directory)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def build_editable(
|
|
39
|
+
wheel_directory: str,
|
|
40
|
+
config_settings: Any = None,
|
|
41
|
+
metadata_directory: str | None = None,
|
|
42
|
+
) -> str:
|
|
43
|
+
_materialize_version_lock()
|
|
44
|
+
return uv_build.build_editable(wheel_directory, config_settings, metadata_directory)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
get_requires_for_build_sdist = uv_build.get_requires_for_build_sdist
|
|
48
|
+
get_requires_for_build_wheel = uv_build.get_requires_for_build_wheel
|
|
49
|
+
get_requires_for_build_editable = uv_build.get_requires_for_build_editable
|
|
50
|
+
prepare_metadata_for_build_wheel = uv_build.prepare_metadata_for_build_wheel
|
|
51
|
+
prepare_metadata_for_build_editable = uv_build.prepare_metadata_for_build_editable
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
[build-system]
|
|
2
2
|
requires = ["uv_build==0.9.26"]
|
|
3
|
-
build-backend = "
|
|
3
|
+
build-backend = "build_backend"
|
|
4
|
+
backend-path = ["."]
|
|
4
5
|
|
|
5
6
|
[project]
|
|
6
7
|
name = "getcodexy"
|
|
7
|
-
version = "1.
|
|
8
|
+
version = "1.4.0"
|
|
8
9
|
description = "Version-pinned MCP runtime bootstrap tools for Codexy"
|
|
9
10
|
requires-python = ">=3.10"
|
|
10
11
|
license = "MIT"
|
|
@@ -14,8 +15,12 @@ authors = [{ name = "Eunsoo Lee" }]
|
|
|
14
15
|
Repository = "https://github.com/eunsoogi/codexy"
|
|
15
16
|
|
|
16
17
|
[project.scripts]
|
|
18
|
+
getcodexy = "codexy_runtime_tools.component_cli:main"
|
|
17
19
|
codexy-mcp-runtime = "codexy_runtime_tools.runtime:main"
|
|
20
|
+
codexy-github-install = "codexy_runtime_tools.github_pre_session:main"
|
|
21
|
+
codexy-github-check = "codexy_runtime_tools.github_checks:main"
|
|
18
22
|
|
|
19
23
|
[tool.uv.build-backend]
|
|
20
24
|
module-name = "codexy_runtime_tools"
|
|
21
25
|
module-root = "src"
|
|
26
|
+
source-include = ["build_backend.py", "uv.lock"]
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""Narrow, no-follow rollback snapshot for public component activation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import stat
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True)
|
|
12
|
+
class Entry:
|
|
13
|
+
relative: Path
|
|
14
|
+
data: bytes | None
|
|
15
|
+
mode: int
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class ActivationSnapshot:
|
|
20
|
+
home: Path
|
|
21
|
+
entries: tuple[Entry, ...]
|
|
22
|
+
|
|
23
|
+
@classmethod
|
|
24
|
+
def capture(cls, home: Path) -> "ActivationSnapshot":
|
|
25
|
+
if not home.exists():
|
|
26
|
+
return cls(home, ())
|
|
27
|
+
files: list[Entry] = []
|
|
28
|
+
for root in (
|
|
29
|
+
Path("config.toml"),
|
|
30
|
+
Path("agents/codexy"),
|
|
31
|
+
Path("agents/codexy-github"),
|
|
32
|
+
):
|
|
33
|
+
files.extend(_capture(home, root))
|
|
34
|
+
for backup in home.glob("config.toml.codexy-backup-*"):
|
|
35
|
+
files.extend(_capture(home, backup.relative_to(home)))
|
|
36
|
+
return cls(home, tuple(files))
|
|
37
|
+
|
|
38
|
+
def restore(self) -> None:
|
|
39
|
+
for root in (
|
|
40
|
+
Path("agents/codexy-github"),
|
|
41
|
+
Path("agents/codexy"),
|
|
42
|
+
Path("config.toml"),
|
|
43
|
+
):
|
|
44
|
+
_remove_tree(self.home / root)
|
|
45
|
+
if self.home.exists():
|
|
46
|
+
for backup in self.home.glob("config.toml.codexy-backup-*"):
|
|
47
|
+
_remove_tree(backup)
|
|
48
|
+
if self.entries:
|
|
49
|
+
self.home.mkdir(parents=True, exist_ok=True)
|
|
50
|
+
for entry in sorted(self.entries, key=lambda item: len(item.relative.parts)):
|
|
51
|
+
path = self.home / entry.relative
|
|
52
|
+
if entry.data is None:
|
|
53
|
+
path.mkdir(mode=entry.mode, exist_ok=True)
|
|
54
|
+
else:
|
|
55
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
56
|
+
descriptor = os.open(
|
|
57
|
+
path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, entry.mode
|
|
58
|
+
)
|
|
59
|
+
with os.fdopen(descriptor, "wb") as output:
|
|
60
|
+
output.write(entry.data)
|
|
61
|
+
_remove_if_empty(self.home / "agents")
|
|
62
|
+
_remove_if_empty(self.home)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _capture(home: Path, relative: Path) -> list[Entry]:
|
|
66
|
+
path = home / relative
|
|
67
|
+
if not os.path.lexists(path):
|
|
68
|
+
return []
|
|
69
|
+
metadata = path.lstat()
|
|
70
|
+
_require_safe(metadata, path)
|
|
71
|
+
if stat.S_ISREG(metadata.st_mode):
|
|
72
|
+
return [
|
|
73
|
+
Entry(relative, _read_file(path, metadata), stat.S_IMODE(metadata.st_mode))
|
|
74
|
+
]
|
|
75
|
+
descriptor = _open_directory(path, metadata)
|
|
76
|
+
try:
|
|
77
|
+
return _capture_directory(relative, descriptor)
|
|
78
|
+
finally:
|
|
79
|
+
os.close(descriptor)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _capture_directory(relative: Path, descriptor: int) -> list[Entry]:
|
|
83
|
+
metadata = os.fstat(descriptor)
|
|
84
|
+
_require_safe(metadata, relative)
|
|
85
|
+
entries = [Entry(relative, None, stat.S_IMODE(metadata.st_mode))]
|
|
86
|
+
for name in sorted(os.listdir(descriptor)):
|
|
87
|
+
child = os.stat(name, dir_fd=descriptor, follow_symlinks=False)
|
|
88
|
+
child_relative = relative / name
|
|
89
|
+
_require_safe(child, child_relative)
|
|
90
|
+
if stat.S_ISREG(child.st_mode):
|
|
91
|
+
entries.append(
|
|
92
|
+
Entry(
|
|
93
|
+
child_relative,
|
|
94
|
+
_read_child(descriptor, name, child),
|
|
95
|
+
stat.S_IMODE(child.st_mode),
|
|
96
|
+
)
|
|
97
|
+
)
|
|
98
|
+
continue
|
|
99
|
+
child_descriptor = os.open(
|
|
100
|
+
name,
|
|
101
|
+
os.O_RDONLY | os.O_DIRECTORY | getattr(os, "O_NOFOLLOW", 0),
|
|
102
|
+
dir_fd=descriptor,
|
|
103
|
+
)
|
|
104
|
+
try:
|
|
105
|
+
opened = os.fstat(child_descriptor)
|
|
106
|
+
if (opened.st_dev, opened.st_ino) != (child.st_dev, child.st_ino):
|
|
107
|
+
raise ValueError(
|
|
108
|
+
f"activation state changed while reading: {child_relative}"
|
|
109
|
+
)
|
|
110
|
+
entries.extend(_capture_directory(child_relative, child_descriptor))
|
|
111
|
+
finally:
|
|
112
|
+
os.close(child_descriptor)
|
|
113
|
+
return entries
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _remove_tree(path: Path) -> None:
|
|
117
|
+
if not os.path.lexists(path):
|
|
118
|
+
return
|
|
119
|
+
metadata = path.lstat()
|
|
120
|
+
_require_safe(metadata, path)
|
|
121
|
+
if stat.S_ISREG(metadata.st_mode):
|
|
122
|
+
path.unlink()
|
|
123
|
+
return
|
|
124
|
+
for child in sorted(path.iterdir()):
|
|
125
|
+
_remove_tree(child)
|
|
126
|
+
path.rmdir()
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _remove_if_empty(path: Path) -> None:
|
|
130
|
+
try:
|
|
131
|
+
path.rmdir()
|
|
132
|
+
except FileNotFoundError:
|
|
133
|
+
pass
|
|
134
|
+
except OSError:
|
|
135
|
+
pass
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _read_file(path: Path, expected: os.stat_result) -> bytes:
|
|
139
|
+
descriptor = os.open(path, os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0))
|
|
140
|
+
try:
|
|
141
|
+
return _read_descriptor(descriptor, expected, path)
|
|
142
|
+
finally:
|
|
143
|
+
os.close(descriptor)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _read_child(directory: int, name: str, expected: os.stat_result) -> bytes:
|
|
147
|
+
descriptor = os.open(
|
|
148
|
+
name, os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0), dir_fd=directory
|
|
149
|
+
)
|
|
150
|
+
try:
|
|
151
|
+
return _read_descriptor(descriptor, expected, Path(name))
|
|
152
|
+
finally:
|
|
153
|
+
os.close(descriptor)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _read_descriptor(descriptor: int, expected: os.stat_result, path: Path) -> bytes:
|
|
157
|
+
opened = os.fstat(descriptor)
|
|
158
|
+
_require_safe(opened, path)
|
|
159
|
+
if (opened.st_dev, opened.st_ino) != (expected.st_dev, expected.st_ino):
|
|
160
|
+
raise ValueError(f"activation state changed while reading: {path}")
|
|
161
|
+
with os.fdopen(descriptor, "rb", closefd=False) as source:
|
|
162
|
+
return source.read()
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _open_directory(path: Path, expected: os.stat_result) -> int:
|
|
166
|
+
descriptor = os.open(
|
|
167
|
+
path, os.O_RDONLY | os.O_DIRECTORY | getattr(os, "O_NOFOLLOW", 0)
|
|
168
|
+
)
|
|
169
|
+
opened = os.fstat(descriptor)
|
|
170
|
+
if (opened.st_dev, opened.st_ino) != (expected.st_dev, expected.st_ino):
|
|
171
|
+
os.close(descriptor)
|
|
172
|
+
raise ValueError(f"activation state changed while reading: {path}")
|
|
173
|
+
return descriptor
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _require_safe(metadata: os.stat_result, path: Path) -> None:
|
|
177
|
+
reparse = getattr(stat, "FILE_ATTRIBUTE_REPARSE_POINT", 0x400)
|
|
178
|
+
if (
|
|
179
|
+
stat.S_ISLNK(metadata.st_mode)
|
|
180
|
+
or getattr(metadata, "st_file_attributes", 0) & reparse
|
|
181
|
+
):
|
|
182
|
+
raise ValueError(f"activation state refuses link: {path}")
|
|
183
|
+
if stat.S_ISREG(metadata.st_mode) and metadata.st_nlink == 1:
|
|
184
|
+
return
|
|
185
|
+
if stat.S_ISDIR(metadata.st_mode):
|
|
186
|
+
return
|
|
187
|
+
raise ValueError(
|
|
188
|
+
f"activation state requires real directories and regular files: {path}"
|
|
189
|
+
)
|
|
@@ -26,21 +26,36 @@ def plugin_release(manifest_path: Path, package_override: bool = False) -> str:
|
|
|
26
26
|
return release
|
|
27
27
|
|
|
28
28
|
|
|
29
|
-
def releases_match(
|
|
29
|
+
def releases_match(
|
|
30
|
+
expected_manifest: Path, observed_manifest: Path
|
|
31
|
+
) -> tuple[bool, str]:
|
|
30
32
|
try:
|
|
31
33
|
expected = plugin_release(expected_manifest)
|
|
32
34
|
except (OSError, ValueError, json.JSONDecodeError):
|
|
33
|
-
return
|
|
35
|
+
return (
|
|
36
|
+
False,
|
|
37
|
+
"runtime package release mismatch: expected valid plugin release, observed missing or invalid",
|
|
38
|
+
)
|
|
34
39
|
try:
|
|
35
40
|
observed = plugin_release(observed_manifest)
|
|
36
41
|
except (OSError, ValueError, json.JSONDecodeError):
|
|
37
|
-
return
|
|
42
|
+
return (
|
|
43
|
+
False,
|
|
44
|
+
f"runtime package release mismatch: expected {expected}, observed missing or invalid",
|
|
45
|
+
)
|
|
38
46
|
if expected != observed:
|
|
39
|
-
return
|
|
47
|
+
return (
|
|
48
|
+
False,
|
|
49
|
+
f"runtime package release mismatch: expected {expected}, observed {observed}",
|
|
50
|
+
)
|
|
40
51
|
return True, ""
|
|
41
52
|
|
|
42
53
|
|
|
43
|
-
def runtime_cache_key(
|
|
54
|
+
def runtime_cache_key(
|
|
55
|
+
*, manifest: Path, package_override: bool, identity: list[str]
|
|
56
|
+
) -> str:
|
|
44
57
|
release = plugin_release(manifest, package_override)
|
|
45
|
-
digest_input = "\0".join(
|
|
58
|
+
digest_input = "\0".join(
|
|
59
|
+
("codexy.runtime-cache/v2", *identity[:-1], release, identity[-1])
|
|
60
|
+
)
|
|
46
61
|
return f"v2-{hashlib.sha256(digest_input.encode()).hexdigest()}"
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema": "getcodexy.component-manifest.v1",
|
|
3
|
+
"marketplace": {
|
|
4
|
+
"name": "codexy",
|
|
5
|
+
"source": "https://github.com/eunsoogi/codexy.git"
|
|
6
|
+
},
|
|
7
|
+
"domainErrors": {
|
|
8
|
+
"component-version-mismatch": "Installed versions must match the post-operation manifest target.",
|
|
9
|
+
"components-not-accepted": "The command does not accept component arguments.",
|
|
10
|
+
"conflicting-component-request": "A component request contains a duplicate or conflicting operand.",
|
|
11
|
+
"conflicting-installed-state": "An installed record does not match its official marketplace asset.",
|
|
12
|
+
"dependency-protected-removal": "A retained component still requires a requested removal.",
|
|
13
|
+
"incompatible-component-selection": "A component selection is not in the manifest compatibility set.",
|
|
14
|
+
"inconsistent-installed-state": "The installed inventory violates component dependencies.",
|
|
15
|
+
"installed-state-mismatch": "The fresh installed inventory differs from the expected selection.",
|
|
16
|
+
"invalid-installed-inventory": "The host plugin inventory has an invalid shape.",
|
|
17
|
+
"missing-removal-target": "remove requires one or more component arguments.",
|
|
18
|
+
"mixed-version-state": "Installed components do not share one lockstep version.",
|
|
19
|
+
"no-recorded-selection": "update requires a recorded installed component inventory.",
|
|
20
|
+
"operation-failed": "A mutation did not complete and its pre-operation selection was restored.",
|
|
21
|
+
"unknown-component": "A named component is not core, github, or devtools.",
|
|
22
|
+
"unknown-installed-component": "The official Codexy marketplace inventory contains an unrecognized product."
|
|
23
|
+
},
|
|
24
|
+
"components": [
|
|
25
|
+
{
|
|
26
|
+
"id": "core",
|
|
27
|
+
"plugin": "codexy",
|
|
28
|
+
"version": "1.4.0",
|
|
29
|
+
"dependencies": [],
|
|
30
|
+
"asset": {
|
|
31
|
+
"pluginId": "codexy@codexy",
|
|
32
|
+
"packageRoot": "plugins/codexy",
|
|
33
|
+
"requiredPaths": [
|
|
34
|
+
".codex-plugin/plugin.json",
|
|
35
|
+
"assets/codexy-icon.png"
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"id": "github",
|
|
41
|
+
"plugin": "codexy-github",
|
|
42
|
+
"version": "1.4.0",
|
|
43
|
+
"dependencies": [
|
|
44
|
+
"core"
|
|
45
|
+
],
|
|
46
|
+
"asset": {
|
|
47
|
+
"pluginId": "codexy-github@codexy",
|
|
48
|
+
"packageRoot": "plugins/codexy-github",
|
|
49
|
+
"requiredPaths": [
|
|
50
|
+
".codex-plugin/plugin.json",
|
|
51
|
+
"skills/git-workflow/SKILL.md"
|
|
52
|
+
]
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"id": "devtools",
|
|
57
|
+
"plugin": "codexy-devtools",
|
|
58
|
+
"version": "1.4.0",
|
|
59
|
+
"dependencies": [
|
|
60
|
+
"core"
|
|
61
|
+
],
|
|
62
|
+
"asset": {
|
|
63
|
+
"pluginId": "codexy-devtools@codexy",
|
|
64
|
+
"packageRoot": "plugins/codexy-devtools",
|
|
65
|
+
"requiredPaths": [
|
|
66
|
+
".codex-plugin/plugin.json",
|
|
67
|
+
".mcp.json"
|
|
68
|
+
]
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
],
|
|
72
|
+
"compatibleCombinations": [
|
|
73
|
+
{
|
|
74
|
+
"components": [],
|
|
75
|
+
"version": "1.4.0"
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"components": [
|
|
79
|
+
"core"
|
|
80
|
+
],
|
|
81
|
+
"version": "1.4.0"
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"components": [
|
|
85
|
+
"core",
|
|
86
|
+
"github"
|
|
87
|
+
],
|
|
88
|
+
"version": "1.4.0"
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
"components": [
|
|
92
|
+
"core",
|
|
93
|
+
"devtools"
|
|
94
|
+
],
|
|
95
|
+
"version": "1.4.0"
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"components": [
|
|
99
|
+
"core",
|
|
100
|
+
"github",
|
|
101
|
+
"devtools"
|
|
102
|
+
],
|
|
103
|
+
"version": "1.4.0"
|
|
104
|
+
}
|
|
105
|
+
]
|
|
106
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""Public command line for transactional component lifecycle operations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from .component_inspection import doctor, status
|
|
12
|
+
from .component_lifecycle import PreAdmissionError, run_operation
|
|
13
|
+
from .component_transaction_identity import operation_id
|
|
14
|
+
from .component_transition_model import OperationReceipt
|
|
15
|
+
from .component_transition_rejections import Rejection, RejectionStage, StateFailure
|
|
16
|
+
from .monolith_migration import migrate
|
|
17
|
+
from .version_lock import default_package_version
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def main(argv: list[str] | None = None) -> int:
|
|
21
|
+
parser = argparse.ArgumentParser(prog="getcodexy", allow_abbrev=False)
|
|
22
|
+
parser.add_argument(
|
|
23
|
+
"--codex",
|
|
24
|
+
type=Path,
|
|
25
|
+
help="optional absolute path supplied by the trusted Codex host",
|
|
26
|
+
)
|
|
27
|
+
parser.add_argument(
|
|
28
|
+
"--codex-home",
|
|
29
|
+
type=Path,
|
|
30
|
+
default=Path(os.environ.get("CODEX_HOME", Path.home() / ".codex")),
|
|
31
|
+
)
|
|
32
|
+
commands = parser.add_subparsers(dest="command", required=True)
|
|
33
|
+
for command in ("install", "update", "remove", "migrate"):
|
|
34
|
+
child = commands.add_parser(command, allow_abbrev=False)
|
|
35
|
+
child.add_argument("components", nargs="*")
|
|
36
|
+
child.add_argument("--json", action="store_true", dest="json_output")
|
|
37
|
+
for command in ("status", "doctor"):
|
|
38
|
+
child = commands.add_parser(command, allow_abbrev=False)
|
|
39
|
+
child.add_argument("--json", action="store_true", dest="json_output")
|
|
40
|
+
bootstrap = commands.add_parser("bootstrap", allow_abbrev=False)
|
|
41
|
+
bootstrap.add_argument("components", nargs="*")
|
|
42
|
+
bootstrap.add_argument("--json", action="store_true", dest="json_output")
|
|
43
|
+
arguments = parser.parse_args(argv)
|
|
44
|
+
try:
|
|
45
|
+
if arguments.command == "status":
|
|
46
|
+
receipt = status(arguments.codex_home, codex=arguments.codex)
|
|
47
|
+
elif arguments.command == "doctor":
|
|
48
|
+
receipt = doctor(arguments.codex_home, codex=arguments.codex)
|
|
49
|
+
elif arguments.command == "migrate":
|
|
50
|
+
if arguments.codex is None:
|
|
51
|
+
raise PreAdmissionError("migrate requires a trusted Codex executable")
|
|
52
|
+
receipt = migrate(
|
|
53
|
+
arguments.codex_home,
|
|
54
|
+
arguments.codex,
|
|
55
|
+
lambda command: _run_migration(command, arguments.codex_home),
|
|
56
|
+
tuple(arguments.components),
|
|
57
|
+
)
|
|
58
|
+
else:
|
|
59
|
+
receipt = run_operation(
|
|
60
|
+
arguments.command,
|
|
61
|
+
tuple(getattr(arguments, "components", ())),
|
|
62
|
+
arguments.codex_home,
|
|
63
|
+
arguments.codex,
|
|
64
|
+
)
|
|
65
|
+
except PreAdmissionError as error:
|
|
66
|
+
if arguments.command == "bootstrap" and arguments.json_output:
|
|
67
|
+
print(json.dumps(_bootstrap_host_failure(), sort_keys=True))
|
|
68
|
+
return 2
|
|
69
|
+
if arguments.command == "migrate" and arguments.json_output:
|
|
70
|
+
print(json.dumps(_migration_host_failure(), sort_keys=True))
|
|
71
|
+
return 2
|
|
72
|
+
print(f"getcodexy {arguments.command}: {error}", file=sys.stderr)
|
|
73
|
+
return 1
|
|
74
|
+
except Exception as error:
|
|
75
|
+
print(f"getcodexy {arguments.command}: {error}", file=sys.stderr)
|
|
76
|
+
return 1
|
|
77
|
+
if arguments.json_output:
|
|
78
|
+
print(json.dumps(receipt, sort_keys=True))
|
|
79
|
+
else:
|
|
80
|
+
print(_human(arguments.command, receipt))
|
|
81
|
+
unhealthy = arguments.command in {"status", "doctor"} and bool(
|
|
82
|
+
receipt.get("errors")
|
|
83
|
+
)
|
|
84
|
+
return 0 if receipt["outcome"] == "completed" and not unhealthy else 2
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _run_migration(command: list[str], home: Path):
|
|
88
|
+
from .pre_session import _run
|
|
89
|
+
|
|
90
|
+
return _run(command, home)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _human(command: str, receipt: dict[str, object]) -> str:
|
|
94
|
+
if command == "status":
|
|
95
|
+
return "getcodexy status: installed={installed}; inventory={inventory}; consistency={consistency}; errors={errors}".format(
|
|
96
|
+
installed=",".join(receipt.get("installed_components", [])) or "none",
|
|
97
|
+
inventory=receipt.get("inventory", {}).get("state", "unknown")
|
|
98
|
+
if isinstance(receipt.get("inventory"), dict)
|
|
99
|
+
else "unknown",
|
|
100
|
+
consistency=receipt.get("inventory_consistency", "unknown"),
|
|
101
|
+
errors=",".join(
|
|
102
|
+
error.get("code", "unknown")
|
|
103
|
+
for error in receipt.get("errors", [])
|
|
104
|
+
if isinstance(error, dict)
|
|
105
|
+
)
|
|
106
|
+
or "none",
|
|
107
|
+
)
|
|
108
|
+
if command == "doctor":
|
|
109
|
+
health = receipt.get("component_health", [])
|
|
110
|
+
summary = (
|
|
111
|
+
",".join(
|
|
112
|
+
f"{entry.get('component')}={entry.get('state')}:{entry.get('repair', 'none')}"
|
|
113
|
+
for entry in health
|
|
114
|
+
if isinstance(entry, dict)
|
|
115
|
+
)
|
|
116
|
+
or "none"
|
|
117
|
+
)
|
|
118
|
+
readiness = receipt.get("host_readiness", {})
|
|
119
|
+
missing = (
|
|
120
|
+
",".join(readiness.get("missing_requirements", []))
|
|
121
|
+
if isinstance(readiness, dict)
|
|
122
|
+
else "unknown"
|
|
123
|
+
)
|
|
124
|
+
return f"getcodexy doctor: health={summary}; missing={missing or 'none'}; errors={','.join(error.get('code', 'unknown') for error in receipt.get('errors', []) if isinstance(error, dict)) or 'none'}"
|
|
125
|
+
return f"getcodexy {command}: {receipt['outcome']}"
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _bootstrap_host_failure() -> dict[str, object]:
|
|
129
|
+
rejection = Rejection.from_failure(
|
|
130
|
+
RejectionStage.HOST, StateFailure.INCONSISTENT_INSTALLED_STATE
|
|
131
|
+
)
|
|
132
|
+
return OperationReceipt.rejected(
|
|
133
|
+
operation_id(None), "bootstrap", (), (), rejection
|
|
134
|
+
).encode()
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _migration_host_failure() -> dict[str, object]:
|
|
138
|
+
return {
|
|
139
|
+
"schema": "getcodexy.monolith-migration-receipt.v1",
|
|
140
|
+
"command": "migrate",
|
|
141
|
+
"outcome": "rejected",
|
|
142
|
+
"source_version": None,
|
|
143
|
+
"target_version": default_package_version(),
|
|
144
|
+
"selection_after": [],
|
|
145
|
+
"errors": [{"code": "trusted-codex-required"}],
|
|
146
|
+
"recovery": "rerun from the trusted Codex host",
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
if __name__ == "__main__":
|
|
151
|
+
raise SystemExit(main())
|