wv-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.
wv_cli/__init__.py ADDED
File without changes
File without changes
@@ -0,0 +1,113 @@
1
+ """wv build — production build with optional Windows installer packaging."""
2
+ import os
3
+ import platform
4
+ import sys
5
+
6
+ import click
7
+
8
+ from ..utils import find_project_root, load_config, run_cmd, fix_router_history, ensure_npm_deps, inject_favicon
9
+
10
+
11
+ @click.command('build')
12
+ @click.option(
13
+ '--publish',
14
+ is_flag=True,
15
+ default=False,
16
+ help='Also create a Windows installer with Inno Setup after building.',
17
+ )
18
+ def build(publish: bool):
19
+ """Build the project for production.
20
+
21
+ Use --publish to additionally generate a Windows installer via Inno Setup.
22
+ """
23
+
24
+ project_root = find_project_root()
25
+ config = load_config(project_root)
26
+
27
+ project_name = config['project']['name']
28
+ version = config['project']['version']
29
+
30
+ click.echo(f'🏗 生产构建:{project_name} v{version}')
31
+
32
+ # 1. Fix Vue Router history mode
33
+ fix_router_history(project_root)
34
+
35
+ # 2. Build frontend
36
+ frontend_dir = os.path.join(project_root, 'frontend')
37
+ click.echo('\n📦 构建前端…')
38
+ ensure_npm_deps(frontend_dir)
39
+ run_cmd(['npm', 'run', 'build'], cwd=frontend_dir)
40
+ inject_favicon(project_root)
41
+
42
+ # Verify frontend/dist
43
+ dist_dir = os.path.join(frontend_dir, 'dist')
44
+ if not os.path.isdir(dist_dir):
45
+ raise click.ClickException(
46
+ "frontend/dist 不存在。\n"
47
+ "请检查 npm run build 是否成功执行,然后重试。"
48
+ )
49
+
50
+ # 3. Run PyInstaller
51
+ build_dir = os.path.join(project_root, 'build')
52
+ spec_file = os.path.join(build_dir, f'{project_name}.spec')
53
+
54
+ if not os.path.isfile(spec_file):
55
+ raise click.ClickException(
56
+ f"未找到 spec 文件:{spec_file}\n"
57
+ "请确认当前目录是 wv 项目根目录,且 build/ 目录完整。"
58
+ )
59
+
60
+ click.echo('\n📦 PyInstaller 打包…')
61
+ backend_dir = os.path.join(project_root, 'backend')
62
+ run_cmd(
63
+ ['uv', 'run', 'pyinstaller', spec_file, '--distpath', os.path.join(build_dir, 'dist')],
64
+ cwd=backend_dir,
65
+ )
66
+
67
+ click.echo(f'\n✔ 构建完成:build/dist/{project_name}/')
68
+
69
+ # 4. Optional: Inno Setup packaging (Windows only)
70
+ if publish:
71
+ _publish_installer(project_root, config, project_name, version, build_dir)
72
+
73
+
74
+ def _publish_installer(
75
+ project_root: str,
76
+ config: dict,
77
+ project_name: str,
78
+ version: str,
79
+ build_dir: str,
80
+ ) -> None:
81
+ """Generate a Windows installer using Inno Setup."""
82
+
83
+ if platform.system() != 'Windows':
84
+ click.echo(
85
+ '\n⚠ --publish 仅支持 Windows 平台(需要 Inno Setup),已跳过安装包生成。'
86
+ )
87
+ return
88
+
89
+ inno_path = config.get('build', {}).get(
90
+ 'inno_setup_path',
91
+ 'C:/Program Files (x86)/Inno Setup 6/ISCC.exe',
92
+ )
93
+
94
+ if not os.path.isfile(inno_path):
95
+ raise click.ClickException(
96
+ f"未找到 Inno Setup:{inno_path}\n"
97
+ "请安装 Inno Setup 后在 wv.toml 中配置正确的 inno_setup_path。\n"
98
+ "下载:https://jrsoftware.org/isdl.php"
99
+ )
100
+
101
+ iss_file = os.path.join(build_dir, f'{project_name}.iss')
102
+ if not os.path.isfile(iss_file):
103
+ raise click.ClickException(
104
+ f"未找到 iss 文件:{iss_file}"
105
+ )
106
+
107
+ click.echo('\n📦 Inno Setup 打包安装程序…')
108
+ run_cmd([inno_path, iss_file])
109
+
110
+ installer = os.path.join(
111
+ build_dir, 'publish', f'{project_name}-{version}-setup.exe'
112
+ )
113
+ click.echo(f'\n✔ 安装包已生成:{installer}')
@@ -0,0 +1,178 @@
1
+ """wv create — scaffold a new pywebview + Vue3 project."""
2
+ import os
3
+ import shutil
4
+
5
+ import click
6
+ import toml
7
+
8
+ from ..utils import require_node, require_uv, run_cmd
9
+ from ..templates import (
10
+ WV_TOML,
11
+ CONFIG_PY,
12
+ MAIN_PY,
13
+ BRIDGE_INIT_PY,
14
+ BRIDGE_API_PY,
15
+ SPEC_FILE,
16
+ ISS_FILE,
17
+ ROOT_GITIGNORE,
18
+ BACKEND_GITIGNORE,
19
+ )
20
+
21
+ # 包内默认图标目录:wv_cli/icon/
22
+ _PKG_ICON_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'icon')
23
+
24
+
25
+ # ---------------------------------------------------------------------------
26
+ # Directory / file creation helpers
27
+ # ---------------------------------------------------------------------------
28
+
29
+ def _makedirs(path: str) -> None:
30
+ os.makedirs(path, exist_ok=True)
31
+
32
+
33
+ def _write_text(path: str, content: str, overwrite: bool = False) -> None:
34
+ """Write text to path; skip if file already exists and overwrite is False."""
35
+ if os.path.exists(path) and not overwrite:
36
+ return
37
+ os.makedirs(os.path.dirname(path), exist_ok=True)
38
+ with open(path, 'w', encoding='utf-8') as f:
39
+ f.write(content)
40
+
41
+
42
+ def _copy_default_icons(project_dir: str) -> None:
43
+ """
44
+ Copy default favicon.ico and logo.png from the wv-cli package's icon/
45
+ directory into the project's icon/ directory.
46
+ Skips files that already exist (safe for `wv create .`).
47
+ """
48
+ for filename in ('favicon.ico', 'logo.png'):
49
+ src = os.path.join(_PKG_ICON_DIR, filename)
50
+ dst = os.path.join(project_dir, 'icon', filename)
51
+ if os.path.exists(dst):
52
+ continue
53
+ if os.path.isfile(src):
54
+ shutil.copy2(src, dst)
55
+ else:
56
+ click.echo(f' ⚠ 未找到默认图标:{src},跳过复制')
57
+
58
+
59
+ def _scaffold_directories(project_dir: str) -> None:
60
+ dirs = [
61
+ 'icon',
62
+ 'frontend',
63
+ 'backend/src/bridge',
64
+ 'backend/tests',
65
+ 'build/publish',
66
+ ]
67
+ for d in dirs:
68
+ _makedirs(os.path.join(project_dir, d))
69
+
70
+
71
+ def _scaffold_files(
72
+ project_dir: str,
73
+ project_name: str,
74
+ version: str,
75
+ window_title: str,
76
+ author: str,
77
+ ) -> None:
78
+ ctx = dict(
79
+ project_name=project_name,
80
+ version=version,
81
+ window_title=window_title,
82
+ author=author,
83
+ )
84
+
85
+ _write_text(os.path.join(project_dir, 'wv.toml'), WV_TOML.format(**ctx))
86
+
87
+ _write_text(
88
+ os.path.join(project_dir, 'backend', 'src', 'config.py'),
89
+ CONFIG_PY.format(**ctx),
90
+ )
91
+
92
+ _write_text(os.path.join(project_dir, 'backend', 'src', 'main.py'), MAIN_PY)
93
+
94
+ _write_text(
95
+ os.path.join(project_dir, 'backend', 'src', 'bridge', '__init__.py'),
96
+ BRIDGE_INIT_PY,
97
+ )
98
+
99
+ _write_text(
100
+ os.path.join(project_dir, 'backend', 'src', 'bridge', 'api.py'),
101
+ BRIDGE_API_PY,
102
+ )
103
+
104
+ _write_text(
105
+ os.path.join(project_dir, 'build', f'{project_name}.spec'),
106
+ SPEC_FILE.format(**ctx),
107
+ )
108
+
109
+ _write_text(
110
+ os.path.join(project_dir, 'build', f'{project_name}.iss'),
111
+ ISS_FILE.format(**ctx),
112
+ )
113
+
114
+ # 项目根目录 .gitignore
115
+ _write_text(os.path.join(project_dir, '.gitignore'), ROOT_GITIGNORE)
116
+
117
+
118
+ # ---------------------------------------------------------------------------
119
+ # wv create command
120
+ # ---------------------------------------------------------------------------
121
+
122
+ @click.command('create')
123
+ @click.argument('directory', required=False, default=None)
124
+ def create(directory):
125
+ """Create a new pywebview + Vue3 desktop app project."""
126
+
127
+ cwd = os.path.abspath(os.getcwd())
128
+
129
+ if directory is None:
130
+ project_name = click.prompt('项目名称', default='my-app')
131
+ project_dir = os.path.join(cwd, project_name)
132
+ elif directory == '.':
133
+ default_name = os.path.basename(cwd)
134
+ project_name = click.prompt('项目名称', default=default_name)
135
+ project_dir = cwd
136
+ else:
137
+ project_dir = os.path.abspath(directory)
138
+ project_name = click.prompt('项目名称', default=os.path.basename(project_dir))
139
+
140
+ window_title = click.prompt('窗口标题', default=project_name)
141
+ version = click.prompt('版本号', default='1.0.0')
142
+ author = click.prompt('作者', default='')
143
+
144
+ click.echo('\n🔍 检查运行环境…')
145
+ require_node()
146
+ click.echo(' ✔ Node.js / npm')
147
+ require_uv()
148
+ click.echo(' ✔ uv')
149
+
150
+ click.echo('\n📁 创建项目结构…')
151
+ _scaffold_directories(project_dir)
152
+ _scaffold_files(project_dir, project_name, version, window_title, author)
153
+ _copy_default_icons(project_dir)
154
+ click.echo(' ✔ 目录与文件已生成')
155
+
156
+ click.echo('\n🖼 初始化前端(npm create vue@latest)…')
157
+ frontend_dir = os.path.join(project_dir, 'frontend')
158
+ _makedirs(frontend_dir)
159
+ run_cmd(['npm', 'create', 'vue@latest', '.'], cwd=frontend_dir)
160
+
161
+ click.echo('\n🐍 初始化后端(uv)…')
162
+ backend_dir = os.path.join(project_dir, 'backend')
163
+ run_cmd(['uv', 'init', '--no-workspace', '--vcs', 'none'], cwd=backend_dir)
164
+ run_cmd(['uv', 'venv'], cwd=backend_dir)
165
+ run_cmd(['uv', 'add', 'pywebview', 'pyinstaller'], cwd=backend_dir)
166
+
167
+ _write_text(os.path.join(backend_dir, '.gitignore'), BACKEND_GITIGNORE)
168
+
169
+ rel = os.path.relpath(project_dir, cwd)
170
+ cd_hint = f'\n cd {rel}' if rel != '.' else ''
171
+
172
+ click.echo(f"""
173
+ ✔ 项目创建完成!{cd_hint}
174
+
175
+ 下一步:
176
+ wv run # 开发模式运行(构建前端 + 启动 pywebview)
177
+ wv build # 生产构建(PyInstaller 打包)
178
+ """)
wv_cli/commands/run.py ADDED
@@ -0,0 +1,39 @@
1
+ """wv run — development mode: build frontend then launch pywebview."""
2
+ import os
3
+
4
+ import click
5
+
6
+ from ..utils import find_project_root, load_config, run_cmd, fix_router_history, ensure_npm_deps, inject_favicon
7
+
8
+
9
+ @click.command('run')
10
+ def run():
11
+ """Run the app in development mode (builds frontend, then starts pywebview)."""
12
+
13
+ project_root = find_project_root()
14
+ config = load_config(project_root)
15
+
16
+ click.echo('🔧 开发模式启动…')
17
+
18
+ # 1. Fix Vue Router history mode for file:// compatibility
19
+ fix_router_history(project_root)
20
+
21
+ # 2. Build frontend
22
+ frontend_dir = os.path.join(project_root, 'frontend')
23
+ click.echo('\n📦 构建前端…')
24
+ ensure_npm_deps(frontend_dir)
25
+ run_cmd(['npm', 'run', 'build'], cwd=frontend_dir)
26
+ inject_favicon(project_root)
27
+
28
+ # 3. Verify frontend/dist exists
29
+ dist_dir = os.path.join(frontend_dir, 'dist')
30
+ if not os.path.isdir(dist_dir):
31
+ raise click.ClickException(
32
+ "frontend/dist 不存在。\n"
33
+ "请检查 npm run build 是否成功执行。"
34
+ )
35
+
36
+ # 4. Launch pywebview via uv
37
+ backend_dir = os.path.join(project_root, 'backend')
38
+ click.echo('\n🚀 启动 pywebview…')
39
+ run_cmd(['uv', 'run', 'src/main.py'], cwd=backend_dir)
Binary file
wv_cli/icon/logo.png ADDED
Binary file
wv_cli/main.py ADDED
@@ -0,0 +1,17 @@
1
+ """wv-cli: Scaffold tool for pywebview + Vue3 desktop apps."""
2
+ import click
3
+ from .commands.create import create
4
+ from .commands.run import run
5
+ from .commands.build import build
6
+
7
+
8
+ @click.group()
9
+ @click.version_option(version="0.1.0", prog_name="wv")
10
+ def cli():
11
+ """wv-cli — pywebview + Vue3 desktop app scaffold tool."""
12
+ pass
13
+
14
+
15
+ cli.add_command(create)
16
+ cli.add_command(run)
17
+ cli.add_command(build)
wv_cli/templates.py ADDED
@@ -0,0 +1,212 @@
1
+ """
2
+ Template content for files generated by `wv create`.
3
+ All templates are plain strings; variable substitution is done in commands/create.py.
4
+ """
5
+
6
+ WV_TOML = """\
7
+ [project]
8
+ name = "{project_name}"
9
+ version = "{version}"
10
+ window_title = "{window_title}"
11
+ author = "{author}"
12
+
13
+ [build]
14
+ # Windows default path; ignored on non-Windows platforms
15
+ inno_setup_path = "C:/Program Files (x86)/Inno Setup 6/ISCC.exe"
16
+ """
17
+
18
+ CONFIG_PY = """\
19
+ # Development mode: load index.html directly from frontend/dist
20
+ HTML_PATH_DEV = '../../frontend/dist/index.html'
21
+
22
+ # Packaged mode: PyInstaller bundles frontend/dist contents into _f_dist
23
+ HTML_PATH_APP = '_f_dist/index.html'
24
+
25
+ # Window title (injected from wv.toml at project creation time)
26
+ WINDOW_TITLE = "{window_title}"
27
+ """
28
+
29
+ MAIN_PY = """\
30
+ import sys
31
+ import os
32
+ import webview
33
+ from config import WINDOW_TITLE, HTML_PATH_DEV, HTML_PATH_APP
34
+
35
+
36
+ def get_html_path() -> str:
37
+ \"\"\"Resolve the correct HTML path depending on the runtime environment.\"\"\"
38
+ if getattr(sys, 'frozen', False):
39
+ # PyInstaller packaged environment: use sys._MEIPASS to locate resources
40
+ base = sys._MEIPASS
41
+ return os.path.join(base, HTML_PATH_APP)
42
+ else:
43
+ # Development / testing environment: use relative path to frontend/dist
44
+ base = os.path.dirname(os.path.abspath(__file__))
45
+ return os.path.join(base, HTML_PATH_DEV)
46
+
47
+
48
+ def main():
49
+ from bridge.api import Api
50
+ api = Api()
51
+ window = webview.create_window(
52
+ WINDOW_TITLE,
53
+ url=get_html_path(),
54
+ js_api=api,
55
+ )
56
+ webview.start()
57
+
58
+
59
+ if __name__ == '__main__':
60
+ main()
61
+ """
62
+
63
+ BRIDGE_INIT_PY = """\
64
+ # bridge package: pywebview JS API bridge classes live here
65
+ """
66
+
67
+ BRIDGE_API_PY = """\
68
+ class Api:
69
+ \"\"\"
70
+ pywebview JS API example.
71
+ The frontend calls methods of this class via window.pywebview.api.<method>().
72
+ Add your own methods below and expose them to the Vue frontend.
73
+ \"\"\"
74
+
75
+ def greet(self, name: str) -> str:
76
+ return f"Hello, {name}!"
77
+ """
78
+
79
+ SPEC_FILE = """\
80
+ # -*- mode: python ; coding: utf-8 -*-
81
+ block_cipher = None
82
+
83
+ a = Analysis(
84
+ ['../backend/src/main.py'],
85
+ pathex=['../backend/src'],
86
+ binaries=[],
87
+ datas=[
88
+ ('../frontend/dist', '_f_dist'), # Bundle frontend build output
89
+ ('../icon', 'icon'), # Icon resources
90
+ ],
91
+ hiddenimports=['webview'],
92
+ hookspath=[],
93
+ runtime_hooks=[],
94
+ excludes=[],
95
+ win_no_prefer_redirects=False,
96
+ win_private_assemblies=False,
97
+ cipher=block_cipher,
98
+ )
99
+
100
+ pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
101
+
102
+ exe = EXE(
103
+ pyz,
104
+ a.scripts,
105
+ [],
106
+ exclude_binaries=True,
107
+ name='{project_name}',
108
+ debug=False,
109
+ bootloader_ignore_signals=False,
110
+ strip=False,
111
+ upx=True,
112
+ console=False,
113
+ icon='../icon/favicon.ico',
114
+ )
115
+
116
+ coll = COLLECT(
117
+ exe,
118
+ a.binaries,
119
+ a.zipfiles,
120
+ a.datas,
121
+ strip=False,
122
+ upx=True,
123
+ upx_exclude=[],
124
+ name='{project_name}',
125
+ )
126
+ """
127
+
128
+ ISS_FILE = """\
129
+ [Setup]
130
+ AppName={project_name}
131
+ AppVersion={version}
132
+ DefaultDirName={{autopf}}\\{project_name}
133
+ DefaultGroupName={project_name}
134
+ OutputDir=publish
135
+ OutputBaseFilename={project_name}-{version}-setup
136
+ SetupIconFile=../icon/favicon.ico
137
+ Compression=lzma
138
+ SolidCompression=yes
139
+
140
+ [Files]
141
+ Source: "../build/dist/{project_name}/*"; DestDir: "{{app}}"; Flags: recursesubdirs createallsubdirs
142
+
143
+ [Icons]
144
+ Name: "{{group}}\\{project_name}"; Filename: "{{app}}\\{project_name}.exe"
145
+ Name: "{{commondesktop}}\\{project_name}"; Filename: "{{app}}\\{project_name}.exe"
146
+
147
+ [Run]
148
+ Filename: "{{app}}\\{project_name}.exe"; Description: "立即启动"; Flags: nowait postinstall skipifsilent
149
+ """
150
+
151
+ ROOT_GITIGNORE = """\
152
+ # ── Build output ─────────────────────────────────────────────────────────────
153
+ build/dist/
154
+ build/publish/
155
+
156
+ # ── Frontend ──────────────────────────────────────────────────────────────────
157
+ frontend/dist/
158
+ frontend/node_modules/
159
+
160
+ # ── Backend ───────────────────────────────────────────────────────────────────
161
+ backend/.venv/
162
+ backend/__pycache__/
163
+ backend/**/__pycache__/
164
+
165
+ # ── OS / IDE ──────────────────────────────────────────────────────────────────
166
+ .DS_Store
167
+ Thumbs.db
168
+ .vscode/
169
+ .idea/
170
+ """
171
+
172
+ BACKEND_GITIGNORE = """\
173
+ # Python
174
+ __pycache__/
175
+ *.py[cod]
176
+ *.pyo
177
+ *.pyd
178
+ *.egg
179
+ *.egg-info/
180
+
181
+ # Virtual environment (uv)
182
+ .venv/
183
+
184
+ # uv
185
+ .uv/
186
+ uv.lock
187
+
188
+ # Distribution
189
+ dist/
190
+ build/
191
+
192
+ # PyInstaller
193
+ *.spec.bak
194
+
195
+ # Testing
196
+ .pytest_cache/
197
+ .coverage
198
+ htmlcov/
199
+
200
+ # IDE
201
+ .vscode/
202
+ .idea/
203
+ *.swp
204
+ *.swo
205
+
206
+ # OS
207
+ .DS_Store
208
+ Thumbs.db
209
+
210
+ # Logs
211
+ *.log
212
+ """
wv_cli/utils.py ADDED
@@ -0,0 +1,181 @@
1
+ """Shared utility functions for wv-cli."""
2
+ import os
3
+ import platform
4
+ import re
5
+ import shutil
6
+ import subprocess
7
+ import sys
8
+
9
+ import click
10
+ import toml
11
+
12
+
13
+ # ---------------------------------------------------------------------------
14
+ # wv.toml helpers
15
+ # ---------------------------------------------------------------------------
16
+
17
+ def load_config(project_root: str) -> dict:
18
+ """Load wv.toml from the project root. Raises click.ClickException on failure."""
19
+ config_path = os.path.join(project_root, "wv.toml")
20
+ if not os.path.isfile(config_path):
21
+ raise click.ClickException(
22
+ f"wv.toml not found in {project_root}. "
23
+ "Are you inside a wv project directory?"
24
+ )
25
+ with open(config_path, "r", encoding="utf-8") as f:
26
+ return toml.load(f)
27
+
28
+
29
+ def find_project_root() -> str:
30
+ """Walk up from cwd until wv.toml is found. Returns the directory path."""
31
+ cwd = os.path.abspath(os.getcwd())
32
+ candidate = cwd
33
+ while True:
34
+ if os.path.isfile(os.path.join(candidate, "wv.toml")):
35
+ return candidate
36
+ parent = os.path.dirname(candidate)
37
+ if parent == candidate:
38
+ raise click.ClickException(
39
+ "wv.toml not found. Run this command from inside a wv project."
40
+ )
41
+ candidate = parent
42
+
43
+
44
+ # ---------------------------------------------------------------------------
45
+ # Environment checks
46
+ # ---------------------------------------------------------------------------
47
+
48
+ def check_command(cmd: str) -> bool:
49
+ """Return True if the shell command is available on PATH."""
50
+ return shutil.which(cmd) is not None
51
+
52
+
53
+ def require_node():
54
+ """Abort with a helpful message if node/npm is missing."""
55
+ if not check_command("node") or not check_command("npm"):
56
+ raise click.ClickException(
57
+ "Node.js / npm not found.\n"
58
+ "Please install Node.js from: https://nodejs.org"
59
+ )
60
+
61
+
62
+ def require_uv():
63
+ """Abort with a helpful message if uv is missing."""
64
+ if not check_command("uv"):
65
+ raise click.ClickException(
66
+ "uv not found.\n"
67
+ "Install it with: pip install uv\n"
68
+ " or (Windows): winget install astral-sh.uv"
69
+ )
70
+
71
+
72
+ # ---------------------------------------------------------------------------
73
+ # Subprocess helpers
74
+ # ---------------------------------------------------------------------------
75
+
76
+ def _resolve_cmd(cmd: str) -> str:
77
+ """On Windows, resolve e.g. 'npm' → 'npm.cmd' so subprocess can find it."""
78
+ if platform.system() == "Windows":
79
+ resolved = shutil.which(cmd)
80
+ if resolved:
81
+ return resolved
82
+ return cmd
83
+
84
+
85
+ def ensure_npm_deps(frontend_dir: str) -> None:
86
+ """Run `npm install` if node_modules is missing or package.json has changed."""
87
+ node_modules = os.path.join(frontend_dir, 'node_modules')
88
+ if not os.path.isdir(node_modules):
89
+ click.echo(' 📥 安装前端依赖(npm install)…')
90
+ run_cmd(['npm', 'install'], cwd=frontend_dir)
91
+
92
+
93
+ def run_cmd(args: list, cwd: str = None, shell: bool = False):
94
+ """Run a command, streaming output to the terminal. Raise on non-zero exit."""
95
+ args = [_resolve_cmd(args[0])] + args[1:]
96
+ click.echo(f" $ {' '.join(args)}")
97
+ result = subprocess.run(args, cwd=cwd, shell=shell)
98
+ if result.returncode != 0:
99
+ raise click.ClickException(
100
+ f"Command failed (exit {result.returncode}): {' '.join(args)}"
101
+ )
102
+
103
+
104
+
105
+ # ---------------------------------------------------------------------------
106
+ # Favicon injection
107
+ # ---------------------------------------------------------------------------
108
+
109
+ def inject_favicon(project_root: str) -> None:
110
+ """
111
+ After `npm run build`, overwrite every favicon.ico found under
112
+ frontend/dist/ with the project's own icon/favicon.ico.
113
+ Idempotent and skipped gracefully when source or dist is missing.
114
+ """
115
+ src_ico = os.path.join(project_root, 'icon', 'favicon.ico')
116
+
117
+ if not os.path.isfile(src_ico):
118
+ click.echo('跳过 favicon 注入:icon/favicon.ico 不存在')
119
+ return
120
+
121
+ dist_dir = os.path.join(project_root, 'frontend', 'dist')
122
+ if not os.path.isdir(dist_dir):
123
+ click.echo('跳过 favicon 注入:frontend/dist 不存在')
124
+ return
125
+
126
+ replaced = 0
127
+ for dirpath, _, filenames in os.walk(dist_dir):
128
+ for filename in filenames:
129
+ if filename.lower() == 'favicon.ico':
130
+ dst = os.path.join(dirpath, filename)
131
+ shutil.copy2(src_ico, dst)
132
+ rel = os.path.relpath(dst, project_root)
133
+ click.echo(f'✔ 已注入 favicon:{rel}')
134
+ replaced += 1
135
+
136
+ if replaced == 0:
137
+ click.echo('跳过 favicon 注入:dist 中未找到 favicon.ico')
138
+
139
+ # ---------------------------------------------------------------------------
140
+ # Frontend router fix
141
+ # ---------------------------------------------------------------------------
142
+
143
+ def fix_router_history(project_root: str) -> None:
144
+ """
145
+ Replace createWebHistory with createWebHashHistory in the Vue Router
146
+ entry file so that file:// protocol works correctly after packaging.
147
+ This function is idempotent.
148
+ """
149
+ router_dir = os.path.join(project_root, "frontend", "src", "router")
150
+
151
+ if not os.path.isdir(router_dir):
152
+ click.echo("跳过路由修复:未检测到 router 目录")
153
+ return
154
+
155
+ target_file = None
156
+ for filename in ("index.ts", "index.js"):
157
+ candidate = os.path.join(router_dir, filename)
158
+ if os.path.isfile(candidate):
159
+ target_file = candidate
160
+ break
161
+
162
+ if target_file is None:
163
+ click.echo("跳过路由修复:未找到 router/index.ts 或 router/index.js")
164
+ return
165
+
166
+ with open(target_file, "r", encoding="utf-8") as f:
167
+ content = f.read()
168
+
169
+ if "createWebHistory" not in content:
170
+ click.echo("跳过路由修复:未检测到 createWebHistory,无需修改")
171
+ return
172
+
173
+ # \b boundary: createWebHashHistory does NOT contain createWebHistory as a
174
+ # substring, so this replacement is idempotent.
175
+ new_content = re.sub(r"\bcreateWebHistory\b", "createWebHashHistory", content)
176
+
177
+ with open(target_file, "w", encoding="utf-8") as f:
178
+ f.write(new_content)
179
+
180
+ rel_path = os.path.relpath(target_file, project_root)
181
+ click.echo(f"✔ 已修复:{rel_path}(createWebHistory → createWebHashHistory)")
@@ -0,0 +1,163 @@
1
+ Metadata-Version: 2.4
2
+ Name: wv-cli
3
+ Version: 0.1.0
4
+ Summary: CLI scaffold tool for pywebview + Vue3 desktop apps
5
+ Requires-Python: >=3.9
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: click>=8.0
8
+ Requires-Dist: toml>=0.10
9
+
10
+ # wv-cli
11
+
12
+ A command-line scaffold tool for building **pywebview (Python backend) + Vue 3 (frontend)** desktop apps.
13
+
14
+ ## Requirements
15
+
16
+ - Python ≥ 3.9
17
+ - [uv](https://docs.astral.sh/uv/) — Python package manager
18
+ - [Node.js / npm](https://nodejs.org) — for the Vue 3 frontend
19
+ - [Inno Setup 6](https://jrsoftware.org/isdl.php) *(Windows only, required for `--publish`)*
20
+
21
+ ## Installation
22
+
23
+ ```bash
24
+ pip install wv-cli
25
+ ```
26
+
27
+ Or install from source with `uv`:
28
+
29
+ ```bash
30
+ git clone https://github.com/yourname/wv-cli
31
+ cd wv-cli
32
+ uv pip install -e .
33
+ ```
34
+
35
+ ## Quick Start
36
+
37
+ ### Create a new project
38
+
39
+ ```bash
40
+ # Interactive — creates ./my-app/
41
+ wv create
42
+
43
+ # In the current directory
44
+ wv create .
45
+
46
+ # Explicit directory
47
+ wv create path/to/my-app
48
+ ```
49
+
50
+ You will be prompted for:
51
+ | Prompt | Default |
52
+ |---|---|
53
+ | 项目名称 (project name) | directory name |
54
+ | 窗口标题 (window title) | project name |
55
+ | 版本号 (version) | `1.0.0` |
56
+ | 作者 (author) | *(empty)* |
57
+
58
+ After answering, the CLI will:
59
+ 1. Scaffold the full directory structure
60
+ 2. Run `npm create vue@latest` for the frontend (you drive the Vue prompts)
61
+ 3. Run `uv init / venv / add pywebview pyinstaller` for the backend
62
+
63
+ ### Run in development mode
64
+
65
+ ```bash
66
+ cd my-app
67
+ wv run
68
+ ```
69
+
70
+ Builds the Vue frontend, then launches the pywebview window loading `frontend/dist`.
71
+
72
+ ### Production build
73
+
74
+ ```bash
75
+ wv build
76
+ ```
77
+
78
+ Builds the frontend and runs PyInstaller to produce `build/dist/<project-name>/`.
79
+
80
+ ### Build + Windows installer
81
+
82
+ ```bash
83
+ wv build --publish
84
+ ```
85
+
86
+ Runs the full build, then calls Inno Setup to produce
87
+ `build/publish/<project-name>-<version>-setup.exe`.
88
+
89
+ Configure the Inno Setup path in `wv.toml` if needed:
90
+
91
+ ```toml
92
+ [build]
93
+ inno_setup_path = "C:/Program Files (x86)/Inno Setup 6/ISCC.exe"
94
+ ```
95
+
96
+ ## Generated Project Structure
97
+
98
+ ```
99
+ my-app/
100
+ ├── icon/
101
+ │ ├── favicon.ico
102
+ │ └── logo.png
103
+ ├── frontend/ ← Vue 3 (npm create vue@latest)
104
+ │ └── dist/ ← built by wv run / wv build
105
+ ├── backend/
106
+ │ ├── .venv/ ← uv virtual environment
107
+ │ └── src/
108
+ │ ├── main.py
109
+ │ ├── config.py
110
+ │ └── bridge/
111
+ │ ├── __init__.py
112
+ │ └── api.py
113
+ ├── build/
114
+ │ ├── my-app.spec ← PyInstaller config
115
+ │ ├── my-app.iss ← Inno Setup config
116
+ │ └── publish/ ← installer output
117
+ └── wv.toml
118
+ ```
119
+
120
+ ## Frontend Router Auto-Fix
121
+
122
+ `wv run` and `wv build` automatically replace `createWebHistory` with
123
+ `createWebHashHistory` in `frontend/src/router/index.{ts,js}` before building.
124
+ This ensures the app works correctly when loaded via the `file://` protocol
125
+ after packaging. The replacement is **idempotent** — running it multiple times
126
+ has no side effects.
127
+
128
+ ## `wv.toml` Reference
129
+
130
+ ```toml
131
+ [project]
132
+ name = "my-app"
133
+ version = "1.0.0"
134
+ window_title = "My App"
135
+ author = ""
136
+
137
+ [build]
138
+ inno_setup_path = "C:/Program Files (x86)/Inno Setup 6/ISCC.exe"
139
+ ```
140
+
141
+ ## Extending the JS Bridge
142
+
143
+ Edit `backend/src/bridge/api.py`:
144
+
145
+ ```python
146
+ class Api:
147
+ def greet(self, name: str) -> str:
148
+ return f"Hello, {name}!"
149
+
150
+ def read_file(self, path: str) -> str:
151
+ with open(path) as f:
152
+ return f.read()
153
+ ```
154
+
155
+ Call from Vue:
156
+
157
+ ```js
158
+ const result = await window.pywebview.api.greet('World')
159
+ ```
160
+
161
+ ## License
162
+
163
+ MIT
@@ -0,0 +1,15 @@
1
+ wv_cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ wv_cli/main.py,sha256=tM7fLjlMIXaIjUdYVQaFUndTDpGR2lbX-fmczdQV2Mg,403
3
+ wv_cli/templates.py,sha256=iY41IdQlshfD1NWVKgQP-xDozaqMkmcyfyIXjE1pWdU,5012
4
+ wv_cli/utils.py,sha256=aj3jKsex1uoHpYp8d3JlzdWbddIcyQdCzyetq5TLdSk,6422
5
+ wv_cli/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ wv_cli/commands/build.py,sha256=eVeY30idUiOFGTUC3rlaqayCyXRF1TOvAnpK3ik1JP0,3498
7
+ wv_cli/commands/create.py,sha256=29H901zFngFWd-kC0V70z4-K1w0pDUT2WUEcvBcA4_4,5468
8
+ wv_cli/commands/run.py,sha256=dQcvTUjRuq35i_frlr71SIBAt0TeUxNcF81OmpXRMeE,1268
9
+ wv_cli/icon/favicon.ico,sha256=rKwWhFU5Gqg1X2l8W9jMKmDm0Ss9kb4ro8CbmvpyuGE,4069
10
+ wv_cli/icon/logo.png,sha256=EmXS4QtmafWqZtXKEgLXs1PSAzu3BDu6xqRhQ6L192A,410862
11
+ wv_cli-0.1.0.dist-info/METADATA,sha256=h-Ju_OoRVY8Iher1XVPNBz8wWZTEQzeurJBceqPRShY,3741
12
+ wv_cli-0.1.0.dist-info/WHEEL,sha256=YCfwYGOYMi5Jhw2fU4yNgwErybb2IX5PEwBKV4ZbdBo,91
13
+ wv_cli-0.1.0.dist-info/entry_points.txt,sha256=qaE3RufSmXgg3JL_t25pKLjaRPUTxlOv8N8DceJG2R8,39
14
+ wv_cli-0.1.0.dist-info/top_level.txt,sha256=ofuqIKjvgMWv3bm_8iT1p5EALQX4zPRXP4b0NosFbNc,7
15
+ wv_cli-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ wv = wv_cli.main:cli
@@ -0,0 +1 @@
1
+ wv_cli