mirror of
https://github.com/github/spec-kit.git
synced 2026-02-02 14:03:36 +00:00
Update __init__.py
This commit is contained in:
@@ -146,10 +146,6 @@ AGENT_CONFIG = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
# Derived dictionaries for backward compatibility
|
|
||||||
AI_CHOICES = {key: config["name"] for key, config in AGENT_CONFIG.items()}
|
|
||||||
AGENT_FOLDER_MAP = {key: config["folder"] for key, config in AGENT_CONFIG.items()}
|
|
||||||
|
|
||||||
SCRIPT_TYPE_CHOICES = {"sh": "POSIX Shell (bash/zsh)", "ps": "PowerShell"}
|
SCRIPT_TYPE_CHOICES = {"sh": "POSIX Shell (bash/zsh)", "ps": "PowerShell"}
|
||||||
|
|
||||||
CLAUDE_LOCAL_PATH = Path.home() / ".claude" / "local" / "claude"
|
CLAUDE_LOCAL_PATH = Path.home() / ".claude" / "local" / "claude"
|
||||||
@@ -202,7 +198,7 @@ class StepTracker:
|
|||||||
s["detail"] = detail
|
s["detail"] = detail
|
||||||
self._maybe_refresh()
|
self._maybe_refresh()
|
||||||
return
|
return
|
||||||
# If not present, add it
|
|
||||||
self.steps.append({"key": key, "label": key, "status": status, "detail": detail})
|
self.steps.append({"key": key, "label": key, "status": status, "detail": detail})
|
||||||
self._maybe_refresh()
|
self._maybe_refresh()
|
||||||
|
|
||||||
@@ -219,7 +215,6 @@ class StepTracker:
|
|||||||
label = step["label"]
|
label = step["label"]
|
||||||
detail_text = step["detail"].strip() if step["detail"] else ""
|
detail_text = step["detail"].strip() if step["detail"] else ""
|
||||||
|
|
||||||
# Circles (unchanged styling)
|
|
||||||
status = step["status"]
|
status = step["status"]
|
||||||
if status == "done":
|
if status == "done":
|
||||||
symbol = "[green]●[/green]"
|
symbol = "[green]●[/green]"
|
||||||
@@ -343,7 +338,6 @@ def select_with_arrows(options: dict, prompt_text: str = "Select an option", def
|
|||||||
console.print("\n[red]Selection failed.[/red]")
|
console.print("\n[red]Selection failed.[/red]")
|
||||||
raise typer.Exit(1)
|
raise typer.Exit(1)
|
||||||
|
|
||||||
# Suppress explicit selection print; tracker / later logic will report consolidated status
|
|
||||||
return selected_key
|
return selected_key
|
||||||
|
|
||||||
console = Console()
|
console = Console()
|
||||||
@@ -367,7 +361,6 @@ app = typer.Typer(
|
|||||||
|
|
||||||
def show_banner():
|
def show_banner():
|
||||||
"""Display the ASCII art banner."""
|
"""Display the ASCII art banner."""
|
||||||
# Create gradient effect with different colors
|
|
||||||
banner_lines = BANNER.strip().split('\n')
|
banner_lines = BANNER.strip().split('\n')
|
||||||
colors = ["bright_blue", "blue", "cyan", "bright_cyan", "white", "bright_white"]
|
colors = ["bright_blue", "blue", "cyan", "bright_cyan", "white", "bright_white"]
|
||||||
|
|
||||||
@@ -383,8 +376,6 @@ def show_banner():
|
|||||||
@app.callback()
|
@app.callback()
|
||||||
def callback(ctx: typer.Context):
|
def callback(ctx: typer.Context):
|
||||||
"""Show banner when no subcommand is provided."""
|
"""Show banner when no subcommand is provided."""
|
||||||
# Show banner only when no subcommand and no help flag
|
|
||||||
# (help is handled by BannerGroup)
|
|
||||||
if ctx.invoked_subcommand is None and "--help" not in sys.argv and "-h" not in sys.argv:
|
if ctx.invoked_subcommand is None and "--help" not in sys.argv and "-h" not in sys.argv:
|
||||||
show_banner()
|
show_banner()
|
||||||
console.print(Align.center("[dim]Run 'specify --help' for usage information[/dim]"))
|
console.print(Align.center("[dim]Run 'specify --help' for usage information[/dim]"))
|
||||||
@@ -515,7 +506,6 @@ def download_template_from_github(ai_assistant: str, download_dir: Path, *, scri
|
|||||||
console.print(Panel(str(e), title="Fetch Error", border_style="red"))
|
console.print(Panel(str(e), title="Fetch Error", border_style="red"))
|
||||||
raise typer.Exit(1)
|
raise typer.Exit(1)
|
||||||
|
|
||||||
# Find the template asset for the specified AI assistant
|
|
||||||
assets = release_data.get("assets", [])
|
assets = release_data.get("assets", [])
|
||||||
pattern = f"spec-kit-template-{ai_assistant}-{script_type}"
|
pattern = f"spec-kit-template-{ai_assistant}-{script_type}"
|
||||||
matching_assets = [
|
matching_assets = [
|
||||||
@@ -600,7 +590,6 @@ def download_and_extract_template(project_path: Path, ai_assistant: str, script_
|
|||||||
"""
|
"""
|
||||||
current_dir = Path.cwd()
|
current_dir = Path.cwd()
|
||||||
|
|
||||||
# Step: fetch + download combined
|
|
||||||
if tracker:
|
if tracker:
|
||||||
tracker.start("fetch", "contacting GitHub API")
|
tracker.start("fetch", "contacting GitHub API")
|
||||||
try:
|
try:
|
||||||
@@ -633,12 +622,10 @@ def download_and_extract_template(project_path: Path, ai_assistant: str, script_
|
|||||||
console.print("Extracting template...")
|
console.print("Extracting template...")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Create project directory only if not using current directory
|
|
||||||
if not is_current_dir:
|
if not is_current_dir:
|
||||||
project_path.mkdir(parents=True)
|
project_path.mkdir(parents=True)
|
||||||
|
|
||||||
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
|
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
|
||||||
# List all files in the ZIP for debugging
|
|
||||||
zip_contents = zip_ref.namelist()
|
zip_contents = zip_ref.namelist()
|
||||||
if tracker:
|
if tracker:
|
||||||
tracker.start("zip-list")
|
tracker.start("zip-list")
|
||||||
@@ -646,13 +633,11 @@ def download_and_extract_template(project_path: Path, ai_assistant: str, script_
|
|||||||
elif verbose:
|
elif verbose:
|
||||||
console.print(f"[cyan]ZIP contains {len(zip_contents)} items[/cyan]")
|
console.print(f"[cyan]ZIP contains {len(zip_contents)} items[/cyan]")
|
||||||
|
|
||||||
# For current directory, extract to a temp location first
|
|
||||||
if is_current_dir:
|
if is_current_dir:
|
||||||
with tempfile.TemporaryDirectory() as temp_dir:
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
temp_path = Path(temp_dir)
|
temp_path = Path(temp_dir)
|
||||||
zip_ref.extractall(temp_path)
|
zip_ref.extractall(temp_path)
|
||||||
|
|
||||||
# Check what was extracted
|
|
||||||
extracted_items = list(temp_path.iterdir())
|
extracted_items = list(temp_path.iterdir())
|
||||||
if tracker:
|
if tracker:
|
||||||
tracker.start("extracted-summary")
|
tracker.start("extracted-summary")
|
||||||
@@ -660,7 +645,6 @@ def download_and_extract_template(project_path: Path, ai_assistant: str, script_
|
|||||||
elif verbose:
|
elif verbose:
|
||||||
console.print(f"[cyan]Extracted {len(extracted_items)} items to temp location[/cyan]")
|
console.print(f"[cyan]Extracted {len(extracted_items)} items to temp location[/cyan]")
|
||||||
|
|
||||||
# Handle GitHub-style ZIP with a single root directory
|
|
||||||
source_dir = temp_path
|
source_dir = temp_path
|
||||||
if len(extracted_items) == 1 and extracted_items[0].is_dir():
|
if len(extracted_items) == 1 and extracted_items[0].is_dir():
|
||||||
source_dir = extracted_items[0]
|
source_dir = extracted_items[0]
|
||||||
@@ -670,14 +654,12 @@ def download_and_extract_template(project_path: Path, ai_assistant: str, script_
|
|||||||
elif verbose:
|
elif verbose:
|
||||||
console.print(f"[cyan]Found nested directory structure[/cyan]")
|
console.print(f"[cyan]Found nested directory structure[/cyan]")
|
||||||
|
|
||||||
# Copy contents to current directory
|
|
||||||
for item in source_dir.iterdir():
|
for item in source_dir.iterdir():
|
||||||
dest_path = project_path / item.name
|
dest_path = project_path / item.name
|
||||||
if item.is_dir():
|
if item.is_dir():
|
||||||
if dest_path.exists():
|
if dest_path.exists():
|
||||||
if verbose and not tracker:
|
if verbose and not tracker:
|
||||||
console.print(f"[yellow]Merging directory:[/yellow] {item.name}")
|
console.print(f"[yellow]Merging directory:[/yellow] {item.name}")
|
||||||
# Recursively copy directory contents
|
|
||||||
for sub_item in item.rglob('*'):
|
for sub_item in item.rglob('*'):
|
||||||
if sub_item.is_file():
|
if sub_item.is_file():
|
||||||
rel_path = sub_item.relative_to(item)
|
rel_path = sub_item.relative_to(item)
|
||||||
@@ -693,10 +675,8 @@ def download_and_extract_template(project_path: Path, ai_assistant: str, script_
|
|||||||
if verbose and not tracker:
|
if verbose and not tracker:
|
||||||
console.print(f"[cyan]Template files merged into current directory[/cyan]")
|
console.print(f"[cyan]Template files merged into current directory[/cyan]")
|
||||||
else:
|
else:
|
||||||
# Extract directly to project directory (original behavior)
|
|
||||||
zip_ref.extractall(project_path)
|
zip_ref.extractall(project_path)
|
||||||
|
|
||||||
# Check what was extracted
|
|
||||||
extracted_items = list(project_path.iterdir())
|
extracted_items = list(project_path.iterdir())
|
||||||
if tracker:
|
if tracker:
|
||||||
tracker.start("extracted-summary")
|
tracker.start("extracted-summary")
|
||||||
@@ -706,16 +686,14 @@ def download_and_extract_template(project_path: Path, ai_assistant: str, script_
|
|||||||
for item in extracted_items:
|
for item in extracted_items:
|
||||||
console.print(f" - {item.name} ({'dir' if item.is_dir() else 'file'})")
|
console.print(f" - {item.name} ({'dir' if item.is_dir() else 'file'})")
|
||||||
|
|
||||||
# Handle GitHub-style ZIP with a single root directory
|
|
||||||
if len(extracted_items) == 1 and extracted_items[0].is_dir():
|
if len(extracted_items) == 1 and extracted_items[0].is_dir():
|
||||||
# Move contents up one level
|
|
||||||
nested_dir = extracted_items[0]
|
nested_dir = extracted_items[0]
|
||||||
temp_move_dir = project_path.parent / f"{project_path.name}_temp"
|
temp_move_dir = project_path.parent / f"{project_path.name}_temp"
|
||||||
# Move the nested directory contents to temp location
|
|
||||||
shutil.move(str(nested_dir), str(temp_move_dir))
|
shutil.move(str(nested_dir), str(temp_move_dir))
|
||||||
# Remove the now-empty project directory
|
|
||||||
project_path.rmdir()
|
project_path.rmdir()
|
||||||
# Rename temp directory to project directory
|
|
||||||
shutil.move(str(temp_move_dir), str(project_path))
|
shutil.move(str(temp_move_dir), str(project_path))
|
||||||
if tracker:
|
if tracker:
|
||||||
tracker.add("flatten", "Flatten nested directory")
|
tracker.add("flatten", "Flatten nested directory")
|
||||||
@@ -731,7 +709,7 @@ def download_and_extract_template(project_path: Path, ai_assistant: str, script_
|
|||||||
console.print(f"[red]Error extracting template:[/red] {e}")
|
console.print(f"[red]Error extracting template:[/red] {e}")
|
||||||
if debug:
|
if debug:
|
||||||
console.print(Panel(str(e), title="Extraction Error", border_style="red"))
|
console.print(Panel(str(e), title="Extraction Error", border_style="red"))
|
||||||
# Clean up project directory if created and not current directory
|
|
||||||
if not is_current_dir and project_path.exists():
|
if not is_current_dir and project_path.exists():
|
||||||
shutil.rmtree(project_path)
|
shutil.rmtree(project_path)
|
||||||
raise typer.Exit(1)
|
raise typer.Exit(1)
|
||||||
@@ -741,7 +719,7 @@ def download_and_extract_template(project_path: Path, ai_assistant: str, script_
|
|||||||
finally:
|
finally:
|
||||||
if tracker:
|
if tracker:
|
||||||
tracker.add("cleanup", "Remove temporary archive")
|
tracker.add("cleanup", "Remove temporary archive")
|
||||||
# Clean up downloaded ZIP file
|
|
||||||
if zip_path.exists():
|
if zip_path.exists():
|
||||||
zip_path.unlink()
|
zip_path.unlink()
|
||||||
if tracker:
|
if tracker:
|
||||||
@@ -836,7 +814,6 @@ def init(
|
|||||||
|
|
||||||
show_banner()
|
show_banner()
|
||||||
|
|
||||||
# Handle '.' as shorthand for current directory (equivalent to --here)
|
|
||||||
if project_name == ".":
|
if project_name == ".":
|
||||||
here = True
|
here = True
|
||||||
project_name = None # Clear project_name to use existing validation logic
|
project_name = None # Clear project_name to use existing validation logic
|
||||||
@@ -887,14 +864,11 @@ def init(
|
|||||||
f"{'Working Path':<15} [dim]{current_dir}[/dim]",
|
f"{'Working Path':<15} [dim]{current_dir}[/dim]",
|
||||||
]
|
]
|
||||||
|
|
||||||
# Add target path only if different from working dir
|
|
||||||
if not here:
|
if not here:
|
||||||
setup_lines.append(f"{'Target Path':<15} [dim]{project_path}[/dim]")
|
setup_lines.append(f"{'Target Path':<15} [dim]{project_path}[/dim]")
|
||||||
|
|
||||||
console.print(Panel("\n".join(setup_lines), border_style="cyan", padding=(1, 2)))
|
console.print(Panel("\n".join(setup_lines), border_style="cyan", padding=(1, 2)))
|
||||||
|
|
||||||
# Check git only if we might need it (not --no-git)
|
|
||||||
# Only set to True if the user wants it and the tool is available
|
|
||||||
should_init_git = False
|
should_init_git = False
|
||||||
if not no_git:
|
if not no_git:
|
||||||
should_init_git = check_tool("git", "https://git-scm.com/downloads")
|
should_init_git = check_tool("git", "https://git-scm.com/downloads")
|
||||||
@@ -902,32 +876,32 @@ def init(
|
|||||||
console.print("[yellow]Git not found - will skip repository initialization[/yellow]")
|
console.print("[yellow]Git not found - will skip repository initialization[/yellow]")
|
||||||
|
|
||||||
if ai_assistant:
|
if ai_assistant:
|
||||||
if ai_assistant not in AI_CHOICES:
|
if ai_assistant not in AGENT_CONFIG:
|
||||||
console.print(f"[red]Error:[/red] Invalid AI assistant '{ai_assistant}'. Choose from: {', '.join(AI_CHOICES.keys())}")
|
console.print(f"[red]Error:[/red] Invalid AI assistant '{ai_assistant}'. Choose from: {', '.join(AGENT_CONFIG.keys())}")
|
||||||
raise typer.Exit(1)
|
raise typer.Exit(1)
|
||||||
selected_ai = ai_assistant
|
selected_ai = ai_assistant
|
||||||
else:
|
else:
|
||||||
# Use arrow-key selection interface
|
# Create options dict for selection (agent_key: display_name)
|
||||||
|
ai_choices = {key: config["name"] for key, config in AGENT_CONFIG.items()}
|
||||||
selected_ai = select_with_arrows(
|
selected_ai = select_with_arrows(
|
||||||
AI_CHOICES,
|
ai_choices,
|
||||||
"Choose your AI assistant:",
|
"Choose your AI assistant:",
|
||||||
"copilot"
|
"copilot"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check agent tools unless ignored
|
|
||||||
if not ignore_agent_tools:
|
if not ignore_agent_tools:
|
||||||
agent_config = AGENT_CONFIG.get(selected_ai)
|
agent_config = AGENT_CONFIG.get(selected_ai)
|
||||||
if agent_config and agent_config["requires_cli"]:
|
if agent_config and agent_config["requires_cli"]:
|
||||||
cli_tool = selected_ai
|
cli_tool = selected_ai
|
||||||
if selected_ai == "cursor":
|
if selected_ai == "cursor":
|
||||||
cli_tool = "cursor-agent"
|
cli_tool = "cursor-agent"
|
||||||
|
|
||||||
install_url = agent_config["install_url"]
|
install_url = agent_config["install_url"]
|
||||||
if not check_tool(cli_tool, install_url):
|
if not check_tool(cli_tool, install_url):
|
||||||
error_panel = Panel(
|
error_panel = Panel(
|
||||||
f"[cyan]{selected_ai}[/cyan] not found\n"
|
f"[cyan]{selected_ai}[/cyan] not found\n"
|
||||||
f"Install from: [cyan]{install_url}[/cyan]\n"
|
f"Install from: [cyan]{install_url}[/cyan]\n"
|
||||||
f"{AI_CHOICES[selected_ai]} is required to continue with this project type.\n\n"
|
f"{agent_config['name']} is required to continue with this project type.\n\n"
|
||||||
"Tip: Use [cyan]--ignore-agent-tools[/cyan] to skip this check",
|
"Tip: Use [cyan]--ignore-agent-tools[/cyan] to skip this check",
|
||||||
title="[red]Agent Detection Error[/red]",
|
title="[red]Agent Detection Error[/red]",
|
||||||
border_style="red",
|
border_style="red",
|
||||||
@@ -937,16 +911,14 @@ def init(
|
|||||||
console.print(error_panel)
|
console.print(error_panel)
|
||||||
raise typer.Exit(1)
|
raise typer.Exit(1)
|
||||||
|
|
||||||
# Determine script type (explicit, interactive, or OS default)
|
|
||||||
if script_type:
|
if script_type:
|
||||||
if script_type not in SCRIPT_TYPE_CHOICES:
|
if script_type not in SCRIPT_TYPE_CHOICES:
|
||||||
console.print(f"[red]Error:[/red] Invalid script type '{script_type}'. Choose from: {', '.join(SCRIPT_TYPE_CHOICES.keys())}")
|
console.print(f"[red]Error:[/red] Invalid script type '{script_type}'. Choose from: {', '.join(SCRIPT_TYPE_CHOICES.keys())}")
|
||||||
raise typer.Exit(1)
|
raise typer.Exit(1)
|
||||||
selected_script = script_type
|
selected_script = script_type
|
||||||
else:
|
else:
|
||||||
# Auto-detect default
|
|
||||||
default_script = "ps" if os.name == "nt" else "sh"
|
default_script = "ps" if os.name == "nt" else "sh"
|
||||||
# Provide interactive selection similar to AI if stdin is a TTY
|
|
||||||
if sys.stdin.isatty():
|
if sys.stdin.isatty():
|
||||||
selected_script = select_with_arrows(SCRIPT_TYPE_CHOICES, "Choose script type (or press Enter)", default_script)
|
selected_script = select_with_arrows(SCRIPT_TYPE_CHOICES, "Choose script type (or press Enter)", default_script)
|
||||||
else:
|
else:
|
||||||
@@ -955,12 +927,10 @@ def init(
|
|||||||
console.print(f"[cyan]Selected AI assistant:[/cyan] {selected_ai}")
|
console.print(f"[cyan]Selected AI assistant:[/cyan] {selected_ai}")
|
||||||
console.print(f"[cyan]Selected script type:[/cyan] {selected_script}")
|
console.print(f"[cyan]Selected script type:[/cyan] {selected_script}")
|
||||||
|
|
||||||
# Download and set up project
|
|
||||||
# New tree-based progress (no emojis); include earlier substeps
|
|
||||||
tracker = StepTracker("Initialize Specify Project")
|
tracker = StepTracker("Initialize Specify Project")
|
||||||
# Flag to allow suppressing legacy headings
|
|
||||||
sys._specify_tracker_active = True
|
sys._specify_tracker_active = True
|
||||||
# Pre steps recorded as completed before live rendering
|
|
||||||
tracker.add("precheck", "Check required tools")
|
tracker.add("precheck", "Check required tools")
|
||||||
tracker.complete("precheck", "ok")
|
tracker.complete("precheck", "ok")
|
||||||
tracker.add("ai-select", "Select AI assistant")
|
tracker.add("ai-select", "Select AI assistant")
|
||||||
@@ -980,21 +950,17 @@ def init(
|
|||||||
]:
|
]:
|
||||||
tracker.add(key, label)
|
tracker.add(key, label)
|
||||||
|
|
||||||
# Use transient so live tree is replaced by the final static render (avoids duplicate output)
|
|
||||||
with Live(tracker.render(), console=console, refresh_per_second=8, transient=True) as live:
|
with Live(tracker.render(), console=console, refresh_per_second=8, transient=True) as live:
|
||||||
tracker.attach_refresh(lambda: live.update(tracker.render()))
|
tracker.attach_refresh(lambda: live.update(tracker.render()))
|
||||||
try:
|
try:
|
||||||
# Create a httpx client with verify based on skip_tls
|
|
||||||
verify = not skip_tls
|
verify = not skip_tls
|
||||||
local_ssl_context = ssl_context if verify else False
|
local_ssl_context = ssl_context if verify else False
|
||||||
local_client = httpx.Client(verify=local_ssl_context)
|
local_client = httpx.Client(verify=local_ssl_context)
|
||||||
|
|
||||||
download_and_extract_template(project_path, selected_ai, selected_script, here, verbose=False, tracker=tracker, client=local_client, debug=debug, github_token=github_token)
|
download_and_extract_template(project_path, selected_ai, selected_script, here, verbose=False, tracker=tracker, client=local_client, debug=debug, github_token=github_token)
|
||||||
|
|
||||||
# Ensure scripts are executable (POSIX)
|
|
||||||
ensure_executable_scripts(project_path, tracker=tracker)
|
ensure_executable_scripts(project_path, tracker=tracker)
|
||||||
|
|
||||||
# Git step
|
|
||||||
if not no_git:
|
if not no_git:
|
||||||
tracker.start("git")
|
tracker.start("git")
|
||||||
if is_git_repo(project_path):
|
if is_git_repo(project_path):
|
||||||
@@ -1026,16 +992,15 @@ def init(
|
|||||||
shutil.rmtree(project_path)
|
shutil.rmtree(project_path)
|
||||||
raise typer.Exit(1)
|
raise typer.Exit(1)
|
||||||
finally:
|
finally:
|
||||||
# Force final render
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Final static tree (ensures finished state visible after Live context ends)
|
|
||||||
console.print(tracker.render())
|
console.print(tracker.render())
|
||||||
console.print("\n[bold green]Project ready.[/bold green]")
|
console.print("\n[bold green]Project ready.[/bold green]")
|
||||||
|
|
||||||
# Agent folder security notice
|
# Agent folder security notice
|
||||||
if selected_ai in AGENT_FOLDER_MAP:
|
agent_config = AGENT_CONFIG.get(selected_ai)
|
||||||
agent_folder = AGENT_FOLDER_MAP[selected_ai]
|
if agent_config:
|
||||||
|
agent_folder = agent_config["folder"]
|
||||||
security_notice = Panel(
|
security_notice = Panel(
|
||||||
f"Some agents may store credentials, auth tokens, or other identifying and private artifacts in the agent folder within your project.\n"
|
f"Some agents may store credentials, auth tokens, or other identifying and private artifacts in the agent folder within your project.\n"
|
||||||
f"Consider adding [cyan]{agent_folder}[/cyan] (or parts of it) to [cyan].gitignore[/cyan] to prevent accidental credential leakage.",
|
f"Consider adding [cyan]{agent_folder}[/cyan] (or parts of it) to [cyan].gitignore[/cyan] to prevent accidental credential leakage.",
|
||||||
@@ -1046,7 +1011,6 @@ def init(
|
|||||||
console.print()
|
console.print()
|
||||||
console.print(security_notice)
|
console.print(security_notice)
|
||||||
|
|
||||||
# Boxed "Next steps" section
|
|
||||||
steps_lines = []
|
steps_lines = []
|
||||||
if not here:
|
if not here:
|
||||||
steps_lines.append(f"1. Go to the project folder: [cyan]cd {project_name}[/cyan]")
|
steps_lines.append(f"1. Go to the project folder: [cyan]cd {project_name}[/cyan]")
|
||||||
|
|||||||
Reference in New Issue
Block a user