assayfluxos 0.1.0__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.
@@ -0,0 +1,17 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 AssayFluxOS contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
@@ -0,0 +1,195 @@
1
+ Metadata-Version: 2.4
2
+ Name: assayfluxos
3
+ Version: 0.1.0
4
+ Summary: A thoughtfully designed Python shell and FluxScript workspace for Termux, Android-oriented workflows, and desktop Python.
5
+ Author: AssayFluxOS contributors
6
+ License: MIT
7
+ Keywords: assayfluxos,shell,terminal,termux,android,fluxscript,interpreter,workspace,developer-tools,python
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Environment :: Console
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Software Development :: Interpreters
19
+ Classifier: Topic :: System :: Shells
20
+ Classifier: Topic :: Utilities
21
+ Requires-Python: >=3.10
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Dynamic: license-file
25
+
26
+ # AssayFluxOS 0.1.0
27
+
28
+ **A calm, capable command-line workspace for Python, Termux, and the emerging FluxScript language.**
29
+
30
+ AssayFluxOS brings everyday file work, a persistent project workspace, familiar full-word commands, and a small expressive scripting language into one approachable environment. It is designed for people who want to organise projects and experiment with a custom scripting workflow from a Python-capable terminal—including Termux on Android—without pretending that an early CLI is already a complete mobile operating system.
31
+
32
+ > **Release status:** 0.1.0 is an alpha foundation. It provides a Python command-line application and an early FluxScript interpreter. A native Android APK/AAB, graphical terminal/editor, and some advanced language features are planned separately and are **not included** in this Python package.
33
+
34
+ ## Highlights
35
+
36
+ - **Readable commands:** prefer `copy`, `move`, `rename`, `delete`, `directory`, and `change directory` instead of requiring cryptic abbreviations.
37
+ - **A home for your work:** creates a structured `AssayFluxOS Files` workspace with project, document, data, script, module, plugin, download, log, backup, and configuration folders.
38
+ - **FluxScript starter language:** variables, output and input, conditions, loops, functions, return/stop, collections, indexing, arithmetic, comparisons, and boolean expressions.
39
+ - **Persistent preferences:** settings, aliases, and command history are stored in the workspace.
40
+ - **Useful terminal workflows:** basic pipelines, output redirection, text search, file properties, and workspace tree inspection.
41
+ - **Extension point:** trusted Python plugins can register additional commands.
42
+ - **Backup tools:** create and restore workspace archives with path checks and a confirmation step.
43
+ - **Python integration:** run trusted Python scripts from the CLI when appropriate.
44
+ - **No mandatory runtime dependencies:** the core package uses the Python standard library.
45
+
46
+ ## Install
47
+
48
+ Python 3.10 or newer is required.
49
+
50
+ ```bash
51
+ python -m pip install assayfluxos
52
+ ```
53
+
54
+ Start the shell with either command:
55
+
56
+ ```bash
57
+ assayfluxos
58
+ # or the short alias
59
+ af
60
+ ```
61
+
62
+ See command-line options:
63
+
64
+ ```bash
65
+ assayfluxos --help
66
+ ```
67
+
68
+ ### Install from a downloaded source project
69
+
70
+ From the project directory:
71
+
72
+ ```bash
73
+ python -m pip install .
74
+ ```
75
+
76
+ ## Android / Termux workspace
77
+
78
+ In Termux, enable Android shared-storage access when prompted:
79
+
80
+ ```bash
81
+ termux-setup-storage
82
+ ```
83
+
84
+ AssayFluxOS attempts to use `/storage/emulated/0/AssayFluxOS Files` when the shared-storage parent is writable. Otherwise it falls back to `~/AssayFluxOS Files`. You can explicitly choose a workspace where supported with `--workspace PATH`.
85
+
86
+ The workspace initialisation creates these folders when missing and does not intentionally erase existing content:
87
+
88
+ ```text
89
+ AssayFluxOS Files/
90
+ ├── Projects/
91
+ │ ├── Python/
92
+ │ ├── FluxScript/
93
+ │ ├── Web/
94
+ │ └── Experiments/
95
+ ├── Documents/
96
+ ├── Data/
97
+ ├── Scripts/
98
+ ├── Modules/
99
+ ├── Downloads/
100
+ ├── Logs/
101
+ ├── Backups/
102
+ ├── Config/
103
+ └── Plugins/
104
+ ```
105
+
106
+ For a future native Android app, the recommended design is to use Android's Storage Access Framework for user-selected directories rather than assuming unrestricted filesystem access.
107
+
108
+ ## Everyday commands
109
+
110
+ Examples (the exact command grammar is documented in the CLI help and source):
111
+
112
+ ```text
113
+ directory
114
+ change directory to Projects
115
+ create file "hello world.flux"
116
+ write file "hello world.flux"
117
+ read file "hello world.flux"
118
+ copy file "hello world.flux" to "hello backup.flux"
119
+ move "hello backup.flux" to Backups
120
+ rename file old.txt new.txt
121
+ search hello
122
+ findtext "Welcome" Projects
123
+ properties "hello world.flux"
124
+ tree
125
+ ```
126
+
127
+ Longer, natural command forms are supported where implemented. Full-word forms are preferred for readability.
128
+
129
+ ## FluxScript quick start
130
+
131
+ ```text
132
+ let items = ["alpha", "beta"]
133
+
134
+ function greet(name)
135
+ say "Hello, " + name
136
+ end
137
+
138
+ call greet("Researcher")
139
+
140
+ loop 2
141
+ say items[0]
142
+ end
143
+
144
+ if 2 > 1
145
+ say "The expression is true"
146
+ else
147
+ say "The expression is false"
148
+ end
149
+ ```
150
+
151
+ ### Current language scope
152
+
153
+ The implemented subset includes `say`, `ask`, `let`, `if/else/end`, `loop/end`, `while/end`, `function/end`, `call`, `return`, and `stop`, along with common literals, lists, tuples, dictionaries, indexing, arithmetic, comparisons, and boolean operations. Expression handling uses a restricted Python AST evaluator; it does not call Python `eval()` or `exec()` for FluxScript expressions.
154
+
155
+ User-defined classes, asynchronous tasks, module imports (`use`), exception blocks, and general operating-system APIs are not implemented in this release. Execution limits help guard against accidental runaway scripts, but **FluxScript is not a security sandbox for hostile code**.
156
+
157
+ ## Aliases, settings, history, and plugins
158
+
159
+ - `set NAME = VALUE` stores supported JSON-compatible settings in `Config/settings.json`.
160
+ - `alias ll = directory` creates a persistent command alias; `unalias ll` removes one.
161
+ - Command history is written to `Logs/history.txt`.
162
+ - Trusted Python plugins in `Plugins/` define `register(registry)` and may register commands using `registry.add(name, handler, help_text)`.
163
+
164
+ **Security note:** Python scripts and plugins execute with the permissions of the current user. Only run code you trust. Keep backups of important data. Restore operations can overwrite existing files even though archive paths are validated and links/devices are rejected.
165
+
166
+ ## Test and develop
167
+
168
+ From the project root:
169
+
170
+ ```bash
171
+ python -m pip install -e .
172
+ python -m unittest discover -s tests -v
173
+ python -m compileall -q src tests tools
174
+ python tools/assayflux_preflight.py
175
+ ```
176
+
177
+ The preflight helper checks for an installed `assayflux` command and uses its available version/help/doctor interfaces. It does not assume the external CLI has an Android packaging feature.
178
+
179
+ ## Roadmap
180
+
181
+ 1. Stabilise the CLI and expand automated tests.
182
+ 2. Extend FluxScript with carefully designed modules, classes, and async tasks.
183
+ 3. Build a native Android application shell with a terminal, file browser, and graphical editor.
184
+ 4. Integrate Android storage selection and manage runtime lifecycle correctly.
185
+ 5. Build and test APK/AAB artifacts on real devices before calling them production-ready.
186
+
187
+ See `ANDROID_ROADMAP.md`, `android/README.md`, and `docs/FLUXSCRIPT_REFERENCE.md` for more details.
188
+
189
+ ## Publishing
190
+
191
+ A Google Colab notebook is provided separately to run tests, build source and wheel distributions, validate metadata, and upload to PyPI using a token entered securely at runtime. Never commit or share a PyPI token. PyPI does not allow replacing an already-published file for the same project version; if `assayfluxos==0.1.0` already exists on PyPI, increment the version before publishing.
192
+
193
+ ## License
194
+
195
+ MIT. See `LICENSE`.
@@ -0,0 +1,170 @@
1
+ # AssayFluxOS 0.1.0
2
+
3
+ **A calm, capable command-line workspace for Python, Termux, and the emerging FluxScript language.**
4
+
5
+ AssayFluxOS brings everyday file work, a persistent project workspace, familiar full-word commands, and a small expressive scripting language into one approachable environment. It is designed for people who want to organise projects and experiment with a custom scripting workflow from a Python-capable terminal—including Termux on Android—without pretending that an early CLI is already a complete mobile operating system.
6
+
7
+ > **Release status:** 0.1.0 is an alpha foundation. It provides a Python command-line application and an early FluxScript interpreter. A native Android APK/AAB, graphical terminal/editor, and some advanced language features are planned separately and are **not included** in this Python package.
8
+
9
+ ## Highlights
10
+
11
+ - **Readable commands:** prefer `copy`, `move`, `rename`, `delete`, `directory`, and `change directory` instead of requiring cryptic abbreviations.
12
+ - **A home for your work:** creates a structured `AssayFluxOS Files` workspace with project, document, data, script, module, plugin, download, log, backup, and configuration folders.
13
+ - **FluxScript starter language:** variables, output and input, conditions, loops, functions, return/stop, collections, indexing, arithmetic, comparisons, and boolean expressions.
14
+ - **Persistent preferences:** settings, aliases, and command history are stored in the workspace.
15
+ - **Useful terminal workflows:** basic pipelines, output redirection, text search, file properties, and workspace tree inspection.
16
+ - **Extension point:** trusted Python plugins can register additional commands.
17
+ - **Backup tools:** create and restore workspace archives with path checks and a confirmation step.
18
+ - **Python integration:** run trusted Python scripts from the CLI when appropriate.
19
+ - **No mandatory runtime dependencies:** the core package uses the Python standard library.
20
+
21
+ ## Install
22
+
23
+ Python 3.10 or newer is required.
24
+
25
+ ```bash
26
+ python -m pip install assayfluxos
27
+ ```
28
+
29
+ Start the shell with either command:
30
+
31
+ ```bash
32
+ assayfluxos
33
+ # or the short alias
34
+ af
35
+ ```
36
+
37
+ See command-line options:
38
+
39
+ ```bash
40
+ assayfluxos --help
41
+ ```
42
+
43
+ ### Install from a downloaded source project
44
+
45
+ From the project directory:
46
+
47
+ ```bash
48
+ python -m pip install .
49
+ ```
50
+
51
+ ## Android / Termux workspace
52
+
53
+ In Termux, enable Android shared-storage access when prompted:
54
+
55
+ ```bash
56
+ termux-setup-storage
57
+ ```
58
+
59
+ AssayFluxOS attempts to use `/storage/emulated/0/AssayFluxOS Files` when the shared-storage parent is writable. Otherwise it falls back to `~/AssayFluxOS Files`. You can explicitly choose a workspace where supported with `--workspace PATH`.
60
+
61
+ The workspace initialisation creates these folders when missing and does not intentionally erase existing content:
62
+
63
+ ```text
64
+ AssayFluxOS Files/
65
+ ├── Projects/
66
+ │ ├── Python/
67
+ │ ├── FluxScript/
68
+ │ ├── Web/
69
+ │ └── Experiments/
70
+ ├── Documents/
71
+ ├── Data/
72
+ ├── Scripts/
73
+ ├── Modules/
74
+ ├── Downloads/
75
+ ├── Logs/
76
+ ├── Backups/
77
+ ├── Config/
78
+ └── Plugins/
79
+ ```
80
+
81
+ For a future native Android app, the recommended design is to use Android's Storage Access Framework for user-selected directories rather than assuming unrestricted filesystem access.
82
+
83
+ ## Everyday commands
84
+
85
+ Examples (the exact command grammar is documented in the CLI help and source):
86
+
87
+ ```text
88
+ directory
89
+ change directory to Projects
90
+ create file "hello world.flux"
91
+ write file "hello world.flux"
92
+ read file "hello world.flux"
93
+ copy file "hello world.flux" to "hello backup.flux"
94
+ move "hello backup.flux" to Backups
95
+ rename file old.txt new.txt
96
+ search hello
97
+ findtext "Welcome" Projects
98
+ properties "hello world.flux"
99
+ tree
100
+ ```
101
+
102
+ Longer, natural command forms are supported where implemented. Full-word forms are preferred for readability.
103
+
104
+ ## FluxScript quick start
105
+
106
+ ```text
107
+ let items = ["alpha", "beta"]
108
+
109
+ function greet(name)
110
+ say "Hello, " + name
111
+ end
112
+
113
+ call greet("Researcher")
114
+
115
+ loop 2
116
+ say items[0]
117
+ end
118
+
119
+ if 2 > 1
120
+ say "The expression is true"
121
+ else
122
+ say "The expression is false"
123
+ end
124
+ ```
125
+
126
+ ### Current language scope
127
+
128
+ The implemented subset includes `say`, `ask`, `let`, `if/else/end`, `loop/end`, `while/end`, `function/end`, `call`, `return`, and `stop`, along with common literals, lists, tuples, dictionaries, indexing, arithmetic, comparisons, and boolean operations. Expression handling uses a restricted Python AST evaluator; it does not call Python `eval()` or `exec()` for FluxScript expressions.
129
+
130
+ User-defined classes, asynchronous tasks, module imports (`use`), exception blocks, and general operating-system APIs are not implemented in this release. Execution limits help guard against accidental runaway scripts, but **FluxScript is not a security sandbox for hostile code**.
131
+
132
+ ## Aliases, settings, history, and plugins
133
+
134
+ - `set NAME = VALUE` stores supported JSON-compatible settings in `Config/settings.json`.
135
+ - `alias ll = directory` creates a persistent command alias; `unalias ll` removes one.
136
+ - Command history is written to `Logs/history.txt`.
137
+ - Trusted Python plugins in `Plugins/` define `register(registry)` and may register commands using `registry.add(name, handler, help_text)`.
138
+
139
+ **Security note:** Python scripts and plugins execute with the permissions of the current user. Only run code you trust. Keep backups of important data. Restore operations can overwrite existing files even though archive paths are validated and links/devices are rejected.
140
+
141
+ ## Test and develop
142
+
143
+ From the project root:
144
+
145
+ ```bash
146
+ python -m pip install -e .
147
+ python -m unittest discover -s tests -v
148
+ python -m compileall -q src tests tools
149
+ python tools/assayflux_preflight.py
150
+ ```
151
+
152
+ The preflight helper checks for an installed `assayflux` command and uses its available version/help/doctor interfaces. It does not assume the external CLI has an Android packaging feature.
153
+
154
+ ## Roadmap
155
+
156
+ 1. Stabilise the CLI and expand automated tests.
157
+ 2. Extend FluxScript with carefully designed modules, classes, and async tasks.
158
+ 3. Build a native Android application shell with a terminal, file browser, and graphical editor.
159
+ 4. Integrate Android storage selection and manage runtime lifecycle correctly.
160
+ 5. Build and test APK/AAB artifacts on real devices before calling them production-ready.
161
+
162
+ See `ANDROID_ROADMAP.md`, `android/README.md`, and `docs/FLUXSCRIPT_REFERENCE.md` for more details.
163
+
164
+ ## Publishing
165
+
166
+ A Google Colab notebook is provided separately to run tests, build source and wheel distributions, validate metadata, and upload to PyPI using a token entered securely at runtime. Never commit or share a PyPI token. PyPI does not allow replacing an already-published file for the same project version; if `assayfluxos==0.1.0` already exists on PyPI, increment the version before publishing.
167
+
168
+ ## License
169
+
170
+ MIT. See `LICENSE`.
@@ -0,0 +1,39 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "assayfluxos"
7
+ version = "0.1.0"
8
+ description = "A thoughtfully designed Python shell and FluxScript workspace for Termux, Android-oriented workflows, and desktop Python."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "AssayFluxOS contributors" }]
13
+ keywords = ["assayfluxos", "shell", "terminal", "termux", "android", "fluxscript", "interpreter", "workspace", "developer-tools", "python"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Environment :: Console",
17
+ "Intended Audience :: Developers",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Operating System :: OS Independent",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.10",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ "Programming Language :: Python :: 3.13",
25
+ "Topic :: Software Development :: Interpreters",
26
+ "Topic :: System :: Shells",
27
+ "Topic :: Utilities",
28
+ ]
29
+ dependencies = []
30
+
31
+ [project.scripts]
32
+ assayfluxos = "assayfluxos.cli:main"
33
+ af = "assayfluxos.cli:main"
34
+
35
+ [tool.setuptools.packages.find]
36
+ where = ["src"]
37
+
38
+ [tool.pytest.ini_options]
39
+ testpaths = ["tests"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,2 @@
1
+ """AssayFluxOS: a custom Python shell and FluxScript workspace."""
2
+ __version__ = "0.1.0"
@@ -0,0 +1,13 @@
1
+ from __future__ import annotations
2
+ import argparse
3
+ from .shell import AssayFluxShell
4
+
5
+ def main() -> None:
6
+ parser = argparse.ArgumentParser(prog="assayfluxos", description="AssayFluxOS custom Python shell")
7
+ parser.add_argument("--workspace", help="Use a specific workspace directory")
8
+ args = parser.parse_args()
9
+ shell = AssayFluxShell(workspace=args.workspace)
10
+ shell.loop()
11
+
12
+ if __name__ == "__main__":
13
+ main()
@@ -0,0 +1,24 @@
1
+ from __future__ import annotations
2
+ import json
3
+ from pathlib import Path
4
+
5
+ class Settings:
6
+ """Small JSON settings store. Invalid settings are preserved and safely ignored."""
7
+ def __init__(self, workspace: Path):
8
+ self.path = workspace / "Config" / "settings.json"
9
+ self.path.parent.mkdir(parents=True, exist_ok=True)
10
+ self.data = {"theme": "dark", "aliases": {}, "variables": {}, "plugins_enabled": True}
11
+ try:
12
+ loaded = json.loads(self.path.read_text(encoding="utf-8"))
13
+ if isinstance(loaded, dict):
14
+ self.data.update(loaded)
15
+ except (OSError, ValueError):
16
+ pass
17
+ if not isinstance(self.data.get("aliases"), dict): self.data["aliases"] = {}
18
+ if not isinstance(self.data.get("variables"), dict): self.data["variables"] = {}
19
+
20
+ def save(self):
21
+ self.path.parent.mkdir(parents=True, exist_ok=True)
22
+ tmp = self.path.with_suffix(".json.tmp")
23
+ tmp.write_text(json.dumps(self.data, ensure_ascii=False, indent=2), encoding="utf-8")
24
+ tmp.replace(self.path)
@@ -0,0 +1,131 @@
1
+ from __future__ import annotations
2
+ import os
3
+ import shutil
4
+ from pathlib import Path
5
+
6
+ MASTER_FOLDER = "AssayFluxOS Files"
7
+ DEFAULT_FOLDERS = ("Projects/Python", "Projects/FluxScript", "Projects/Web", "Projects/Experiments", "Documents", "Data", "Scripts", "Modules", "Downloads", "Logs", "Backups", "Config", "Plugins")
8
+
9
+ class Workspace:
10
+ """Filesystem facade confined to a workspace root, including symlink resolution."""
11
+ def __init__(self, root: str | Path | None = None):
12
+ if root is None:
13
+ shared = Path("/storage/emulated/0") / MASTER_FOLDER
14
+ home = Path.home() / MASTER_FOLDER
15
+ root_path = shared if shared.parent.exists() and os.access(shared.parent, os.W_OK) else home
16
+ else:
17
+ root_path = Path(root).expanduser()
18
+ root_path.mkdir(parents=True, exist_ok=True)
19
+ self.root = root_path.resolve()
20
+ self.cwd = self.root
21
+ for item in DEFAULT_FOLDERS:
22
+ (self.root / item).mkdir(parents=True, exist_ok=True)
23
+
24
+ def resolve(self, raw: str | Path = ".") -> Path:
25
+ p = Path(raw).expanduser()
26
+ if not p.is_absolute(): p = self.cwd / p
27
+ resolved = p.resolve(strict=False)
28
+ if resolved != self.root and self.root not in resolved.parents:
29
+ raise PermissionError("Path is outside the AssayFluxOS workspace.")
30
+ return resolved
31
+
32
+ def change(self, raw: str) -> Path:
33
+ target = self.resolve(raw)
34
+ if not target.is_dir(): raise NotADirectoryError(str(target))
35
+ self.cwd = target
36
+ return target
37
+
38
+ def list(self, raw: str = ".") -> list[Path]:
39
+ target = self.resolve(raw)
40
+ if not target.is_dir(): raise NotADirectoryError(str(target))
41
+ return sorted(target.iterdir(), key=lambda p: (not p.is_dir(), p.name.casefold()))
42
+
43
+ def create_file(self, raw: str) -> Path:
44
+ target = self.resolve(raw)
45
+ target.parent.mkdir(parents=True, exist_ok=True)
46
+ target.touch(exist_ok=False)
47
+ return target
48
+
49
+ def make_dir(self, raw: str) -> Path:
50
+ target = self.resolve(raw)
51
+ target.mkdir(parents=True, exist_ok=False)
52
+ return target
53
+
54
+ def read_text(self, raw: str, encoding: str = "utf-8") -> str:
55
+ target = self.resolve(raw)
56
+ if not target.is_file(): raise IsADirectoryError(str(target))
57
+ if target.stat().st_size > 20_000_000: raise ValueError("File exceeds 20 MB text-read limit.")
58
+ return target.read_text(encoding=encoding)
59
+
60
+ def write_text(self, raw: str, content: str, encoding: str = "utf-8") -> Path:
61
+ target = self.resolve(raw)
62
+ target.parent.mkdir(parents=True, exist_ok=True)
63
+ target.write_text(content, encoding=encoding)
64
+ return target
65
+
66
+ def copy(self, source: str, destination: str) -> Path:
67
+ src, dst = self.resolve(source), self.resolve(destination)
68
+ if not src.exists(): raise FileNotFoundError(str(src))
69
+ if src == self.root: raise PermissionError("Copying the workspace root is not supported.")
70
+ if src.is_dir() and (dst == src or src in dst.parents):
71
+ raise ValueError("Cannot copy a directory into itself or one of its descendants.")
72
+ if dst.exists() and dst.is_dir(): dst = dst / src.name
73
+ if dst.exists(): raise FileExistsError(str(dst))
74
+ dst.parent.mkdir(parents=True, exist_ok=True)
75
+ if src.is_dir(): shutil.copytree(src, dst)
76
+ else: shutil.copy2(src, dst)
77
+ return dst
78
+
79
+ def move(self, source: str, destination: str) -> Path:
80
+ src, dst = self.resolve(source), self.resolve(destination)
81
+ if not src.exists(): raise FileNotFoundError(str(src))
82
+ if src == self.root: raise PermissionError("The workspace root cannot be moved.")
83
+ if src.is_dir() and (dst == src or src in dst.parents):
84
+ raise ValueError("Cannot move a directory into itself or one of its descendants.")
85
+ if dst.exists() and dst.is_dir(): dst = dst / src.name
86
+ if dst.exists(): raise FileExistsError(str(dst))
87
+ dst.parent.mkdir(parents=True, exist_ok=True)
88
+ result = Path(shutil.move(str(src), str(dst)))
89
+ if self.cwd == src or src in self.cwd.parents: self.cwd = self.root
90
+ return result
91
+
92
+ def rename(self, source: str, destination: str) -> Path:
93
+ src, dst = self.resolve(source), self.resolve(destination)
94
+ if src == self.root: raise PermissionError("The workspace root cannot be renamed.")
95
+ if not src.exists(): raise FileNotFoundError(str(src))
96
+ if dst.exists(): raise FileExistsError(str(dst))
97
+ dst.parent.mkdir(parents=True, exist_ok=True)
98
+ return src.rename(dst)
99
+
100
+ def delete(self, raw: str) -> Path:
101
+ target = self.resolve(raw)
102
+ if target == self.root: raise PermissionError("The workspace root cannot be deleted.")
103
+ if not target.exists(): raise FileNotFoundError(str(target))
104
+ if target.is_dir(): shutil.rmtree(target)
105
+ else: target.unlink()
106
+ if self.cwd == target or target in self.cwd.parents: self.cwd = self.root
107
+ return target
108
+
109
+ def search_names(self, needle: str) -> list[Path]:
110
+ key = needle.casefold()
111
+ found = []
112
+ for p in self.cwd.rglob("*"):
113
+ try:
114
+ resolved = p.resolve()
115
+ if (resolved == self.root or self.root in resolved.parents) and key in p.name.casefold(): found.append(p)
116
+ except OSError: continue
117
+ return found
118
+
119
+ def find_text(self, needle: str, raw: str = ".") -> list[tuple[Path, int, str]]:
120
+ target = self.resolve(raw)
121
+ candidates = [target] if target.is_file() else target.rglob("*")
122
+ results = []
123
+ for p in candidates:
124
+ try:
125
+ if not p.is_file() or p.stat().st_size > 2_000_000: continue
126
+ resolved = p.resolve()
127
+ if resolved != self.root and self.root not in resolved.parents: continue
128
+ for line_no, line in enumerate(p.read_text(encoding="utf-8").splitlines(), 1):
129
+ if needle.casefold() in line.casefold(): results.append((p, line_no, line.strip()))
130
+ except (UnicodeDecodeError, OSError, PermissionError): continue
131
+ return results