sillo-start 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,36 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ build/
8
+ dist/
9
+ *.egg-info/
10
+ .eggs/
11
+
12
+ # Virtual environments
13
+ .venv/
14
+ venv/
15
+ env/
16
+
17
+ # Tooling caches
18
+ .pytest_cache/
19
+ .ruff_cache/
20
+ .mypy_cache/
21
+ .coverage
22
+ .coverage.*
23
+ htmlcov/
24
+ coverage.xml
25
+
26
+ # Editors
27
+ .idea/
28
+ .vscode/
29
+ *.swp
30
+
31
+ # OS
32
+ .DS_Store
33
+ Thumbs.db
34
+
35
+ # uv.lock is committed deliberately: sillo-start is an application-style CLI,
36
+ # so a reproducible resolution is worth more than floating dependencies.
@@ -0,0 +1,153 @@
1
+ Metadata-Version: 2.4
2
+ Name: sillo-start
3
+ Version: 0.1.0
4
+ Summary: Create a Sillo application from a starter repository.
5
+ Project-URL: Homepage, https://github.com/sillohq/start
6
+ Project-URL: Repository, https://github.com/sillohq/start
7
+ Project-URL: Documentation, https://docs.sillo.build
8
+ Author-email: Chidebele Dunamis <techwithdunamix@gmail.com>
9
+ License-Expression: BSD-3-Clause
10
+ Keywords: asgi,cli,scaffolding,sillo,starter
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Topic :: Software Development :: Code Generators
18
+ Requires-Python: >=3.11
19
+ Requires-Dist: rich>=13.7.0
20
+ Requires-Dist: typer>=0.12.0
21
+ Provides-Extra: dev
22
+ Requires-Dist: mypy>=1.15.0; extra == 'dev'
23
+ Requires-Dist: pytest-cov>=5.0.0; extra == 'dev'
24
+ Requires-Dist: pytest>=8.3.5; extra == 'dev'
25
+ Requires-Dist: ruff>=0.6.0; extra == 'dev'
26
+ Description-Content-Type: text/markdown
27
+
28
+ # Sillo Start
29
+
30
+ Creates a Sillo application from a starter repository.
31
+
32
+ ```bash
33
+ uvx sillo-start create-app myapp
34
+ cd myapp
35
+ make setup
36
+ make dev
37
+ ```
38
+
39
+ That is the whole tool. It fetches a real, working application, renames it to
40
+ yours, gives it its own secrets, and gets out of the way.
41
+
42
+ ## Why a starter repository, not a generator
43
+
44
+ A generator renders templates. Templates are checked for *rendering*, which is
45
+ not the same as working — a project can produce valid Python, import cleanly,
46
+ render every page, and still fail on its first real request. Middleware
47
+ registered in the wrong order, an auth backend reading the wrong claim, a
48
+ missing static mount: all of them render perfectly.
49
+
50
+ [`sillohq/starter`](https://github.com/sillohq/starter) is a real application
51
+ with its own CI. Every push boots it and exercises every route, on three
52
+ Python versions. What you get has been run, not just written.
53
+
54
+ It also means the starter can be read, forked and improved on its own, and
55
+ that `sillo-start` never has to be released to fix a bug in what it produces.
56
+
57
+ ## Install
58
+
59
+ ```bash
60
+ # Run it without installing
61
+ uvx sillo-start create-app myapp
62
+
63
+ # Or install it
64
+ uv tool install sillo-start
65
+ pipx install sillo-start
66
+ ```
67
+
68
+ Python 3.11 or newer. The only dependencies are `typer` and `rich`.
69
+
70
+ ## Usage
71
+
72
+ ```bash
73
+ sillo-start create-app myapp # the default starter
74
+ sillo-start create-app sillohq/starter myapp # named explicitly
75
+ sillo-start create-app sillohq/starter@v1.2 myapp # pinned to a tag
76
+ sillo-start create-app acme/our-template myapp # your own starter
77
+ ```
78
+
79
+ Any public GitHub repository works. A full URL is accepted too, so pasting what
80
+ is in your address bar does what you expect.
81
+
82
+ | Option | |
83
+ | --- | --- |
84
+ | `--ref <branch\|tag>` | Which revision to take. Defaults to `main` |
85
+ | `-d`, `--directory <path>` | Where to create it. Defaults to `./<name>` |
86
+ | `--install` | Install dependencies straight away |
87
+ | `--no-git` | Do not initialise a git repository |
88
+ | `--force` | Allow a directory that is not empty |
89
+ | `-v`, `--verbose` | Show tracebacks |
90
+
91
+ Dependencies are **not** installed by default, so creating a project takes a
92
+ second rather than a minute. `make setup` in the new project does it, along
93
+ with everything else a first run needs.
94
+
95
+ ## What it does to the project
96
+
97
+ The starter arrives as a tarball rather than a clone. That needs no `git` on
98
+ the machine, brings no history for you to delete before your first commit, and
99
+ pins to a tag as easily as to a branch.
100
+
101
+ Then three things happen:
102
+
103
+ **It takes your name.** The package name, the application title, the SQLite
104
+ path. Rewriting is targeted rather than a blanket find-and-replace, so prose
105
+ that happens to say "starter" — a README sentence, a comment — is left as
106
+ written.
107
+
108
+ **It gets its own secrets.** `.env` is created from `.env.example` with a fresh
109
+ key for `SECRET_KEY`, `JWT_SECRET` and `APP_KEY`. A secret committed to a
110
+ starter is a placeholder by definition; no two projects should ever share a
111
+ signing key. An existing `.env` is never touched — it may hold real credentials.
112
+
113
+ **It becomes a git repository**, unless you pass `--no-git`.
114
+
115
+ ## What it deliberately does not do
116
+
117
+ Migrations, creating users, running the queue worker, starting the server —
118
+ none of that is here.
119
+
120
+ Those belong to the project, in its own `console.py`:
121
+
122
+ ```bash
123
+ python console.py db migrate
124
+ python console.py user admin ada@example.com ada
125
+ python console.py worker
126
+ python console.py serve --reload
127
+ ```
128
+
129
+ The framework provides the operations as plain functions —
130
+ `sillo.record.commands`, `sillo.users.commands`, `sillo.work.commands` — and
131
+ the project decides how to expose them. Nothing in a created project depends on
132
+ `sillo-start`, so it cannot rot when this tool changes, and you can delete this
133
+ tool the moment your project exists.
134
+
135
+ ## Safety
136
+
137
+ An archive member whose path escapes the destination is refused rather than
138
+ sanitised — that is how a malicious archive overwrites files elsewhere on your
139
+ machine.
140
+
141
+ A directory that is not empty is refused unless you pass `--force`, and even
142
+ then existing files are left in place.
143
+
144
+ A project name that is not a usable Python identifier is refused *before*
145
+ anything is fetched, since it becomes both a package name and a directory.
146
+
147
+ ## Development
148
+
149
+ See [docs/contributing.md](docs/contributing.md).
150
+
151
+ ## Licence
152
+
153
+ BSD-3-Clause.
@@ -0,0 +1,126 @@
1
+ # Sillo Start
2
+
3
+ Creates a Sillo application from a starter repository.
4
+
5
+ ```bash
6
+ uvx sillo-start create-app myapp
7
+ cd myapp
8
+ make setup
9
+ make dev
10
+ ```
11
+
12
+ That is the whole tool. It fetches a real, working application, renames it to
13
+ yours, gives it its own secrets, and gets out of the way.
14
+
15
+ ## Why a starter repository, not a generator
16
+
17
+ A generator renders templates. Templates are checked for *rendering*, which is
18
+ not the same as working — a project can produce valid Python, import cleanly,
19
+ render every page, and still fail on its first real request. Middleware
20
+ registered in the wrong order, an auth backend reading the wrong claim, a
21
+ missing static mount: all of them render perfectly.
22
+
23
+ [`sillohq/starter`](https://github.com/sillohq/starter) is a real application
24
+ with its own CI. Every push boots it and exercises every route, on three
25
+ Python versions. What you get has been run, not just written.
26
+
27
+ It also means the starter can be read, forked and improved on its own, and
28
+ that `sillo-start` never has to be released to fix a bug in what it produces.
29
+
30
+ ## Install
31
+
32
+ ```bash
33
+ # Run it without installing
34
+ uvx sillo-start create-app myapp
35
+
36
+ # Or install it
37
+ uv tool install sillo-start
38
+ pipx install sillo-start
39
+ ```
40
+
41
+ Python 3.11 or newer. The only dependencies are `typer` and `rich`.
42
+
43
+ ## Usage
44
+
45
+ ```bash
46
+ sillo-start create-app myapp # the default starter
47
+ sillo-start create-app sillohq/starter myapp # named explicitly
48
+ sillo-start create-app sillohq/starter@v1.2 myapp # pinned to a tag
49
+ sillo-start create-app acme/our-template myapp # your own starter
50
+ ```
51
+
52
+ Any public GitHub repository works. A full URL is accepted too, so pasting what
53
+ is in your address bar does what you expect.
54
+
55
+ | Option | |
56
+ | --- | --- |
57
+ | `--ref <branch\|tag>` | Which revision to take. Defaults to `main` |
58
+ | `-d`, `--directory <path>` | Where to create it. Defaults to `./<name>` |
59
+ | `--install` | Install dependencies straight away |
60
+ | `--no-git` | Do not initialise a git repository |
61
+ | `--force` | Allow a directory that is not empty |
62
+ | `-v`, `--verbose` | Show tracebacks |
63
+
64
+ Dependencies are **not** installed by default, so creating a project takes a
65
+ second rather than a minute. `make setup` in the new project does it, along
66
+ with everything else a first run needs.
67
+
68
+ ## What it does to the project
69
+
70
+ The starter arrives as a tarball rather than a clone. That needs no `git` on
71
+ the machine, brings no history for you to delete before your first commit, and
72
+ pins to a tag as easily as to a branch.
73
+
74
+ Then three things happen:
75
+
76
+ **It takes your name.** The package name, the application title, the SQLite
77
+ path. Rewriting is targeted rather than a blanket find-and-replace, so prose
78
+ that happens to say "starter" — a README sentence, a comment — is left as
79
+ written.
80
+
81
+ **It gets its own secrets.** `.env` is created from `.env.example` with a fresh
82
+ key for `SECRET_KEY`, `JWT_SECRET` and `APP_KEY`. A secret committed to a
83
+ starter is a placeholder by definition; no two projects should ever share a
84
+ signing key. An existing `.env` is never touched — it may hold real credentials.
85
+
86
+ **It becomes a git repository**, unless you pass `--no-git`.
87
+
88
+ ## What it deliberately does not do
89
+
90
+ Migrations, creating users, running the queue worker, starting the server —
91
+ none of that is here.
92
+
93
+ Those belong to the project, in its own `console.py`:
94
+
95
+ ```bash
96
+ python console.py db migrate
97
+ python console.py user admin ada@example.com ada
98
+ python console.py worker
99
+ python console.py serve --reload
100
+ ```
101
+
102
+ The framework provides the operations as plain functions —
103
+ `sillo.record.commands`, `sillo.users.commands`, `sillo.work.commands` — and
104
+ the project decides how to expose them. Nothing in a created project depends on
105
+ `sillo-start`, so it cannot rot when this tool changes, and you can delete this
106
+ tool the moment your project exists.
107
+
108
+ ## Safety
109
+
110
+ An archive member whose path escapes the destination is refused rather than
111
+ sanitised — that is how a malicious archive overwrites files elsewhere on your
112
+ machine.
113
+
114
+ A directory that is not empty is refused unless you pass `--force`, and even
115
+ then existing files are left in place.
116
+
117
+ A project name that is not a usable Python identifier is refused *before*
118
+ anything is fetched, since it becomes both a package name and a directory.
119
+
120
+ ## Development
121
+
122
+ See [docs/contributing.md](docs/contributing.md).
123
+
124
+ ## Licence
125
+
126
+ BSD-3-Clause.
@@ -0,0 +1,76 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "sillo-start"
7
+ version = "0.1.0"
8
+ description = "Create a Sillo application from a starter repository."
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = "BSD-3-Clause"
12
+ authors = [
13
+ { name = "Chidebele Dunamis", email = "techwithdunamix@gmail.com" },
14
+ ]
15
+ keywords = ["sillo", "scaffolding", "cli", "starter", "asgi"]
16
+ classifiers = [
17
+ "Development Status :: 4 - Beta",
18
+ "Environment :: Console",
19
+ "Intended Audience :: Developers",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Programming Language :: Python :: 3.13",
23
+ "Topic :: Software Development :: Code Generators",
24
+ ]
25
+ dependencies = [
26
+ "typer>=0.12.0",
27
+ "rich>=13.7.0",
28
+ ]
29
+
30
+ [project.optional-dependencies]
31
+ dev = [
32
+ "pytest>=8.3.5",
33
+ "pytest-cov>=5.0.0",
34
+ "mypy>=1.15.0",
35
+ "ruff>=0.6.0",
36
+ ]
37
+
38
+ [project.urls]
39
+ Homepage = "https://github.com/sillohq/start"
40
+ Repository = "https://github.com/sillohq/start"
41
+ Documentation = "https://docs.sillo.build"
42
+
43
+ [project.scripts]
44
+ sillo-start = "sillo_start.__main__:main"
45
+
46
+ [tool.hatch.build.targets.wheel]
47
+ packages = ["sillo_start"]
48
+
49
+ [tool.hatch.build.targets.sdist]
50
+ include = ["sillo_start", "tests", "README.md"]
51
+
52
+ [tool.pytest.ini_options]
53
+ testpaths = ["tests"]
54
+ addopts = "-q"
55
+
56
+ [tool.ruff]
57
+ line-length = 88
58
+ target-version = "py311"
59
+
60
+ [tool.ruff.lint]
61
+ select = ["E", "F", "I", "UP", "B"]
62
+ ignore = [
63
+ "E501",
64
+ # Typer expresses defaults as function calls in the signature.
65
+ "B008",
66
+ ]
67
+
68
+ [tool.ruff.lint.per-file-ignores]
69
+ "__init__.py" = ["F401"]
70
+
71
+ [tool.mypy]
72
+ python_version = "3.11"
73
+ ignore_missing_imports = true
74
+ warn_unused_ignores = true
75
+ disallow_untyped_defs = false
76
+ files = ["sillo_start"]
@@ -0,0 +1,35 @@
1
+ """Sillo Start — bootstrapper and orchestrator for Sillo applications.
2
+
3
+ Sillo Start creates properly structured Sillo projects, installs and removes
4
+ features against an existing project, and runs the development services a
5
+ project needs. ``sillo.toml`` at the project root is the authoritative record
6
+ of what a project is; every command reads and updates it.
7
+
8
+ The package is layered so the CLI stays a thin shell over reusable logic:
9
+
10
+ ``config``
11
+ The manifest schema and its loading/writing.
12
+ ``operations``
13
+ Reversible units of work (write a file, edit TOML, install a package) and
14
+ the transaction that applies them or rolls them back.
15
+ ``packages``
16
+ The package-group registry, dependency resolver, and installers.
17
+ ``blueprints``
18
+ Named project archetypes that turn wizard answers into a manifest.
19
+ ``project``
20
+ Creation, inspection and validation of projects on disk.
21
+ ``generators``
22
+ Component scaffolding for an existing project.
23
+ ``orchestration``
24
+ The supervised process manager behind ``sillo-start dev``.
25
+ ``prompts``
26
+ The interactive wizard.
27
+ ``cli``
28
+ Typer commands, which do argument handling and rendering only.
29
+ """
30
+
31
+ from __future__ import annotations
32
+
33
+ __version__ = "0.1.0"
34
+
35
+ __all__ = ["__version__"]
@@ -0,0 +1,19 @@
1
+ """``python -m sillo_start`` and the ``sillo-start`` console script."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ def main() -> None:
7
+ """Load the command tree and run the CLI."""
8
+ # Imported here rather than at module scope so that plugin loading and
9
+ # command registration happen once, at invocation, and a broken plugin
10
+ # cannot make `python -m sillo_start` unimportable.
11
+ from .cli import build_cli
12
+
13
+ # A Typer app is called, not `.main()`ed — calling it is what runs the
14
+ # underlying Click command with argv.
15
+ build_cli()()
16
+
17
+
18
+ if __name__ == "__main__":
19
+ main()
@@ -0,0 +1,20 @@
1
+ """The Typer command tree.
2
+
3
+ Importing the command module registers its command on the shared ``app``, so
4
+ :func:`build_cli` imports it and returns the app.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import typer
10
+
11
+
12
+ def build_cli() -> typer.Typer:
13
+ """Assemble and return the CLI application."""
14
+ from . import create # noqa: F401 — importing registers the command
15
+ from .app import app
16
+
17
+ return app
18
+
19
+
20
+ __all__ = ["build_cli"]
@@ -0,0 +1,106 @@
1
+ """The Typer application and its shared behaviour.
2
+
3
+ The CLI layer stays thin: it parses arguments, calls into
4
+ :mod:`sillo_start.project.template`, and renders the result. The fetching and
5
+ personalising happen there, which is what lets them be driven from tests
6
+ without a terminal.
7
+
8
+ Errors are handled in one place. Anything deriving from
9
+ :class:`~sillo_start.exceptions.SilloStartError` becomes a clean message plus
10
+ its hint and a predictable exit code; the traceback appears only under
11
+ ``--verbose``.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import sys
17
+ from collections.abc import Callable
18
+ from functools import wraps
19
+ from typing import Any
20
+
21
+ import typer
22
+
23
+ from .. import __version__
24
+ from ..exceptions import SilloStartError
25
+ from ..utils.console import console
26
+
27
+ app = typer.Typer(
28
+ name="sillo-start",
29
+ help="Create a Sillo application from a starter repository.",
30
+ add_completion=True,
31
+ no_args_is_help=True,
32
+ rich_markup_mode="rich",
33
+ context_settings={"help_option_names": ["-h", "--help"]},
34
+ )
35
+
36
+
37
+ def version_callback(value: bool) -> None:
38
+ """Print the version and exit."""
39
+ if value:
40
+ console.print(f"sillo-start {__version__}")
41
+ raise typer.Exit()
42
+
43
+
44
+ @app.callback()
45
+ def main(
46
+ version: bool = typer.Option(
47
+ False,
48
+ "--version",
49
+ "-V",
50
+ help="Show the version and exit.",
51
+ callback=version_callback,
52
+ is_eager=True,
53
+ ),
54
+ verbose: bool = typer.Option(
55
+ False, "--verbose", "-v", help="Show debug output and tracebacks."
56
+ ),
57
+ quiet: bool = typer.Option(
58
+ False, "--quiet", "-q", help="Suppress non-essential output."
59
+ ),
60
+ ) -> None:
61
+ """Sillo Start — create a Sillo application from a starter repository."""
62
+ console.configure(verbose=verbose, quiet=quiet)
63
+
64
+
65
+ def handle_errors(func: Callable[..., Any]) -> Callable[..., Any]:
66
+ """Turn deliberate failures into clean messages and exit codes.
67
+
68
+ Applied to every command body. ``typer.Exit`` and ``typer.Abort`` pass
69
+ through untouched so control flow still works.
70
+ """
71
+
72
+ @wraps(func)
73
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
74
+ try:
75
+ return func(*args, **kwargs)
76
+ except (typer.Exit, typer.Abort):
77
+ raise
78
+ except SilloStartError as exc:
79
+ console.error(exc.message, hint=exc.hint)
80
+ if console.verbose:
81
+ console.rich.print_exception()
82
+ raise typer.Exit(code=exc.exit_code) from exc
83
+ except KeyboardInterrupt:
84
+ console.blank()
85
+ console.warning("Cancelled.")
86
+ raise typer.Exit(code=130) from None
87
+ except Exception as exc: # noqa: BLE001 — last resort, reported not swallowed
88
+ console.error(
89
+ f"Unexpected error: {exc}",
90
+ hint="Re-run with --verbose for the full traceback, and please report this.",
91
+ )
92
+ if console.verbose:
93
+ console.rich.print_exception()
94
+ raise typer.Exit(code=1) from exc
95
+
96
+ return wrapper
97
+
98
+
99
+ def run() -> None:
100
+ """Entrypoint used by the console script."""
101
+ try:
102
+ app()
103
+ except SilloStartError as exc:
104
+ # Reached only for failures raised outside a command body.
105
+ console.error(exc.message, hint=exc.hint)
106
+ sys.exit(exc.exit_code)
@@ -0,0 +1,114 @@
1
+ """``sillo-start create-app``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ import typer
8
+
9
+ from ..exceptions import UsageError
10
+ from ..utils.console import console
11
+ from ..utils.naming import is_valid_project_name
12
+ from .app import app, handle_errors
13
+
14
+
15
+ @app.command("create-app")
16
+ @handle_errors
17
+ def create_app(
18
+ template: str = typer.Argument(
19
+ None, help="Starter repository, e.g. sillohq/starter or sillohq/starter@v1."
20
+ ),
21
+ name: str = typer.Argument(None, help="Project name. Also the directory name."),
22
+ directory: Path = typer.Option(
23
+ None,
24
+ "--directory",
25
+ "-d",
26
+ help="Where to create the project. Defaults to ./<name>.",
27
+ ),
28
+ ref: str = typer.Option(
29
+ None, "--ref", help="Branch or tag to take. Defaults to main."
30
+ ),
31
+ install: bool = typer.Option(
32
+ False, "--install/--no-install", help="Install dependencies after fetching."
33
+ ),
34
+ git: bool = typer.Option(
35
+ True, "--git/--no-git", help="Initialise a git repository."
36
+ ),
37
+ force: bool = typer.Option(False, "--force", help="Allow a non-empty directory."),
38
+ ) -> None:
39
+ """Create a project from a starter repository.
40
+
41
+ sillo-start create-app myapp
42
+ sillo-start create-app sillohq/starter myapp
43
+ sillo-start create-app sillohq/starter@v1.2 myapp
44
+
45
+ The starter is a real application with its own CI, so what you get has been
46
+ booted and exercised rather than only rendered. With one argument the
47
+ default starter is used and the argument is the project name.
48
+ """
49
+ from ..project.template import DEFAULT_TEMPLATE, Template, fetch, personalise
50
+
51
+ # One argument is the project name; the starter is only ever given when
52
+ # both are, so `create-app myapp` does the obvious thing.
53
+ if name is None:
54
+ name, template = template, DEFAULT_TEMPLATE
55
+ if not name:
56
+ raise UsageError(
57
+ "A project name is required.",
58
+ hint="sillo-start create-app myapp",
59
+ )
60
+ if not is_valid_project_name(name):
61
+ raise UsageError(
62
+ f"'{name}' is not a valid project name.",
63
+ hint="Use a letter followed by letters, digits, hyphens or underscores.",
64
+ )
65
+
66
+ parsed = Template.parse(template or DEFAULT_TEMPLATE, ref=ref)
67
+ root = (directory or Path.cwd() / name).resolve()
68
+ if root.exists() and any(root.iterdir()) and not force:
69
+ raise UsageError(
70
+ f"{root} is not empty.",
71
+ hint="Choose another directory, or pass --force.",
72
+ )
73
+
74
+ console.header(f"Creating {name}", f"from {parsed.slug}@{parsed.ref}")
75
+ with console.progress(f"Fetching {parsed.slug}…"):
76
+ fetch(parsed, root)
77
+
78
+ changed = personalise(root, name)
79
+ console.success(f"Fetched {parsed.slug} and renamed {len(changed)} file(s)")
80
+
81
+ if git:
82
+ from ..utils.subprocess import run, tool_exists
83
+
84
+ if tool_exists("git") and not (root / ".git").exists():
85
+ run(["git", "init", "--quiet"], cwd=root, check=False)
86
+
87
+ if install:
88
+ from ..utils.pkgmanagers import detect_python_manager
89
+ from ..utils.subprocess import run
90
+
91
+ manager = detect_python_manager()
92
+ console.header("Dependencies", f"installing with {manager.name}")
93
+ with console.progress("Resolving…"):
94
+ result = run(manager.sync_command(), cwd=root, check=False, timeout=900)
95
+ if not result.ok:
96
+ console.failure(f"{manager.name} exited with code {result.returncode}.")
97
+ if result.output:
98
+ console.raw(result.output)
99
+ raise typer.Exit(code=1)
100
+ console.success("Dependencies installed.")
101
+
102
+ console.blank()
103
+ console.print("[bold]Next steps[/bold]")
104
+ steps = [f"cd {root.name}"]
105
+ if not install:
106
+ steps.append("make setup")
107
+ else:
108
+ steps.append("make migrate")
109
+ steps.append("make dev")
110
+ console.commands(steps)
111
+ console.blank()
112
+ console.hint(
113
+ "The starter's README covers configuration, migrations and deployment."
114
+ )