pptx-cli 1.0.0__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.
pptx_cli/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """pptx_cli package."""
2
+
3
+ __all__ = ["__version__"]
4
+
5
+ __version__ = "1.0.0"
pptx_cli/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ from .cli import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
pptx_cli/cli.py ADDED
@@ -0,0 +1,372 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
5
+ from typing import Annotated, Any
6
+
7
+ import typer
8
+ import yaml
9
+
10
+ from pptx_cli.commands.compose import deck_build, slide_create
11
+ from pptx_cli.commands.guide import build_guide_document
12
+ from pptx_cli.commands.init import run_init
13
+ from pptx_cli.commands.inspect import (
14
+ doctor,
15
+ list_assets,
16
+ list_layouts,
17
+ list_placeholders,
18
+ show_layout,
19
+ show_theme,
20
+ )
21
+ from pptx_cli.commands.manifest_ops import manifest_diff, manifest_schema
22
+ from pptx_cli.commands.validate import validate_command
23
+ from pptx_cli.commands.wrapper import wrapper_generate
24
+ from pptx_cli.core.composition import CompositionError
25
+ from pptx_cli.core.runtime import build_runtime_context, stdout_is_tty
26
+ from pptx_cli.core.validation import ValidationError
27
+ from pptx_cli.models.envelope import CliMessage, Envelope, Metrics
28
+
29
+ app = typer.Typer(help="Template-bound PowerPoint generation for enterprise decks.")
30
+ layouts_app = typer.Typer(help="Inspect approved layouts from a manifest package.")
31
+ placeholders_app = typer.Typer(help="Inspect placeholders for a layout contract.")
32
+ theme_app = typer.Typer(help="Inspect extracted theme metadata.")
33
+ assets_app = typer.Typer(help="Inspect extracted asset references.")
34
+ slide_app = typer.Typer(help="Create slides from approved layouts.")
35
+ deck_app = typer.Typer(help="Build full decks from structured specs.")
36
+ manifest_app = typer.Typer(help="Work with manifest packages and schemas.")
37
+ wrapper_app = typer.Typer(help="Generate thin template-specific wrapper CLIs.")
38
+
39
+ app.add_typer(layouts_app, name="layouts")
40
+ app.add_typer(placeholders_app, name="placeholders")
41
+ app.add_typer(theme_app, name="theme")
42
+ app.add_typer(assets_app, name="assets")
43
+ app.add_typer(slide_app, name="slide")
44
+ app.add_typer(deck_app, name="deck")
45
+ app.add_typer(manifest_app, name="manifest")
46
+ app.add_typer(wrapper_app, name="wrapper")
47
+
48
+ FormatOption = Annotated[
49
+ str | None,
50
+ typer.Option("--format", help="Output format: json or text."),
51
+ ]
52
+ DryRunOption = Annotated[
53
+ bool,
54
+ typer.Option("--dry-run", help="Preview changes without writing files."),
55
+ ]
56
+ ManifestOption = Annotated[
57
+ Path,
58
+ typer.Option("--manifest", help="Path to the manifest package directory."),
59
+ ]
60
+
61
+ _EXIT_CODE_MAP = {
62
+ "validation": 10,
63
+ "policy": 20,
64
+ "conflict": 40,
65
+ "io": 50,
66
+ "internal": 90,
67
+ }
68
+
69
+
70
+ def resolve_output_format(runtime: Any, explicit_format: str | None) -> str:
71
+ if explicit_format is not None:
72
+ return explicit_format
73
+ if runtime.llm_mode or not stdout_is_tty():
74
+ return "json"
75
+ return "text"
76
+
77
+
78
+ def emit_json(envelope: Envelope) -> None:
79
+ typer.echo(json.dumps(envelope.model_dump(mode="json", exclude_none=True), indent=2))
80
+
81
+
82
+ def emit_result(result: Any, envelope: Envelope, format: str) -> None:
83
+ if format == "json":
84
+ emit_json(envelope)
85
+ return
86
+ if format == "text":
87
+ typer.echo(yaml.safe_dump(result, sort_keys=False, allow_unicode=True).rstrip())
88
+ return
89
+ raise typer.BadParameter("format must be 'json' or 'text'")
90
+
91
+
92
+ def _exit_code_for_error(error_code: str) -> int:
93
+ if error_code.startswith("ERR_VALIDATION"):
94
+ return _EXIT_CODE_MAP["validation"]
95
+ if error_code.startswith("ERR_POLICY"):
96
+ return _EXIT_CODE_MAP["policy"]
97
+ if error_code.startswith("ERR_CONFLICT"):
98
+ return _EXIT_CODE_MAP["conflict"]
99
+ if error_code.startswith("ERR_IO"):
100
+ return _EXIT_CODE_MAP["io"]
101
+ return _EXIT_CODE_MAP["internal"]
102
+
103
+
104
+ def _message_for_error(
105
+ error_code: str,
106
+ message: str,
107
+ details: dict[str, Any] | None = None,
108
+ ) -> CliMessage:
109
+ if error_code.startswith("ERR_VALIDATION"):
110
+ suggested_action = "fix_input"
111
+ elif error_code.startswith("ERR_IO"):
112
+ suggested_action = "retry"
113
+ else:
114
+ suggested_action = "escalate"
115
+ retryable = error_code.startswith("ERR_IO")
116
+ return CliMessage(
117
+ code=error_code,
118
+ message=message,
119
+ retryable=retryable,
120
+ suggested_action=suggested_action,
121
+ details=details or {},
122
+ )
123
+
124
+
125
+ def fail(
126
+ command: str,
127
+ runtime: Any,
128
+ format: str,
129
+ error_code: str,
130
+ message: str,
131
+ details: dict[str, Any] | None = None,
132
+ ) -> None:
133
+ error = _message_for_error(error_code, message, details)
134
+ envelope = Envelope(
135
+ request_id=runtime.request_id,
136
+ ok=False,
137
+ command=command,
138
+ result=None,
139
+ warnings=[],
140
+ errors=[error],
141
+ metrics=Metrics(duration_ms=runtime.duration_ms),
142
+ )
143
+ emit_result(None, envelope, format)
144
+ raise typer.Exit(code=_exit_code_for_error(error_code))
145
+
146
+
147
+ def success(
148
+ command: str,
149
+ runtime: Any,
150
+ format: str,
151
+ result: Any,
152
+ *,
153
+ warnings: list[CliMessage] | None = None,
154
+ ) -> None:
155
+ envelope = Envelope(
156
+ request_id=runtime.request_id,
157
+ ok=True,
158
+ command=command,
159
+ result=result,
160
+ warnings=warnings or [],
161
+ errors=[],
162
+ metrics=Metrics(duration_ms=runtime.duration_ms),
163
+ )
164
+ emit_result(result, envelope, format)
165
+
166
+
167
+ def execute(command: str, format: str | None, func: Any) -> None:
168
+ runtime = build_runtime_context()
169
+ resolved_format = resolve_output_format(runtime, format)
170
+ try:
171
+ result = func()
172
+ except CompositionError as exc:
173
+ fail(command, runtime, resolved_format, exc.code, str(exc))
174
+ except ValidationError as exc:
175
+ fail(command, runtime, resolved_format, exc.code, str(exc))
176
+ except ValueError as exc:
177
+ fail(command, runtime, resolved_format, "ERR_VALIDATION_INPUT", str(exc))
178
+ success(command, runtime, resolved_format, result)
179
+
180
+
181
+ @app.command("guide")
182
+ def guide(format: FormatOption = None) -> None:
183
+ """Show the machine-readable CLI guide."""
184
+
185
+ execute("guide.show", format, lambda: build_guide_document().model_dump(mode="json"))
186
+
187
+
188
+ @app.command("init")
189
+ def init_command(
190
+ template: Annotated[Path, typer.Argument(help="Path to the source .pptx template")],
191
+ out: Annotated[Path, typer.Option("--out", help="Output directory for the manifest package")],
192
+ dry_run: DryRunOption = False,
193
+ format: FormatOption = None,
194
+ ) -> None:
195
+ """Initialize a manifest package from a source template."""
196
+
197
+ runtime = build_runtime_context()
198
+ resolved_format = resolve_output_format(runtime, format)
199
+ if not template.exists():
200
+ fail(
201
+ "template.init",
202
+ runtime,
203
+ resolved_format,
204
+ "ERR_IO_NOT_FOUND",
205
+ f"Template not found: {template}",
206
+ {"template": str(template)},
207
+ )
208
+ if template.suffix.lower() != ".pptx":
209
+ fail(
210
+ "template.init",
211
+ runtime,
212
+ resolved_format,
213
+ "ERR_VALIDATION_TEMPLATE_TYPE",
214
+ "Template input must be a .pptx file",
215
+ {"template": str(template)},
216
+ )
217
+ try:
218
+ result = run_init(template, out, dry_run=dry_run)
219
+ except OSError as exc:
220
+ fail("template.init", runtime, resolved_format, "ERR_IO_WRITE", str(exc))
221
+ success("template.init", runtime, resolved_format, result)
222
+
223
+
224
+ @app.command("doctor")
225
+ def doctor_command(manifest: ManifestOption, format: FormatOption = None) -> None:
226
+ """Show compatibility findings for a manifest package."""
227
+
228
+ execute("doctor.show", format, lambda: doctor(manifest))
229
+
230
+
231
+ @layouts_app.command("list")
232
+ def layouts_list(manifest: ManifestOption, format: FormatOption = None) -> None:
233
+ """List available layouts from a manifest package."""
234
+
235
+ execute("layouts.list", format, lambda: list_layouts(manifest))
236
+
237
+
238
+ @layouts_app.command("show")
239
+ def layouts_show(
240
+ layout_id: Annotated[str, typer.Argument(help="Layout ID or source layout name")],
241
+ manifest: ManifestOption,
242
+ format: FormatOption = None,
243
+ ) -> None:
244
+ """Show a single layout contract."""
245
+
246
+ execute("layouts.show", format, lambda: show_layout(manifest, layout_id))
247
+
248
+
249
+ @placeholders_app.command("list")
250
+ def placeholders_list_command(
251
+ layout_id: Annotated[str, typer.Argument(help="Layout ID or source layout name")],
252
+ manifest: ManifestOption,
253
+ format: FormatOption = None,
254
+ ) -> None:
255
+ """List placeholders for a layout."""
256
+
257
+ execute("placeholders.list", format, lambda: list_placeholders(manifest, layout_id))
258
+
259
+
260
+ @theme_app.command("show")
261
+ def theme_show(manifest: ManifestOption, format: FormatOption = None) -> None:
262
+ """Show extracted theme metadata."""
263
+
264
+ execute("theme.show", format, lambda: show_theme(manifest))
265
+
266
+
267
+ @assets_app.command("list")
268
+ def assets_list_command(manifest: ManifestOption, format: FormatOption = None) -> None:
269
+ """List extracted assets."""
270
+
271
+ execute("assets.list", format, lambda: list_assets(manifest))
272
+
273
+
274
+ @slide_app.command("create")
275
+ def slide_create_command(
276
+ manifest: ManifestOption,
277
+ layout: Annotated[str, typer.Option("--layout", help="Layout ID from the manifest")],
278
+ out: Annotated[Path, typer.Option("--out", help="Output .pptx file")],
279
+ set_values: Annotated[
280
+ list[str] | None,
281
+ typer.Option(
282
+ "--set",
283
+ help="Placeholder assignment like key=value or key=@file",
284
+ ),
285
+ ] = None,
286
+ dry_run: DryRunOption = False,
287
+ format: FormatOption = None,
288
+ ) -> None:
289
+ """Create a deck containing a single slide from an approved layout."""
290
+
291
+ execute(
292
+ "slide.create",
293
+ format,
294
+ lambda: slide_create(
295
+ manifest,
296
+ layout,
297
+ list(set_values or []),
298
+ out,
299
+ dry_run=dry_run,
300
+ ),
301
+ )
302
+
303
+
304
+ @deck_app.command("build")
305
+ def deck_build_command(
306
+ manifest: ManifestOption,
307
+ spec: Annotated[Path, typer.Option("--spec", help="Path to the YAML or JSON deck spec")],
308
+ out: Annotated[Path, typer.Option("--out", help="Output .pptx file")],
309
+ dry_run: DryRunOption = False,
310
+ format: FormatOption = None,
311
+ ) -> None:
312
+ """Build a deck from a structured spec."""
313
+
314
+ execute("deck.build", format, lambda: deck_build(manifest, spec, out, dry_run=dry_run))
315
+
316
+
317
+ @app.command("validate")
318
+ def validate_deck_command(
319
+ manifest: ManifestOption,
320
+ deck: Annotated[Path, typer.Option("--deck", help="Path to the generated .pptx deck")],
321
+ strict: Annotated[
322
+ bool,
323
+ typer.Option(
324
+ "--strict",
325
+ help="Escalate warnings into validation failures where applicable.",
326
+ ),
327
+ ] = False,
328
+ format: FormatOption = None,
329
+ ) -> None:
330
+ """Validate a generated deck against the manifest contract."""
331
+
332
+ execute("validate.run", format, lambda: validate_command(manifest, deck, strict=strict))
333
+
334
+
335
+ @manifest_app.command("diff")
336
+ def manifest_diff_command(
337
+ left: Annotated[Path, typer.Argument(help="Left-hand manifest directory")],
338
+ right: Annotated[Path, typer.Argument(help="Right-hand manifest directory")],
339
+ format: FormatOption = None,
340
+ ) -> None:
341
+ """Compare two manifest packages."""
342
+
343
+ execute("manifest.diff", format, lambda: manifest_diff(left, right))
344
+
345
+
346
+ @manifest_app.command("schema")
347
+ def manifest_schema_command(format: FormatOption = None) -> None:
348
+ """Emit the JSON schema for manifest.yaml."""
349
+
350
+ execute("manifest.schema", format, manifest_schema)
351
+
352
+
353
+ @wrapper_app.command("generate")
354
+ def wrapper_generate_command(
355
+ manifest: ManifestOption,
356
+ out: Annotated[
357
+ Path,
358
+ typer.Option(
359
+ "--out",
360
+ help="Output directory for the generated wrapper package",
361
+ ),
362
+ ],
363
+ dry_run: DryRunOption = False,
364
+ format: FormatOption = None,
365
+ ) -> None:
366
+ """Generate a thin template-specific wrapper scaffold."""
367
+
368
+ execute("wrapper.generate", format, lambda: wrapper_generate(manifest, out, dry_run=dry_run))
369
+
370
+
371
+ def main() -> None:
372
+ app()
@@ -0,0 +1 @@
1
+ """CLI command modules."""
@@ -0,0 +1,73 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Any
5
+
6
+ from pptx_cli.core.composition import (
7
+ build_presentation,
8
+ create_single_slide_spec,
9
+ parse_set_arguments,
10
+ save_presentation,
11
+ )
12
+ from pptx_cli.core.manifest_store import load_deck_spec, load_manifest
13
+
14
+
15
+ def slide_create(
16
+ manifest_dir: Path,
17
+ layout_id: str,
18
+ set_values: list[str],
19
+ output_path: Path,
20
+ *,
21
+ dry_run: bool,
22
+ ) -> dict[str, Any]:
23
+ manifest = load_manifest(manifest_dir)
24
+ content = parse_set_arguments(set_values)
25
+ spec = create_single_slide_spec(layout_id, content)
26
+ planned_changes = [
27
+ {
28
+ "target": str(output_path),
29
+ "operation": "replace" if output_path.exists() else "create",
30
+ "artifact_type": "pptx",
31
+ }
32
+ ]
33
+ if not dry_run:
34
+ prs = build_presentation(manifest_dir, manifest, spec)
35
+ save_presentation(prs, output_path)
36
+ return {
37
+ "dry_run": dry_run,
38
+ "manifest": str(manifest_dir),
39
+ "layout": layout_id,
40
+ "out": str(output_path),
41
+ "changes": planned_changes,
42
+ "summary": {"slides": 1, "artifacts": 1},
43
+ }
44
+
45
+
46
+ def deck_build(
47
+ manifest_dir: Path,
48
+ spec_path: Path,
49
+ output_path: Path,
50
+ *,
51
+ dry_run: bool,
52
+ ) -> dict[str, Any]:
53
+ manifest = load_manifest(manifest_dir)
54
+ spec = load_deck_spec(spec_path)
55
+ planned_changes = [
56
+ {
57
+ "target": str(output_path),
58
+ "operation": "replace" if output_path.exists() else "create",
59
+ "artifact_type": "pptx",
60
+ }
61
+ ]
62
+ if not dry_run:
63
+ prs = build_presentation(manifest_dir, manifest, spec)
64
+ save_presentation(prs, output_path)
65
+ return {
66
+ "dry_run": dry_run,
67
+ "manifest": str(manifest_dir),
68
+ "spec": str(spec_path),
69
+ "out": str(output_path),
70
+ "changes": planned_changes,
71
+ "summary": {"slides": len(spec.slides), "artifacts": 1},
72
+ "metadata": spec.metadata,
73
+ }
@@ -0,0 +1,157 @@
1
+ from __future__ import annotations
2
+
3
+ from pptx_cli.models.envelope import GuideCommand, GuideDocument
4
+ from pptx_cli.models.manifest import DeckSpec, ManifestDocument, ValidationResult
5
+
6
+
7
+ def build_guide_document() -> GuideDocument:
8
+ return GuideDocument(
9
+ compatibility={
10
+ "additive_changes": "minor",
11
+ "breaking_changes": "major",
12
+ },
13
+ commands=[
14
+ GuideCommand(
15
+ id="guide.show",
16
+ summary="Show the machine-readable CLI guide",
17
+ mutates=False,
18
+ examples=["pptx guide --format json"],
19
+ ),
20
+ GuideCommand(
21
+ id="template.init",
22
+ summary="Initialize a manifest package from a source template",
23
+ mutates=True,
24
+ output_schema={"$ref": "#/definitions/Envelope"},
25
+ examples=["pptx init Template.pptx --out ./corp-template --dry-run"],
26
+ ),
27
+ GuideCommand(
28
+ id="doctor.show",
29
+ summary="Show manifest compatibility findings",
30
+ mutates=False,
31
+ examples=["pptx doctor --manifest ./corp-template --format json"],
32
+ ),
33
+ GuideCommand(
34
+ id="layouts.list",
35
+ summary="List available layouts from a manifest package",
36
+ mutates=False,
37
+ examples=["pptx layouts list --manifest ./corp-template --format json"],
38
+ ),
39
+ GuideCommand(
40
+ id="layouts.show",
41
+ summary="Show a single layout contract",
42
+ mutates=False,
43
+ examples=["pptx layouts show title-only --manifest ./corp-template --format json"],
44
+ ),
45
+ GuideCommand(
46
+ id="placeholders.list",
47
+ summary="List placeholders for a layout",
48
+ mutates=False,
49
+ examples=[
50
+ "pptx placeholders list title-only --manifest ./corp-template --format json"
51
+ ],
52
+ ),
53
+ GuideCommand(
54
+ id="theme.show",
55
+ summary="Show theme metadata extracted into the manifest",
56
+ mutates=False,
57
+ examples=["pptx theme show --manifest ./corp-template --format json"],
58
+ ),
59
+ GuideCommand(
60
+ id="assets.list",
61
+ summary="List extracted assets from the manifest package",
62
+ mutates=False,
63
+ examples=["pptx assets list --manifest ./corp-template --format json"],
64
+ ),
65
+ GuideCommand(
66
+ id="slide.create",
67
+ summary="Create a slide from an approved layout",
68
+ mutates=True,
69
+ input_schema=DeckSpec.model_json_schema(),
70
+ examples=[
71
+ "pptx slide create --manifest ./corp-template --layout title-only "
72
+ "--set title=Hello --out ./out/slide.pptx --dry-run"
73
+ ],
74
+ ),
75
+ GuideCommand(
76
+ id="deck.build",
77
+ summary="Build a deck from a structured spec",
78
+ mutates=True,
79
+ input_schema=DeckSpec.model_json_schema(),
80
+ examples=[
81
+ "pptx deck build --manifest ./corp-template --spec deck.yaml "
82
+ "--out ./out/deck.pptx --dry-run"
83
+ ],
84
+ ),
85
+ GuideCommand(
86
+ id="validate.run",
87
+ summary="Validate a deck against a manifest package",
88
+ mutates=False,
89
+ output_schema=ValidationResult.model_json_schema(),
90
+ examples=[
91
+ "pptx validate --manifest ./corp-template --deck ./out/deck.pptx "
92
+ "--strict --format json"
93
+ ],
94
+ ),
95
+ GuideCommand(
96
+ id="manifest.diff",
97
+ summary="Compare two manifest packages and report additive vs. breaking changes",
98
+ mutates=False,
99
+ examples=["pptx manifest diff ./corp-template-v1 ./corp-template-v2 --format json"],
100
+ ),
101
+ GuideCommand(
102
+ id="manifest.schema",
103
+ summary="Emit the JSON schema for manifest.yaml",
104
+ mutates=False,
105
+ output_schema=ManifestDocument.model_json_schema(),
106
+ examples=["pptx manifest schema --format json"],
107
+ ),
108
+ GuideCommand(
109
+ id="wrapper.generate",
110
+ summary="Generate a thin template-specific wrapper scaffold",
111
+ mutates=True,
112
+ examples=[
113
+ "pptx wrapper generate --manifest ./corp-template --out ./wrappers/acme "
114
+ "--dry-run"
115
+ ],
116
+ ),
117
+ ],
118
+ exit_codes={
119
+ "success": 0,
120
+ "validation_error": 10,
121
+ "policy_error": 20,
122
+ "conflict": 40,
123
+ "io_error": 50,
124
+ "internal_error": 90,
125
+ },
126
+ error_codes={
127
+ "ERR_VALIDATION_LAYOUT_UNKNOWN": {"exit_code": 10, "retryable": False},
128
+ "ERR_VALIDATION_PLACEHOLDER_UNKNOWN": {"exit_code": 10, "retryable": False},
129
+ "ERR_VALIDATION_PLACEHOLDER_REQUIRED": {"exit_code": 10, "retryable": False},
130
+ "ERR_VALIDATION_CONTENT_TYPE": {"exit_code": 10, "retryable": False},
131
+ "ERR_IO_NOT_FOUND": {"exit_code": 50, "retryable": False},
132
+ "ERR_CONFLICT_OUTPUT_EXISTS": {"exit_code": 40, "retryable": False},
133
+ "ERR_INTERNAL_PLACEHOLDER_MISSING": {"exit_code": 90, "retryable": False},
134
+ },
135
+ identifier_conventions={
136
+ "command_ids": (
137
+ "canonical dotted identifiers like guide.show, layouts.list, or deck.build"
138
+ ),
139
+ "layout_ids": (
140
+ "slugified manifest layout identifiers derived from template layout names"
141
+ ),
142
+ "placeholder_keys": (
143
+ "logical placeholder keys such as title, subtitle, content_1, or picture"
144
+ ),
145
+ "manifest_path": "path to a manifest package directory containing manifest.yaml",
146
+ },
147
+ concurrency={
148
+ "rule": (
149
+ "Read commands can run in parallel; mutating commands writing the same "
150
+ "output path must run sequentially."
151
+ ),
152
+ "safe_patterns": [
153
+ "Run guide, layouts, theme, assets, and doctor commands in parallel",
154
+ "Run deck.build and validate sequentially against the same output file",
155
+ ],
156
+ },
157
+ )
@@ -0,0 +1,52 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Any
5
+
6
+ from pptx_cli.core.manifest_store import write_manifest_package
7
+ from pptx_cli.core.template import (
8
+ build_manifest_package,
9
+ ensure_manifest_directories,
10
+ plan_manifest_writes,
11
+ write_fingerprints,
12
+ )
13
+
14
+
15
+ def plan_init(template: Path, output_dir: Path) -> dict[str, Any]:
16
+ changes = plan_manifest_writes(template, output_dir)
17
+ return {
18
+ "template": str(template),
19
+ "output_dir": str(output_dir),
20
+ "changes": changes,
21
+ "artifacts": [item["target"] for item in changes],
22
+ }
23
+
24
+
25
+ def run_init(template: Path, output_dir: Path, *, dry_run: bool) -> dict[str, Any]:
26
+ plan = plan_init(template, output_dir)
27
+ if dry_run:
28
+ return {
29
+ "dry_run": True,
30
+ "summary": {"total_outputs": len(plan["artifacts"])},
31
+ "plan": plan,
32
+ }
33
+
34
+ ensure_manifest_directories(output_dir)
35
+ manifest, annotations, init_report = build_manifest_package(template, output_dir)
36
+ write_manifest_package(
37
+ output_dir,
38
+ manifest,
39
+ annotations,
40
+ init_report.model_dump(mode="json"),
41
+ )
42
+ write_fingerprints(output_dir, manifest)
43
+ return {
44
+ "dry_run": False,
45
+ "summary": {
46
+ "total_outputs": len(plan["artifacts"]),
47
+ "layout_count": len(manifest.layouts),
48
+ "asset_count": len(manifest.assets),
49
+ },
50
+ "plan": plan,
51
+ "manifest": str(output_dir),
52
+ }