Skip to content

Commit fd8eb49

Browse files
committed
feat: Introduce 'run' command and Multiverse Workflows
This major feature release introduces the '8pkg run' command for executing scripts with automatic, in-process healing of version conflicts via bubbles. Adds the ability for workflows to self-provision required Python interpreters on the fly. Implements major performance optimizations to knowledge base syncing, making repeated operations nearly instantaneous. All features are proven to work under heavy system load.
1 parent 88fa0c4 commit fd8eb49

12 files changed

Lines changed: 1711 additions & 409 deletions

CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,20 @@
11
# Changelog
22

3+
## [1.3.0] - 2025-09-06
4+
5+
### Added
6+
- **`omnipkg run` Command:** A powerful new way to execute scripts. Features automatic detection of runtime `AssertionError`s for package versions and "auto-heals" the script by re-running it inside a temporary bubble.
7+
- **Automatic Python Provisioning:** Scripts can now ensure required Python interpreters are available, with `omnipkg` automatically running `python adopt` if a version is missing.
8+
- **Performance Timers:** The `multiverse_analysis` test script now instruments and reports on the speed of dimension swaps and package preparation.
9+
10+
### Changed
11+
- **Major Performance Boost:** The knowledge base sync and package satisfaction checks are now dramatically faster, using single subprocess calls to validate the entire environment, reducing checks from many seconds to milliseconds.
12+
- **Quieter Logging:** The bubble creation process is now significantly less verbose during large, multi-dependency installations, providing clean, high-level summaries instead.
13+
- **CLI Refactoring:** Command logic for `run` has been moved to the new `omnipkg/commands/` directory for better structure.
14+
15+
### Fixed
16+
- **Critical Context Bug:** The knowledge base is now always updated by the correct Python interpreter context, especially after a `swap` or during scripted installs, ensuring data for different Python versions is stored correctly.
17+
318
## v.1.2.1
419

520
omnipkg v1.2.1: The Phoenix Release — True Multi-Interpreter Freedom

omnipkg/cli.py

Lines changed: 61 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
11
"""omnipkg CLI - Enhanced with runtime interpreter switching and language support"""
22
import sys
33
import argparse
4-
import subprocess
54
from pathlib import Path
6-
import textwrap
75
import 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
811
from .i18n import _, SUPPORTED_LANGUAGES
912
from .core import omnipkg as OmnipkgCore
1013
from .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+
1117
TESTS_DIR = Path(__file__).parent.parent / 'tests'
1218
DEMO_DIR = Path(__file__).parent
1319

@@ -20,44 +26,35 @@
2026
def 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

3739
def 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'
11399
VERSION = 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-
270208
def 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>'?"))

omnipkg/commands/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)