flyshell 1.0.0__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.
- flyshell/__init__.py +7 -0
- flyshell/abstract/__init__.py +0 -0
- flyshell/abstract/base_cmd.py +26 -0
- flyshell/abstract/base_plugin.py +75 -0
- flyshell/abstract/parent_cmd.py +59 -0
- flyshell/builtin/__init__.py +0 -0
- flyshell/builtin/fs.py +161 -0
- flyshell/builtin/system.py +124 -0
- flyshell/builtin/utils.py +113 -0
- flyshell/core/__init__.py +0 -0
- flyshell/core/auth.py +122 -0
- flyshell/core/console.py +64 -0
- flyshell/core/data.py +234 -0
- flyshell/core/directory.py +242 -0
- flyshell/core/loader.py +90 -0
- flyshell/main.py +115 -0
- flyshell-1.0.0.dist-info/METADATA +208 -0
- flyshell-1.0.0.dist-info/RECORD +22 -0
- flyshell-1.0.0.dist-info/WHEEL +5 -0
- flyshell-1.0.0.dist-info/entry_points.txt +2 -0
- flyshell-1.0.0.dist-info/licenses/LICENSE +21 -0
- flyshell-1.0.0.dist-info/top_level.txt +1 -0
flyshell/core/auth.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# src\flyshell\core\auth.py
|
|
2
|
+
|
|
3
|
+
from flyshell.abstract.base_cmd import BaseCmd
|
|
4
|
+
from flyshell.core import data
|
|
5
|
+
|
|
6
|
+
import getpass
|
|
7
|
+
import hashlib
|
|
8
|
+
import re
|
|
9
|
+
import secrets
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
def hash_password(password, salt=None):
|
|
13
|
+
if salt is None:
|
|
14
|
+
salt = secrets.token_bytes(16)
|
|
15
|
+
elif isinstance(salt, str):
|
|
16
|
+
salt = bytes.fromhex(salt)
|
|
17
|
+
pwd_hash = hashlib.pbkdf2_hmac('sha256', password.encode('utf-8'), salt, 600000)
|
|
18
|
+
return pwd_hash.hex(), salt.hex()
|
|
19
|
+
|
|
20
|
+
def verify_password(stored_hash: str, stored_salt: str, password: str) -> bool:
|
|
21
|
+
if not stored_hash or not stored_salt or not password:
|
|
22
|
+
return False
|
|
23
|
+
try:
|
|
24
|
+
salt_bytes = bytes.fromhex(stored_salt)
|
|
25
|
+
attempt_hash, _ = hash_password(password, salt_bytes)
|
|
26
|
+
return secrets.compare_digest(attempt_hash, stored_hash)
|
|
27
|
+
except (ValueError, TypeError):
|
|
28
|
+
return False
|
|
29
|
+
|
|
30
|
+
def setup_acc() -> bool:
|
|
31
|
+
print("\nWelcome to Flyshell, New User!")
|
|
32
|
+
print("Please follow the prompts to create your Flyshell profile.")
|
|
33
|
+
while True:
|
|
34
|
+
username = input("Create Username: ").strip()
|
|
35
|
+
if not username:
|
|
36
|
+
print("\nAccount Error: Username cannot be empty.\n")
|
|
37
|
+
else:
|
|
38
|
+
print(f"\nUsername set as '{username}'\n")
|
|
39
|
+
break
|
|
40
|
+
i = 5
|
|
41
|
+
print("Your password MUST:")
|
|
42
|
+
print("- Contain at least 10 total characters")
|
|
43
|
+
print("- Contain 1 or more upper case letters")
|
|
44
|
+
print("- Contain 1 or more lower case letters")
|
|
45
|
+
print("- Contain 1 or more numbers")
|
|
46
|
+
print("- Contain 1 or more special characters\n")
|
|
47
|
+
while i > 0:
|
|
48
|
+
password = getpass.getpass("Create Password: ")
|
|
49
|
+
confirm = getpass.getpass("Confirm Password: ")
|
|
50
|
+
if password != confirm:
|
|
51
|
+
print("\nAccount Error: Passwords do not match.")
|
|
52
|
+
i -= 1
|
|
53
|
+
if i == 0:
|
|
54
|
+
print("Too many attempts.\n")
|
|
55
|
+
return False
|
|
56
|
+
print(f"You have {i} attempts remaining.\n")
|
|
57
|
+
continue
|
|
58
|
+
has_upper = any(char.isupper() for char in password)
|
|
59
|
+
has_lower = any(char.islower() for char in password)
|
|
60
|
+
has_digit = any(char.isdigit() for char in password)
|
|
61
|
+
has_speci = bool(re.search(r'[^a-zA-Z0-9]', password))
|
|
62
|
+
if len(password) >= 10 and has_upper and has_lower and has_digit and has_speci:
|
|
63
|
+
break
|
|
64
|
+
else:
|
|
65
|
+
print("\nAccount Error: Password does not meet required conditions.\n")
|
|
66
|
+
i -= 1
|
|
67
|
+
if i == 0:
|
|
68
|
+
print("Too many attempts.\n")
|
|
69
|
+
return False
|
|
70
|
+
print(f"You have {i} attempts remaining.\n")
|
|
71
|
+
continue
|
|
72
|
+
pwd_hash, salt = hash_password(password)
|
|
73
|
+
auth_data = {
|
|
74
|
+
"username": username,
|
|
75
|
+
"hash": pwd_hash,
|
|
76
|
+
"salt": salt
|
|
77
|
+
}
|
|
78
|
+
data.set_auth(username, pwd_hash, salt)
|
|
79
|
+
print("\nSUCCESS: Account created successfully!")
|
|
80
|
+
return True
|
|
81
|
+
|
|
82
|
+
def login_flow(is_lock=False) -> bool:
|
|
83
|
+
auth_info = data.get_auth()
|
|
84
|
+
if not auth_info or "hash" not in auth_info:
|
|
85
|
+
if not setup_acc():
|
|
86
|
+
return False
|
|
87
|
+
auth_info = data.get_auth()
|
|
88
|
+
stored_user = auth_info.get("username", "User")
|
|
89
|
+
stored_hash = auth_info.get("hash")
|
|
90
|
+
stored_salt = auth_info.get("salt")
|
|
91
|
+
if is_lock:
|
|
92
|
+
input(f"\nFlyshell is locked. To unlock, press ENTER.")
|
|
93
|
+
print(f"\nWelcome back, {stored_user}!")
|
|
94
|
+
else:
|
|
95
|
+
print(f"\nWelcome, {stored_user}!")
|
|
96
|
+
i = 5
|
|
97
|
+
while i > 0:
|
|
98
|
+
entered_pwd = getpass.getpass("Enter Password: ")
|
|
99
|
+
if verify_password(stored_hash, stored_salt, entered_pwd):
|
|
100
|
+
if is_lock:
|
|
101
|
+
print()
|
|
102
|
+
return True
|
|
103
|
+
i -= 1
|
|
104
|
+
print(f"\nAccount Error: Password does not match.")
|
|
105
|
+
if i == 0:
|
|
106
|
+
print("Too many attempts.\n")
|
|
107
|
+
return False
|
|
108
|
+
print(f"You have {i} attempts remaining.\n")
|
|
109
|
+
|
|
110
|
+
class Lock(BaseCmd):
|
|
111
|
+
name = "lock"
|
|
112
|
+
description = "Lock the Flyshell system"
|
|
113
|
+
min_args = 0
|
|
114
|
+
options = None
|
|
115
|
+
aliases = []
|
|
116
|
+
|
|
117
|
+
def execute(self, args):
|
|
118
|
+
if login_flow(is_lock=True):
|
|
119
|
+
return (0, "")
|
|
120
|
+
else:
|
|
121
|
+
print("\nShutting down...\n")
|
|
122
|
+
sys.exit(0)
|
flyshell/core/console.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# src\flyshell\core\console.py
|
|
2
|
+
|
|
3
|
+
from flyshell.core import data
|
|
4
|
+
from flyshell.core.directory import execute_line
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import random
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
readline = None
|
|
11
|
+
|
|
12
|
+
def check_readline() -> bool:
|
|
13
|
+
global readline
|
|
14
|
+
try:
|
|
15
|
+
import readline
|
|
16
|
+
return True
|
|
17
|
+
except ImportError:
|
|
18
|
+
try:
|
|
19
|
+
import pyreadline3 as readline
|
|
20
|
+
return True
|
|
21
|
+
except ImportError:
|
|
22
|
+
readline = None
|
|
23
|
+
return False
|
|
24
|
+
|
|
25
|
+
def load_history():
|
|
26
|
+
if not readline:
|
|
27
|
+
return
|
|
28
|
+
history_entries = data.get_history(limit=100)
|
|
29
|
+
for entry in history_entries:
|
|
30
|
+
if isinstance(entry, dict) and "command" in entry:
|
|
31
|
+
readline.add_history(entry["command"])
|
|
32
|
+
|
|
33
|
+
def boot():
|
|
34
|
+
load_history()
|
|
35
|
+
print("\033[H\033[2J", end="")
|
|
36
|
+
print(f"\nBoot successful, welcome to Flyshell! (Version {data.VERSION})")
|
|
37
|
+
user_info = data.get_auth() or {}
|
|
38
|
+
username = user_info.get("username")
|
|
39
|
+
GREETINGS = [
|
|
40
|
+
f"Hi {username}, what's it going to be today?",
|
|
41
|
+
f"The mic is yours, {username}, go and blow me away!",
|
|
42
|
+
f"Hey {username}, let's get this party started!",
|
|
43
|
+
f"What's up, {username}? Let's go!",
|
|
44
|
+
f"I'm awaiting your first command, {username}!",
|
|
45
|
+
f"How's it going {username}? I'm here to help.",
|
|
46
|
+
f"{username}, it's your call! Show me what you've got, what are we doing today?",
|
|
47
|
+
f"My wish is your command, {username}! (unless it's not on my command list)",
|
|
48
|
+
f"I have goods for you {username}, it's called a Command Interface.",
|
|
49
|
+
f"I'm your boss! Wait, no... sorry... you, {username}, are my boss!",
|
|
50
|
+
]
|
|
51
|
+
greeting = random.choice(GREETINGS)
|
|
52
|
+
print(f"\n{greeting}\n")
|
|
53
|
+
while True:
|
|
54
|
+
try:
|
|
55
|
+
folder = os.path.basename(os.getcwd())
|
|
56
|
+
raw_cmd = input(f"Flyshell [v{data.VERSION}] ({folder})>>")
|
|
57
|
+
if not raw_cmd.strip():
|
|
58
|
+
continue
|
|
59
|
+
execute_line(raw_cmd)
|
|
60
|
+
except KeyboardInterrupt:
|
|
61
|
+
print()
|
|
62
|
+
except EOFError:
|
|
63
|
+
print("\n\nFlyshell session closed.\n")
|
|
64
|
+
sys.exit(0)
|
flyshell/core/data.py
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
# src\flyshell\core\data.py
|
|
2
|
+
|
|
3
|
+
BUILD = 99
|
|
4
|
+
VERSION = "1.0.0"
|
|
5
|
+
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import platform
|
|
10
|
+
import sqlite3
|
|
11
|
+
import time
|
|
12
|
+
|
|
13
|
+
HOST_OS = platform.system()
|
|
14
|
+
|
|
15
|
+
PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
|
|
16
|
+
|
|
17
|
+
def get_app_dir() -> Path:
|
|
18
|
+
home = Path.home()
|
|
19
|
+
if HOST_OS == "Windows":
|
|
20
|
+
base = Path(os.environ.get("APPDATA", home / "AppData" / "Roaming"))
|
|
21
|
+
elif HOST_OS == "Darwin":
|
|
22
|
+
base = home / "Library" / "Application Support"
|
|
23
|
+
else:
|
|
24
|
+
base = Path(os.environ.get("XDG_DATA_HOME", home / ".local" / "share"))
|
|
25
|
+
app_dir = base / "Flyshell"
|
|
26
|
+
app_dir.mkdir(parents=True, exist_ok=True)
|
|
27
|
+
return app_dir
|
|
28
|
+
|
|
29
|
+
APP_DIR = get_app_dir()
|
|
30
|
+
FILE_PATH = APP_DIR / "flyshell.db"
|
|
31
|
+
USER_PLUGIN_DIR = APP_DIR / "plugins"
|
|
32
|
+
|
|
33
|
+
SESSION_START_TIME = time.time()
|
|
34
|
+
SESSION_CMD_COUNT = 0
|
|
35
|
+
PLUGINS = {}
|
|
36
|
+
|
|
37
|
+
def get_storage_size() -> str:
|
|
38
|
+
if not FILE_PATH.exists():
|
|
39
|
+
return "File not found"
|
|
40
|
+
num_bytes = float(FILE_PATH.stat().st_size)
|
|
41
|
+
for unit in ["B", "KB", "MB", "GB"]:
|
|
42
|
+
if num_bytes < 1024.0:
|
|
43
|
+
return f"{int(num_bytes)} {unit}" if unit == "B" else f"{num_bytes:.2f} {unit}"
|
|
44
|
+
num_bytes /= 1024.0
|
|
45
|
+
return f"{num_bytes:.2f} TB"
|
|
46
|
+
|
|
47
|
+
def _get_connection(filename=FILE_PATH):
|
|
48
|
+
conn = sqlite3.connect(filename)
|
|
49
|
+
conn.execute("PRAGMA foreign_keys = ON;")
|
|
50
|
+
return conn
|
|
51
|
+
|
|
52
|
+
INITIALISED = set()
|
|
53
|
+
|
|
54
|
+
def initialise(filename=FILE_PATH):
|
|
55
|
+
global INITIALISED
|
|
56
|
+
target = str(filename)
|
|
57
|
+
if target in INITIALISED:
|
|
58
|
+
return
|
|
59
|
+
with _get_connection(filename) as conn:
|
|
60
|
+
conn.execute("PRAGMA foreign_keys = ON;")
|
|
61
|
+
current_version = conn.execute("PRAGMA user_version;").fetchone()[0]
|
|
62
|
+
if current_version == 0:
|
|
63
|
+
with conn:
|
|
64
|
+
conn.execute("""
|
|
65
|
+
CREATE TABLE IF NOT EXISTS auth (
|
|
66
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
67
|
+
username TEXT NOT NULL,
|
|
68
|
+
hash TEXT NOT NULL,
|
|
69
|
+
salt TEXT NOT NULL,
|
|
70
|
+
created_at TEXT NOT NULL
|
|
71
|
+
);
|
|
72
|
+
""")
|
|
73
|
+
conn.execute("""
|
|
74
|
+
CREATE TABLE IF NOT EXISTS cmd_history (
|
|
75
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
76
|
+
command TEXT NOT NULL,
|
|
77
|
+
timestamp TEXT NOT NULL
|
|
78
|
+
);
|
|
79
|
+
""")
|
|
80
|
+
conn.execute("""
|
|
81
|
+
CREATE TABLE IF NOT EXISTS core_config (
|
|
82
|
+
key TEXT PRIMARY KEY,
|
|
83
|
+
value TEXT NOT NULL
|
|
84
|
+
);
|
|
85
|
+
""")
|
|
86
|
+
conn.execute("""
|
|
87
|
+
CREATE TABLE IF NOT EXISTS plugins (
|
|
88
|
+
plugin_name TEXT PRIMARY KEY,
|
|
89
|
+
installed_at TEXT NOT NULL,
|
|
90
|
+
is_enabled INTEGER NOT NULL DEFAULT 1
|
|
91
|
+
);
|
|
92
|
+
""")
|
|
93
|
+
conn.execute("""
|
|
94
|
+
CREATE TABLE IF NOT EXISTS plugin_kv (
|
|
95
|
+
plugin_name TEXT NOT NULL,
|
|
96
|
+
key TEXT NOT NULL,
|
|
97
|
+
value TEXT NOT NULL,
|
|
98
|
+
PRIMARY KEY (plugin_name, key),
|
|
99
|
+
FOREIGN KEY (plugin_name) REFERENCES plugins(plugin_name) ON DELETE CASCADE
|
|
100
|
+
);
|
|
101
|
+
""")
|
|
102
|
+
conn.execute("PRAGMA user_version = 1;")
|
|
103
|
+
INITIALISED.add(target)
|
|
104
|
+
|
|
105
|
+
def get_auth(filename=FILE_PATH) -> dict | None:
|
|
106
|
+
initialise(filename)
|
|
107
|
+
with _get_connection(filename) as conn:
|
|
108
|
+
row = conn.execute("SELECT username, hash, salt, created_at FROM auth WHERE id = 1").fetchone()
|
|
109
|
+
if not row:
|
|
110
|
+
return None
|
|
111
|
+
return {"username": row[0], "hash": row[1], "salt": row[2], "created_at": row[3]}
|
|
112
|
+
|
|
113
|
+
def set_auth(username: str, pwd_hash: str, salt: str, filename=FILE_PATH) -> None:
|
|
114
|
+
initialise(filename)
|
|
115
|
+
now_utc = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
|
116
|
+
with _get_connection(filename) as conn:
|
|
117
|
+
with conn:
|
|
118
|
+
conn.execute("""
|
|
119
|
+
INSERT OR REPLACE INTO auth (id, username, hash, salt, created_at)
|
|
120
|
+
VALUES (1, ?, ?, ?, ?)
|
|
121
|
+
""", (username, pwd_hash, salt, now_utc))
|
|
122
|
+
|
|
123
|
+
def clear_auth(filename=FILE_PATH) -> bool:
|
|
124
|
+
initialise(filename)
|
|
125
|
+
with _get_connection(filename) as conn:
|
|
126
|
+
with conn:
|
|
127
|
+
cursor = conn.execute("DELETE FROM auth WHERE id = 1")
|
|
128
|
+
return cursor.rowcount > 0
|
|
129
|
+
|
|
130
|
+
def append_history(cmd: str, timestamp: str, filename=FILE_PATH) -> None:
|
|
131
|
+
initialise(filename)
|
|
132
|
+
with _get_connection(filename) as conn:
|
|
133
|
+
with conn:
|
|
134
|
+
conn.execute("INSERT INTO cmd_history (command, timestamp) VALUES (?, ?)", (cmd, timestamp))
|
|
135
|
+
|
|
136
|
+
def get_history(limit: int | None = None, filename=FILE_PATH) -> list[dict]:
|
|
137
|
+
initialise(filename)
|
|
138
|
+
with _get_connection(filename) as conn:
|
|
139
|
+
if limit is not None and limit > 0:
|
|
140
|
+
query = "SELECT command, timestamp FROM cmd_history ORDER BY id DESC LIMIT ?"
|
|
141
|
+
rows = conn.execute(query, (limit,)).fetchall()
|
|
142
|
+
rows.reverse()
|
|
143
|
+
else:
|
|
144
|
+
query = "SELECT command, timestamp FROM cmd_history ORDER BY id ASC"
|
|
145
|
+
rows = conn.execute(query).fetchall()
|
|
146
|
+
return [{"command": r[0], "timestamp": r[1]} for r in rows]
|
|
147
|
+
|
|
148
|
+
def clear_history(filename=FILE_PATH) -> bool:
|
|
149
|
+
initialise(filename)
|
|
150
|
+
with _get_connection(filename) as conn:
|
|
151
|
+
with conn:
|
|
152
|
+
conn.execute("DELETE FROM cmd_history")
|
|
153
|
+
return True
|
|
154
|
+
|
|
155
|
+
def get_config(key: str, default=None, filename=FILE_PATH):
|
|
156
|
+
initialise(filename)
|
|
157
|
+
with _get_connection(filename) as conn:
|
|
158
|
+
row = conn.execute("SELECT value FROM core_config WHERE key = ?", (key,)).fetchone()
|
|
159
|
+
if not row:
|
|
160
|
+
return default
|
|
161
|
+
try:
|
|
162
|
+
return json.loads(row[0])
|
|
163
|
+
except (ValueError, TypeError):
|
|
164
|
+
return row[0]
|
|
165
|
+
|
|
166
|
+
def set_config(key: str, value, filename=FILE_PATH) -> None:
|
|
167
|
+
initialise(filename)
|
|
168
|
+
serialized = json.dumps(value)
|
|
169
|
+
with _get_connection(filename) as conn:
|
|
170
|
+
with conn:
|
|
171
|
+
conn.execute("""
|
|
172
|
+
INSERT OR REPLACE INTO core_config (key, value)
|
|
173
|
+
VALUES (?, ?)
|
|
174
|
+
""", (key, serialized))
|
|
175
|
+
|
|
176
|
+
def delete_config(key: str, filename=FILE_PATH) -> bool:
|
|
177
|
+
initialise(filename)
|
|
178
|
+
with _get_connection(filename) as conn:
|
|
179
|
+
with conn:
|
|
180
|
+
cursor = conn.execute("DELETE FROM core_config WHERE key = ?", (key,))
|
|
181
|
+
return cursor.rowcount > 0
|
|
182
|
+
|
|
183
|
+
def register_plugin(plugin_name: str, filename=FILE_PATH) -> None:
|
|
184
|
+
initialise(filename)
|
|
185
|
+
now_utc = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
|
186
|
+
with _get_connection(filename) as conn:
|
|
187
|
+
with conn:
|
|
188
|
+
conn.execute("""
|
|
189
|
+
INSERT OR IGNORE INTO plugins (plugin_name, installed_at, is_enabled)
|
|
190
|
+
VALUES (?, ?, 1)
|
|
191
|
+
""", (plugin_name, now_utc))
|
|
192
|
+
|
|
193
|
+
def get_plugin_data(plugin_name: str, key: str, default=None, filename=FILE_PATH):
|
|
194
|
+
initialise(filename)
|
|
195
|
+
with _get_connection(filename) as conn:
|
|
196
|
+
row = conn.execute("""
|
|
197
|
+
SELECT value FROM plugin_kv WHERE plugin_name = ? AND key = ?
|
|
198
|
+
""", (plugin_name, key)).fetchone()
|
|
199
|
+
if not row:
|
|
200
|
+
return default
|
|
201
|
+
try:
|
|
202
|
+
return json.loads(row[0])
|
|
203
|
+
except (ValueError, TypeError):
|
|
204
|
+
return row[0]
|
|
205
|
+
|
|
206
|
+
def set_plugin_data(plugin_name: str, key: str, value, filename=FILE_PATH) -> None:
|
|
207
|
+
initialise(filename)
|
|
208
|
+
register_plugin(plugin_name, filename=filename)
|
|
209
|
+
serialized = json.dumps(value)
|
|
210
|
+
with _get_connection(filename) as conn:
|
|
211
|
+
with conn:
|
|
212
|
+
conn.execute("""
|
|
213
|
+
INSERT OR REPLACE INTO plugin_kv (plugin_name, key, value)
|
|
214
|
+
VALUES (?, ?, ?)
|
|
215
|
+
""", (plugin_name, key, serialized))
|
|
216
|
+
|
|
217
|
+
def get_all_plugin_data(plugin_name: str, filename=FILE_PATH) -> dict:
|
|
218
|
+
initialise(filename)
|
|
219
|
+
with _get_connection(filename) as conn:
|
|
220
|
+
rows = conn.execute("SELECT key, value FROM plugin_kv WHERE plugin_name = ?", (plugin_name,)).fetchall()
|
|
221
|
+
result = {}
|
|
222
|
+
for k, v in rows:
|
|
223
|
+
try:
|
|
224
|
+
result[k] = json.loads(v)
|
|
225
|
+
except (ValueError, TypeError):
|
|
226
|
+
result[k] = v
|
|
227
|
+
return result
|
|
228
|
+
|
|
229
|
+
def purge_plugin(plugin_name: str, filename=FILE_PATH) -> bool:
|
|
230
|
+
initialise(filename)
|
|
231
|
+
with _get_connection(filename) as conn:
|
|
232
|
+
with conn:
|
|
233
|
+
cursor = conn.execute("DELETE FROM plugins WHERE plugin_name = ?", (plugin_name,))
|
|
234
|
+
return cursor.rowcount > 0
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
# src\flyshell\core\directory.py
|
|
2
|
+
|
|
3
|
+
from flyshell.abstract.base_cmd import BaseCmd
|
|
4
|
+
from flyshell.abstract.parent_cmd import ParentCmd
|
|
5
|
+
from flyshell.builtin import fs, system, utils
|
|
6
|
+
from flyshell.core import auth, data
|
|
7
|
+
|
|
8
|
+
from datetime import datetime, timezone
|
|
9
|
+
import shlex
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Cmds(BaseCmd):
|
|
13
|
+
name = "cmds"
|
|
14
|
+
description = "List all available commands and their functions"
|
|
15
|
+
min_args = 0
|
|
16
|
+
options = None
|
|
17
|
+
aliases = ["help"]
|
|
18
|
+
|
|
19
|
+
def execute(self, args):
|
|
20
|
+
lines = ["\nAvailable Commands:"]
|
|
21
|
+
seen_cmds = set()
|
|
22
|
+
for cmd in COMMAND_REGISTRY.values():
|
|
23
|
+
if cmd in seen_cmds:
|
|
24
|
+
continue
|
|
25
|
+
seen_cmds.add(cmd)
|
|
26
|
+
cmd_label = f"{cmd.name} (alias: {', '.join(cmd.aliases)})" if cmd.aliases else cmd.name
|
|
27
|
+
if not cmd.options:
|
|
28
|
+
lines.append(f"\n{cmd_label}: {cmd.description} (Requires {cmd.min_args} parameter(s))")
|
|
29
|
+
else:
|
|
30
|
+
lines.append(f"\n{cmd_label}: {cmd.description} (Requires {cmd.min_args} parameter(s)) [args: {cmd.options}]")
|
|
31
|
+
|
|
32
|
+
lines.append(f"\nTotal available commands: {len(seen_cmds)}")
|
|
33
|
+
lines.append("Use '-h' or '--help' after any command to reveal its specific help list.")
|
|
34
|
+
lines.append("Flyshell also supports ;, && and || command chaining. Give it a go!\n")
|
|
35
|
+
return (0, "\n".join(lines))
|
|
36
|
+
|
|
37
|
+
INSTALLED_COMMANDS: list[BaseCmd] = [
|
|
38
|
+
system.Cd(),
|
|
39
|
+
Cmds(),
|
|
40
|
+
utils.Clear(),
|
|
41
|
+
system.Dir(),
|
|
42
|
+
fs.Fs(),
|
|
43
|
+
utils.History(),
|
|
44
|
+
utils.Kill(),
|
|
45
|
+
auth.Lock(),
|
|
46
|
+
system.OpenFile(),
|
|
47
|
+
utils.Reboot(),
|
|
48
|
+
utils.Sleep(),
|
|
49
|
+
system.SysExe(),
|
|
50
|
+
]
|
|
51
|
+
COMMAND_REGISTRY: dict[str, BaseCmd] = {}
|
|
52
|
+
for cmd_obj in INSTALLED_COMMANDS:
|
|
53
|
+
COMMAND_REGISTRY[cmd_obj.name] = cmd_obj
|
|
54
|
+
for alias_name in cmd_obj.aliases:
|
|
55
|
+
COMMAND_REGISTRY[alias_name] = cmd_obj
|
|
56
|
+
|
|
57
|
+
def log(cmd):
|
|
58
|
+
utc_now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
59
|
+
data.append_history(cmd, utc_now)
|
|
60
|
+
|
|
61
|
+
def _execute_single(raw_cmd: str) -> int:
|
|
62
|
+
try:
|
|
63
|
+
posix_mode = False if data.HOST_OS == "Windows" else True
|
|
64
|
+
cmd = shlex.split(raw_cmd, posix=posix_mode)
|
|
65
|
+
except ValueError as e:
|
|
66
|
+
print("\nCommand Error: Invalid syntax.")
|
|
67
|
+
print(f"Details: {e}\n")
|
|
68
|
+
return 1
|
|
69
|
+
if not cmd:
|
|
70
|
+
return 0
|
|
71
|
+
cmd_name = cmd[0]
|
|
72
|
+
args = cmd[1:]
|
|
73
|
+
if "-h" in args or "--help" in args:
|
|
74
|
+
if cmd_name in COMMAND_REGISTRY:
|
|
75
|
+
command = COMMAND_REGISTRY[cmd_name]
|
|
76
|
+
if isinstance(command, ParentCmd):
|
|
77
|
+
return command.execute(args)
|
|
78
|
+
command.help()
|
|
79
|
+
return 0
|
|
80
|
+
elif cmd_name in data.PLUGINS:
|
|
81
|
+
plugin = data.PLUGINS[cmd_name]
|
|
82
|
+
if hasattr(plugin, "help"):
|
|
83
|
+
plugin.help()
|
|
84
|
+
print()
|
|
85
|
+
return 0
|
|
86
|
+
else:
|
|
87
|
+
print(f"\nPlugin Error: Plugin '{cmd_name}' does not provide help details.\n")
|
|
88
|
+
return 1
|
|
89
|
+
if "-d" in args or "--docs" in args:
|
|
90
|
+
if cmd_name in data.PLUGINS:
|
|
91
|
+
plugin = data.PLUGINS[cmd_name]
|
|
92
|
+
if hasattr(plugin, "show_docs") and plugin.show_docs():
|
|
93
|
+
return 0
|
|
94
|
+
print(f"\nPlugin Error: Plugin '{cmd_name}' has not provided documentation.\n")
|
|
95
|
+
return 1
|
|
96
|
+
if cmd_name in COMMAND_REGISTRY:
|
|
97
|
+
command = COMMAND_REGISTRY[cmd_name]
|
|
98
|
+
if len(args) < command.min_args:
|
|
99
|
+
print(f"\nCommand Error: '{cmd_name}' requires at least {command.min_args} parameter(s).\n")
|
|
100
|
+
return 1
|
|
101
|
+
if cmd[0] != "history":
|
|
102
|
+
log(raw_cmd)
|
|
103
|
+
try:
|
|
104
|
+
result = command.execute(args)
|
|
105
|
+
except Exception as e:
|
|
106
|
+
print(f"\nCommand Error: Failed during execution of '{cmd_name}'.")
|
|
107
|
+
print(f"Details: {e}\n")
|
|
108
|
+
return 1
|
|
109
|
+
data.SESSION_CMD_COUNT += 1
|
|
110
|
+
if isinstance(result, tuple):
|
|
111
|
+
code, message = result
|
|
112
|
+
if message:
|
|
113
|
+
print(message)
|
|
114
|
+
return code
|
|
115
|
+
elif isinstance(result, int):
|
|
116
|
+
return result
|
|
117
|
+
elif isinstance(result, str):
|
|
118
|
+
if result:
|
|
119
|
+
print(result)
|
|
120
|
+
return 0
|
|
121
|
+
return 0
|
|
122
|
+
elif cmd_name in data.PLUGINS:
|
|
123
|
+
log(raw_cmd)
|
|
124
|
+
plugin = data.PLUGINS[cmd_name]
|
|
125
|
+
try:
|
|
126
|
+
result = plugin.execute(args)
|
|
127
|
+
return result if isinstance(result, int) else 0
|
|
128
|
+
except NotImplementedError:
|
|
129
|
+
print(f"\nPlugin Error: '{cmd_name}' has not implemented the execute method.\n")
|
|
130
|
+
return 1
|
|
131
|
+
except Exception as e:
|
|
132
|
+
print(f"\nPlugin Error: '{cmd_name}' crashed unexpectedly.")
|
|
133
|
+
print(f"Details: {e}")
|
|
134
|
+
print("Returning to main Flyshell interface...\n")
|
|
135
|
+
return 1
|
|
136
|
+
finally:
|
|
137
|
+
if hasattr(plugin, "on_unload"):
|
|
138
|
+
plugin.on_unload()
|
|
139
|
+
else:
|
|
140
|
+
print(f"\nCommand Error: '{cmd_name}' is not a recognised command. Use 'cmds' for help.\n")
|
|
141
|
+
return 1
|
|
142
|
+
|
|
143
|
+
def _parse_commands(raw_cmd: str) -> list:
|
|
144
|
+
chain = []
|
|
145
|
+
current = []
|
|
146
|
+
in_single_quote = False
|
|
147
|
+
in_double_quote = False
|
|
148
|
+
escape = False
|
|
149
|
+
i = 0
|
|
150
|
+
n = len(raw_cmd)
|
|
151
|
+
pending_op = None
|
|
152
|
+
while i < n:
|
|
153
|
+
char = raw_cmd[i]
|
|
154
|
+
if escape:
|
|
155
|
+
current.append(char)
|
|
156
|
+
escape = False
|
|
157
|
+
i += 1
|
|
158
|
+
continue
|
|
159
|
+
is_windows = (data.HOST_OS == "Windows")
|
|
160
|
+
if char == "\\":
|
|
161
|
+
if not is_windows:
|
|
162
|
+
escape = True
|
|
163
|
+
i += 1
|
|
164
|
+
continue
|
|
165
|
+
else:
|
|
166
|
+
if i + 1 < n and raw_cmd[i + 1] in ('"', "'"):
|
|
167
|
+
escape = True
|
|
168
|
+
i += 1
|
|
169
|
+
continue
|
|
170
|
+
current.append(char)
|
|
171
|
+
i += 1
|
|
172
|
+
continue
|
|
173
|
+
if char == "'" and not in_double_quote:
|
|
174
|
+
in_single_quote = not in_single_quote
|
|
175
|
+
current.append(char)
|
|
176
|
+
i += 1
|
|
177
|
+
continue
|
|
178
|
+
if char == '"' and not in_single_quote:
|
|
179
|
+
in_double_quote = not in_double_quote
|
|
180
|
+
current.append(char)
|
|
181
|
+
i += 1
|
|
182
|
+
continue
|
|
183
|
+
if not in_single_quote and not in_double_quote:
|
|
184
|
+
if raw_cmd[i:i+2] in ("&&", "||"):
|
|
185
|
+
op = raw_cmd[i:i+2]
|
|
186
|
+
cmd_str = "".join(current).strip()
|
|
187
|
+
if cmd_str:
|
|
188
|
+
chain.append((pending_op, cmd_str))
|
|
189
|
+
pending_op = op
|
|
190
|
+
current = []
|
|
191
|
+
i += 2
|
|
192
|
+
continue
|
|
193
|
+
if char == ";":
|
|
194
|
+
cmd_str = "".join(current).strip()
|
|
195
|
+
if cmd_str:
|
|
196
|
+
chain.append((pending_op, cmd_str))
|
|
197
|
+
pending_op = ";"
|
|
198
|
+
current = []
|
|
199
|
+
i += 1
|
|
200
|
+
continue
|
|
201
|
+
current.append(char)
|
|
202
|
+
i += 1
|
|
203
|
+
if in_single_quote or in_double_quote:
|
|
204
|
+
quote_char = "'" if in_single_quote else '"'
|
|
205
|
+
print(f"\nSyntax Error: Unclosed {quote_char} quote detected.\n")
|
|
206
|
+
return []
|
|
207
|
+
if escape:
|
|
208
|
+
print("\nSyntax Error: Dangling '\\' escape character at end of command.\n")
|
|
209
|
+
return []
|
|
210
|
+
final_cmd = "".join(current).strip()
|
|
211
|
+
if final_cmd:
|
|
212
|
+
chain.append((pending_op, final_cmd))
|
|
213
|
+
return chain
|
|
214
|
+
|
|
215
|
+
def execute_line(raw_cmd: str):
|
|
216
|
+
chain = _parse_commands(raw_cmd)
|
|
217
|
+
if not chain:
|
|
218
|
+
return
|
|
219
|
+
last_code = 0
|
|
220
|
+
skipped_last = False
|
|
221
|
+
for i, (prev_op, cmd_str) in enumerate(chain):
|
|
222
|
+
should_run = True
|
|
223
|
+
if prev_op == ";":
|
|
224
|
+
should_run = True
|
|
225
|
+
skipped_last = False
|
|
226
|
+
elif skipped_last:
|
|
227
|
+
if prev_op == "&&":
|
|
228
|
+
should_run = False
|
|
229
|
+
elif prev_op == "||":
|
|
230
|
+
should_run = True
|
|
231
|
+
else:
|
|
232
|
+
if prev_op == "&&" and last_code != 0:
|
|
233
|
+
should_run = False
|
|
234
|
+
elif prev_op == "||" and last_code == 0:
|
|
235
|
+
should_run = False
|
|
236
|
+
if should_run:
|
|
237
|
+
if i > 0:
|
|
238
|
+
print("-" * 40)
|
|
239
|
+
last_code = _execute_single(cmd_str)
|
|
240
|
+
skipped_last = False
|
|
241
|
+
else:
|
|
242
|
+
skipped_last = True
|