pcf-toolkit 0.2.5__py3-none-any.whl

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.
pcf_toolkit/cli.py ADDED
@@ -0,0 +1,738 @@
1
+ """CLI entrypoints for the PCF toolkit."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import sys
7
+ import textwrap
8
+ from importlib import metadata
9
+ from pathlib import Path
10
+
11
+ import typer
12
+ import yaml
13
+ from pydantic import ValidationError
14
+
15
+ from pcf_toolkit.cli_helpers import render_validation_table, rich_console
16
+ from pcf_toolkit.io import load_manifest
17
+ from pcf_toolkit.json_schema import manifest_schema_text
18
+ from pcf_toolkit.proxy.cli import app as proxy_app
19
+ from pcf_toolkit.rich_help import RichTyperCommand, RichTyperGroup
20
+ from pcf_toolkit.schema_snapshot import load_schema_snapshot
21
+ from pcf_toolkit.xml import ManifestXmlSerializer
22
+ from pcf_toolkit.xml_import import parse_manifest_xml_path
23
+
24
+ APP_HELP = textwrap.dedent(
25
+ """
26
+ [bold]PCF toolkit[/bold]
27
+
28
+ Author [i]ControlManifest.Input.xml[/i] as code.
29
+ Validate with strong typing.
30
+ Generate deterministic XML.
31
+ Proxy PCF webresources for local dev.
32
+ """
33
+ ).strip()
34
+
35
+ APP_EPILOG = textwrap.dedent(
36
+ """
37
+ [bold cyan]Examples[/bold cyan]
38
+
39
+ - pcf-toolkit validate manifest.yaml
40
+
41
+ - pcf-toolkit generate manifest.yaml -o ControlManifest.Input.xml
42
+
43
+ - pcf-toolkit proxy start MyComponent
44
+
45
+ - pcf-toolkit export-json-schema -o schemas/pcf-manifest.schema.json
46
+
47
+ [bold cyan]Tips[/bold cyan]
48
+
49
+ - Install shell completion: pcf-toolkit --install-completion
50
+
51
+ - Use YAML schema validation:
52
+
53
+ # yaml-language-server: $schema=https://raw.githubusercontent.com/vectorfy-co/pcf-toolkit/refs/heads/main/schemas/pcf-manifest.schema.json
54
+ """
55
+ ).strip()
56
+
57
+
58
+ def _version_callback(value: bool) -> None:
59
+ """Callback for --version option that prints version and exits.
60
+
61
+ Args:
62
+ value: Whether the version flag was set.
63
+ """
64
+ if not value:
65
+ return
66
+ version = metadata.version("pcf-toolkit")
67
+ typer.echo(f"pcf-toolkit {version}")
68
+ raise typer.Exit()
69
+
70
+
71
+ def _validate_manifest_path(value: str) -> str:
72
+ """Validates that a manifest path exists and is a file.
73
+
74
+ Args:
75
+ value: Path to validate, or "-" for stdin.
76
+
77
+ Returns:
78
+ The validated path string.
79
+
80
+ Raises:
81
+ typer.BadParameter: If the path doesn't exist or is a directory.
82
+ """
83
+ if value == "-":
84
+ return value
85
+ path = Path(value)
86
+ if not path.exists():
87
+ raise typer.BadParameter("File does not exist.")
88
+ if path.is_dir():
89
+ raise typer.BadParameter("Expected a file path, got a directory.")
90
+ return value
91
+
92
+
93
+ def _validate_xml_path(value: str) -> str:
94
+ """Validates that an XML path exists, is a file, and has .xml extension.
95
+
96
+ Args:
97
+ value: Path to validate, or "-" for stdin.
98
+
99
+ Returns:
100
+ The validated path string.
101
+
102
+ Raises:
103
+ typer.BadParameter: If the path doesn't exist, is a directory, or lacks
104
+ .xml extension.
105
+ """
106
+ if value == "-":
107
+ return value
108
+ path = Path(value)
109
+ if not path.exists():
110
+ raise typer.BadParameter("File does not exist.")
111
+ if path.is_dir():
112
+ raise typer.BadParameter("Expected a file path, got a directory.")
113
+ if path.suffix.lower() != ".xml":
114
+ raise typer.BadParameter("Expected a .xml file.")
115
+ return value
116
+
117
+
118
+ def _autocomplete_xml_path(ctx: typer.Context, args: list[str], incomplete: str) -> list[str]:
119
+ """Provides autocomplete suggestions for XML file paths.
120
+
121
+ Args:
122
+ ctx: Typer context (unused).
123
+ args: Command arguments (unused).
124
+ incomplete: The incomplete path string to complete.
125
+
126
+ Returns:
127
+ List of completion candidates (XML files or directories).
128
+ """
129
+ if incomplete == "-":
130
+ return ["-"]
131
+ if incomplete.startswith("-"):
132
+ return []
133
+ if incomplete in {"", "."}:
134
+ base = Path(".")
135
+ prefix = ""
136
+ elif incomplete.endswith(("/", "\\")):
137
+ base = Path(incomplete)
138
+ prefix = ""
139
+ else:
140
+ path = Path(incomplete)
141
+ base = path.parent if str(path.parent) else Path(".")
142
+ prefix = path.name
143
+ if not base.exists():
144
+ return []
145
+ completions: list[str] = []
146
+ try:
147
+ for child in base.iterdir():
148
+ if not child.name.startswith(prefix):
149
+ continue
150
+ if child.is_dir():
151
+ completions.append(f"{child}/")
152
+ elif child.is_file() and child.suffix.lower() == ".xml":
153
+ completions.append(str(child))
154
+ except PermissionError:
155
+ return []
156
+ return completions
157
+
158
+
159
+ def _render_validation_error(exc: ValidationError) -> None:
160
+ """Renders Pydantic validation errors in a user-friendly format.
161
+
162
+ Args:
163
+ exc: The ValidationError exception to render.
164
+ """
165
+ typer.secho("Manifest is invalid.", fg=typer.colors.RED, bold=True, err=True)
166
+ errors = exc.errors()
167
+ if render_validation_table(errors, title="Validation Errors", stderr=True):
168
+ return
169
+ for error in errors:
170
+ loc = ".".join(str(part) for part in error.get("loc", [])) or "<root>"
171
+ msg = error.get("msg", "Invalid value")
172
+ error_type = error.get("type", "validation_error")
173
+ typer.echo(f" - {loc}: {msg} ({error_type})", err=True)
174
+ typer.echo(
175
+ "Tip: run 'pcf-toolkit export-json-schema' and use the schema in your editor.",
176
+ err=True,
177
+ )
178
+
179
+
180
+ app = typer.Typer(
181
+ name="pcf-toolkit",
182
+ cls=RichTyperGroup,
183
+ help=APP_HELP,
184
+ epilog=APP_EPILOG,
185
+ short_help="PCF toolkit for manifests and local proxy workflows.",
186
+ no_args_is_help=True,
187
+ rich_markup_mode="rich",
188
+ pretty_exceptions_enable=True,
189
+ pretty_exceptions_show_locals=False,
190
+ suggest_commands=True,
191
+ context_settings={"help_option_names": ["-h", "--help"]},
192
+ )
193
+
194
+ app.add_typer(
195
+ proxy_app,
196
+ name="proxy",
197
+ help="Local dev proxy for PCF components.",
198
+ )
199
+
200
+
201
+ @app.callback()
202
+ def main(
203
+ version: bool = typer.Option(
204
+ False,
205
+ "--version",
206
+ help="Show the installed version and exit.",
207
+ is_eager=True,
208
+ callback=_version_callback,
209
+ rich_help_panel="Global options",
210
+ ),
211
+ ) -> None:
212
+ """Main CLI entrypoint for the PCF toolkit."""
213
+
214
+
215
+ VALIDATE_HELP = textwrap.dedent(
216
+ """
217
+ Validate a manifest definition against the PCF schema.
218
+
219
+ Supports YAML and JSON. Use '-' to read from stdin.
220
+ """
221
+ ).strip()
222
+
223
+ VALIDATE_EPILOG = textwrap.dedent(
224
+ """
225
+ - pcf-toolkit validate manifest.yaml
226
+
227
+ - cat manifest.yaml | pcf-toolkit validate -
228
+ """
229
+ ).strip()
230
+
231
+
232
+ @app.command(
233
+ "validate",
234
+ cls=RichTyperCommand,
235
+ help=VALIDATE_HELP,
236
+ epilog=VALIDATE_EPILOG,
237
+ rich_help_panel="Core commands",
238
+ )
239
+ def validate_manifest(
240
+ path: str = typer.Argument(
241
+ ...,
242
+ metavar="MANIFEST",
243
+ help="Path to a YAML/JSON manifest definition, or '-' for stdin.",
244
+ allow_dash=True,
245
+ callback=_validate_manifest_path,
246
+ ),
247
+ ) -> None:
248
+ """Validates a manifest definition against the PCF schema.
249
+
250
+ Args:
251
+ path: Path to a YAML/JSON manifest file, or '-' to read from stdin.
252
+
253
+ Raises:
254
+ typer.Exit: Exit code 1 if validation fails.
255
+ """
256
+ try:
257
+ load_manifest(path)
258
+ except ValidationError as exc:
259
+ _render_validation_error(exc)
260
+ raise typer.Exit(code=1) from exc
261
+ typer.secho("Manifest is valid.", fg=typer.colors.GREEN, bold=True)
262
+
263
+
264
+ GENERATE_HELP = textwrap.dedent(
265
+ """
266
+ Generate ControlManifest.Input.xml from a manifest definition.
267
+
268
+ Supports YAML and JSON. Use '-' to read from stdin.
269
+ """
270
+ ).strip()
271
+
272
+ GENERATE_EPILOG = textwrap.dedent(
273
+ """
274
+ - pcf-toolkit generate manifest.yaml -o ControlManifest.Input.xml
275
+
276
+ - cat manifest.yaml | pcf-toolkit generate -
277
+ """
278
+ ).strip()
279
+
280
+
281
+ @app.command(
282
+ "generate",
283
+ cls=RichTyperCommand,
284
+ help=GENERATE_HELP,
285
+ epilog=GENERATE_EPILOG,
286
+ rich_help_panel="Core commands",
287
+ )
288
+ def generate_manifest(
289
+ path: str = typer.Argument(
290
+ ...,
291
+ metavar="MANIFEST",
292
+ help="Path to a YAML/JSON manifest definition, or '-' for stdin.",
293
+ allow_dash=True,
294
+ callback=_validate_manifest_path,
295
+ ),
296
+ output: Path | None = typer.Option(
297
+ None,
298
+ "--output",
299
+ "-o",
300
+ metavar="FILE",
301
+ help="Write XML to a file instead of stdout.",
302
+ dir_okay=False,
303
+ writable=True,
304
+ rich_help_panel="Output",
305
+ ),
306
+ no_declaration: bool = typer.Option(
307
+ False,
308
+ "--no-declaration",
309
+ help="Omit the XML declaration header.",
310
+ rich_help_panel="Output",
311
+ ),
312
+ ) -> None:
313
+ """Generates ControlManifest.Input.xml from a manifest definition.
314
+
315
+ Args:
316
+ path: Path to a YAML/JSON manifest file, or '-' to read from stdin.
317
+ output: Optional file path to write XML to. If not provided, writes to
318
+ stdout.
319
+ no_declaration: If True, omits the XML declaration header.
320
+ """
321
+ manifest = load_manifest(path)
322
+ serializer = ManifestXmlSerializer(xml_declaration=not no_declaration)
323
+ xml_text = serializer.to_string(manifest)
324
+ if output:
325
+ output.write_text(xml_text, encoding="utf-8")
326
+ typer.secho(f"Wrote {output}", fg=typer.colors.GREEN, bold=True)
327
+ return
328
+ typer.echo(xml_text)
329
+
330
+
331
+ IMPORT_XML_HELP = textwrap.dedent(
332
+ """
333
+ Convert an existing ControlManifest.Input.xml into YAML or JSON.
334
+
335
+ The output matches the manifest schema so you can keep using YAML from now on.
336
+ """
337
+ ).strip()
338
+
339
+ IMPORT_XML_EPILOG = textwrap.dedent(
340
+ """
341
+ - pcf-toolkit import-xml ControlManifest.Input.xml -o manifest.yaml
342
+
343
+ - pcf-toolkit import-xml ControlManifest.Input.xml --format json
344
+ """
345
+ ).strip()
346
+
347
+
348
+ @app.command(
349
+ "import-xml",
350
+ cls=RichTyperCommand,
351
+ help=IMPORT_XML_HELP,
352
+ epilog=IMPORT_XML_EPILOG,
353
+ rich_help_panel="Core commands",
354
+ )
355
+ def import_xml(
356
+ path: str = typer.Argument(
357
+ ...,
358
+ metavar="XML",
359
+ help="Path to ControlManifest.Input.xml (.xml), or '-' for stdin.",
360
+ allow_dash=True,
361
+ callback=_validate_xml_path,
362
+ autocompletion=_autocomplete_xml_path,
363
+ ),
364
+ output: Path | None = typer.Option(
365
+ None,
366
+ "--output",
367
+ "-o",
368
+ metavar="FILE",
369
+ help="Write YAML/JSON to a file instead of stdout.",
370
+ dir_okay=False,
371
+ writable=True,
372
+ rich_help_panel="Output",
373
+ ),
374
+ output_format: str = typer.Option(
375
+ "yaml",
376
+ "--format",
377
+ help="Output format: yaml or json.",
378
+ rich_help_panel="Output",
379
+ ),
380
+ schema_directive: bool = typer.Option(
381
+ True,
382
+ "--schema-directive/--no-schema-directive",
383
+ help="Include schema header when outputting YAML/JSON.",
384
+ rich_help_panel="Output",
385
+ ),
386
+ schema_path: str = typer.Option(
387
+ "https://raw.githubusercontent.com/vectorfy-co/pcf-toolkit/refs/heads/main/schemas/pcf-manifest.schema.json",
388
+ "--schema-path",
389
+ help="Schema path used in the YAML/JSON output.",
390
+ rich_help_panel="Output",
391
+ ),
392
+ validate: bool = typer.Option(
393
+ True,
394
+ "--validate/--no-validate",
395
+ help="Validate the imported manifest against the schema.",
396
+ rich_help_panel="Validation",
397
+ ),
398
+ ) -> None:
399
+ """Converts ControlManifest.Input.xml into YAML or JSON format.
400
+
401
+ Args:
402
+ path: Path to ControlManifest.Input.xml file, or '-' to read from stdin.
403
+ output: Optional file path to write output to. If not provided, writes to
404
+ stdout.
405
+ output_format: Output format, either 'yaml' or 'json'.
406
+ schema_directive: If True, includes schema directive in output.
407
+ schema_path: URL or path to schema file for the directive.
408
+ validate: If True, validates the imported manifest against the schema.
409
+
410
+ Raises:
411
+ typer.BadParameter: If output_format is not 'yaml' or 'json'.
412
+ typer.Exit: Exit code 1 if import or validation fails.
413
+ """
414
+ try:
415
+ raw = parse_manifest_xml_path(path)
416
+ except ValueError as exc:
417
+ typer.secho(f"Import failed: {exc}", fg=typer.colors.RED, bold=True, err=True)
418
+ typer.echo(
419
+ "Tip: pass a valid ControlManifest.Input.xml file, or use '-' for stdin.",
420
+ err=True,
421
+ )
422
+ raise typer.Exit(code=1) from exc
423
+ except Exception as exc: # pragma: no cover - defensive: unexpected parser issues
424
+ typer.secho(
425
+ f"Import failed due to an unexpected error: {exc}",
426
+ fg=typer.colors.RED,
427
+ bold=True,
428
+ err=True,
429
+ )
430
+ typer.echo("Tip: try --format json to inspect the parsed structure.", err=True)
431
+ raise typer.Exit(code=1) from exc
432
+
433
+ if validate:
434
+ from pcf_toolkit.models import Manifest
435
+
436
+ try:
437
+ manifest = Manifest.model_validate(raw)
438
+ except ValidationError as exc:
439
+ _render_validation_error(exc)
440
+ raise typer.Exit(code=1) from exc
441
+ data = manifest.model_dump(
442
+ by_alias=True,
443
+ exclude_none=True,
444
+ exclude_defaults=True,
445
+ mode="json",
446
+ )
447
+ else:
448
+ data = raw
449
+
450
+ output_format = output_format.lower()
451
+ if output_format not in {"yaml", "json"}:
452
+ raise typer.BadParameter("format must be 'yaml' or 'json'")
453
+
454
+ if output_format == "json":
455
+ if schema_directive and "$schema" not in data:
456
+ data = {"$schema": schema_path, **data}
457
+ text = json.dumps(data, indent=2, ensure_ascii=True)
458
+ else:
459
+ try:
460
+ yaml_text = yaml.safe_dump(
461
+ data,
462
+ sort_keys=False,
463
+ default_flow_style=False,
464
+ allow_unicode=False,
465
+ )
466
+ except Exception as exc: # pragma: no cover - safe dump should be robust
467
+ typer.secho(
468
+ f"Failed to render YAML output: {exc}",
469
+ fg=typer.colors.RED,
470
+ bold=True,
471
+ err=True,
472
+ )
473
+ typer.echo(
474
+ "Tip: try --format json to inspect the parsed structure.",
475
+ err=True,
476
+ )
477
+ raise typer.Exit(code=1) from exc
478
+ if schema_directive:
479
+ yaml_text = f"# yaml-language-server: $schema={schema_path}\n{yaml_text}"
480
+ text = yaml_text
481
+
482
+ if output:
483
+ output.write_text(text, encoding="utf-8")
484
+ typer.secho(f"Wrote {output}", fg=typer.colors.GREEN, bold=True)
485
+ return
486
+ typer.echo(text)
487
+
488
+
489
+ EXPORT_SCHEMA_HELP = textwrap.dedent(
490
+ """
491
+ Export the docs-derived schema snapshot used for model generation.
492
+ """
493
+ ).strip()
494
+
495
+ EXPORT_SCHEMA_EPILOG = textwrap.dedent(
496
+ """
497
+ - pcf-toolkit export-schema -o data/schema_snapshot.json
498
+ """
499
+ ).strip()
500
+
501
+
502
+ @app.command(
503
+ "export-schema",
504
+ cls=RichTyperCommand,
505
+ help=EXPORT_SCHEMA_HELP,
506
+ epilog=EXPORT_SCHEMA_EPILOG,
507
+ rich_help_panel="Schema tools",
508
+ )
509
+ def export_schema(
510
+ output: Path | None = typer.Option(
511
+ None,
512
+ "--output",
513
+ "-o",
514
+ metavar="FILE",
515
+ help="Write snapshot JSON to a file instead of stdout.",
516
+ dir_okay=False,
517
+ writable=True,
518
+ ),
519
+ ) -> None:
520
+ """Exports the machine-readable schema snapshot JSON.
521
+
522
+ Args:
523
+ output: Optional file path to write schema to. If not provided, writes to
524
+ stdout.
525
+ """
526
+ snapshot = load_schema_snapshot()
527
+ if output:
528
+ output.write_text(snapshot, encoding="utf-8")
529
+ typer.secho(f"Wrote {output}", fg=typer.colors.GREEN, bold=True)
530
+ return
531
+ typer.echo(snapshot)
532
+
533
+
534
+ EXPORT_JSON_SCHEMA_HELP = textwrap.dedent(
535
+ """
536
+ Export JSON Schema for YAML/JSON validation.
537
+ """
538
+ ).strip()
539
+
540
+ EXPORT_JSON_SCHEMA_EPILOG = textwrap.dedent(
541
+ """
542
+ - pcf-toolkit export-json-schema -o schemas/pcf-manifest.schema.json
543
+ """
544
+ ).strip()
545
+
546
+
547
+ @app.command(
548
+ "export-json-schema",
549
+ cls=RichTyperCommand,
550
+ help=EXPORT_JSON_SCHEMA_HELP,
551
+ epilog=EXPORT_JSON_SCHEMA_EPILOG,
552
+ rich_help_panel="Schema tools",
553
+ )
554
+ def export_json_schema(
555
+ output: Path | None = typer.Option(
556
+ None,
557
+ "--output",
558
+ "-o",
559
+ metavar="FILE",
560
+ help="Write JSON Schema to a file instead of stdout.",
561
+ dir_okay=False,
562
+ writable=True,
563
+ ),
564
+ ) -> None:
565
+ """Exports JSON Schema for manifest definitions.
566
+
567
+ Args:
568
+ output: Optional file path to write schema to. If not provided, writes to
569
+ stdout.
570
+ """
571
+ schema_text = manifest_schema_text()
572
+ if output:
573
+ output.write_text(schema_text, encoding="utf-8")
574
+ typer.secho(f"Wrote {output}", fg=typer.colors.GREEN, bold=True)
575
+ return
576
+ typer.echo(schema_text)
577
+
578
+
579
+ EXAMPLES_HELP = textwrap.dedent(
580
+ """
581
+ Show end-to-end examples for authoring and generating a manifest.
582
+ """
583
+ ).strip()
584
+
585
+
586
+ @app.command(
587
+ "examples",
588
+ cls=RichTyperCommand,
589
+ help=EXAMPLES_HELP,
590
+ rich_help_panel="Developer tools",
591
+ )
592
+ def show_examples() -> None:
593
+ """Prints curated examples for quick start.
594
+
595
+ Displays markdown-formatted examples showing common usage patterns.
596
+ """
597
+ console = rich_console(stderr=False)
598
+ if console is None:
599
+ typer.echo("Examples are best viewed with Rich enabled. Install extra dependencies.")
600
+ return
601
+
602
+ from rich.markdown import Markdown
603
+ from rich.panel import Panel
604
+
605
+ markdown = textwrap.dedent(
606
+ """
607
+ ## Validate a manifest
608
+
609
+ ```bash
610
+ pcf-toolkit validate manifest.yaml
611
+ ```
612
+
613
+ ## Generate XML
614
+
615
+ ```bash
616
+ pcf-toolkit generate manifest.yaml -o ControlManifest.Input.xml
617
+ ```
618
+
619
+ ## Import XML to YAML
620
+
621
+ ```bash
622
+ pcf-toolkit import-xml ControlManifest.Input.xml -o manifest.yaml
623
+ ```
624
+
625
+ ## Add YAML schema validation
626
+
627
+ ```yaml
628
+ # yaml-language-server: $schema=https://raw.githubusercontent.com/vectorfy-co/pcf-toolkit/refs/heads/main/schemas/pcf-manifest.schema.json
629
+ ```
630
+
631
+ ## Minimal YAML
632
+
633
+ ```yaml
634
+ control:
635
+ namespace: MyNameSpace
636
+ constructor: MyControl
637
+ version: 1.0.0
638
+ display-name-key: MyControl_Display_Key
639
+ resources:
640
+ code:
641
+ path: index.ts
642
+ order: 1
643
+ ```
644
+ """
645
+ ).strip()
646
+ panel = Panel(Markdown(markdown), title="Examples", title_align="left")
647
+ console.print(panel)
648
+
649
+
650
+ DOCTOR_HELP = textwrap.dedent(
651
+ """
652
+ Check your environment and repository setup.
653
+ """
654
+ ).strip()
655
+
656
+
657
+ @app.command(
658
+ "doctor",
659
+ cls=RichTyperCommand,
660
+ help=DOCTOR_HELP,
661
+ rich_help_panel="Developer tools",
662
+ )
663
+ def doctor(
664
+ strict: bool = typer.Option(
665
+ False,
666
+ "--strict",
667
+ help="Return non-zero exit code on warnings.",
668
+ ),
669
+ ) -> None:
670
+ """Inspects environment for common issues.
671
+
672
+ Checks Python version, schema files, and other prerequisites.
673
+
674
+ Args:
675
+ strict: If True, returns non-zero exit code on warnings.
676
+
677
+ Raises:
678
+ typer.Exit: Exit code 1 if issues found (or warnings in strict mode).
679
+ """
680
+ console = rich_console(stderr=False)
681
+ issues = 0
682
+ warnings = 0
683
+
684
+ def add_check(name: str, status: str, detail: str) -> None:
685
+ nonlocal issues, warnings
686
+ if status == "FAIL":
687
+ issues += 1
688
+ elif status == "WARN":
689
+ warnings += 1
690
+ if console is None:
691
+ typer.echo(f"{status}: {name} - {detail}")
692
+ return
693
+ from rich.table import Table
694
+
695
+ if not hasattr(add_check, "_table"):
696
+ table = Table(title="Doctor", show_lines=False)
697
+ table.add_column("Check", style="bold")
698
+ table.add_column("Status")
699
+ table.add_column("Details")
700
+ add_check._table = table
701
+ table = add_check._table
702
+ style = {"OK": "green", "WARN": "yellow", "FAIL": "red"}.get(status, "white")
703
+ table.add_row(name, f"[{style}]{status}[/{style}]", detail)
704
+
705
+ # Python version
706
+ if sys.version_info < (3, 13):
707
+ add_check("Python", "FAIL", "Requires Python 3.13+")
708
+ else:
709
+ add_check("Python", "OK", f"{sys.version_info.major}.{sys.version_info.minor}")
710
+
711
+ # Schema files
712
+ schema_path = Path("schemas/pcf-manifest.schema.json")
713
+ if schema_path.exists():
714
+ add_check("JSON Schema", "OK", str(schema_path))
715
+ try:
716
+ json.loads(schema_path.read_text(encoding="utf-8"))
717
+ except json.JSONDecodeError:
718
+ add_check("JSON Schema Parse", "FAIL", "Invalid JSON")
719
+ else:
720
+ add_check("JSON Schema", "WARN", "schemas/pcf-manifest.schema.json missing")
721
+
722
+ snapshot_path = Path("data/schema_snapshot.json")
723
+ if snapshot_path.exists():
724
+ add_check("Schema snapshot", "OK", str(snapshot_path))
725
+ else:
726
+ add_check("Schema snapshot", "WARN", "data/schema_snapshot.json missing")
727
+
728
+ packaged_schema = Path("src/pcf_toolkit/data/manifest.schema.json")
729
+ if packaged_schema.exists():
730
+ add_check("Packaged schema", "OK", str(packaged_schema))
731
+ else:
732
+ add_check("Packaged schema", "WARN", "Package schema missing")
733
+
734
+ if console is not None and hasattr(add_check, "_table"):
735
+ console.print(add_check._table)
736
+
737
+ if issues or (strict and warnings):
738
+ raise typer.Exit(code=1)