11"""omnipkg CLI - Enhanced with runtime interpreter switching and language support"""
22import sys
33import argparse
4- import subprocess
54from pathlib import Path
6- import textwrap
75import os
6+ import subprocess # <-- FIX: Added this import
7+ import tempfile # <-- FIX: Added this import
8+ import json # <-- FIX: Added this import
9+
10+ # [REFACTORED] Import utilities from the common module
811from .i18n import _ , SUPPORTED_LANGUAGES
912from .core import omnipkg as OmnipkgCore
1013from .core import ConfigManager
14+ from .common_utils import print_header , run_script_in_omnipkg_env , UVFailureDetector
15+ from .commands .run import execute_run_command # <-- NEW IMPORT
16+
1117TESTS_DIR = Path (__file__ ).parent .parent / 'tests'
1218DEMO_DIR = Path (__file__ ).parent
1319
2026def get_actual_python_version ():
2127 """Get the actual Python version being used by omnipkg, not just sys.version_info."""
2228 try :
23- # Try to get it from omnipkg's config first
2429 cm = ConfigManager ()
2530 configured_exe = cm .config .get ('python_executable' )
2631 if configured_exe :
2732 version_tuple = cm ._verify_python_version (configured_exe )
2833 if version_tuple :
29- return version_tuple [:2 ] # Return (major, minor)
30-
31- # Fallback to sys.version_info if omnipkg config fails
34+ return version_tuple [:2 ]
3235 return sys .version_info [:2 ]
3336 except Exception :
34- # Last resort fallback
3537 return sys .version_info [:2 ]
3638
3739def handle_python_requirement (required_version_str : str , pkg_instance : OmnipkgCore , parser_prog : str ) -> bool :
3840 """
3941 Checks if the current Python version matches the requirement.
4042 If not, it attempts to automatically adopt and/or swap.
41- Returns True if the environment is now correctly configured for the demo, False otherwise.
4243 """
4344 actual_version_tuple = get_actual_python_version ()
4445 required_version_tuple = tuple (map (int , required_version_str .split ('.' )))
4546
4647 if actual_version_tuple == required_version_tuple :
47- return True # Correct version is already active
48+ return True
4849
49- # Display initial message
50- print ('=' * 60 )
50+ print_header (_ ('Python Version Requirement' ))
5151 print (_ (' ⚠️ This Demo Requires Python {}' ).format (required_version_str ))
52- print ('=' * 60 )
53- print (_ ('Current Python version: {}.{}' ).format (actual_version_tuple [0 ], actual_version_tuple [1 ]))
54- print (_ ('omnipkg will now attempt to automatically configure the correct interpreter.' ))
52+ print (_ (' - Current Python version: {}.{}' ).format (actual_version_tuple [0 ], actual_version_tuple [1 ]))
53+ print (_ (' - omnipkg will now attempt to automatically configure the correct interpreter.' ))
5554 print ('-' * 60 )
5655
57- # Check if the required version is already managed (adopted)
5856 managed_interpreters = pkg_instance .interpreter_manager .list_available_interpreters ()
5957 if required_version_str not in managed_interpreters :
60- # If not managed, check if it's discoverable on the system
6158 discovered_interpreters = pkg_instance .config_manager .list_available_pythons ()
6259 if required_version_str in discovered_interpreters :
6360 print (_ ('🐍 Python {} found on your system. Adopting it...' ).format (required_version_str ))
@@ -67,13 +64,11 @@ def handle_python_requirement(required_version_str: str, pkg_instance: OmnipkgCo
6764 return False
6865 print (_ ('✅ Successfully adopted Python {}.' ).format (required_version_str ))
6966 else :
70- # If not discoverable, fail
7167 print (_ ('❌ Required Python version {} not found on your system.' ).format (required_version_str ))
7268 print (_ (' Please install Python {} and ensure it is in your PATH.' ).format (required_version_str ))
7369 return False
7470
75- # At this point, the version is guaranteed to be managed. Now, swap to it.
76- print (_ ('🔄 Swapping active interpreter to Python {} for the demo...' ).format (required_version_str ))
71+ print (_ ('🔄 Swapping active interpreter to Python {}...' ).format (required_version_str ))
7772 if pkg_instance .switch_active_python (required_version_str ) != 0 :
7873 print (_ ('❌ Failed to swap to Python {}. Please try manually.' ).format (required_version_str ))
7974 print (_ (" Run: {} swap python {}" ).format (parser_prog , required_version_str ))
@@ -90,25 +85,16 @@ def get_version():
9085 from importlib .metadata import version
9186 return version ('omnipkg' )
9287 except Exception :
88+ # Fallback for development environments
9389 try :
9490 import tomllib
9591 toml_path = Path (__file__ ).parent .parent / 'pyproject.toml'
9692 if toml_path .exists ():
9793 with open (toml_path , 'rb' ) as f :
9894 data = tomllib .load (f )
9995 return data .get ('project' , {}).get ('version' , 'unknown' )
100- except ImportError :
101- try :
102- import tomli
103- toml_path = Path (__file__ ).parent .parent / 'pyproject.toml'
104- if toml_path .exists ():
105- with open (toml_path , 'rb' ) as f :
106- data = tomli .load (f )
107- return data .get ('project' , {}).get ('version' , 'unknown' )
108- except ImportError :
109- pass
110- except Exception :
111- pass
96+ except (ImportError , Exception ):
97+ pass # Ignore errors if tomllib/tomli is not available or file is malformed
11298 return 'unknown'
11399VERSION = get_version ()
114100
@@ -219,54 +205,6 @@ def run_demo_with_live_streaming(test_file, demo_name):
219205 traceback .print_exc ()
220206 return 1
221207
222- def run_demo_with_fallback_streaming (test_file , demo_name ):
223- """Fallback method with manual streaming if direct doesn't work."""
224- print (_ ('🚀 Running {} test from {}...' ).format (demo_name .capitalize (), test_file ))
225- print (_ ('📡 Streaming output in real-time...' ))
226- print (_ ('💡 Heavy package installations may have natural pauses - this is normal!' ))
227- print (_ ('🛑 Press Ctrl+C to safely cancel' ))
228- print ('-' * 60 )
229- try :
230- cm = ConfigManager ()
231- current_lang = cm .config .get ('language' , 'en' )
232- env = os .environ .copy ()
233- env ['OMNIPKG_LANG' ] = current_lang
234- env ['LANG' ] = f'{ current_lang } .UTF-8'
235- env ['LANGUAGE' ] = current_lang
236- env ['PYTHONUNBUFFERED' ] = '1'
237- process = subprocess .Popen ([sys .executable , str (test_file )], stdout = subprocess .PIPE , stderr = subprocess .STDOUT , bufsize = 0 , text = True , env = env )
238- while True :
239- char = process .stdout .read (1 )
240- if char == '' and process .poll () is not None :
241- break
242- if char :
243- print (char , end = '' , flush = True )
244- returncode = process .wait ()
245- print ('\n ' + '-' * 60 )
246- if returncode == 0 :
247- if demo_name == 'tensorflow' :
248- print (_ ('😎 TensorFlow escaped the matrix! 🚀' ))
249- print (_ ('🎉 Demo completed successfully!' ))
250- print (_ ("💡 Run 'omnipkg demo' to try another test." ))
251- else :
252- print (_ ('❌ Demo failed with return code {}' ).format (returncode ))
253- return returncode
254- except KeyboardInterrupt :
255- print (_ ('\n ⚠️ Demo cancelled by user (Ctrl+C)' ))
256- print (_ ('🛡️ Cleaning up safely...' ))
257- try :
258- process .terminate ()
259- process .wait (timeout = 5 )
260- except :
261- try :
262- process .kill ()
263- except :
264- pass
265- return 130
266- except Exception as e :
267- print (_ ('\n ❌ Demo failed with error: {}' ).format (e ))
268- return 1
269-
270208def create_8pkg_parser ():
271209 """Creates parser for the 8pkg alias (same as omnipkg but with different prog name)."""
272210 parser = create_parser ()
@@ -305,10 +243,14 @@ def create_parser():
305243 list_parser .add_argument ('filter' , nargs = '?' , help = _ ('Filter packages by name pattern' ))
306244 python_parser = subparsers .add_parser ('python' , help = _ ('Manage Python interpreters for the environment' ))
307245 python_subparsers = python_parser .add_subparsers (dest = 'python_command' , help = _ ('Available subcommands:' ), required = True )
308- python_adopt_parser = python_subparsers .add_parser ('adopt' , help = _ ('Copy a system Python into this environment' ))
309- python_adopt_parser .add_argument ('version' , help = _ ('The discovered version to adopt (e.g., "3.10 ")' ))
246+ python_adopt_parser = python_subparsers .add_parser ('adopt' , help = _ ('Copy or download a Python version into the environment' ))
247+ python_adopt_parser .add_argument ('version' , help = _ ('The version to adopt (e.g., "3.9 ")' ))
310248 python_switch_parser = python_subparsers .add_parser ('switch' , help = _ ('Switch the active Python interpreter for this environment' ))
311- python_switch_parser .add_argument ('version' , help = _ ('The version to switch to (e.g., "3.10", "3.11")' ))
249+ python_switch_parser .add_argument ('version' , help = _ ('The version to switch to (e.g., "3.10")' ))
250+ python_rescan_parser = python_subparsers .add_parser ('rescan' , help = _ ('Force a re-scan and repair of the interpreter registry' ))
251+ remove_parser = python_subparsers .add_parser ('remove' , help = 'Forcefully remove a managed Python interpreter.' )
252+ remove_parser .add_argument ('version' , help = 'The version of the managed Python interpreter to remove (e.g., "3.9").' )
253+ remove_parser .add_argument ('-y' , '--yes' , action = 'store_true' , help = 'Do not ask for confirmation.' )
312254 status_parser = subparsers .add_parser ('status' , help = _ ('Environment health dashboard' ))
313255 demo_parser = subparsers .add_parser ('demo' , help = _ ('Interactive demo for version switching' ))
314256 stress_parser = subparsers .add_parser ('stress-test' , help = _ ('Ultimate demonstration with heavy packages' ))
@@ -320,9 +262,14 @@ def create_parser():
320262 reset_config_parser .add_argument ('--yes' , '-y' , dest = 'force' , action = 'store_true' , help = _ ('Skip confirmation' ))
321263 config_parser = subparsers .add_parser ('config' , help = _ ('View or edit omnipkg configuration' ))
322264 config_subparsers = config_parser .add_subparsers (dest = 'config_command' , required = True )
265+ config_view_parser = config_subparsers .add_parser ('view' , help = _ ('Display the current configuration for this environment' ))
323266 config_set_parser = config_subparsers .add_parser ('set' , help = _ ('Set a configuration value' ))
324267 config_set_parser .add_argument ('key' , choices = ['language' , 'install_strategy' ], help = _ ('Configuration key to set' ))
325- config_set_parser .add_argument ('value' , help = _ ('Value to set (e.g., en, es, de, ja, zh_CN)' ))
268+ config_set_parser .add_argument ('value' , help = _ ('Value to set for the key' ))
269+ config_reset_parser = config_subparsers .add_parser ('reset' , help = _ ('Reset a specific configuration key to its default' ))
270+ config_reset_parser .add_argument ('key' , choices = ['interpreters' ], help = _ ('Configuration key to reset (e.g., interpreters)' ))
271+ run_parser = subparsers .add_parser ('run' , help = _ ('Run a script with auto-healing for version conflicts' ))
272+ run_parser .add_argument ('script_and_args' , nargs = argparse .REMAINDER , help = _ ('The script to run, followed by its arguments' ))
326273 prune_parser = subparsers .add_parser ('prune' , help = _ ('Clean up old, bubbled package versions' ))
327274 prune_parser .add_argument ('package' , help = _ ('Package whose bubbles to prune' ))
328275 prune_parser .add_argument ('--keep-latest' , type = int , metavar = 'N' , help = _ ('Keep N most recent bubbled versions' ))
@@ -359,8 +306,14 @@ def main():
359306 parser .print_help ()
360307 print (_ ('\n 👋 Welcome back to omnipkg! Run a command or see --help for details.' ))
361308 return 0
309+ # [FIXED] Corrected logic for the 'config' command
362310 if args .command == 'config' :
363- if args .config_command == 'set' :
311+ if args .config_command == 'view' :
312+ print_header ("omnipkg Configuration" )
313+ for key , value in sorted (cm .config .items ()):
314+ print (f" - { key } : { value } " )
315+ return 0
316+ elif args .config_command == 'set' :
364317 if args .key == 'language' :
365318 if args .value not in SUPPORTED_LANGUAGES :
366319 print (_ ("❌ Error: Language '{}' not supported. Supported: {}" ).format (args .value , ', ' .join (SUPPORTED_LANGUAGES .keys ())))
@@ -370,16 +323,26 @@ def main():
370323 lang_name = SUPPORTED_LANGUAGES .get (args .value , args .value )
371324 print (_ ('✅ Language permanently set to: {lang}' ).format (lang = lang_name ))
372325 elif args .key == 'install_strategy' :
373- valid_strategies = ['stable-main' , 'latest-active' , 'fast-compat' ]
326+ valid_strategies = ['stable-main' , 'latest-active' ]
374327 if args .value not in valid_strategies :
375328 print (_ ('❌ Error: Invalid install strategy. Must be one of: {}' ).format (', ' .join (valid_strategies )))
376329 return 1
377330 cm .set ('install_strategy' , args .value )
378331 print (_ ('✅ Install strategy permanently set to: {}' ).format (args .value ))
379332 else :
333+ # This handles any key that isn't 'language' or 'install_strategy'
380334 parser .print_help ()
381335 return 1
382- return 0
336+ return 0 # Success for the 'set' command
337+ elif args .config_command == 'reset' :
338+ if args .key == 'interpreters' :
339+ print (_ ("Resetting managed interpreters registry..." ))
340+ return pkg_instance .rescan_interpreters ()
341+ return 0 # Success for the 'reset' command
342+
343+ # Fallback for any other config subcommand
344+ parser .print_help ()
345+ return 1
383346 elif args .command == 'list' :
384347 if args .filter and args .filter .lower () == 'python' :
385348 interpreters = pkg_instance .interpreter_manager .list_available_interpreters ()
@@ -401,6 +364,16 @@ def main():
401364 elif args .command == 'python' :
402365 if args .python_command == 'adopt' :
403366 return pkg_instance .adopt_interpreter (args .version )
367+ elif args .python_command == 'rescan' :
368+ return pkg_instance .rescan_interpreters ()
369+ # [NOTE] You may also need a 'switch' handler here if it's not already present
370+ elif args .python_command == 'remove' : # <--- CORRECTED LINE
371+ return pkg_instance .remove_interpreter (args .version , force = args .yes )
372+ elif args .python_command == 'switch' :
373+ return pkg_instance .switch_active_python (args .version )
374+ else :
375+ parser .print_help ()
376+ return 1
404377 elif args .command == 'swap' :
405378 if not args .target :
406379 print (_ ('❌ Error: You must specify what to swap.' ))
@@ -437,7 +410,6 @@ def main():
437410 elif args .command == 'status' :
438411 return pkg_instance .show_multiversion_status ()
439412 elif args .command == 'demo' :
440- print_header (_ ('Interactive Omnipkg Demo' ))
441413 actual_version = get_actual_python_version ()
442414 print (_ ('Current Python version: {}.{}' ).format (actual_version [0 ], actual_version [1 ]))
443415 print (_ ('🎪 Omnipkg supports version switching for:' ))
@@ -550,6 +522,8 @@ def main():
550522 return 0
551523 elif args .command == 'reset-config' :
552524 return pkg_instance .reset_configuration (force = args .force )
525+ elif args .command == 'run' :
526+ return execute_run_command (args .script_and_args , cm )
553527 else :
554528 parser .print_help ()
555529 print (_ ("\n 💡 Did you mean 'omnipkg config set language <code>'?" ))
0 commit comments