@@ -138,18 +138,23 @@ DEFAULT_HEALTH = "/api/health?from=devserver.py"
UPGRADE_MARKER = " auto-upgrade@fastapi-vue-setup "
UPGRADE_MARKER = " auto-upgrade@fastapi-vue-setup "
# pyproject.toml additions for patched projects
# pyproject.toml additions for patched projects
# packages/only-include use the package path relative to the project root
# (MODULE_NAME placeholder), so both flat and nested layouts work; the wheel
# target strips the leading directories, mapping e.g. backend/my_app -> my_app
PYPROJECT_ADDITIONS = {
PYPROJECT_ADDITIONS = {
" tool " : {
" tool " : {
" hatch " : {
" hatch " : {
" build " : {
" build " : {
" packages " : [ " MODULE_NAME " ] ,
" artifacts " : [ " MODULE_NAME/frontend-build " ] ,
" artifacts " : [ " MODULE_NAME/frontend-build " ] ,
" targets " : {
" targets " : {
" wheel " : {
" packages " : [ " MODULE_NAME " ] ,
} ,
" sdist " : {
" sdist " : {
" only-include " : [ " MODULE_NAME " , " scripts/fastapi-vue " ] ,
" hooks " : { " custom " : { " path " : " scripts/fastapi-vue/buildhook.py " } } ,
" hooks " : { " custom " : { " path " : " scripts/fastapi-vue/buildhook.py " } } ,
}
} ,
} ,
} ,
" only-packages " : True ,
}
}
}
}
} ,
} ,
@@ -368,6 +373,19 @@ def find_module_name(project_dir: Path) -> str | None:
return None
return None
def find_module_dir ( project_dir : Path , module_name : str ) - > Path :
""" Locate the Python package directory.
Supports flat (<module>), src/<module>, and backend/<module> layouts.
For new projects (no package yet), defaults to the backend/ layout.
"""
for sub in ( module_name , f " src/ { module_name } " , f " backend/ { module_name } " ) :
candidate = project_dir / sub
if candidate . is_dir ( ) :
return candidate
return project_dir / " backend " / module_name
def find_fastapi_app ( module_dir : Path , project_dir : Path | None = None ) - > tuple [ Path , str ] | None :
def find_fastapi_app ( module_dir : Path , project_dir : Path | None = None ) - > tuple [ Path , str ] | None :
""" Find the FastAPI app in a module directory.
""" Find the FastAPI app in a module directory.
@@ -1180,13 +1198,26 @@ def _write_fallback_file(
def merge_pyproject (
def merge_pyproject (
data : tomlkit . TOMLDocument , additions : dict , module_name : str
data : tomlkit . TOMLDocument , additions : dict , module_rel : str
) - > tomlkit . TOMLDocument :
) - > tomlkit . TOMLDocument :
""" Merge additions into pyproject.toml data, preserving comments and existing values. """
""" Merge additions into pyproject.toml data, preserving comments and existing values.
module_rel is the package path relative to the project root (e.g. " backend/my_app " ).
"""
# Ensure hatchling build system is configured
# Ensure hatchling build system is configured
if " build-system " not in data :
if " build-system " not in data :
data [ " build-system " ] = tomlkit . table ( )
data [ " build-system " ] = tomlkit . table ( )
# Replace uv_build with hatchling: the frontend build hook and artifacts
# handling require hatch, which uv_build does not support
if data [ " build-system " ] . get ( " build-backend " ) == " uv_build " :
data [ " build-system " ] [ " requires " ] = [ " hatchling " ]
data [ " build-system " ] [ " build-backend " ] = " hatchling.build "
uv_tool = data . get ( " tool " , { } ) . get ( " uv " , { } ) if " tool " in data else { }
if " build-backend " in uv_tool :
del data [ " tool " ] [ " uv " ] [ " build-backend " ]
print ( " ℹ ️ Replaced uv_build with hatchling (required for the frontend build hook)" )
# Add hatchling to requires if not present, preserving existing requires
# Add hatchling to requires if not present, preserving existing requires
if " requires " not in data [ " build-system " ] :
if " requires " not in data [ " build-system " ] :
data [ " build-system " ] [ " requires " ] = [ " hatchling " ]
data [ " build-system " ] [ " requires " ] = [ " hatchling " ]
@@ -1226,38 +1257,45 @@ def merge_pyproject(
hatch_build = data [ " tool " ] [ " hatch " ] [ " build " ]
hatch_build = data [ " tool " ] [ " hatch " ] [ " build " ]
hatch_additions = additions [ " tool " ] [ " hatch " ] [ " build " ]
hatch_additions = additions [ " tool " ] [ " hatch " ] [ " build " ]
# Set packages if not already set
if " packages " not in hatch_build :
hatch_build [ " packages " ] = [
p . replace ( " MODULE_NAME " , module_name ) for p in hatch_additions [ " packages " ]
]
# Set artifacts if not already set
# Set artifacts if not already set
if " artifacts " not in hatch_build :
if " artifacts " not in hatch_build :
hatch_build [ " artifacts " ] = [
hatch_build [ " artifacts " ] = [
a . replace ( " MODULE_NAME " , module_name ) for a in hatch_additions [ " artifacts " ]
a . replace ( " MODULE_NAME " , module_rel ) for a in hatch_additions [ " artifacts " ]
]
]
# Set only-packages if not already set
# Add wheel/sdist targets
if " only-packages " not in hatch_build :
hatch_build [ " only-packages " ] = hatch_additions [ " only-packages " ]
# Add sdist target with custom hook
if " targets " not in hatch_build :
if " targets " not in hatch_build :
hatch_build [ " targets " ] = tomlkit . table ( )
hatch_build [ " targets " ] = tomlkit . table ( )
if " sdist " not in hatch_build [ " targets " ] :
targets = hatch_build [ " targets " ]
hatch_build [ " targets " ] [ " sdist " ] = tomlkit . table ( )
target_additions = hatch_additions [ " targets " ]
if " hooks " not in hatch_build [ " targets " ] [ " sdist " ] :
hatch_build [ " targets " ] [ " sdist " ] [ " hooks " ] = tomlkit . table ( )
# Wheel: set packages if neither the wheel target nor the top-level
if " custom " not in hatch_build [ " targets " ] [ " sdist " ] [ " hooks " ] :
# hatch build config (older setups) defines them
hatch_build [ " targets " ] [ " sdist " ] [ " hooks " ] [ " custom " ] = tomlkit . table ( )
if " wheel " not in targets :
if " path " not in hatch_build [ " targets " ] [ " sdist " ] [ " hooks " ] [ " custom " ] :
targets [ " wheel " ] = tomlkit . table ( )
hatch_build [ " targets " ] [ " sdist " ] [ " hooks " ] [ " custom " ] [ " path " ] = hatch_additions [ " targets " ] [
if " packages " not in targets [ " wheel " ] and " packages " not in hatch_build :
" sdist "
targets [ " wheel " ] [ " packages " ] = [
] [ " hooks " ] [ " custom " ] [ " path " ]
p . replace ( " MODULE_NAME " , module_rel ) for p in target_additions [ " wheel " ] [ " packages " ]
elif hatch_build [ " targets " ] [ " sdist " ] [ " hooks " ] [ " custom " ] [ " path " ] == OLD_BUILD_HOOK_PATH :
]
# Sdist: only-include (keeps the package directory prefix so wheels built
# from the sdist find the same layout) plus the frontend build hook
if " sdist " not in targets :
targets [ " sdist " ] = tomlkit . table ( )
sdist = targets [ " sdist " ]
if " only-include " not in sdist :
sdist [ " only-include " ] = [
p . replace ( " MODULE_NAME " , module_rel ) for p in target_additions [ " sdist " ] [ " only-include " ]
]
if " hooks " not in sdist :
sdist [ " hooks " ] = tomlkit . table ( )
if " custom " not in sdist [ " hooks " ] :
sdist [ " hooks " ] [ " custom " ] = tomlkit . table ( )
if " path " not in sdist [ " hooks " ] [ " custom " ] :
sdist [ " hooks " ] [ " custom " ] [ " path " ] = target_additions [ " sdist " ] [ " hooks " ] [ " custom " ] [ " path " ]
elif sdist [ " hooks " ] [ " custom " ] [ " path " ] == OLD_BUILD_HOOK_PATH :
# Migrate old build hook path to new name
# Migrate old build hook path to new name
hatch_build [ " targets " ] [ " sdist " ] [ " hooks " ] [ " custom " ] [ " path " ] = NEW_BUILD_HOOK_PATH
sdist [ " hooks " ] [ " custom " ] [ " path " ] = NEW_BUILD_HOOK_PATH
return data
return data
@@ -1267,40 +1305,132 @@ def merge_pyproject(
# =============================================================================
# =============================================================================
def find_js_runtime ( ) - > tuple [ str , str ] | None :
MIN_NODE_VERSION = 20
""" Find a JavaScript runtime from JS_RUNTIME env or auto-detect.
Returns (tool_path, tool_name) where tool_name is " deno " , " npm " , or " bun " .
Returns None if no runtime is found.
def _check_node_version ( node_path : str ) - > None :
""" Check if Node.js version is >= 20.
Raises RuntimeError if version is too old or cannot be determined.
"""
"""
options = [ " deno " , " npm " , " bun " ]
try :
result = subprocess . run ( # noqa: S603
[ node_path , " --version " ] ,
capture_output = True ,
text = True ,
check = True ,
)
version_str = result . stdout . strip ( )
# Parse version like "v20.10.0" or "v18.17.1"
match = re . match ( r " v( \ d+) " , version_str )
if match :
major_version = int ( match . group ( 1 ) )
if major_version > = MIN_NODE_VERSION :
return
msg = f " Node.js { version_str } found, but v { MIN_NODE_VERSION } + required "
raise RuntimeError ( msg )
except ( subprocess . CalledProcessError , FileNotFoundError , ValueError ) :
pass
msg = " Could not determine Node.js version "
raise RuntimeError ( msg )
def _validate_npm_runtime ( tool : str ) - > bool :
""" Validate npm runtime by checking Node.js version. Returns True if valid. """
node_path = shutil . which ( " node " , path = str ( Path ( tool ) . parent ) )
if node_path is None :
return False
try :
_check_node_version ( node_path )
except RuntimeError :
return False
return True
def _find_runtime_from_env ( options : list [ str ] ) - > tuple [ str , str ] | None :
""" Find runtime specified by JS_RUNTIME environment variable. """
js_runtime_env = os . environ . get ( " JS_RUNTIME " )
if not js_runtime_env :
return None
# Check for JS_RUNTIME environment variable
if js_runtime_env := os . environ . get ( " JS_RUNTIME " ) :
js_runtime = js_runtime_env
js_runtime = js_runtime_env
js_path = Path ( js_runtime )
js_path = Path ( js_runtime )
runtime_name = js_path . name
runtime_name = js_path . name
# Map node to npm
# Map node to npm
if runtime_name == " node " :
if runtime_name == " node " :
runtime_name = " npm "
runtime_name = " npm "
js_runtime = str ( js_path . parent / " npm " ) if js_path . parent . name else " npm "
js_runtime = str ( js_path . parent / " npm " ) if js_path . parent . name else " npm "
for option in options :
for option in options :
if option = = runtime_name or runtime_name . startswith ( option ) :
if option ! = runtime_name and not runtime_name . startswith ( option ) :
continue
tool = shutil . which ( js_runtime )
tool = shutil . which ( js_runtime )
if tool is None :
if tool is None :
print ( f " ⚠️ JS_RUNTIME= { js_runtime_env } not found " )
print ( f " ⚠️ JS_RUNTIME= { js_runtime_env } : { option } not found " )
return None
return None
if option == " npm " :
node_path = shutil . which ( " node " , path = str ( Path ( tool ) . parent ) )
if node_path is None :
print ( f " ⚠️ JS_RUNTIME= { js_runtime_env } : node not found " )
return None
try :
_check_node_version ( node_path )
except RuntimeError as e :
print ( f " ⚠️ JS_RUNTIME= { js_runtime_env } : { e } " )
return None
return tool , option
return tool , option
print ( f " ⚠️ JS_RUNTIME= { js_runtime_env } not recognized " )
print ( f " ⚠️ JS_RUNTIME= { js_runtime_env } not recognized " )
return None
return None
# Auto-detect
def _auto_detect_runtime ( options : list [ str ] ) - > tuple [ str , str ] | None :
""" Auto-detect JavaScript runtime from available options. """
node_version_error : RuntimeError | None = None
for option in options :
for option in options :
if tool : = shutil . which ( option ) :
tool = shutil . which ( option )
if not tool :
continue
if option == " npm " and not _validate_npm_runtime ( tool ) :
try :
node_path = shutil . which ( " node " , path = str ( Path ( tool ) . parent ) )
if node_path :
_check_node_version ( node_path )
except RuntimeError as e :
node_version_error = e
continue
return tool , option
return tool , option
if node_version_error :
print ( f " ⚠️ { node_version_error } " )
return None
return None
def find_js_runtime ( ) - > tuple [ str , str ] | None :
""" Find a JavaScript runtime from JS_RUNTIME env or auto-detect.
Returns (tool_path, tool_name) where tool_name is " npm " , " deno " , or " bun " .
Returns None if no runtime is found.
"""
options = [ " npm " , " deno " , " bun " ]
# Check for JS_RUNTIME environment variable
if result := _find_runtime_from_env ( options ) :
return result
if os . environ . get ( " JS_RUNTIME " ) :
return None
# Auto-detect
return _auto_detect_runtime ( options )
def ensure_python_project ( project_dir : Path , * , dry : bool = False ) - > bool :
def ensure_python_project ( project_dir : Path , * , dry : bool = False ) - > bool :
""" Ensure pyproject.toml exists, run uv init if needed. """
""" Ensure pyproject.toml exists, run uv init if needed. """
pyproject = project_dir / " pyproject.toml "
pyproject = project_dir / " pyproject.toml "
@@ -1308,17 +1438,20 @@ def ensure_python_project(project_dir: Path, *, dry: bool = False) -> bool:
return True
return True
if dry :
if dry :
print ( f " 📦 Would run: uv init { project_dir } " )
print ( f " 📦 Would run: uv init --bare --build-backend hatchling { project_dir } " )
return True
return True
print ( " 📦 No pyproject.toml found, initializing Python project... " )
print ( " 📦 No pyproject.toml found, initializing Python project... " )
print ( " >>> uv init " )
print ( " >>> uv init --bare --build-backend hatchling " )
result = subprocess . run ( [ " uv " , " init " , str ( project_dir ) ] , check = False ) # noqa: S603, S607
result = subprocess . run ( # noqa: S603
[ " uv " , " init " , " --bare " , " --vcs " , " git " , " --build-backend " , " hatchling " , str ( project_dir ) ] , # noqa: S607
check = False ,
)
if result . returncode != 0 :
if result . returncode != 0 :
print ( " ❌ uv init failed " )
print ( " ❌ uv init failed " )
return False
return False
# Remove files created by uv init that we don't need
# Remove boilerplate files created by older uv init versions
for filename in [ " hello.py " , " main.py " , " .python-version " ] :
for filename in [ " hello.py " , " main.py " , " .python-version " ] :
filepath = project_dir / filename
filepath = project_dir / filename
if filepath . exists ( ) :
if filepath . exists ( ) :
@@ -1341,7 +1474,7 @@ def ensure_frontend(
# Find JS runtime
# Find JS runtime
runtime = find_js_runtime ( )
runtime = find_js_runtime ( )
if runtime is None :
if runtime is None :
print ( " ❌ No JavaScript runtime found (need deno, npm , or bun) " )
print ( " ❌ No JavaScript runtime found (need Node.js v20+/npm, deno , or bun) " )
return False
return False
js_tool , js_name = runtime
js_tool , js_name = runtime
@@ -1361,14 +1494,37 @@ def ensure_frontend(
print ( f " 🎨 Would run: { ' ' . join ( create_cmd ) } " )
print ( f " 🎨 Would run: { ' ' . join ( create_cmd ) } " )
return True
return True
# Remember pre-existing content so a failed create-vue can be cleaned up
pre_existing = set ( frontend_dir . iterdir ( ) ) if frontend_dir . exists ( ) else set ( )
print ( " 🎨 No frontend/ found, creating Vue project... " )
print ( " 🎨 No frontend/ found, creating Vue project... " )
print ( f " >>> { ' ' . join ( create_cmd ) } " )
print ( f " >>> { ' ' . join ( create_cmd ) } " )
if not vue_args :
if not vue_args :
print ( " (Follow the prompts to configure your Vue app) " )
print ( " (Follow the prompts to configure your Vue app) " )
print ( )
print ( )
try :
result = subprocess . run ( create_cmd , cwd = project_dir , check = False ) # noqa: S603
result = subprocess . run ( create_cmd , cwd = project_dir , check = False ) # noqa: S603
if result . returncode != 0 :
except KeyboardInterrupt :
print ( " ❌ create-vue failed " )
result = None
# create-vue exits 0 when its prompts are cancelled, so a zero return
# code alone doesn't mean a project was created.
if result is None or result . returncode != 0 or not package_json . exists ( ) :
reason = " interrupted " if result is None else " failed or was cancelled "
print ( f " ❌ create-vue { reason } " )
leftovers = (
set ( frontend_dir . iterdir ( ) ) - pre_existing if frontend_dir . exists ( ) else set ( )
)
if leftovers :
if pre_existing :
for path in leftovers :
if path . is_dir ( ) :
shutil . rmtree ( path , ignore_errors = True )
else :
path . unlink ( missing_ok = True )
print ( " 🧹 Removed partial files created by create-vue " )
else :
shutil . rmtree ( frontend_dir , ignore_errors = True )
print ( " 🧹 Removed partial frontend/ " )
return False
return False
return True
return True
@@ -1398,20 +1554,26 @@ def cmd_setup(args: argparse.Namespace) -> int:
print ( " 🏃 DRY RUN MODE - no changes will be made \n " )
print ( " 🏃 DRY RUN MODE - no changes will be made \n " )
# Create project directory if it doesn't exist
# Create project directory if it doesn't exist
created_project_dir = False
if not project_dir . exists ( ) :
if not project_dir . exists ( ) :
if dry :
if dry :
print ( f " ✅ Would create directory: { project_dir } " )
print ( f " ✅ Would create directory: { project_dir } " )
else :
else :
project_dir . mkdir ( parents = True )
project_dir . mkdir ( parents = True )
created_project_dir = True
print ( f " ✅ Created { project_dir } " )
print ( f " ✅ Created { project_dir } " )
print ( f " 🔧 Setting up project: { project_dir } " )
print ( f " 🔧 Setting up project: { project_dir } " )
# Step 1: Ensure frontend exists (do this first so cancellation doesn't leave partial setup)
# Step 1: Ensure frontend exists (do this first so cancellation doesn't leave partial setup)
if not ensure_frontend ( project_dir , vue_args = args . vue_args , dry = dry ) :
if not ensure_frontend ( project_dir , vue_args = args . vue_args , dry = dry ) :
if created_project_dir :
shutil . rmtree ( project_dir , ignore_errors = True )
print ( f " 🧹 Removed { project_dir } " )
return 1
return 1
# Step 2: Ensure Python project exists
# Step 2: Ensure Python project exists
fresh_project = not ( project_dir / " pyproject.toml " ) . exists ( )
if not ensure_python_project ( project_dir , dry = dry ) :
if not ensure_python_project ( project_dir , dry = dry ) :
return 1
return 1
@@ -1424,7 +1586,10 @@ def cmd_setup(args: argparse.Namespace) -> int:
else :
else :
print ( f " 📦 Module: { module_name } " )
print ( f " 📦 Module: { module_name } " )
module_dir = project_dir / module_name
module_dir = find_module_dir ( project_dir , module_name )
module_rel = module_dir . relative_to ( project_dir ) . as_posix ( )
if module_rel != module_name :
print ( f " 📂 Package layout: { module_rel } / " )
scripts_dir = project_dir / " scripts "
scripts_dir = project_dir / " scripts "
fastapi_vue_scripts = scripts_dir / " fastapi-vue "
fastapi_vue_scripts = scripts_dir / " fastapi-vue "
@@ -1436,8 +1601,9 @@ def cmd_setup(args: argparse.Namespace) -> int:
if existing_cli_module :
if existing_cli_module :
print ( f " ℹ ️ Using existing CLI: { existing_cli_module } " )
print ( f " ℹ ️ Using existing CLI: { existing_cli_module } " )
# Resolve the CLI main module to a file path
# Resolve the CLI main module to a file path (relative to the import root,
main = project_dir / Path ( * main_module_path . split ( " . " ) ) . with_suffix ( " .py " )
# i.e. the parent of the package directory)
main = module_dir . parent / Path ( * main_module_path . split ( " . " ) ) . with_suffix ( " .py " )
# Determine port configuration
# Determine port configuration
# Priority: --ports argument > existing project values > defaults
# Priority: --ports argument > existing project values > defaults
@@ -1485,6 +1651,7 @@ def cmd_setup(args: argparse.Namespace) -> int:
# Template variables
# Template variables
tpl_vars = {
tpl_vars = {
" MODULE_NAME " : module_name ,
" MODULE_NAME " : module_name ,
" MODULE_DIR " : module_rel ,
" PROJECT_TITLE " : project_title ,
" PROJECT_TITLE " : project_title ,
" TEMPLATE_DEFAULT_PORT " : str ( default_port ) ,
" TEMPLATE_DEFAULT_PORT " : str ( default_port ) ,
" TEMPLATE_VITE_PORT " : str ( vite_port ) ,
" TEMPLATE_VITE_PORT " : str ( vite_port ) ,
@@ -1499,8 +1666,8 @@ def cmd_setup(args: argparse.Namespace) -> int:
app_file , app_var = app_info
app_file , app_var = app_info
print ( f " 📍 Found FastAPI app: { app_var } in { app_file . name } " )
print ( f " 📍 Found FastAPI app: { app_var } in { app_file . name } " )
tpl_vars [ " APP_VAR " ] = app_var
tpl_vars [ " APP_VAR " ] = app_var
# Dotted module path relative to project dir (e.g. "{module_name}.api.main")
# Dotted module path (e.g. "{module_name}.api.main")
app_module = " . " . join ( app_file . relative_to ( project_dir ) . with_suffix ( " " ) . parts )
app_module = " . " . join ( app_file . relative_to ( module_dir . parent ) . with_suffix ( " " ) . parts )
tpl_vars [ " APP_MODULE " ] = app_module
tpl_vars [ " APP_MODULE " ] = app_module
else :
else :
print ( " 📍 No existing FastAPI app found, will create new one " )
print ( " 📍 No existing FastAPI app found, will create new one " )
@@ -1583,12 +1750,15 @@ def cmd_setup(args: argparse.Namespace) -> int:
)
)
else :
else :
# No app: create full app.py
# No app: create full app.py
# Create __init__.py if missing
# Create __init__.py if missing (overwrite uv init hello-world boilerplate)
init_file = module_dir / " __init__.py "
init_file = module_dir / " __init__.py "
if not init_file . exists ( ) :
boilerplate = init_file . exists ( ) and re . search (
r ' ^def main \ ( \ ) -> None: \ s* \ n \ s*print \ ( " Hello from ' , init_file . read_text ( " UTF-8 " )
)
if not init_file . exists ( ) or boilerplate :
template = load_template ( " backend/__init__.py " )
template = load_template ( " backend/__init__.py " )
content = render_template ( template , * * tpl_vars )
content = render_template ( template , * * tpl_vars )
write_file ( init_file , content , overwrite = False , dry = dry )
write_file ( init_file , content , overwrite = False , dry = dry , force = boilerplate )
# Create app.py
# Create app.py
app_file_path = module_dir / " app.py "
app_file_path = module_dir / " app.py "
@@ -1669,9 +1839,10 @@ def cmd_setup(args: argparse.Namespace) -> int:
plugin_file = frontend_dir / " vite-plugin-fastapi.js "
plugin_file = frontend_dir / " vite-plugin-fastapi.js "
# Upgrade old plugin versions that lack the auto-upgrade marker
# Upgrade old plugin versions that lack the auto-upgrade marker
_upgrade_old_vite_plugin ( plugin_file , module_name , dry = dry )
_upgrade_old_vite_plugin ( plugin_file , module_name , dry = dry )
template = load_template ( " frontend/vite-plugin-fastapi.js " )
for plugin_template in ( " vite-plugin-fastapi.js " , " vite-plugin-fastapi.d.ts " ) :
template = load_template ( f " frontend/ { plugin_template } " )
content = render_template ( template , * * tpl_vars )
content = render_template ( template , * * tpl_vars )
write_file ( plugin_file , content , overwrite = True , dry = dry )
write_file ( frontend_dir / plugin_template , content , overwrite = True , dry = dry )
# Find existing vite config (prefer .ts, fall back to .js)
# Find existing vite config (prefer .ts, fall back to .js)
vite_config_ts = frontend_dir / " vite.config.ts "
vite_config_ts = frontend_dir / " vite.config.ts "
@@ -1694,13 +1865,29 @@ def cmd_setup(args: argparse.Namespace) -> int:
old_content = pyproject_path . read_text ( " UTF-8 " )
old_content = pyproject_path . read_text ( " UTF-8 " )
data = tomlkit . parse ( old_content )
data = tomlkit . parse ( old_content )
updated = merge_pyproject ( data , PYPROJECT_ADDITIONS , module_name )
updated = merge_pyproject ( data , PYPROJECT_ADDITIONS , module_rel )
# Only add script entry if project doesn't already have scripts
# Ensure a script entry pointing at our __main__:main
if " scripts " not in updated [ " project " ] or not updated [ " project " ] [ " scripts " ] :
scripts = updated [ " project " ] . get ( " scripts " )
main_entry = f " { module_name } .__main__:main "
if not scripts :
updated [ " project " ] [ " scripts " ] = tomlkit . table ( )
updated [ " project " ] [ " scripts " ] = tomlkit . table ( )
script_name = module_name . replace ( " _ " , " - " )
updated [ " project " ] [ " scripts " ] [ module_name . replace ( " _ " , " - " ) ] = main_entry
updated [ " project " ] [ " scripts " ] [ script_name ] = f " { module_name } .__main__:main "
else :
has_our_entry = any (
isinstance ( entry , str ) and entry . startswith ( f " { module_name } . " )
for entry in scripts . values ( )
)
if not has_our_entry :
# Rewrite uv init boilerplate ("my-app = \"my_app:main\"") if present
boilerplate = next (
( name for name , entry in scripts . items ( ) if entry == f " { module_name } :main " ) ,
None ,
)
if boilerplate :
scripts [ boilerplate ] = main_entry
else :
scripts [ module_name . replace ( " _ " , " - " ) ] = main_entry
# Check if content actually changed
# Check if content actually changed
new_content = tomlkit . dumps ( updated )
new_content = tomlkit . dumps ( updated )
@@ -1715,8 +1902,16 @@ def cmd_setup(args: argparse.Namespace) -> int:
# === Update .gitignore ===
# === Update .gitignore ===
gitignore_path = project_dir / " .gitignore "
gitignore_path = project_dir / " .gitignore "
gitignore_entry = f " / { module_name } /frontend-build "
gitignore_entry = f " / { module_rel } /frontend-build "
if gitignore_path . exists ( ) :
if fresh_project :
# New project: opinionated .gitignore (ignore dotfiles and lockfiles)
content = f " { gitignore_entry } \n .* \n !.gitignore \n *.lock \n "
if dry :
print ( f " ✅ Would create opinionated .gitignore in { project_dir } " )
else :
gitignore_path . write_text ( content , " UTF-8 " , newline = " \n " )
print ( " ✅ Created .gitignore " )
elif gitignore_path . exists ( ) :
gitignore_content = gitignore_path . read_bytes ( )
gitignore_content = gitignore_path . read_bytes ( )
if b " frontend-build " in gitignore_content :
if b " frontend-build " in gitignore_content :
print ( " ✔️ .gitignore (frontend-build already ignored) " )
print ( " ✔️ .gitignore (frontend-build already ignored) " )