flint-kit 0.14.0__py3-none-any.whl

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.
Files changed (193) hide show
  1. flint/__init__.py +3 -0
  2. flint/__main__.py +4 -0
  3. flint/cli.py +289 -0
  4. flint/errors.py +17 -0
  5. flint/generator.py +446 -0
  6. flint/naming.py +97 -0
  7. flint/postgen.py +112 -0
  8. flint/prefs.py +110 -0
  9. flint/prompts.py +268 -0
  10. flint/skills/alembic/content/SKILL.md.jinja +78 -0
  11. flint/skills/alembic/content/guides/first-migration.md.jinja +58 -0
  12. flint/skills/alembic/content/guides/reading-a-migration.md.jinja +109 -0
  13. flint/skills/alembic/content/references/env-and-config.md.jinja +130 -0
  14. flint/skills/alembic/content/references/gotchas.md.jinja +100 -0
  15. flint/skills/alembic/skill.json +5 -0
  16. flint/skills/celery/content/SKILL.md.jinja +83 -0
  17. flint/skills/celery/content/guides/add-a-task.md.jinja +92 -0
  18. flint/skills/celery/content/references/gotchas.md.jinja +88 -0
  19. flint/skills/celery/content/references/tasks-and-results.md.jinja +93 -0
  20. flint/skills/celery/skill.json +5 -0
  21. flint/skills/fastapi/content/SKILL.md.jinja +54 -0
  22. flint/skills/fastapi/content/guides/add-an-endpoint.md.jinja +97 -0
  23. flint/skills/fastapi/content/guides/testing.md.jinja +49 -0
  24. flint/skills/fastapi/content/references/gotchas.md.jinja +61 -0
  25. flint/skills/fastapi/content/references/routing-and-dependencies.md.jinja +85 -0
  26. flint/skills/fastapi/skill.json +5 -0
  27. flint/skills/flask/content/SKILL.md.jinja +94 -0
  28. flint/skills/flask/content/guides/add-a-resource.md.jinja +139 -0
  29. flint/skills/flask/content/guides/testing.md.jinja +110 -0
  30. flint/skills/flask/content/references/app-factory-and-config.md.jinja +110 -0
  31. flint/skills/flask/content/references/gotchas.md.jinja +146 -0
  32. flint/skills/flask/content/references/routing-and-blueprints.md.jinja +155 -0
  33. flint/skills/flask/skill.json +5 -0
  34. flint/skills/flask-migrate/content/SKILL.md.jinja +70 -0
  35. flint/skills/flask-migrate/content/guides/first-migration.md.jinja +63 -0
  36. flint/skills/flask-migrate/content/references/cli-and-wiring.md.jinja +76 -0
  37. flint/skills/flask-migrate/content/references/gotchas.md.jinja +66 -0
  38. flint/skills/flask-migrate/skill.json +5 -0
  39. flint/skills/flask-sqlalchemy/content/SKILL.md.jinja +84 -0
  40. flint/skills/flask-sqlalchemy/content/guides/add-a-model.md.jinja +90 -0
  41. flint/skills/flask-sqlalchemy/content/references/db-object-and-wiring.md.jinja +105 -0
  42. flint/skills/flask-sqlalchemy/content/references/gotchas.md.jinja +107 -0
  43. flint/skills/flask-sqlalchemy/skill.json +5 -0
  44. flint/skills/pydantic-settings/content/SKILL.md.jinja +75 -0
  45. flint/skills/pydantic-settings/content/guides/add-a-setting.md.jinja +78 -0
  46. flint/skills/pydantic-settings/content/references/framework-integration.md.jinja +104 -0
  47. flint/skills/pydantic-settings/content/references/settings-and-env.md.jinja +112 -0
  48. flint/skills/pydantic-settings/skill.json +5 -0
  49. flint/skills/pytest/content/SKILL.md.jinja +102 -0
  50. flint/skills/pytest/content/guides/parametrize.md.jinja +77 -0
  51. flint/skills/pytest/content/guides/writing-a-good-test.md.jinja +85 -0
  52. flint/skills/pytest/content/references/database-isolation.md.jinja +115 -0
  53. flint/skills/pytest/content/references/gotchas.md.jinja +113 -0
  54. flint/skills/pytest/skill.json +5 -0
  55. flint/skills/redis/content/SKILL.md.jinja +76 -0
  56. flint/skills/redis/content/guides/add-caching-to-an-endpoint.md.jinja +169 -0
  57. flint/skills/redis/content/references/gotchas.md.jinja +54 -0
  58. flint/skills/redis/content/references/redis-operations.md.jinja +67 -0
  59. flint/skills/redis/skill.json +5 -0
  60. flint/skills/sqlalchemy/content/SKILL.md.jinja +101 -0
  61. flint/skills/sqlalchemy/content/guides/add-a-model.md.jinja +205 -0
  62. flint/skills/sqlalchemy/content/references/gotchas.md.jinja +140 -0
  63. flint/skills/sqlalchemy/content/references/sessions-and-queries.md.jinja +153 -0
  64. flint/skills/sqlalchemy/skill.json +5 -0
  65. flint/skills/sqlmodel/content/SKILL.md.jinja +84 -0
  66. flint/skills/sqlmodel/content/guides/add-a-model.md.jinja +131 -0
  67. flint/skills/sqlmodel/content/references/gotchas.md.jinja +80 -0
  68. flint/skills/sqlmodel/content/references/models-and-sessions.md.jinja +142 -0
  69. flint/skills/sqlmodel/skill.json +5 -0
  70. flint/skills/taskiq/content/SKILL.md.jinja +72 -0
  71. flint/skills/taskiq/content/guides/add-a-task.md.jinja +88 -0
  72. flint/skills/taskiq/content/references/broker-and-tasks.md.jinja +121 -0
  73. flint/skills/taskiq/content/references/gotchas.md.jinja +100 -0
  74. flint/skills/taskiq/skill.json +5 -0
  75. flint/templates/fastapi/hello-world/README.md +63 -0
  76. flint/templates/fastapi/hello-world/config/env.jinja +2 -0
  77. flint/templates/fastapi/hello-world/config/src/{{package_name}}/core/__init__.py.jinja +0 -0
  78. flint/templates/fastapi/hello-world/config/src/{{package_name}}/core/config.py.jinja +11 -0
  79. flint/templates/fastapi/hello-world/config/src/{{package_name}}/main.py.jinja +10 -0
  80. flint/templates/fastapi/hello-world/docker/Dockerfile.jinja +19 -0
  81. flint/templates/fastapi/hello-world/docker/dockerignore.jinja +8 -0
  82. flint/templates/fastapi/hello-world/files/AGENTS.md.jinja +58 -0
  83. flint/templates/fastapi/hello-world/files/README.md.jinja +46 -0
  84. flint/templates/fastapi/hello-world/files/gitignore.jinja +6 -0
  85. flint/templates/fastapi/hello-world/files/pyproject.toml.jinja +24 -0
  86. flint/templates/fastapi/hello-world/files/src/{{package_name}}/__init__.py.jinja +1 -0
  87. flint/templates/fastapi/hello-world/files/src/{{package_name}}/main.py.jinja +8 -0
  88. flint/templates/fastapi/hello-world/files/tests/test_main.py.jinja +11 -0
  89. flint/templates/fastapi/hello-world/template.json +23 -0
  90. flint/templates/fastapi/rest-api/README.md +142 -0
  91. flint/templates/fastapi/rest-api/db-sqlalchemy/src/{{package_name}}/core/db.py.jinja +19 -0
  92. flint/templates/fastapi/rest-api/db-sqlalchemy/src/{{package_name}}/models.py.jinja +13 -0
  93. flint/templates/fastapi/rest-api/db-sqlalchemy/src/{{package_name}}/routes/items.py.jinja +41 -0
  94. flint/templates/fastapi/rest-api/db-sqlalchemy/tests/conftest.py.jinja +44 -0
  95. flint/templates/fastapi/rest-api/db-sqlalchemy/tests/test_main.py.jinja +29 -0
  96. flint/templates/fastapi/rest-api/db-sqlmodel/src/{{package_name}}/core/db.py.jinja +20 -0
  97. flint/templates/fastapi/rest-api/db-sqlmodel/src/{{package_name}}/models.py.jinja +7 -0
  98. flint/templates/fastapi/rest-api/db-sqlmodel/src/{{package_name}}/routes/items.py.jinja +41 -0
  99. flint/templates/fastapi/rest-api/db-sqlmodel/tests/conftest.py.jinja +45 -0
  100. flint/templates/fastapi/rest-api/db-sqlmodel/tests/test_main.py.jinja +29 -0
  101. flint/templates/fastapi/rest-api/docker/Dockerfile.jinja +19 -0
  102. flint/templates/fastapi/rest-api/docker/dockerignore.jinja +11 -0
  103. flint/templates/fastapi/rest-api/files/AGENTS.md.jinja +124 -0
  104. flint/templates/fastapi/rest-api/files/README.md.jinja +150 -0
  105. flint/templates/fastapi/rest-api/files/env.jinja +13 -0
  106. flint/templates/fastapi/rest-api/files/gitignore.jinja +9 -0
  107. flint/templates/fastapi/rest-api/files/pyproject.toml.jinja +56 -0
  108. flint/templates/fastapi/rest-api/files/src/{{package_name}}/__init__.py.jinja +1 -0
  109. flint/templates/fastapi/rest-api/files/src/{{package_name}}/core/__init__.py.jinja +0 -0
  110. flint/templates/fastapi/rest-api/files/src/{{package_name}}/core/config.py.jinja +22 -0
  111. flint/templates/fastapi/rest-api/files/src/{{package_name}}/main.py.jinja +61 -0
  112. flint/templates/fastapi/rest-api/files/src/{{package_name}}/routes/__init__.py.jinja +0 -0
  113. flint/templates/fastapi/rest-api/files/src/{{package_name}}/routes/items.py.jinja +39 -0
  114. flint/templates/fastapi/rest-api/files/src/{{package_name}}/schemas.py.jinja +12 -0
  115. flint/templates/fastapi/rest-api/files/tests/test_main.py.jinja +31 -0
  116. flint/templates/fastapi/rest-api/migrations-sqlalchemy/alembic/env.py.jinja +50 -0
  117. flint/templates/fastapi/rest-api/migrations-sqlalchemy/alembic/script.py.mako +27 -0
  118. flint/templates/fastapi/rest-api/migrations-sqlalchemy/alembic/versions/gitkeep.jinja +0 -0
  119. flint/templates/fastapi/rest-api/migrations-sqlalchemy/alembic.ini.jinja +37 -0
  120. flint/templates/fastapi/rest-api/migrations-sqlmodel/alembic/env.py.jinja +51 -0
  121. flint/templates/fastapi/rest-api/migrations-sqlmodel/alembic/script.py.mako +28 -0
  122. flint/templates/fastapi/rest-api/migrations-sqlmodel/alembic/versions/gitkeep.jinja +0 -0
  123. flint/templates/fastapi/rest-api/migrations-sqlmodel/alembic.ini.jinja +37 -0
  124. flint/templates/fastapi/rest-api/redis/src/{{package_name}}/core/redis.py.jinja +5 -0
  125. flint/templates/fastapi/rest-api/template.json +91 -0
  126. flint/templates/fastapi/rest-api/worker-celery/src/{{package_name}}/tasks/__init__.py.jinja +0 -0
  127. flint/templates/fastapi/rest-api/worker-celery/src/{{package_name}}/tasks/example.py.jinja +6 -0
  128. flint/templates/fastapi/rest-api/worker-celery/src/{{package_name}}/worker.py.jinja +16 -0
  129. flint/templates/fastapi/rest-api/worker-taskiq/src/{{package_name}}/tasks/__init__.py.jinja +0 -0
  130. flint/templates/fastapi/rest-api/worker-taskiq/src/{{package_name}}/tasks/example.py.jinja +6 -0
  131. flint/templates/fastapi/rest-api/worker-taskiq/src/{{package_name}}/worker.py.jinja +19 -0
  132. flint/templates/fastapi/template.json +7 -0
  133. flint/templates/flask/hello-world/README.md +142 -0
  134. flint/templates/flask/hello-world/config/env.jinja +2 -0
  135. flint/templates/flask/hello-world/config/src/{{package_name}}/core/__init__.py.jinja +0 -0
  136. flint/templates/flask/hello-world/config/src/{{package_name}}/core/config.py.jinja +11 -0
  137. flint/templates/flask/hello-world/config/src/{{package_name}}/main.py.jinja +12 -0
  138. flint/templates/flask/hello-world/docker/Dockerfile.jinja +19 -0
  139. flint/templates/flask/hello-world/docker/dockerignore.jinja +8 -0
  140. flint/templates/flask/hello-world/files/AGENTS.md.jinja +58 -0
  141. flint/templates/flask/hello-world/files/README.md.jinja +46 -0
  142. flint/templates/flask/hello-world/files/gitignore.jinja +6 -0
  143. flint/templates/flask/hello-world/files/pyproject.toml.jinja +26 -0
  144. flint/templates/flask/hello-world/files/src/{{package_name}}/__init__.py.jinja +1 -0
  145. flint/templates/flask/hello-world/files/src/{{package_name}}/main.py.jinja +8 -0
  146. flint/templates/flask/hello-world/files/tests/test_main.py.jinja +8 -0
  147. flint/templates/flask/hello-world/template.json +23 -0
  148. flint/templates/flask/rest-api/README.md +343 -0
  149. flint/templates/flask/rest-api/db-flask-sqlalchemy/src/{{package_name}}/core/db.py.jinja +32 -0
  150. flint/templates/flask/rest-api/db-flask-sqlalchemy/src/{{package_name}}/models.py.jinja +9 -0
  151. flint/templates/flask/rest-api/db-flask-sqlalchemy/src/{{package_name}}/routes/items.py.jinja +44 -0
  152. flint/templates/flask/rest-api/db-flask-sqlalchemy/tests/conftest.py.jinja +13 -0
  153. flint/templates/flask/rest-api/db-flask-sqlalchemy/tests/test_main.py.jinja +24 -0
  154. flint/templates/flask/rest-api/db-sqlalchemy/src/{{package_name}}/core/db.py.jinja +52 -0
  155. flint/templates/flask/rest-api/db-sqlalchemy/src/{{package_name}}/models.py.jinja +13 -0
  156. flint/templates/flask/rest-api/db-sqlalchemy/src/{{package_name}}/routes/items.py.jinja +49 -0
  157. flint/templates/flask/rest-api/db-sqlalchemy/tests/conftest.py.jinja +13 -0
  158. flint/templates/flask/rest-api/db-sqlalchemy/tests/test_main.py.jinja +24 -0
  159. flint/templates/flask/rest-api/docker/Dockerfile.jinja +20 -0
  160. flint/templates/flask/rest-api/docker/dockerignore.jinja +12 -0
  161. flint/templates/flask/rest-api/files/AGENTS.md.jinja +139 -0
  162. flint/templates/flask/rest-api/files/README.md.jinja +151 -0
  163. flint/templates/flask/rest-api/files/env.jinja +13 -0
  164. flint/templates/flask/rest-api/files/gitignore.jinja +10 -0
  165. flint/templates/flask/rest-api/files/pyproject.toml.jinja +43 -0
  166. flint/templates/flask/rest-api/files/src/{{package_name}}/__init__.py.jinja +1 -0
  167. flint/templates/flask/rest-api/files/src/{{package_name}}/core/__init__.py.jinja +0 -0
  168. flint/templates/flask/rest-api/files/src/{{package_name}}/core/config.py.jinja +22 -0
  169. flint/templates/flask/rest-api/files/src/{{package_name}}/main.py.jinja +50 -0
  170. flint/templates/flask/rest-api/files/src/{{package_name}}/routes/__init__.py.jinja +0 -0
  171. flint/templates/flask/rest-api/files/src/{{package_name}}/routes/items.py.jinja +46 -0
  172. flint/templates/flask/rest-api/files/src/{{package_name}}/schemas.py.jinja +14 -0
  173. flint/templates/flask/rest-api/files/tests/test_main.py.jinja +30 -0
  174. flint/templates/flask/rest-api/migrations-flask-sqlalchemy/migrations/README +1 -0
  175. flint/templates/flask/rest-api/migrations-flask-sqlalchemy/migrations/alembic.ini +50 -0
  176. flint/templates/flask/rest-api/migrations-flask-sqlalchemy/migrations/env.py +113 -0
  177. flint/templates/flask/rest-api/migrations-flask-sqlalchemy/migrations/script.py.mako +24 -0
  178. flint/templates/flask/rest-api/migrations-flask-sqlalchemy/migrations/versions/gitkeep.jinja +0 -0
  179. flint/templates/flask/rest-api/migrations-flask-sqlalchemy/src/{{package_name}}/core/db.py.jinja +39 -0
  180. flint/templates/flask/rest-api/migrations-sqlalchemy/alembic/env.py.jinja +44 -0
  181. flint/templates/flask/rest-api/migrations-sqlalchemy/alembic/script.py.mako +27 -0
  182. flint/templates/flask/rest-api/migrations-sqlalchemy/alembic/versions/gitkeep.jinja +0 -0
  183. flint/templates/flask/rest-api/migrations-sqlalchemy/alembic.ini.jinja +37 -0
  184. flint/templates/flask/rest-api/redis/src/{{package_name}}/core/redis.py.jinja +5 -0
  185. flint/templates/flask/rest-api/template.json +89 -0
  186. flint/templates/flask/rest-api/worker-celery/src/{{package_name}}/tasks/__init__.py.jinja +0 -0
  187. flint/templates/flask/rest-api/worker-celery/src/{{package_name}}/tasks/example.py.jinja +6 -0
  188. flint/templates/flask/rest-api/worker-celery/src/{{package_name}}/worker.py.jinja +16 -0
  189. flint/templates/flask/template.json +7 -0
  190. flint_kit-0.14.0.dist-info/METADATA +120 -0
  191. flint_kit-0.14.0.dist-info/RECORD +193 -0
  192. flint_kit-0.14.0.dist-info/WHEEL +4 -0
  193. flint_kit-0.14.0.dist-info/entry_points.txt +3 -0
flint/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """Flint — interactive project scaffolding for Python frameworks."""
2
+
3
+ __version__ = "0.14.0"
flint/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ from flint.cli import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
flint/cli.py ADDED
@@ -0,0 +1,289 @@
1
+ """The `flint` command-line app.
2
+
3
+ See PRODUCT_FLOW.md §1 for the entry-point table this implements: bare
4
+ `flint` behaves like `flint new` (an interactive wizard), `flint new
5
+ NAME --flags...` skips prompts for anything a flag already answers, and
6
+ `--yes` / a non-TTY stdin skips prompting entirely.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import sys
12
+ from pathlib import Path
13
+ from typing import Annotated, Optional
14
+
15
+ import typer
16
+ from rich.console import Console
17
+ from rich.table import Table
18
+
19
+ from flint import __version__
20
+ from flint import generator, postgen, prefs, prompts
21
+ from flint.errors import FlintUserError
22
+
23
+ app = typer.Typer(add_completion=False, no_args_is_help=False)
24
+ console = Console()
25
+
26
+
27
+ @app.callback(invoke_without_command=True)
28
+ def root(
29
+ ctx: typer.Context,
30
+ version: Annotated[
31
+ bool, typer.Option("--version", help="Show the version and exit.")
32
+ ] = False,
33
+ ) -> None:
34
+ """Flint — interactive project scaffolding for Python frameworks."""
35
+ if version:
36
+ console.print(f"flint {__version__}")
37
+ raise typer.Exit()
38
+ if ctx.invoked_subcommand is None:
39
+ _run_new(
40
+ name=None,
41
+ framework=None,
42
+ template=None,
43
+ option=[],
44
+ docker=None,
45
+ git=None,
46
+ install=None,
47
+ yes=False,
48
+ force=False,
49
+ remember=True,
50
+ )
51
+
52
+
53
+ @app.command()
54
+ def new(
55
+ name: Annotated[
56
+ Optional[str], typer.Argument(help="Project name, e.g. 'my-api'.")
57
+ ] = None,
58
+ framework: Annotated[
59
+ Optional[str], typer.Option(help="Framework to use, e.g. 'fastapi'.")
60
+ ] = None,
61
+ template: Annotated[
62
+ Optional[str],
63
+ typer.Option(help="Template variant within the framework, e.g. 'hello-world'."),
64
+ ] = None,
65
+ option: Annotated[
66
+ list[str],
67
+ typer.Option(
68
+ "--option",
69
+ "-o",
70
+ help=(
71
+ "Set a template-specific option as key=value (repeatable), "
72
+ "e.g. -o database=postgres -o orm=sqlmodel. See the wizard "
73
+ "or the template's README for available keys."
74
+ ),
75
+ ),
76
+ ] = [], # noqa: B006 - Typer's documented pattern for repeatable options
77
+ docker: Annotated[
78
+ Optional[bool],
79
+ typer.Option("--docker/--no-docker", help="Add a Dockerfile."),
80
+ ] = None,
81
+ git: Annotated[
82
+ Optional[bool],
83
+ typer.Option("--git/--no-git", help="Initialize a git repository."),
84
+ ] = None,
85
+ install: Annotated[
86
+ Optional[bool],
87
+ typer.Option("--install/--no-install", help="Install dependencies with uv."),
88
+ ] = None,
89
+ yes: Annotated[
90
+ bool,
91
+ typer.Option(
92
+ "--yes", "-y", help="Accept defaults for anything not given as a flag."
93
+ ),
94
+ ] = False,
95
+ force: Annotated[
96
+ bool,
97
+ typer.Option(
98
+ "--force", help="Generate even if the target directory already exists."
99
+ ),
100
+ ] = False,
101
+ remember: Annotated[
102
+ bool,
103
+ typer.Option(
104
+ "--remember/--no-remember",
105
+ help=(
106
+ "Remember these choices in ~/.flint/last.json as the "
107
+ "default for next time (framework/template + per-template "
108
+ "options, docker, git, install). Explicit flags/--option "
109
+ "always win regardless."
110
+ ),
111
+ ),
112
+ ] = True,
113
+ ) -> None:
114
+ """Generate a new project."""
115
+ _run_new(
116
+ name=name,
117
+ framework=framework,
118
+ template=template,
119
+ option=option,
120
+ docker=docker,
121
+ git=git,
122
+ install=install,
123
+ yes=yes,
124
+ force=force,
125
+ remember=remember,
126
+ )
127
+
128
+
129
+ @app.command("list-templates")
130
+ def list_templates_cmd() -> None:
131
+ """List available framework/template combinations."""
132
+ table = Table()
133
+ table.add_column("Framework")
134
+ table.add_column("Template")
135
+ table.add_column("Description")
136
+ table.add_column("Docker", justify="center")
137
+
138
+ for framework in generator.list_frameworks():
139
+ framework_label = (
140
+ framework.label if framework.enabled else f"{framework.label} (coming soon)"
141
+ )
142
+ for template in generator.list_templates(framework.id):
143
+ template_label = (
144
+ template.label if template.enabled else f"{template.label} (coming soon)"
145
+ )
146
+ table.add_row(
147
+ framework_label,
148
+ template_label,
149
+ template.description,
150
+ "✓" if template.supports_docker else "—",
151
+ )
152
+
153
+ console.print(table)
154
+ console.print(
155
+ "\nPass a pair with e.g. [bold]flint new my-api --framework fastapi "
156
+ "--template rest-api[/bold]. Templates with their own choices "
157
+ "(database, ORM, ...) take -o key=value — see the wizard or each "
158
+ "template's README for available keys."
159
+ )
160
+
161
+
162
+ def _run_new(
163
+ *,
164
+ name: Optional[str],
165
+ framework: Optional[str],
166
+ template: Optional[str],
167
+ option: list[str],
168
+ docker: Optional[bool],
169
+ git: Optional[bool],
170
+ install: Optional[bool],
171
+ yes: bool,
172
+ force: bool,
173
+ remember: bool,
174
+ ) -> None:
175
+ interactive = sys.stdin.isatty() and not yes
176
+ stored_prefs = prefs.load_prefs() if remember else {}
177
+
178
+ try:
179
+ project_name, slug, package_name = prompts.prompt_project_name(name, interactive)
180
+
181
+ target_dir = Path.cwd() / slug
182
+ if target_dir.exists() and any(target_dir.iterdir()):
183
+ force = prompts.prompt_force_overwrite(slug, force, interactive)
184
+ if not force:
185
+ raise FlintUserError(
186
+ f"Directory '{slug}' already exists and is not empty. "
187
+ "Use --force to generate into it anyway."
188
+ )
189
+
190
+ frameworks = generator.list_frameworks()
191
+ framework_id = prompts.prompt_framework(
192
+ framework,
193
+ frameworks,
194
+ interactive,
195
+ default_id=prefs.get_last_framework(stored_prefs),
196
+ )
197
+
198
+ chosen_framework = generator.get_framework(framework_id)
199
+
200
+ templates = generator.list_templates(framework_id)
201
+ template_id = prompts.prompt_template(
202
+ template,
203
+ templates,
204
+ interactive,
205
+ default_id=prefs.get_last_template(stored_prefs, framework_id),
206
+ )
207
+
208
+ chosen_template = generator.get_template(framework_id, template_id)
209
+ remembered = prefs.get_template_prefs(stored_prefs, chosen_template.full_id)
210
+
211
+ provided_options = prompts.parse_option_flags(option)
212
+ resolved_options = prompts.prompt_template_options(
213
+ chosen_template, provided_options, interactive, last=remembered["options"]
214
+ )
215
+
216
+ docker_requested = prompts.prompt_docker(
217
+ docker, interactive, remembered=remembered["docker"]
218
+ )
219
+ if docker_requested and not chosen_template.supports_docker:
220
+ console.print(
221
+ f"[yellow]![/yellow] {chosen_template.full_id} doesn't support "
222
+ "--docker yet — skipping the Dockerfile."
223
+ )
224
+ docker_requested = False
225
+
226
+ if interactive:
227
+ console.print("Using uv to manage dependencies.")
228
+
229
+ git_init = prompts.prompt_git_init(git, interactive, remembered=remembered["git_init"])
230
+ install_now = prompts.prompt_install(install, interactive, remembered=remembered["install"])
231
+
232
+ answers = generator.Answers(
233
+ project_name=project_name,
234
+ slug=slug,
235
+ package_name=package_name,
236
+ framework=framework_id,
237
+ template=template_id,
238
+ git_init=git_init,
239
+ install=install_now,
240
+ docker=docker_requested,
241
+ options=resolved_options,
242
+ )
243
+ created = generator.render(framework_id, template_id, target_dir, answers, force=force)
244
+
245
+ if remember:
246
+ prefs.save_prefs(
247
+ prefs.record_run(
248
+ stored_prefs,
249
+ framework_id=framework_id,
250
+ template_id=template_id,
251
+ full_id=chosen_template.full_id,
252
+ options=resolved_options,
253
+ docker=docker_requested,
254
+ git_init=git_init,
255
+ install=install_now,
256
+ )
257
+ )
258
+
259
+ git_ok = postgen.git_init(target_dir) if git_init else False
260
+ installed_ok = postgen.install_dependencies(target_dir) if install_now else False
261
+
262
+ postgen.print_summary(
263
+ project_name=project_name,
264
+ slug=slug,
265
+ template_full_id=chosen_template.full_id,
266
+ target_dir=target_dir,
267
+ created=created,
268
+ git_ok=git_ok,
269
+ installed_ok=installed_ok,
270
+ installed_requested=install_now,
271
+ run_command=chosen_framework.run_command.format(package_name=package_name),
272
+ options=resolved_options,
273
+ )
274
+ except FlintUserError as exc:
275
+ console.print(f"[red]Error:[/red] {exc}")
276
+ raise typer.Exit(1) from None
277
+ except typer.Exit:
278
+ raise
279
+ except Exception as exc: # noqa: BLE001 - top-level safety net, see errors.py
280
+ console.print(f"[red]Unexpected error:[/red] {exc}")
281
+ raise typer.Exit(2) from None
282
+
283
+
284
+ def main() -> None:
285
+ app()
286
+
287
+
288
+ if __name__ == "__main__":
289
+ main()
flint/errors.py ADDED
@@ -0,0 +1,17 @@
1
+ """Flint's error hierarchy.
2
+
3
+ `FlintUserError` maps to exit code 1 (the user did something we can
4
+ explain, e.g. an existing directory or an invalid name). Anything else
5
+ bubbles up and is treated as exit code 2 (unexpected/internal error), per
6
+ PRODUCT_FLOW.md §5.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+
12
+ class FlintError(Exception):
13
+ """Base class for all Flint-raised errors."""
14
+
15
+
16
+ class FlintUserError(FlintError):
17
+ """An error caused by user input/environment, not a Flint bug."""