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.
Files changed (61) hide show
  1. {getcodexy-1.2.2 → getcodexy-1.4.0}/PKG-INFO +1 -1
  2. getcodexy-1.4.0/build_backend.py +51 -0
  3. {getcodexy-1.2.2 → getcodexy-1.4.0}/pyproject.toml +7 -2
  4. getcodexy-1.4.0/src/codexy_runtime_tools/_version_data/uv.lock +8 -0
  5. getcodexy-1.4.0/src/codexy_runtime_tools/activation_transaction.py +189 -0
  6. {getcodexy-1.2.2 → getcodexy-1.4.0}/src/codexy_runtime_tools/cache.py +21 -6
  7. getcodexy-1.4.0/src/codexy_runtime_tools/component-manifest.json +106 -0
  8. getcodexy-1.4.0/src/codexy_runtime_tools/component_cli.py +151 -0
  9. getcodexy-1.4.0/src/codexy_runtime_tools/component_health.py +156 -0
  10. getcodexy-1.4.0/src/codexy_runtime_tools/component_inspection.py +209 -0
  11. getcodexy-1.4.0/src/codexy_runtime_tools/component_integrity.py +207 -0
  12. getcodexy-1.4.0/src/codexy_runtime_tools/component_inventory_classification.py +132 -0
  13. getcodexy-1.4.0/src/codexy_runtime_tools/component_inventory_records.py +82 -0
  14. getcodexy-1.4.0/src/codexy_runtime_tools/component_lifecycle.py +11 -0
  15. getcodexy-1.4.0/src/codexy_runtime_tools/component_lifecycle_admission.py +120 -0
  16. getcodexy-1.4.0/src/codexy_runtime_tools/component_lifecycle_interlock.py +38 -0
  17. getcodexy-1.4.0/src/codexy_runtime_tools/component_lifecycle_operation.py +246 -0
  18. getcodexy-1.4.0/src/codexy_runtime_tools/component_lifecycle_preflight.py +49 -0
  19. getcodexy-1.4.0/src/codexy_runtime_tools/component_lifecycle_recovery.py +234 -0
  20. getcodexy-1.4.0/src/codexy_runtime_tools/component_lifecycle_support.py +33 -0
  21. getcodexy-1.4.0/src/codexy_runtime_tools/component_lifecycle_terminal.py +44 -0
  22. getcodexy-1.4.0/src/codexy_runtime_tools/component_manifest.py +247 -0
  23. getcodexy-1.4.0/src/codexy_runtime_tools/component_manifest_dependencies.py +44 -0
  24. getcodexy-1.4.0/src/codexy_runtime_tools/component_manifest_values.py +24 -0
  25. getcodexy-1.4.0/src/codexy_runtime_tools/component_observed_inventory.py +99 -0
  26. getcodexy-1.4.0/src/codexy_runtime_tools/component_registration_health.py +232 -0
  27. getcodexy-1.4.0/src/codexy_runtime_tools/component_resolver.py +187 -0
  28. getcodexy-1.4.0/src/codexy_runtime_tools/component_transaction_durability.py +22 -0
  29. getcodexy-1.4.0/src/codexy_runtime_tools/component_transaction_identity.py +21 -0
  30. getcodexy-1.4.0/src/codexy_runtime_tools/component_transaction_receipts.py +50 -0
  31. getcodexy-1.4.0/src/codexy_runtime_tools/component_transaction_snapshot.py +21 -0
  32. getcodexy-1.4.0/src/codexy_runtime_tools/component_transaction_state.py +237 -0
  33. getcodexy-1.4.0/src/codexy_runtime_tools/component_transition_journal.py +206 -0
  34. getcodexy-1.4.0/src/codexy_runtime_tools/component_transition_model.py +107 -0
  35. getcodexy-1.4.0/src/codexy_runtime_tools/component_transition_receipt.py +207 -0
  36. getcodexy-1.4.0/src/codexy_runtime_tools/component_transition_rejections.py +159 -0
  37. getcodexy-1.4.0/src/codexy_runtime_tools/contract.py +248 -0
  38. getcodexy-1.4.0/src/codexy_runtime_tools/github_checks.py +170 -0
  39. getcodexy-1.4.0/src/codexy_runtime_tools/github_pre_session.py +226 -0
  40. getcodexy-1.4.0/src/codexy_runtime_tools/identity.py +97 -0
  41. getcodexy-1.4.0/src/codexy_runtime_tools/installer.py +161 -0
  42. getcodexy-1.4.0/src/codexy_runtime_tools/marketplace_repin.py +167 -0
  43. getcodexy-1.4.0/src/codexy_runtime_tools/monolith_baseline.py +88 -0
  44. getcodexy-1.4.0/src/codexy_runtime_tools/monolith_classifier.py +50 -0
  45. getcodexy-1.4.0/src/codexy_runtime_tools/monolith_migration.py +204 -0
  46. getcodexy-1.4.0/src/codexy_runtime_tools/monolith_migration_host.py +186 -0
  47. getcodexy-1.4.0/src/codexy_runtime_tools/monolith_migration_plan.py +58 -0
  48. getcodexy-1.4.0/src/codexy_runtime_tools/monolith_migration_state.py +183 -0
  49. {getcodexy-1.2.2 → getcodexy-1.4.0}/src/codexy_runtime_tools/package.py +80 -70
  50. getcodexy-1.4.0/src/codexy_runtime_tools/package_archive.py +86 -0
  51. getcodexy-1.4.0/src/codexy_runtime_tools/plugin_resolution.py +154 -0
  52. getcodexy-1.4.0/src/codexy_runtime_tools/pre_session.py +184 -0
  53. getcodexy-1.4.0/src/codexy_runtime_tools/runtime.py +223 -0
  54. getcodexy-1.4.0/src/codexy_runtime_tools/runtime_configuration.py +135 -0
  55. getcodexy-1.4.0/src/codexy_runtime_tools/source.py +182 -0
  56. getcodexy-1.4.0/src/codexy_runtime_tools/updater.py +179 -0
  57. getcodexy-1.4.0/src/codexy_runtime_tools/version_lock.py +58 -0
  58. getcodexy-1.4.0/uv.lock +8 -0
  59. getcodexy-1.2.2/src/codexy_runtime_tools/installer.py +0 -107
  60. getcodexy-1.2.2/src/codexy_runtime_tools/runtime.py +0 -169
  61. {getcodexy-1.2.2 → getcodexy-1.4.0}/src/codexy_runtime_tools/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: getcodexy
3
- Version: 1.2.2
3
+ Version: 1.4.0
4
4
  Summary: Version-pinned MCP runtime bootstrap tools for Codexy
5
5
  Author: Eunsoo Lee
6
6
  License-Expression: MIT
@@ -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 = "uv_build"
3
+ build-backend = "build_backend"
4
+ backend-path = ["."]
4
5
 
5
6
  [project]
6
7
  name = "getcodexy"
7
- version = "1.2.2"
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,8 @@
1
+ version = 1
2
+ revision = 3
3
+ requires-python = ">=3.10"
4
+
5
+ [[package]]
6
+ name = "getcodexy"
7
+ version = "1.4.0"
8
+ source = { editable = "." }
@@ -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(expected_manifest: Path, observed_manifest: Path) -> tuple[bool, str]:
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 False, "runtime package release mismatch: expected valid plugin release, observed missing or invalid"
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 False, f"runtime package release mismatch: expected {expected}, observed missing or invalid"
42
+ return (
43
+ False,
44
+ f"runtime package release mismatch: expected {expected}, observed missing or invalid",
45
+ )
38
46
  if expected != observed:
39
- return False, f"runtime package release mismatch: expected {expected}, observed {observed}"
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(*, manifest: Path, package_override: bool, identity: list[str]) -> str:
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(("codexy.runtime-cache/v2", *identity[:-1], release, identity[-1]))
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())