cox-protocol-package 1.0.2 → 1.2.0
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.
- package/CHANGELOG.md +22 -0
- package/COX_FILE_FORMAT_SPECIFICATION_v1.0.md +40 -200
- package/FINALIZATION.md +50 -0
- package/LocalAppManager/Dashboard/analytics_sidebar_panels/analytics_sidebar_panels.cox +56 -0
- package/LocalAppManager/Dashboard/analytics_sidebar_panels/analytics_sidebar_panels_screenshot.placeholder.txt +1 -0
- package/LocalAppManager/Dashboard/dashboard.cox +62 -0
- package/LocalAppManager/Dashboard/dashboard_screenshot.placeholder.txt +1 -0
- package/LocalAppManager/Dashboard/left_navigation_sidebar/left_navigation_sidebar.cox +56 -0
- package/LocalAppManager/Dashboard/left_navigation_sidebar/left_navigation_sidebar_screenshot.placeholder.txt +1 -0
- package/LocalAppManager/Dashboard/projects_grid_cards/projects_grid_cards.cox +56 -0
- package/LocalAppManager/Dashboard/projects_grid_cards/projects_grid_cards_screenshot.placeholder.txt +1 -0
- package/LocalAppManager/Dashboard/top_header_stats_bar/top_header_stats_bar.cox +56 -0
- package/LocalAppManager/Dashboard/top_header_stats_bar/top_header_stats_bar_screenshot.placeholder.txt +1 -0
- package/MANIFEST.in +20 -0
- package/README.md +67 -154
- package/bin/cox-protocol.js +23 -97
- package/cox_protocol/__init__.py +2 -0
- package/cox_protocol/__main__.py +3 -0
- package/cox_protocol/compiler.py +273 -0
- package/cox_protocol/cox_cli.py +146 -0
- package/cox_protocol/cox_lib.py +599 -0
- package/cox_protocol/package_target.py +262 -0
- package/cox_protocol/react_target.py +306 -0
- package/cox_protocol/scaffold.py +56 -0
- package/cox_protocol/schemas/cox.component.v1.schema.json +180 -0
- package/cox_protocol/schemas/cox.crop_metadata.v1.schema.json +83 -0
- package/cox_protocol/schemas/cox.manifest.v1.schema.json +172 -0
- package/cox_protocol/schemas/cox.page.v1.schema.json +242 -0
- package/cox_protocol/schemas/cox.protocol.v1.schema.json +38 -0
- package/cox_protocol/starter/README.md +7 -0
- package/cox_protocol/starter/manifest.cox +26 -0
- package/cox_protocol/starter/page/action_button/action_button.cox +34 -0
- package/cox_protocol/starter/page/action_button/reference.svg +1 -0
- package/cox_protocol/starter/page/page.cox +39 -0
- package/cox_protocol/starter/page/reference.svg +1 -0
- package/editor-support/vscode/CHANGELOG.md +6 -0
- package/editor-support/vscode/README.md +2 -1
- package/editor-support/vscode/package.json +3 -3
- package/editor-support/vscode/snippets/cox-common.json +1 -1
- package/editor-support/vscode/snippets/cox-component.json +2 -1
- package/editor-support/vscode/snippets/cox-crop-metadata.json +1 -1
- package/editor-support/vscode/snippets/cox-manifest.json +1 -1
- package/editor-support/vscode/snippets/cox-page.json +2 -1
- package/editor-support/vscode/snippets/cox-protocol.json +1 -1
- package/examples/quickstart/README.md +7 -0
- package/examples/quickstart/manifest.cox +26 -0
- package/examples/quickstart/page/action_button/action_button.cox +34 -0
- package/examples/quickstart/page/action_button/reference.svg +1 -0
- package/examples/quickstart/page/page.cox +39 -0
- package/examples/quickstart/page/reference.svg +1 -0
- package/guides/contract-demo.md +24 -0
- package/guides/npm-installation.md +16 -287
- package/manifest.cox +7 -4
- package/package.json +30 -14
- package/protocols/recursive_decomposition_protocol.cox +8 -8
- package/pyproject.toml +17 -6
- package/schemas/cox.component.v1.schema.json +17 -4
- package/schemas/cox.crop_metadata.v1.schema.json +1 -1
- package/schemas/cox.manifest.v1.schema.json +1 -1
- package/schemas/cox.page.v1.schema.json +17 -4
- package/schemas/cox.protocol.v1.schema.json +1 -1
- package/scripts/check_distribution.py +87 -0
- package/scripts/demo.py +62 -0
- package/scripts/package-vscode.mjs +12 -0
- package/scripts/release_check.py +86 -0
- package/tests/conftest.py +21 -0
- package/tests/test_cli_and_typescript.py +123 -0
- package/tests/test_compiler.py +279 -0
- package/tests/test_validation.py +215 -0
- package/tools/__init__.py +2 -2
- package/tools/cox_cli.py +2 -123
- package/tools/cox_lib.py +2 -558
- package/tools/package_target.py +2 -214
- package/tools/react_target.py +2 -278
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
"""Build into an isolated staging directory, then publish only owned artifacts."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
import shutil
|
|
10
|
+
import tempfile
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any, Sequence
|
|
13
|
+
|
|
14
|
+
from . import __version__
|
|
15
|
+
from .cox_lib import (
|
|
16
|
+
CoxError,
|
|
17
|
+
fail,
|
|
18
|
+
normalize_compile_targets,
|
|
19
|
+
validate_project,
|
|
20
|
+
write_json,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
MARKER = ".cox-build.json"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def digest(path: Path) -> str:
|
|
27
|
+
return hashlib.sha256(path.read_bytes()).hexdigest()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def build_location(summary, out_dir: Path) -> Path:
|
|
31
|
+
candidate = out_dir if out_dir.is_absolute() else summary.project_root / out_dir
|
|
32
|
+
if candidate.is_symlink():
|
|
33
|
+
fail("OUTPUT_SYMLINK", "Output directory must not be a symlink", str(candidate))
|
|
34
|
+
# Canonicalize parents (including macOS /var and /tmp aliases) before overlap
|
|
35
|
+
# checks. The chosen root and every managed path inside it reject symlinks.
|
|
36
|
+
root = candidate.resolve()
|
|
37
|
+
if summary.project_root.is_relative_to(root):
|
|
38
|
+
fail(
|
|
39
|
+
"OUTPUT_OVERLAP",
|
|
40
|
+
"Output cannot be the project root or an ancestor",
|
|
41
|
+
str(root),
|
|
42
|
+
)
|
|
43
|
+
for source in [summary.manifest_path, *summary.referenced_paths]:
|
|
44
|
+
if source.is_relative_to(root) or (
|
|
45
|
+
source.is_dir() and root.is_relative_to(source)
|
|
46
|
+
):
|
|
47
|
+
fail("OUTPUT_OVERLAP", "Output overlaps a manifest input", str(source))
|
|
48
|
+
if root.exists() and not root.is_dir():
|
|
49
|
+
fail("OUTPUT_TYPE", "Output must be a directory", str(root))
|
|
50
|
+
return root
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def artifact_paths(stage: Path) -> list[Path]:
|
|
54
|
+
return sorted(p for p in stage.rglob("*") if p.is_file())
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def load_ownership(build_root: Path) -> dict[str, str]:
|
|
58
|
+
marker = build_root / MARKER
|
|
59
|
+
if marker.is_symlink():
|
|
60
|
+
fail("OUTPUT_SYMLINK", "Build marker must not be a symlink", str(marker))
|
|
61
|
+
if not marker.exists():
|
|
62
|
+
return {}
|
|
63
|
+
try:
|
|
64
|
+
data = json.loads(marker.read_text(encoding="utf-8"))
|
|
65
|
+
if data["format"] != "cox-build/1" or not isinstance(data["files"], dict):
|
|
66
|
+
raise ValueError("Unsupported build marker")
|
|
67
|
+
for raw, checksum in data["files"].items():
|
|
68
|
+
if (
|
|
69
|
+
not raw
|
|
70
|
+
or any(part in ("", ".", "..") for part in raw.split("/"))
|
|
71
|
+
or any(c in raw for c in '\\<>:"|?*')
|
|
72
|
+
or any(ord(c) < 32 for c in raw)
|
|
73
|
+
or raw == MARKER
|
|
74
|
+
):
|
|
75
|
+
raise ValueError("Invalid owned output path")
|
|
76
|
+
if not isinstance(checksum, str) or not re.fullmatch(
|
|
77
|
+
"[0-9a-f]{64}", checksum
|
|
78
|
+
):
|
|
79
|
+
raise ValueError("Invalid checksum in build marker")
|
|
80
|
+
return data["files"]
|
|
81
|
+
except (OSError, ValueError, KeyError, TypeError) as exc:
|
|
82
|
+
fail("OUTPUT_MARKER", f"Invalid build ownership marker: {exc}", str(marker))
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def publish(stage: Path, build_root: Path) -> None:
|
|
86
|
+
outputs = {p.relative_to(stage).as_posix(): p for p in artifact_paths(stage)}
|
|
87
|
+
owned = load_ownership(build_root)
|
|
88
|
+
# A compiler owns only the exact generated bytes recorded on the previous run.
|
|
89
|
+
for rel in sorted(set(outputs) | set(owned)):
|
|
90
|
+
path = build_root / rel
|
|
91
|
+
for parent in (path, *path.parents):
|
|
92
|
+
if parent == build_root:
|
|
93
|
+
break
|
|
94
|
+
if parent.is_symlink():
|
|
95
|
+
fail(
|
|
96
|
+
"OUTPUT_SYMLINK",
|
|
97
|
+
"Refusing to replace an output through a symlink",
|
|
98
|
+
str(parent),
|
|
99
|
+
)
|
|
100
|
+
if parent != path and parent.exists() and not parent.is_dir():
|
|
101
|
+
fail("OUTPUT_COLLISION", "Output parent is a file", str(parent))
|
|
102
|
+
if path.exists():
|
|
103
|
+
if not path.is_file() or rel not in owned:
|
|
104
|
+
fail(
|
|
105
|
+
"OUTPUT_COLLISION",
|
|
106
|
+
"Existing file is not owned by COX; use a fresh --out directory",
|
|
107
|
+
str(path),
|
|
108
|
+
)
|
|
109
|
+
if digest(path) != owned[rel]:
|
|
110
|
+
fail(
|
|
111
|
+
"OUTPUT_MODIFIED",
|
|
112
|
+
"Generated file has local edits; preserve them and use a fresh --out directory",
|
|
113
|
+
str(path),
|
|
114
|
+
)
|
|
115
|
+
build_root.mkdir(parents=True, exist_ok=True)
|
|
116
|
+
with tempfile.TemporaryDirectory(
|
|
117
|
+
prefix=".cox-backup-", dir=build_root.parent
|
|
118
|
+
) as backup_name:
|
|
119
|
+
backup = Path(backup_name)
|
|
120
|
+
previous = [rel for rel in owned if (build_root / rel).is_file()]
|
|
121
|
+
for rel in previous:
|
|
122
|
+
dest = backup / rel
|
|
123
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
124
|
+
shutil.copy2(build_root / rel, dest)
|
|
125
|
+
marker = build_root / MARKER
|
|
126
|
+
old_marker = marker.read_bytes() if marker.exists() else None
|
|
127
|
+
installed = []
|
|
128
|
+
try:
|
|
129
|
+
hashes = {rel: digest(path) for rel, path in outputs.items()}
|
|
130
|
+
for rel, source in outputs.items():
|
|
131
|
+
dest = build_root / rel
|
|
132
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
133
|
+
os.replace(source, dest)
|
|
134
|
+
installed.append(rel)
|
|
135
|
+
for rel in set(owned) - set(outputs):
|
|
136
|
+
(build_root / rel).unlink(missing_ok=True)
|
|
137
|
+
temporary_marker = backup / "new-marker.json"
|
|
138
|
+
write_json(temporary_marker, {"format": "cox-build/1", "files": hashes})
|
|
139
|
+
os.replace(temporary_marker, marker)
|
|
140
|
+
except OSError as exc:
|
|
141
|
+
for rel in installed:
|
|
142
|
+
(build_root / rel).unlink(missing_ok=True)
|
|
143
|
+
for rel in previous:
|
|
144
|
+
dest = build_root / rel
|
|
145
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
146
|
+
shutil.copy2(backup / rel, dest)
|
|
147
|
+
if old_marker is None:
|
|
148
|
+
marker.unlink(missing_ok=True)
|
|
149
|
+
else:
|
|
150
|
+
marker.write_bytes(old_marker)
|
|
151
|
+
raise CoxError(
|
|
152
|
+
f"Could not publish build; previous generated files restored: {exc}"
|
|
153
|
+
) from exc
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def compile_project(
|
|
157
|
+
target: Path,
|
|
158
|
+
out_dir: Path,
|
|
159
|
+
targets: Sequence[str] | None = None,
|
|
160
|
+
strict: bool = False,
|
|
161
|
+
) -> dict[str, Any]:
|
|
162
|
+
summary = validate_project(target)
|
|
163
|
+
if strict and summary.warnings:
|
|
164
|
+
fail("WARNINGS_AS_ERRORS", "\n".join(summary.warnings))
|
|
165
|
+
selected = normalize_compile_targets(targets)
|
|
166
|
+
build_root = build_location(summary, out_dir)
|
|
167
|
+
try:
|
|
168
|
+
# Stage on the destination filesystem so publication also works on another volume.
|
|
169
|
+
build_root.parent.mkdir(parents=True, exist_ok=True)
|
|
170
|
+
with tempfile.TemporaryDirectory(
|
|
171
|
+
prefix=".cox-stage-", dir=build_root.parent
|
|
172
|
+
) as temporary:
|
|
173
|
+
stage = Path(temporary)
|
|
174
|
+
results = {}
|
|
175
|
+
if "json" in selected:
|
|
176
|
+
for source, payload in sorted(summary.documents.items()):
|
|
177
|
+
if source != summary.manifest_path:
|
|
178
|
+
write_json(
|
|
179
|
+
(
|
|
180
|
+
stage
|
|
181
|
+
/ "normalized"
|
|
182
|
+
/ source.relative_to(summary.project_root)
|
|
183
|
+
).with_suffix(".json"),
|
|
184
|
+
payload,
|
|
185
|
+
)
|
|
186
|
+
(stage / "normalized").mkdir(exist_ok=True)
|
|
187
|
+
results["json"] = {
|
|
188
|
+
"output_root": stage / "normalized",
|
|
189
|
+
"document_count": len(summary.documents) - 1,
|
|
190
|
+
}
|
|
191
|
+
if "react" in selected:
|
|
192
|
+
from .react_target import emit_react_stubs
|
|
193
|
+
|
|
194
|
+
results["react"] = emit_react_stubs(summary, stage)
|
|
195
|
+
if "package" in selected:
|
|
196
|
+
from .package_target import package_filename
|
|
197
|
+
|
|
198
|
+
name = package_filename(summary)
|
|
199
|
+
results["package"] = {
|
|
200
|
+
"output_root": stage / "package",
|
|
201
|
+
"archive": stage / "package" / name,
|
|
202
|
+
"manifest": stage
|
|
203
|
+
/ "package"
|
|
204
|
+
/ (Path(name).stem + ".manifest.json"),
|
|
205
|
+
}
|
|
206
|
+
manifest_payload = {
|
|
207
|
+
"compiledWith": f"cox {__version__}",
|
|
208
|
+
"mimeType": "application/vnd.cox+yaml",
|
|
209
|
+
"targets": selected,
|
|
210
|
+
"manifest": summary.documents[summary.manifest_path],
|
|
211
|
+
"resolvedFiles": {
|
|
212
|
+
key: value
|
|
213
|
+
for key, value in summary.build_index.items()
|
|
214
|
+
if key not in ("referenced_paths", "cox_documents")
|
|
215
|
+
},
|
|
216
|
+
"targetOutputs": {
|
|
217
|
+
name: {
|
|
218
|
+
key: value.relative_to(stage).as_posix()
|
|
219
|
+
if isinstance(value, Path)
|
|
220
|
+
else value
|
|
221
|
+
for key, value in result.items()
|
|
222
|
+
}
|
|
223
|
+
for name, result in results.items()
|
|
224
|
+
},
|
|
225
|
+
"warnings": summary.warnings,
|
|
226
|
+
}
|
|
227
|
+
write_json(stage / "manifest.compiled.json", manifest_payload)
|
|
228
|
+
report = {
|
|
229
|
+
"ok": True,
|
|
230
|
+
"formatVersion": "1.0",
|
|
231
|
+
"compilerVersion": __version__,
|
|
232
|
+
"project": summary.documents[summary.manifest_path]["Slug"],
|
|
233
|
+
"validatedDocuments": len(summary.documents),
|
|
234
|
+
"referencedPathsChecked": len(summary.referenced_paths),
|
|
235
|
+
"targets": selected,
|
|
236
|
+
"warnings": summary.warnings,
|
|
237
|
+
}
|
|
238
|
+
write_json(stage / "reports" / "verification_report.json", report)
|
|
239
|
+
text = (
|
|
240
|
+
"COX verification report\n"
|
|
241
|
+
+ "\n".join(
|
|
242
|
+
f"{key}: {json.dumps(value, ensure_ascii=True)}"
|
|
243
|
+
for key, value in report.items()
|
|
244
|
+
)
|
|
245
|
+
+ "\n"
|
|
246
|
+
)
|
|
247
|
+
(stage / "reports" / "verification_report.txt").write_text(
|
|
248
|
+
text, encoding="utf-8"
|
|
249
|
+
)
|
|
250
|
+
if "package" in selected:
|
|
251
|
+
from .package_target import emit_package_archive
|
|
252
|
+
|
|
253
|
+
results["package"] = emit_package_archive(summary, stage, results)
|
|
254
|
+
final_results = {
|
|
255
|
+
name: {
|
|
256
|
+
key: build_root / value.relative_to(stage)
|
|
257
|
+
if isinstance(value, Path)
|
|
258
|
+
else value
|
|
259
|
+
for key, value in result.items()
|
|
260
|
+
}
|
|
261
|
+
for name, result in results.items()
|
|
262
|
+
}
|
|
263
|
+
publish(stage, build_root)
|
|
264
|
+
return {
|
|
265
|
+
"build_root": build_root,
|
|
266
|
+
"targets": selected,
|
|
267
|
+
"target_results": final_results,
|
|
268
|
+
"compiled_manifest": build_root / "manifest.compiled.json",
|
|
269
|
+
"report": build_root / "reports" / "verification_report.txt",
|
|
270
|
+
"warnings": summary.warnings,
|
|
271
|
+
}
|
|
272
|
+
except OSError as exc:
|
|
273
|
+
raise CoxError(f"Build I/O failed: {exc}") from exc
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""Human-readable and machine-readable interfaces to the same COX contract."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from . import __version__
|
|
11
|
+
from .cox_lib import CoxError, compile_project, validate_project, fail
|
|
12
|
+
from .package_target import verify_archive
|
|
13
|
+
from .scaffold import init_project
|
|
14
|
+
|
|
15
|
+
app = typer.Typer(
|
|
16
|
+
add_completion=False,
|
|
17
|
+
no_args_is_help=True,
|
|
18
|
+
help="Create, validate, compile and verify COX design contracts.",
|
|
19
|
+
)
|
|
20
|
+
console = Console()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def serializable(value):
|
|
24
|
+
if isinstance(value, Path):
|
|
25
|
+
return str(value)
|
|
26
|
+
if isinstance(value, dict):
|
|
27
|
+
return {key: serializable(item) for key, item in value.items()}
|
|
28
|
+
if isinstance(value, list):
|
|
29
|
+
return [serializable(item) for item in value]
|
|
30
|
+
return value
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def output(payload, machine: bool, title: str):
|
|
34
|
+
if machine:
|
|
35
|
+
typer.echo(json.dumps(serializable(payload), indent=2))
|
|
36
|
+
else:
|
|
37
|
+
console.print(title, style="bold green", markup=False)
|
|
38
|
+
for key, value in payload.items():
|
|
39
|
+
if key not in ("ok", "target_results"):
|
|
40
|
+
console.print(f"{key}: {serializable(value)}", markup=False)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def failure(exc: Exception, machine: bool):
|
|
44
|
+
if not isinstance(exc, CoxError):
|
|
45
|
+
exc = CoxError(f"I/O operation failed: {exc}")
|
|
46
|
+
if machine:
|
|
47
|
+
typer.echo(json.dumps(exc.as_dict(), indent=2))
|
|
48
|
+
else:
|
|
49
|
+
console.print("COX failed", style="bold red", markup=False)
|
|
50
|
+
console.print(str(exc), markup=False)
|
|
51
|
+
raise typer.Exit(1)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def version_callback(value: bool):
|
|
55
|
+
if value:
|
|
56
|
+
typer.echo(__version__)
|
|
57
|
+
raise typer.Exit()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@app.callback()
|
|
61
|
+
def main(
|
|
62
|
+
version: bool = typer.Option(
|
|
63
|
+
False,
|
|
64
|
+
"--version",
|
|
65
|
+
callback=version_callback,
|
|
66
|
+
is_eager=True,
|
|
67
|
+
help="Show the tool version.",
|
|
68
|
+
),
|
|
69
|
+
):
|
|
70
|
+
pass
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@app.command("validate")
|
|
74
|
+
def validate_command(
|
|
75
|
+
project_path: Path = typer.Argument(Path(".")),
|
|
76
|
+
machine: bool = typer.Option(False, "--json", help="Emit JSON diagnostics."),
|
|
77
|
+
strict: bool = typer.Option(False, "--strict", help="Fail on warnings."),
|
|
78
|
+
):
|
|
79
|
+
"""Check canonical schemas, paths and page/component bindings."""
|
|
80
|
+
try:
|
|
81
|
+
summary = validate_project(project_path)
|
|
82
|
+
if strict and summary.warnings:
|
|
83
|
+
fail("WARNINGS_AS_ERRORS", "\n".join(summary.warnings))
|
|
84
|
+
output(
|
|
85
|
+
{
|
|
86
|
+
"ok": True,
|
|
87
|
+
"version": __version__,
|
|
88
|
+
"project": str(summary.project_root),
|
|
89
|
+
"documentCount": len(summary.documents),
|
|
90
|
+
"warnings": summary.warnings,
|
|
91
|
+
},
|
|
92
|
+
machine,
|
|
93
|
+
"COX validation passed",
|
|
94
|
+
)
|
|
95
|
+
except (CoxError, OSError) as exc:
|
|
96
|
+
failure(exc, machine)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@app.command("compile")
|
|
100
|
+
def compile_command(
|
|
101
|
+
project_path: Path = typer.Argument(Path(".")),
|
|
102
|
+
out_dir: Path = typer.Option(Path("build"), "--out", "-o"),
|
|
103
|
+
targets: list[str] = typer.Option(
|
|
104
|
+
[], "--target", "-t", help="json, react, package or all; repeat or use commas."
|
|
105
|
+
),
|
|
106
|
+
machine: bool = typer.Option(False, "--json"),
|
|
107
|
+
strict: bool = typer.Option(False, "--strict"),
|
|
108
|
+
):
|
|
109
|
+
"""Compile validated contracts into deterministic JSON, React stubs and ZIPs."""
|
|
110
|
+
try:
|
|
111
|
+
result = compile_project(project_path, out_dir, targets=targets, strict=strict)
|
|
112
|
+
output({"ok": True, **result}, machine, "COX compilation passed")
|
|
113
|
+
except (CoxError, OSError) as exc:
|
|
114
|
+
failure(exc, machine)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@app.command("init")
|
|
118
|
+
def init_command(
|
|
119
|
+
destination: Path = typer.Argument(...),
|
|
120
|
+
name: Optional[str] = typer.Option(None, "--name"),
|
|
121
|
+
machine: bool = typer.Option(False, "--json"),
|
|
122
|
+
):
|
|
123
|
+
"""Create a complete example project in a new or empty directory."""
|
|
124
|
+
try:
|
|
125
|
+
output(
|
|
126
|
+
{"ok": True, "project": init_project(destination, name)},
|
|
127
|
+
machine,
|
|
128
|
+
"COX project created",
|
|
129
|
+
)
|
|
130
|
+
except (CoxError, OSError) as exc:
|
|
131
|
+
failure(exc, machine)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@app.command("verify-package")
|
|
135
|
+
def verify_command(
|
|
136
|
+
archive: Path = typer.Argument(...), machine: bool = typer.Option(False, "--json")
|
|
137
|
+
):
|
|
138
|
+
"""Verify ZIP contents against the embedded SHA-256 inventory (not a signature)."""
|
|
139
|
+
try:
|
|
140
|
+
output(verify_archive(archive), machine, "COX package verified")
|
|
141
|
+
except (CoxError, OSError) as exc:
|
|
142
|
+
failure(exc, machine)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
if __name__ == "__main__":
|
|
146
|
+
app()
|