Extending Protostar¶
Protostar's architecture strictly isolates state definition from execution. This guarantees that you can add entirely new languages, tools, or domain workflows without altering the core orchestrator or the system executor.
-
Bootstrap Modules
Define the foundational environment footprint (languages, core tooling). Evaluated during
protostar init. -
Preset Modules
Lighter wrappers that inject domain-specific dependencies and directories onto a bootstrap foundation.
Building a Custom Bootstrap Module¶
Bootstrap modules define the structural environment footprint. To create a new module, subclass BootstrapModule from protostar.modules.base.
You must define its CLI flags, a human-readable name, and the build method. You can also optionally define pre_flight checks, collision_markers, and required_languages to enforce strict footprint constraints.
Dynamic CLI Registration
The CLI parser dynamically reads the cli_flags and cli_help attributes at runtime. Once you append your module to the TOOLING_MODULES tuple in protostar/modules/__init__.py, it will automatically appear in the protostar init --help output.
Here is a complete example of a module that scaffolds a justfile (a modern Makefile alternative):
from pathlib import Path
from protostar.modules import BootstrapModule
from protostar.manifest import EnvironmentManifest
class JustModule(BootstrapModule):
"""Configures a justfile for project task execution."""
cli_flags = ("--just",)
cli_help = "Scaffold a standard justfile for project tasks"
config_key = "just"
@property
def name(self) -> str:
return "Just"
@property
def collision_markers(self) -> list[Path]:
return [Path("justfile")]
def pre_flight(self) -> None:
import shutil
if not shutil.which("just"):
raise RuntimeError("Missing dependency: 'just' is not installed.")
def build(self, manifest: EnvironmentManifest) -> None:
content = """default:
\t@just --list
lint:
\tuv run ruff check .
\tuv run ruff format --check .
test:
\tuv run pytest
"""
manifest.add_file_injection("justfile", content)
Core Interface: BootstrapModule
protostar.modules.base.BootstrapModule ¶
Bases: ABC
Appends module-specific requirements to the environment manifest.
Source code in src/protostar/modules/base.py
cli_flags
class-attribute
¶
The CLI flags to trigger this module (e.g., ('-p', '--python')).
config_key
class-attribute
¶
The global configuration key used to evaluate if this module is active.
aliases
property
¶
Returns a list of configuration aliases that map to this module.
Used for dynamic resolution from the global configuration file.
collision_markers
property
¶
Returns a list of critical filesystem paths to evaluate for collisions during pre-flight.
Returns:
| Type | Description |
|---|---|
list[Path]
|
A list of Path objects representing critical configuration files or directories |
list[Path]
|
managed by this module. Defaults to an empty list. |
pre_flight ¶
Verifies system prerequisites before manifest building begins.
Raises:
| Type | Description |
|---|---|
RuntimeError
|
If a critical dependency (e.g., 'uv', 'cargo') is missing. |
build
abstractmethod
¶
Appends module-specific requirements to the environment manifest.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
manifest
|
EnvironmentManifest
|
The centralized state object. |
required |
Deep Dive: Pre-flight vs Build
pre_flight(): Executes before any state changes occur. Ifshutil.which("just")fails here, the orchestrator immediately halts, guaranteeing the environment remains untouched.build(): Only queues state changes. Notice how we usemanifest.add_file_injection()instead ofPath("justfile").write_text().
Building a Custom Domain Preset¶
Presets sit on top of the base language footprint. They inherit from PresetModule in protostar.presets.base and strictly define arrays of dependencies and directory structures.
from protostar.presets import PresetModule
class DataEngineeringPreset(PresetModule):
"""Injects ETL and data pipeline dependencies."""
cli_flags = ("--data-eng",)
cli_help = "Inject data engineering dependencies"
@property
def name(self) -> str:
return "Data Engineering"
@property
def default_dependencies(self) -> list[str]:
return ["polars", "pyarrow", "duckdb", "dbt-core"]
@property
def default_directories(self) -> list[str]:
return ["pipelines", "data/raw", "data/processed", "tests/data"]
@property
def default_ignores(self) -> list[str]:
return ["*.parquet", "*.duckdb", "dbt_packages/"]
Domain-Specific Dependencies: PresetModule
protostar.presets.base.PresetModule ¶
Bases: ABC
Appends module-specific requirements to the environment manifest.
Source code in src/protostar/presets/base.py
cli_flags
class-attribute
¶
The CLI flags to trigger this preset (e.g., ('-a', '--astro')).
default_dependencies
property
¶
Returns a list of default packages to inject for this preset.
default_directories
property
¶
Returns a list of default directories to scaffold for this preset.
default_ignores
property
¶
Returns a list of default VCS ignore patterns for this preset.
build ¶
Appends preset-specific dependencies and directories to the manifest.
Automatically applies configuration overrides if present. Otherwise, injects the default packages, directories, and ignores defined by the preset subclass.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
manifest
|
EnvironmentManifest
|
The centralized state object. |
required |
Source code in src/protostar/presets/base.py
Configuration Overrides
Register your preset in protostar/presets/__init__.py. Protostar automatically handles merging any user-defined overrides for these defaults found in their global config.toml.
The Manifest API¶
No Direct Disk I/O
Never call subprocess.run or write to disk inside a module's build() method. Modules must strictly communicate via the EnvironmentManifest to ensure the Orchestrator maintains atomicity.
The manifest exposes the following methods to queue state changes:
| Method Signature | Execution Behavior |
|---|---|
add_dependency(package: str) |
Queues a standard package for resolution. |
add_dev_dependency(package: str) |
Queues a development or tooling package. |
add_file_injection(path: str, content: str) |
Queues a complete file write. Fails if the file exists unless explicitly marked for overwrite. |
add_file_append(path: str, content: str) |
Queues a string payload for late-binding concatenation or TOML AST deep-merging. |
add_system_task(command: list[str], timeout: int | None = 30, description: str | None = None) |
Queues a subprocess command to execute after the disk scaffolding phase is complete. Allows an optional execution timeout and UI description. |
add_post_install_task(command: list[str], timeout: int | None = 30, description: str | None = None) |
Queues a subprocess command to execute after all dependencies have been installed. Allows an optional execution timeout and UI description. |
add_vcs_ignore(path: str) |
Appends a tracking exclusion entry to the version control ignore manifest (e.g., .gitignore). |