BigConfig Package Spec
A friendly implementation guide for building a repository that bc-pkg can pin, initialize, and run. The launcher contract is intentionally small: one language manifest, one root run file, installable package code, and a command surface that forwards cleanly to your BigConfig lifecycle.
bc-pkg. There are Python and npm implementations of the launcher; there is no Clojure implementation of bc-pkg itself, but both launchers can run Clojure target packages.
Quick checklist
If these are true, your package is usually ready for a first launcher test:
Detectable
Exactly one root manifest exists: deps.edn, package.json, or pyproject.toml. TypeScript and Python manifests declare a package name.
Runnable
A root run file exists, forwards argv, and calls your CLI/workflow with a safe default profile or options map.
Installable
The package works from a GitHub commit SHA and from a local path. Generated code, built JS, and template resources are included.
Safe
No credentials ship in source. Destructive actions require an explicit user-controlled setting or environment override.
Package shape
A BigConfig package is just a normal language package with one extra convention: a root run file that becomes the user-facing entrypoint after initialization.
my-package/
├── package.json # exactly one root manifest
├── run # copied by GitHub mode, symlinked by local mode
├── src/
│ ├── cli.ts
│ └── resources/...
└── dist/ # built JS included for install/run
my-package/
├── pyproject.toml # exactly one root manifest
├── run # copied by GitHub mode, symlinked by local mode
└── src/
├── my_package/
└── resources/...
my-package/
├── deps.edn # exactly one root manifest
├── bb.edn # useful in the package repo; generated for consumers too
├── run # copied by GitHub mode, symlinked by local mode
└── src/
├── clj/...
└── resources/...
deps.edn, package.json, and pyproject.toml as ambiguous. Keep each launcher target in its own language-specific root.
Launcher contract
what bc-pkg doesThe launcher does not know your domain model. It only resolves a target, writes a small consumer project in the current directory, then forwards the remaining arguments to your copied run file.
Repository root contract
| File | Used for | Notes |
|---|---|---|
deps.edn | Clojure target detection | Selected only when no package.json or pyproject.toml exists at the root. |
package.json | TypeScript target detection | name is used as the dependency name in the generated consumer manifest. |
pyproject.toml | Python target detection | [project].name is used as the dependency name in the generated consumer manifest. |
run | User-facing entrypoint | Required. GitHub targets copy it; local targets symlink it where possible. |
Language-specific manifest notes
Clojure CLJ
Declare normal Clojure deps and include resources in :paths. The launcher synthesizes the GitHub coordinate as io.github.<owner>/<repo> for GitHub targets.
Python PY
Expose [project].name, keep the package installable by uv, and include template resources in the wheel/sdist. With Hatch, force-include is one option.
TypeScript TS
Expose name, include the root run, resources, and built JavaScript in files. Local mode expects the built runtime target to exist.
// package.json (compact shape)
{
"name": "my-package",
"type": "module",
"bin": { "my-package": "./dist/src/cli.js" },
"files": ["dist/src", "src/resources", "run"]
}
# pyproject.toml (compact shape)
[project]
name = "my-package"
requires-python = ">=3.12"
dependencies = ["big-config @ git+https://github.com/..."]
[tool.hatch.build.targets.wheel]
packages = ["src/my_package"]
force-include = { "src/resources" = "src/resources" }
;; deps.edn (compact shape)
{:paths ["src/clj" "src/resources"]
:deps {io.github.bigconfig-ai/big-config {:git/sha "..."}}}
Generated consumer files
On first run, bc-pkg creates or updates a small native project in the current directory. The metadata records the chosen target and prevents accidental switches later.
| Target language | Generated files | Runtime command | Dependency form |
|---|---|---|---|
| Clojure | run, deps.edn, bb.edn | bb run ... | :git/url + :git/sha, or :local/root |
| TypeScript | run, package.json | node run ... | github:owner/repo#sha, or file: |
| Python | run, pyproject.toml | uv run python run ... | Git PEP 508 dependency, or editable [tool.uv.sources] |
Local development mode
For live package development, pass a local path as the first argument. Paths starting with /, ./, ../, ~, or exactly ./.. are treated as local targets.
# Python launcher
uvx bc-pkg ../once/python package build
# npm launcher
npx bc-pkg ../once/typescript package build
# Clojure target through either launcher
uvx bc-pkg /abs/path/to/once/clojure package validate
run so run-file edits are picked up immediately, but it refuses to target the current directory. Run the launcher from a separate consumer directory so it cannot overwrite the package's own manifest.
Pins, updates, and re-entry
- A GitHub branch or tag is resolved once to a full 40-character commit SHA.
- Subsequent runs read the generated BigConfig metadata and reuse the pinned target.
- Passing the same repo/ref again is allowed only when it resolves to the same SHA.
- Switching repo, ref, SHA, local path, or local-vs-GitHub mode is a hard error rather than an implicit upgrade.
To intentionally move to a new package revision, use a new consumer directory or deliberately remove the generated consumer files and re-initialize with care.
Package contract
what authors provideRoot run file
The root run file is the package's handoff to the user. Treat it as both an executable launcher and a small editable config template. It should forward argv and pass a safe default profile/options map into your package CLI or workflow.
#!/usr/bin/env node
const DEFAULT_PROFILE = {
profile: "default",
params: { domain: "REPLACE_ME", package: "default" },
};
const { main } = await import("my-package/dist/src/cli.js");
main(process.argv.slice(2), DEFAULT_PROFILE);
#!/usr/bin/env python3
import sys
from my_package.cli import main
DEFAULT_PROFILE = {
"profile": "default",
"params": {"domain": "REPLACE_ME", "package": "default"},
}
if __name__ == "__main__":
main(sys.argv[1:], DEFAULT_PROFILE)
#!/usr/bin/env bb
(require '[my.package.cli :as cli])
(def default-profile
{:big-config.render/profile "default"
:big-config.workflow/params {:domain "REPLACE_ME"
:package "default"}})
(cli/main* *command-line-args* default-profile)
run file lands in a user's working directory. Use placeholders like REPLACE_ME, never real tokens, SSH keys, API passwords, or customer-specific infrastructure IDs.
Command surface
The launcher forwards arguments exactly as written. Your package decides what commands mean. A BigConfig package is easiest to understand when it exposes a small lifecycle vocabulary:
| Command | Purpose |
|---|---|
package validate | Pre-flight checks: profile shape, tools, credentials, reachable dependencies, image references, and other safe checks. |
package build | Render artifacts into .dist/ without applying changes. |
package create | Create or reconcile real resources. |
package describe | Report the current state after provisioning. |
package delete | Tear down resources, usually behind explicit safety settings. |
Packages can support chaining, for example package validate create or package delete create, when their workflow engine composes named steps in order.
BigConfig lifecycle
BigConfig workflows thread an opts map/dict/object through step functions. Reserved engine keys are namespaced strings such as big-config/exit, big-config/err, big-config.workflow/steps, and big-config.workflow/params. Keep those boundary keys stable and keep shell execution behind the runner seam so package tests can run without spawning real tools.
Workflow steps
Use workflow, workflow_star / createWorkflowStar, and run_steps / runSteps to assemble lifecycle commands from smaller tool workflows.
Pure reports
Keep report builders like validation and describe pure, with side effects injected. That lets agents and CI verify behavior without provisioning resources.
Profiles, params, and environment overrides
Profile params should use kebab-case strings that match template variable names. Runtime overrides use BC_PAR_*: uppercase the key and replace hyphens or dots with underscores.
export BC_PAR_DOMAIN="example.com"
export BC_PAR_PROVIDER_BACKEND="r2"
export BC_PAR_COMPUTE_PREVENT_DESTROY="false"
Profiles can be composed from smaller provider/application maps, as once does for compute provider, SMTP, DNS, backend, deploy keys, and application definitions.
Templates and resources
Put template trees in packaged resources and make sure they ship in the installable artifact. The common convention is src/resources/<namespace-path>/tools/.... The renderer copies selected templates into .dist/.
| Language | Resource packaging reminder |
|---|---|
| Clojure | Add src/resources to :paths. |
| Python | Include resources in the wheel/sdist; force-include is one Hatch option. |
| TypeScript | Include src/resources in files and resolve it from built code. |
Many BigConfig packages use custom Selmer delimiters for file content. Once configures tag open <, tag close >, filter open {, and filter close }, so template variables are written as <{ var }>. That leaves literal {{ ... }} available for downstream tools like Ansible.
[[provider, delimiters]], so a render step copies only the matching provider subdirectory.
Safety conventions
- Generated output belongs in
.dist/; never edit it as source. - Destructive operations should have an explicit guard or profile parameter.
- Validation should be opt-in and runnable before create, but packages can let users chain
validate create. - Keep credentials in the user's environment or private ignored files, not in package source or copied
runfiles.
Author test matrix
Before publishing a package ref, test both the GitHub and local path flows in a disposable consumer directory.
# GitHub target through both launchers where practical
uvx bc-pkg owner/repo@ref package validate
npx bc-pkg owner/repo@ref package validate
# Re-entry should omit the target and use generated metadata
uvx bc-pkg package build
npx bc-pkg package build
# Local live-development target
uvx bc-pkg ../path/to/package package build
npx bc-pkg ../path/to/package package build
- Delete the generated
runfile and re-run to confirm restoration works. - Try an intentionally different repo/ref/local path and confirm the launcher refuses the switch.
- Inspect generated manifests for correct metadata and dependency form.
- Search copied files for accidental credentials.
- If your package promises deterministic
buildoutput, compare the generated.dist/artifact.
Troubleshooting
| Message / symptom | Likely fix |
|---|---|
has no deps.edn, package.json, or pyproject.toml | Add exactly one supported root manifest. |
ambiguous; found ... manifests | Split language implementations into separate roots or worktrees. |
has no run | Add a root run file and make it executable on POSIX systems. |
| Existing non-bc-pkg manifest conflict | Initialize in a fresh consumer directory or migrate the manifest carefully. |
Missing node, npm, or uv | Install the runtime required by the target language. |
| Private repo or rate-limit failure | Set GITHUB_TOKEN and retry. |
| Clojure target cache problems | Remove the relevant bc-pkg cache directory for Babashka/JDK and retry; install git manually if auto-install is unavailable. |
Future conformance checklist/test
A future conformance tool could make this checklist executable. The rough shape is a small harness that creates a temporary consumer directory, runs both launchers against GitHub and local targets, verifies generated metadata, tests re-entry and refusal semantics, restores a missing run file, and executes a harmless lifecycle command such as package validate or package build.
That harness is intentionally not part of this page. For now, this spec is the human-readable guide package authors can use while implementing and reviewing packages.