salpa-cli
The Salpa node authoring CLI. Scaffolds node packages that match the shipped template contract — the deterministic path to a new node — checks them against that contract, and puts them into the Salpa app on your machine.
python3 -m pip install salpa-cli
salpa new my-analyzer # interactive
salpa new my-suite -t multi-node-package --yes
salpa validate my_suite # will it install and register?
salpa smoke my_suite # does it actually run?
salpa env status # what is built, and which checks would run
salpa push my_suite # into your running Salpa app
salpa unpush my_suite # and back out again
Installing adds the salpa command. If your shell can't find salpa afterward,
your Python scripts directory isn't on your PATH —
installing into a virtual environment is the simplest fix (it puts salpa on PATH).
Requirements
Python 3.9 or newer. pip install salpa-cli reports a clear error below that.
- macOS — the system
python3is 3.9, which is enough. For a newer one:brew install python, or python.org. - Linux — most distributions ship 3.9+. Otherwise use your package manager
(
apt install python3,dnf install python3, …) or pyenv. - Windows — often has no
python3at all; install from python.org or the Microsoft Store, and tick "Add python.exe to PATH".
pixi — needed only for pixi run test (building a node's
environment and running its tests). You do not have to install it separately if
you run Salpa: the app keeps a pixi under ~/.bocoflow, and salpa new finds it
and prints its path. To have one on your own PATH, install it from
pixi.sh. Stdlib-only nodes need no pixi at all.
salpa new creates ./<under_scored_name>/ from a bundled template, with every
placeholder substituted and the directory underscored (the dir name becomes the
Python package name, so a hyphen would break from .core import ...; meta.toml's
name stays kebab-case).
Templates
Templates ship inside the wheel and are the single source of truth for the node contract, so there is no second copy to drift.
| Template | For |
|---|---|
individual-node |
one node with its own pixi env (default) |
multi-node-package |
several related nodes sharing one pixi env, each runnable on its own |
Options
salpa new [NAME]
-t, --template individual-node | multi-node-package
-d, --description one-line description
--author author name
--category UI category (e.g. "Cheminformatics")
-o, --output directory to create the package in (default: .)
-y, --yes non-interactive: accept defaults, no prompts
--install DEPRECATED — use `salpa env install`
Validating
salpa validate [PATH] checks a package against the contract the app enforces at
install and registration time. Every check maps to a real failure — a node missing
from [package.nodes], a shared-environment name that drifted between three files,
a stray per-node pixi.toml that silently opts a node out of the shared env. It is
not a linter: style is not checked.
salpa validate [PATH] # default: the current directory
--strict # warnings become errors (CI; except H2 — see below)
--ignore CODE # suppress one check by code, repeatable
--json # machine-readable report
--no-import # skip the import checks
--python PATH # override the auto-detected import-check interpreter
Exit codes: 0 clean · 1 at least one error · 2 the path is not a package.
Findings are ERROR (will not install or will not register) or WARN (installs,
but is probably not what you meant). Each carries a stable code (E3, S2, …) so
it can be quoted, grepped, and suppressed. salpa docs codes lists every one
of them with what it means.
Import checks. I1-I3 import your node.py in a subprocess to confirm the
app can read its OPTIONS. They need the authoring SDK (bocoflow_core), and the
interpreter that has it is found for you — your package already declares the SDK in
its own pixi.toml, so its solved test environment is the right one to judge the
package against:
salpa env install # solve the test env — the one your pixi.toml puts the SDK in
salpa validate # the import checks now run, no flags
They are skipped by default, and the summary says so. Without the SDK there is
no way to import your node.py, so salpa validate reports the structural checks
only and marks the count:
0 errors, 3 warnings · import checks DID NOT RUN
Worth taking seriously rather than reading as a pass — I1 is the check that catches
a node.py the app cannot load at all, which no amount of structural checking will
find.
The search order, first hit wins: .pixi/envs/test → .pixi/envs/default → the
interpreter running salpa. A candidate that cannot import bocoflow_core is
passed over, and the report names the one that answered. pip install bocoflow-core-sdk beside salpa works too, and --python PATH overrides the
search entirely (a --python that lacks the SDK skips rather than falling back —
naming an interpreter is an instruction, not a hint).
With no interpreter at all, the checks are skipped, and both the status line and
the summary say so — No findings (import checks skipped) is a smaller claim than
No findings. This package should install and register., and reads as one.
Suppressing a check. Some warnings describe a shape that is occasionally
correct — E3 (a per-node pixi.toml) is a deliberate opt-out when a node's
dependencies conflict with its package's shared environment. --ignore lets such a
package pass --strict without silencing everything:
salpa validate --strict --ignore E3
Suppressed findings are still reported (and appear under suppressed in --json)
— they are hidden from the exit code, not from you.
One code --strict does not promote. H2 reports build artifacts
(__pycache__, .pytest_cache), and the documented order is pixi run test then
salpa validate — so the test run creates exactly what the next command reports.
Promoting that would fail a package for having been tested, which it did: 37 of the
39 first-party packages were rejected, 36 of them on H2 alone. It stays a WARN,
still reported and still counted. It is right about a published tree and wrong
about a working one, so it is enforced at publish time instead.
salpa new runs a validation pass over its own output and prints a one-line
summary. That pass covers structure, naming and environment only; the import
checks are left to salpa validate.
Smoke-testing
salpa validate proves a package installs and registers, and every check it
makes is static — a node whose execute() raises on its own sample input passes it
cleanly. salpa smoke [PATH] closes that gap by running the thing:
salpa smoke [PATH] # default: the current directory
--python PATH # interpreter to run the node with (same resolution
# as `validate`; name it explicitly in automation)
--timeout SECONDS # per node, default 300 — covers all of its runs
--json # machine-readable report
Exit codes: 0 every node behaved · 1 one did not, or the checks could not
run · 2 the path is not a package.
Three checks:
- smoke — import
node.py, callexecute()on a file fromdemo_data/, and requiresuccesswith a non-trivial payload. - negative — run it again with its real input taken away: a path that does not
exist for a node that reads a file, no upstream for a node that reads
predecessor_data. A node that still reports success is doingexcept: success = True, which is the most common way a broken node looks fine. Raising here is a pass; claiming success is a failure. If a node has neither kind of input, the report says the check did not apply — "not checked" must not look like "checked and fine". - determinism — the same input twice gives the same payload.
- idempotence — running twice into the same output directory gives the same result. Catches the classic self-input leak: a node that scans its working directory for input and writes its output there picks up its own previous output on a re-run.
- path-independence — the same input read from a different directory still works, and still gives the same content. Catches a hardcoded path.
The last three are advisory — reported, never blocking. A node that samples may legitimately be non-deterministic, and a node that appends may legitimately be non-idempotent. They exist to tell you something a static check cannot.
Each node is executed up to three times — once for smoke, once for negative, and
once more for determinism (the smoke run is reused as the first sample rather than
running a fourth). All three share one --timeout budget, so raise it for an
expensive node.
Multi-node packages are not chained. Every node runs on its own demo_data/ and
its own declared parameters, because a package manifest gives its membership and
never its connections — and guessing at connections produced both false failures
and undetectable false passes.
That is also the bar a node should meet: every input is a parameter, and
predecessor_data only fills one in when the canvas left it blank. A node that can
only be driven by its upstream cannot be tested on its own, which is a gap in the
node. Smoke reports such a node as not checked — never a failure, and never a
pass.
Telling it how to run your node
node.py carries a DEMO_CONFIG dict — parameter name to value, with demo_data/… strings
resolved relative to the node. The scaffolded node.py test builds its flow_vars from the same
dict, so the declaration has exactly one source and the test proves it is right.
Without it, a checker can only infer from a parameter's type — which gives its shape and never its value. It cannot know which of two files is the topology, and a node driven by an ID has no file to infer from. Anything undeclared is still inferred, so it is worth adding incrementally.
What it does not prove
That the node is correct. It proves the node runs, returns something, and refuses bad input. A node whose science is wrong — or absent — passes all three as long as it returns a plausible dict. No checker that reads a package in isolation can do better: that needs an expected answer the package does not contain. Green here means "nothing obviously broken", which is a smaller claim than it reads as.
The development environment
Your node's dependencies live in a pixi environment beside the package. salpa env
manages it — it never touches the Salpa app's environments, which are shared between
packages and removed by reference count.
salpa env status [PATH] what is built, how big, and which checks would run now
salpa env install [PATH] build the test environment (`pixi install -e test`)
salpa env clean [PATH] remove the package's .pixi/ (keeps pixi.lock)
status answers the question that otherwise has no home — not what exists, but
what would happen:
$ salpa env status
my_analyzer (single)
test 412.6 MB python 3.12.13 SDK 0.1.1
default not built
total 412.6 MB — `salpa env clean` reclaims it
Right now:
salpa validate import checks will RUN (.pixi/envs/test/bin/python)
salpa smoke will run with .pixi/envs/test/bin/python
pixi run test ready
Why test and not the default environment. The default one deliberately excludes
the authoring SDK — it would shadow the runtime bocoflow_core the app provides — so a
package built that way cannot run its tests, validate's import checks, or
salpa smoke. install always builds test, which carries your dependencies as well.
pixi run test builds that environment on demand, so install is a convenience rather
than a requirement. clean is not: a trivial package's environment is ~100 MB and one
pulling AmberTools or GROMACS is gigabytes, and nothing else reclaims it.
Pushing it into Salpa
salpa push [PATH] makes a package appear in your running Salpa app's
Marketplace ▸ Browse, ready to install.
It is not publishing. It targets the app on your machine; nothing is uploaded anywhere, and there is no public hub involved.
salpa push [PATH]
--copy copy into the app instead of linking your directory
--source-id id for this source in the app
--source-name display name for this source
--port port the app is listening on (default 18000, or $SALPA_PORT)
--force push despite `salpa validate` errors; with --copy, also
replace a package copy that this push did not create
-y, --yes accept the layout change without asking
Exit codes: 0 ok · 1 refused or failed · 2 the path is not a package.
push validates, arranges your package into the layout a package source uses
(registry.json + packages/<pkg>/, asking before it moves anything), generates
the catalog file, and asks the app to sync it and put the package on the shelf.
The install itself stays in the app. push stops at the shelf — installing is
user-consented in the UI, and runs the same installer every other package goes
through. There is no second installer in this CLI, and no assumption anywhere about
where the app keeps its files: the app is asked, over the API its own UI uses.
Salpa must therefore be running; if it isn't, push says so and stops.
By default your directory is the source — nothing is copied, so re-running after an edit refreshes what the app offers:
salpa push packages/my_suite # edit, push, repeat
--copy copies the package into the app instead. Self-contained and survives
moving your working copy, at the cost of needing another --copy push per edit.
It lands in the directory the app keeps its own packages in, so it will replace an
earlier copy of yours but refuses to overwrite anything else — your [package].name
decides the directory name, and a collision there is someone else's package.
Taking it back out
salpa unpush [PATH] is the reverse: it removes the package from the app's shelf
and deregisters the source, so the app stops offering it.
salpa unpush [PATH]
--source-id id of the source to remove (default: whichever the push created)
--port port the app is listening on (default 18000, or $SALPA_PORT)
--force remove it even though the package is installed
-y, --yes remove without asking
Exit codes: 0 ok (including nothing to remove) · 1 refused or failed · 2 the
path is not a package.
Your own directory is never touched — the packages/ layout and registry.json
that push wrote stay put, and salpa push registers them again whenever you want
the package back. You do not have to remember whether you used --copy: unpush
reads which mode was used off what is actually in the app, and cleans up the copy
in the app's catalog when there is one.
Unpushing something that was never pushed reports Nothing to remove and exits 0,
so it is safe to run twice.
unpush refuses while the package is installed, and points you at
Marketplace ▸ Installed to uninstall it first — removing the source out from under
an installed copy would leave it pointing at a source that no longer exists.
--force removes it anyway. As with push stopping at the shelf, unpush never
uninstalls anything itself; that stays in the app.
Full walkthrough: salpa docs publishing-to-your-app.
Authoring guide
salpa docs prints the bundled node-authoring guide. Full documentation:
https://salpa.app/docs/custom-nodes
Reserved
salpa publish / install / run are reserved for later and not implemented
yet. The current surface is salpa new, salpa validate, salpa push,
salpa unpush and salpa docs.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distributions
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file salpa_cli-0.6.2-py3-none-any.whl.
File metadata
- Download URL: salpa_cli-0.6.2-py3-none-any.whl
- Upload date:
- Size: 139.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b89ad8b4c580e02c4a99cb6c0de7430596f1b04b4f2b70abf2205e53cd98ff24
|
|
| MD5 |
3a26578d8418c9b7100645cc4f6b50bf
|
|
| BLAKE2b-256 |
94fbf5d9e0dfdd823c669e9ee2d884cfaf29e159e831cdc02c10fe43e8bc8fb8
|