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/__init__.py +6 -0
- pcf_toolkit/cli.py +738 -0
- pcf_toolkit/cli_helpers.py +62 -0
- pcf_toolkit/data/__init__.py +1 -0
- pcf_toolkit/data/manifest.schema.json +1097 -0
- pcf_toolkit/data/schema_snapshot.json +2377 -0
- pcf_toolkit/data/spec_raw.json +2877 -0
- pcf_toolkit/io.py +65 -0
- pcf_toolkit/json_schema.py +30 -0
- pcf_toolkit/models.py +384 -0
- pcf_toolkit/proxy/__init__.py +1 -0
- pcf_toolkit/proxy/addons/__init__.py +1 -0
- pcf_toolkit/proxy/addons/redirect_bundle.py +70 -0
- pcf_toolkit/proxy/browser.py +157 -0
- pcf_toolkit/proxy/cli.py +1570 -0
- pcf_toolkit/proxy/config.py +310 -0
- pcf_toolkit/proxy/doctor.py +279 -0
- pcf_toolkit/proxy/mitm.py +206 -0
- pcf_toolkit/proxy/server.py +50 -0
- pcf_toolkit/py.typed +1 -0
- pcf_toolkit/rich_help.py +173 -0
- pcf_toolkit/schema_snapshot.py +47 -0
- pcf_toolkit/types.py +95 -0
- pcf_toolkit/xml.py +484 -0
- pcf_toolkit/xml_import.py +548 -0
- pcf_toolkit-0.2.5.dist-info/METADATA +494 -0
- pcf_toolkit-0.2.5.dist-info/RECORD +31 -0
- pcf_toolkit-0.2.5.dist-info/WHEEL +5 -0
- pcf_toolkit-0.2.5.dist-info/entry_points.txt +2 -0
- pcf_toolkit-0.2.5.dist-info/licenses/LICENSE.md +183 -0
- pcf_toolkit-0.2.5.dist-info/top_level.txt +1 -0
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)
|