flyshell 1.0.0__tar.gz → 1.0.1__tar.gz
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-1.0.0/src/flyshell.egg-info → flyshell-1.0.1}/PKG-INFO +1 -1
- {flyshell-1.0.0 → flyshell-1.0.1}/pyproject.toml +1 -1
- {flyshell-1.0.0 → flyshell-1.0.1}/src/flyshell/builtin/fs.py +11 -10
- {flyshell-1.0.0 → flyshell-1.0.1}/src/flyshell/builtin/system.py +5 -7
- {flyshell-1.0.0 → flyshell-1.0.1}/src/flyshell/builtin/utils.py +3 -2
- flyshell-1.0.1/src/flyshell/constants.py +77 -0
- {flyshell-1.0.0 → flyshell-1.0.1}/src/flyshell/core/console.py +5 -16
- {flyshell-1.0.0 → flyshell-1.0.1}/src/flyshell/core/data.py +21 -52
- {flyshell-1.0.0 → flyshell-1.0.1}/src/flyshell/core/directory.py +10 -9
- {flyshell-1.0.0 → flyshell-1.0.1}/src/flyshell/core/loader.py +8 -9
- {flyshell-1.0.0 → flyshell-1.0.1}/src/flyshell/main.py +11 -31
- {flyshell-1.0.0 → flyshell-1.0.1/src/flyshell.egg-info}/PKG-INFO +1 -1
- {flyshell-1.0.0 → flyshell-1.0.1}/src/flyshell.egg-info/SOURCES.txt +1 -0
- {flyshell-1.0.0 → flyshell-1.0.1}/tests/test_loader.py +9 -8
- {flyshell-1.0.0 → flyshell-1.0.1}/LICENSE +0 -0
- {flyshell-1.0.0 → flyshell-1.0.1}/README.md +0 -0
- {flyshell-1.0.0 → flyshell-1.0.1}/setup.cfg +0 -0
- {flyshell-1.0.0 → flyshell-1.0.1}/src/flyshell/__init__.py +0 -0
- {flyshell-1.0.0 → flyshell-1.0.1}/src/flyshell/abstract/__init__.py +0 -0
- {flyshell-1.0.0 → flyshell-1.0.1}/src/flyshell/abstract/base_cmd.py +0 -0
- {flyshell-1.0.0 → flyshell-1.0.1}/src/flyshell/abstract/base_plugin.py +0 -0
- {flyshell-1.0.0 → flyshell-1.0.1}/src/flyshell/abstract/parent_cmd.py +0 -0
- {flyshell-1.0.0 → flyshell-1.0.1}/src/flyshell/builtin/__init__.py +0 -0
- {flyshell-1.0.0 → flyshell-1.0.1}/src/flyshell/core/__init__.py +0 -0
- {flyshell-1.0.0 → flyshell-1.0.1}/src/flyshell/core/auth.py +0 -0
- {flyshell-1.0.0 → flyshell-1.0.1}/src/flyshell.egg-info/dependency_links.txt +0 -0
- {flyshell-1.0.0 → flyshell-1.0.1}/src/flyshell.egg-info/entry_points.txt +0 -0
- {flyshell-1.0.0 → flyshell-1.0.1}/src/flyshell.egg-info/requires.txt +0 -0
- {flyshell-1.0.0 → flyshell-1.0.1}/src/flyshell.egg-info/top_level.txt +0 -0
- {flyshell-1.0.0 → flyshell-1.0.1}/tests/test_auth.py +0 -0
- {flyshell-1.0.0 → flyshell-1.0.1}/tests/test_data.py +0 -0
- {flyshell-1.0.0 → flyshell-1.0.1}/tests/test_directory.py +0 -0
- {flyshell-1.0.0 → flyshell-1.0.1}/tests/test_security.py +0 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# src\flyshell\builtin\fs.py
|
|
2
2
|
|
|
3
|
+
from flyshell import constants as c
|
|
3
4
|
from flyshell.abstract.base_cmd import BaseCmd
|
|
4
5
|
from flyshell.abstract.parent_cmd import ParentCmd
|
|
5
6
|
from flyshell.core import data
|
|
@@ -64,10 +65,10 @@ class FsInfo(BaseCmd):
|
|
|
64
65
|
return (0, (
|
|
65
66
|
"\nFlyshell System Information:\n"
|
|
66
67
|
"System Name: Flyshell\n"
|
|
67
|
-
f"System Version: {
|
|
68
|
-
f"System Build: {
|
|
69
|
-
f"Host Operating System: '{
|
|
70
|
-
f"Installed Plugin Count: {len(
|
|
68
|
+
f"System Version: {c.VERSION}\n"
|
|
69
|
+
f"System Build: {c.BUILD}\n"
|
|
70
|
+
f"Host Operating System: '{c.HOST_OS}'\n"
|
|
71
|
+
f"Installed Plugin Count: {len(c.PLUGINS)}\n"
|
|
71
72
|
"Original Release Date: 8th August 2026\n"
|
|
72
73
|
))
|
|
73
74
|
|
|
@@ -98,13 +99,13 @@ class FsPlugins(BaseCmd):
|
|
|
98
99
|
def execute(self, args):
|
|
99
100
|
lines = ["\nInstalled Plugins:"]
|
|
100
101
|
if data.PLUGINS:
|
|
101
|
-
for name, plugin in
|
|
102
|
+
for name, plugin in c.PLUGINS.items():
|
|
102
103
|
p_name = getattr(plugin, "name", name)
|
|
103
104
|
p_desc = getattr(plugin, "description", "No description provided.")
|
|
104
105
|
lines.append(f"Plugin '{name}' [{p_name}]: {p_desc}")
|
|
105
106
|
else:
|
|
106
107
|
lines.append("No plugins installed.")
|
|
107
|
-
lines.append(f"\nTotal available plugins: {len(
|
|
108
|
+
lines.append(f"\nTotal available plugins: {len(c.PLUGINS)}\n")
|
|
108
109
|
return (0, "\n".join(lines))
|
|
109
110
|
|
|
110
111
|
class FsStatus(BaseCmd):
|
|
@@ -115,7 +116,7 @@ class FsStatus(BaseCmd):
|
|
|
115
116
|
aliases = []
|
|
116
117
|
|
|
117
118
|
def execute(self, args):
|
|
118
|
-
uptime = int(time.time() -
|
|
119
|
+
uptime = int(time.time() - c.SESSION_START_TIME)
|
|
119
120
|
mins, secs = divmod(uptime, 60)
|
|
120
121
|
hours, mins = divmod(mins, 60)
|
|
121
122
|
uptime_str = f"{hours}h {mins}m {secs}s"
|
|
@@ -128,12 +129,12 @@ class FsStatus(BaseCmd):
|
|
|
128
129
|
"\nFlyshell System Status:\n"
|
|
129
130
|
f"User: '{current_user}'\n"
|
|
130
131
|
f"Session Uptime: {uptime_str}\n"
|
|
131
|
-
f"Commands (Session): {
|
|
132
|
+
f"Commands (Session): {c.SESSION_CMD_COUNT}\n"
|
|
132
133
|
f"Commands (Lifetime): {total_history_count}\n"
|
|
133
134
|
f"Working Directory: '{os.getcwd()}'\n"
|
|
134
135
|
f"Storage File Size: {storage_size_str}\n"
|
|
135
136
|
f"Python Environment: v{sys.version.split()[0]}\n"
|
|
136
|
-
f"\nFlyshell Version {
|
|
137
|
+
f"\nFlyshell Version {c.VERSION} (Build {c.BUILD})\n"
|
|
137
138
|
))
|
|
138
139
|
|
|
139
140
|
class FsVersion(BaseCmd):
|
|
@@ -143,7 +144,7 @@ class FsVersion(BaseCmd):
|
|
|
143
144
|
options = None
|
|
144
145
|
aliases = []
|
|
145
146
|
def execute(self, args):
|
|
146
|
-
return (0, f"\nYou are on Flyshell Version {
|
|
147
|
+
return (0, f"\nYou are on Flyshell Version {c.VERSION} (Build {c.BUILD})\n")
|
|
147
148
|
|
|
148
149
|
class Fs(ParentCmd):
|
|
149
150
|
name = "fs"
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# src\flyshell\builtin\system.py
|
|
2
2
|
|
|
3
|
+
from flyshell import constants as c
|
|
3
4
|
from flyshell.abstract.base_cmd import BaseCmd
|
|
4
5
|
from flyshell.core import auth, data
|
|
5
6
|
|
|
@@ -8,7 +9,6 @@ import os
|
|
|
8
9
|
import re
|
|
9
10
|
import shutil
|
|
10
11
|
import subprocess
|
|
11
|
-
import sys
|
|
12
12
|
|
|
13
13
|
class Cd(BaseCmd):
|
|
14
14
|
name = "cd"
|
|
@@ -68,9 +68,9 @@ class OpenFile(BaseCmd):
|
|
|
68
68
|
if not (is_path or is_url or is_bin):
|
|
69
69
|
return (1, f"\nCommand Error: File or application '{target}' could not be found.\n")
|
|
70
70
|
try:
|
|
71
|
-
if
|
|
71
|
+
if c.HOST_OS == "Windows":
|
|
72
72
|
os.startfile(target)
|
|
73
|
-
elif
|
|
73
|
+
elif c.HOST_OS == "Darwin":
|
|
74
74
|
subprocess.Popen(["open", target], stderr=subprocess.DEVNULL)
|
|
75
75
|
else:
|
|
76
76
|
subprocess.Popen(["xdg-open", target], stderr=subprocess.DEVNULL)
|
|
@@ -95,10 +95,8 @@ class SysExe(BaseCmd):
|
|
|
95
95
|
override = False
|
|
96
96
|
cmd = " ".join(args).strip().strip('"\'')
|
|
97
97
|
cmd_normalised = " ".join(cmd.lower().split())
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
DANGER_COMMANDS = {"del", "rmdir", "rd", "rm", "taskkill", "killall"}
|
|
101
|
-
is_dangerous = any(re.search(rf"\b{re.escape(d)}\b", cmd_normalised) for d in DANGER_COMMANDS)
|
|
98
|
+
is_blocked = any(re.search(rf"\b{re.escape(b)}\b", cmd_normalised) for b in c.BLOCKED_COMMANDS)
|
|
99
|
+
is_dangerous = any(re.search(rf"\b{re.escape(d)}\b", cmd_normalised) for d in c.DANGER_COMMANDS)
|
|
102
100
|
if is_blocked:
|
|
103
101
|
return (1, (
|
|
104
102
|
"\nSecurity Error: Flyshell has blocked execution of this command.\n"
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# src\flyshell\builtin\utils.py
|
|
2
2
|
|
|
3
|
+
from flyshell import constants as c
|
|
3
4
|
from flyshell.abstract.base_cmd import BaseCmd
|
|
4
5
|
from flyshell.core import data
|
|
5
6
|
|
|
@@ -17,7 +18,7 @@ class Clear(BaseCmd):
|
|
|
17
18
|
aliases = ["cls"]
|
|
18
19
|
|
|
19
20
|
def execute(self, args):
|
|
20
|
-
return (0,
|
|
21
|
+
return (0, c.CLEAR_SCREEN)
|
|
21
22
|
|
|
22
23
|
class History(BaseCmd):
|
|
23
24
|
name = "history"
|
|
@@ -83,7 +84,7 @@ class Reboot(BaseCmd):
|
|
|
83
84
|
cmd = sys.argv
|
|
84
85
|
else:
|
|
85
86
|
cmd = [sys.executable] + sys.argv
|
|
86
|
-
if
|
|
87
|
+
if c.HOST_OS == "Windows":
|
|
87
88
|
old_handler = signal.signal(signal.SIGINT, signal.SIG_IGN)
|
|
88
89
|
try:
|
|
89
90
|
p = subprocess.run(cmd)
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# src\flyshell\constants.py
|
|
2
|
+
|
|
3
|
+
from flyshell.core.directory import COMMAND_REGISTRY
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import os
|
|
7
|
+
import platform
|
|
8
|
+
import time
|
|
9
|
+
|
|
10
|
+
BUILD = 100
|
|
11
|
+
VERSION = "1.0.1"
|
|
12
|
+
|
|
13
|
+
def _resolve_app_dir() -> Path:
|
|
14
|
+
home = Path.home()
|
|
15
|
+
if HOST_OS == "Windows":
|
|
16
|
+
base = Path(os.environ.get("APPDATA", home / "AppData" / "Roaming"))
|
|
17
|
+
elif HOST_OS == "Darwin":
|
|
18
|
+
base = home / "Library" / "Application Support"
|
|
19
|
+
else:
|
|
20
|
+
base = Path(os.environ.get("XDG_DATA_HOME", home / ".local" / "share"))
|
|
21
|
+
target = base / "Flyshell"
|
|
22
|
+
target.mkdir(parents=True, exist_ok=True)
|
|
23
|
+
return target
|
|
24
|
+
|
|
25
|
+
HOST_OS = platform.system()
|
|
26
|
+
PACKAGE_ROOT = Path(__file__).resolve().parent
|
|
27
|
+
SRC_DIR = PACKAGE_ROOT.parent
|
|
28
|
+
PROJECT_ROOT = SRC_DIR.parent
|
|
29
|
+
APP_DIR = _resolve_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
|
+
|
|
36
|
+
PLUGINS = {}
|
|
37
|
+
RESERVED_NAMES = set(COMMAND_REGISTRY.keys())
|
|
38
|
+
|
|
39
|
+
BLOCKED_COMMANDS = frozenset({"format", "diskpart", "dd"})
|
|
40
|
+
DANGER_COMMANDS = frozenset({"del", "rmdir", "rd", "rm", "taskkill", "killall"})
|
|
41
|
+
SUPPORTED_OS = frozenset({"Windows", "Darwin", "Linux"})
|
|
42
|
+
SUPPORTED_PYTHON = (3, 10)
|
|
43
|
+
|
|
44
|
+
CLEAR_SCREEN = "\033[H\033[2J"
|
|
45
|
+
|
|
46
|
+
INITIALISED = set()
|
|
47
|
+
|
|
48
|
+
REQUIRED_COMPONENTS = [
|
|
49
|
+
"abstract",
|
|
50
|
+
"abstract/base_cmd.py",
|
|
51
|
+
"abstract/base_plugin.py",
|
|
52
|
+
"abstract/parent_cmd.py",
|
|
53
|
+
"builtin",
|
|
54
|
+
"builtin/fs.py",
|
|
55
|
+
"builtin/system.py",
|
|
56
|
+
"builtin/utils.py",
|
|
57
|
+
"constants.py",
|
|
58
|
+
"core",
|
|
59
|
+
"core/auth.py",
|
|
60
|
+
"core/console.py",
|
|
61
|
+
"core/data.py",
|
|
62
|
+
"core/directory.py",
|
|
63
|
+
"core/loader.py",
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
GREETINGS = [
|
|
67
|
+
"Hi {username}, what's it going to be today?",
|
|
68
|
+
"The mic is yours, {username}, go and blow me away!",
|
|
69
|
+
"Hey {username}, let's get this party started!",
|
|
70
|
+
"What's up, {username}? Let's go!",
|
|
71
|
+
"I'm awaiting your first command, {username}!",
|
|
72
|
+
"How's it going {username}? I'm here to help.",
|
|
73
|
+
"{username}, it's your call! Show me what you've got, what are we doing today?",
|
|
74
|
+
"My wish is your command, {username}! (unless it's not on my command list)",
|
|
75
|
+
"I have goods for you {username}, it's called a Command Interface.",
|
|
76
|
+
"I'm your boss! Wait, no... sorry... you, {username}, are my boss!",
|
|
77
|
+
]
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# src\flyshell\core\console.py
|
|
2
2
|
|
|
3
|
+
from flyshell import constants as c
|
|
3
4
|
from flyshell.core import data
|
|
4
5
|
from flyshell.core.directory import execute_line
|
|
5
6
|
|
|
@@ -32,28 +33,16 @@ def load_history():
|
|
|
32
33
|
|
|
33
34
|
def boot():
|
|
34
35
|
load_history()
|
|
35
|
-
print(
|
|
36
|
-
print(f"\nBoot successful, welcome to Flyshell! (Version {
|
|
36
|
+
print(c.CLEAR_SCREEN, end="")
|
|
37
|
+
print(f"\nBoot successful, welcome to Flyshell! (Version {c.VERSION})")
|
|
37
38
|
user_info = data.get_auth() or {}
|
|
38
39
|
username = user_info.get("username")
|
|
39
|
-
|
|
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)
|
|
40
|
+
greeting = random.choice(c.GREETINGS).format(username=username)
|
|
52
41
|
print(f"\n{greeting}\n")
|
|
53
42
|
while True:
|
|
54
43
|
try:
|
|
55
44
|
folder = os.path.basename(os.getcwd())
|
|
56
|
-
raw_cmd = input(f"Flyshell [v{
|
|
45
|
+
raw_cmd = input(f"Flyshell [v{c.VERSION}] ({folder})>>")
|
|
57
46
|
if not raw_cmd.strip():
|
|
58
47
|
continue
|
|
59
48
|
execute_line(raw_cmd)
|
|
@@ -1,60 +1,29 @@
|
|
|
1
1
|
# src\flyshell\core\data.py
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
VERSION = "1.0.0"
|
|
3
|
+
from flyshell import constants as c
|
|
5
4
|
|
|
6
|
-
from pathlib import Path
|
|
7
5
|
import json
|
|
8
|
-
import os
|
|
9
|
-
import platform
|
|
10
6
|
import sqlite3
|
|
11
7
|
import time
|
|
12
8
|
|
|
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
9
|
def get_storage_size() -> str:
|
|
38
|
-
if not FILE_PATH.exists():
|
|
10
|
+
if not c.FILE_PATH.exists():
|
|
39
11
|
return "File not found"
|
|
40
|
-
num_bytes = float(FILE_PATH.stat().st_size)
|
|
12
|
+
num_bytes = float(c.FILE_PATH.stat().st_size)
|
|
41
13
|
for unit in ["B", "KB", "MB", "GB"]:
|
|
42
14
|
if num_bytes < 1024.0:
|
|
43
15
|
return f"{int(num_bytes)} {unit}" if unit == "B" else f"{num_bytes:.2f} {unit}"
|
|
44
16
|
num_bytes /= 1024.0
|
|
45
17
|
return f"{num_bytes:.2f} TB"
|
|
46
18
|
|
|
47
|
-
def _get_connection(filename=FILE_PATH):
|
|
19
|
+
def _get_connection(filename=c.FILE_PATH):
|
|
48
20
|
conn = sqlite3.connect(filename)
|
|
49
21
|
conn.execute("PRAGMA foreign_keys = ON;")
|
|
50
22
|
return conn
|
|
51
23
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
def initialise(filename=FILE_PATH):
|
|
55
|
-
global INITIALISED
|
|
24
|
+
def initialise(filename=c.FILE_PATH, initialised=c.INITIALISED):
|
|
56
25
|
target = str(filename)
|
|
57
|
-
if target in
|
|
26
|
+
if target in initialised:
|
|
58
27
|
return
|
|
59
28
|
with _get_connection(filename) as conn:
|
|
60
29
|
conn.execute("PRAGMA foreign_keys = ON;")
|
|
@@ -100,9 +69,9 @@ def initialise(filename=FILE_PATH):
|
|
|
100
69
|
);
|
|
101
70
|
""")
|
|
102
71
|
conn.execute("PRAGMA user_version = 1;")
|
|
103
|
-
|
|
72
|
+
initialised.add(target)
|
|
104
73
|
|
|
105
|
-
def get_auth(filename=FILE_PATH) -> dict | None:
|
|
74
|
+
def get_auth(filename=c.FILE_PATH) -> dict | None:
|
|
106
75
|
initialise(filename)
|
|
107
76
|
with _get_connection(filename) as conn:
|
|
108
77
|
row = conn.execute("SELECT username, hash, salt, created_at FROM auth WHERE id = 1").fetchone()
|
|
@@ -110,7 +79,7 @@ def get_auth(filename=FILE_PATH) -> dict | None:
|
|
|
110
79
|
return None
|
|
111
80
|
return {"username": row[0], "hash": row[1], "salt": row[2], "created_at": row[3]}
|
|
112
81
|
|
|
113
|
-
def set_auth(username: str, pwd_hash: str, salt: str, filename=FILE_PATH) -> None:
|
|
82
|
+
def set_auth(username: str, pwd_hash: str, salt: str, filename=c.FILE_PATH) -> None:
|
|
114
83
|
initialise(filename)
|
|
115
84
|
now_utc = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
|
116
85
|
with _get_connection(filename) as conn:
|
|
@@ -120,20 +89,20 @@ def set_auth(username: str, pwd_hash: str, salt: str, filename=FILE_PATH) -> Non
|
|
|
120
89
|
VALUES (1, ?, ?, ?, ?)
|
|
121
90
|
""", (username, pwd_hash, salt, now_utc))
|
|
122
91
|
|
|
123
|
-
def clear_auth(filename=FILE_PATH) -> bool:
|
|
92
|
+
def clear_auth(filename=c.FILE_PATH) -> bool:
|
|
124
93
|
initialise(filename)
|
|
125
94
|
with _get_connection(filename) as conn:
|
|
126
95
|
with conn:
|
|
127
96
|
cursor = conn.execute("DELETE FROM auth WHERE id = 1")
|
|
128
97
|
return cursor.rowcount > 0
|
|
129
98
|
|
|
130
|
-
def append_history(cmd: str, timestamp: str, filename=FILE_PATH) -> None:
|
|
99
|
+
def append_history(cmd: str, timestamp: str, filename=c.FILE_PATH) -> None:
|
|
131
100
|
initialise(filename)
|
|
132
101
|
with _get_connection(filename) as conn:
|
|
133
102
|
with conn:
|
|
134
103
|
conn.execute("INSERT INTO cmd_history (command, timestamp) VALUES (?, ?)", (cmd, timestamp))
|
|
135
104
|
|
|
136
|
-
def get_history(limit: int | None = None, filename=FILE_PATH) -> list[dict]:
|
|
105
|
+
def get_history(limit: int | None = None, filename=c.FILE_PATH) -> list[dict]:
|
|
137
106
|
initialise(filename)
|
|
138
107
|
with _get_connection(filename) as conn:
|
|
139
108
|
if limit is not None and limit > 0:
|
|
@@ -145,14 +114,14 @@ def get_history(limit: int | None = None, filename=FILE_PATH) -> list[dict]:
|
|
|
145
114
|
rows = conn.execute(query).fetchall()
|
|
146
115
|
return [{"command": r[0], "timestamp": r[1]} for r in rows]
|
|
147
116
|
|
|
148
|
-
def clear_history(filename=FILE_PATH) -> bool:
|
|
117
|
+
def clear_history(filename=c.FILE_PATH) -> bool:
|
|
149
118
|
initialise(filename)
|
|
150
119
|
with _get_connection(filename) as conn:
|
|
151
120
|
with conn:
|
|
152
121
|
conn.execute("DELETE FROM cmd_history")
|
|
153
122
|
return True
|
|
154
123
|
|
|
155
|
-
def get_config(key: str, default=None, filename=FILE_PATH):
|
|
124
|
+
def get_config(key: str, default=None, filename=c.FILE_PATH):
|
|
156
125
|
initialise(filename)
|
|
157
126
|
with _get_connection(filename) as conn:
|
|
158
127
|
row = conn.execute("SELECT value FROM core_config WHERE key = ?", (key,)).fetchone()
|
|
@@ -163,7 +132,7 @@ def get_config(key: str, default=None, filename=FILE_PATH):
|
|
|
163
132
|
except (ValueError, TypeError):
|
|
164
133
|
return row[0]
|
|
165
134
|
|
|
166
|
-
def set_config(key: str, value, filename=FILE_PATH) -> None:
|
|
135
|
+
def set_config(key: str, value, filename=c.FILE_PATH) -> None:
|
|
167
136
|
initialise(filename)
|
|
168
137
|
serialized = json.dumps(value)
|
|
169
138
|
with _get_connection(filename) as conn:
|
|
@@ -173,14 +142,14 @@ def set_config(key: str, value, filename=FILE_PATH) -> None:
|
|
|
173
142
|
VALUES (?, ?)
|
|
174
143
|
""", (key, serialized))
|
|
175
144
|
|
|
176
|
-
def delete_config(key: str, filename=FILE_PATH) -> bool:
|
|
145
|
+
def delete_config(key: str, filename=c.FILE_PATH) -> bool:
|
|
177
146
|
initialise(filename)
|
|
178
147
|
with _get_connection(filename) as conn:
|
|
179
148
|
with conn:
|
|
180
149
|
cursor = conn.execute("DELETE FROM core_config WHERE key = ?", (key,))
|
|
181
150
|
return cursor.rowcount > 0
|
|
182
151
|
|
|
183
|
-
def register_plugin(plugin_name: str, filename=FILE_PATH) -> None:
|
|
152
|
+
def register_plugin(plugin_name: str, filename=c.FILE_PATH) -> None:
|
|
184
153
|
initialise(filename)
|
|
185
154
|
now_utc = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
|
186
155
|
with _get_connection(filename) as conn:
|
|
@@ -190,7 +159,7 @@ def register_plugin(plugin_name: str, filename=FILE_PATH) -> None:
|
|
|
190
159
|
VALUES (?, ?, 1)
|
|
191
160
|
""", (plugin_name, now_utc))
|
|
192
161
|
|
|
193
|
-
def get_plugin_data(plugin_name: str, key: str, default=None, filename=FILE_PATH):
|
|
162
|
+
def get_plugin_data(plugin_name: str, key: str, default=None, filename=c.FILE_PATH):
|
|
194
163
|
initialise(filename)
|
|
195
164
|
with _get_connection(filename) as conn:
|
|
196
165
|
row = conn.execute("""
|
|
@@ -203,7 +172,7 @@ def get_plugin_data(plugin_name: str, key: str, default=None, filename=FILE_PATH
|
|
|
203
172
|
except (ValueError, TypeError):
|
|
204
173
|
return row[0]
|
|
205
174
|
|
|
206
|
-
def set_plugin_data(plugin_name: str, key: str, value, filename=FILE_PATH) -> None:
|
|
175
|
+
def set_plugin_data(plugin_name: str, key: str, value, filename=c.FILE_PATH) -> None:
|
|
207
176
|
initialise(filename)
|
|
208
177
|
register_plugin(plugin_name, filename=filename)
|
|
209
178
|
serialized = json.dumps(value)
|
|
@@ -214,7 +183,7 @@ def set_plugin_data(plugin_name: str, key: str, value, filename=FILE_PATH) -> No
|
|
|
214
183
|
VALUES (?, ?, ?)
|
|
215
184
|
""", (plugin_name, key, serialized))
|
|
216
185
|
|
|
217
|
-
def get_all_plugin_data(plugin_name: str, filename=FILE_PATH) -> dict:
|
|
186
|
+
def get_all_plugin_data(plugin_name: str, filename=c.FILE_PATH) -> dict:
|
|
218
187
|
initialise(filename)
|
|
219
188
|
with _get_connection(filename) as conn:
|
|
220
189
|
rows = conn.execute("SELECT key, value FROM plugin_kv WHERE plugin_name = ?", (plugin_name,)).fetchall()
|
|
@@ -226,7 +195,7 @@ def get_all_plugin_data(plugin_name: str, filename=FILE_PATH) -> dict:
|
|
|
226
195
|
result[k] = v
|
|
227
196
|
return result
|
|
228
197
|
|
|
229
|
-
def purge_plugin(plugin_name: str, filename=FILE_PATH) -> bool:
|
|
198
|
+
def purge_plugin(plugin_name: str, filename=c.FILE_PATH) -> bool:
|
|
230
199
|
initialise(filename)
|
|
231
200
|
with _get_connection(filename) as conn:
|
|
232
201
|
with conn:
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# src\flyshell\core\directory.py
|
|
2
2
|
|
|
3
|
+
from flyshell import constants as c
|
|
3
4
|
from flyshell.abstract.base_cmd import BaseCmd
|
|
4
5
|
from flyshell.abstract.parent_cmd import ParentCmd
|
|
5
6
|
from flyshell.builtin import fs, system, utils
|
|
@@ -60,7 +61,7 @@ def log(cmd):
|
|
|
60
61
|
|
|
61
62
|
def _execute_single(raw_cmd: str) -> int:
|
|
62
63
|
try:
|
|
63
|
-
posix_mode = False if
|
|
64
|
+
posix_mode = False if c.HOST_OS == "Windows" else True
|
|
64
65
|
cmd = shlex.split(raw_cmd, posix=posix_mode)
|
|
65
66
|
except ValueError as e:
|
|
66
67
|
print("\nCommand Error: Invalid syntax.")
|
|
@@ -77,8 +78,8 @@ def _execute_single(raw_cmd: str) -> int:
|
|
|
77
78
|
return command.execute(args)
|
|
78
79
|
command.help()
|
|
79
80
|
return 0
|
|
80
|
-
elif cmd_name in
|
|
81
|
-
plugin =
|
|
81
|
+
elif cmd_name in c.PLUGINS:
|
|
82
|
+
plugin = c.PLUGINS[cmd_name]
|
|
82
83
|
if hasattr(plugin, "help"):
|
|
83
84
|
plugin.help()
|
|
84
85
|
print()
|
|
@@ -87,8 +88,8 @@ def _execute_single(raw_cmd: str) -> int:
|
|
|
87
88
|
print(f"\nPlugin Error: Plugin '{cmd_name}' does not provide help details.\n")
|
|
88
89
|
return 1
|
|
89
90
|
if "-d" in args or "--docs" in args:
|
|
90
|
-
if cmd_name in
|
|
91
|
-
plugin =
|
|
91
|
+
if cmd_name in c.PLUGINS:
|
|
92
|
+
plugin = c.PLUGINS[cmd_name]
|
|
92
93
|
if hasattr(plugin, "show_docs") and plugin.show_docs():
|
|
93
94
|
return 0
|
|
94
95
|
print(f"\nPlugin Error: Plugin '{cmd_name}' has not provided documentation.\n")
|
|
@@ -106,7 +107,7 @@ def _execute_single(raw_cmd: str) -> int:
|
|
|
106
107
|
print(f"\nCommand Error: Failed during execution of '{cmd_name}'.")
|
|
107
108
|
print(f"Details: {e}\n")
|
|
108
109
|
return 1
|
|
109
|
-
|
|
110
|
+
c.SESSION_CMD_COUNT += 1
|
|
110
111
|
if isinstance(result, tuple):
|
|
111
112
|
code, message = result
|
|
112
113
|
if message:
|
|
@@ -119,9 +120,9 @@ def _execute_single(raw_cmd: str) -> int:
|
|
|
119
120
|
print(result)
|
|
120
121
|
return 0
|
|
121
122
|
return 0
|
|
122
|
-
elif cmd_name in
|
|
123
|
+
elif cmd_name in c.PLUGINS:
|
|
123
124
|
log(raw_cmd)
|
|
124
|
-
plugin =
|
|
125
|
+
plugin = c.PLUGINS[cmd_name]
|
|
125
126
|
try:
|
|
126
127
|
result = plugin.execute(args)
|
|
127
128
|
return result if isinstance(result, int) else 0
|
|
@@ -156,7 +157,7 @@ def _parse_commands(raw_cmd: str) -> list:
|
|
|
156
157
|
escape = False
|
|
157
158
|
i += 1
|
|
158
159
|
continue
|
|
159
|
-
is_windows = (
|
|
160
|
+
is_windows = (c.HOST_OS == "Windows")
|
|
160
161
|
if char == "\\":
|
|
161
162
|
if not is_windows:
|
|
162
163
|
escape = True
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# src\flyshell\core\loader.py
|
|
2
2
|
|
|
3
|
+
from flyshell import constants as c
|
|
3
4
|
from flyshell.abstract.base_plugin import BasePlugin
|
|
4
5
|
from flyshell.core import data
|
|
5
6
|
from flyshell.core.directory import COMMAND_REGISTRY
|
|
@@ -10,11 +11,9 @@ import inspect
|
|
|
10
11
|
import os
|
|
11
12
|
import sys
|
|
12
13
|
|
|
13
|
-
RESERVED_NAMES = set(COMMAND_REGISTRY.keys())
|
|
14
|
-
|
|
15
14
|
def scan_plugins(plugin_folder=None):
|
|
16
|
-
folder = Path(plugin_folder) if plugin_folder else
|
|
17
|
-
|
|
15
|
+
folder = Path(plugin_folder) if plugin_folder else c.USER_PLUGIN_DIR
|
|
16
|
+
c.PLUGINS.clear()
|
|
18
17
|
if not folder.exists():
|
|
19
18
|
folder.mkdir(parents=True, exist_ok=True)
|
|
20
19
|
print("\nNo plugins found.")
|
|
@@ -53,7 +52,7 @@ def scan_plugins(plugin_folder=None):
|
|
|
53
52
|
first_print = True
|
|
54
53
|
for name, entries in candidates.items():
|
|
55
54
|
failed = False
|
|
56
|
-
if name in RESERVED_NAMES:
|
|
55
|
+
if name in c.RESERVED_NAMES:
|
|
57
56
|
print(f"\n⚠️ Collision Error - Plugin '{name}' conflicts with a built-in Flyshell command and will not be loaded.")
|
|
58
57
|
failed = True
|
|
59
58
|
if len(entries) > 1:
|
|
@@ -64,7 +63,7 @@ def scan_plugins(plugin_folder=None):
|
|
|
64
63
|
if failed:
|
|
65
64
|
continue
|
|
66
65
|
instance, _ = entries[0]
|
|
67
|
-
|
|
66
|
+
c.PLUGINS[name] = instance
|
|
68
67
|
if first_print:
|
|
69
68
|
print("\nInstalled Plugins:")
|
|
70
69
|
first_print = False
|
|
@@ -81,9 +80,9 @@ def build_context(plugin_name, file_path=None):
|
|
|
81
80
|
"plugin_name": plugin_name,
|
|
82
81
|
"file_path": str(file_path) if file_path else None,
|
|
83
82
|
"metadata": {
|
|
84
|
-
"host_os":
|
|
85
|
-
"version":
|
|
86
|
-
"build":
|
|
83
|
+
"host_os": c.HOST_OS,
|
|
84
|
+
"version": c.VERSION,
|
|
85
|
+
"build": c.BUILD
|
|
87
86
|
},
|
|
88
87
|
"storage": plugin_storage
|
|
89
88
|
}
|
|
@@ -1,40 +1,20 @@
|
|
|
1
1
|
# src\flyshell\main.py
|
|
2
2
|
|
|
3
|
+
from flyshell import constants as c
|
|
4
|
+
|
|
3
5
|
from pathlib import Path
|
|
4
6
|
import os
|
|
5
7
|
import sys
|
|
6
8
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
PROJECT_ROOT = SRC_DIR.parent
|
|
10
|
-
|
|
11
|
-
if str(SRC_DIR) not in sys.path:
|
|
12
|
-
sys.path.insert(0, str(SRC_DIR))
|
|
9
|
+
if str(c.SRC_DIR) not in sys.path:
|
|
10
|
+
sys.path.insert(0, str(c.SRC_DIR))
|
|
13
11
|
|
|
14
12
|
def boot_check():
|
|
15
13
|
print("\nFlyshell will now check that your modules exist.")
|
|
16
|
-
abstract_dir = PACKAGE_ROOT / "abstract"
|
|
17
|
-
builtin_dir = PACKAGE_ROOT / "builtin"
|
|
18
|
-
core_dir = PACKAGE_ROOT / "core"
|
|
19
|
-
REQUIRED_COMPONENTS = [
|
|
20
|
-
abstract_dir,
|
|
21
|
-
abstract_dir / "base_cmd.py",
|
|
22
|
-
abstract_dir / "base_plugin.py",
|
|
23
|
-
abstract_dir / "parent_cmd.py",
|
|
24
|
-
builtin_dir,
|
|
25
|
-
builtin_dir / "fs.py",
|
|
26
|
-
builtin_dir / "system.py",
|
|
27
|
-
builtin_dir / "utils.py",
|
|
28
|
-
core_dir,
|
|
29
|
-
core_dir / "auth.py",
|
|
30
|
-
core_dir / "console.py",
|
|
31
|
-
core_dir / "data.py",
|
|
32
|
-
core_dir / "directory.py",
|
|
33
|
-
core_dir / "loader.py",
|
|
34
|
-
]
|
|
35
14
|
present = []
|
|
36
15
|
missing = []
|
|
37
|
-
for
|
|
16
|
+
for obj in c.REQUIRED_COMPONENTS:
|
|
17
|
+
path = Path(c.PACKAGE_ROOT / obj)
|
|
38
18
|
if path.exists():
|
|
39
19
|
present.append(str(path))
|
|
40
20
|
else:
|
|
@@ -47,7 +27,7 @@ def boot_check():
|
|
|
47
27
|
print(f"❌ {i} - FILE NOT FOUND")
|
|
48
28
|
if missing:
|
|
49
29
|
return False
|
|
50
|
-
tests_dir = PROJECT_ROOT / "tests"
|
|
30
|
+
tests_dir = c.PROJECT_ROOT / "tests"
|
|
51
31
|
if tests_dir.exists():
|
|
52
32
|
print("\nFlyshell will now check internal additional packages and features. These are not required.")
|
|
53
33
|
for path in tests_dir.glob("test_*.py"):
|
|
@@ -67,17 +47,17 @@ def boot_check():
|
|
|
67
47
|
|
|
68
48
|
def main():
|
|
69
49
|
os.system("")
|
|
70
|
-
print(
|
|
50
|
+
print(c.CLEAR_SCREEN, end="")
|
|
71
51
|
print("\nLaunch process initiated. Flyshell is checking it is able to launch...")
|
|
72
52
|
try:
|
|
73
53
|
if boot_check():
|
|
74
54
|
from flyshell.core import auth, console, data, loader
|
|
75
|
-
if
|
|
76
|
-
if sys.version_info <
|
|
55
|
+
if c.HOST_OS in ["Windows", "Darwin", "Linux"]:
|
|
56
|
+
if sys.version_info < c.SUPPORTED_PYTHON:
|
|
77
57
|
print("\nCRITICAL ERROR: Your Python version is too old to be supported by Flyshell.")
|
|
78
58
|
print("Flyshell cannot launch because it requires Python 3.10 or newer.")
|
|
79
59
|
sys.exit(1)
|
|
80
|
-
print(f"\nYour OS '{
|
|
60
|
+
print(f"\nYour OS '{c.HOST_OS}' is compatible with Flyshell.")
|
|
81
61
|
print("Flyshell will now boot.")
|
|
82
62
|
loader.scan_plugins()
|
|
83
63
|
if auth.login_flow():
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# \tests\test_loader.py
|
|
2
2
|
|
|
3
|
+
from flyshell import constants as c
|
|
3
4
|
from flyshell.abstract.base_plugin import BasePlugin
|
|
4
|
-
from flyshell.core import
|
|
5
|
+
from flyshell.core import loader
|
|
5
6
|
|
|
6
7
|
def test_loader_rejects_command_name_collision(tmp_path, monkeypatch):
|
|
7
8
|
bad_plugin = tmp_path / "sys.py"
|
|
@@ -10,9 +11,9 @@ def test_loader_rejects_command_name_collision(tmp_path, monkeypatch):
|
|
|
10
11
|
"class SysPlugin(BasePlugin):\n"
|
|
11
12
|
" def execute(self, args): pass\n"
|
|
12
13
|
)
|
|
13
|
-
monkeypatch.setattr(
|
|
14
|
+
monkeypatch.setattr(c, "USER_PLUGIN_DIR", tmp_path)
|
|
14
15
|
loader.scan_plugins(plugin_folder=tmp_path)
|
|
15
|
-
assert "sys" not in
|
|
16
|
+
assert "sys" not in c.PLUGINS
|
|
16
17
|
|
|
17
18
|
def test_loader_discovers_and_instantiates_valid_plugin(tmp_path, monkeypatch):
|
|
18
19
|
valid_plugin = tmp_path / "sample.py"
|
|
@@ -24,16 +25,16 @@ def test_loader_discovers_and_instantiates_valid_plugin(tmp_path, monkeypatch):
|
|
|
24
25
|
" def execute(self, args):\n"
|
|
25
26
|
" return (0, 'done')\n"
|
|
26
27
|
)
|
|
27
|
-
monkeypatch.setattr(
|
|
28
|
+
monkeypatch.setattr(c, "USER_PLUGIN_DIR", tmp_path)
|
|
28
29
|
loader.scan_plugins(plugin_folder=tmp_path)
|
|
29
|
-
assert "sample" in
|
|
30
|
-
plugin_instance =
|
|
30
|
+
assert "sample" in c.PLUGINS
|
|
31
|
+
plugin_instance = c.PLUGINS["sample"]
|
|
31
32
|
assert isinstance(plugin_instance, BasePlugin)
|
|
32
33
|
assert plugin_instance.name == "Sample Tool"
|
|
33
34
|
|
|
34
35
|
def test_loader_handles_broken_plugin_gracefully(tmp_path, monkeypatch):
|
|
35
36
|
broken_plugin = tmp_path / "broken.py"
|
|
36
37
|
broken_plugin.write_text("import non_existent_library_12345\n")
|
|
37
|
-
monkeypatch.setattr(
|
|
38
|
+
monkeypatch.setattr(c, "USER_PLUGIN_DIR", tmp_path)
|
|
38
39
|
loader.scan_plugins(plugin_folder=tmp_path)
|
|
39
|
-
assert "broken" not in
|
|
40
|
+
assert "broken" not in c.PLUGINS
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|