This commit is contained in:
Iliyan Angelov
2025-12-01 06:50:10 +02:00
parent 91f51bc6fe
commit 62c1fe5951
4682 changed files with 544807 additions and 31208 deletions

View File

@@ -0,0 +1,177 @@
import logging
from pathlib import Path
from typing import Optional
from safety.codebase.render import render_initialization_result
from safety.errors import SafetyError
from safety.events.utils.emission import (
emit_codebase_detection_status,
emit_codebase_setup_completed,
)
from safety.init.main import launch_auth_if_needed
from safety.tool.main import find_local_tool_files
from safety.util import clean_project_id
from typing_extensions import Annotated
import typer
from safety.cli_util import SafetyCLISubGroup, SafetyCLICommand
from .constants import (
CMD_CODEBASE_INIT_NAME,
CMD_HELP_CODEBASE_INIT,
CMD_HELP_CODEBASE,
CMD_CODEBASE_GROUP_NAME,
CMD_HELP_CODEBASE_INIT_DISABLE_FIREWALL,
CMD_HELP_CODEBASE_INIT_LINK_TO,
CMD_HELP_CODEBASE_INIT_NAME,
CMD_HELP_CODEBASE_INIT_PATH,
)
from ..cli_util import CommandType, get_command_for
from ..error_handlers import handle_cmd_exception
from ..decorators import notify
from ..constants import CONTEXT_COMMAND_TYPE, DEFAULT_EPILOG
from safety.console import main_console as console
from .main import initialize_codebase, prepare_unverified_codebase
logger = logging.getLogger(__name__)
cli_apps_opts = {
"rich_markup_mode": "rich",
"cls": SafetyCLISubGroup,
"name": CMD_CODEBASE_GROUP_NAME,
}
codebase_app = typer.Typer(**cli_apps_opts)
DEFAULT_CMD = CMD_CODEBASE_INIT_NAME
@codebase_app.callback(
invoke_without_command=True,
cls=SafetyCLISubGroup,
help=CMD_HELP_CODEBASE,
epilog=DEFAULT_EPILOG,
context_settings={
"allow_extra_args": True,
"ignore_unknown_options": True,
CONTEXT_COMMAND_TYPE: CommandType.BETA,
},
)
def codebase(
ctx: typer.Context,
):
"""
Group command for Safety Codebase (project) operations. Running this command will forward to the default command.
"""
logger.info("codebase started")
# If no subcommand is invoked, forward to the default command
if not ctx.invoked_subcommand:
default_command = get_command_for(name=DEFAULT_CMD, typer_instance=codebase_app)
return ctx.forward(default_command)
@codebase_app.command(
cls=SafetyCLICommand,
help=CMD_HELP_CODEBASE_INIT,
name=CMD_CODEBASE_INIT_NAME,
epilog=DEFAULT_EPILOG,
options_metavar="[OPTIONS]",
context_settings={"allow_extra_args": True, "ignore_unknown_options": True},
)
@handle_cmd_exception
@notify
def init(
ctx: typer.Context,
name: Annotated[
Optional[str],
typer.Option(
help=CMD_HELP_CODEBASE_INIT_NAME,
callback=lambda name: clean_project_id(name) if name else None,
),
] = None,
link_to: Annotated[
Optional[str],
typer.Option(
"--link-to",
help=CMD_HELP_CODEBASE_INIT_LINK_TO,
callback=lambda name: clean_project_id(name) if name else None,
),
] = None,
skip_firewall_setup: Annotated[
bool, typer.Option(help=CMD_HELP_CODEBASE_INIT_DISABLE_FIREWALL)
] = False,
codebase_path: Annotated[
Path,
typer.Option(
"--path",
exists=True,
file_okay=False,
dir_okay=True,
writable=True,
readable=True,
resolve_path=True,
show_default=False,
help=CMD_HELP_CODEBASE_INIT_PATH,
),
] = Path("."),
):
"""
Initialize a Safety Codebase. The codebase may be entirely new to Safety Platform,
or may already exist in Safety Platform and the user is wanting to initialize it locally.
"""
logger.info("codebase init started")
if link_to and name:
raise typer.BadParameter("--link-to and --name cannot be used together")
org_slug = launch_auth_if_needed(ctx, console)
if not org_slug:
raise SafetyError(
"Organization not found, please run 'safety auth status' or 'safety auth login'"
)
should_enable_firewall = not skip_firewall_setup and ctx.obj.firewall_enabled
unverified_codebase = prepare_unverified_codebase(
codebase_path=codebase_path,
user_provided_name=name,
user_provided_link_to=link_to,
)
local_files = find_local_tool_files(codebase_path)
emit_codebase_detection_status(
event_bus=ctx.obj.event_bus,
ctx=ctx,
detected=any(local_files),
detected_files=local_files if local_files else None,
)
project_file_created, project_status = initialize_codebase(
ctx=ctx,
console=console,
codebase_path=codebase_path,
unverified_codebase=unverified_codebase,
org_slug=org_slug,
link_to=link_to,
should_enable_firewall=should_enable_firewall,
)
codebase_init_status = (
"reinitialized" if unverified_codebase.created else project_status
)
codebase_id = ctx.obj.project.id if ctx.obj.project and ctx.obj.project.id else None
render_initialization_result(
console=console,
codebase_init_status=codebase_init_status,
codebase_id=codebase_id,
)
emit_codebase_setup_completed(
event_bus=ctx.obj.event_bus,
ctx=ctx,
is_created=project_file_created,
codebase_id=codebase_id,
)

View File

@@ -0,0 +1,27 @@
CMD_HELP_CODEBASE_INIT = "Initialize a Safety Codebase (like git init for security). Sets up a new codebase or connects your local project to an existing one on Safety Platform."
CMD_HELP_CODEBASE = (
"[BETA] Manage your Safety Codebase integration.\nExample: safety codebase init"
)
CMD_CODEBASE_GROUP_NAME = "codebase"
CMD_CODEBASE_INIT_NAME = "init"
# init options help
CMD_HELP_CODEBASE_INIT_NAME = "Name of the codebase. Defaults to GIT origin name, parent directory name, or random string if parent directory is unnamed. The value will be normalized for use as an identifier."
CMD_HELP_CODEBASE_INIT_LINK_TO = (
"Link to an existing codebase using its codebase slug (found in Safety Platform)."
)
CMD_HELP_CODEBASE_INIT_DISABLE_FIREWALL = "Don't enable Firewall protection for this codebase (enabled by default when available in your organization)"
CMD_HELP_CODEBASE_INIT_PATH = (
"Path to the codebase directory. Defaults to current directory."
)
CODEBASE_INIT_REINITIALIZED = "Reinitialized existing codebase {codebase_name}"
CODEBASE_INIT_ALREADY_EXISTS = "A codebase already exists in this directory. Please delete .safety-project.ini and run `safety codebase init` again to initialize a new codebase."
CODEBASE_INIT_NOT_FOUND_LINK_TO = "\nError: codebase '{codebase_name}' specified with --link-to does not exist.\n\nTo create a new codebase instead, use one of:\n safety codebase init\n safety codebase init --name \"custom name\"\n\nTo link to an existing codebase, verify the codebase id and try again."
CODEBASE_INIT_NOT_FOUND_PROJECT_FILE = "\nError: codebase '{codebase_name}' specified with the current .safety-project.ini file does not exist.\n\nTo create a new codebase instead, delete the corrupted .safety-project.ini file and then use one of:\n safety codebase init\n safety codebase init --name \"custom name\"\n\nTo link to an existing codebase, verify the codebase id and try again."
CODEBASE_INIT_LINKED = "Linked to codebase {codebase_name}."
CODEBASE_INIT_CREATED = "Created new codebase {codebase_name}."
CODEBASE_INIT_ERROR = "Error: unable to initialize the codebase. Please try again."

View File

@@ -0,0 +1,115 @@
from typing import TYPE_CHECKING, Optional
from ..codebase_utils import load_unverified_project_from_config
from safety.errors import SafetyError, SafetyException
from pathlib import Path
from safety.codebase.constants import (
CODEBASE_INIT_ERROR,
CODEBASE_INIT_NOT_FOUND_LINK_TO,
CODEBASE_INIT_NOT_FOUND_PROJECT_FILE,
)
from safety.init.main import create_project
from typer import Context
from rich.console import Console
import sys
if TYPE_CHECKING:
from ..codebase_utils import UnverifiedProjectModel
def initialize_codebase(
ctx: Context,
console: Console,
codebase_path: Path,
unverified_codebase: "UnverifiedProjectModel",
org_slug: str,
link_to: Optional[str] = None,
should_enable_firewall: bool = False,
):
is_interactive = sys.stdin.isatty()
link_behavior = "prompt"
create_if_missing = True
is_codebase_file_created = unverified_codebase.created
if link_to or is_codebase_file_created:
link_behavior = "always"
create_if_missing = False
elif not is_interactive:
link_behavior = "never"
project_file_created, project_status = create_project(
ctx=ctx,
console=console,
target=codebase_path,
unverified_project=unverified_codebase,
create_if_missing=create_if_missing,
link_behavior=link_behavior,
)
if project_status == "not_found":
codebase_name = "Unknown"
msg = "Codebase not found."
if link_to:
msg = CODEBASE_INIT_NOT_FOUND_LINK_TO
codebase_name = link_to
elif is_codebase_file_created:
msg = CODEBASE_INIT_NOT_FOUND_PROJECT_FILE
codebase_name = unverified_codebase.id
raise SafetyError(msg.format(codebase_name=codebase_name))
elif project_status == "found" and not is_interactive:
# Non-TTY mode: Project exists but we can't link (link_behavior="never")
suggested_name = unverified_codebase.id
raise SafetyError(
f"Project '{suggested_name}' already exists. "
f"In non-interactive mode, use --link-to '{suggested_name}' to link to the existing project, "
f"or use --name with a different project name to create a new one."
)
if not ctx.obj.project:
raise SafetyException(CODEBASE_INIT_ERROR)
if should_enable_firewall:
from ..tool.main import configure_local_directory
configure_local_directory(codebase_path, org_slug, ctx.obj.project.id)
return project_file_created, project_status
def fail_if_codebase_name_mismatch(
provided_name: str,
unverified_codebase: "UnverifiedProjectModel",
) -> None:
"""
Useful to prevent the user from overwriting an existing codebase by mistyping the name.
"""
if unverified_codebase.id and provided_name != unverified_codebase.id:
from safety.codebase.constants import CODEBASE_INIT_ALREADY_EXISTS
raise SafetyError(CODEBASE_INIT_ALREADY_EXISTS)
def prepare_unverified_codebase(
codebase_path: Path,
user_provided_name: Optional[str] = None,
user_provided_link_to: Optional[str] = None,
) -> "UnverifiedProjectModel":
"""
Prepare the unverified codebase object based on the provided name and link to.
"""
unverified_codebase = load_unverified_project_from_config(
project_root=codebase_path
)
provided_name = user_provided_name or user_provided_link_to
if provided_name:
fail_if_codebase_name_mismatch(
provided_name=provided_name,
unverified_codebase=unverified_codebase,
)
unverified_codebase.id = provided_name
return unverified_codebase

View File

@@ -0,0 +1,34 @@
from typing import TYPE_CHECKING, Optional
if TYPE_CHECKING:
from rich.console import Console
def render_initialization_result(
console: "Console",
codebase_init_status: Optional[str] = None,
codebase_id: Optional[str] = None,
):
if not codebase_init_status or not codebase_id:
console.print("Error: unable to initialize codebase")
return
message = None
if codebase_init_status == "created":
from safety.codebase.constants import CODEBASE_INIT_CREATED
message = CODEBASE_INIT_CREATED
if codebase_init_status == "linked":
from safety.codebase.constants import CODEBASE_INIT_LINKED
message = CODEBASE_INIT_LINKED
if codebase_init_status == "reinitialized":
from safety.codebase.constants import CODEBASE_INIT_REINITIALIZED
message = CODEBASE_INIT_REINITIALIZED
if message:
console.print(message.format(codebase_name=codebase_id))