diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index f99c22f..0000000 --- a/.gitattributes +++ /dev/null @@ -1,3 +0,0 @@ -# 官方文档 https://github.com/github-linguist/linguist/blob/master/docs/overrides.md - -src/fcbyk/web/** linguist-generated diff --git a/CHANGELOG.md b/CHANGELOG.md index c6232d3..3604cf1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,29 @@ +## 🎉 v0.3.0-alpha.1 (2026-02-13) + +### ♻️ Refactor + +- **pick**: 迁移样式从 SCSS 到 Tailwind CSS +- **slide**: 迁移样式,从 SCSS 到 Tailwind +- **lansend**: 样式从 SCSS 迁移到 Tailwind CSS + +### ✨ Features + +- **cli**: 初始化默认配置文件 +- **lansend**: 重构上传功能并集成到文件列表 +- **get**: 新增资源获取命令,支持下载和打开官网 +- **cmd**: 添加危险命令执行前的检测和确认机制 +- **cmd**: 新增可复用命令管理能力 +- **alias**: 改进别名添加和显示功能 +- **alias**: 支持为多级命令新建别名 + +### 🐛 Bug Fixes + +- **socket**: 去除前端构建的警告 + +### 🔧 Chores + +- 更新 commitizen 配置和笔记文档 + ## 🎉 v0.2.2 (2026-01-25) ### ✨ Features diff --git a/docs/LANSend.md b/docs/LANSend.md new file mode 100644 index 0000000..52eedbe --- /dev/null +++ b/docs/LANSend.md @@ -0,0 +1,103 @@ +## LANSend 子命令 + +LANSend 用于在局域网内快速共享本地目录,通过浏览器即可浏览、预览、下载文件,并可选开启上传、聊天室和网速测试等功能。 + +### 基本功能 +- 在指定目录上启动一个局域网 Web 文件服务 +- 自动列出目录树、预览文本/图片/视频等常见文件 +- 支持文件下载(可在前端隐藏下载按钮) +- 可选开启文件上传(支持上传密码) +- 内置简单聊天室功能(可选) +- 内置上传/下载测速接口,方便测试局域网带宽 + +### 基本用法 +```bash +fcbyk lansend [选项] +``` + +启动后,命令会: +- 检查共享目录是否存在且为目录 +- 检查端口是否可用 +- 输出当前共享目录与可访问的局域网地址列表 +- 自动将其中一个访问地址复制到剪贴板 +- 默认自动在浏览器中打开访问页面(可通过参数关闭) + +### 参数说明 +- `-p, --port INTEGER` + 指定 Web 服务端口,默认 `80`。 + 例如:`-p 8080` + +- `-d, --directory PATH` + 指定要共享的本地目录,默认当前目录 `.`。 + 该目录及其子目录中的内容会被前端页面列出与访问。 + 例如:`-d D:\ShareFolder` + +- `-ap, --ask-password` + 是否为上传功能设置密码。 + - 仅在未禁用上传(即没有使用 `--disable-upload`)时生效 + - 启动时会在命令行中交互式询问上传密码 + - 直接回车会使用默认密码 `123456` + - 设置后,上传接口会强制校验上传密码 + +- `-nb, --no-browser` + 禁止自动在本机浏览器中打开页面。 + 使用该参数时,只会在命令行中打印访问 URL,并复制到剪贴板。 + +- `-nd, --hide-download` + 在前端目录页面中隐藏“下载”按钮。 + 适用于只允许预览、不希望用户便捷下载文件的场景。 + 注意:底层下载接口仍然存在,主要是前端 UI 级的限制,适合配合受信任用户使用。 + +- `-nu, --disable-upload` + 禁用上传功能。 + - 所有上传相关接口不会被注册 + - 前端页面不会显示上传入口 + 适用于只读共享场景,推荐在无需接收文件时开启。 + +- `--chat` + 启用局域网聊天室功能,主要用于在局域网内快速共享文本信息(如链接、命令、临时说明等)。 + 打开后,前端会显示一个简单的聊天面板,所有连接到该 LANSend 服务的用户都可以通过该面板收发消息,消息只保存在内存中,重启后会清空。 + +### 常见用法示例 + +1. 在当前目录启动默认服务(端口 80,自动打开浏览器) +```bash +fcbyk lansend +``` + +2. 指定端口和目录共享 +```bash +fcbyk lansend -p 8080 -d D:\ShareFolder +``` + +3. 开启上传并设置上传密码 +```bash +fcbyk lansend -p 8080 -d D:\ShareFolder -ap +``` +启动后会提示输入密码,若直接回车则使用 `123456`。 + +4. 只读共享目录(禁用上传) +```bash +fcbyk lansend -p 8080 -d D:\ShareFolder --disable-upload +``` + +5. 只允许在线预览,不鼓励下载 +```bash +fcbyk lansend -p 8080 -d D:\ShareFolder --hide-download +``` + +6. 启用聊天室功能 +```bash +fcbyk lansend -p 8080 -d D:\ShareFolder --chat +``` + +7. 在服务器上启动但不自动打开浏览器 +```bash +fcbyk lansend -p 8080 -d /data/share --no-browser +``` + +### 注意事项 +- 请确保共享目录中不包含敏感文件,局域网内任何可以访问该服务的设备都可能浏览到这些文件。 +- 如果需要一定的访问控制,建议至少开启上传密码,并搭配只读或隐藏下载按钮等选项使用。 +- LANSend 仅面向局域网场景设计,不推荐直接暴露到公网环境。 + diff --git a/pyproject.toml b/pyproject.toml index c8d236f..08ebef1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ addopts = "-v --cov=fcbyk --cov-report=term-missing" [tool.commitizen] name = "cz_customize" -version = "0.2.2" +version = "0.3.0a1" tag_format = "v$version" changelog_file = "CHANGELOG.md" bump_message = "🎉 release: v$new_version" diff --git a/setup.cfg b/setup.cfg index ef0cc99..b12dea8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = fcbyk-cli -version = 0.2.2 +version = 0.3.0a1 description = A CLI tool for fcbyk long_description = file: README.md long_description_content_type = text/markdown @@ -61,7 +61,9 @@ exclude = [options.entry_points] console_scripts = - fcbyk = fcbyk.cli:cli + fcbyk = fcbyk.cli:main + byk = fcbyk.cli:main + [options.package_data] fcbyk = diff --git a/src/fcbyk/cli.py b/src/fcbyk/cli.py index c3db9d9..5e5ab5a 100644 --- a/src/fcbyk/cli.py +++ b/src/fcbyk/cli.py @@ -1,11 +1,12 @@ #!/usr/bin/env python3 -import click,random - -from fcbyk import commands +import click, random, os +from fcbyk import commands, defaults +from fcbyk.utils import storage from fcbyk.commands.alias import AliasedGroup from fcbyk.cli_support import ( version_callback, print_aliases, + print_commands, add_gui_options, banner ) @@ -28,19 +29,25 @@ ) @add_gui_options @click.pass_context -def cli(ctx): +def main(ctx): + # 初始化默认配置 + config_path = storage.get_path(defaults.CONFIG_FILE) + if not os.path.exists(config_path): + storage.save_json(config_path, defaults.DEFAULT_CONFIG) + if ctx.invoked_subcommand is None: banner_text = random.choice(banner) click.secho(banner_text, fg="white", dim=True) click.echo(ctx.get_help()) # 帮助信息 print_aliases() # 打印别名,如果有 + print_commands(leading_newline=False) # 打印已存脚本,如果有 # 注册子命令 for cmd_name in commands.__all__: - cli.add_command(getattr(commands, cmd_name)) + main.add_command(getattr(commands, cmd_name)) if __name__ == "__main__": - cli() + main() diff --git a/src/fcbyk/cli_support/__init__.py b/src/fcbyk/cli_support/__init__.py index cdcde83..4faf28c 100644 --- a/src/fcbyk/cli_support/__init__.py +++ b/src/fcbyk/cli_support/__init__.py @@ -1,7 +1,11 @@ """CLI 支持模块""" -from .callbacks import version_callback, print_aliases +from .callbacks import ( + version_callback, + print_aliases, + print_commands +) from .gui import add_gui_options from .helpers import banner -__all__ = ['version_callback', 'print_aliases', 'add_gui_options', 'banner'] \ No newline at end of file +__all__ = ['version_callback', 'print_aliases', 'add_gui_options', 'banner', 'print_commands'] \ No newline at end of file diff --git a/src/fcbyk/cli_support/callbacks.py b/src/fcbyk/cli_support/callbacks.py index a3a8d39..46722ae 100644 --- a/src/fcbyk/cli_support/callbacks.py +++ b/src/fcbyk/cli_support/callbacks.py @@ -82,3 +82,32 @@ def print_aliases(show_empty=False, leading_newline=True): click.echo("No aliases configured.") except Exception: pass + + +def print_commands(show_empty=False, leading_newline=True): + """打印已保存的命令脚本列表""" + try: + from fcbyk.commands.cmd.cli import load_commands + commands = load_commands() + if commands: + if leading_newline: + click.echo() + click.echo("Scripts:") + items = list(commands.items()) + max_name_len = max(len(str(name)) for name, _ in items) + for name, cmd_data in items: + if isinstance(cmd_data, str): + command = cmd_data + cwd_str = "" + else: + command = cmd_data.get("command", "") + cwd = cmd_data.get("cwd") + cwd_str = f" [CWD: {cwd}]" if cwd else "" + + padding = " " * (max_name_len - len(str(name)) + 2) + click.echo(f" {name}{padding}-> {command}{cwd_str}") + click.echo() + elif show_empty: + click.echo("No scripts saved yet.") + except Exception: + pass diff --git a/src/fcbyk/commands/__init__.py b/src/fcbyk/commands/__init__.py index 04cbc0c..aaaebbd 100644 --- a/src/fcbyk/commands/__init__.py +++ b/src/fcbyk/commands/__init__.py @@ -3,5 +3,7 @@ from .pick.cli import pick from .slide.cli import slide from .alias import alias +from .cmd.cli import cmd +from .get.cli import get -__all__ = ['lansend', 'ai', 'pick', 'slide', 'alias'] +__all__ = ['lansend', 'ai', 'pick', 'slide', 'alias', 'cmd', 'get'] diff --git a/src/fcbyk/commands/cmd/__init__.py b/src/fcbyk/commands/cmd/__init__.py new file mode 100644 index 0000000..04a1c07 --- /dev/null +++ b/src/fcbyk/commands/cmd/__init__.py @@ -0,0 +1,3 @@ +from .cli import cmd + +__all__ = ['cmd'] diff --git a/src/fcbyk/commands/cmd/cli.py b/src/fcbyk/commands/cmd/cli.py new file mode 100644 index 0000000..1f1541a --- /dev/null +++ b/src/fcbyk/commands/cmd/cli.py @@ -0,0 +1,159 @@ +import click +import subprocess +import re +from fcbyk.utils import storage + +# 持久化配置 +DATA_FILE = storage.get_path('cmd_data.json', subdir='data') +DEFAULT_DATA = {} + +# 危险命令检测正则列表 +DANGEROUS_PATTERNS = [ + r'rm\s+-[^ ]*[rf]', # rm -rf, rm -f, rm -rvf + r'git\s+push\s+.*(-f|--force)', # git push -f, git push --force + r'shutdown', # 关机 + r'reboot', # 重启 + r'format\s+[a-zA-Z]:', # Windows 格式化 + r'rd\s+/[sq]', # Windows 静默删除目录 + r'del\s+/[sq]', # Windows 静默删除文件 + r'>\s*/dev/sd', # Linux 直接写磁盘 +] + +def load_commands(): + """从磁盘加载命令数据""" + return storage.load_json(DATA_FILE, default=DEFAULT_DATA, create_if_missing=True) + +def save_commands(commands): + """保存命令数据到磁盘""" + storage.save_json(DATA_FILE, commands) + +def is_dangerous(command): + """检测命令是否包含危险操作""" + for pattern in DANGEROUS_PATTERNS: + if re.search(pattern, command, re.IGNORECASE): + return True + return False + +@click.group(name='cmd', help='Manage reusable command snippets') +def cmd(): + """Manage reusable command snippets.""" + pass + +@cmd.command(name='add') +@click.argument('name') +@click.argument('command') +@click.option('--cwd', '-C', type=click.Path(exists=True, file_okay=False, dir_okay=True, resolve_path=True), help='Specify the default directory for the command') +@click.option('--yes', '-y', is_flag=True, help='Confirm overwrite of existing command') +def add(name, command, cwd, yes): + """Add a new command snippet. + + Supports using {1}, {2} or $1, $2 as placeholders. + Note: When using $1 in double quotes, the Shell might try to expand it. + Use single quotes or {1} syntax to avoid this. + """ + commands = load_commands() + + # 检查是否已存在同名命令 + if name in commands and not yes: + old_cmd = commands[name] + old_cmd_str = old_cmd if isinstance(old_cmd, str) else old_cmd.get("command", "") + if not click.confirm(f"Command '{name}' already exists (Current: {old_cmd_str}). Overwrite?"): + click.echo("Aborted.") + return + + # 存储为字典以支持扩展属性(如 cwd) + cmd_info = { + "command": command + } + if cwd: + # 保存时已经是绝对路径(由 click.Path(resolve_path=True) 处理) + cmd_info["cwd"] = cwd + + commands[name] = cmd_info + save_commands(commands) + + msg = f"Added command '{name}': {command}" + if cwd: + msg += f" (CWD: {cwd})" + click.echo(msg) + + # 检查潜在的 Shell 变量展开问题 + if '$' in command and any(f'${i}' not in command for i in range(1, 10) if f'${i}' in command): + pass + + # 如果发现异常空格,发出警告 + if command.count(' ') > 0 or command.endswith(' '): + click.secho("\nWarning: Your command contains suspicious empty spaces. ", fg="yellow", err=True) + click.secho("If you used $1, $2 in double quotes, the shell might have eaten them.", fg="yellow", err=True) + click.secho("Try using single quotes: fcbyk cmd add name 'command $1'", fg="yellow", err=True) + click.secho("Or use shell-safe syntax: fcbyk cmd add name \"command {1}\"", fg="yellow", err=True) + +@cmd.command(name='run') +@click.argument('name') +@click.argument('args', nargs=-1) +@click.option('--cwd', '-C', type=click.Path(exists=True, file_okay=False, dir_okay=True, resolve_path=True), help='Temporarily specify or override the execution directory') +def run(name, args, cwd): + """Run a saved command snippet with optional arguments.""" + import os + commands = load_commands() + if name in commands: + cmd_data = commands[name] + + # 兼容旧版本的字符串格式 + if isinstance(cmd_data, str): + command = cmd_data + saved_cwd = None + else: + command = cmd_data.get("command", "") + saved_cwd = cmd_data.get("cwd") + + # 优先级:命令行指定的 cwd > 保存的 cwd + target_cwd = cwd if cwd else saved_cwd + + # 检查目录是否存在 + if target_cwd and not os.path.exists(target_cwd): + click.secho(f"Error: Directory '{target_cwd}' not found.", fg="red", err=True) + return + + # 替换占位符:同时支持 $1 和 {1} 风格 + for i, arg in enumerate(args, 1): + command = command.replace(f"${i}", arg) + command = command.replace(f"{{{i}}}", arg) + + # 危险命令检测 + if is_dangerous(command): + click.secho("\n[WARNING] DANGEROUS COMMAND DETECTED!", fg="red", bold=True) + click.secho(f"Command: {command}", fg="red") + if not click.confirm("This command contains potentially harmful operations. Are you sure you want to execute it?", default=False): + click.echo("Execution aborted.") + return + + if target_cwd: + click.echo(f"Running in {target_cwd}: {command}") + else: + click.echo(f"Running: {command}") + + try: + subprocess.run(command, shell=True, cwd=target_cwd) + except Exception as e: + click.secho(f"Error executing command: {e}", fg="red", err=True) + else: + click.echo(f"Command '{name}' not found.") + +@cmd.command(name='list') +def list_cmds(): + """List all saved command snippets.""" + from fcbyk.cli_support import print_commands + print_commands(show_empty=True, leading_newline=False) + +@cmd.command(name='rm') +@click.argument('name') +def rm(name): + """Remove a command snippet.""" + commands = load_commands() + if name in commands: + del commands[name] + save_commands(commands) + click.echo(f"Removed command '{name}'.") + else: + click.echo(f"Command '{name}' not found.") diff --git a/src/fcbyk/commands/get/__init__.py b/src/fcbyk/commands/get/__init__.py new file mode 100644 index 0000000..f6e7de7 --- /dev/null +++ b/src/fcbyk/commands/get/__init__.py @@ -0,0 +1,3 @@ +from .cli import get + +__all__ = ['get'] diff --git a/src/fcbyk/commands/get/cli.py b/src/fcbyk/commands/get/cli.py new file mode 100644 index 0000000..90ab2b2 --- /dev/null +++ b/src/fcbyk/commands/get/cli.py @@ -0,0 +1,85 @@ +import click +import webbrowser +import os +from rich.console import Console +from rich.table import Table +from fcbyk.utils.download import download_file + +# 资源映射表 +RESOURCES = { + "vscode": { + "home": "https://code.visualstudio.com/", + "download": "https://code.visualstudio.com/sha/download?build=stable&os=win32-x64-user", + "filename": "VSCodeUserSetup-x64.exe" + }, + "py": { + "home": "https://www.python.org/", + "download": "https://www.python.org/ftp/python/3.12.1/python-3.12.1-amd64.exe", + "filename": "python-3.12.1-amd64.exe" + } +} + +@click.command() +@click.argument("resource", required=False) +@click.option("-d", "--download", is_flag=True, help="Download the package. Optional: provide a path after -d.") +@click.argument("download_dir", required=False) +@click.option("-l", "--list", "list_resources", is_flag=True, help="List all supported resources") +def get(resource, download, download_dir, list_resources): + """Get resources: opens the official website by default, or downloads the package with -d.""" + + if list_resources: + console = Console() + table = Table(title="Supported Resources") + table.add_column("Name", style="cyan") + table.add_column("Home Page", style="green") + + for name, info in RESOURCES.items(): + table.add_row(name, info["home"]) + + console.print(table) + return + + if not resource: + click.echo(click.get_current_context().get_help()) + return + + # Handle download logic + if download: + # If no download_dir provided after -d, default to current directory + if not download_dir: + download_dir = "." + + res_key = resource.lower() + if res_key not in RESOURCES: + click.secho(f"Resource not found: {resource}", fg="red") + click.echo(f"Currently supported resources: {', '.join(RESOURCES.keys())}") + return + + res = RESOURCES[res_key] + + # Trigger download + if not os.path.exists(download_dir): + try: + os.makedirs(download_dir) + except Exception as e: + click.secho(f"Failed to create directory: {e}", fg="red") + return + + dest_path = os.path.join(download_dir, res["filename"]) + click.echo(f"Starting download for {resource} to {dest_path}...") + try: + download_file(res["download"], dest_path) + click.secho(f"\nDownload completed: {dest_path}", fg="green") + except Exception as e: + click.secho(f"\nDownload failed: {e}", fg="red") + else: + # Open official website + res_key = resource.lower() + if res_key not in RESOURCES: + click.secho(f"Resource not found: {resource}", fg="red") + click.echo(f"Currently supported resources: {', '.join(RESOURCES.keys())}") + return + + res = RESOURCES[res_key] + click.echo(f"Opening {resource} official website: {res['home']}") + webbrowser.open(res["home"]) diff --git a/src/fcbyk/commands/lansend/cli.py b/src/fcbyk/commands/lansend/cli.py index c586079..74048c4 100644 --- a/src/fcbyk/commands/lansend/cli.py +++ b/src/fcbyk/commands/lansend/cli.py @@ -1,98 +1,38 @@ """ -lansend 命令行接口模块 - -对外提供 lansend 命令,用于在局域网内共享文件。 - -函数: -- lansend(): Click 命令入口,提供完整参数选项 +lansend 命令行接口模块 (局域网内共享文件) """ -import os -import webbrowser - -import click - -from fcbyk.cli_support.output import echo_network_urls, show_dict, copy_to_clipboard +import os, webbrowser, click +from fcbyk.cli_support.output import echo_network_urls, copy_to_clipboard from fcbyk.cli_support.guard import check_port -from fcbyk.utils import storage from fcbyk.utils.network import get_private_networks from .controller import start_web_server from .service import LansendConfig, LansendService -def _show_lansend_config(ctx: click.Context, param, value: bool) -> None: - if not value: - return - - try: - data = storage.load_section("fcbyk_config.json", "lansend", default={}) - except Exception: - data = {} - - if not isinstance(data, dict): - data = {} - - show_dict(ctx, param, True, "fcbyk_config.json:lansend", data) - - @click.command(help="Start a local web server for sharing files over LAN") @click.option("-p", "--port", default=80, help="Web server port (default: 80)") @click.option("-d", "--directory", default=".", help="Directory to share (default: current directory)") @click.option( - "-pw", - "--password", + "-ap", + "--ask-password", is_flag=True, default=False, - help="Prompt to set upload password (default: no password, or 123456 if skipped)", + help="Prompt to set upload password (default: 123456 if confirmed)", ) @click.option("-nb", "--no-browser", is_flag=True, help="Disable automatic browser opening") -@click.option("-un-d","--un-download", is_flag=True, default=False, help="Hide download buttons in directory tab") -@click.option("-un-up","--un-upload", is_flag=True, default=False, help="Disable upload functionality") +@click.option("-nd", "--hide-download", is_flag=True, default=False, help="Hide download buttons in directory tab") +@click.option("-nu", "--disable-upload", is_flag=True, default=False, help="Disable upload functionality") @click.option("--chat", is_flag=True, default=False, help="Enable chat functionality") -@click.option( - "--show-config", - is_flag=True, - is_eager=True, - expose_value=False, - callback=_show_lansend_config, - help="Show saved config and exit", -) -@click.option("--last", is_flag=True, default=False, help="Reuse last saved config") -@click.option("--save", is_flag=True, default=False, help="Save current args to config") def lansend( port: int, directory: str, - password: bool = False, + ask_password: bool = False, no_browser: bool = False, - un_download: bool = False, - un_upload: bool = False, + hide_download: bool = False, + disable_upload: bool = False, chat: bool = False, - last: bool = False, - save: bool = False, ): - # --last: 完全复用持久化配置(忽略其它参数) - if last: - try: - cfg = storage.load_section("fcbyk_config.json", "lansend", default=None) - except Exception: - cfg = None - - if not isinstance(cfg, dict): - click.echo("Error: No saved lansend config found. Use --save first.") - return - - directory = str(cfg.get("shared_directory") or ".") - try: - port = int(cfg.get("port") or 80) - except Exception: - port = 80 - - password = bool(cfg.get("password_flag") or False) - no_browser = bool(cfg.get("no_browser") or False) - un_download = bool(cfg.get("un_download") or False) - un_upload = bool(cfg.get("un_upload") or False) - chat = bool(cfg.get("chat") or False) - if not os.path.exists(directory): click.echo(f"Error: Directory {directory} does not exist") return @@ -106,12 +46,12 @@ def lansend( config = LansendConfig( shared_directory=shared_directory, upload_password=None, - un_download=un_download, - un_upload=un_upload, + un_download=hide_download, + un_upload=disable_upload, chat_enabled=chat, ) service = LansendService(config) - config.upload_password = service.pick_upload_password(password, un_upload, click) + config.upload_password = service.pick_upload_password(ask_password, disable_upload, click) click.echo() private_networks = get_private_networks() @@ -126,24 +66,6 @@ def lansend( echo_network_urls(private_networks, port, include_virtual=True) copy_to_clipboard(f"http://{local_ip}:{port}") - if save: - try: - storage.save_section( - "fcbyk_config.json", - "lansend", - { - "shared_directory": shared_directory, - "port": str(port), - "password_flag": bool(password), - "no_browser": bool(no_browser), - "un_download": bool(un_download), - "un_upload": bool(un_upload), - "chat": bool(chat), - }, - ) - except Exception: - pass - if not no_browser: webbrowser.open(f"http://{local_ip}:{port}") click.echo() diff --git a/src/fcbyk/defaults.py b/src/fcbyk/defaults.py new file mode 100644 index 0000000..f094f03 --- /dev/null +++ b/src/fcbyk/defaults.py @@ -0,0 +1,29 @@ +"""fcbyk 默认配置定义 + +该字典的结构与 ~/.fcbyk/fcbyk_config.json 完全一致。 +""" + +CONFIG_FILE = "fcbyk_config.json" + +DEFAULT_CONFIG = { + "ai": { + "model": "deepseek-chat", + "api_url": "https://api.deepseek.com/v1/chat/completions", + "api_key": None, + "stream": False, + "rich": False, + }, + "aliases": { + "ls": ["lansend"], + "run": ["cmd", "run"], + }, + "lansend": { + "port": 80, + "shared_directory": ".", + "password_flag": False, + "no_browser": False, + "un_download": False, + "un_upload": False, + "chat": False, + }, +} diff --git a/src/fcbyk/tests/lansend/test_cli.py b/src/fcbyk/tests/lansend/test_cli.py index ef90919..5522b36 100644 --- a/src/fcbyk/tests/lansend/test_cli.py +++ b/src/fcbyk/tests/lansend/test_cli.py @@ -3,7 +3,7 @@ def test_lansend_help(): from click.testing import CliRunner - from fcbyk.cli import cli + from fcbyk.cli import main - r = CliRunner().invoke(cli, ["lansend", "--help"]) + r = CliRunner().invoke(main, ["lansend", "--help"]) assert r.exit_code == 0 diff --git a/src/fcbyk/tests/pick/test_cli.py b/src/fcbyk/tests/pick/test_cli.py index 014ce16..03945a7 100644 --- a/src/fcbyk/tests/pick/test_cli.py +++ b/src/fcbyk/tests/pick/test_cli.py @@ -3,9 +3,9 @@ def test_pick_help(): from click.testing import CliRunner - from fcbyk.cli import cli + from fcbyk.cli import main - r = CliRunner().invoke(cli, ["pick", "--help"]) + r = CliRunner().invoke(main, ["pick", "--help"]) assert r.exit_code == 0 diff --git a/src/fcbyk/tests/slide/test_cli.py b/src/fcbyk/tests/slide/test_cli.py index 1ecb4ca..8cc6161 100644 --- a/src/fcbyk/tests/slide/test_cli.py +++ b/src/fcbyk/tests/slide/test_cli.py @@ -3,10 +3,10 @@ def test_slide_help(): from click.testing import CliRunner - from fcbyk.cli import cli + from fcbyk.cli import main runner = CliRunner() - r = runner.invoke(cli, ["slide", "--help"]) + r = runner.invoke(main, ["slide", "--help"]) assert r.exit_code == 0 assert "PPT remote control" in r.output.lower() or "ppt" in r.output.lower() diff --git a/src/fcbyk/tests/test_cli.py b/src/fcbyk/tests/test_cli.py index 04d37a2..77e7eea 100644 --- a/src/fcbyk/tests/test_cli.py +++ b/src/fcbyk/tests/test_cli.py @@ -1,19 +1,19 @@ import pytest from click.testing import CliRunner -from fcbyk.cli import cli +from fcbyk.cli import main def test_version_command(): runner = CliRunner() - result = runner.invoke(cli, ['--version']) + result = runner.invoke(main, ['--version']) assert result.exit_code == 0 assert 'v' in result.output def test_lansend_command_help(): runner = CliRunner() - result = runner.invoke(cli, ['lansend', '--help']) + result = runner.invoke(main, ['lansend', '--help']) assert result.exit_code == 0 assert 'Start a local web server for sharing files over LAN' in result.output diff --git a/src/fcbyk/utils/download.py b/src/fcbyk/utils/download.py new file mode 100644 index 0000000..f5ace32 --- /dev/null +++ b/src/fcbyk/utils/download.py @@ -0,0 +1,47 @@ +"""Download utility functions""" +import os +import requests +from rich.progress import ( + Progress, + TextColumn, + BarColumn, + DownloadColumn, + TransferSpeedColumn, + TimeRemainingColumn, +) + +def download_file(url, dest_path): + """ + General download utility with rich progress bar. + Supports Python 3.6+. + + Args: + url (str): Download URL + dest_path (str): Destination path + """ + response = requests.get(url, stream=True) + response.raise_for_status() + + total_size = int(response.headers.get('content-length', 0)) + + # Ensure directory exists + dest_dir = os.path.dirname(os.path.abspath(dest_path)) + if not os.path.exists(dest_dir): + os.makedirs(dest_dir) + + filename = os.path.basename(dest_path) + + with Progress( + TextColumn("[bold blue]{task.description}"), + BarColumn(), + DownloadColumn(), + TransferSpeedColumn(), + TimeRemainingColumn(), + ) as progress: + task = progress.add_task(f"Downloading {filename}", total=total_size if total_size > 0 else None) + + with open(dest_path, 'wb') as f: + for chunk in response.iter_content(chunk_size=8192): + if chunk: + f.write(chunk) + progress.update(task, advance=len(chunk)) diff --git a/web-ui/package.json b/web-ui/package.json index e816fb1..bc5c516 100644 --- a/web-ui/package.json +++ b/web-ui/package.json @@ -19,9 +19,10 @@ "vue-router": "^4.6.4" }, "devDependencies": { + "@tailwindcss/vite": "^4.1.18", "@types/node": "^20.19.27", "@vitejs/plugin-vue": "^5.0.0", - "sass-embedded": "^1.97.1", + "tailwindcss": "^4.1.18", "typescript": "^5.3.0", "vite": "^5.0.0", "vue-tsc": "^1.8.0" diff --git a/web-ui/pnpm-lock.yaml b/web-ui/pnpm-lock.yaml index 312fff1..804167c 100644 --- a/web-ui/pnpm-lock.yaml +++ b/web-ui/pnpm-lock.yaml @@ -33,15 +33,18 @@ importers: specifier: ^4.6.4 version: 4.6.4(vue@3.5.26(typescript@5.9.3)) devDependencies: + '@tailwindcss/vite': + specifier: ^4.1.18 + version: 4.1.18(vite@5.4.21(@types/node@20.19.27)(lightningcss@1.30.2)(sass-embedded@1.97.1)(sass@1.97.1)) '@types/node': specifier: ^20.19.27 version: 20.19.27 '@vitejs/plugin-vue': specifier: ^5.0.0 version: 5.2.4(vite@5.4.21(@types/node@20.19.27)(lightningcss@1.30.2)(sass-embedded@1.97.1)(sass@1.97.1))(vue@3.5.26(typescript@5.9.3)) - sass-embedded: - specifier: ^1.97.1 - version: 1.97.1 + tailwindcss: + specifier: ^4.1.18 + version: 4.1.18 typescript: specifier: ^5.3.0 version: 5.9.3 @@ -224,9 +227,22 @@ packages: highlight.js: ^11.0.1 vue: ^3 + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@parcel/watcher-android-arm64@2.5.1': resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==} engines: {node: '>= 10.0.0'} @@ -439,6 +455,100 @@ packages: '@socket.io/component-emitter@3.1.2': resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} + '@tailwindcss/node@4.1.18': + resolution: {integrity: sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==} + + '@tailwindcss/oxide-android-arm64@4.1.18': + resolution: {integrity: sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.1.18': + resolution: {integrity: sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.1.18': + resolution: {integrity: sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.1.18': + resolution: {integrity: sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18': + resolution: {integrity: sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.18': + resolution: {integrity: sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-arm64-musl@4.1.18': + resolution: {integrity: sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-linux-x64-gnu@4.1.18': + resolution: {integrity: sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-x64-musl@4.1.18': + resolution: {integrity: sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-wasm32-wasi@4.1.18': + resolution: {integrity: sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.18': + resolution: {integrity: sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.1.18': + resolution: {integrity: sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.1.18': + resolution: {integrity: sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==} + engines: {node: '>= 10'} + + '@tailwindcss/vite@4.1.18': + resolution: {integrity: sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA==} + peerDependencies: + vite: ^5.2.0 || ^6 || ^7 + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} @@ -555,6 +665,10 @@ packages: resolution: {integrity: sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==} engines: {node: '>=10.0.0'} + enhanced-resolve@5.18.4: + resolution: {integrity: sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==} + engines: {node: '>=10.13.0'} + entities@7.0.0: resolution: {integrity: sha512-FDWG5cmEYf2Z00IkYRhbFrwIwvdFKH07uV8dvNy0omp/Qb1xcyCWp2UDtcwJF4QZZvk0sLudP6/hAu42TaqVhQ==} engines: {node: '>=0.12'} @@ -576,6 +690,9 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} @@ -603,6 +720,10 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + jiti@2.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + hasBin: true + lightningcss-android-arm64@1.30.2: resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==} engines: {node: '>= 12.0.0'} @@ -884,6 +1005,13 @@ packages: resolution: {integrity: sha512-GTt8rSKje5FilG+wEdfCkOcLL7LWqpMlr2c3LRuKt/YXxcJ52aGSbGBAdI4L3aaqfrBt6y711El53ItyH1NWzg==} engines: {node: '>=16.0.0'} + tailwindcss@4.1.18: + resolution: {integrity: sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==} + + tapable@2.3.0: + resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} + engines: {node: '>=6'} + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -986,7 +1114,8 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 - '@bufbuild/protobuf@2.10.2': {} + '@bufbuild/protobuf@2.10.2': + optional: true '@esbuild/aix-ppc64@0.21.5': optional: true @@ -1066,8 +1195,25 @@ snapshots: highlight.js: 11.11.1 vue: 3.5.26(typescript@5.9.3) + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + '@jridgewell/sourcemap-codec@1.5.5': {} + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + '@parcel/watcher-android-arm64@2.5.1': optional: true @@ -1197,6 +1343,74 @@ snapshots: '@socket.io/component-emitter@3.1.2': {} + '@tailwindcss/node@4.1.18': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.18.4 + jiti: 2.6.1 + lightningcss: 1.30.2 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.1.18 + + '@tailwindcss/oxide-android-arm64@4.1.18': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.1.18': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.1.18': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.1.18': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.18': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.1.18': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.1.18': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.1.18': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.1.18': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.18': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.1.18': + optional: true + + '@tailwindcss/oxide@4.1.18': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.1.18 + '@tailwindcss/oxide-darwin-arm64': 4.1.18 + '@tailwindcss/oxide-darwin-x64': 4.1.18 + '@tailwindcss/oxide-freebsd-x64': 4.1.18 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.18 + '@tailwindcss/oxide-linux-arm64-gnu': 4.1.18 + '@tailwindcss/oxide-linux-arm64-musl': 4.1.18 + '@tailwindcss/oxide-linux-x64-gnu': 4.1.18 + '@tailwindcss/oxide-linux-x64-musl': 4.1.18 + '@tailwindcss/oxide-wasm32-wasi': 4.1.18 + '@tailwindcss/oxide-win32-arm64-msvc': 4.1.18 + '@tailwindcss/oxide-win32-x64-msvc': 4.1.18 + + '@tailwindcss/vite@4.1.18(vite@5.4.21(@types/node@20.19.27)(lightningcss@1.30.2)(sass-embedded@1.97.1)(sass@1.97.1))': + dependencies: + '@tailwindcss/node': 4.1.18 + '@tailwindcss/oxide': 4.1.18 + tailwindcss: 4.1.18 + vite: 5.4.21(@types/node@20.19.27)(lightningcss@1.30.2)(sass-embedded@1.97.1)(sass@1.97.1) + '@types/estree@1.0.8': {} '@types/node@20.19.27': @@ -1302,14 +1516,16 @@ snapshots: fill-range: 7.1.1 optional: true - buffer-builder@0.2.0: {} + buffer-builder@0.2.0: + optional: true chokidar@4.0.3: dependencies: readdirp: 4.1.2 optional: true - colorjs.io@0.5.2: {} + colorjs.io@0.5.2: + optional: true computeds@0.0.1: {} @@ -1324,8 +1540,7 @@ snapshots: detect-libc@1.0.3: optional: true - detect-libc@2.1.2: - optional: true + detect-libc@2.1.2: {} engine.io-client@6.6.4: dependencies: @@ -1341,6 +1556,11 @@ snapshots: engine.io-parser@5.2.3: {} + enhanced-resolve@5.18.4: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.0 + entities@7.0.0: {} esbuild@0.21.5: @@ -1379,13 +1599,17 @@ snapshots: fsevents@2.3.3: optional: true - has-flag@4.0.0: {} + graceful-fs@4.2.11: {} + + has-flag@4.0.0: + optional: true he@1.2.0: {} highlight.js@11.11.1: {} - immutable@5.1.4: {} + immutable@5.1.4: + optional: true is-extglob@2.1.1: optional: true @@ -1398,6 +1622,8 @@ snapshots: is-number@7.0.0: optional: true + jiti@2.6.1: {} + lightningcss-android-arm64@1.30.2: optional: true @@ -1446,7 +1672,6 @@ snapshots: lightningcss-linux-x64-musl: 1.30.2 lightningcss-win32-arm64-msvc: 1.30.2 lightningcss-win32-x64-msvc: 1.30.2 - optional: true lucide-vue-next@0.562.0(vue@3.5.26(typescript@5.9.3)): dependencies: @@ -1522,6 +1747,7 @@ snapshots: rxjs@7.8.2: dependencies: tslib: 2.8.1 + optional: true sass-embedded-all-unknown@1.97.1: dependencies: @@ -1610,6 +1836,7 @@ snapshots: sass-embedded-unknown-all: 1.97.1 sass-embedded-win32-arm64: 1.97.1 sass-embedded-win32-x64: 1.97.1 + optional: true sass@1.97.1: dependencies: @@ -1645,25 +1872,34 @@ snapshots: supports-color@8.1.1: dependencies: has-flag: 4.0.0 + optional: true sync-child-process@1.0.2: dependencies: sync-message-port: 1.1.3 + optional: true - sync-message-port@1.1.3: {} + sync-message-port@1.1.3: + optional: true + + tailwindcss@4.1.18: {} + + tapable@2.3.0: {} to-regex-range@5.0.1: dependencies: is-number: 7.0.0 optional: true - tslib@2.8.1: {} + tslib@2.8.1: + optional: true typescript@5.9.3: {} undici-types@6.21.0: {} - varint@6.0.0: {} + varint@6.0.0: + optional: true vite@5.4.21(@types/node@20.19.27)(lightningcss@1.30.2)(sass-embedded@1.97.1)(sass@1.97.1): dependencies: diff --git a/web-ui/src/pages/lansend/App.vue b/web-ui/src/pages/lansend/App.vue index 050c8ee..2d36ed9 100644 --- a/web-ui/src/pages/lansend/App.vue +++ b/web-ui/src/pages/lansend/App.vue @@ -1,7 +1,6 @@