The Command Line
Installing SPOC gives you one command, spoc, with five subcommands: two that
generate (init, app) and three that inspect (check, list,
explain). The help below is captured from the real command at build time, so
it can't drift from what your terminal says:
usage: spoc [-h] [--version]
{init,app,check,list,explain,stubs,projection} ...
Scaffold, validate, and inspect spoc projects: init generates a runnable
project, app adds one to it; check dry-boots and reports problems before
runtime; list and explain read the registry; stubs writes the types an editor
needs to complete resolve(); projection writes the registry as JSON for any
other tool.
positional arguments:
{init,app,check,list,explain,stubs,projection}
init Generate a new project that starts unedited.
app Generate one additional app into an existing project.
check Validate the project before runtime.
list List every registered identifier.
explain Describe one registered identifier.
stubs Generate a type stub for the project's resolution
surface.
projection Write the registry as a JSON document.
options:
-h, --help show this help message and exit
--version show program's version number and exit
spoc init — a new project
Creates ./hello with settings, a framework declaration, one app, and an
entry point — the quick start walks
through every file.
usage: spoc init [-h] [--path PATH] [--app APP] [--kinds KINDS]
[--template TEMPLATE]
name
Generate a new project: configuration, framework declaration, one app, and an
entry point. Add further apps with `spoc app <name>`; a spoc app is an
__init__ plus a module per kind.
positional arguments:
name Project name (lowercase, snake_case).
options:
-h, --help show this help message and exit
--path PATH Directory to generate into. Defaults to ./<name>.
--app APP Name of the starter app (default: core).
--kinds KINDS Comma-separated kinds the framework declares (default:
models,views).
--template TEMPLATE Template set to render (default: default). One of: an
installed set's name; a directory path (./mytemplates,
C:\templates);
gh:owner/repo[@revision][#subdirectory=path]; an
https:// archive URL; or
git+https://host/owner/repo[@revision]. A remote
reference is the only thing that causes spoc to access
the network.
If anything would collide with existing files, nothing is written at all.
Every generated project also gets a .spoc-template.json noting which template
set produced it — whatever set you named, and whoever wrote it. Nothing reads it
at runtime — delete it and the project still starts — but spoc app uses it to
warn you when you're about to add an app from a different template than the rest
of the project came from.
SPOC writes that file itself. A template set cannot suppress it by leaving it out, and cannot supply what it says: a set that declares a file landing there is refused before anything is written. A record its own subject could author would tell you nothing.
spoc app — one more app
Generates apps/blog/ with one module per kind — and it reads the kinds from
your own framework.py, so you never restate them. It won't edit your
settings; it prints the exact line to add:
Install it: add "apps.blog" to a mode list under [spoc.apps] in config/spoc.toml, e.g.
development = [..., "apps.blog"]
Templates
--template takes four forms. Which one you mean is decided by how you spell
it, before SPOC looks at anything — so a typo is always reported as a typo,
never as a missing directory you never named.
| Form | Example |
|---|---|
| An installed set's name | default |
| A directory | ./mytemplates, C:\templates |
| A GitHub repository | gh:owner/repo |
| Any archive URL | https://host/sets.tar.gz |
The last two carry optional parts, spelled the way pip spells them:
spoc init hello --template gh:owner/repo@v1.2
spoc init hello --template gh:owner/repo@v1.2#subdirectory=templates/minimal
spoc init hello --template git+https://gitlab.com/owner/repo@v1.2
@v1.2 pins a revision; #subdirectory= picks one set out of a repo that holds
several. If you don't pin, SPOC resolves the reference to an exact commit before
fetching, and tells you which one, so you can pin it next time:
Generated from gh:owner/repo at revision 8f2c1ab….
Reproduce this exact project with:
--template gh:owner/repo@8f2c1ab…
Fetched templates are cached by commit, so generating a second project from the same reference does no network work — and still works offline.
What a template can and cannot do
A template set cannot run code. SPOC substitutes named values and nothing
else: no expressions, no conditionals, no hooks, no scripts that run during
generation. This is a guarantee, not an implementation detail, and it is what
makes --template gh:someone/repo a reasonable thing to type — unlike
scaffolding tools that execute template-supplied hooks by design.
What you should still weigh: the generated project is code written by whoever
wrote the template, and you're about to run it. That's the same trust decision
as git clone, and no tool can make it for you.
A template set cannot write .spoc-template.json. That destination is
reserved: SPOC writes the origin record for every generation, and a manifest
declaring a file that lands there is refused, naming it. The three values that
used to feed the record — template_reference, template_revision, and
template_set_name — are no longer part of the substitution vocabulary, so a
set that declares one is refused as unsatisfiable. If you are writing a template
set, don't declare the record; you get it for free.
A remote reference is also the only thing that makes SPOC touch the network. No other command opens a connection.
spoc check — find problems before runtime
check does a dry boot and reports everything the first real boot would
complain about: settings typos, apps that don't import, dependency cycles,
name-tag collisions, async hooks on the sync path. Every finding uses the same
precise wording the runtime error would. Exit code 0 means clean — perfect
for CI.
Note
A dry boot still imports your app modules. Nothing outlives the check — the framework is torn down and import state restored.
When another tool is the reader, ask for the document instead:
A finding in the list carries its area (config / locate / lifecycle /
boot) and its message. The document is the covered surface — the prose
rendering above is free to change — and the exit code stays the same either
way. The message text inside a finding is still prose; branch on ok and
area, never on wording.
spoc list and spoc explain — read the shelf
identifier: models:core.example
kind: models
namespace: core
object_name: example
object: apps.core.models:Example
shape: constructible
Both boot, read, and tear down — nothing stays running.
shape is what you may do with the block: constructible if it is a class you can
build, callable if it is a function you can call, value if it is just a thing.
Both also take --json. spoc list --json emits
{"format_version": …, "components": […]} and spoc explain --json a single
component — and each entry is exactly the component object of the projection
document (see spoc projection below), so what these report and what the
projection publishes cannot drift. The JSON is the covered surface; the line
rendering above is free to change.
spoc stubs — teach your editor the registry
spoc stubs
spoc stubs --check # verify the committed stub is current; never writes
spoc stubs --strict # make a misspelled identifier a type error
This writes framework.pyi beside your framework.py. After it, resolve() returns the
real type of each block and your editor completes both the identifier string and the
object you get back — with no change to your own code.
The stub is a type stub, so it never executes: it adds no runtime coupling between your apps, and deleting it changes nothing about how your program runs.
Full walkthrough: Get Editor Autocomplete.
usage: spoc stubs [-h] [--framework FRAMEWORK] [--check] [--strict] [path]
Dry-boot the project and write a type stub beside its composition root, so
resolve() returns the real type of each component and editors complete both
the identifier and the object. The stub never executes, so it adds no runtime
coupling between apps. Note: generating imports your app modules.
positional arguments:
path Project directory (default: current directory).
options:
-h, --help show this help message and exit
--framework FRAMEWORK
module.path:attribute holding the Framework (default:
framework:framework, what `spoc init` emits).
--check Verify the stored stub is current without writing it.
--strict Omit the catch-all overload, making a misspelled
identifier a type error. Requires every resolve() call
to use a literal.
spoc projection — hand your registry to another tool
spoc projection # the whole registry, as JSON, on stdout
spoc projection > registry.json # save it
spoc projection | jq '.components[] | select(.kind == "models")'
{
"format_version": "1.0",
"kinds": ["models", "views"],
"components": [
{
"identifier": "models:blog.post",
"kind": "models",
"namespace": "blog",
"object_name": "post",
"location": "apps.blog.models:Post",
"shape": "constructible"
}
]
}
spoc list is for you; this is for your other programs. Anything that wants to know what
your project registered — a router generator, an admin page, a docs build, a script in
another language entirely — reads this instead of importing your code.
Three things make it safe to build on:
- It validates. A JSON Schema ships with SPOC, so a consumer can check what it got
without reading any Python. Find it at
spoc/projection/schema.jsoninside the installed package, or viaspoc.projection.schema_path(). - It doesn't start your app. Discovery runs; your startup hooks do not. A project that needs a database to boot is still describable on a laptop that has none. The flip side: it describes the registry as of the end of discovery, so anything a startup hook registers afterwards is not in here.
- It doesn't churn. Entries come out in identifier order every time, so two runs of an
unchanged project produce identical bytes and a diff means something really changed —
not that you reordered your
[spoc.apps]list.
format_version is the version of this document shape, not of SPOC. Branch on it, not on
the release you happen to have installed.
usage: spoc projection [-h] [--framework FRAMEWORK] [path]
Dry-boot the project and write its registry to standard output as JSON: every
registered component, its canonical identifier and the three facets composing
it, where its object is defined, and its shape, plus the declared kind set.
Validates against the schema published with spoc. Discovery runs but
initialization does not, so a project whose startup hooks would fail is still
describable. Note: projecting imports your app modules.
positional arguments:
path Project directory (default: current directory).
options:
-h, --help show this help message and exit
--framework FRAMEWORK
module.path:attribute holding the Framework (default:
framework:framework, what `spoc init` emits).
Where's the framework?
The inspect commands accept a project path and, if your declaration doesn't
live at the default spot (framework:framework, what init emits), a
pointer to it:
usage: spoc check [-h] [--framework FRAMEWORK] [--json] [path]
Dry-boot the project and report configuration problems, unresolvable apps and
plugins, dependency cycles, identity collisions, and coroutine hooks the
synchronous lifecycle would refuse. Note: checking imports your app modules.
positional arguments:
path Project directory (default: current directory).
options:
-h, --help show this help message and exit
--framework FRAMEWORK
module.path:attribute holding the Framework (default:
framework:framework, what `spoc init` emits).
--json Emit the result as JSON on stdout and nothing else.
This output is the covered surface; the prose
rendering is free to change.
Next: testing your project.