machineconfig 1.94__py3-none-any.whl → 1.95__py3-none-any.whl
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.
Potentially problematic release.
This version of machineconfig might be problematic. Click here for more details.
- machineconfig/cluster/data_transfer.py +2 -1
- machineconfig/cluster/job_params.py +1 -1
- machineconfig/cluster/script_execution.py +1 -1
- machineconfig/jobs/__pycache__/__init__.cpython-311.pyc +0 -0
- machineconfig/jobs/linux/msc/lid.sh +2 -4
- machineconfig/jobs/linux/msc/network.sh +3 -6
- machineconfig/jobs/python/check_installations.py +6 -6
- machineconfig/jobs/python/checkout_version.py +4 -4
- machineconfig/jobs/python/python_cargo_build_share.py +2 -2
- machineconfig/jobs/python/python_ve_symlink.py +4 -4
- machineconfig/jobs/python/vscode/api.py +2 -2
- machineconfig/jobs/python/vscode/link_ve.py +4 -4
- machineconfig/jobs/python/vscode/select_interpreter.py +4 -4
- machineconfig/jobs/python/vscode/sync_code.py +6 -6
- machineconfig/jobs/python_custom_installers/archive/ngrok.py +4 -4
- machineconfig/jobs/python_custom_installers/dev/aider.py +4 -4
- machineconfig/jobs/python_custom_installers/dev/alacritty.py +4 -4
- machineconfig/jobs/python_custom_installers/dev/brave.py +4 -4
- machineconfig/jobs/python_custom_installers/dev/bypass_paywall.py +4 -4
- machineconfig/jobs/python_custom_installers/dev/code.py +4 -4
- machineconfig/jobs/python_custom_installers/dev/docker.py +4 -4
- machineconfig/jobs/python_custom_installers/dev/docker_desktop.py +4 -4
- machineconfig/jobs/python_custom_installers/dev/espanso.py +8 -8
- machineconfig/jobs/python_custom_installers/dev/goes.py +4 -4
- machineconfig/jobs/python_custom_installers/dev/lvim.py +4 -4
- machineconfig/jobs/python_custom_installers/dev/nerdfont.py +4 -4
- machineconfig/jobs/python_custom_installers/dev/redis.py +4 -4
- machineconfig/jobs/python_custom_installers/dev/warp-cli.py +4 -4
- machineconfig/jobs/python_custom_installers/dev/wezterm.py +4 -4
- machineconfig/jobs/python_custom_installers/gh.py +6 -6
- machineconfig/jobs/python_custom_installers/hx.py +28 -58
- machineconfig/jobs/python_custom_installers/scripts/linux/brave.sh +4 -8
- machineconfig/jobs/python_custom_installers/scripts/linux/docker.sh +5 -10
- machineconfig/jobs/python_custom_installers/scripts/linux/docker_start.sh +3 -6
- machineconfig/jobs/python_custom_installers/scripts/linux/edge.sh +3 -6
- machineconfig/jobs/python_custom_installers/scripts/linux/nerdfont.sh +5 -10
- machineconfig/jobs/python_custom_installers/scripts/linux/pgsql.sh +4 -8
- machineconfig/jobs/python_custom_installers/scripts/linux/redis.sh +5 -10
- machineconfig/jobs/python_custom_installers/scripts/linux/timescaledb.sh +6 -12
- machineconfig/jobs/python_custom_installers/scripts/linux/vscode.sh +9 -8
- machineconfig/jobs/python_custom_installers/scripts/linux/warp-cli.sh +5 -10
- machineconfig/jobs/python_custom_installers/scripts/linux/wezterm.sh +3 -6
- machineconfig/jobs/python_generic_installers/__pycache__/__init__.cpython-311.pyc +0 -0
- machineconfig/jobs/python_linux_installers/__pycache__/__init__.cpython-311.pyc +0 -0
- machineconfig/profile/shell.py +26 -47
- machineconfig/scripts/__pycache__/__init__.cpython-311.pyc +0 -0
- machineconfig/scripts/cloud/init.sh +9 -18
- machineconfig/scripts/linux/fire +5 -24
- machineconfig/scripts/linux/share_cloud.sh +6 -12
- machineconfig/scripts/python/__pycache__/__init__.cpython-311.pyc +0 -0
- machineconfig/scripts/python/__pycache__/cloud_copy.cpython-311.pyc +0 -0
- machineconfig/scripts/python/__pycache__/cloud_mount.cpython-311.pyc +0 -0
- machineconfig/scripts/python/__pycache__/cloud_repo_sync.cpython-311.pyc +0 -0
- machineconfig/scripts/python/__pycache__/cloud_sync.cpython-311.pyc +0 -0
- machineconfig/scripts/python/__pycache__/croshell.cpython-311.pyc +0 -0
- machineconfig/scripts/python/__pycache__/devops.cpython-311.pyc +0 -0
- machineconfig/scripts/python/__pycache__/devops_devapps_install.cpython-311.pyc +0 -0
- machineconfig/scripts/python/__pycache__/devops_update_repos.cpython-311.pyc +0 -0
- machineconfig/scripts/python/__pycache__/fire_jobs.cpython-311.pyc +0 -0
- machineconfig/scripts/python/__pycache__/get_zellij_cmd.cpython-311.pyc +0 -0
- machineconfig/scripts/python/__pycache__/repos.cpython-311.pyc +0 -0
- machineconfig/scripts/python/archive/im2text.py +30 -30
- machineconfig/scripts/python/archive/tmate_conn.py +10 -13
- machineconfig/scripts/python/archive/tmate_start.py +12 -16
- machineconfig/scripts/python/choose_wezterm_theme.py +9 -18
- machineconfig/scripts/python/cloud_copy.py +38 -93
- machineconfig/scripts/python/cloud_manager.py +61 -53
- machineconfig/scripts/python/cloud_mount.py +23 -34
- machineconfig/scripts/python/cloud_repo_sync.py +20 -69
- machineconfig/scripts/python/cloud_sync.py +35 -45
- machineconfig/scripts/python/croshell.py +48 -73
- machineconfig/scripts/python/devops.py +50 -104
- machineconfig/scripts/python/devops_add_identity.py +41 -101
- machineconfig/scripts/python/devops_add_ssh_key.py +33 -140
- machineconfig/scripts/python/devops_backup_retrieve.py +23 -112
- machineconfig/scripts/python/devops_devapps_install.py +0 -4
- machineconfig/scripts/python/devops_update_repos.py +1 -1
- machineconfig/scripts/python/fire_jobs.py +73 -25
- machineconfig/scripts/python/helpers/__pycache__/__init__.cpython-311.pyc +0 -0
- machineconfig/scripts/python/helpers/__pycache__/cloud_helpers.cpython-311.pyc +0 -0
- machineconfig/scripts/python/helpers/__pycache__/helpers2.cpython-311.pyc +0 -0
- machineconfig/scripts/python/helpers/__pycache__/helpers4.cpython-311.pyc +0 -0
- machineconfig/scripts/python/helpers/__pycache__/repo_sync_helpers.cpython-311.pyc +0 -0
- machineconfig/scripts/python/helpers/cloud_helpers.py +37 -34
- machineconfig/scripts/python/helpers/helpers2.py +17 -31
- machineconfig/scripts/python/helpers/repo_sync_helpers.py +19 -54
- machineconfig/scripts/python/pomodoro.py +1 -1
- machineconfig/scripts/python/repos.py +49 -34
- machineconfig/scripts/python/wifi_conn.py +5 -3
- machineconfig/scripts/windows/fire.ps1 +27 -15
- machineconfig/settings/__pycache__/__init__.cpython-311.pyc +0 -0
- machineconfig/settings/shells/ipy/profiles/default/__pycache__/__init__.cpython-311.pyc +0 -0
- machineconfig/settings/shells/ipy/profiles/default/startup/__pycache__/__init__.cpython-311.pyc +0 -0
- machineconfig/settings/shells/ipy/profiles/default/startup/__pycache__/playext.cpython-311.pyc +0 -0
- machineconfig/setup_linux/nix/cli_installation.sh +9 -18
- machineconfig/setup_linux/others/openssh-server_add_pub_key.sh +3 -6
- machineconfig/setup_linux/web_shortcuts/all.sh +5 -10
- machineconfig/setup_linux/web_shortcuts/ascii_art.sh +7 -14
- machineconfig/setup_linux/web_shortcuts/croshell.sh +6 -12
- machineconfig/setup_linux/web_shortcuts/interactive.sh +34 -68
- machineconfig/setup_linux/web_shortcuts/ssh.sh +8 -16
- machineconfig/setup_linux/web_shortcuts/update_system.sh +7 -14
- machineconfig/setup_windows/wt_and_pwsh/set_wt_settings.py +16 -12
- machineconfig/utils/ai/browser_user_wrapper.py +60 -45
- machineconfig/utils/ai/generate_file_checklist.py +4 -7
- machineconfig/utils/ai/url2md.py +13 -5
- machineconfig/utils/{utils_code.py → code.py} +4 -10
- machineconfig/utils/installer.py +4 -10
- machineconfig/utils/{utils_links.py → links.py} +9 -20
- machineconfig/utils/{utils_options.py → options.py} +10 -20
- machineconfig/utils/{utils_path.py → path.py} +28 -80
- machineconfig/utils/procs.py +26 -30
- machineconfig/utils/scheduling.py +11 -11
- machineconfig/utils/utils.py +12 -19
- machineconfig/utils/ve.py +5 -21
- machineconfig/utils/ve_utils/ve2.py +15 -2
- {machineconfig-1.94.dist-info → machineconfig-1.95.dist-info}/METADATA +4 -2
- {machineconfig-1.94.dist-info → machineconfig-1.95.dist-info}/RECORD +120 -118
- {machineconfig-1.94.dist-info → machineconfig-1.95.dist-info}/WHEEL +1 -1
- {machineconfig-1.94.dist-info → machineconfig-1.95.dist-info}/top_level.txt +0 -0
|
@@ -5,6 +5,13 @@ from machineconfig.utils.utils import display_options, PROGRAM_PATH, write_shell
|
|
|
5
5
|
from platform import system
|
|
6
6
|
from enum import Enum
|
|
7
7
|
from typing import Optional
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.panel import Panel
|
|
10
|
+
from rich import box # Import box
|
|
11
|
+
|
|
12
|
+
console = Console()
|
|
13
|
+
|
|
14
|
+
BOX_WIDTH = 150 # width for box drawing
|
|
8
15
|
|
|
9
16
|
|
|
10
17
|
class Options(Enum):
|
|
@@ -25,11 +32,8 @@ class Options(Enum):
|
|
|
25
32
|
|
|
26
33
|
|
|
27
34
|
def args_parser():
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
║ 🛠️ DevOps Tool Suite ║
|
|
31
|
-
╚{'═' * 70}╝
|
|
32
|
-
""")
|
|
35
|
+
# Print header
|
|
36
|
+
console.print(Panel("🛠️ DevOps Tool Suite", title_align="left", border_style="blue", width=BOX_WIDTH))
|
|
33
37
|
|
|
34
38
|
import argparse
|
|
35
39
|
parser = argparse.ArgumentParser()
|
|
@@ -39,177 +43,119 @@ def args_parser():
|
|
|
39
43
|
main(which=args.which)
|
|
40
44
|
|
|
41
45
|
|
|
46
|
+
def display_title(title):
|
|
47
|
+
console.print(Panel(title, box=box.DOUBLE_EDGE, title_align="left")) # Replace print with Panel
|
|
48
|
+
|
|
49
|
+
def display_task_title(title):
|
|
50
|
+
console.print(Panel(title, box=box.ROUNDED, title_align="left")) # Replace print with Panel
|
|
51
|
+
|
|
52
|
+
def display_task_status(status):
|
|
53
|
+
console.print(Panel(status, box=box.ROUNDED, title_align="left")) # Replace print with Panel
|
|
54
|
+
|
|
55
|
+
def display_task_result(result):
|
|
56
|
+
console.print(Panel(result, box=box.ROUNDED, title_align="left")) # Replace print with Panel
|
|
57
|
+
|
|
58
|
+
def display_task_error(error):
|
|
59
|
+
console.print(Panel(error, box=box.ROUNDED, border_style="red", title_align="left")) # Replace print with Panel
|
|
60
|
+
|
|
61
|
+
def display_task_warning(warning):
|
|
62
|
+
console.print(Panel(warning, box=box.ROUNDED, border_style="yellow", title_align="left")) # Replace print with Panel
|
|
63
|
+
|
|
64
|
+
def display_task_success(success):
|
|
65
|
+
console.print(Panel(success, box=box.ROUNDED, border_style="green", title_align="left")) # Replace print with Panel
|
|
66
|
+
|
|
67
|
+
|
|
42
68
|
def main(which: Optional[str] = None):
|
|
43
69
|
PROGRAM_PATH.delete(sure=True, verbose=False)
|
|
44
|
-
print(
|
|
45
|
-
╭{'─' * 70}╮
|
|
46
|
-
│ 🚀 Initializing DevOps operation... │
|
|
47
|
-
╰{'─' * 70}╯
|
|
48
|
-
""")
|
|
70
|
+
console.print(Panel("🚀 Initializing DevOps operation...", width=BOX_WIDTH, border_style="blue"))
|
|
49
71
|
|
|
50
72
|
options = [op.value for op in Options]
|
|
51
73
|
if which is None:
|
|
52
74
|
try:
|
|
53
75
|
choice_key = display_options(msg="", options=options, header="🛠️ DEVOPS", default=options[0])
|
|
54
76
|
except KeyboardInterrupt:
|
|
55
|
-
print(
|
|
56
|
-
╔{'═' * 70}╗
|
|
57
|
-
║ ❌ Operation cancelled by user ║
|
|
58
|
-
╚{'═' * 70}╝
|
|
59
|
-
""")
|
|
77
|
+
console.print(Panel("❌ Operation cancelled by user", title_align="left", border_style="red", width=BOX_WIDTH))
|
|
60
78
|
return
|
|
61
79
|
else: choice_key = Options[which].value
|
|
62
80
|
|
|
63
|
-
print(f"""
|
|
64
|
-
╔{'═' * 70}╗
|
|
65
|
-
║ 🔧 SELECTED OPERATION ║
|
|
66
|
-
╠{'═' * 70}╣
|
|
67
|
-
║ {choice_key.center(68)} ║
|
|
68
|
-
╚{'═' * 70}╝
|
|
69
|
-
""")
|
|
81
|
+
console.print(Panel(f"🔧 SELECTED OPERATION\n{choice_key}", title_align="left", border_style="green", width=BOX_WIDTH))
|
|
70
82
|
|
|
71
83
|
if choice_key == Options.update.value:
|
|
72
|
-
print(
|
|
73
|
-
╭{'─' * 70}╮
|
|
74
|
-
│ 🔄 Updating essential repositories... │
|
|
75
|
-
╰{'─' * 70}╯
|
|
76
|
-
""")
|
|
84
|
+
console.print(Panel("🔄 Updating essential repositories...", width=BOX_WIDTH, border_style="blue"))
|
|
77
85
|
import machineconfig.scripts.python.devops_update_repos as helper
|
|
78
86
|
program = helper.main()
|
|
79
87
|
|
|
80
88
|
elif choice_key == Options.ve.value:
|
|
81
|
-
print(
|
|
82
|
-
╭{'─' * 70}╮
|
|
83
|
-
│ 🐍 Setting up virtual environment... │
|
|
84
|
-
╰{'─' * 70}╯
|
|
85
|
-
""")
|
|
89
|
+
console.print(Panel("🐍 Setting up virtual environment...", width=BOX_WIDTH, border_style="blue"))
|
|
86
90
|
from machineconfig.utils.ve import get_ve_install_script
|
|
87
91
|
program = get_ve_install_script()
|
|
88
92
|
|
|
89
93
|
elif choice_key == Options.cli_install.value:
|
|
90
|
-
print(
|
|
91
|
-
╭{'─' * 70}╮
|
|
92
|
-
│ ⚙️ Installing development applications... │
|
|
93
|
-
╰{'─' * 70}╯
|
|
94
|
-
""")
|
|
94
|
+
console.print(Panel("⚙️ Installing development applications...", width=BOX_WIDTH, border_style="blue"))
|
|
95
95
|
import machineconfig.scripts.python.devops_devapps_install as helper
|
|
96
96
|
program = helper.main()
|
|
97
97
|
|
|
98
98
|
elif choice_key == Options.sym_new.value:
|
|
99
|
-
print(
|
|
100
|
-
╭{'─' * 70}╮
|
|
101
|
-
│ 🔄 Creating new symlinks... │
|
|
102
|
-
╰{'─' * 70}╯
|
|
103
|
-
""")
|
|
99
|
+
console.print(Panel("🔄 Creating new symlinks...", width=BOX_WIDTH, border_style="blue"))
|
|
104
100
|
import machineconfig.jobs.python.python_ve_symlink as helper
|
|
105
101
|
program = helper.main()
|
|
106
102
|
|
|
107
103
|
elif choice_key == Options.sym_path_shell.value:
|
|
108
|
-
print(
|
|
109
|
-
╭{'─' * 70}╮
|
|
110
|
-
│ 🔗 Setting up symlinks, PATH, and shell profile... │
|
|
111
|
-
╰{'─' * 70}╯
|
|
112
|
-
""")
|
|
104
|
+
console.print(Panel("🔗 Setting up symlinks, PATH, and shell profile...", width=BOX_WIDTH, border_style="blue"))
|
|
113
105
|
import machineconfig.profile.create as helper
|
|
114
106
|
helper.main()
|
|
115
107
|
program = "echo '✅ done with symlinks'"
|
|
116
108
|
|
|
117
109
|
elif choice_key == Options.ssh_add_pubkey.value:
|
|
118
|
-
print(
|
|
119
|
-
╭{'─' * 70}╮
|
|
120
|
-
│ 🔑 Adding public SSH key to this machine... │
|
|
121
|
-
╰{'─' * 70}╯
|
|
122
|
-
""")
|
|
110
|
+
console.print(Panel("🔑 Adding public SSH key to this machine...", width=BOX_WIDTH, border_style="blue"))
|
|
123
111
|
import machineconfig.scripts.python.devops_add_ssh_key as helper
|
|
124
112
|
program = helper.main()
|
|
125
113
|
|
|
126
114
|
elif choice_key == Options.ssh_use_pair.value:
|
|
127
|
-
print(
|
|
128
|
-
╔{'═' * 70}╗
|
|
129
|
-
║ ❌ ERROR: Not Implemented ║
|
|
130
|
-
║ SSH key pair connection feature is not yet implemented ║
|
|
131
|
-
╚{'═' * 70}╝
|
|
132
|
-
""")
|
|
115
|
+
console.print(Panel("❌ ERROR: Not Implemented\nSSH key pair connection feature is not yet implemented", title_align="left", border_style="red", width=BOX_WIDTH))
|
|
133
116
|
raise NotImplementedError
|
|
134
117
|
|
|
135
118
|
elif choice_key == Options.ssh_add_id.value: # so that you can SSH directly withuot pointing to identity key.
|
|
136
|
-
print(
|
|
137
|
-
╭{'─' * 70}╮
|
|
138
|
-
│ 🗝️ Adding SSH identity (private key) to this machine... │
|
|
139
|
-
╰{'─' * 70}╯
|
|
140
|
-
""")
|
|
119
|
+
console.print(Panel("🗝️ Adding SSH identity (private key) to this machine...", width=BOX_WIDTH, border_style="blue"))
|
|
141
120
|
import machineconfig.scripts.python.devops_add_identity as helper
|
|
142
121
|
program = helper.main()
|
|
143
122
|
|
|
144
123
|
elif choice_key == Options.ssh_setup.value:
|
|
145
|
-
print(
|
|
146
|
-
╭{'─' * 70}╮
|
|
147
|
-
│ 📡 Setting up SSH... │
|
|
148
|
-
╰{'─' * 70}╯
|
|
149
|
-
""")
|
|
124
|
+
console.print(Panel("📡 Setting up SSH...", width=BOX_WIDTH, border_style="blue"))
|
|
150
125
|
program_windows = """Invoke-WebRequest https://raw.githubusercontent.com/thisismygitrepo/machineconfig/main/src/machineconfig/setup_windows/openssh_all.ps1 | Invoke-Expression # https://github.com/thisismygitrepo.keys"""
|
|
151
126
|
program_linux = """curl https://raw.githubusercontent.com/thisismygitrepo/machineconfig/main/src/machineconfig/setup_linux/openssh_all.sh | sudo bash # https://github.com/thisismygitrepo.keys"""
|
|
152
127
|
program = program_linux if system() == "Linux" else program_windows
|
|
153
128
|
|
|
154
129
|
elif choice_key == Options.ssh_setup_wsl.value:
|
|
155
|
-
print(
|
|
156
|
-
╭{'─' * 70}╮
|
|
157
|
-
│ 🐧 Setting up SSH for WSL... │
|
|
158
|
-
╰{'─' * 70}╯
|
|
159
|
-
""")
|
|
130
|
+
console.print(Panel("🐧 Setting up SSH for WSL...", width=BOX_WIDTH, border_style="blue"))
|
|
160
131
|
program = """curl https://raw.githubusercontent.com/thisismygitrepo/machineconfig/main/src/machineconfig/setup_linux/openssh_wsl.sh | sudo bash"""
|
|
161
132
|
|
|
162
133
|
elif choice_key == Options.backup.value:
|
|
163
|
-
print(
|
|
164
|
-
╭{'─' * 70}╮
|
|
165
|
-
│ 💾 Creating backup... │
|
|
166
|
-
╰{'─' * 70}╯
|
|
167
|
-
""")
|
|
134
|
+
console.print(Panel("💾 Creating backup...", width=BOX_WIDTH, border_style="blue"))
|
|
168
135
|
from machineconfig.scripts.python.devops_backup_retrieve import main_backup_retrieve as helper
|
|
169
136
|
program = helper(direction="BACKUP")
|
|
170
137
|
|
|
171
138
|
elif choice_key == Options.retreive.value:
|
|
172
|
-
print(
|
|
173
|
-
╭{'─' * 70}╮
|
|
174
|
-
│ 📥 Retrieving backup... │
|
|
175
|
-
╰{'─' * 70}╯
|
|
176
|
-
""")
|
|
139
|
+
console.print(Panel("📥 Retrieving backup...", width=BOX_WIDTH, border_style="blue"))
|
|
177
140
|
from machineconfig.scripts.python.devops_backup_retrieve import main_backup_retrieve as helper
|
|
178
141
|
program = helper(direction="RETRIEVE")
|
|
179
142
|
|
|
180
143
|
elif choice_key == Options.scheduler.value:
|
|
181
|
-
print(
|
|
182
|
-
╭{'─' * 70}╮
|
|
183
|
-
│ ⏰ Setting up scheduler... │
|
|
184
|
-
╰{'─' * 70}╯
|
|
185
|
-
""")
|
|
144
|
+
console.print(Panel("⏰ Setting up scheduler...", width=BOX_WIDTH, border_style="blue"))
|
|
186
145
|
from machineconfig.scripts.python.scheduler import main as helper
|
|
187
146
|
program = helper()
|
|
188
147
|
|
|
189
148
|
elif choice_key == Options.dot_files_sync.value:
|
|
190
|
-
print(
|
|
191
|
-
╭{'─' * 70}╮
|
|
192
|
-
│ 🔗 Synchronizing dotfiles... │
|
|
193
|
-
╰{'─' * 70}╯
|
|
194
|
-
""")
|
|
149
|
+
console.print(Panel("🔗 Synchronizing dotfiles...", width=BOX_WIDTH, border_style="blue"))
|
|
195
150
|
from machineconfig.scripts.python.cloud_repo_sync import main as helper, P
|
|
196
151
|
program = helper(cloud=None, path=str(P.home() / "dotfiles"), pwd=None, action="ask")
|
|
197
152
|
|
|
198
153
|
else:
|
|
199
|
-
print(
|
|
200
|
-
╔{'═' * 70}╗
|
|
201
|
-
║ ❌ ERROR: Invalid choice ║
|
|
202
|
-
║ The selected operation is not implemented: {choice_key}
|
|
203
|
-
╚{'═' * 70}╝
|
|
204
|
-
""")
|
|
154
|
+
console.print(Panel("❌ ERROR: Invalid choice", title_align="left", border_style="red", width=BOX_WIDTH))
|
|
205
155
|
raise ValueError(f"Unimplemented choice: {choice_key}")
|
|
206
156
|
|
|
207
157
|
if program:
|
|
208
|
-
print(
|
|
209
|
-
╭{'─' * 70}╮
|
|
210
|
-
│ 📜 Preparing shell script... │
|
|
211
|
-
╰{'─' * 70}╯
|
|
212
|
-
""")
|
|
158
|
+
console.print(Panel("📜 Preparing shell script...", width=BOX_WIDTH, border_style="blue"))
|
|
213
159
|
write_shell_script_to_default_program_path(program=program, display=True, preserve_cwd=True, desc="🔧 Shell script prepared by Python.", execute=True if which is not None else False)
|
|
214
160
|
else:
|
|
215
161
|
write_shell_script_to_default_program_path(program="echo '✨ Done.'", display=False, desc="🔧 Shell script prepared by Python.", preserve_cwd=True, execute=False)
|
|
@@ -4,125 +4,65 @@
|
|
|
4
4
|
|
|
5
5
|
# from platform import system
|
|
6
6
|
from crocodile.file_management import P
|
|
7
|
-
from machineconfig.utils.
|
|
7
|
+
from machineconfig.utils.options import display_options
|
|
8
|
+
from rich.panel import Panel
|
|
9
|
+
from rich.text import Text
|
|
10
|
+
|
|
11
|
+
BOX_WIDTH = 150 # width for box drawing
|
|
8
12
|
|
|
9
13
|
|
|
10
14
|
def main():
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
print(f"""
|
|
18
|
-
╭{'─' * 70}╮
|
|
19
|
-
│ 🔍 Searching for existing SSH keys... │
|
|
20
|
-
╰{'─' * 70}╯
|
|
21
|
-
""")
|
|
22
|
-
|
|
15
|
+
title = "🔑 SSH IDENTITY MANAGEMENT"
|
|
16
|
+
print(Panel(Text(title, justify="center"), expand=False))
|
|
17
|
+
|
|
18
|
+
print(Panel("🔍 Searching for existing SSH keys...", expand=False))
|
|
19
|
+
|
|
23
20
|
private_keys = P.home().joinpath(".ssh").search("*.pub").apply(lambda x: x.with_name(x.stem)).filter(lambda x: x.exists())
|
|
24
|
-
|
|
21
|
+
|
|
25
22
|
if private_keys:
|
|
26
|
-
print(f""
|
|
27
|
-
╭{'─' * 70}╮
|
|
28
|
-
│ ✅ Found {len(private_keys)} SSH private key(s) │
|
|
29
|
-
╰{'─' * 70}╯
|
|
30
|
-
""")
|
|
23
|
+
print(Panel(f"✅ Found {len(private_keys)} SSH private key(s)", expand=False))
|
|
31
24
|
else:
|
|
32
|
-
print(
|
|
33
|
-
|
|
34
|
-
│ ⚠️ No SSH private keys found │
|
|
35
|
-
╰{'─' * 70}╯
|
|
36
|
-
""")
|
|
37
|
-
|
|
25
|
+
print(Panel("⚠️ No SSH private keys found", expand=False))
|
|
26
|
+
|
|
38
27
|
choice = display_options(msg="Path to private key to be used when ssh'ing: ", options=private_keys.apply(str).list + ["I have the path to the key file", "I want to paste the key itself"])
|
|
39
|
-
|
|
28
|
+
|
|
40
29
|
if choice == "I have the path to the key file":
|
|
41
|
-
print(
|
|
42
|
-
╭{'─' * 70}╮
|
|
43
|
-
│ 📄 Please enter the path to your private key file │
|
|
44
|
-
╰{'─' * 70}╯
|
|
45
|
-
""")
|
|
30
|
+
print(Panel("📄 Please enter the path to your private key file", expand=False))
|
|
46
31
|
path_to_key = P(input("📋 Input path here: ")).expanduser().absolute()
|
|
47
|
-
print(f""
|
|
48
|
-
|
|
49
|
-
│ 📂 Using key from custom path: {path_to_key} │
|
|
50
|
-
╰{'─' * 70}╯
|
|
51
|
-
""")
|
|
52
|
-
|
|
32
|
+
print(Panel(f"📂 Using key from custom path: {path_to_key}", expand=False))
|
|
33
|
+
|
|
53
34
|
elif choice == "I want to paste the key itself":
|
|
54
|
-
print(
|
|
55
|
-
╭{'─' * 70}╮
|
|
56
|
-
│ 📋 Please provide a filename and paste the private key content │
|
|
57
|
-
╰{'─' * 70}╯
|
|
58
|
-
""")
|
|
35
|
+
print(Panel("📋 Please provide a filename and paste the private key content", expand=False))
|
|
59
36
|
key_filename = input("📝 File name (default: my_pasted_key): ") or "my_pasted_key"
|
|
60
37
|
path_to_key = P.home().joinpath(f".ssh/{key_filename}").write_text(input("🔑 Paste the private key here: "))
|
|
61
|
-
print(f""
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
╰{'─' * 70}╯
|
|
65
|
-
""")
|
|
66
|
-
|
|
67
|
-
elif isinstance(choice, str):
|
|
38
|
+
print(Panel(f"💾 Key saved to: {path_to_key}", expand=False))
|
|
39
|
+
|
|
40
|
+
elif isinstance(choice, str):
|
|
68
41
|
path_to_key = P(choice)
|
|
69
|
-
print(f""
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
""")
|
|
74
|
-
|
|
75
|
-
else:
|
|
76
|
-
print(f"""
|
|
77
|
-
╔{'═' * 70}╗
|
|
78
|
-
║ ❌ ERROR: Invalid choice ║
|
|
79
|
-
║ The selected option is not supported: {choice} ║
|
|
80
|
-
╚{'═' * 70}╝
|
|
81
|
-
""")
|
|
42
|
+
print(Panel(f"🔑 Using selected key: {path_to_key.name}", expand=False))
|
|
43
|
+
|
|
44
|
+
else:
|
|
45
|
+
error_message = f"❌ ERROR: Invalid choice\nThe selected option is not supported: {choice}"
|
|
46
|
+
print(Panel(Text(error_message, justify="center"), expand=False, border_style="red"))
|
|
82
47
|
raise NotImplementedError(f"Choice {choice} not supported")
|
|
83
|
-
|
|
48
|
+
|
|
84
49
|
txt = f"IdentityFile {path_to_key.collapseuser().as_posix()}" # adds this id for all connections, no host specified.
|
|
85
50
|
config_path = P.home().joinpath(".ssh/config")
|
|
86
|
-
|
|
87
|
-
print(
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
╰{'─' * 70}╯
|
|
91
|
-
""")
|
|
92
|
-
|
|
93
|
-
if config_path.exists():
|
|
51
|
+
|
|
52
|
+
print(Panel("📝 Updating SSH configuration...", expand=False))
|
|
53
|
+
|
|
54
|
+
if config_path.exists():
|
|
94
55
|
config_path.modify_text(txt_search=txt, txt_alt=txt, replace_line=True, notfound_append=True, prepend=True) # note that Identity line must come on top of config file otherwise it won't work, hence `prepend=True`
|
|
95
|
-
print(
|
|
96
|
-
|
|
97
|
-
│ ✏️ Updated existing SSH config file │
|
|
98
|
-
╰{'─' * 70}╯
|
|
99
|
-
""")
|
|
100
|
-
else:
|
|
56
|
+
print(Panel("✏️ Updated existing SSH config file", expand=False))
|
|
57
|
+
else:
|
|
101
58
|
config_path.write_text(txt)
|
|
102
|
-
print(
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
""
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
╔{'═' * 70}╗
|
|
110
|
-
║ ✅ SSH IDENTITY CONFIGURATION COMPLETE ║
|
|
111
|
-
╠{'═' * 70}╣
|
|
112
|
-
║ Identity added to SSH config file ║
|
|
113
|
-
║ Consider reloading the SSH config to apply changes ║
|
|
114
|
-
╚{'═' * 70}╝
|
|
115
|
-
'"""
|
|
116
|
-
|
|
117
|
-
print(f"""
|
|
118
|
-
╔{'═' * 70}╗
|
|
119
|
-
║ 🎉 CONFIGURATION SUCCESSFUL ║
|
|
120
|
-
╠{'═' * 70}╣
|
|
121
|
-
║ Identity added: {path_to_key.name}
|
|
122
|
-
║ Config file: {config_path}
|
|
123
|
-
╚{'═' * 70}╝
|
|
124
|
-
""")
|
|
125
|
-
|
|
59
|
+
print(Panel("📄 Created new SSH config file", expand=False))
|
|
60
|
+
|
|
61
|
+
program = f"""echo '{Panel(Text("✅ SSH IDENTITY CONFIGURATION COMPLETE\nIdentity added to SSH config file\nConsider reloading the SSH config to apply changes", justify="center"), expand=False, border_style="green")}' """
|
|
62
|
+
|
|
63
|
+
success_message = f"🎉 CONFIGURATION SUCCESSFUL\nIdentity added: {path_to_key.name}\nConfig file: {config_path}"
|
|
64
|
+
print(Panel(Text(success_message, justify="center"), expand=False, border_style="green"))
|
|
65
|
+
|
|
126
66
|
return program
|
|
127
67
|
|
|
128
68
|
|
|
@@ -5,71 +5,38 @@
|
|
|
5
5
|
from platform import system
|
|
6
6
|
from machineconfig.utils.utils import LIBRARY_ROOT, display_options
|
|
7
7
|
from crocodile.file_management import P
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.panel import Panel
|
|
10
|
+
from rich import box # Import box
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
console = Console()
|
|
8
14
|
|
|
9
15
|
|
|
10
16
|
def get_add_ssh_key_script(path_to_key: P):
|
|
11
|
-
print(
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
╚{'═' * 70}╝
|
|
15
|
-
""")
|
|
16
|
-
|
|
17
|
-
if system() == "Linux":
|
|
17
|
+
console.print(Panel("🔑 SSH KEY CONFIGURATION", title="[bold blue]SSH Setup[/bold blue]"))
|
|
18
|
+
|
|
19
|
+
if system() == "Linux":
|
|
18
20
|
authorized_keys = P.home().joinpath(".ssh/authorized_keys")
|
|
19
|
-
print(f"""
|
|
20
|
-
|
|
21
|
-
│ 🐧 Linux SSH configuration │
|
|
22
|
-
│ 📄 Authorized keys file: {authorized_keys} │
|
|
23
|
-
╰{'─' * 70}╯
|
|
24
|
-
""")
|
|
25
|
-
elif system() == "Windows":
|
|
21
|
+
console.print(Panel(f"🐧 Linux SSH configuration\n📄 Authorized keys file: {authorized_keys}", title="[bold blue]System Info[/bold blue]"))
|
|
22
|
+
elif system() == "Windows":
|
|
26
23
|
authorized_keys = P("C:/ProgramData/ssh/administrators_authorized_keys")
|
|
27
|
-
print(f"""
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
│ 📄 Authorized keys file: {authorized_keys} │
|
|
31
|
-
╰{'─' * 70}╯
|
|
32
|
-
""")
|
|
33
|
-
else:
|
|
34
|
-
print(f"""
|
|
35
|
-
╔{'═' * 70}╗
|
|
36
|
-
║ ❌ ERROR: Unsupported operating system ║
|
|
37
|
-
║ Only Linux and Windows are supported ║
|
|
38
|
-
╚{'═' * 70}╝
|
|
39
|
-
""")
|
|
24
|
+
console.print(Panel(f"🪟 Windows SSH configuration\n📄 Authorized keys file: {authorized_keys}", title="[bold blue]System Info[/bold blue]"))
|
|
25
|
+
else:
|
|
26
|
+
console.print(Panel("❌ ERROR: Unsupported operating system\nOnly Linux and Windows are supported", title="[bold red]Error[/bold red]"))
|
|
40
27
|
raise NotImplementedError
|
|
41
28
|
|
|
42
29
|
if authorized_keys.exists():
|
|
43
30
|
split = "\n"
|
|
44
31
|
keys_text = authorized_keys.read_text().split(split)
|
|
45
32
|
key_count = len([k for k in keys_text if k.strip()])
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
╭{'─' * 70}╮
|
|
49
|
-
│ 🔍 Current SSH authorization status │
|
|
50
|
-
│ ✅ Found {key_count} authorized key(s) │
|
|
51
|
-
╰{'─' * 70}╯
|
|
52
|
-
""")
|
|
53
|
-
|
|
33
|
+
console.print(Panel(f"🔍 Current SSH authorization status\n✅ Found {key_count} authorized key(s)", title="[bold blue]Status[/bold blue]"))
|
|
34
|
+
|
|
54
35
|
if path_to_key.read_text() in authorized_keys.read_text():
|
|
55
|
-
print(f"""
|
|
56
|
-
╔{'═' * 70}╗
|
|
57
|
-
║ ⚠️ Key already authorized ║
|
|
58
|
-
╠{'═' * 70}╣
|
|
59
|
-
║ Key: {path_to_key.name}
|
|
60
|
-
║ Status: Already present in authorized_keys file
|
|
61
|
-
║ No action required
|
|
62
|
-
╚{'═' * 70}╝
|
|
63
|
-
""")
|
|
36
|
+
console.print(Panel(f"⚠️ Key already authorized\nKey: {path_to_key.name}\nStatus: Already present in authorized_keys file\nNo action required", title="[bold yellow]Warning[/bold yellow]"))
|
|
64
37
|
program = ""
|
|
65
38
|
else:
|
|
66
|
-
print(f"""
|
|
67
|
-
╭{'─' * 70}╮
|
|
68
|
-
│ ➕ Adding new SSH key to authorized keys │
|
|
69
|
-
│ 🔑 Key file: {path_to_key.name} │
|
|
70
|
-
╰{'─' * 70}╯
|
|
71
|
-
""")
|
|
72
|
-
|
|
39
|
+
console.print(Panel(f"➕ Adding new SSH key to authorized keys\n🔑 Key file: {path_to_key.name}", title="[bold blue]Action[/bold blue]"))
|
|
73
40
|
if system() == "Linux":
|
|
74
41
|
program = f"cat {path_to_key} >> ~/.ssh/authorized_keys"
|
|
75
42
|
elif system() == "Windows":
|
|
@@ -78,34 +45,18 @@ def get_add_ssh_key_script(path_to_key: P):
|
|
|
78
45
|
place_holder = r'$sshfile = "$env:USERPROFILE\.ssh\pubkey.pub"'
|
|
79
46
|
assert place_holder in program, f"This section performs string manipulation on the script {program_path} to add the key to the authorized_keys file. The script has changed and the string {place_holder} is not found."
|
|
80
47
|
program = program.replace(place_holder, f'$sshfile = "{path_to_key}"')
|
|
81
|
-
print(
|
|
82
|
-
╭{'─' * 70}╮
|
|
83
|
-
│ 🔧 Configured PowerShell script for Windows │
|
|
84
|
-
│ 📝 Replaced placeholder with actual key path │
|
|
85
|
-
╰{'─' * 70}╯
|
|
86
|
-
""")
|
|
48
|
+
console.print(Panel("🔧 Configured PowerShell script for Windows\n📝 Replaced placeholder with actual key path", title="[bold blue]Configuration[/bold blue]"))
|
|
87
49
|
else: raise NotImplementedError
|
|
88
50
|
else:
|
|
89
|
-
print(f"""
|
|
90
|
-
╭{'─' * 70}╮
|
|
91
|
-
│ 📝 Creating new authorized_keys file │
|
|
92
|
-
│ 🔑 Using key: {path_to_key.name} │
|
|
93
|
-
╰{'─' * 70}╯
|
|
94
|
-
""")
|
|
95
|
-
|
|
51
|
+
console.print(Panel(f"📝 Creating new authorized_keys file\n🔑 Using key: {path_to_key.name}", title="[bold blue]Action[/bold blue]"))
|
|
96
52
|
if system() == "Linux":
|
|
97
53
|
program = f"cat {path_to_key} > ~/.ssh/authorized_keys"
|
|
98
54
|
else:
|
|
99
55
|
program_path = LIBRARY_ROOT.joinpath("setup_windows/openssh-server_add-sshkey.ps1")
|
|
100
56
|
program = P(program_path).expanduser().read_text().replace('$sshfile=""', f'$sshfile="{path_to_key}"')
|
|
101
|
-
print(
|
|
102
|
-
╭{'─' * 70}╮
|
|
103
|
-
│ 🔧 Configured PowerShell script for Windows │
|
|
104
|
-
│ 📝 Set key path in script │
|
|
105
|
-
╰{'─' * 70}╯
|
|
106
|
-
""")
|
|
57
|
+
console.print(Panel("🔧 Configured PowerShell script for Windows\n📝 Set key path in script", title="[bold blue]Configuration[/bold blue]"))
|
|
107
58
|
|
|
108
|
-
if system() == "Linux":
|
|
59
|
+
if system() == "Linux":
|
|
109
60
|
program += """
|
|
110
61
|
|
|
111
62
|
sudo chmod 700 ~/.ssh
|
|
@@ -113,50 +64,21 @@ sudo chmod 644 ~/.ssh/authorized_keys
|
|
|
113
64
|
sudo chmod 644 ~/.ssh/*.pub
|
|
114
65
|
sudo service ssh --full-restart
|
|
115
66
|
# from superuser.com/questions/215504/permissions-on-private-key-in-ssh-folder
|
|
116
|
-
|
|
117
67
|
"""
|
|
118
|
-
print(f"""
|
|
119
|
-
╭{'─' * 70}╮
|
|
120
|
-
│ 🔒 Setting proper SSH permissions and restarting service │
|
|
121
|
-
╰{'─' * 70}╯
|
|
122
|
-
""")
|
|
123
|
-
|
|
124
|
-
print(f"""
|
|
125
|
-
╔{'═' * 70}╗
|
|
126
|
-
║ ✅ SSH KEY CONFIGURATION PREPARED ║
|
|
127
|
-
╚{'═' * 70}╝
|
|
128
|
-
""")
|
|
129
|
-
|
|
130
68
|
return program
|
|
131
69
|
|
|
132
70
|
|
|
133
71
|
def main():
|
|
134
|
-
print(
|
|
135
|
-
╔{'═' * 70}╗
|
|
136
|
-
║ 🔐 SSH PUBLIC KEY AUTHORIZATION TOOL ║
|
|
137
|
-
╚{'═' * 70}╝
|
|
138
|
-
""")
|
|
72
|
+
console.print(Panel("🔐 SSH PUBLIC KEY AUTHORIZATION TOOL", box=box.DOUBLE_EDGE, title_align="left"))
|
|
139
73
|
|
|
140
|
-
print(
|
|
141
|
-
╭{'─' * 70}╮
|
|
142
|
-
│ 🔍 Searching for public keys... │
|
|
143
|
-
╰{'─' * 70}╯
|
|
144
|
-
""")
|
|
74
|
+
console.print(Panel("🔍 Searching for public keys...", title="[bold blue]SSH Setup[/bold blue]", border_style="blue"))
|
|
145
75
|
|
|
146
76
|
pub_keys = P.home().joinpath(".ssh").search("*.pub")
|
|
147
77
|
|
|
148
78
|
if pub_keys:
|
|
149
|
-
print(f"""
|
|
150
|
-
╭{'─' * 70}╮
|
|
151
|
-
│ ✅ Found {len(pub_keys)} public key(s) │
|
|
152
|
-
╰{'─' * 70}╯
|
|
153
|
-
""")
|
|
79
|
+
console.print(Panel(f"✅ Found {len(pub_keys)} public key(s)", title="[bold green]Status[/bold green]", border_style="green"))
|
|
154
80
|
else:
|
|
155
|
-
print(
|
|
156
|
-
╭{'─' * 70}╮
|
|
157
|
-
│ ⚠️ No public keys found │
|
|
158
|
-
╰{'─' * 70}╯
|
|
159
|
-
""")
|
|
81
|
+
console.print(Panel("⚠️ No public keys found", title="[bold yellow]Warning[/bold yellow]", border_style="yellow"))
|
|
160
82
|
|
|
161
83
|
all_keys_option = f"all pub keys available ({len(pub_keys)})"
|
|
162
84
|
i_have_path_option = "I have the path to the key file"
|
|
@@ -166,57 +88,28 @@ def main():
|
|
|
166
88
|
assert isinstance(res, str), f"Got {res} of type {type(res)} instead of str."
|
|
167
89
|
|
|
168
90
|
if res == all_keys_option:
|
|
169
|
-
print(f"""
|
|
170
|
-
╭{'─' * 70}╮
|
|
171
|
-
│ 🔄 Processing all {len(pub_keys)} public keys... │
|
|
172
|
-
╰{'─' * 70}╯
|
|
173
|
-
""")
|
|
91
|
+
console.print(Panel(f"🔄 Processing all {len(pub_keys)} public keys...", title="[bold blue]Processing[/bold blue]", border_style="blue"))
|
|
174
92
|
program = "\n\n\n".join(pub_keys.apply(get_add_ssh_key_script))
|
|
175
93
|
|
|
176
94
|
elif res == i_have_path_option:
|
|
177
|
-
print(
|
|
178
|
-
╭{'─' * 70}╮
|
|
179
|
-
│ 📂 Please provide the path to your public key │
|
|
180
|
-
╰{'─' * 70}╯
|
|
181
|
-
""")
|
|
95
|
+
console.print(Panel("📂 Please provide the path to your public key", title="[bold blue]Input Required[/bold blue]", border_style="blue"))
|
|
182
96
|
key_path = P(input("📋 Path: ")).expanduser().absolute()
|
|
183
|
-
print(f"""
|
|
184
|
-
╭{'─' * 70}╮
|
|
185
|
-
│ 📄 Using key from path: {key_path} │
|
|
186
|
-
╰{'─' * 70}╯
|
|
187
|
-
""")
|
|
97
|
+
console.print(Panel(f"📄 Using key from path: {key_path}", title="[bold blue]Info[/bold blue]", border_style="blue"))
|
|
188
98
|
program = get_add_ssh_key_script(key_path)
|
|
189
99
|
|
|
190
100
|
elif res == i_paste_option:
|
|
191
|
-
print(
|
|
192
|
-
╭{'─' * 70}╮
|
|
193
|
-
│ 📋 Please provide a filename and paste the public key content │
|
|
194
|
-
╰{'─' * 70}╯
|
|
195
|
-
""")
|
|
101
|
+
console.print(Panel("📋 Please provide a filename and paste the public key content", title="[bold blue]Input Required[/bold blue]", border_style="blue"))
|
|
196
102
|
key_filename = input("📝 File name (default: my_pasted_key.pub): ") or "my_pasted_key.pub"
|
|
197
103
|
key_path = P.home().joinpath(f".ssh/{key_filename}")
|
|
198
104
|
key_path.write_text(input("🔑 Paste the public key here: "))
|
|
199
|
-
print(f"""
|
|
200
|
-
╭{'─' * 70}╮
|
|
201
|
-
│ 💾 Key saved to: {key_path} │
|
|
202
|
-
╰{'─' * 70}╯
|
|
203
|
-
""")
|
|
105
|
+
console.print(Panel(f"💾 Key saved to: {key_path}", title="[bold green]Success[/bold green]", border_style="green"))
|
|
204
106
|
program = get_add_ssh_key_script(key_path)
|
|
205
107
|
|
|
206
108
|
else:
|
|
207
|
-
print(f"""
|
|
208
|
-
╭{'─' * 70}╮
|
|
209
|
-
│ 🔑 Using selected key: {P(res).name} │
|
|
210
|
-
╰{'─' * 70}╯
|
|
211
|
-
""")
|
|
109
|
+
console.print(Panel(f"🔑 Using selected key: {P(res).name}", title="[bold blue]Info[/bold blue]", border_style="blue"))
|
|
212
110
|
program = get_add_ssh_key_script(P(res))
|
|
213
111
|
|
|
214
|
-
print(
|
|
215
|
-
╔{'═' * 70}╗
|
|
216
|
-
║ 🚀 SSH KEY AUTHORIZATION READY ║
|
|
217
|
-
║ Run the generated script to apply changes ║
|
|
218
|
-
╚{'═' * 70}╝
|
|
219
|
-
""")
|
|
112
|
+
console.print(Panel("🚀 SSH KEY AUTHORIZATION READY\nRun the generated script to apply changes", box=box.DOUBLE_EDGE, title_align="left"))
|
|
220
113
|
|
|
221
114
|
return program
|
|
222
115
|
|