gitarsenal-cli 1.8.4 ā 1.8.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/activate_venv.sh +4 -0
- package/package.json +1 -1
- package/python/__pycache__/auth_manager.cpython-313.pyc +0 -0
- package/python/auth_manager.py +485 -0
- package/python/debug_delete.py +167 -0
- package/python/gitarsenal.py +188 -0
- package/python/gitarsenal_keys.py +43 -55
- package/python/test_modalSandboxScript.py +212 -2
- package/scripts/postinstall.js +96 -41
- package/test_modalSandboxScript.py +212 -2
package/scripts/postinstall.js
CHANGED
@@ -65,46 +65,95 @@ async function checkAndInstallUv() {
|
|
65
65
|
}
|
66
66
|
}
|
67
67
|
|
68
|
-
// Function to
|
69
|
-
async function
|
68
|
+
// Function to create and activate virtual environment
|
69
|
+
async function createVirtualEnvironment() {
|
70
|
+
const venvPath = path.join(__dirname, '..', 'venv');
|
70
71
|
const packages = ['modal', 'gitingest', 'requests'];
|
71
72
|
|
72
|
-
console.log(chalk.yellow(`š¦
|
73
|
+
console.log(chalk.yellow(`š¦ Creating virtual environment and installing packages: ${packages.join(', ')}`));
|
73
74
|
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
console.log(chalk.green('ā
Python packages installed successfully!'));
|
96
|
-
return true;
|
97
|
-
} catch (error) {
|
98
|
-
console.log(chalk.gray(`ā ļø ${installCommand} failed, trying next...`));
|
75
|
+
try {
|
76
|
+
// Create virtual environment directory
|
77
|
+
await fs.ensureDir(path.dirname(venvPath));
|
78
|
+
|
79
|
+
// Try different Python commands to create virtual environment
|
80
|
+
const pythonCommands = ['python3', 'python', 'py'];
|
81
|
+
let pythonCmd = null;
|
82
|
+
|
83
|
+
for (const cmd of pythonCommands) {
|
84
|
+
try {
|
85
|
+
await execAsync(`${cmd} --version`);
|
86
|
+
pythonCmd = cmd;
|
87
|
+
break;
|
88
|
+
} catch (error) {
|
89
|
+
// Continue to next command
|
90
|
+
}
|
91
|
+
}
|
92
|
+
|
93
|
+
if (!pythonCmd) {
|
94
|
+
console.log(chalk.red('ā No Python command found'));
|
95
|
+
return false;
|
99
96
|
}
|
97
|
+
|
98
|
+
console.log(chalk.gray(`š Creating virtual environment with ${pythonCmd}...`));
|
99
|
+
|
100
|
+
// Create virtual environment
|
101
|
+
await execAsync(`${pythonCmd} -m venv "${venvPath}"`, {
|
102
|
+
env: { ...process.env, PYTHONIOENCODING: 'utf-8' }
|
103
|
+
});
|
104
|
+
|
105
|
+
console.log(chalk.green('ā
Virtual environment created successfully!'));
|
106
|
+
|
107
|
+
// Determine the pip path based on OS
|
108
|
+
const isWindows = process.platform === 'win32';
|
109
|
+
const pipPath = isWindows ? path.join(venvPath, 'Scripts', 'pip.exe') : path.join(venvPath, 'bin', 'pip');
|
110
|
+
const pythonPath = isWindows ? path.join(venvPath, 'Scripts', 'python.exe') : path.join(venvPath, 'bin', 'python');
|
111
|
+
|
112
|
+
// Verify virtual environment was created properly
|
113
|
+
if (!(await fs.pathExists(pipPath))) {
|
114
|
+
console.log(chalk.red('ā Virtual environment pip not found'));
|
115
|
+
return false;
|
116
|
+
}
|
117
|
+
|
118
|
+
console.log(chalk.gray(`š Installing packages in virtual environment...`));
|
119
|
+
|
120
|
+
// Install packages in virtual environment
|
121
|
+
await execAsync(`"${pipPath}" install ${packages.join(' ')}`, {
|
122
|
+
env: { ...process.env, PYTHONIOENCODING: 'utf-8' }
|
123
|
+
});
|
124
|
+
|
125
|
+
console.log(chalk.green('ā
Python packages installed successfully in virtual environment!'));
|
126
|
+
|
127
|
+
// Create a script to activate the virtual environment
|
128
|
+
const activateScript = isWindows ?
|
129
|
+
`@echo off
|
130
|
+
cd /d "%~dp0"
|
131
|
+
call "${venvPath.replace(/\\/g, '\\\\')}\\Scripts\\activate.bat"
|
132
|
+
%*` :
|
133
|
+
`#!/bin/bash
|
134
|
+
cd "$(dirname "$0")"
|
135
|
+
source "${venvPath}/bin/activate"
|
136
|
+
exec "$@"`;
|
137
|
+
|
138
|
+
const activateScriptPath = path.join(__dirname, '..', 'activate_venv' + (isWindows ? '.bat' : '.sh'));
|
139
|
+
await fs.writeFile(activateScriptPath, activateScript);
|
140
|
+
|
141
|
+
if (!isWindows) {
|
142
|
+
await fs.chmod(activateScriptPath, 0o755);
|
143
|
+
}
|
144
|
+
|
145
|
+
console.log(chalk.green(`ā
Virtual environment activation script created: ${activateScriptPath}`));
|
146
|
+
|
147
|
+
return true;
|
148
|
+
} catch (error) {
|
149
|
+
console.log(chalk.red(`ā Error creating virtual environment: ${error.message}`));
|
150
|
+
console.log(chalk.yellow('š” Please run manually:'));
|
151
|
+
console.log(chalk.yellow(' python3 -m venv venv'));
|
152
|
+
console.log(chalk.yellow(' source venv/bin/activate # On Unix/macOS'));
|
153
|
+
console.log(chalk.yellow(' venv\\Scripts\\activate.bat # On Windows'));
|
154
|
+
console.log(chalk.yellow(' pip install modal gitingest requests'));
|
155
|
+
return false;
|
100
156
|
}
|
101
|
-
|
102
|
-
console.log(chalk.red('ā Failed to install Python packages'));
|
103
|
-
console.log(chalk.yellow('š” Please run manually:'));
|
104
|
-
console.log(chalk.yellow(' uv pip install modal gitingest requests'));
|
105
|
-
console.log(chalk.yellow(' or: pip install modal gitingest requests'));
|
106
|
-
console.log(chalk.yellow(' or: pip3 install modal gitingest requests'));
|
107
|
-
return false;
|
108
157
|
}
|
109
158
|
|
110
159
|
// Function to check Python
|
@@ -161,9 +210,9 @@ async function postinstall() {
|
|
161
210
|
console.log(chalk.blue('š Checking for uv package manager...'));
|
162
211
|
await checkAndInstallUv();
|
163
212
|
|
164
|
-
// Install Python packages
|
165
|
-
console.log(chalk.blue('š Installing Python dependencies...'));
|
166
|
-
await
|
213
|
+
// Install Python packages in virtual environment
|
214
|
+
console.log(chalk.blue('š Installing Python dependencies in virtual environment...'));
|
215
|
+
await createVirtualEnvironment();
|
167
216
|
|
168
217
|
// Create the Python directory if it doesn't exist
|
169
218
|
await fs.ensureDir(pythonScriptDir);
|
@@ -278,15 +327,21 @@ if __name__ == "__main__":
|
|
278
327
|
|
279
328
|
What was installed:
|
280
329
|
⢠GitArsenal CLI (npm package)
|
281
|
-
ā¢
|
282
|
-
|
283
|
-
|
330
|
+
⢠Virtual environment with Python packages:
|
331
|
+
- Modal
|
332
|
+
- GitIngest
|
333
|
+
- Requests
|
284
334
|
|
285
335
|
š” Next steps:
|
286
336
|
⢠Run: gitarsenal --help
|
287
337
|
⢠Run: gitarsenal setup
|
288
338
|
⢠Visit: https://gitarsenal.dev
|
289
339
|
|
340
|
+
š” To activate the virtual environment:
|
341
|
+
⢠Unix/macOS: source venv/bin/activate
|
342
|
+
⢠Windows: venv\\Scripts\\activate.bat
|
343
|
+
⢠Or use: ./activate_venv.sh (Unix/macOS) or activate_venv.bat (Windows)
|
344
|
+
|
290
345
|
Having issues? Run: gitarsenal --debug
|
291
346
|
`));
|
292
347
|
|
@@ -16,6 +16,13 @@ import signal
|
|
16
16
|
from pathlib import Path
|
17
17
|
import modal
|
18
18
|
|
19
|
+
# Import authentication manager
|
20
|
+
try:
|
21
|
+
from auth_manager import AuthManager
|
22
|
+
except ImportError:
|
23
|
+
print("ā Authentication module not found. Please ensure auth_manager.py is in the same directory.")
|
24
|
+
sys.exit(1)
|
25
|
+
|
19
26
|
# Parse command-line arguments
|
20
27
|
parser = argparse.ArgumentParser()
|
21
28
|
parser.add_argument('--proxy-url', help='URL of the proxy server')
|
@@ -3325,6 +3332,18 @@ def show_usage_examples():
|
|
3325
3332
|
"""Display usage examples for the script."""
|
3326
3333
|
print("Usage Examples\n")
|
3327
3334
|
|
3335
|
+
print("š Authentication Commands")
|
3336
|
+
print("āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā")
|
3337
|
+
print("ā gitarsenal --register # Register new account ā")
|
3338
|
+
print("ā gitarsenal --login # Login to existing account ā")
|
3339
|
+
print("ā gitarsenal --logout # Logout from account ā")
|
3340
|
+
print("ā gitarsenal --user-info # Show current user information ā")
|
3341
|
+
print("ā gitarsenal --change-password # Change password ā")
|
3342
|
+
print("ā gitarsenal --delete-account # Delete account ā")
|
3343
|
+
print("ā gitarsenal --store-api-key openai # Store OpenAI API key ā")
|
3344
|
+
print("ā gitarsenal --auth # Interactive auth management ā")
|
3345
|
+
print("āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n")
|
3346
|
+
|
3328
3347
|
print("Basic Container Creation")
|
3329
3348
|
print("āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā")
|
3330
3349
|
print("ā gitarsenal --gpu A10G --repo-url https://github.com/username/repo.git ā")
|
@@ -3359,19 +3378,32 @@ def show_usage_examples():
|
|
3359
3378
|
print("ā --use-api ā")
|
3360
3379
|
print("āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n")
|
3361
3380
|
|
3381
|
+
print("Development Mode (Skip Authentication)")
|
3382
|
+
print("āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā")
|
3383
|
+
print("ā gitarsenal --skip-auth --gpu A10G --repo-url https://github.com/username/repo.git ā")
|
3384
|
+
print("āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n")
|
3385
|
+
|
3362
3386
|
print("Available GPU Options:")
|
3363
3387
|
print(" T4, L4, A10G, A100-40GB, A100-80GB, L40S, H100, H200, B200")
|
3364
3388
|
print()
|
3389
|
+
print("Authentication Behavior:")
|
3390
|
+
print(" ⢠First time: Interactive registration/login required")
|
3391
|
+
print(" ⢠Subsequent runs: Automatic login with stored session")
|
3392
|
+
print(" ⢠Use --skip-auth for development (bypasses auth)")
|
3393
|
+
print()
|
3365
3394
|
print("GPU Selection Behavior:")
|
3366
3395
|
print(" ⢠With --gpu: Uses specified GPU without prompting")
|
3367
3396
|
print(" ⢠Without --gpu: Shows interactive GPU selection menu")
|
3368
3397
|
print()
|
3369
3398
|
print("Examples:")
|
3370
|
-
print(" #
|
3399
|
+
print(" # First time setup (will prompt for registration):")
|
3371
3400
|
print(" gitarsenal --gpu A10G --repo-url https://github.com/username/repo.git")
|
3372
3401
|
print()
|
3373
|
-
print(" #
|
3402
|
+
print(" # Subsequent runs (automatic login):")
|
3374
3403
|
print(" gitarsenal --repo-url https://github.com/username/repo.git")
|
3404
|
+
print()
|
3405
|
+
print(" # Development mode (skip authentication):")
|
3406
|
+
print(" gitarsenal --skip-auth --repo-url https://github.com/username/repo.git")
|
3375
3407
|
|
3376
3408
|
def make_api_request_with_retry(url, payload, max_retries=2, timeout=180):
|
3377
3409
|
"""Make an API request with retry mechanism."""
|
@@ -4005,6 +4037,158 @@ def fallback_preprocess_commands(setup_commands, stored_credentials):
|
|
4005
4037
|
print(f"š§ Fallback preprocessing completed: {len(processed_commands)} commands")
|
4006
4038
|
return processed_commands
|
4007
4039
|
|
4040
|
+
def _check_authentication(auth_manager):
|
4041
|
+
"""Check if user is authenticated, prompt for login if not"""
|
4042
|
+
if auth_manager.is_authenticated():
|
4043
|
+
user = auth_manager.get_current_user()
|
4044
|
+
print(f"ā
Authenticated as: {user['username']}")
|
4045
|
+
return True
|
4046
|
+
|
4047
|
+
print("\nš Authentication required")
|
4048
|
+
return auth_manager.interactive_auth_flow()
|
4049
|
+
|
4050
|
+
def _handle_auth_commands(auth_manager, args):
|
4051
|
+
"""Handle authentication-related commands"""
|
4052
|
+
if args.login:
|
4053
|
+
print("\nš LOGIN")
|
4054
|
+
username = input("Username: ").strip()
|
4055
|
+
password = getpass.getpass("Password: ").strip()
|
4056
|
+
if auth_manager.login_user(username, password):
|
4057
|
+
print("ā
Login successful!")
|
4058
|
+
else:
|
4059
|
+
print("ā Login failed.")
|
4060
|
+
|
4061
|
+
elif args.register:
|
4062
|
+
print("\nš REGISTRATION")
|
4063
|
+
username = input("Username (min 3 characters): ").strip()
|
4064
|
+
email = input("Email: ").strip()
|
4065
|
+
password = getpass.getpass("Password (min 8 characters): ").strip()
|
4066
|
+
confirm_password = getpass.getpass("Confirm password: ").strip()
|
4067
|
+
|
4068
|
+
if password != confirm_password:
|
4069
|
+
print("ā Passwords do not match.")
|
4070
|
+
return
|
4071
|
+
|
4072
|
+
if auth_manager.register_user(username, email, password):
|
4073
|
+
print("ā
Registration successful!")
|
4074
|
+
# Auto-login after registration
|
4075
|
+
if auth_manager.login_user(username, password):
|
4076
|
+
print("ā
Auto-login successful!")
|
4077
|
+
else:
|
4078
|
+
print("ā Registration failed.")
|
4079
|
+
|
4080
|
+
elif args.logout:
|
4081
|
+
auth_manager.logout_user()
|
4082
|
+
|
4083
|
+
elif args.user_info:
|
4084
|
+
auth_manager.show_user_info()
|
4085
|
+
|
4086
|
+
elif args.change_password:
|
4087
|
+
if not auth_manager.is_authenticated():
|
4088
|
+
print("ā Not logged in. Please login first.")
|
4089
|
+
return
|
4090
|
+
|
4091
|
+
current_password = getpass.getpass("Current password: ").strip()
|
4092
|
+
new_password = getpass.getpass("New password (min 8 characters): ").strip()
|
4093
|
+
confirm_password = getpass.getpass("Confirm new password: ").strip()
|
4094
|
+
|
4095
|
+
if new_password != confirm_password:
|
4096
|
+
print("ā New passwords do not match.")
|
4097
|
+
return
|
4098
|
+
|
4099
|
+
if auth_manager.change_password(current_password, new_password):
|
4100
|
+
print("ā
Password changed successfully!")
|
4101
|
+
else:
|
4102
|
+
print("ā Failed to change password.")
|
4103
|
+
|
4104
|
+
elif args.delete_account:
|
4105
|
+
if not auth_manager.is_authenticated():
|
4106
|
+
print("ā Not logged in. Please login first.")
|
4107
|
+
return
|
4108
|
+
|
4109
|
+
password = getpass.getpass("Enter your password to confirm deletion: ").strip()
|
4110
|
+
if auth_manager.delete_account(password):
|
4111
|
+
print("ā
Account deleted successfully!")
|
4112
|
+
else:
|
4113
|
+
print("ā Failed to delete account.")
|
4114
|
+
|
4115
|
+
elif args.store_api_key:
|
4116
|
+
if not auth_manager.is_authenticated():
|
4117
|
+
print("ā Not logged in. Please login first.")
|
4118
|
+
return
|
4119
|
+
|
4120
|
+
service = args.store_api_key
|
4121
|
+
api_key = getpass.getpass(f"Enter {service} API key: ").strip()
|
4122
|
+
|
4123
|
+
if auth_manager.store_api_key(service, api_key):
|
4124
|
+
print(f"ā
{service} API key stored successfully!")
|
4125
|
+
else:
|
4126
|
+
print(f"ā Failed to store {service} API key.")
|
4127
|
+
|
4128
|
+
elif args.auth:
|
4129
|
+
# Interactive authentication management
|
4130
|
+
while True:
|
4131
|
+
print("\n" + "="*60)
|
4132
|
+
print("š AUTHENTICATION MANAGEMENT")
|
4133
|
+
print("="*60)
|
4134
|
+
print("1. Login")
|
4135
|
+
print("2. Register")
|
4136
|
+
print("3. Show user info")
|
4137
|
+
print("4. Change password")
|
4138
|
+
print("5. Store API key")
|
4139
|
+
print("6. Delete account")
|
4140
|
+
print("7. Logout")
|
4141
|
+
print("8. Exit")
|
4142
|
+
|
4143
|
+
choice = input("\nSelect an option (1-8): ").strip()
|
4144
|
+
|
4145
|
+
if choice == "1":
|
4146
|
+
username = input("Username: ").strip()
|
4147
|
+
password = getpass.getpass("Password: ").strip()
|
4148
|
+
auth_manager.login_user(username, password)
|
4149
|
+
elif choice == "2":
|
4150
|
+
username = input("Username (min 3 characters): ").strip()
|
4151
|
+
email = input("Email: ").strip()
|
4152
|
+
password = getpass.getpass("Password (min 8 characters): ").strip()
|
4153
|
+
confirm_password = getpass.getpass("Confirm password: ").strip()
|
4154
|
+
if password == confirm_password:
|
4155
|
+
auth_manager.register_user(username, email, password)
|
4156
|
+
else:
|
4157
|
+
print("ā Passwords do not match.")
|
4158
|
+
elif choice == "3":
|
4159
|
+
auth_manager.show_user_info()
|
4160
|
+
elif choice == "4":
|
4161
|
+
if auth_manager.is_authenticated():
|
4162
|
+
current_password = getpass.getpass("Current password: ").strip()
|
4163
|
+
new_password = getpass.getpass("New password (min 8 characters): ").strip()
|
4164
|
+
confirm_password = getpass.getpass("Confirm new password: ").strip()
|
4165
|
+
if new_password == confirm_password:
|
4166
|
+
auth_manager.change_password(current_password, new_password)
|
4167
|
+
else:
|
4168
|
+
print("ā New passwords do not match.")
|
4169
|
+
else:
|
4170
|
+
print("ā Not logged in.")
|
4171
|
+
elif choice == "5":
|
4172
|
+
if auth_manager.is_authenticated():
|
4173
|
+
service = input("Service name (e.g., openai, modal): ").strip()
|
4174
|
+
api_key = getpass.getpass(f"Enter {service} API key: ").strip()
|
4175
|
+
auth_manager.store_api_key(service, api_key)
|
4176
|
+
else:
|
4177
|
+
print("ā Not logged in.")
|
4178
|
+
elif choice == "6":
|
4179
|
+
if auth_manager.is_authenticated():
|
4180
|
+
password = getpass.getpass("Enter your password to confirm deletion: ").strip()
|
4181
|
+
auth_manager.delete_account(password)
|
4182
|
+
else:
|
4183
|
+
print("ā Not logged in.")
|
4184
|
+
elif choice == "7":
|
4185
|
+
auth_manager.logout_user()
|
4186
|
+
elif choice == "8":
|
4187
|
+
print("š Goodbye!")
|
4188
|
+
break
|
4189
|
+
else:
|
4190
|
+
print("ā Invalid option. Please try again.")
|
4191
|
+
|
4008
4192
|
# Replace the existing GPU argument parsing in the main section
|
4009
4193
|
if __name__ == "__main__":
|
4010
4194
|
# Parse command line arguments when script is run directly
|
@@ -4030,8 +4214,27 @@ if __name__ == "__main__":
|
|
4030
4214
|
parser.add_argument('--list-gpus', action='store_true', help='List available GPU types with their specifications')
|
4031
4215
|
parser.add_argument('--interactive', action='store_true', help='Run in interactive mode with prompts')
|
4032
4216
|
|
4217
|
+
# Authentication-related arguments
|
4218
|
+
parser.add_argument('--auth', action='store_true', help='Manage authentication (login, register, logout)')
|
4219
|
+
parser.add_argument('--login', action='store_true', help='Login to GitArsenal')
|
4220
|
+
parser.add_argument('--register', action='store_true', help='Register new account')
|
4221
|
+
parser.add_argument('--logout', action='store_true', help='Logout from GitArsenal')
|
4222
|
+
parser.add_argument('--user-info', action='store_true', help='Show current user information')
|
4223
|
+
parser.add_argument('--change-password', action='store_true', help='Change password')
|
4224
|
+
parser.add_argument('--delete-account', action='store_true', help='Delete account')
|
4225
|
+
parser.add_argument('--store-api-key', type=str, help='Store API key for a service (e.g., openai, modal)')
|
4226
|
+
parser.add_argument('--skip-auth', action='store_true', help='Skip authentication check (for development)')
|
4227
|
+
|
4033
4228
|
args = parser.parse_args()
|
4034
4229
|
|
4230
|
+
# Initialize authentication manager
|
4231
|
+
auth_manager = AuthManager()
|
4232
|
+
|
4233
|
+
# Handle authentication-related commands
|
4234
|
+
if args.auth or args.login or args.register or args.logout or args.user_info or args.change_password or args.delete_account or args.store_api_key:
|
4235
|
+
_handle_auth_commands(auth_manager, args)
|
4236
|
+
sys.exit(0)
|
4237
|
+
|
4035
4238
|
# If --list-gpus is specified, just show GPU options and exit
|
4036
4239
|
if args.list_gpus:
|
4037
4240
|
prompt_for_gpu()
|
@@ -4042,6 +4245,13 @@ if __name__ == "__main__":
|
|
4042
4245
|
show_usage_examples()
|
4043
4246
|
sys.exit(0)
|
4044
4247
|
|
4248
|
+
# Check authentication (unless skipped for development)
|
4249
|
+
if not args.skip_auth:
|
4250
|
+
if not _check_authentication(auth_manager):
|
4251
|
+
print("\nā Authentication required. Please login or register first.")
|
4252
|
+
print("Use --login to login or --register to create an account.")
|
4253
|
+
sys.exit(1)
|
4254
|
+
|
4045
4255
|
# Check for dependencies
|
4046
4256
|
print("ā Checking dependencies...")
|
4047
4257
|
print("--- Dependency Check ---")
|