Skip to content

What sillo-start rewrites when it renames a starter to your project: the exact substitutions, the lockfile, and the files it deliberately leaves alone.

The starter calls itself starter. Your project does not. Personalisation is the step between.

✓ Fetched sillohq/starter and renamed 3 file(s)

Every substitution is a named file and an exact string. Nothing is a blanket search across the tree.

That matters more than it sounds. A starter’s README says “starter” in prose several times, a Makefile comment mentions it, a docstring explains what the starter is for. A find-and-replace turns all of those into sentences about myapp that no longer parse as English. Naming each substitution means prose is left as written.

For a project named myapp:

FileFromTo
pyproject.tomlname = "starter"name = "myapp"
app/config.py"""Typed settings for Starter.""""""Typed settings for Myapp."""
app/config.pyapp_name: str = "Starter"app_name: str = "Myapp"
app/config.pysqlite://storage/starter.dbsqlite://storage/myapp.db
.env.example# Starter environment.# Myapp environment.
.env.exampleAPP_NAME=StarterAPP_NAME=Myapp
.env.examplesqlite://storage/starter.dbsqlite://storage/myapp.db

Two capitalisations are used. myapp is the name as you typed it; Myapp is its title-cased form with separators removed. my-cool-app becomes MyCoolApp.

A file that is not there, or a string that is not in it, is skipped rather than being an error. That is what lets a custom starter share some of these conventions without having to adopt all of them.

The count reported at the end is the number of files changed, not substitutions.

uv.lock gets one more: the project names itself as a member of its own workspace, so the lockfile has to agree with pyproject.toml or uv sync fails on a renamed project.

name = "starter" → name = "myapp"

This one is a plain replacement across the file, because in a lockfile that string only ever means the package name.

Model modules are absent from the list, on purpose.

A model’s docstring becomes its table_description in the generated schema. So rewriting one puts your models out of step with the migration the starter committed, and the next sillo db:make notices the difference and writes a spurious second migration describing a comment change.

The cost of leaving them is a docstring that says “starter”. The cost of rewriting them is a migration nobody asked for on the first day of the project.

READMEs, comments, docstrings outside the list above. See Targeted, not find-and-replace.

Never touched. It may hold real credentials. See Secrets and .env.

Not applicable in the normal case (the starter arrives as a tarball with no history) but worth stating for --force into an existing checkout.

personalise is a plain function, and it takes the starter’s own name so it can be pointed at a custom one:

from pathlib import Path
from sillo_start.project.template import personalise
changed = personalise(Path("./myapp"), "myapp", template_name="our-template")

It returns the list of paths it changed, relative to the root. See Internals.

Personalisation is deliberately small. Anything beyond renaming (choosing a database driver, dropping a subsystem, adding a frontend) belongs in the starter itself, as something the application already supports and you configure, rather than as a substitution the scaffolder performs.

That is the same reasoning as fetching a repository rather than rendering templates: options a generator offers are options nobody ever ran together.