Contributing to Puls
The one rule
If you can't describe it in one line, it doesn't belong in the DSL.
Infrastructure is "give me X" - not "give me X configured with A, B, and C." The complexity lives in the implementation, not the interface.
// This is Puls
Proxmox.VM("game-server").image(OS.UBUNTU_24_04).cores(4).memory(8192).provision("config/default.yaml")
// This is not
new VM({ image: "ubuntu", cores: 4, memory: 8192, network: { bridge: "vmbr1", vlan: 2010, virtio: true }, storage: { pool: "rbd_pool", format: "raw", size: "32G" }, cloudinit: { ... } })
If a use case is too complex to fit a one-liner - that's fine. It belongs in userland TypeScript, not in the DSL. See Why Puls? for the full motivation.
What belongs in the DSL
A contribution belongs here if it:
- Hides real complexity behind a sane default. The user says what they want; the DSL figures out how.
allowPublicWeb()opens ports 80 and 443 - the user doesn't think about firewall rules. - Is idempotent by default. Running the same stack twice must always be safe. Every resource checks current state before acting.
- Follows eager discovery. The moment a resource is declared, it starts checking the real API in the background. No separate plan step, no local state files.
- Uses the constants pattern. IDs, sizes, regions, and credentials live in typed constants - not inline strings.
What doesn't belong
- Config objects with more than a handful of keys exposed to the user
- Resources that require a specific call order to work correctly
- Escape hatches that let users pass raw provider config through the DSL surface
- Anything that breaks the one-expression-per-resource model
Adding a provider
A new provider must implement:
- Eager discovery in the constructor -
this.discoveryPromise = this.discover(name) - Idempotent
deploy()- check the discovery result, skip if already correct destroy()- stop and remove the resource cleanly- Dry-run support -
this.isDryRunActive()gates every API write Output<T>fields - expose primary identifiers (IP, ARN, ID) under.outso other resources can depend on them- Constants - a types file with named environments, images, sizes, etc.
- Sane defaults - a resource declared with only a name should be deployable
The existing providers (AWS, DigitalOcean, Proxmox, Firebase) are the reference implementation. When in doubt, look at how they do it.
The bar for a PR
Before opening a pull request, ask:
Can a user express this in one line with obvious method names and no required knowledge of the underlying API?
If yes - it probably belongs here. If not - simplify the interface until it does, or ship it as a standalone package that wraps the DSL.
Questions? Join us on Discord: pulsdev.io