aevum-cli 0.5.0__tar.gz → 0.7.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aevum-cli
3
- Version: 0.5.0
3
+ Version: 0.7.0
4
4
  Summary: Aevum -- command-line interface for operating Aevum nodes.
5
5
  Project-URL: Homepage, https://aevum.build
6
6
  Project-URL: Repository, https://github.com/aevum-labs/aevum
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "aevum-cli"
3
- version = "0.5.0"
3
+ version = "0.7.0"
4
4
  description = "Aevum -- command-line interface for operating Aevum nodes."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -1,3 +1,4 @@
1
+ # SPDX-License-Identifier: Apache-2.0
1
2
  """
2
3
  aevum.cli -- Command-line interface for operating Aevum nodes.
3
4
 
@@ -1,3 +1,4 @@
1
+ # SPDX-License-Identifier: Apache-2.0
1
2
  """
2
3
  Entry point: aevum <command>
3
4
  Invoked via [project.scripts] aevum = "aevum.cli.__main__:app"
@@ -5,9 +5,10 @@ Top-level typer app — CLI v2. Sub-commands and direct commands registered here
5
5
  """
6
6
  from __future__ import annotations
7
7
 
8
+ import hashlib
8
9
  import json
9
10
  from pathlib import Path
10
- from typing import Annotated
11
+ from typing import Annotated, Any
11
12
 
12
13
  import typer
13
14
 
@@ -199,6 +200,173 @@ def conform(
199
200
  raise typer.Exit(code=1)
200
201
 
201
202
 
203
+ @app.command(name="verify-receipt")
204
+ def verify_receipt(
205
+ receipt_file: Annotated[
206
+ Path | None,
207
+ typer.Argument(help="Path to COSE_Sign1 receipt file"),
208
+ ] = None,
209
+ receipt_hash: Annotated[
210
+ str | None,
211
+ typer.Option("--hash", help="SHA3-256 hex hash — lookup from AEVUM_RECEIPT_DB"),
212
+ ] = None,
213
+ ) -> None:
214
+ """
215
+ Verify an Aevum COSE_Sign1 receipt file or hash.
216
+
217
+ Decodes the receipt, verifies the Ed25519 signature over the canonical payload,
218
+ and prints a human-readable summary. Exit 0 on valid, exit 1 on invalid signature.
219
+ Exit 2 on unsupported algorithm or hash not found.
220
+
221
+ Examples:
222
+ aevum verify-receipt receipt.cbor
223
+ aevum verify-receipt --hash <sha3-256-hex>
224
+ """
225
+ import cbor2
226
+ import nacl.exceptions
227
+ import nacl.signing
228
+ from aevum.core.receipt import AevumReceipt
229
+
230
+ if receipt_file is None and receipt_hash is None:
231
+ typer.echo("Provide a receipt file path or --hash <hash>.", err=True)
232
+ raise typer.Exit(code=1)
233
+ if receipt_file is not None and receipt_hash is not None:
234
+ typer.echo("Provide either a file path or --hash, not both.", err=True)
235
+ raise typer.Exit(code=1)
236
+
237
+ store_info: dict[str, object] | None = None
238
+
239
+ if receipt_hash is not None:
240
+ try:
241
+ from aevum.core.sqlite_store import SqliteReceiptStore
242
+ store = SqliteReceiptStore.from_env()
243
+ raw = store.get(receipt_hash)
244
+ if raw is None:
245
+ typer.echo(f"RECEIPT NOT FOUND: {receipt_hash}", err=True)
246
+ raise typer.Exit(code=2)
247
+ store_info = store.get_receipt_info(receipt_hash)
248
+ except typer.Exit:
249
+ raise
250
+ except RuntimeError as exc:
251
+ typer.echo(f"Store error: {exc}", err=True)
252
+ raise typer.Exit(code=1) from None
253
+ else:
254
+ assert receipt_file is not None
255
+ if not receipt_file.exists():
256
+ typer.echo(f"File not found: {receipt_file}", err=True)
257
+ raise typer.Exit(code=1)
258
+ raw = receipt_file.read_bytes()
259
+
260
+ try:
261
+ cose = cbor2.loads(raw)
262
+ except Exception as exc: # noqa: BLE001
263
+ typer.echo(f"INVALID: not a valid CBOR file: {exc}", err=True)
264
+ raise typer.Exit(code=1) from None
265
+
266
+ if not isinstance(cose, list) or len(cose) != 4:
267
+ typer.echo("INVALID: not a 4-element COSE_Sign1 array", err=True)
268
+ raise typer.Exit(code=1)
269
+
270
+ protected_bstr, unprotected, payload_bstr, signature_bytes = cose
271
+
272
+ try:
273
+ protected = cbor2.loads(protected_bstr)
274
+ except Exception as exc: # noqa: BLE001
275
+ typer.echo(f"INVALID: cannot decode protected header: {exc}", err=True)
276
+ raise typer.Exit(code=1) from None
277
+
278
+ alg = protected.get(1)
279
+ if alg != -8:
280
+ typer.echo(f"UNSUPPORTED ALGORITHM: alg={alg!r} (expected -8 for EdDSA/Ed25519)", err=True)
281
+ raise typer.Exit(code=2) from None
282
+
283
+ try:
284
+ receipt_data = cbor2.loads(payload_bstr)
285
+ receipt = AevumReceipt.model_validate(receipt_data)
286
+ except Exception as exc: # noqa: BLE001
287
+ typer.echo(f"INVALID: cannot decode receipt payload: {exc}", err=True)
288
+ raise typer.Exit(code=1) from None
289
+
290
+ # Reconstruct Sig_Structure and verify signature
291
+ sig_structure = cbor2.dumps(["Signature1", protected_bstr, b"", payload_bstr])
292
+ digest = hashlib.sha3_256(sig_structure).digest()
293
+
294
+ # Try to find Ed25519 public key from state dir (kid field in protected header reserved for future)
295
+ pub_key_bytes: bytes | None = None
296
+ state_dir = Path.home() / ".aevum"
297
+ ed25519_pub_path = state_dir / "ed25519.pub"
298
+ if ed25519_pub_path.exists():
299
+ pub_key_bytes = ed25519_pub_path.read_bytes()
300
+
301
+ if pub_key_bytes is None:
302
+ typer.echo(
303
+ "WARNING: no public key found in ~/.aevum/ed25519.pub — skipping signature check.",
304
+ err=True,
305
+ )
306
+ typer.echo("WARNING: receipt content is displayed but authenticity is NOT verified.", err=True)
307
+ verified = False
308
+ else:
309
+ try:
310
+ verify_key = nacl.signing.VerifyKey(pub_key_bytes)
311
+ verify_key.verify(digest, bytes(signature_bytes))
312
+ verified = True
313
+ except nacl.exceptions.BadSignatureError:
314
+ typer.echo("SIGNATURE INVALID", err=True)
315
+ _print_receipt_summary(receipt, unprotected, verified=False, store_info=store_info)
316
+ raise typer.Exit(code=1) from None
317
+ except Exception as exc: # noqa: BLE001
318
+ typer.echo(f"SIGNATURE INVALID: {exc}", err=True)
319
+ raise typer.Exit(code=1) from None
320
+
321
+ _print_receipt_summary(receipt, unprotected, verified=verified, store_info=store_info)
322
+
323
+
324
+ def _print_receipt_summary(
325
+ receipt: Any,
326
+ unprotected: dict, # type: ignore[type-arg]
327
+ verified: bool,
328
+ store_info: dict[str, object] | None = None,
329
+ ) -> None:
330
+ """Print human-readable receipt summary."""
331
+
332
+ status_line = "✓ Aevum Receipt Verified" if verified else "! Aevum Receipt (unverified)"
333
+ typer.echo(status_line)
334
+ typer.echo("─" * 36)
335
+
336
+ tsa_info = "none"
337
+ if 9 in unprotected:
338
+ tsa_info = f"<RFC 3161 token, {len(unprotected[9])} bytes>"
339
+
340
+ barriers_summary = (
341
+ ", ".join(f"{k}:{v}" for k, v in receipt.barrier_evaluations.items())
342
+ if receipt.barrier_evaluations
343
+ else "none"
344
+ )
345
+
346
+ prior_display = receipt.prior_hash[:12] if len(receipt.prior_hash) >= 12 else receipt.prior_hash
347
+ model_display = receipt.model_identity_hash[:12] if len(receipt.model_identity_hash) >= 12 else receipt.model_identity_hash
348
+
349
+ typer.echo(f"Action: {receipt.action}")
350
+ typer.echo(f"Agent: {receipt.agent_id}")
351
+ typer.echo(f"Principal: {receipt.principal}")
352
+ typer.echo(f"Occurred at: {receipt.occurred_at}")
353
+ typer.echo(f"Sequence: {receipt.sequence}")
354
+ typer.echo(f"Prior hash: {prior_display}...")
355
+ typer.echo(f"Handoff type: {receipt.handoff_type or 'none'}")
356
+ typer.echo(f"Model hash: {model_display}...")
357
+ typer.echo(f"Policy version: {receipt.policy_version}")
358
+ typer.echo(f"TSA timestamp: {tsa_info}")
359
+ typer.echo(f"Barriers: {barriers_summary}")
360
+
361
+ if store_info is not None:
362
+ tier = store_info.get("tier", "unknown")
363
+ locked = store_info.get("locked", False)
364
+ rekor_ref = store_info.get("rekor_entry_ref", "") or "not submitted"
365
+ typer.echo(f"Tier: {tier}")
366
+ typer.echo(f"Crash-protected:{' yes' if locked else ' no'}")
367
+ typer.echo(f"Rekor ref: {rekor_ref}")
368
+
369
+
202
370
  @app.command()
203
371
  def replay(
204
372
  session_id: Annotated[str, typer.Argument(help="Session ID to replay")],
@@ -1 +1,2 @@
1
+ # SPDX-License-Identifier: Apache-2.0
1
2
  """aevum.cli.commands -- sub-command modules."""
@@ -1,3 +1,4 @@
1
+ # SPDX-License-Identifier: Apache-2.0
1
2
  """
2
3
  aevum complication list/suspend/resume -- manage complications.
3
4
  """
@@ -1,3 +1,4 @@
1
+ # SPDX-License-Identifier: Apache-2.0
1
2
  """
2
3
  aevum conformance run -- run the conformance suite.
3
4
  """
@@ -1,3 +1,4 @@
1
+ # SPDX-License-Identifier: Apache-2.0
1
2
  """
2
3
  aevum server start -- start the Aevum HTTP API server.
3
4
  """
@@ -0,0 +1,121 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """
3
+ aevum store — manage graph store backends and receipt storage.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import Annotated
9
+
10
+ import typer
11
+
12
+ app = typer.Typer(help="Manage graph store backends and receipt storage.")
13
+
14
+
15
+ @app.command("migrate")
16
+ def migrate(
17
+ from_backend: Annotated[str, typer.Option("--from", help="Source backend (oxigraph:<path>)")] = "",
18
+ to_backend: Annotated[str, typer.Option("--to", help="Target backend (postgres:<dsn>)")] = "",
19
+ ) -> None:
20
+ """Migrate graph data between backends."""
21
+ if not from_backend or not to_backend:
22
+ typer.echo("Both --from and --to are required.", err=True)
23
+ raise typer.Exit(code=1)
24
+
25
+ if not from_backend.startswith("oxigraph:"):
26
+ typer.echo(f"Unsupported source backend: {from_backend!r}", err=True)
27
+ typer.echo("Currently supported source: oxigraph:<path>", err=True)
28
+ raise typer.Exit(code=1)
29
+
30
+ if not to_backend.startswith("postgres:"):
31
+ typer.echo(f"Unsupported target backend: {to_backend!r}", err=True)
32
+ typer.echo("Currently supported target: postgres:<dsn>", err=True)
33
+ raise typer.Exit(code=1)
34
+
35
+ try:
36
+ from aevum.store.postgres.migrate import migrate_from_oxigraph
37
+ except ImportError:
38
+ typer.echo("Error: aevum-store-postgres is not installed.", err=True)
39
+ raise typer.Exit(code=1) from None
40
+
41
+ oxigraph_path = from_backend[len("oxigraph:"):]
42
+ postgres_dsn = to_backend[len("postgres:"):]
43
+
44
+ typer.echo(f"Migrating: {oxigraph_path} -> PostgreSQL")
45
+ try:
46
+ import psycopg
47
+ conn = psycopg.connect(postgres_dsn)
48
+ migrated = migrate_from_oxigraph(oxigraph_path, conn) # type: ignore[arg-type] # Phase 2: wire store construction
49
+ typer.echo(f"Migration complete: {migrated} entities transferred.")
50
+ except Exception as e:
51
+ typer.echo(f"Migration failed: {e}", err=True)
52
+ raise typer.Exit(code=1) from e
53
+
54
+
55
+ @app.command("migrate-receipts")
56
+ def migrate_receipts(
57
+ oxigraph_path: Annotated[
58
+ str,
59
+ typer.Option("--oxigraph", help="Path to Oxigraph store directory"),
60
+ ] = "",
61
+ ) -> None:
62
+ """
63
+ Migrate receipt blobs from Oxigraph provenance graph to SQLite receipt store.
64
+
65
+ Checks for receipt blobs stored as xsd:base64Binary literals in the Oxigraph
66
+ provenance graph and moves them to the SQLite receipt store (AEVUM_RECEIPT_DB).
67
+
68
+ This is a one-time idempotent migration. For new deployments (no receipts in
69
+ Oxigraph), this is a no-op. Receipt blobs were never stored in Oxigraph in
70
+ versions <= 0.6.0, so this command is a no-op for all current deployments.
71
+ """
72
+ try:
73
+ from aevum.core.sqlite_store import SqliteReceiptStore
74
+ store = SqliteReceiptStore.from_env()
75
+ except RuntimeError as exc:
76
+ typer.echo(f"Store error: {exc}", err=True)
77
+ raise typer.Exit(code=1) from None
78
+
79
+ if not oxigraph_path:
80
+ typer.echo("No receipts to migrate. SQLite store is current.")
81
+ raise typer.Exit(code=0)
82
+
83
+ try:
84
+ from aevum.store.oxigraph.store import OxigraphStore
85
+ graph_store = OxigraphStore(path=oxigraph_path)
86
+ except ImportError:
87
+ typer.echo("Error: aevum-store-oxigraph is not installed.", err=True)
88
+ raise typer.Exit(code=1) from None
89
+
90
+ # Query for any receipt blobs stored as literals in the provenance graph.
91
+ # Receipt blobs were never stored in Oxigraph in v0.6.0 or earlier, so
92
+ # this query returns zero rows for all current deployments.
93
+ sparql = """
94
+ SELECT ?h ?b WHERE {
95
+ GRAPH <urn:aevum:provenance> {
96
+ ?h <https://aevum.build/vocab/receiptBlob> ?b
97
+ }
98
+ }
99
+ """
100
+ rows = graph_store.sparql_select(sparql)
101
+
102
+ if not rows:
103
+ typer.echo("No receipts to migrate. SQLite store is current.")
104
+ raise typer.Exit(code=0)
105
+
106
+ import base64
107
+ migrated = 0
108
+ for row in rows:
109
+ h = row.get("h", "")
110
+ b_str = row.get("b", "")
111
+ if not h or not b_str:
112
+ continue
113
+ try:
114
+ blob = base64.b64decode(b_str)
115
+ receipt_hash = h.split(":")[-1] if ":" in h else h
116
+ store.put(receipt_hash=receipt_hash, blob=blob)
117
+ migrated += 1
118
+ except Exception as exc: # noqa: BLE001
119
+ typer.echo(f" Skipped {h}: {exc}", err=True)
120
+
121
+ typer.echo(f"Migrated {migrated} receipts from Oxigraph to SQLite.")
@@ -1,3 +1,4 @@
1
+ # SPDX-License-Identifier: Apache-2.0
1
2
  """
2
3
  aevum version -- print installed package versions.
3
4
  """
@@ -1,3 +1,4 @@
1
+ # SPDX-License-Identifier: Apache-2.0
1
2
  """
2
3
  Tests for aevum-cli commands.
3
4
  Uses typer's CliRunner -- no real server, no real graph backend.
@@ -0,0 +1,143 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """
3
+ Tests for aevum verify-receipt CLI command.
4
+ Uses typer's CliRunner.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import re
9
+ from pathlib import Path
10
+
11
+ import cbor2
12
+ import nacl.signing
13
+ import pytest
14
+ from typer.testing import CliRunner
15
+
16
+ from aevum.cli.app import app
17
+
18
+ runner = CliRunner()
19
+
20
+ _ANSI = re.compile(r"\x1b\[[0-9;]*[mGKH]")
21
+
22
+
23
+ def plain(text: str) -> str:
24
+ return _ANSI.sub("", text)
25
+
26
+
27
+ def _make_valid_receipt_file(tmp_path: Path) -> tuple[Path, nacl.signing.SigningKey]:
28
+ """Create a valid COSE_Sign1 receipt file and return its path + signing key."""
29
+ from aevum.core.audit.event import AuditEvent
30
+ from aevum.core.receipt import AevumReceipt
31
+ from aevum.publish.encoder import ReceiptEncoder
32
+
33
+ signer_key = nacl.signing.SigningKey.generate()
34
+
35
+ class _NaClSigner:
36
+ key_id = "test-verify-key"
37
+ provenance = "test"
38
+ def sign(self, digest: bytes) -> bytes:
39
+ return bytes(signer_key.sign(digest).signature)
40
+ def public_key_bytes(self) -> bytes:
41
+ return bytes(signer_key.verify_key)
42
+
43
+ encoder = ReceiptEncoder(signer=_NaClSigner(), dev_mode=True) # type: ignore[arg-type]
44
+ event = AuditEvent(
45
+ event_id="ev-cli-test-01",
46
+ episode_id="ep-cli-01",
47
+ sequence=1,
48
+ event_type="cli.test.action",
49
+ schema_version="1.0",
50
+ valid_from="2026-05-24T00:00:00+00:00",
51
+ valid_to=None,
52
+ system_time=0,
53
+ causation_id=None,
54
+ correlation_id=None,
55
+ actor="cli-test-agent",
56
+ trace_id=None,
57
+ span_id=None,
58
+ payload={},
59
+ payload_hash=AuditEvent.hash_payload({}),
60
+ prior_hash="c" * 64,
61
+ signature="dGVzdA",
62
+ signer_key_id="test-key",
63
+ )
64
+ receipt = AevumReceipt.from_sigchain_event(event)
65
+ receipt_cbor = encoder.encode(receipt)
66
+
67
+ receipt_file = tmp_path / "test_receipt.cose"
68
+ receipt_file.write_bytes(receipt_cbor)
69
+
70
+ pub_key_path = tmp_path / ".aevum" / "ed25519.pub"
71
+ pub_key_path.parent.mkdir(parents=True, exist_ok=True)
72
+ pub_key_path.write_bytes(bytes(signer_key.verify_key))
73
+
74
+ return receipt_file, signer_key
75
+
76
+
77
+ def test_verify_receipt_help() -> None:
78
+ result = runner.invoke(app, ["verify-receipt", "--help"])
79
+ assert result.exit_code == 0
80
+ assert "receipt" in plain(result.output).lower()
81
+
82
+
83
+ def test_verify_receipt_file_not_found(tmp_path: Path) -> None:
84
+ result = runner.invoke(app, ["verify-receipt", str(tmp_path / "nonexistent.cose")])
85
+ assert result.exit_code == 1
86
+ assert "not found" in plain(result.output + (result.stdout or "")).lower()
87
+
88
+
89
+ def test_verify_receipt_invalid_cbor(tmp_path: Path) -> None:
90
+ bad_file = tmp_path / "bad.cose"
91
+ bad_file.write_bytes(b"\xff\xff\xff not valid cbor")
92
+ result = runner.invoke(app, ["verify-receipt", str(bad_file)])
93
+ assert result.exit_code == 1
94
+
95
+
96
+ def test_verify_receipt_wrong_algorithm(tmp_path: Path) -> None:
97
+ protected_bstr = cbor2.dumps({1: -7, 3: "application/aevum-receipt+cbor", 4: b"kid"})
98
+ payload = cbor2.dumps({"action": "test"})
99
+ cose = [protected_bstr, {}, payload, b"sig"]
100
+ bad_file = tmp_path / "wrong_alg.cose"
101
+ bad_file.write_bytes(cbor2.dumps(cose))
102
+ result = runner.invoke(app, ["verify-receipt", str(bad_file)])
103
+ assert result.exit_code == 2
104
+ assert "UNSUPPORTED ALGORITHM" in plain(result.output + (result.stdout or ""))
105
+
106
+
107
+ def test_verify_receipt_no_public_key_shows_warning(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
108
+ from aevum.core.audit.event import AuditEvent
109
+ from aevum.core.audit.signer import InProcessSigner
110
+ from aevum.core.receipt import AevumReceipt
111
+ from aevum.publish.encoder import ReceiptEncoder
112
+
113
+ monkeypatch.setenv("HOME", str(tmp_path))
114
+ signer = InProcessSigner()
115
+ encoder = ReceiptEncoder(signer=signer, dev_mode=True)
116
+ event = AuditEvent(
117
+ event_id="ev-warn-01",
118
+ episode_id="ep-warn-01",
119
+ sequence=1,
120
+ event_type="warn.test",
121
+ schema_version="1.0",
122
+ valid_from="2026-05-24T00:00:00+00:00",
123
+ valid_to=None,
124
+ system_time=0,
125
+ causation_id=None,
126
+ correlation_id=None,
127
+ actor="warn-agent",
128
+ trace_id=None,
129
+ span_id=None,
130
+ payload={},
131
+ payload_hash=AuditEvent.hash_payload({}),
132
+ prior_hash="d" * 64,
133
+ signature="dGVzdA",
134
+ signer_key_id="test-key",
135
+ )
136
+ receipt = AevumReceipt.from_sigchain_event(event)
137
+ receipt_cbor = encoder.encode(receipt)
138
+ receipt_file = tmp_path / "no_key_receipt.cose"
139
+ receipt_file.write_bytes(receipt_cbor)
140
+
141
+ result = runner.invoke(app, ["verify-receipt", str(receipt_file)])
142
+ output = plain(result.output + (result.stdout or ""))
143
+ assert "WARNING" in output or "unverified" in output.lower()
@@ -1,51 +0,0 @@
1
- """
2
- aevum store migrate -- migrate between graph backends.
3
- """
4
-
5
- from __future__ import annotations
6
-
7
- from typing import Annotated
8
-
9
- import typer
10
-
11
- app = typer.Typer(help="Manage graph store backends.")
12
-
13
-
14
- @app.command("migrate")
15
- def migrate(
16
- from_backend: Annotated[str, typer.Option("--from", help="Source backend (oxigraph:<path>)")] = "",
17
- to_backend: Annotated[str, typer.Option("--to", help="Target backend (postgres:<dsn>)")] = "",
18
- ) -> None:
19
- """Migrate graph data between backends."""
20
- if not from_backend or not to_backend:
21
- typer.echo("Both --from and --to are required.", err=True)
22
- raise typer.Exit(code=1)
23
-
24
- if not from_backend.startswith("oxigraph:"):
25
- typer.echo(f"Unsupported source backend: {from_backend!r}", err=True)
26
- typer.echo("Currently supported source: oxigraph:<path>", err=True)
27
- raise typer.Exit(code=1)
28
-
29
- if not to_backend.startswith("postgres:"):
30
- typer.echo(f"Unsupported target backend: {to_backend!r}", err=True)
31
- typer.echo("Currently supported target: postgres:<dsn>", err=True)
32
- raise typer.Exit(code=1)
33
-
34
- try:
35
- from aevum.store.postgres.migrate import migrate_from_oxigraph
36
- except ImportError:
37
- typer.echo("Error: aevum-store-postgres is not installed.", err=True)
38
- raise typer.Exit(code=1) from None
39
-
40
- oxigraph_path = from_backend[len("oxigraph:"):]
41
- postgres_dsn = to_backend[len("postgres:"):]
42
-
43
- typer.echo(f"Migrating: {oxigraph_path} -> PostgreSQL")
44
- try:
45
- import psycopg
46
- conn = psycopg.connect(postgres_dsn)
47
- migrated = migrate_from_oxigraph(oxigraph_path, conn) # type: ignore[arg-type] # Phase 2: wire store construction
48
- typer.echo(f"Migration complete: {migrated} entities transferred.")
49
- except Exception as e:
50
- typer.echo(f"Migration failed: {e}", err=True)
51
- raise typer.Exit(code=1) from e
File without changes
File without changes