xb-init 1.3.4__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.
- xb/__init__.py +10 -0
- xb/cli.py +105 -0
- xb/commands/__init__.py +1 -0
- xb/commands/build.py +78 -0
- xb/commands/dev.py +48 -0
- xb/commands/doctor.py +91 -0
- xb/commands/init.py +273 -0
- xb/commands/upgrade.py +63 -0
- xb/commands/version.py +65 -0
- xb/templates/backend/api/__init__.py.j2 +3 -0
- xb/templates/backend/api/config.py.j2 +371 -0
- xb/templates/backend/api/ports.py.j2 +105 -0
- xb/templates/backend/backend_build.py.j2 +29 -0
- xb/templates/backend/main.py.j2 +104 -0
- xb/templates/backend/managers/__init__.py.j2 +7 -0
- xb/templates/backend/managers/logger_manager.py.j2 +42 -0
- xb/templates/backend/managers/path_manager.py.j2 +178 -0
- xb/templates/backend/managers/sudoers_manager.py.j2 +141 -0
- xb/templates/backend/requirements.txt.j2 +4 -0
- xb/templates/configs/global_config.yaml.j2 +4 -0
- xb/templates/configs/secrets.yaml.example.j2 +3 -0
- xb/templates/configs/secrets.yaml.j2 +3 -0
- xb/templates/electron/launcher.js.j2 +153 -0
- xb/templates/electron/main.js.j2 +139 -0
- xb/templates/electron/package.json.j2 +65 -0
- xb/templates/electron/port_diagnostics.js.j2 +187 -0
- xb/templates/electron/resources/postinst.j2 +183 -0
- xb/templates/electron/resources/postrm.j2 +115 -0
- xb/templates/frontend/index.html.j2 +12 -0
- xb/templates/frontend/package.json.j2 +18 -0
- xb/templates/frontend/src/App.vue.j2 +67 -0
- xb/templates/frontend/src/components/ConfigSetup.vue.j2 +254 -0
- xb/templates/frontend/src/components/DashboardHome.vue.j2 +40 -0
- xb/templates/frontend/src/components/FileManager.vue.j2 +627 -0
- xb/templates/frontend/src/components/GitVersionBadge.vue.j2 +306 -0
- xb/templates/frontend/src/components/RefreshButton.vue.j2 +142 -0
- xb/templates/frontend/src/main.js.j2 +5 -0
- xb/templates/frontend/src/style.css.j2 +404 -0
- xb/templates/frontend/vite.config.js.j2 +49 -0
- xb/templates/root/.gitignore.j2 +35 -0
- xb/templates/root/AGENTS.md.j2 +424 -0
- xb/templates/root/README.md.j2 +155 -0
- xb/templates/root/build.sh.j2 +156 -0
- xb/templates/root/dev.sh.j2 +346 -0
- xb/templates/root/pyproject.toml.j2 +23 -0
- xb/templates/scripts/example.sh.j2 +3 -0
- xb/templates/version/hooks/pre-commit.j2 +72 -0
- xb/templates/version/scripts/gui_version_manager.py.j2 +174 -0
- xb/templates/version/scripts/install_hooks.sh.j2 +82 -0
- xb/templates/version/scripts/version_manager.py.j2 +111 -0
- xb/templates/version/scripts/web_version_manager.py.j2 +241 -0
- xb/utils/__init__.py +1 -0
- xb/utils/click_helpers.py +96 -0
- xb/utils/template_engine.py +267 -0
- xb/utils/validators.py +8 -0
- xb/utils/version_check.py +190 -0
- xb_init-1.3.4.dist-info/METADATA +281 -0
- xb_init-1.3.4.dist-info/RECORD +60 -0
- xb_init-1.3.4.dist-info/WHEEL +4 -0
- xb_init-1.3.4.dist-info/entry_points.txt +2 -0
xb/__init__.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""
|
|
2
|
+
xb - Project management tool for UV + FastAPI + Vue3 + Electron desktop applications
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from importlib.metadata import PackageNotFoundError, version as _get_version
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
__version__ = _get_version("xb-init")
|
|
9
|
+
except PackageNotFoundError:
|
|
10
|
+
__version__ = "0.0.0+unknown"
|
xb/cli.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""
|
|
2
|
+
xb CLI 入口
|
|
3
|
+
提供项目初始化、开发、构建等命令
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
|
|
9
|
+
from . import __version__
|
|
10
|
+
|
|
11
|
+
from .commands.build import build
|
|
12
|
+
from .commands.dev import dev
|
|
13
|
+
from .commands.doctor import doctor
|
|
14
|
+
from .commands.init import XbGroup, init_command
|
|
15
|
+
from .commands.upgrade import run_upgrade
|
|
16
|
+
from .commands.version import version
|
|
17
|
+
from .utils.version_check import ensure_check, get_pending_upgrade_hint
|
|
18
|
+
|
|
19
|
+
console = Console()
|
|
20
|
+
COMMAND_ORDER = ["doctor", "init", "dev", "build", "version"]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _format_command_options(cmd: click.Command) -> str:
|
|
24
|
+
params = []
|
|
25
|
+
for param in cmd.params:
|
|
26
|
+
if isinstance(param, click.Argument):
|
|
27
|
+
if isinstance(param.type, click.Choice):
|
|
28
|
+
choices = "|".join(param.type.choices)
|
|
29
|
+
params.append(f"{param.name}: {choices}")
|
|
30
|
+
else:
|
|
31
|
+
params.append(param.name)
|
|
32
|
+
continue
|
|
33
|
+
|
|
34
|
+
if not isinstance(param, click.Option):
|
|
35
|
+
continue
|
|
36
|
+
if param.name == "help":
|
|
37
|
+
continue
|
|
38
|
+
|
|
39
|
+
option = param.opts[0] if param.opts else param.name
|
|
40
|
+
if not param.is_flag:
|
|
41
|
+
metavar = param.metavar or param.name.upper()
|
|
42
|
+
option = f"{option} {metavar}"
|
|
43
|
+
params.append(option)
|
|
44
|
+
|
|
45
|
+
return ", ".join(params)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class ColorfulXbGroup(XbGroup):
|
|
49
|
+
def list_commands(self, ctx):
|
|
50
|
+
existing = set(super().list_commands(ctx))
|
|
51
|
+
ordered = [name for name in COMMAND_ORDER if name in existing]
|
|
52
|
+
ordered.extend(name for name in super().list_commands(ctx) if name not in ordered)
|
|
53
|
+
return ordered
|
|
54
|
+
|
|
55
|
+
def format_help(self, ctx, formatter):
|
|
56
|
+
console.print(
|
|
57
|
+
"\n[bold green]Usage:[/bold green] [cyan]xb [OPTIONS] COMMAND [ARGS]...[/cyan]\n"
|
|
58
|
+
)
|
|
59
|
+
console.print("[dim]xb - UV + FastAPI + Vue3 + Electron 桌面应用项目管理工具[/dim]")
|
|
60
|
+
console.print("[dim]类似 uv,专为 Electron 桌面应用设计。[/dim]\n")
|
|
61
|
+
console.print("[bold green]Options:[/bold green]")
|
|
62
|
+
console.print(" [cyan]--version[/cyan] 显示 xb 工具版本号并退出。")
|
|
63
|
+
console.print(" [cyan]--upgrade[/cyan] 升级 xb 到 PyPI 最新版本并退出。")
|
|
64
|
+
console.print(" [cyan]-h, --help[/cyan] 显示帮助信息并退出。\n")
|
|
65
|
+
console.print("[bold green]Commands:[/bold green]")
|
|
66
|
+
for subcommand in self.list_commands(ctx):
|
|
67
|
+
cmd = self.get_command(ctx, subcommand)
|
|
68
|
+
if cmd is None:
|
|
69
|
+
continue
|
|
70
|
+
help_text = cmd.get_short_help_str()
|
|
71
|
+
console.print(f" [cyan]{subcommand:<10}[/cyan] {help_text}")
|
|
72
|
+
options = _format_command_options(cmd)
|
|
73
|
+
if options:
|
|
74
|
+
console.print(f" [dim]{'':<10} 参数: {options}[/dim]")
|
|
75
|
+
console.print()
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@click.group(cls=ColorfulXbGroup, context_settings={"help_option_names": ["-h", "--help"]})
|
|
79
|
+
@click.version_option(version=__version__, prog_name="xb")
|
|
80
|
+
@click.option("--upgrade", "upgrade_requested", is_flag=True, is_eager=True, help="升级 xb 到 PyPI 最新版本并退出。")
|
|
81
|
+
def main(upgrade_requested: bool):
|
|
82
|
+
"""
|
|
83
|
+
xb - UV + FastAPI + Vue3 + Electron 桌面应用项目管理工具
|
|
84
|
+
|
|
85
|
+
类似 uv,专为 Electron 桌面应用设计。
|
|
86
|
+
"""
|
|
87
|
+
if upgrade_requested:
|
|
88
|
+
run_upgrade()
|
|
89
|
+
raise click.exceptions.Exit()
|
|
90
|
+
|
|
91
|
+
ensure_check(__version__)
|
|
92
|
+
hint = get_pending_upgrade_hint(__version__)
|
|
93
|
+
if hint:
|
|
94
|
+
console.print(f"[yellow]{hint}[/yellow]\n")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
main.add_command(doctor, name="doctor")
|
|
98
|
+
main.add_command(init_command, name="init")
|
|
99
|
+
main.add_command(dev, name="dev")
|
|
100
|
+
main.add_command(build, name="build")
|
|
101
|
+
main.add_command(version, name="version")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
if __name__ == "__main__":
|
|
105
|
+
main()
|
xb/commands/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""xb commands package"""
|
xb/commands/build.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""
|
|
2
|
+
xb build 命令实现
|
|
3
|
+
构建项目
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import subprocess
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
|
|
11
|
+
from ..utils.click_helpers import ChineseHelpCommand, HELP_CONTEXT
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def is_project_root(path: Path) -> bool:
|
|
15
|
+
return (path / "pyproject.toml").exists() and (path / "build.sh").exists()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def find_project_root() -> Path | None:
|
|
19
|
+
cwd = Path.cwd()
|
|
20
|
+
for path in [cwd] + list(cwd.parents):
|
|
21
|
+
if is_project_root(path):
|
|
22
|
+
return path
|
|
23
|
+
return None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _selected_target(target: str, frontend: bool, backend: bool, electron: bool, all_build: bool) -> str:
|
|
27
|
+
selected_flags = [
|
|
28
|
+
("frontend", frontend),
|
|
29
|
+
("backend", backend),
|
|
30
|
+
("electron", electron),
|
|
31
|
+
("all", all_build),
|
|
32
|
+
]
|
|
33
|
+
selected = [name for name, enabled in selected_flags if enabled]
|
|
34
|
+
if len(selected) > 1:
|
|
35
|
+
raise click.UsageError("构建目标只能指定一个")
|
|
36
|
+
if selected:
|
|
37
|
+
if target != "all":
|
|
38
|
+
raise click.UsageError("位置参数和 -a/-f/-b/-e 只能二选一")
|
|
39
|
+
return selected[0]
|
|
40
|
+
return target
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@click.command(cls=ChineseHelpCommand, context_settings=HELP_CONTEXT)
|
|
44
|
+
@click.argument(
|
|
45
|
+
"target",
|
|
46
|
+
required=False,
|
|
47
|
+
default="all",
|
|
48
|
+
type=click.Choice(["all", "frontend", "backend", "electron"], case_sensitive=False),
|
|
49
|
+
)
|
|
50
|
+
@click.option("-a", "all_build", is_flag=True, default=False, help="构建所有")
|
|
51
|
+
@click.option("-f", "frontend", is_flag=True, default=False, help="只构建前端")
|
|
52
|
+
@click.option("-b", "backend", is_flag=True, default=False, help="只构建后端")
|
|
53
|
+
@click.option("-e", "electron", is_flag=True, default=False, help="只构建 Electron DEB")
|
|
54
|
+
def build(target: str, all_build: bool, frontend: bool, backend: bool, electron: bool):
|
|
55
|
+
"""构建项目
|
|
56
|
+
|
|
57
|
+
示例:
|
|
58
|
+
xb build
|
|
59
|
+
xb build all
|
|
60
|
+
xb build frontend
|
|
61
|
+
xb build backend
|
|
62
|
+
xb build electron
|
|
63
|
+
xb build -f
|
|
64
|
+
"""
|
|
65
|
+
project_root = find_project_root()
|
|
66
|
+
if not project_root:
|
|
67
|
+
click.echo("❌ 未找到项目根目录(缺少 pyproject.toml 或 build.sh)")
|
|
68
|
+
raise click.Abort()
|
|
69
|
+
|
|
70
|
+
build_script = project_root / "build.sh"
|
|
71
|
+
selected = _selected_target(target.lower(), frontend, backend, electron, all_build)
|
|
72
|
+
arg_map = {
|
|
73
|
+
"all": "-a",
|
|
74
|
+
"frontend": "-f",
|
|
75
|
+
"backend": "-b",
|
|
76
|
+
"electron": "-e",
|
|
77
|
+
}
|
|
78
|
+
subprocess.run(["bash", str(build_script), arg_map[selected]], cwd=project_root)
|
xb/commands/dev.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""
|
|
2
|
+
xb dev 命令实现
|
|
3
|
+
启动/停止开发环境
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import subprocess
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
|
|
11
|
+
from ..utils.click_helpers import ChineseHelpCommand, HELP_CONTEXT
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def is_project_root(path: Path) -> bool:
|
|
15
|
+
return (path / "pyproject.toml").exists() and (path / "dev.sh").exists()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def find_project_root() -> Path | None:
|
|
19
|
+
cwd = Path.cwd()
|
|
20
|
+
for path in [cwd] + list(cwd.parents):
|
|
21
|
+
if is_project_root(path):
|
|
22
|
+
return path
|
|
23
|
+
return None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@click.command(cls=ChineseHelpCommand, context_settings=HELP_CONTEXT)
|
|
27
|
+
@click.argument(
|
|
28
|
+
"action",
|
|
29
|
+
required=False,
|
|
30
|
+
default="start",
|
|
31
|
+
type=click.Choice(["start", "stop", "status"], case_sensitive=False),
|
|
32
|
+
)
|
|
33
|
+
def dev(action: str):
|
|
34
|
+
"""启动/停止开发环境
|
|
35
|
+
|
|
36
|
+
示例:
|
|
37
|
+
xb dev
|
|
38
|
+
xb dev start
|
|
39
|
+
xb dev stop
|
|
40
|
+
xb dev status
|
|
41
|
+
"""
|
|
42
|
+
project_root = find_project_root()
|
|
43
|
+
if not project_root:
|
|
44
|
+
click.echo("❌ 未找到项目根目录(缺少 pyproject.toml 或 dev.sh)")
|
|
45
|
+
raise click.Abort()
|
|
46
|
+
|
|
47
|
+
dev_script = project_root / "dev.sh"
|
|
48
|
+
subprocess.run(["bash", str(dev_script), action.lower()], cwd=project_root)
|
xb/commands/doctor.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""
|
|
2
|
+
xb doctor 命令实现
|
|
3
|
+
检查本机运行 xb 生成项目所需的基础环境。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import shutil
|
|
9
|
+
import socket
|
|
10
|
+
import subprocess
|
|
11
|
+
import sys
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
|
|
14
|
+
import click
|
|
15
|
+
from rich.console import Console
|
|
16
|
+
from rich.table import Table
|
|
17
|
+
|
|
18
|
+
from .. import __version__
|
|
19
|
+
from ..utils.click_helpers import ChineseHelpCommand, HELP_CONTEXT
|
|
20
|
+
|
|
21
|
+
console = Console()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class CheckResult:
|
|
26
|
+
name: str
|
|
27
|
+
ok: bool
|
|
28
|
+
detail: str
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _command_version(command: str, args: list[str] | None = None) -> str | None:
|
|
32
|
+
path = shutil.which(command)
|
|
33
|
+
if not path:
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
cmd = [command, *(args or ['--version'])]
|
|
37
|
+
try:
|
|
38
|
+
result = subprocess.run(cmd, capture_output=True, text=True, timeout=5, check=False)
|
|
39
|
+
except Exception:
|
|
40
|
+
return path
|
|
41
|
+
|
|
42
|
+
output = (result.stdout or result.stderr).strip().splitlines()
|
|
43
|
+
return output[0] if output else path
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _port_is_free(port: int) -> bool:
|
|
47
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
|
48
|
+
sock.settimeout(0.3)
|
|
49
|
+
return sock.connect_ex(('127.0.0.1', port)) != 0
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _collect_checks() -> list[CheckResult]:
|
|
53
|
+
checks = [
|
|
54
|
+
CheckResult('Python', sys.version_info >= (3, 12), sys.version.split()[0]),
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
for label, command in (
|
|
58
|
+
('uv', 'uv'),
|
|
59
|
+
('Node.js', 'node'),
|
|
60
|
+
('npm', 'npm'),
|
|
61
|
+
('git', 'git'),
|
|
62
|
+
):
|
|
63
|
+
version = _command_version(command)
|
|
64
|
+
checks.append(CheckResult(label, version is not None, version or '未找到'))
|
|
65
|
+
|
|
66
|
+
for port in (8000, 5173):
|
|
67
|
+
free = _port_is_free(port)
|
|
68
|
+
checks.append(CheckResult(f'端口 {port}', free, '可用' if free else '已被占用'))
|
|
69
|
+
|
|
70
|
+
return checks
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@click.command(cls=ChineseHelpCommand, context_settings=HELP_CONTEXT)
|
|
74
|
+
def doctor() -> None:
|
|
75
|
+
"""检查 xb 开发环境"""
|
|
76
|
+
console.print(f'[bold green]环境检查[/bold green] [dim]xb {__version__}[/dim]')
|
|
77
|
+
|
|
78
|
+
table = Table(show_header=True, header_style='bold cyan')
|
|
79
|
+
table.add_column('项目')
|
|
80
|
+
table.add_column('状态')
|
|
81
|
+
table.add_column('详情')
|
|
82
|
+
|
|
83
|
+
checks = _collect_checks()
|
|
84
|
+
for item in checks:
|
|
85
|
+
status = '[green]通过[/green]' if item.ok else '[red]需要处理[/red]'
|
|
86
|
+
table.add_row(item.name, status, item.detail)
|
|
87
|
+
|
|
88
|
+
console.print(table)
|
|
89
|
+
|
|
90
|
+
if not all(item.ok for item in checks):
|
|
91
|
+
console.print('[yellow]提示:[/yellow] 端口占用可先停止相关服务,或修改生成项目的 configs/global_config.yaml。')
|
xb/commands/init.py
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
"""
|
|
2
|
+
xb init 命令实现
|
|
3
|
+
功能: 在当前目录初始化项目结构
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import shutil
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
import click
|
|
13
|
+
from rich.console import Console
|
|
14
|
+
from rich.panel import Panel
|
|
15
|
+
from rich.prompt import Confirm, Prompt
|
|
16
|
+
|
|
17
|
+
from .. import __version__
|
|
18
|
+
from ..utils.click_helpers import ChineseHelpCommand, HELP_CONTEXT
|
|
19
|
+
from ..utils.template_engine import TemplateEngine
|
|
20
|
+
from ..utils.validators import validate_package_name
|
|
21
|
+
from ..utils.version_check import get_latest_if_newer
|
|
22
|
+
|
|
23
|
+
console = Console()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _prompt_upgrade_before_init() -> None:
|
|
27
|
+
"""检测到新版 xb 时询问是否先升级;同意则升级后用 execvp 重跑 init 替换当前进程。
|
|
28
|
+
|
|
29
|
+
execvp 而非新起 subprocess:
|
|
30
|
+
- 升级后的 xb 入口才能加载新版依赖;当前进程已经 import 了旧模板代码
|
|
31
|
+
- 用户原始 sys.argv 直接转交,不丢任何参数(--sudoers 等)
|
|
32
|
+
"""
|
|
33
|
+
latest = get_latest_if_newer(__version__)
|
|
34
|
+
if not latest:
|
|
35
|
+
return
|
|
36
|
+
|
|
37
|
+
if not Confirm.ask(
|
|
38
|
+
f"[yellow]检测到 xb {latest} 可用[/yellow],是否先升级再创建项目?",
|
|
39
|
+
default=True,
|
|
40
|
+
):
|
|
41
|
+
console.print("[dim]跳过升级,使用当前版本继续。[/dim]\n")
|
|
42
|
+
return
|
|
43
|
+
|
|
44
|
+
if not shutil.which("uv"):
|
|
45
|
+
console.print(
|
|
46
|
+
"[red]✗[/red] 未找到 uv 命令,无法升级。"
|
|
47
|
+
"请手动 [cyan]xb upgrade[/cyan] 或继续创建项目。"
|
|
48
|
+
)
|
|
49
|
+
if not Confirm.ask("继续用当前版本创建项目?", default=False):
|
|
50
|
+
raise click.Abort()
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
console.print(f"[cyan]→[/cyan] 升级到 xb-init {latest} ...")
|
|
54
|
+
result = subprocess.run(
|
|
55
|
+
["uv", "tool", "install", "xb-init@latest", "--reinstall"],
|
|
56
|
+
check=False,
|
|
57
|
+
)
|
|
58
|
+
if result.returncode != 0:
|
|
59
|
+
console.print(f"[red]✗[/red] 升级失败 (exit {result.returncode})")
|
|
60
|
+
if not Confirm.ask("继续用当前版本创建项目?", default=False):
|
|
61
|
+
raise click.Abort()
|
|
62
|
+
return
|
|
63
|
+
|
|
64
|
+
console.print("[green]✓[/green] 升级完成,正在用新版本重新执行 init...\n")
|
|
65
|
+
xb_entry = shutil.which("xb")
|
|
66
|
+
if not xb_entry:
|
|
67
|
+
console.print(
|
|
68
|
+
"[yellow]⚠[/yellow] 升级后未找到 xb 命令,请手动重新执行:\n"
|
|
69
|
+
f" [cyan]{' '.join(sys.argv)}[/cyan]"
|
|
70
|
+
)
|
|
71
|
+
raise click.Abort()
|
|
72
|
+
os.execvp(xb_entry, [xb_entry, *sys.argv[1:]])
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def resolve_icon_path(explicit_icon: str | None, package: str) -> Path | None:
|
|
76
|
+
"""解析应用图标路径:显式 --icon 优先,其次查找约定路径。"""
|
|
77
|
+
candidates: list[Path] = []
|
|
78
|
+
|
|
79
|
+
if explicit_icon:
|
|
80
|
+
icon_path = Path(explicit_icon).expanduser()
|
|
81
|
+
if not icon_path.is_absolute():
|
|
82
|
+
icon_path = Path.cwd() / icon_path
|
|
83
|
+
if not icon_path.exists():
|
|
84
|
+
raise click.BadParameter(f"图标文件不存在: {icon_path}", param_hint="--icon")
|
|
85
|
+
return icon_path.resolve()
|
|
86
|
+
|
|
87
|
+
for name in (
|
|
88
|
+
"app-icon.png",
|
|
89
|
+
"icon.png",
|
|
90
|
+
f"{package}.png",
|
|
91
|
+
"assets/app-icon.png",
|
|
92
|
+
"assets/icon.png",
|
|
93
|
+
"resources/icon.png",
|
|
94
|
+
):
|
|
95
|
+
candidates.append(Path.cwd() / name)
|
|
96
|
+
|
|
97
|
+
for candidate in candidates:
|
|
98
|
+
if candidate.exists():
|
|
99
|
+
return candidate.resolve()
|
|
100
|
+
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def init_git_repo(target_dir: Path, package: str) -> bool:
|
|
105
|
+
"""在目标目录初始化 git 仓库并提交首个 commit。
|
|
106
|
+
|
|
107
|
+
成功返回 True;失败仅打印警告,不抛异常(不应阻塞项目创建)。
|
|
108
|
+
"""
|
|
109
|
+
if shutil.which("git") is None:
|
|
110
|
+
console.print("[yellow]⚠[/yellow] 未检测到 git 命令,已跳过仓库初始化")
|
|
111
|
+
return False
|
|
112
|
+
|
|
113
|
+
def _run(cmd: list[str]) -> tuple[bool, str]:
|
|
114
|
+
try:
|
|
115
|
+
r = subprocess.run(
|
|
116
|
+
cmd, cwd=target_dir, capture_output=True, text=True, timeout=15
|
|
117
|
+
)
|
|
118
|
+
return r.returncode == 0, (r.stderr or r.stdout).strip()
|
|
119
|
+
except Exception as e:
|
|
120
|
+
return False, str(e)
|
|
121
|
+
|
|
122
|
+
ok, msg = _run(["git", "init", "-q"])
|
|
123
|
+
if not ok:
|
|
124
|
+
console.print(f"[yellow]⚠[/yellow] git init 失败: {msg}")
|
|
125
|
+
return False
|
|
126
|
+
|
|
127
|
+
_run(["git", "add", "."])
|
|
128
|
+
|
|
129
|
+
ok, msg = _run(
|
|
130
|
+
["git", "commit", "-q", "-m", f"chore: xb init 初始化 {package} 项目"]
|
|
131
|
+
)
|
|
132
|
+
if not ok:
|
|
133
|
+
console.print(
|
|
134
|
+
f"[yellow]⚠[/yellow] git commit 失败(可能未配置 user.name/user.email): {msg}\n"
|
|
135
|
+
f" 稍后请手动: git -C {target_dir} commit -m 'chore: xb init 初始化项目'"
|
|
136
|
+
)
|
|
137
|
+
return False
|
|
138
|
+
|
|
139
|
+
return True
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class ParamSummaryCommand(ChineseHelpCommand):
|
|
143
|
+
pass
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class XbGroup(click.Group):
|
|
147
|
+
def format_commands(self, ctx, formatter):
|
|
148
|
+
commands = []
|
|
149
|
+
for subcommand in self.list_commands(ctx):
|
|
150
|
+
cmd = self.get_command(ctx, subcommand)
|
|
151
|
+
if cmd is None:
|
|
152
|
+
continue
|
|
153
|
+
commands.append((subcommand, cmd.get_short_help_str()))
|
|
154
|
+
|
|
155
|
+
if commands:
|
|
156
|
+
formatter.width - 6 - max(len(cmd[0]) for cmd in commands)
|
|
157
|
+
rows = []
|
|
158
|
+
for subcommand, help_text in commands:
|
|
159
|
+
rows.append((subcommand, help_text))
|
|
160
|
+
|
|
161
|
+
with formatter.section("Commands"):
|
|
162
|
+
formatter.write_dl(rows)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@click.command(cls=ParamSummaryCommand, context_settings=HELP_CONTEXT)
|
|
166
|
+
@click.argument("package")
|
|
167
|
+
@click.option(
|
|
168
|
+
"--sudoers",
|
|
169
|
+
is_flag=True,
|
|
170
|
+
default=False,
|
|
171
|
+
help="启用 sudo 免密配置 (需要输入密码)",
|
|
172
|
+
)
|
|
173
|
+
@click.option(
|
|
174
|
+
"--icon",
|
|
175
|
+
"icon",
|
|
176
|
+
type=click.Path(exists=True, dir_okay=False, path_type=str),
|
|
177
|
+
default=None,
|
|
178
|
+
help="应用图标 PNG 路径;不传时自动查找 ./app-icon.png、./icon.png、./assets/icon.png 等约定路径",
|
|
179
|
+
)
|
|
180
|
+
def init_command(package: str, sudoers: bool, icon: str | None):
|
|
181
|
+
"""
|
|
182
|
+
初始化项目结构
|
|
183
|
+
|
|
184
|
+
在当前目录创建一个完整的 UV + FastAPI + Vue3 + Electron 项目
|
|
185
|
+
|
|
186
|
+
示例:
|
|
187
|
+
xb init demo
|
|
188
|
+
xb init myapp --sudoers
|
|
189
|
+
"""
|
|
190
|
+
# 检测到 PyPI 有新版 xb 时,先询问是否升级再创建项目,避免用旧模板生成项目
|
|
191
|
+
_prompt_upgrade_before_init()
|
|
192
|
+
|
|
193
|
+
# 验证包名
|
|
194
|
+
if not validate_package_name(package):
|
|
195
|
+
console.print(
|
|
196
|
+
f"[red]✗[/red] 项目名称 '{package}' 无效! "
|
|
197
|
+
"请使用小写字母、数字和下划线 (例如: demo, my_app)",
|
|
198
|
+
style="red",
|
|
199
|
+
)
|
|
200
|
+
raise click.Abort()
|
|
201
|
+
|
|
202
|
+
# 获取当前目录
|
|
203
|
+
target_dir = Path.cwd() / package
|
|
204
|
+
|
|
205
|
+
# 检查目录是否已存在
|
|
206
|
+
if target_dir.exists():
|
|
207
|
+
console.print(f"[yellow]⚠[/yellow] 目录 {target_dir} 已存在!")
|
|
208
|
+
if not Confirm.ask("是否覆盖现有目录?", default=False):
|
|
209
|
+
console.print("[yellow]已取消操作[/yellow]")
|
|
210
|
+
raise click.Abort()
|
|
211
|
+
shutil.rmtree(target_dir)
|
|
212
|
+
|
|
213
|
+
icon_path = resolve_icon_path(icon, package)
|
|
214
|
+
if icon_path:
|
|
215
|
+
console.print(f"[green]→[/green] 使用应用图标: [cyan]{icon_path}[/cyan]")
|
|
216
|
+
|
|
217
|
+
# sudo 免密配置
|
|
218
|
+
enable_sudo = False
|
|
219
|
+
sudo_password = ""
|
|
220
|
+
|
|
221
|
+
if sudoers:
|
|
222
|
+
console.print()
|
|
223
|
+
console.print(
|
|
224
|
+
Panel.fit(
|
|
225
|
+
"[bold cyan]Sudo 免密配置[/bold cyan]\n\n"
|
|
226
|
+
"启用 sudo 免密执行特定命令。\n"
|
|
227
|
+
"密码将以明文存储在 configs/secrets.yaml 中,\n"
|
|
228
|
+
"请确保该文件权限设置为 600 (仅所有者可读写)。",
|
|
229
|
+
border_style="cyan",
|
|
230
|
+
)
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
sudo_password = Prompt.ask("[cyan]请输入 sudo 密码[/cyan]", password=True)
|
|
234
|
+
enable_sudo = True
|
|
235
|
+
|
|
236
|
+
# 创建项目
|
|
237
|
+
console.print()
|
|
238
|
+
console.print(f"[green]→[/green] 正在创建项目 [bold]{package}[/bold] ...")
|
|
239
|
+
|
|
240
|
+
try:
|
|
241
|
+
engine = TemplateEngine()
|
|
242
|
+
engine.render_project(
|
|
243
|
+
target_dir=target_dir,
|
|
244
|
+
package_name=package,
|
|
245
|
+
enable_sudo=enable_sudo,
|
|
246
|
+
sudo_password=sudo_password,
|
|
247
|
+
icon_path=icon_path,
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
git_ok = init_git_repo(target_dir, package)
|
|
251
|
+
git_line = (
|
|
252
|
+
"[green]→[/green] 已初始化 git 仓库并完成首个 commit\n"
|
|
253
|
+
if git_ok
|
|
254
|
+
else "[yellow]⚠[/yellow] git 仓库未初始化,请稍后手动 git init\n"
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
console.print()
|
|
258
|
+
console.print(
|
|
259
|
+
Panel.fit(
|
|
260
|
+
f"[bold green]✓ 项目创建成功![/bold green]\n\n"
|
|
261
|
+
f"项目位置: [cyan]{target_dir}[/cyan]\n"
|
|
262
|
+
f"{git_line}\n"
|
|
263
|
+
f"[bold]下一步:[/bold]\n"
|
|
264
|
+
f" cd {package}\n"
|
|
265
|
+
f" bash dev.sh start # 启动开发环境\n\n"
|
|
266
|
+
f"[dim]更多命令请查看 README.md 与 AGENTS.md[/dim]",
|
|
267
|
+
border_style="green",
|
|
268
|
+
)
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
except Exception as e:
|
|
272
|
+
console.print(f"[red]✗[/red] 创建项目失败: {e}", style="red")
|
|
273
|
+
raise click.Abort() from None
|
xb/commands/upgrade.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""
|
|
2
|
+
xb upgrade 命令实现
|
|
3
|
+
|
|
4
|
+
封装 `uv tool install xb-init@latest --reinstall`,让用户一行命令搞定升级。
|
|
5
|
+
|
|
6
|
+
为什么不用 `uv tool upgrade`:
|
|
7
|
+
- uv tool 默认装包时把版本 exact-pin 到 pyproject 里 (xb-init==1.1.5)
|
|
8
|
+
- `uv tool upgrade` 命令明确不破坏已有 pin,对 pin 用户它是 no-op
|
|
9
|
+
- 必须用 `uv tool install <pkg>@latest --reinstall` 才能跨过 pin 取 PyPI 最新
|
|
10
|
+
|
|
11
|
+
为什么不用 pip:xb 是 uv tool 装的,pip 会和 uv 管理的 venv 错乱。
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import shutil
|
|
17
|
+
import subprocess
|
|
18
|
+
import sys
|
|
19
|
+
|
|
20
|
+
import click
|
|
21
|
+
|
|
22
|
+
from ..utils.click_helpers import ChineseHelpCommand, HELP_CONTEXT
|
|
23
|
+
from rich.console import Console
|
|
24
|
+
|
|
25
|
+
console = Console()
|
|
26
|
+
|
|
27
|
+
PACKAGE_NAME = "xb-init"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def run_upgrade() -> None:
|
|
31
|
+
"""升级 xb 到 PyPI 最新版本"""
|
|
32
|
+
if not shutil.which("uv"):
|
|
33
|
+
console.print(
|
|
34
|
+
"[red]❌ 未找到 uv 命令。[/red]"
|
|
35
|
+
"请先安装 uv:[cyan]curl -LsSf https://astral.sh/uv/install.sh | sh[/cyan]"
|
|
36
|
+
)
|
|
37
|
+
sys.exit(1)
|
|
38
|
+
|
|
39
|
+
cmd = ["uv", "tool", "install", f"{PACKAGE_NAME}@latest", "--reinstall"]
|
|
40
|
+
|
|
41
|
+
console.print(f"[dim]运行: {' '.join(cmd)}[/dim]")
|
|
42
|
+
try:
|
|
43
|
+
result = subprocess.run(cmd, check=False)
|
|
44
|
+
except FileNotFoundError:
|
|
45
|
+
console.print("[red]❌ uv 命令执行失败[/red]")
|
|
46
|
+
sys.exit(1)
|
|
47
|
+
|
|
48
|
+
if result.returncode == 0:
|
|
49
|
+
console.print("[green]✅ 升级完成[/green]")
|
|
50
|
+
else:
|
|
51
|
+
console.print(f"[red]❌ 升级失败 (exit {result.returncode})[/red]")
|
|
52
|
+
sys.exit(result.returncode)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@click.command(cls=ChineseHelpCommand, context_settings=HELP_CONTEXT)
|
|
56
|
+
@click.option(
|
|
57
|
+
"--force",
|
|
58
|
+
is_flag=True,
|
|
59
|
+
help="无视 pin 强制装 PyPI 最新版(已是默认行为,保留兼容)",
|
|
60
|
+
)
|
|
61
|
+
def upgrade(force: bool) -> None:
|
|
62
|
+
"""升级 xb 到 PyPI 最新版本"""
|
|
63
|
+
run_upgrade()
|