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.
- resolve_script/__init__.py +3 -0
- resolve_script/analyze.py +277 -0
- resolve_script/cli.py +748 -0
- resolve_script/config.py +62 -0
- resolve_script/consolidate.py +604 -0
- resolve_script/fetch.py +90 -0
- resolve_script/install/__init__.py +49 -0
- resolve_script/install/discovery.py +57 -0
- resolve_script/install/installer.py +397 -0
- resolve_script/install/registry.py +106 -0
- resolve_script/manifest/__init__.py +1 -0
- resolve_script/manifest/json_reader.py +40 -0
- resolve_script/manifest/model.py +316 -0
- resolve_script/manifest/validation.py +81 -0
- resolve_script/manifest/xml_reader.py +162 -0
- resolve_script/package.py +103 -0
- resolve_script/resolver.py +204 -0
- resolve_script/sandbox/__init__.py +38 -0
- resolve_script/sandbox/api.py +393 -0
- resolve_script/sandbox/env.py +82 -0
- resolve_script/sandbox/loader.py +72 -0
- resolve_script/sandbox/repl.py +57 -0
- resolve_script/sandbox/smoke.py +104 -0
- resolve_script/scaffold.py +126 -0
- resolve_script/semver.py +236 -0
- resolve_script/sources/__init__.py +15 -0
- resolve_script/sources/archive.py +82 -0
- resolve_script/sources/git.py +107 -0
- resolve_script/sources/known.py +47 -0
- resolve_script/sources/release.py +55 -0
- resolve_script/spec.py +137 -0
- resolve_script/templates/extension/@NAME@/__init__.py +7 -0
- resolve_script/templates/extension/@NAME@/menu.py +12 -0
- resolve_script/templates/extension/@NAME@.py +13 -0
- resolve_script/templates/extension/README.md +20 -0
- resolve_script/templates/extension/conftest.py +13 -0
- resolve_script/templates/extension/manifest.json.j2 +23 -0
- resolve_script/templates/extension/manifest.xml.j2 +24 -0
- resolve_script/templates/extension/tests/test_smoke.py +26 -0
- resolve_script/templates/inapp/register.py +28 -0
- resolve_script/testing/__init__.py +6 -0
- resolve_script/testing/fixtures.py +47 -0
- resolve_script/workspace.py +66 -0
- resolvescript-0.1.2.dist-info/METADATA +146 -0
- resolvescript-0.1.2.dist-info/RECORD +49 -0
- resolvescript-0.1.2.dist-info/WHEEL +5 -0
- resolvescript-0.1.2.dist-info/entry_points.txt +2 -0
- resolvescript-0.1.2.dist-info/licenses/LICENSE +21 -0
- 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())
|