resolvescript 0.1.2__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.
Files changed (49) hide show
  1. resolve_script/__init__.py +3 -0
  2. resolve_script/analyze.py +277 -0
  3. resolve_script/cli.py +748 -0
  4. resolve_script/config.py +62 -0
  5. resolve_script/consolidate.py +604 -0
  6. resolve_script/fetch.py +90 -0
  7. resolve_script/install/__init__.py +49 -0
  8. resolve_script/install/discovery.py +57 -0
  9. resolve_script/install/installer.py +397 -0
  10. resolve_script/install/registry.py +106 -0
  11. resolve_script/manifest/__init__.py +1 -0
  12. resolve_script/manifest/json_reader.py +40 -0
  13. resolve_script/manifest/model.py +316 -0
  14. resolve_script/manifest/validation.py +81 -0
  15. resolve_script/manifest/xml_reader.py +162 -0
  16. resolve_script/package.py +103 -0
  17. resolve_script/resolver.py +204 -0
  18. resolve_script/sandbox/__init__.py +38 -0
  19. resolve_script/sandbox/api.py +393 -0
  20. resolve_script/sandbox/env.py +82 -0
  21. resolve_script/sandbox/loader.py +72 -0
  22. resolve_script/sandbox/repl.py +57 -0
  23. resolve_script/sandbox/smoke.py +104 -0
  24. resolve_script/scaffold.py +126 -0
  25. resolve_script/semver.py +236 -0
  26. resolve_script/sources/__init__.py +15 -0
  27. resolve_script/sources/archive.py +82 -0
  28. resolve_script/sources/git.py +107 -0
  29. resolve_script/sources/known.py +47 -0
  30. resolve_script/sources/release.py +55 -0
  31. resolve_script/spec.py +137 -0
  32. resolve_script/templates/extension/@NAME@/__init__.py +7 -0
  33. resolve_script/templates/extension/@NAME@/menu.py +12 -0
  34. resolve_script/templates/extension/@NAME@.py +13 -0
  35. resolve_script/templates/extension/README.md +20 -0
  36. resolve_script/templates/extension/conftest.py +13 -0
  37. resolve_script/templates/extension/manifest.json.j2 +23 -0
  38. resolve_script/templates/extension/manifest.xml.j2 +24 -0
  39. resolve_script/templates/extension/tests/test_smoke.py +26 -0
  40. resolve_script/templates/inapp/register.py +28 -0
  41. resolve_script/testing/__init__.py +6 -0
  42. resolve_script/testing/fixtures.py +47 -0
  43. resolve_script/workspace.py +66 -0
  44. resolvescript-0.1.2.dist-info/METADATA +146 -0
  45. resolvescript-0.1.2.dist-info/RECORD +49 -0
  46. resolvescript-0.1.2.dist-info/WHEEL +5 -0
  47. resolvescript-0.1.2.dist-info/entry_points.txt +2 -0
  48. resolvescript-0.1.2.dist-info/licenses/LICENSE +21 -0
  49. resolvescript-0.1.2.dist-info/top_level.txt +1 -0
resolve_script/cli.py ADDED
@@ -0,0 +1,748 @@
1
+ """Command-line entry point for ``resolvescript``.
2
+
3
+ Every subcommand maps to a handler function returning an exit code
4
+ (0 = ok, 1 = error, 2 = usage). Production implementations are wired in by
5
+ their owning milestone; anything still on the M0 skeleton prints a notice.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import argparse
11
+ import json
12
+ import sys
13
+ from pathlib import Path
14
+ from typing import Callable
15
+
16
+ from . import __version__
17
+ from .consolidate import (
18
+ BuildConfig,
19
+ ConsolidateError,
20
+ config_from_manifest,
21
+ consolidate,
22
+ summarize,
23
+ )
24
+ from .manifest.model import ManifestError
25
+ from .scaffold import ScaffoldError, scaffold_project
26
+
27
+ USAGE = 2
28
+
29
+
30
+ def _skeleton(name: str) -> Callable[[argparse.Namespace], int]:
31
+ """Return a stub handler until the owning milestone wires the command."""
32
+
33
+ def run(args: argparse.Namespace) -> int:
34
+ del args
35
+ print(f"resolvescript: '{name}' is not implemented yet (M0 skeleton)", file=sys.stderr)
36
+ return 1
37
+
38
+ return run
39
+
40
+
41
+ def _cmd_create(args: argparse.Namespace) -> int:
42
+ destination = Path(args.dir) if args.dir else None
43
+ try:
44
+ root, written = scaffold_project(
45
+ args.name,
46
+ destination=destination,
47
+ fmt=args.fmt,
48
+ template=args.template,
49
+ )
50
+ except ScaffoldError as exc:
51
+ print(f"resolvescript: cannot create: {exc}", file=sys.stderr)
52
+ return 1
53
+ print(f"Created {args.fmt.upper()} manifest Resolve script project in {root}")
54
+ for rel in sorted(written):
55
+ print(f" {rel}")
56
+ print("\nNext steps:")
57
+ print(f" cd {root}")
58
+ print(" resolvescript dev # iterate against the mock Resolve API")
59
+ print(" resolvescript test # run the smoke tests")
60
+ print(" resolvescript build # consolidate into a single file")
61
+ print(" resolvescript install # install into DaVinci Resolve")
62
+ return 0
63
+
64
+
65
+ def _cmd_build(args: argparse.Namespace) -> int:
66
+ root = Path.cwd()
67
+ manifest_path = root / "manifest.json"
68
+ if not manifest_path.is_file():
69
+ manifest_path = root / "manifest.xml"
70
+ if not manifest_path.is_file():
71
+ print(
72
+ "resolvescript: no manifest.json or manifest.xml in the current directory "
73
+ "(run 'resolvescript create' or use 'resolvescript consolidate <dir>')",
74
+ file=sys.stderr,
75
+ )
76
+ return 1
77
+ try:
78
+ if manifest_path.suffix == ".xml":
79
+ from .manifest.xml_reader import load_manifest
80
+ else:
81
+ from .manifest.json_reader import load_manifest
82
+ manifest = load_manifest(manifest_path)
83
+ except ManifestError as exc:
84
+ print(f"resolvescript: {exc}", file=sys.stderr)
85
+ return 1
86
+
87
+ if not manifest.consolidate.enabled:
88
+ print("resolvescript: consolidate is disabled in the manifest; nothing to build")
89
+ return 0
90
+ output = Path(args.output) if args.output else None
91
+ config = config_from_manifest(root, manifest, output_override=output)
92
+ try:
93
+ result = consolidate(config)
94
+ except ConsolidateError as exc:
95
+ print(f"resolvescript: build failed: {exc}", file=sys.stderr)
96
+ return 1
97
+ print(summarize(result, config))
98
+ return 0
99
+
100
+
101
+ def _cmd_package(args: argparse.Namespace) -> int:
102
+ from .package import PackageError, package_project
103
+
104
+ root = Path.cwd()
105
+ dist_dir = Path(args.dist) if args.dist else None
106
+ try:
107
+ result = package_project(root, dist_dir)
108
+ except (ManifestError, PackageError) as exc:
109
+ print(f"resolvescript: package: {exc}", file=sys.stderr)
110
+ return 1
111
+ print(f"Created {result.archive.name} ({result.archive.stat().st_size} bytes)")
112
+ print(f"SHA-256: {result.sha256}")
113
+ if result.checksum_file:
114
+ print(f"Wrote {result.checksum_file.name}")
115
+ return 0
116
+
117
+
118
+ def _cmd_consolidate(args: argparse.Namespace) -> int:
119
+ config = BuildConfig(
120
+ package_root=Path(args.package_dir),
121
+ output=Path(args.output),
122
+ )
123
+ try:
124
+ result = consolidate(config)
125
+ except ConsolidateError as exc:
126
+ print(f"resolvescript: consolidate failed: {exc}", file=sys.stderr)
127
+ return 1
128
+ print(summarize(result, config))
129
+ return 0
130
+
131
+
132
+ def _resolve_scripts_root(override: str | None) -> Path:
133
+ from .install.discovery import resolve_scripts_root
134
+
135
+ return resolve_scripts_root(override)
136
+
137
+
138
+ def _cache_dir(cwd: Path) -> Path:
139
+ path = cwd / ".resolvescript" / "cache"
140
+ path.mkdir(parents=True, exist_ok=True)
141
+ return path
142
+
143
+
144
+ def _materialize(spec_text: str, scripts_root: Path, *, cwd: Path | None = None):
145
+ from .resolver import resolve_spec
146
+
147
+ cwd = cwd or Path.cwd()
148
+ return resolve_spec(spec_text, cwd=cwd, work_dir=_cache_dir(cwd))
149
+
150
+
151
+ def _major(version: str) -> int:
152
+ try:
153
+ return int(version.split(".")[0])
154
+ except (ValueError, IndexError):
155
+ return -1
156
+
157
+
158
+ def _pin_spec(spec_text: str, precise: str, cwd: Path) -> str:
159
+ from .spec import parse_specifier
160
+
161
+ spec = parse_specifier(spec_text, cwd=cwd)
162
+ if spec.kind == "github" and spec.owner and spec.repo:
163
+ return f"github:{spec.owner}/{spec.repo}#{precise}"
164
+ if spec.kind == "name":
165
+ from .sources import known
166
+
167
+ entry = known.lookup(spec.source)
168
+ if entry and entry["source"].startswith("github:"):
169
+ return entry["source"] + f"#{precise}"
170
+ return spec_text
171
+
172
+
173
+ def _cmd_install(args: argparse.Namespace) -> int:
174
+ from .install.discovery import resolve_scripts_root
175
+ from .install.installer import InstallError, InstallOptions, install_package, install_project
176
+ from .install.registry import get_extension, read_registry
177
+ from .resolver import ResolveError, lockfile_satisfies
178
+ from .workspace import read_workspace
179
+
180
+ cwd = Path.cwd()
181
+ scripts_root = resolve_scripts_root(args.scripts_root)
182
+
183
+ def _report(result) -> int:
184
+ for line in result.describe():
185
+ print(line)
186
+ if result.dry_run:
187
+ print("(dry run - nothing was written)")
188
+ else:
189
+ print(f".resolvescript registry: {scripts_root / '.resolvescript' / 'install.json'}")
190
+ return 0
191
+
192
+ try:
193
+ if args.spec:
194
+ resolved = _materialize(args.spec, scripts_root, cwd=cwd)
195
+ result = install_package(
196
+ resolved.package_dir,
197
+ resolved.manifest,
198
+ InstallOptions(
199
+ scripts_root=scripts_root,
200
+ source=resolved.source,
201
+ resolved=resolved.source,
202
+ integrity=resolved.integrity,
203
+ ),
204
+ )
205
+ return _report(result)
206
+
207
+ has_manifest = (cwd / "manifest.json").is_file() or (cwd / "manifest.xml").is_file()
208
+ if has_manifest:
209
+ result = install_project(cwd, InstallOptions(scripts_root=scripts_root, dry_run=args.dry_run))
210
+ return _report(result)
211
+
212
+ if (cwd / "resolvescript.json").is_file():
213
+ deps = read_workspace(cwd).get("dependencies", {})
214
+ if not deps:
215
+ print("resolvescript: no recorded dependencies in resolvescript.json")
216
+ return 0
217
+ registry = read_registry(scripts_root)
218
+ errors = 0
219
+ for name, spec in sorted(deps.items()):
220
+ entry = get_extension(registry, name)
221
+ if entry and lockfile_satisfies(entry, spec, cwd=cwd):
222
+ if args.locked:
223
+ print(f"locked {name} {entry.get('version', '?')} (registry)")
224
+ else:
225
+ print(f"already installed {name} {entry.get('version', '?')}")
226
+ continue
227
+ if args.locked:
228
+ print(
229
+ f"resolvescript: --locked: {name}: recorded artifact no longer "
230
+ f"satisfies {spec!r}",
231
+ file=sys.stderr,
232
+ )
233
+ errors += 1
234
+ continue
235
+ try:
236
+ resolved = _materialize(spec, scripts_root, cwd=cwd)
237
+ result = install_package(
238
+ resolved.package_dir,
239
+ resolved.manifest,
240
+ InstallOptions(
241
+ scripts_root=scripts_root,
242
+ source=resolved.source,
243
+ resolved=resolved.source,
244
+ integrity=resolved.integrity,
245
+ ),
246
+ )
247
+ except (ResolveError, InstallError) as exc:
248
+ print(f"resolvescript: install {name}: {exc}", file=sys.stderr)
249
+ errors += 1
250
+ continue
251
+ for line in result.describe():
252
+ print(line)
253
+ return 1 if errors else 0
254
+
255
+ print(
256
+ "resolvescript: nothing to install - add a manifest.json, a resolvescript.json, "
257
+ "or pass a specifier",
258
+ file=sys.stderr,
259
+ )
260
+ return 2
261
+ except (ManifestError, ResolveError, InstallError) as exc:
262
+ print(f"resolvescript: install: {exc}", file=sys.stderr)
263
+ return 1
264
+
265
+
266
+ def _cmd_remove(args: argparse.Namespace) -> int:
267
+ from .install.installer import InstallError, InstallOptions, uninstall_package
268
+ from .workspace import has_workspace, read_workspace, remove_dependency, save
269
+
270
+ scripts_root = _resolve_scripts_root(args.scripts_root)
271
+ cwd = Path.cwd()
272
+ had_workspace_entry = (
273
+ has_workspace(cwd)
274
+ and args.name in read_workspace(cwd).get("dependencies", {})
275
+ )
276
+ try:
277
+ removed = uninstall_package(args.name, InstallOptions(scripts_root=scripts_root))
278
+ except InstallError as exc:
279
+ if not had_workspace_entry or args.no_save:
280
+ print(f"resolvescript: remove: {exc}", file=sys.stderr)
281
+ return 1
282
+ removed = []
283
+ for path in removed:
284
+ print(f"removed {path}")
285
+ if had_workspace_entry and not args.no_save:
286
+ save(remove_dependency(args.name, cwd), cwd)
287
+ print(f"unrecorded {args.name} from resolvescript.json")
288
+ return 0
289
+
290
+
291
+ def _cmd_add(args: argparse.Namespace) -> int:
292
+ from .install.installer import InstallError, InstallOptions, install_package
293
+ from .resolver import ResolveError
294
+ from .workspace import add_dependency, save
295
+
296
+ cwd = Path.cwd()
297
+ scripts_root = _resolve_scripts_root(args.scripts_root)
298
+ try:
299
+ resolved = _materialize(args.spec, scripts_root, cwd=cwd)
300
+ result = install_package(
301
+ resolved.package_dir,
302
+ resolved.manifest,
303
+ InstallOptions(
304
+ scripts_root=scripts_root,
305
+ source=resolved.source,
306
+ resolved=resolved.source,
307
+ integrity=resolved.integrity,
308
+ ),
309
+ )
310
+ except (ResolveError, InstallError) as exc:
311
+ print(f"resolvescript: add: {exc}", file=sys.stderr)
312
+ return 1
313
+ for line in result.describe():
314
+ print(line)
315
+ if not args.no_save:
316
+ save(add_dependency(resolved.name, args.spec, cwd), cwd)
317
+ print(f"recorded {resolved.name} -> {args.spec} in resolvescript.json")
318
+ return 0
319
+
320
+
321
+ def _cmd_update(args: argparse.Namespace) -> int:
322
+ from .install.installer import InstallError, InstallOptions, install_package
323
+ from .resolver import ResolveError
324
+ from .workspace import add_dependency, has_workspace, read_workspace, save
325
+
326
+ cwd = Path.cwd()
327
+ if not has_workspace(cwd):
328
+ print(
329
+ "resolvescript: update: no resolvescript.json in the current directory "
330
+ "(run 'resolvescript add <spec>' first)",
331
+ file=sys.stderr,
332
+ )
333
+ return 2
334
+ scripts_root = _resolve_scripts_root(args.scripts_root)
335
+ deps = read_workspace(cwd).get("dependencies", {})
336
+ names = [args.name] if args.name else sorted(deps)
337
+ compat_env = __import__("os").environ.get("RESOLVESCRIPT_COMPAT", "18")
338
+ errors = 0
339
+ for name in names:
340
+ spec = deps.get(name)
341
+ if spec is None:
342
+ print(
343
+ f"resolvescript: update: '{name}' is not recorded in resolvescript.json",
344
+ file=sys.stderr,
345
+ )
346
+ errors += 1
347
+ continue
348
+ pinned = _pin_spec(spec, args.precise, cwd) if args.precise else spec
349
+ try:
350
+ resolved = _materialize(pinned, scripts_root, cwd=cwd)
351
+ except ResolveError as exc:
352
+ print(f"resolvescript: update {name}: {exc}", file=sys.stderr)
353
+ errors += 1
354
+ continue
355
+ if args.precise and resolved.version != args.precise:
356
+ print(
357
+ f"resolvescript: --precise {args.precise}: got version {resolved.version}",
358
+ file=sys.stderr,
359
+ )
360
+ errors += 1
361
+ continue
362
+ if args.fix:
363
+ compat = getattr(resolved.manifest, "compat", None)
364
+ compat_str = getattr(compat, "resolve", None) if compat else None
365
+ if compat_str and _major(compat_str) != _major(compat_env):
366
+ print(
367
+ f"resolvescript: update {name}: not compatible with DaVinci "
368
+ f"Resolve {compat_env} (manifest declares resolve {compat_str}); "
369
+ f"keeping previous version (--fix)",
370
+ file=sys.stderr,
371
+ )
372
+ continue
373
+ try:
374
+ result = install_package(
375
+ resolved.package_dir,
376
+ resolved.manifest,
377
+ InstallOptions(
378
+ scripts_root=scripts_root,
379
+ source=resolved.source,
380
+ resolved=resolved.source,
381
+ integrity=resolved.integrity,
382
+ ),
383
+ )
384
+ except InstallError as exc:
385
+ print(f"resolvescript: update {name}: {exc}", file=sys.stderr)
386
+ errors += 1
387
+ continue
388
+ for line in result.describe():
389
+ print(line)
390
+ if args.precise and pinned != spec:
391
+ save(add_dependency(name, pinned, cwd), cwd)
392
+ print(f"updated resolvescript.json: {name} -> {pinned}")
393
+ return 1 if errors else 0
394
+
395
+
396
+ def _cmd_search(args: argparse.Namespace) -> int:
397
+ from .sources.known import search
398
+
399
+ results = search(args.query)
400
+ if not results:
401
+ print(f"no known Resolve scripts match {args.query!r}")
402
+ return 0
403
+ for entry in results:
404
+ print(f"{entry['name']:20} {entry['source']}")
405
+ print(f" {entry['desc']}")
406
+ return 0
407
+
408
+
409
+ def _cmd_manage_list(args: argparse.Namespace) -> int:
410
+ from .install.registry import read_registry
411
+
412
+ scripts_root = _resolve_scripts_root(getattr(args, "scripts_root", None))
413
+ extensions = read_registry(scripts_root).get("extensions", {})
414
+ if getattr(args, "json", False):
415
+ print(json.dumps(extensions, indent=2))
416
+ return 0
417
+ if not extensions:
418
+ print("no extensions installed")
419
+ return 0
420
+ for key, entry in sorted(extensions.items()):
421
+ targets = ",".join(entry.get("targets", []))
422
+ version = entry.get("version", "?")
423
+ source = entry.get("source", "")
424
+ print(f"{key:24} {version:8} {targets:16} {source}")
425
+ return 0
426
+
427
+
428
+ def _cmd_manage_remove(args: argparse.Namespace) -> int:
429
+ import shutil
430
+
431
+ from .install.discovery import target_dir
432
+ from .install.registry import get_extension, read_registry, remove_entry
433
+
434
+ scripts_root = _resolve_scripts_root(args.scripts_root)
435
+ registry = read_registry(scripts_root)
436
+ entry = get_extension(registry, args.name)
437
+ if entry is None:
438
+ print(
439
+ f"resolvescript: manage remove: '{args.name}' is not installed",
440
+ file=sys.stderr,
441
+ )
442
+ return 1
443
+ as_directory = bool(entry.get("as_directory", True))
444
+ key = entry.get("id") or args.name
445
+ for target in entry.get("targets", []):
446
+ base = target_dir(scripts_root, target)
447
+ if as_directory:
448
+ container = base / (entry.get("name") or key)
449
+ if container.is_dir():
450
+ shutil.rmtree(container)
451
+ print(f"removed {container}")
452
+ else:
453
+ for rel in entry.get("files", []):
454
+ path = base / str(rel)
455
+ if path.is_file() or (path.is_symlink() and not path.exists()):
456
+ path.unlink()
457
+ print(f"removed {path}")
458
+ if args.all:
459
+ remove_entry(scripts_root, key) or remove_entry(scripts_root, key.split(":")[-1])
460
+ print(f"removed registry entry '{args.name}'")
461
+ else:
462
+ print(f"kept registry entry '{args.name}' (use --all to remove it)")
463
+ return 0
464
+
465
+
466
+ def _cmd_test(args: argparse.Namespace) -> int:
467
+ import subprocess
468
+
469
+ from .analyze import analyze_project
470
+
471
+ cwd = Path.cwd()
472
+ try:
473
+ manifest, _path = _load_manifest_from(cwd)
474
+ except ManifestError as exc:
475
+ print(f"resolvescript: test: {exc}", file=sys.stderr)
476
+ return 1
477
+
478
+ module_name = manifest.python or manifest.name
479
+ env = dict(__import__("os").environ)
480
+ pytest_args = ["-p", "no:cacheprovider", "-q"]
481
+ if args.pattern:
482
+ pytest_args += ["-k", args.pattern]
483
+
484
+ if args.built:
485
+ built_file = _resolve_built_file(module_name, manifest.consolidate.output)
486
+ if not built_file.is_file():
487
+ print(
488
+ f"resolvescript: test: built package not found: {built_file} "
489
+ "(run 'resolvescript build' first)",
490
+ file=sys.stderr,
491
+ )
492
+ return 1
493
+ env["RESOLVESCRIPT_TARGET"] = "built"
494
+ env["RESOLVESCRIPT_MODULE"] = module_name
495
+ env["RESOLVESCRIPT_BUILT_PATH"] = str(built_file)
496
+ runner = (
497
+ "import os, sys, pathlib\n"
498
+ "from resolve_script.sandbox.env import install_fake_resolve\n"
499
+ "from resolve_script.sandbox.loader import load_built_module\n"
500
+ "install_fake_resolve()\n"
501
+ "if os.environ.get('RESOLVESCRIPT_TARGET') == 'built':\n"
502
+ " name = os.environ['RESOLVESCRIPT_MODULE']\n"
503
+ " mod = load_built_module(name, os.environ['RESOLVESCRIPT_BUILT_PATH'])\n"
504
+ " sys.modules[name] = mod\n"
505
+ " for pkg_sub in sorted((pathlib.Path.cwd() / name).glob('*.py')):\n"
506
+ " if pkg_sub.stem != '__init__':\n"
507
+ " sys.modules[f'{name}.{pkg_sub.stem}'] = mod\n"
508
+ "import pytest\n"
509
+ "sys.exit(pytest.main(sys.argv[1:]))\n"
510
+ )
511
+ cmd = [sys.executable, "-c", runner] + pytest_args
512
+ else:
513
+ env["RESOLVESCRIPT_TARGET"] = "source"
514
+ cmd = [sys.executable, "-m", "pytest"] + pytest_args
515
+
516
+ result = subprocess.run(cmd, cwd=cwd, env=env)
517
+ if result.returncode != 0:
518
+ return 1
519
+ if args.api_coverage:
520
+ analysis = analyze_project(cwd, manifest)
521
+ for line in analysis.api_report():
522
+ print(line)
523
+ return 0
524
+
525
+
526
+ def _cmd_analyze(args: argparse.Namespace) -> int:
527
+ from .analyze import analyze_project, issues_to_json
528
+
529
+ try:
530
+ manifest, _path = _load_manifest_from(Path.cwd())
531
+ except ManifestError as exc:
532
+ print(f"resolvescript: analyze: {exc}", file=sys.stderr)
533
+ return 1
534
+ analysis = analyze_project(Path.cwd(), manifest)
535
+ if getattr(args, "json", False):
536
+ print(issues_to_json(analysis))
537
+ return 0
538
+ if not analysis.issues:
539
+ for line in analysis.api_report():
540
+ print(line)
541
+ print("no issues found")
542
+ return 0
543
+ for issue in analysis.issues:
544
+ where = f"{issue.file}:{issue.line}" if issue.file else "-"
545
+ print(f"[{issue.severity.upper()}] {issue.code} {where}: {issue.message}")
546
+ for line in analysis.api_report():
547
+ print(line)
548
+ return 1 if any(i.severity == "error" for i in analysis.issues) else 0
549
+
550
+
551
+ def _load_manifest_from(root: Path):
552
+ """Load manifest.json or manifest.xml from ``root`` or raise ManifestError."""
553
+ manifest_path = root / "manifest.json"
554
+ if not manifest_path.is_file():
555
+ manifest_path = root / "manifest.xml"
556
+ if not manifest_path.is_file():
557
+ raise ManifestError(
558
+ f"no manifest.json or manifest.xml in {root} (run 'resolvescript create')"
559
+ )
560
+ if manifest_path.suffix == ".xml":
561
+ from .manifest.xml_reader import load_manifest
562
+ else:
563
+ from .manifest.json_reader import load_manifest
564
+ return load_manifest(manifest_path), manifest_path
565
+
566
+
567
+ def _resolve_built_file(module_name: str, output: str | None) -> Path:
568
+ return Path.cwd() / "dist" / (output or f"{module_name}.py")
569
+
570
+
571
+ def _cmd_dev(args: argparse.Namespace) -> int:
572
+ from .sandbox.env import install_fake_resolve
573
+ from .sandbox.loader import load_built_module, load_source_module
574
+ from .sandbox.repl import start_repl
575
+ from .sandbox.smoke import run_smoke
576
+
577
+ try:
578
+ manifest, _path = _load_manifest_from(Path.cwd())
579
+ except ManifestError as exc:
580
+ print(f"resolvescript: dev: {exc}", file=sys.stderr)
581
+ return 1
582
+
583
+ module_name = manifest.python or manifest.name
584
+ install_fake_resolve()
585
+
586
+ try:
587
+ if args.built:
588
+ built_file = _resolve_built_file(module_name, manifest.consolidate.output)
589
+ if not built_file.is_file():
590
+ raise FileNotFoundError(
591
+ f"built package not found: {built_file} (run 'resolvescript build' first)"
592
+ )
593
+ module = load_built_module(module_name, built_file)
594
+ try:
595
+ display = built_file.relative_to(Path.cwd()).as_posix()
596
+ except ValueError:
597
+ display = built_file.as_posix()
598
+ mode = f"built: {display}"
599
+ else:
600
+ module = load_source_module(module_name, Path.cwd())
601
+ mode = f"source: {module_name}/"
602
+ except (FileNotFoundError, ImportError) as exc:
603
+ print(f"resolvescript: dev: {exc}", file=sys.stderr)
604
+ return 1
605
+
606
+ print(f"Sandbox environment ready ({mode})")
607
+
608
+ if args.editor:
609
+ from .scaffold import TEMPLATES_DIR, render
610
+
611
+ template = TEMPLATES_DIR / "inapp" / "register.py"
612
+ text = template.read_text(encoding="utf-8")
613
+ print(render(text, {"NAME": manifest.name}))
614
+ return 0
615
+
616
+ result = run_smoke(module, verbose=True)
617
+
618
+ if args.repl:
619
+ if not result.ok:
620
+ print("Some checks failed; dropping into the REPL anyway.")
621
+ start_repl(module)
622
+ return 0
623
+ if not result.ok:
624
+ print("Some checks failed.")
625
+ return 1
626
+ print("All checks passed.")
627
+ return 0
628
+
629
+
630
+ def build_parser() -> argparse.ArgumentParser:
631
+ parser = argparse.ArgumentParser(
632
+ prog="resolvescript",
633
+ description="Build, test, package and install DaVinci Resolve scripts.",
634
+ )
635
+ parser.add_argument(
636
+ "--version",
637
+ action="version",
638
+ version=f"%(prog)s {__version__}",
639
+ )
640
+ sub = parser.add_subparsers(dest="command", metavar="<command>")
641
+
642
+ p = sub.add_parser("create", help="scaffold a new Resolve script project")
643
+ p.add_argument("name", help="project name / output directory")
644
+ p.add_argument("--json", dest="fmt", action="store_const", const="json", default="json", help="generate manifest.json (default)")
645
+ p.add_argument("--xml", dest="fmt", action="store_const", const="xml", help="generate manifest.xml")
646
+ p.add_argument("--dir", help="parent directory to create the project in")
647
+ p.add_argument("--template", default="minimal", help="scaffold flavor (minimal, toolkit)")
648
+ p.set_defaults(func=_cmd_create)
649
+
650
+ p = sub.add_parser("dev", help="sandboxed dev loop / REPL against the mock Resolve API")
651
+ p.add_argument("--built", action="store_true", help="run against the consolidated single file")
652
+ p.add_argument("--repl", action="store_true", help="drop into an interactive REPL")
653
+ p.add_argument("--editor", action="store_true", help="print the in-app Resolve script to run")
654
+ p.set_defaults(func=_cmd_dev)
655
+
656
+ p = sub.add_parser("test", help="run the extension's pytest suite against the mock API")
657
+ p.add_argument("--built", action="store_true", help="test the consolidated single file")
658
+ p.add_argument("-k", dest="pattern", help="only run tests matching the expression")
659
+ p.add_argument("--api-coverage", action="store_true", help="report used-vs-mocked Resolve API methods")
660
+ p.set_defaults(func=_cmd_test)
661
+
662
+ p = sub.add_parser("analyze", help="static checks on the extension (imports, manifest, API usage)")
663
+ p.add_argument("--json", action="store_true", help="machine-readable output")
664
+ p.set_defaults(func=_cmd_analyze)
665
+
666
+ p = sub.add_parser("build", help="consolidate the multi-file package into a single file")
667
+ p.add_argument("--output", help="output path (default: dist/<manifest output>)")
668
+ p.set_defaults(func=_cmd_build)
669
+
670
+ p = sub.add_parser("package", help="assemble release artifacts into dist/")
671
+ p.add_argument("--dist", help="output directory (default: dist/)")
672
+ p.set_defaults(func=_cmd_package)
673
+
674
+ p = sub.add_parser("add", help="install a Resolve script and record it in resolvescript.json")
675
+ p.add_argument("spec", help="specifier (name, owner/repo, github:, URL, archive, file:, ./dir)")
676
+ p.add_argument("--scripts-root", help="override OS-detected Scripts root / RESOLVESCRIPT_SCRIPTS_ROOT")
677
+ p.add_argument("--target", help="override the install target (Comp, Utility, ...)")
678
+ p.add_argument("--no-save", action="store_true", help="install without recording")
679
+ p.set_defaults(func=_cmd_add)
680
+
681
+ p = sub.add_parser("install", help="materialize recorded deps, or install a single spec one-off")
682
+ p.add_argument("spec", nargs="?", help="one-off specifier (without it, installs recorded deps)")
683
+ p.add_argument("--scripts-root", help="override OS-detected Scripts root / RESOLVESCRIPT_SCRIPTS_ROOT")
684
+ p.add_argument("--locked", action="store_true", help="fail if recorded artifacts no longer satisfy ranges")
685
+ p.add_argument("--dry-run", action="store_true", help="show what would change, write nothing")
686
+ p.set_defaults(func=_cmd_install)
687
+
688
+ p = sub.add_parser("update", help="re-resolve recorded deps within their ranges")
689
+ p.add_argument("name", nargs="?", help="update only this dependency")
690
+ p.add_argument("--scripts-root", help="override OS-detected Scripts root / RESOLVESCRIPT_SCRIPTS_ROOT")
691
+ p.add_argument("--precise", help="pin an exact version")
692
+ p.add_argument("--fix", action="store_true", help="realign installed versions to manifest compat")
693
+ p.set_defaults(func=_cmd_update)
694
+
695
+ p = sub.add_parser("remove", help="uninstall a Resolve script and unrecord it")
696
+ p.add_argument("name", help="installed extension name")
697
+ p.add_argument("--scripts-root", help="override OS-detected Scripts root / RESOLVESCRIPT_SCRIPTS_ROOT")
698
+ p.add_argument("--no-save", action="store_true", help="uninstall but keep the recorded specifier")
699
+ p.set_defaults(func=_cmd_remove)
700
+
701
+ p = sub.add_parser("search", help="discover Resolve scripts (known table + conventions)")
702
+ p.add_argument("query", help="search term")
703
+ p.set_defaults(func=_cmd_search)
704
+
705
+ p = sub.add_parser("consolidate", help="merge a package directory into a single .py (no manifest needed)")
706
+ p.add_argument("package_dir", help="package directory to consolidate")
707
+ p.add_argument("--output", required=True, help="output file path")
708
+ p.set_defaults(func=_cmd_consolidate)
709
+
710
+ p = sub.add_parser("manage", help="low-level install registry operations")
711
+ manage = p.add_subparsers(dest="manage_command", metavar="<manage>", required=True)
712
+ mp = manage.add_parser("list", help="list installed extensions")
713
+ mp.add_argument("--scripts-root", help="override OS-detected Scripts root / RESOLVESCRIPT_SCRIPTS_ROOT")
714
+ mp.add_argument("--json", action="store_true", help="machine-readable output")
715
+ mp.set_defaults(func=_cmd_manage_list)
716
+ mp = manage.add_parser("remove", help="delete tracked files without touching config")
717
+ mp.add_argument("name")
718
+ mp.add_argument("--scripts-root", help="override OS-detected Scripts root / RESOLVESCRIPT_SCRIPTS_ROOT")
719
+ mp.add_argument("--all", action="store_true", help="also remove the registry entry")
720
+ mp.set_defaults(func=_cmd_manage_remove)
721
+
722
+ p = sub.add_parser("extensions", help="manage ResolveScript framework extensions (plugins)")
723
+ ext = p.add_subparsers(dest="ext_command", metavar="<ext>", required=True)
724
+ ep = ext.add_parser("add", help="install a plugin into the CLI config dir")
725
+ ep.add_argument("spec", help="plugin specifier")
726
+ ep.add_argument("--force", action="store_true", help="reinstall even if present")
727
+ ep.set_defaults(func=_skeleton("extensions add"))
728
+ ep = ext.add_parser("remove", help="uninstall a plugin")
729
+ ep.add_argument("name")
730
+ ep.set_defaults(func=_skeleton("extensions remove"))
731
+ ep = ext.add_parser("list", help="list installed plugins")
732
+ ep.set_defaults(func=_skeleton("extensions list"))
733
+
734
+ return parser
735
+
736
+
737
+ def main(argv: list[str] | None = None) -> int:
738
+ parser = build_parser()
739
+ args = parser.parse_args(argv)
740
+ command = getattr(args, "command", None)
741
+ if command is None:
742
+ parser.print_help()
743
+ return USAGE
744
+ return int(args.func(args))
745
+
746
+
747
+ if __name__ == "__main__":
748
+ sys.exit(main())