just-cli 0.1.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.
- just/__init__.py +40 -0
- just/cli.py +79 -0
- just/commands/__init__.py +0 -0
- just/commands/download.py +62 -0
- just/commands/edit.py +37 -0
- just/commands/ext/__init__.py +7 -0
- just/commands/ext/add.py +58 -0
- just/commands/extract.py +76 -0
- just/commands/install.py +36 -0
- just/commands/linux.py +201 -0
- just/commands/tunnel.py +27 -0
- just/commands/view.py +33 -0
- just/core/__init__.py +0 -0
- just/core/config/__init__.py +34 -0
- just/core/config/config.py +7 -0
- just/core/config/utils.py +94 -0
- just/core/extension/__init__.py +0 -0
- just/core/extension/generator.py +405 -0
- just/core/extension/parser.py +145 -0
- just/core/extension/utils.py +145 -0
- just/core/installer/__init__.py +10 -0
- just/core/installer/binary.py +148 -0
- just/core/installer/decorator.py +15 -0
- just/core/installer/install_package.py +48 -0
- just/core/installer/package_info.py +12 -0
- just/core/installer/simple_release.py +296 -0
- just/core/installer/source/__init__.py +17 -0
- just/core/installer/source/base.py +22 -0
- just/core/installer/source/http.py +103 -0
- just/core/installer/source/local.py +54 -0
- just/core/system_probe/__init__.py +15 -0
- just/core/system_probe/system_info.py +101 -0
- just/core/system_probe/system_probe.py +198 -0
- just/installers/__init__.py +0 -0
- just/installers/cloudflare/__init__.py +1 -0
- just/installers/cloudflare/installer.py +14 -0
- just/installers/edit/__init__.py +0 -0
- just/installers/edit/installer.py +23 -0
- just/tui/__init__.py +4 -0
- just/tui/editor.py +111 -0
- just/tui/extension.py +118 -0
- just/tui/markdown.py +70 -0
- just/utils/__init__.py +38 -0
- just/utils/archive/__init__.py +22 -0
- just/utils/archive/compression_handler.py +178 -0
- just/utils/archive/extractor.py +87 -0
- just/utils/archive/format_detect.py +199 -0
- just/utils/archive/sevenzip_handler.py +44 -0
- just/utils/archive/tar_handler.py +117 -0
- just/utils/archive/zip_handler.py +81 -0
- just/utils/download_utils.py +574 -0
- just/utils/echo_utils.py +74 -0
- just/utils/env_utils.py +164 -0
- just/utils/file_utils.py +184 -0
- just/utils/format_utils.py +9 -0
- just/utils/progress.py +457 -0
- just/utils/shell_utils.py +72 -0
- just/utils/system_probe.py +214 -0
- just/utils/typer_utils.py +35 -0
- just/utils/user_interaction.py +4 -0
- just_cli-0.1.0.dist-info/METADATA +107 -0
- just_cli-0.1.0.dist-info/RECORD +64 -0
- just_cli-0.1.0.dist-info/WHEEL +4 -0
- just_cli-0.1.0.dist-info/entry_points.txt +2 -0
just/__init__.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from typing_extensions import Annotated
|
|
2
|
+
from typer import Option, Argument
|
|
3
|
+
|
|
4
|
+
from just.cli import just_cli,capture_exception
|
|
5
|
+
from just.core.config import JustConfig, load_env_config, update_env_config, get_cache_dir
|
|
6
|
+
from just.core.installer import installer, SimpleReleaseInstaller, BinaryInstaller
|
|
7
|
+
from just.utils import (
|
|
8
|
+
SystemProbe,
|
|
9
|
+
create_typer_app,
|
|
10
|
+
confirm_action,
|
|
11
|
+
docstring,
|
|
12
|
+
download_with_resume,
|
|
13
|
+
echo,
|
|
14
|
+
extract,
|
|
15
|
+
execute_commands
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
config = JustConfig()
|
|
20
|
+
system = SystemProbe()
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"Annotated",
|
|
24
|
+
"Argument",
|
|
25
|
+
"Option",
|
|
26
|
+
"capture_exception",
|
|
27
|
+
"create_typer_app",
|
|
28
|
+
"docstring",
|
|
29
|
+
"echo",
|
|
30
|
+
"extract",
|
|
31
|
+
"execute_commands",
|
|
32
|
+
"config",
|
|
33
|
+
"just_cli",
|
|
34
|
+
"installer",
|
|
35
|
+
"SimpleReleaseInstaller",
|
|
36
|
+
"BinaryInstaller",
|
|
37
|
+
"load_env_config",
|
|
38
|
+
"update_env_config",
|
|
39
|
+
"system"
|
|
40
|
+
]
|
just/cli.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import importlib
|
|
3
|
+
import os
|
|
4
|
+
import traceback
|
|
5
|
+
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typer.core import TyperGroup
|
|
8
|
+
from typing import Any, Callable, List, TypeVar, Optional
|
|
9
|
+
|
|
10
|
+
from just.core.config import load_env_config, get_command_dir, get_extension_dir
|
|
11
|
+
from just.utils import echo
|
|
12
|
+
from just.utils.typer_utils import create_typer_app
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
just_cli = create_typer_app()
|
|
16
|
+
|
|
17
|
+
T = TypeVar('T')
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def capture_exception(func: Callable[..., T]) -> Callable[..., Optional[T]]:
|
|
21
|
+
@functools.wraps(func)
|
|
22
|
+
def wrapper(*args: Any, **kwargs: Any) -> Optional[T]:
|
|
23
|
+
try:
|
|
24
|
+
return func(*args, **kwargs)
|
|
25
|
+
except Exception as e:
|
|
26
|
+
echo.error(str(e))
|
|
27
|
+
exit(1)
|
|
28
|
+
|
|
29
|
+
return wrapper
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class SortedGroup(TyperGroup):
|
|
33
|
+
def list_commands(self, ctx):
|
|
34
|
+
return sorted(super().list_commands(ctx))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def run_just_cli(*args, **kwargs):
|
|
38
|
+
load_env_config()
|
|
39
|
+
just_cli(*args, **kwargs)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def traverse_script_dir(directory: str) -> List[str]:
|
|
43
|
+
script_modules = []
|
|
44
|
+
for root, dirs, files in os.walk(directory):
|
|
45
|
+
for file in files:
|
|
46
|
+
if not file.startswith('_') and file.endswith(".py"):
|
|
47
|
+
script_modules.append(
|
|
48
|
+
"just." +
|
|
49
|
+
'.'.join(Path(root).relative_to(Path(__file__).parent).parts) +
|
|
50
|
+
f".{str(Path(file).stem)}"
|
|
51
|
+
)
|
|
52
|
+
return script_modules
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def main():
|
|
57
|
+
# Dynamically import all script modules to register their commands
|
|
58
|
+
script_modules = []
|
|
59
|
+
script_modules.extend(traverse_script_dir(get_command_dir().as_posix()))
|
|
60
|
+
script_modules.extend(traverse_script_dir(get_extension_dir().as_posix()))
|
|
61
|
+
script_modules.sort()
|
|
62
|
+
missing_packages = []
|
|
63
|
+
for module_name in script_modules:
|
|
64
|
+
try:
|
|
65
|
+
# echo.debug("Importing", module_name)
|
|
66
|
+
importlib.import_module(module_name)
|
|
67
|
+
except ImportError as e:
|
|
68
|
+
traceback.print_exc()
|
|
69
|
+
package_name = e.name
|
|
70
|
+
if package_name not in missing_packages:
|
|
71
|
+
missing_packages.append(package_name)
|
|
72
|
+
# Handle the case where the module cannot be found
|
|
73
|
+
echo.warning(
|
|
74
|
+
f"`{package_name}` is not installed, some sub commands are disabled, "
|
|
75
|
+
f"refer to README.md for instructions."
|
|
76
|
+
)
|
|
77
|
+
continue
|
|
78
|
+
# Run the CLI application
|
|
79
|
+
run_just_cli()
|
|
File without changes
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from typing import List, Optional, Dict
|
|
3
|
+
from typing_extensions import Annotated
|
|
4
|
+
|
|
5
|
+
from just import just_cli, capture_exception, echo
|
|
6
|
+
from just.utils import download_with_resume
|
|
7
|
+
from just.utils.download_utils import DownloadError, NetworkError, FileSystemError, DownloadCancelledError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def parse_headers(header_list: Optional[List[str]]) -> Optional[Dict[str, str]]:
|
|
11
|
+
"""Parse header list to dictionary."""
|
|
12
|
+
if not header_list:
|
|
13
|
+
return None
|
|
14
|
+
headers: Dict[str, str] = {}
|
|
15
|
+
for header in header_list:
|
|
16
|
+
if ':' in header:
|
|
17
|
+
key, value = header.split(':', 1)
|
|
18
|
+
headers[key.strip()] = value.strip()
|
|
19
|
+
return headers
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@just_cli.command(name="download")
|
|
23
|
+
@capture_exception
|
|
24
|
+
def download_command(
|
|
25
|
+
url: Annotated[str, typer.Argument(
|
|
26
|
+
help="URL to download",
|
|
27
|
+
show_default=False
|
|
28
|
+
)],
|
|
29
|
+
headers: Annotated[Optional[List[str]], typer.Option(
|
|
30
|
+
"-H", "--header",
|
|
31
|
+
help="Custom headers (can be used multiple times)"
|
|
32
|
+
)] = None,
|
|
33
|
+
output: Annotated[Optional[str], typer.Option(
|
|
34
|
+
"-o", "--output",
|
|
35
|
+
help="Output filename"
|
|
36
|
+
)] = None,
|
|
37
|
+
verbose: Annotated[bool, typer.Option(
|
|
38
|
+
"--verbose", "-v",
|
|
39
|
+
help="Enable verbose logging"
|
|
40
|
+
)] = False
|
|
41
|
+
) -> None:
|
|
42
|
+
"""
|
|
43
|
+
Download a file from URL with resume support and custom headers.
|
|
44
|
+
|
|
45
|
+
Examples:
|
|
46
|
+
just download https://example.com/file.zip
|
|
47
|
+
just download https://example.com/file.zip -H "Authorization: Bearer token" -H "User-Agent: MyApp/1.0"
|
|
48
|
+
just download https://example.com/file.zip -o myfile.zip
|
|
49
|
+
"""
|
|
50
|
+
try:
|
|
51
|
+
download_with_resume(
|
|
52
|
+
url=url,
|
|
53
|
+
headers=parse_headers(headers),
|
|
54
|
+
output_file=output,
|
|
55
|
+
verbose=verbose
|
|
56
|
+
)
|
|
57
|
+
except DownloadCancelledError as e:
|
|
58
|
+
echo.info(str(e))
|
|
59
|
+
raise typer.Exit(1)
|
|
60
|
+
except (NetworkError, FileSystemError, DownloadError) as e:
|
|
61
|
+
echo.error(f"Download failed: {e}")
|
|
62
|
+
raise typer.Exit(1)
|
just/commands/edit.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import typer
|
|
3
|
+
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing_extensions import Annotated
|
|
6
|
+
|
|
7
|
+
from just import just_cli, capture_exception
|
|
8
|
+
from just.core.config import get_env_config_file
|
|
9
|
+
from just.tui import FileEditor
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
INTERNAL_FILES = {
|
|
13
|
+
"config": str(get_env_config_file())
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def edit_file_by_textual(file_path):
|
|
18
|
+
"""Launch the TUI file editor for the specified file"""
|
|
19
|
+
editor = FileEditor(file_path)
|
|
20
|
+
editor.run()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@just_cli.command(name="edit")
|
|
24
|
+
@capture_exception
|
|
25
|
+
def edit_file(
|
|
26
|
+
file_path: Annotated[str, typer.Argument(
|
|
27
|
+
help="The file to edit",
|
|
28
|
+
show_default=False
|
|
29
|
+
)]
|
|
30
|
+
):
|
|
31
|
+
"""
|
|
32
|
+
Edit file.
|
|
33
|
+
"""
|
|
34
|
+
if file_path.lower() in INTERNAL_FILES:
|
|
35
|
+
file_path = INTERNAL_FILES[file_path.lower()]
|
|
36
|
+
|
|
37
|
+
edit_file_by_textual(file_path)
|
just/commands/ext/add.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import shlex
|
|
2
|
+
import typer
|
|
3
|
+
|
|
4
|
+
from typing import List, Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
from just import echo
|
|
8
|
+
from just.core.extension.generator import generate_extension_script
|
|
9
|
+
from just.core.extension.utils import split_command_line
|
|
10
|
+
from just.tui.extension import ExtensionTUI
|
|
11
|
+
from . import ext_cli
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@ext_cli.command(name="add", context_settings={"ignore_unknown_options": True})
|
|
15
|
+
def add_extension(
|
|
16
|
+
commands: Optional[List[str]] = typer.Argument(
|
|
17
|
+
None,
|
|
18
|
+
help="The command to register as a just extension"
|
|
19
|
+
),
|
|
20
|
+
tui: bool = typer.Option(
|
|
21
|
+
False,
|
|
22
|
+
"--tui",
|
|
23
|
+
help="Launch TUI to configure the command"
|
|
24
|
+
)
|
|
25
|
+
):
|
|
26
|
+
"""
|
|
27
|
+
Parse and register a command as a just extension.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
commands: The commands to register, e.g., "docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' f523e75ca4ef"
|
|
31
|
+
tui: Whether to launch TUI mode
|
|
32
|
+
"""
|
|
33
|
+
# If no command provided or TUI mode requested, launch TUI
|
|
34
|
+
if not commands or tui:
|
|
35
|
+
launch_tui()
|
|
36
|
+
return
|
|
37
|
+
|
|
38
|
+
# Print parsed command for debugging
|
|
39
|
+
echo.echo(str(commands))
|
|
40
|
+
|
|
41
|
+
just_extension_commands = input("Enter extension commands: ")
|
|
42
|
+
# Split the command line to handle annotations with spaces properly
|
|
43
|
+
just_extension_commands = split_command_line(just_extension_commands)
|
|
44
|
+
echo.echo(str(just_extension_commands))
|
|
45
|
+
|
|
46
|
+
# Generate the extension script
|
|
47
|
+
generate_extension_script(
|
|
48
|
+
shlex.join(commands),
|
|
49
|
+
just_extension_commands,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def launch_tui():
|
|
54
|
+
"""Launch TUI for configuring extension commands"""
|
|
55
|
+
|
|
56
|
+
# Launch the TUI app
|
|
57
|
+
app = ExtensionTUI()
|
|
58
|
+
app.run()
|
just/commands/extract.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from just import Annotated, just_cli, capture_exception
|
|
6
|
+
from just.utils import echo
|
|
7
|
+
from just.utils.archive import extract as archive_extract, detect_archive_format, ArchiveFormat
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_default_output_dir(archive_path: str) -> str:
|
|
11
|
+
"""
|
|
12
|
+
Get default output directory from archive filename.
|
|
13
|
+
|
|
14
|
+
Simply removes all extensions by taking everything before the first dot.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
archive_path: Path to the archive file
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
Directory name without extensions
|
|
21
|
+
"""
|
|
22
|
+
return archive_path.split(".")[0]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@just_cli.command(name="extract")
|
|
26
|
+
@capture_exception
|
|
27
|
+
def extract_command(
|
|
28
|
+
archive: Annotated[str, typer.Argument(
|
|
29
|
+
help="Path to the archive file to extract"
|
|
30
|
+
)],
|
|
31
|
+
output: Annotated[Optional[str], typer.Option(
|
|
32
|
+
"-o", "--output",
|
|
33
|
+
help="Output directory for extracted files (default: archive name without extension)"
|
|
34
|
+
)] = None
|
|
35
|
+
) -> None:
|
|
36
|
+
"""
|
|
37
|
+
Extract archive and compressed files with automatic format detection.
|
|
38
|
+
|
|
39
|
+
Supports: ZIP, TAR (with gz/bz2/xz/zst compression), GZIP, BZIP2, XZ, ZSTD, 7Z
|
|
40
|
+
|
|
41
|
+
Automatically detects format using magic bytes (file signatures) for reliable
|
|
42
|
+
identification, even with incorrect file extensions. Extracts to a directory
|
|
43
|
+
named after the archive by default.
|
|
44
|
+
|
|
45
|
+
Optional dependencies:
|
|
46
|
+
- 7Z support: pip install py7zr
|
|
47
|
+
- ZSTD support: pip install zstandard
|
|
48
|
+
|
|
49
|
+
Examples:
|
|
50
|
+
just extract archive.zip # Extracts to ./archive/
|
|
51
|
+
just extract archive.tar.gz # Extracts to ./archive/
|
|
52
|
+
just extract file.gz # Extracts to ./file
|
|
53
|
+
just extract data.7z -o out/ # Extracts to ./out/
|
|
54
|
+
"""
|
|
55
|
+
archive_path = Path(archive)
|
|
56
|
+
|
|
57
|
+
if not archive_path.exists():
|
|
58
|
+
echo.error(f"Archive not found: {archive}")
|
|
59
|
+
raise typer.Exit(1)
|
|
60
|
+
|
|
61
|
+
fmt = detect_archive_format(str(archive_path))
|
|
62
|
+
|
|
63
|
+
if fmt == ArchiveFormat.UNKNOWN:
|
|
64
|
+
echo.error(f"Unknown or unsupported archive format: {archive}")
|
|
65
|
+
raise typer.Exit(1)
|
|
66
|
+
|
|
67
|
+
if fmt == ArchiveFormat.RAR:
|
|
68
|
+
echo.error("RAR format detected but not supported")
|
|
69
|
+
echo.info("RAR is a proprietary format. Install rarfile: pip install rarfile")
|
|
70
|
+
raise typer.Exit(1)
|
|
71
|
+
|
|
72
|
+
if output is None:
|
|
73
|
+
output = get_default_output_dir(str(archive_path))
|
|
74
|
+
|
|
75
|
+
if not archive_extract(str(archive_path), output):
|
|
76
|
+
raise Exception("Failed to extract archive")
|
just/commands/install.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from typing import List, Optional
|
|
3
|
+
from typing_extensions import Annotated
|
|
4
|
+
|
|
5
|
+
from just import just_cli, echo
|
|
6
|
+
from just.core.installer.install_package import install_package
|
|
7
|
+
from just.utils.typer_utils import show_help
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@just_cli.command(name="install", context_settings={"ignore_unknown_options": True})
|
|
11
|
+
def install(
|
|
12
|
+
args: Annotated[Optional[List[str]], typer.Argument(
|
|
13
|
+
help="The arguments to install the package"
|
|
14
|
+
)] = None,
|
|
15
|
+
help_flag: Annotated[bool, typer.Option(
|
|
16
|
+
"--help", "-h",
|
|
17
|
+
help="Show this help message and exit"
|
|
18
|
+
)] = False
|
|
19
|
+
):
|
|
20
|
+
if not args:
|
|
21
|
+
show_help(just_cli, "install")
|
|
22
|
+
return
|
|
23
|
+
|
|
24
|
+
package_name = args.pop(0)
|
|
25
|
+
if help_flag:
|
|
26
|
+
args.append("--help")
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
install_package(package_name, args)
|
|
30
|
+
except Exception as e:
|
|
31
|
+
echo.fail(f"Error installing {package_name}: {e}")
|
|
32
|
+
raise typer.Exit(1)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
if __name__ == "__main__":
|
|
36
|
+
just_cli()
|
just/commands/linux.py
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import typer
|
|
3
|
+
import shutil
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing_extensions import Annotated
|
|
6
|
+
|
|
7
|
+
from just import just_cli, capture_exception, echo
|
|
8
|
+
from just.utils.file_utils import read_file_text
|
|
9
|
+
from just.utils import confirm_action
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@just_cli.command(name="cat")
|
|
13
|
+
@capture_exception
|
|
14
|
+
def cat_file(
|
|
15
|
+
file_paths: Annotated[list[str], typer.Argument(
|
|
16
|
+
help="Files to display",
|
|
17
|
+
show_default=False
|
|
18
|
+
)],
|
|
19
|
+
with_line_numbers: Annotated[bool, typer.Option(
|
|
20
|
+
"--number", "-n",
|
|
21
|
+
help="Number all output lines"
|
|
22
|
+
)] = False
|
|
23
|
+
):
|
|
24
|
+
"""
|
|
25
|
+
Concatenate and print files.
|
|
26
|
+
"""
|
|
27
|
+
for file_path in file_paths:
|
|
28
|
+
if os.path.isdir(file_path):
|
|
29
|
+
echo.red(f"cat: {file_path} is a directory")
|
|
30
|
+
exit(1)
|
|
31
|
+
try:
|
|
32
|
+
echo.echo(read_file_text(file_path, with_line_numbers=with_line_numbers))
|
|
33
|
+
except FileNotFoundError:
|
|
34
|
+
echo.red(f"cat: The file {file_path} does not exist")
|
|
35
|
+
exit(1)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@just_cli.command(name="ls")
|
|
39
|
+
@capture_exception
|
|
40
|
+
def list_files(
|
|
41
|
+
path: Annotated[str, typer.Argument(
|
|
42
|
+
help="Directory to list",
|
|
43
|
+
show_default=False
|
|
44
|
+
)] = ".",
|
|
45
|
+
long_format: Annotated[bool, typer.Option(
|
|
46
|
+
"-l",
|
|
47
|
+
help="Use a long listing format"
|
|
48
|
+
)] = False,
|
|
49
|
+
all_format: Annotated[bool, typer.Option(
|
|
50
|
+
"--all", "-a",
|
|
51
|
+
help="Do not ignore entries starting with ."
|
|
52
|
+
)] = False
|
|
53
|
+
):
|
|
54
|
+
"""
|
|
55
|
+
List directory contents.
|
|
56
|
+
"""
|
|
57
|
+
p = Path(path)
|
|
58
|
+
if not p.exists():
|
|
59
|
+
echo.red(f"ls: cannot access '{path}': No such file or directory")
|
|
60
|
+
exit(1)
|
|
61
|
+
|
|
62
|
+
if not p.is_dir():
|
|
63
|
+
if long_format:
|
|
64
|
+
stat = p.stat()
|
|
65
|
+
echo.echo(f"-rw-r--r-- 1 user group {stat.st_size:>8} {p.name}")
|
|
66
|
+
else:
|
|
67
|
+
echo.echo(p.name)
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
entries = list(p.iterdir())
|
|
71
|
+
if not all_format:
|
|
72
|
+
entries = [entry for entry in entries if not entry.name.startswith('.')]
|
|
73
|
+
|
|
74
|
+
if long_format:
|
|
75
|
+
for entry in entries:
|
|
76
|
+
stat = entry.stat()
|
|
77
|
+
permissions = "drwxr-xr-x" if entry.is_dir() else "-rw-r--r--"
|
|
78
|
+
size = stat.st_size
|
|
79
|
+
echo.echo(f"{permissions} 1 user group {size:>8} {entry.name}")
|
|
80
|
+
else:
|
|
81
|
+
for entry in entries:
|
|
82
|
+
echo.echo(entry.name)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@just_cli.command(name="mkdir")
|
|
86
|
+
@capture_exception
|
|
87
|
+
def make_directory(
|
|
88
|
+
dir_names: Annotated[list[str], typer.Argument(
|
|
89
|
+
help="Directories to create",
|
|
90
|
+
show_default=False
|
|
91
|
+
)],
|
|
92
|
+
make_parents: Annotated[bool, typer.Option(
|
|
93
|
+
"--parents", "-p",
|
|
94
|
+
help="No error if existing, make parent directories as needed"
|
|
95
|
+
)] = False
|
|
96
|
+
):
|
|
97
|
+
"""
|
|
98
|
+
Create directories.
|
|
99
|
+
"""
|
|
100
|
+
for dir_name in dir_names:
|
|
101
|
+
if make_parents:
|
|
102
|
+
os.makedirs(dir_name, exist_ok=True)
|
|
103
|
+
else:
|
|
104
|
+
os.mkdir(dir_name)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@just_cli.command(name="rm")
|
|
108
|
+
@capture_exception
|
|
109
|
+
def remove_files(
|
|
110
|
+
targets: Annotated[list[str], typer.Argument(
|
|
111
|
+
help="Files or directories to remove",
|
|
112
|
+
show_default=False
|
|
113
|
+
)],
|
|
114
|
+
recursive: Annotated[bool, typer.Option(
|
|
115
|
+
"--recursive", "-r",
|
|
116
|
+
help="Remove directories and their contents recursively"
|
|
117
|
+
)] = False
|
|
118
|
+
):
|
|
119
|
+
"""
|
|
120
|
+
Remove files or directories.
|
|
121
|
+
"""
|
|
122
|
+
for target in targets:
|
|
123
|
+
target_path = Path(target)
|
|
124
|
+
if not target_path.exists():
|
|
125
|
+
echo.red(f"rm: cannot remove '{target}': No such file or directory")
|
|
126
|
+
exit(1)
|
|
127
|
+
if target_path.is_dir():
|
|
128
|
+
# Prompt for confirmation when removing directory without -r
|
|
129
|
+
if not recursive and not confirm_action(f"rm: descend into directory '{target}'?"):
|
|
130
|
+
continue
|
|
131
|
+
shutil.rmtree(target_path)
|
|
132
|
+
else:
|
|
133
|
+
os.remove(target_path)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@just_cli.command(name="cp")
|
|
137
|
+
@capture_exception
|
|
138
|
+
def copy_files(
|
|
139
|
+
source: Annotated[str, typer.Argument(
|
|
140
|
+
help="Source file or directory",
|
|
141
|
+
show_default=False
|
|
142
|
+
)],
|
|
143
|
+
destination: Annotated[str, typer.Argument(
|
|
144
|
+
help="Destination file or directory",
|
|
145
|
+
show_default=False
|
|
146
|
+
)],
|
|
147
|
+
recursive: Annotated[bool, typer.Option(
|
|
148
|
+
"--recursive", "-r",
|
|
149
|
+
help="Copy directories recursively"
|
|
150
|
+
)] = False
|
|
151
|
+
):
|
|
152
|
+
"""
|
|
153
|
+
Copy files or directories.
|
|
154
|
+
"""
|
|
155
|
+
source_path = Path(source)
|
|
156
|
+
dest_path = Path(destination)
|
|
157
|
+
|
|
158
|
+
if not source_path.exists():
|
|
159
|
+
echo.red(f"cp: cannot stat '{source}': No such file or directory")
|
|
160
|
+
exit(1)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
if source_path.is_dir():
|
|
164
|
+
if not recursive:
|
|
165
|
+
# Prompt for confirmation when copying directory without -r
|
|
166
|
+
if not confirm_action(f"cp: -r not specified; omitting directory '{source}'"):
|
|
167
|
+
exit(1)
|
|
168
|
+
if dest_path.exists() and dest_path.is_dir():
|
|
169
|
+
# Copy directory into existing directory
|
|
170
|
+
shutil.copytree(source_path, dest_path / source_path.name)
|
|
171
|
+
else:
|
|
172
|
+
shutil.copytree(source_path, dest_path)
|
|
173
|
+
else:
|
|
174
|
+
shutil.copy2(source_path, dest_path)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@just_cli.command(name="mv")
|
|
178
|
+
@capture_exception
|
|
179
|
+
def move_files(
|
|
180
|
+
source: Annotated[str, typer.Argument(
|
|
181
|
+
help="Source file or directory",
|
|
182
|
+
show_default=False
|
|
183
|
+
)],
|
|
184
|
+
destination: Annotated[str, typer.Argument(
|
|
185
|
+
help="Destination file or directory",
|
|
186
|
+
show_default=False
|
|
187
|
+
)]
|
|
188
|
+
):
|
|
189
|
+
"""
|
|
190
|
+
Move or rename files or directories.
|
|
191
|
+
"""
|
|
192
|
+
source_path = Path(source)
|
|
193
|
+
dest_path = Path(destination)
|
|
194
|
+
|
|
195
|
+
if not source_path.exists():
|
|
196
|
+
echo.red(f"mv: cannot stat '{source}': No such file or directory")
|
|
197
|
+
exit(1)
|
|
198
|
+
if dest_path.exists() and not confirm_action(f"mv: overwrite '{destination}'?"):
|
|
199
|
+
exit(1)
|
|
200
|
+
|
|
201
|
+
shutil.move(str(source_path), str(dest_path))
|
just/commands/tunnel.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
|
|
3
|
+
from typing_extensions import Annotated
|
|
4
|
+
|
|
5
|
+
from just import create_typer_app, capture_exception, just_cli
|
|
6
|
+
from just.utils import execute_command
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
tunnel_cli = create_typer_app()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@just_cli.command(name="tunnel")
|
|
13
|
+
@capture_exception
|
|
14
|
+
def create_cloudflare_tunnel(
|
|
15
|
+
url: Annotated[str, typer.Argument(
|
|
16
|
+
help='The URL to tunnel',
|
|
17
|
+
show_default=False
|
|
18
|
+
)]
|
|
19
|
+
):
|
|
20
|
+
"""
|
|
21
|
+
Create Cloudflare tunnel.
|
|
22
|
+
"""
|
|
23
|
+
command = r"""
|
|
24
|
+
cloudflared tunnel --url <URL>
|
|
25
|
+
"""
|
|
26
|
+
command = command.replace('<URL>', url)
|
|
27
|
+
execute_command(command)
|
just/commands/view.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing_extensions import Annotated
|
|
5
|
+
|
|
6
|
+
from just import just_cli
|
|
7
|
+
from just.tui.markdown import MarkdownApp
|
|
8
|
+
from just.commands.edit import edit_file
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def view_markdown_by_textual(file_path: str):
|
|
12
|
+
"""View markdown file."""
|
|
13
|
+
app = MarkdownApp()
|
|
14
|
+
app.path = Path(file_path)
|
|
15
|
+
app.run()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@just_cli.command(name="view")
|
|
19
|
+
def view_file(
|
|
20
|
+
file_path: Annotated[str, typer.Argument(
|
|
21
|
+
help="The file to view",
|
|
22
|
+
show_default=False
|
|
23
|
+
)]
|
|
24
|
+
):
|
|
25
|
+
"""
|
|
26
|
+
Preview the structured text files (e.g., Markdown, JSON, XML)
|
|
27
|
+
"""
|
|
28
|
+
ext = "".join(Path(file_path).suffixes)
|
|
29
|
+
if ext == '.md':
|
|
30
|
+
view_markdown_by_textual(file_path)
|
|
31
|
+
# TODO: support other file types
|
|
32
|
+
else:
|
|
33
|
+
edit_file(file_path)
|
just/core/__init__.py
ADDED
|
File without changes
|