@@ -351,8 +351,9 @@ def _add_file(file_path):
351351 # Write the version file as structured YAML
352352 version_file_path = os .path .join (temp_dir , ".penguin_packaged_version" )
353353 package_metadata = {
354- "format_version" : 1 ,
355- "penguin_version" : VERSION
354+ "format_version" : 2 ,
355+ "penguin_version" : VERSION ,
356+ "base_name" : base_name
356357 }
357358 with open (version_file_path , "w" ) as f :
358359 yaml .dump (package_metadata , f , default_flow_style = False , sort_keys = False )
@@ -914,41 +915,10 @@ def unpackage(ctx, archive, output, force):
914915 else :
915916 output = os .path .abspath (output )
916917
917- # Check if extraction would create a directory that already exists
918- # First peek at what's in the archive to see if it has a top-level directory
919- try :
920- result = subprocess .run (
921- ["tar" , "-tzf" , archive_path ],
922- capture_output = True ,
923- text = True ,
924- check = True
925- )
926- first_entry = result .stdout .split ('\n ' )[0 ]
927- # If first entry is a directory (ends with /), that's our top-level
928- if first_entry .endswith ('/' ):
929- top_level_dir = first_entry .rstrip ('/' )
930- target_dir = os .path .join (output , top_level_dir )
931- else :
932- # No top-level directory in archive, will extract directly
933- target_dir = output
934- except subprocess .CalledProcessError as e :
935- logger .error (f"Failed to read archive: { e } " )
936- exit (1 )
937-
938- if os .path .exists (target_dir ):
939- if os .listdir (target_dir ): # Directory exists and is not empty
940- if force :
941- logger .info (f"Deleting existing directory: { target_dir } " )
942- shutil .rmtree (target_dir , ignore_errors = True )
943- else :
944- raise ValueError (
945- f"Output directory exists and is not empty: { target_dir } . Use --force to delete."
946- )
947-
948918 # Ensure output directory exists
949919 os .makedirs (output , exist_ok = True )
950920
951- # Verify the archive contains the version file
921+ # Verify the archive contains the version file and extract metadata
952922 try :
953923 result = subprocess .run (
954924 ["tar" , "-tzf" , archive_path , ".penguin_packaged_version" ],
@@ -958,18 +928,56 @@ def unpackage(ctx, archive, output, force):
958928 )
959929 if result .returncode != 0 :
960930 raise ValueError (
961- f "Archive is not a valid penguin package: missing .penguin_packaged_version file. "
962- f "This archive was not created with 'penguin package'."
931+ "Archive is not a valid penguin package: missing .penguin_packaged_version file. "
932+ "This archive was not created with 'penguin package'."
963933 )
964934 except subprocess .CalledProcessError as e :
965935 logger .error (f"Failed to verify archive: { e } " )
966936 exit (1 )
967937
968- logger .info (f"Extracting { archive } to { output } ..." )
938+ # Extract metadata to determine the base_name
939+ with tempfile .TemporaryDirectory () as temp_extract_dir :
940+ try :
941+ subprocess .run (
942+ ["tar" , "-I" , "pigz" , "-xf" , archive_path , "-C" , temp_extract_dir , ".penguin_packaged_version" ],
943+ check = True
944+ )
945+ metadata_path = os .path .join (temp_extract_dir , ".penguin_packaged_version" )
946+ with open (metadata_path , "r" ) as f :
947+ metadata = yaml .safe_load (f ) or {}
948+
949+ base_name = metadata .get ("base_name" )
950+ if not base_name :
951+ # Fall back to archive filename without .tar.gz
952+ base_name = os .path .basename (archive_path )
953+ if base_name .endswith (".tar.gz" ):
954+ base_name = base_name [:- 7 ]
955+ logger .warning (f"No base_name in metadata, using archive filename: { base_name } " )
956+ except (subprocess .CalledProcessError , yaml .YAMLError ) as e :
957+ logger .error (f"Failed to extract metadata: { e } " )
958+ exit (1 )
959+
960+ # Set target directory based on metadata
961+ target_dir = os .path .join (output , base_name )
962+
963+ # Check if target directory exists
964+ if os .path .exists (target_dir ):
965+ if force :
966+ logger .info (f"Deleting existing directory: { target_dir } " )
967+ shutil .rmtree (target_dir , ignore_errors = True )
968+ else :
969+ raise ValueError (
970+ f"Output directory already exists: { target_dir } . Use --force to delete."
971+ )
972+
973+ logger .info (f"Extracting { archive } to { target_dir } ..." )
974+
975+ # Create the target directory
976+ os .makedirs (target_dir , exist_ok = True )
969977
970978 try :
971979 subprocess .run (
972- ["tar" , "-I" , "pigz" , "-xf" , archive_path , "-C" , output ],
980+ ["tar" , "-I" , "pigz" , "-xf" , archive_path , "-C" , target_dir ],
973981 check = True
974982 )
975983 except subprocess .CalledProcessError as e :
@@ -979,12 +987,11 @@ def unpackage(ctx, archive, output, force):
979987 logger .info (f"Successfully extracted to { target_dir } " )
980988
981989 # Verify it's a valid penguin project
982- if os .path .isdir (target_dir ):
983- config_path = os .path .join (target_dir , "config.yaml" )
984- if not os .path .exists (config_path ):
985- logger .warning (f"Extracted directory does not contain config.yaml" )
986- else :
987- logger .info (f"Project ready at { target_dir } " )
990+ config_path = os .path .join (target_dir , "config.yaml" )
991+ if not os .path .exists (config_path ):
992+ logger .warning ("Extracted directory does not contain config.yaml" )
993+ else :
994+ logger .info (f"Project ready at { target_dir } " )
988995
989996
990997@cli .command (hidden = True )
@@ -996,6 +1003,7 @@ def export(ctx, project_dir, out):
9961003 """Alias for package"""
9971004 ctx .invoke (package , project_dir = project_dir , out = out )
9981005
1006+
9991007@cli .command (name = "import" , hidden = True )
10001008@click .argument ("archive" , type = click .Path (exists = True ))
10011009@click .option ("-o" , "--output" , type = str , default = "./projects" )
0 commit comments