7272
7373# veFaaS CLI config
7474.vefaas/
75+ vefaas.yaml
7576"""
7677
7778# Default Caddyfile name for static sites
7879DEFAULT_CADDYFILE_NAME = "DefaultCaddyFile"
7980
8081
82+ def generate_app_name_from_path (project_path : str ) -> str :
83+ """
84+ Generate application name from project path.
85+
86+ Rules:
87+ 1. Get project folder name
88+ 2. Convert to lowercase
89+ 3. Replace non-alphanumeric characters with hyphens
90+ 4. Remove consecutive hyphens
91+ 5. Remove leading/trailing hyphens
92+ 6. Truncate if too long
93+ 7. Add random suffix to avoid conflicts
94+
95+ Args:
96+ project_path: Absolute path to project
97+
98+ Returns:
99+ Processed app name, e.g., "my-project-abc123"
100+ """
101+ import re
102+ import random
103+ import string
104+
105+ # Get folder name
106+ folder_name = os .path .basename (os .path .normpath (project_path ))
107+
108+ # Convert to lowercase
109+ name = folder_name .lower ()
110+
111+ # Replace non-alphanumeric with hyphens
112+ name = re .sub (r'[^a-z0-9]' , '-' , name )
113+
114+ # Remove consecutive hyphens
115+ name = re .sub (r'-+' , '-' , name )
116+
117+ # Remove leading/trailing hyphens
118+ name = name .strip ('-' )
119+
120+ # Use default if empty
121+ if not name :
122+ name = "app"
123+
124+ # Truncate to reasonable length (reserve space for suffix)
125+ max_base_len = 20
126+ if len (name ) > max_base_len :
127+ name = name [:max_base_len ].rstrip ('-' )
128+
129+ # Add random suffix to avoid conflicts
130+ suffix = '' .join (random .choices (string .ascii_lowercase + string .digits , k = 6 ))
131+
132+ return f"{ name } -{ suffix } "
133+
134+
81135def read_gitignore_patterns (base_dir : str ) -> List [str ]:
82136 """Read .gitignore file patterns. Ported from vefaas-cli."""
83137 gitignore_path = os .path .join (base_dir , ".gitignore" )
@@ -117,7 +171,7 @@ def create_ignore_filter(
117171) -> pathspec .PathSpec :
118172 """Create a pathspec filter from gitignore/vefaasignore patterns. Ported from vefaas-cli."""
119173 all_patterns = gitignore_patterns + vefaasignore_patterns + (additional_patterns or [])
120- return pathspec .PathSpec .from_lines ("gitwildmatch " , all_patterns )
174+ return pathspec .PathSpec .from_lines ("gitignore " , all_patterns )
121175
122176
123177def render_default_caddyfile_content () -> str :
@@ -245,10 +299,12 @@ def _check_api_error(self, result: dict, action: str) -> None:
245299 # Check for common error patterns and provide clear guidance
246300 if "already exists" in message .lower () or "duplicate" in message .lower ():
247301 raise ValueError (
248- f"[{ action } ] Name already exists: { message } \n "
249- "To update an existing application, get the application_id from `.vefaas/config.json` or console, "
250- "then call deploy_application with application_id parameter. "
251- "Do NOT use function_id directly - always use application_id for updates."
302+ f"[{ action } ] NAME_CONFLICT: { message } \n "
303+ "**YOU MUST ASK THE USER** to choose one of the following options:\n "
304+ " 1. Update existing application: Get application_id from `.vefaas/config.json` or console, "
305+ "then call deploy_application with application_id parameter\n "
306+ " 2. Deploy as new application: Call deploy_application with a different name\n "
307+ "DO NOT automatically choose an option. Present both choices to the user and wait for their decision."
252308 )
253309 elif "not found" in message .lower ():
254310 raise ValueError (f"[{ action } ] Resource not found: { message } " )
@@ -382,6 +438,10 @@ def get_dependency_install_status(self, function_id: str) -> dict:
382438 """Get dependency installation task status"""
383439 return self .call ("GetDependencyInstallTaskStatus" , {"FunctionId" : function_id })
384440
441+ def create_dependency_install_task (self , function_id : str ) -> dict :
442+ """Create dependency installation task for Python projects"""
443+ return self .call ("CreateDependencyInstallTask" , {"FunctionId" : function_id })
444+
385445 # ========== Application Operations ==========
386446
387447 def get_application (self , app_id : str ) -> dict :
@@ -547,7 +607,7 @@ def wait_for_application_deploy(
547607 return {"success" : True , "access_url" : access_url }
548608
549609 if status .lower () in ("deploy_fail" , "deleted" , "delete_fail" ):
550- # Try to get detailed error from GetReleaseStatus (like vefaas-cli)
610+ # Try to get detailed error from GetReleaseStatus
551611 error_details = {}
552612 function_id = None
553613 try :
@@ -617,8 +677,10 @@ def wait_for_dependency_install(
617677 """Wait for Python dependency installation to complete."""
618678 start_time = time .time ()
619679 last_status = ""
680+ poll_count = 0
620681
621682 while time .time () - start_time < timeout_seconds :
683+ poll_count += 1
622684 try :
623685 result = client .get_dependency_install_status (function_id )
624686 status = result .get ("Result" , {}).get ("Status" , "" )
@@ -627,16 +689,29 @@ def wait_for_dependency_install(
627689 logger .info (f"[dependency] Installation status: { status } " )
628690 last_status = status
629691
692+ # Success status
630693 if status .lower () in ("succeeded" , "success" , "done" ):
631694 return {"success" : True , "status" : status }
632695
696+ # Failed status
633697 if status .lower () == "failed" :
634698 raise ValueError ("Dependency installation failed" )
635699
700+ # In-progress status (Dequeued = queued, InProgress = installing)
701+ if status .lower () in ("dequeued" , "inprogress" , "in_progress" , "pending" ):
702+ # Normal intermediate status, continue polling
703+ pass
704+ elif not status and poll_count > 3 :
705+ # Empty status may indicate no dependencies to install
706+ return {"success" : True , "status" : "no_dependency" }
707+
636708 except ValueError :
637709 raise
638710 except Exception as e :
639711 logger .warning (f"[dependency] Error checking status: { e } " )
712+ # Multiple failures may indicate no dependency install task
713+ if poll_count > 5 :
714+ return {"success" : True , "status" : "skipped" }
640715
641716 time .sleep (poll_interval_seconds )
642717
@@ -702,7 +777,30 @@ def package_directory(directory: str, base_dir: Optional[str] = None, include_gi
702777 continue
703778
704779 file_path = os .path .join (root , file )
705- zf .write (file_path , arcname )
780+
781+ # Use ZipInfo to preserve file permissions (especially executable)
782+ info = zipfile .ZipInfo (arcname )
783+
784+ # Get original file permissions
785+ file_stat = os .stat (file_path )
786+ original_mode = file_stat .st_mode & 0o777
787+
788+ # Grant execute permission (755) for script files
789+ script_extensions = ('.sh' , '.bash' , '.py' , '.pl' , '.rb' )
790+ if file .lower ().endswith (script_extensions ):
791+ # Ensure execute permission: original | 0o755
792+ final_mode = original_mode | 0o755
793+ else :
794+ final_mode = original_mode
795+
796+ # Unix permissions stored in high 16 bits of external_attr
797+ # Format: (permissions << 16) | (file_type << 28)
798+ # 0o100000 = regular file
799+ info .external_attr = (final_mode << 16 ) | (0o100000 << 16 )
800+
801+ # Read file content and write to zip
802+ with open (file_path , 'rb' ) as f :
803+ zf .writestr (info , f .read ())
706804
707805 buffer .seek (0 )
708806 zip_bytes = buffer .read ()
@@ -757,16 +855,21 @@ def log(msg: str):
757855 else :
758856 log (f"[config] Config region ({ config_region } ) differs from target region ({ client .region } ), will create new application" )
759857
858+ # Auto-generate app name from project path if not provided for new app
760859 if not config .name and not config .application_id :
761- raise ValueError ("Must provide name or application_id" )
860+ config .name = generate_app_name_from_path (config .project_path )
861+ log (f"[config] Auto-generated app name: { config .name } " )
762862
763863 # 0. Early check for duplicate application name
764864 if config .name and not config .application_id :
765865 existing_app_id = client .find_application_by_name (config .name )
766866 if existing_app_id :
767867 raise ValueError (
768- f"Application name '{ config .name } ' already exists (ID: { existing_app_id } ). "
769- f"To update this application, pass application_id='{ existing_app_id } ' parameter."
868+ f"NAME_CONFLICT: Application name '{ config .name } ' already exists (existing_application_id: { existing_app_id } ). "
869+ f"**YOU MUST ASK THE USER** to choose one of the following options:\n "
870+ f" 1. Update existing application: Call deploy_application with application_id='{ existing_app_id } '\n "
871+ f" 2. Deploy as new application: Call deploy_application with a different name\n "
872+ f"DO NOT automatically choose an option. Present both choices to the user and wait for their decision."
770873 )
771874
772875 # 0.5 Early check: if updating existing app and deployment is in progress, return early
@@ -963,8 +1066,13 @@ def log(msg: str):
9631066
9641067 # 6. Wait for dependency installation (Python)
9651068 if is_python :
966- log ("[6/7] Waiting for dependency installation ..." )
1069+ log ("[6/7] Installing dependencies ..." )
9671070 try :
1071+ # Trigger dependency install task
1072+ client .create_dependency_install_task (target_function_id )
1073+ log (" → Dependency installation task created" )
1074+
1075+ # Wait for installation to complete
9681076 wait_for_dependency_install (client , target_function_id )
9691077 log (" → Dependencies installed" )
9701078 except Exception as e :
0 commit comments