Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cbce326798 | ||
|
|
218c9e203b | ||
|
|
c9557d4308 |
+146
-26
@@ -1305,38 +1305,130 @@ def merge_pyproject(
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
MIN_NODE_VERSION = 20
|
||||||
|
|
||||||
|
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
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
|
||||||
|
|
||||||
|
js_runtime = js_runtime_env
|
||||||
|
js_path = Path(js_runtime)
|
||||||
|
runtime_name = js_path.name
|
||||||
|
|
||||||
|
# Map node to npm
|
||||||
|
if runtime_name == "node":
|
||||||
|
runtime_name = "npm"
|
||||||
|
js_runtime = str(js_path.parent / "npm") if js_path.parent.name else "npm"
|
||||||
|
|
||||||
|
for option in options:
|
||||||
|
if option != runtime_name and not runtime_name.startswith(option):
|
||||||
|
continue
|
||||||
|
|
||||||
|
tool = shutil.which(js_runtime)
|
||||||
|
if tool is None:
|
||||||
|
print(f"⚠️ JS_RUNTIME={js_runtime_env}: {option} not found")
|
||||||
|
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
|
||||||
|
|
||||||
|
print(f"⚠️ JS_RUNTIME={js_runtime_env} not recognized")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
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:
|
||||||
|
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
|
||||||
|
|
||||||
|
if node_version_error:
|
||||||
|
print(f"⚠️ {node_version_error}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def find_js_runtime() -> tuple[str, str] | None:
|
def find_js_runtime() -> tuple[str, str] | None:
|
||||||
"""Find a JavaScript runtime from JS_RUNTIME env or auto-detect.
|
"""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 (tool_path, tool_name) where tool_name is "npm", "deno", or "bun".
|
||||||
Returns None if no runtime is found.
|
Returns None if no runtime is found.
|
||||||
"""
|
"""
|
||||||
options = ["deno", "npm", "bun"]
|
options = ["npm", "deno", "bun"]
|
||||||
|
|
||||||
# Check for JS_RUNTIME environment variable
|
# Check for JS_RUNTIME environment variable
|
||||||
if js_runtime_env := os.environ.get("JS_RUNTIME"):
|
if result := _find_runtime_from_env(options):
|
||||||
js_runtime = js_runtime_env
|
return result
|
||||||
js_path = Path(js_runtime)
|
if os.environ.get("JS_RUNTIME"):
|
||||||
runtime_name = js_path.name
|
|
||||||
# Map node to npm
|
|
||||||
if runtime_name == "node":
|
|
||||||
runtime_name = "npm"
|
|
||||||
js_runtime = str(js_path.parent / "npm") if js_path.parent.name else "npm"
|
|
||||||
for option in options:
|
|
||||||
if option == runtime_name or runtime_name.startswith(option):
|
|
||||||
tool = shutil.which(js_runtime)
|
|
||||||
if tool is None:
|
|
||||||
print(f"⚠️ JS_RUNTIME={js_runtime_env} not found")
|
|
||||||
return None
|
|
||||||
return tool, option
|
|
||||||
print(f"⚠️ JS_RUNTIME={js_runtime_env} not recognized")
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Auto-detect
|
# Auto-detect
|
||||||
for option in options:
|
return _auto_detect_runtime(options)
|
||||||
if tool := shutil.which(option):
|
|
||||||
return tool, option
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def ensure_python_project(project_dir: Path, *, dry: bool = False) -> bool:
|
def ensure_python_project(project_dir: Path, *, dry: bool = False) -> bool:
|
||||||
@@ -1382,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
|
||||||
|
|
||||||
@@ -1402,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()
|
||||||
result = subprocess.run(create_cmd, cwd=project_dir, check=False) # noqa: S603
|
try:
|
||||||
if result.returncode != 0:
|
result = subprocess.run(create_cmd, cwd=project_dir, check=False) # noqa: S603
|
||||||
print("❌ create-vue failed")
|
except KeyboardInterrupt:
|
||||||
|
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
|
||||||
@@ -1439,17 +1554,22 @@ 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
|
||||||
|
|||||||
Reference in new issue
Block a user