Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 1 addition & 1 deletion astrid/_paths.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@ def executor_argv(script_name: str, python_exec: str) -> list[str]:
``bin/`` filename (``"transcribe.py"``).
"""
stem = script_name[:-3] if script_name.endswith(".py") else script_name
return [python_exec, "-m", f"astrid.packs.builtin.{stem}.run"]
return [python_exec, "-m", f"astrid.packs.builtin.executors.{stem}.run"]
219 changes: 218 additions & 1 deletion astrid/core/element/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,16 @@
def main(argv: list[str] | None = None) -> int:
parser = build_parser()
args = parser.parse_args(argv)
# FLAG-S1-002: 'new' short-circuits BEFORE load_default_registry() so
# scaffold commands never load the built-in registry or import pack code.
if getattr(args, "command", None) == "new":
return int(args.handler(args, registry=None))
try:
registry = load_default_registry(active_theme=args.theme, project_root=REPO_ROOT)
registry = load_default_registry(
active_theme=args.theme,
project_root=REPO_ROOT,
extra_pack_roots=tuple(args.pack_root),
)
return int(args.handler(args, registry))
except (KeyError, ElementRegistryError, ElementValidationError, ValueError) as exc:
print(f"elements: {exc}", file=sys.stderr)
Expand All @@ -35,6 +43,7 @@ def build_parser() -> argparse.ArgumentParser:
prog="python3 -m astrid elements",
description="List, inspect, validate, fork, and install Astrid render elements.",
)
parser.add_argument("--pack-root", action="append", default=[], metavar="PATH", help="Extra pack root directory to discover elements from; may be repeated.")
parser.add_argument("--theme", help="Active theme id, theme directory, or path to theme.json.")
subparsers = parser.add_subparsers(dest="command", required=True)

Expand Down Expand Up @@ -73,6 +82,14 @@ def build_parser() -> argparse.ArgumentParser:
install_parser.add_argument("--apply", action="store_true", help="Run the local install commands. Default is dry-run.")
install_parser.set_defaults(handler=_cmd_install)

new_parser = subparsers.add_parser("new", help="Scaffold a new element in an existing pack.")
new_parser.add_argument("kind", choices=ELEMENT_KINDS, help="Element kind: effects, animations, or transitions.")
new_parser.add_argument(
"qualified_id",
help="Qualified element id: <pack>.<slug> (e.g., my_pack.my_effect).",
)
new_parser.set_defaults(handler=_cmd_new)

return parser


Expand Down Expand Up @@ -179,5 +196,205 @@ def _cmd_install(args: argparse.Namespace, registry: Any) -> int:
return result.returncode


# ---------------------------------------------------------------------------
# Scaffold support for ``elements new``
# ---------------------------------------------------------------------------

from astrid.core.executor.cli import _QID_RE # noqa: E402 — import for scaffold

_PLURAL_TO_SINGULAR: dict[str, str] = {
"effects": "effect",
"animations": "animation",
"transitions": "transition",
}

_ELEMENT_MANIFEST_TEMPLATE = """\
# {qualified_id} — element manifest
schema_version: 1
id: {slug}
kind: {kind_singular}
pack_id: {pack}
metadata:
label: "{slug}"
description: "TODO: describe what this element does."
whenToUse: "TODO: when to use this element."
defaults: {{}} # Add default parameter values here
schema:
type: object
properties: {{}}
dependencies:
js_packages: []
python_requirements: []
"""

_COMPONENT_TSX_TEMPLATE = """\
// {qualified_id} — React element component
// Typical imports:
// import React from 'react';
// import {{ useCurrentFrame, useVideoConfig }} from 'remotion';

import React from 'react';

interface Props {{
// Add your element's props here
[key: string]: unknown;
}}

const {ComponentName}: React.FC<Props> = (props) => {{
// TODO: implement your element
return <div>{{/* your element JSX here */}}</div>;
}};

export default {ComponentName};
"""

_ELEMENT_STAGE_MD_TEMPLATE = """\
# {qualified_id}

## Purpose

TODO: describe what this {kind_singular} does and when to use it.

## Inputs / Props

TODO: list the props this element accepts.

## Outputs

TODO: describe what this element renders or produces.

## Dependencies

TODO: any JS packages, Python requirements, or other elements this depends on.
"""


def _cmd_new(args: argparse.Namespace, registry: Any) -> int:
"""Scaffold a new element into an existing pack (CWD-relative).

Short-circuits before ``load_default_registry()`` — never imports pack code.
"""
from pathlib import Path

from astrid.packs.validate import validate_pack

qualified_id: str = args.qualified_id
kind_plural: str = args.kind

# --- 1. Validate the qualified id ------------------------------------------
if not _QID_RE.fullmatch(qualified_id):
print(
f"elements new: qualified id {qualified_id!r} must be "
f"'<pack>.<slug>' with letters/digits/underscore",
file=sys.stderr,
)
return 2

pack, slug = qualified_id.split(".", 1)

# --- 2. Derive singular kind ------------------------------------------------
kind_singular = _PLURAL_TO_SINGULAR.get(kind_plural)
if kind_singular is None:
print(
f"elements new: unknown kind {kind_plural!r}; "
f"expected one of {', '.join(ELEMENT_KINDS)}",
file=sys.stderr,
)
return 2

# --- 3. Find the target pack root (CWD-relative) ---------------------------
pack_root = Path.cwd().resolve()
pack_yaml = pack_root / "pack.yaml"
if not pack_yaml.is_file():
print(
f"elements new: pack.yaml not found at {pack_root}. "
f"Scaffold the pack first with: python3 -m astrid packs new {pack}",
file=sys.stderr,
)
return 1

# Verify the pack id in pack.yaml matches
import yaml as _yaml_module
try:
with open(pack_yaml, "r", encoding="utf-8") as fh:
doc = _yaml_module.safe_load(fh)
except Exception as exc:
print(f"elements new: cannot read {pack_yaml}: {exc}", file=sys.stderr)
return 1

if isinstance(doc, dict) and doc.get("id") != pack:
print(
f"elements new: pack id mismatch — {qualified_id!r} expects "
f"pack id {pack!r} but {pack_yaml} has id {doc.get('id')!r}",
file=sys.stderr,
)
return 1

# --- 4. Determine the elements content root ---------------------------------
content = doc.get("content", {}) if isinstance(doc, dict) else {}
rel_dir = content.get("elements", "elements")
elements_root = pack_root / rel_dir
element_dir = elements_root / kind_plural / slug

# --- 5. Reject overwrite collisions -----------------------------------------
if element_dir.exists():
print(
f"elements new: {element_dir} already exists; refusing to overwrite",
file=sys.stderr,
)
return 1

# --- 6. Create the scaffold -------------------------------------------------
element_dir.mkdir(parents=True)
created: list[str] = []

# Element manifest (element.yaml)
manifest_path = element_dir / "element.yaml"
manifest_text = _ELEMENT_MANIFEST_TEMPLATE.format(
qualified_id=qualified_id, pack=pack, slug=slug, kind_singular=kind_singular
)
manifest_path.write_text(manifest_text, encoding="utf-8")
created.append(str(manifest_path.relative_to(pack_root)))

# component.tsx stub
tsx_path = element_dir / "component.tsx"
# Derive a PascalCase component name from the slug
component_name = "".join(part.capitalize() for part in slug.replace("-", "_").split("_"))
tsx_text = _COMPONENT_TSX_TEMPLATE.format(
qualified_id=qualified_id, ComponentName=component_name
)
tsx_path.write_text(tsx_text, encoding="utf-8")
created.append(str(tsx_path.relative_to(pack_root)))

# STAGE.md stub
stage_md_path = element_dir / "STAGE.md"
stage_md_text = _ELEMENT_STAGE_MD_TEMPLATE.format(
qualified_id=qualified_id, kind_singular=kind_singular
)
stage_md_path.write_text(stage_md_text, encoding="utf-8")
created.append(str(stage_md_path.relative_to(pack_root)))

# --- 7. Validate the pack after scaffolding ---------------------------------
errors, warnings = validate_pack(pack_root)
if errors:
print(
f"elements new: scaffolded element fails validation "
f"({len(errors)} error(s))",
file=sys.stderr,
)
for err in errors:
print(f" {err}", file=sys.stderr)
return 1

# --- 8. Report --------------------------------------------------------------
for rel in created:
print(f"created {rel}")
if warnings:
for w in warnings:
print(f"warning: {w}", file=sys.stderr)
print(f"element {qualified_id!r} created and validated")
return 0


if __name__ == "__main__":
raise SystemExit(main())
27 changes: 22 additions & 5 deletions astrid/core/element/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from typing import Iterable

from astrid._paths import REPO_ROOT
from astrid.core.pack import discover_packs, iter_element_roots, validate_element_pack_id
from astrid.core.pack import PackResolver, discover_packs, iter_element_roots, packs_root, validate_element_pack_id

from .schema import ELEMENT_KINDS, ElementDefinition, ElementKind, ElementValidationError, load_element_definition

Expand Down Expand Up @@ -104,9 +104,13 @@ def load_default_registry(
active_theme: str | Path | None = None,
project_root: str | Path = REPO_ROOT,
include_missing_roots: bool = False,
extra_pack_roots: tuple[str, ...] = (),
include_installed: bool = True,
) -> ElementRegistry:
registry = ElementRegistry()
for element in load_pack_elements():
for element in load_pack_elements(
extra_pack_roots=extra_pack_roots, include_installed=include_installed
):
registry.register(element)
for source in default_sources(active_theme=active_theme, project_root=project_root):
if not source.root.exists():
Expand All @@ -132,11 +136,22 @@ def default_sources(*, active_theme: str | Path | None = None, project_root: str
return tuple(sources)


def load_pack_elements() -> tuple[ElementDefinition, ...]:
def load_pack_elements(
*,
extra_pack_roots: tuple[str, ...] = (),
resolver: PackResolver | None = None,
include_installed: bool = True,
) -> tuple[ElementDefinition, ...]:
elements: list[ElementDefinition] = []
for pack in discover_packs():
if resolver is None:
all_roots = [packs_root(), *extra_pack_roots]
if include_installed:
from astrid.core.pack_store import installed_pack_roots
all_roots.extend(installed_pack_roots())
resolver = PackResolver(*all_roots)
for pack in resolver.packs:
priority = 10 if pack.id == "local" else 30
for kind, root in iter_element_roots(pack):
for kind, root in resolver.iter_element_roots(pack):
element = load_element_definition(
root,
kind=kind,
Expand All @@ -145,6 +160,8 @@ def load_pack_elements() -> tuple[ElementDefinition, ...]:
priority=priority,
)
validate_element_pack_id(element.metadata.get("pack_id"), pack, element_root=root)
# Note: duplicate (kind, id) pairs are expected across packs.
# The ElementRegistry resolves them by priority (local > builtin).
elements.append(element)
return tuple(elements)

Expand Down
18 changes: 14 additions & 4 deletions astrid/core/element/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,10 +179,20 @@ def _element_manifest_path(root: Path) -> Path | None:

def _read_manifest(path: Path) -> dict[str, Any]:
text = path.read_text(encoding="utf-8")
try:
data = json.loads(text)
except json.JSONDecodeError as exc:
raise ElementValidationError(f"{path}: invalid manifest JSON: {exc.msg}") from exc
suffix = path.suffix.lower()
if suffix in (".yaml", ".yml"):
try:
import yaml as _yaml
data = _yaml.safe_load(text)
except ImportError:
raise ElementValidationError(f"{path}: YAML support requires PyYAML") from None
except Exception as exc:
raise ElementValidationError(f"{path}: invalid manifest YAML: {exc}") from exc
else:
try:
data = json.loads(text)
except json.JSONDecodeError as exc:
raise ElementValidationError(f"{path}: invalid manifest JSON: {exc.msg}") from exc
if not isinstance(data, dict):
raise ElementValidationError(f"{path} must contain an object")
return data
Expand Down
2 changes: 0 additions & 2 deletions astrid/core/executor/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@
ExecutorRunResult,
ExecutorRunnerError,
build_executor_command,
build_pipeline_context,
check_executor_binaries,
evaluate_conditions,
run_executor,
Expand Down Expand Up @@ -72,7 +71,6 @@
"IsolationMetadata",
"build_executor_command",
"build_executor_install_plan",
"build_pipeline_context",
"check_executor_binaries",
"discover_folder_executor_roots",
"evaluate_conditions",
Expand Down
Loading