fastvex 0.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fastvex-0.0.1/.gitignore +9 -0
- fastvex-0.0.1/LICENSE +21 -0
- fastvex-0.0.1/PKG-INFO +116 -0
- fastvex-0.0.1/README.md +103 -0
- fastvex-0.0.1/pyproject.toml +40 -0
- fastvex-0.0.1/src/fastvex/__init__.py +2 -0
- fastvex-0.0.1/src/fastvex/cli.py +383 -0
- fastvex-0.0.1/src/fastvex/config_edit.py +26 -0
- fastvex-0.0.1/src/fastvex/display.py +197 -0
- fastvex-0.0.1/src/fastvex/executor.py +291 -0
- fastvex-0.0.1/src/fastvex/models.py +265 -0
- fastvex-0.0.1/src/fastvex/project.py +74 -0
- fastvex-0.0.1/src/fastvex/services.py +285 -0
- fastvex-0.0.1/src/fastvex/state_model.py +106 -0
- fastvex-0.0.1/src/fastvex/storage.py +82 -0
- fastvex-0.0.1/src/fastvex/templates.py +79 -0
- fastvex-0.0.1/src/fastvex/theme.py +97 -0
- fastvex-0.0.1/tests/conftest.py +72 -0
- fastvex-0.0.1/tests/test_cli.py +102 -0
- fastvex-0.0.1/tests/test_executor.py +136 -0
- fastvex-0.0.1/tests/test_services.py +22 -0
fastvex-0.0.1/.gitignore
ADDED
fastvex-0.0.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 WyattYuan
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
fastvex-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fastvex
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Fast VEX V5 slot-oriented build and upload CLI
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Python: >=3.11
|
|
8
|
+
Requires-Dist: pydantic>=2.11
|
|
9
|
+
Requires-Dist: pyyaml>=6.0.2
|
|
10
|
+
Requires-Dist: rich>=13.0
|
|
11
|
+
Requires-Dist: typer>=0.16.0
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# fastvex
|
|
15
|
+
|
|
16
|
+
`fastvex` 是面向 VEX V5 / PROS 机器人项目的槽位构建与上传工具。
|
|
17
|
+
|
|
18
|
+
它会读取机器人项目根目录中的 `fastvex.yaml`,根据 slot、role、route 配置生成构建参数和程序名,调用 PROS 完成构建/上传,并把本机运行状态写入 `.fastvex/state.json`。
|
|
19
|
+
|
|
20
|
+
## 安装与运行
|
|
21
|
+
|
|
22
|
+
临时运行:
|
|
23
|
+
|
|
24
|
+
```powershell
|
|
25
|
+
uvx fastvex validate
|
|
26
|
+
uvx fastvex upload --slots 1,3 -y
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
全局安装:
|
|
30
|
+
|
|
31
|
+
```powershell
|
|
32
|
+
pipx install fastvex
|
|
33
|
+
fastvex show
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
本仓库本地开发时:
|
|
37
|
+
|
|
38
|
+
```powershell
|
|
39
|
+
uv sync
|
|
40
|
+
uv run pytest
|
|
41
|
+
uv run ruff check .
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## 在机器人项目中使用
|
|
45
|
+
|
|
46
|
+
在包含 `fastvex.yaml` 的机器人仓库根目录运行:
|
|
47
|
+
|
|
48
|
+
```powershell
|
|
49
|
+
fastvex validate
|
|
50
|
+
fastvex show
|
|
51
|
+
fastvex upload --slots 1,3 -y
|
|
52
|
+
fastvex route show
|
|
53
|
+
fastvex route set red r1
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
也可以在机器人项目子目录中运行,`fastvex` 会向上查找 `fastvex.yaml`。
|
|
57
|
+
|
|
58
|
+
如果需要显式指定配置:
|
|
59
|
+
|
|
60
|
+
```powershell
|
|
61
|
+
fastvex validate --config D:\path\to\robot\fastvex.yaml
|
|
62
|
+
fastvex --config D:\path\to\robot\fastvex.yaml validate
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## 配置与状态
|
|
66
|
+
|
|
67
|
+
- `fastvex.yaml`:机器人项目配置,建议提交到机器人代码仓库。
|
|
68
|
+
- `.fastvex/state.json`:本机状态与上传历史,建议加入机器人仓库的 `.gitignore`。
|
|
69
|
+
- `vex_upload_config.yaml`:旧配置文件名仍可读取,但新项目建议迁移到 `fastvex.yaml`。
|
|
70
|
+
|
|
71
|
+
## 常用命令
|
|
72
|
+
|
|
73
|
+
```powershell
|
|
74
|
+
# 初始化 fastvex.yaml 和 .fastvex/state.json,不覆盖已有文件
|
|
75
|
+
fastvex init
|
|
76
|
+
|
|
77
|
+
# 校验配置
|
|
78
|
+
fastvex validate
|
|
79
|
+
|
|
80
|
+
# 展示槽位映射、当前状态和历史
|
|
81
|
+
fastvex show
|
|
82
|
+
|
|
83
|
+
# 预览上传,不执行 PROS 构建/上传
|
|
84
|
+
fastvex upload --slots 3 --dry-run
|
|
85
|
+
|
|
86
|
+
# 上传指定槽位
|
|
87
|
+
fastvex upload --slots 1,3 -y
|
|
88
|
+
|
|
89
|
+
# 按配置中的分组上传
|
|
90
|
+
fastvex upload --group all-enabled -y
|
|
91
|
+
|
|
92
|
+
# 查看和切换当前路线
|
|
93
|
+
fastvex route show
|
|
94
|
+
fastvex route set red r1
|
|
95
|
+
|
|
96
|
+
# 查看和清理历史
|
|
97
|
+
fastvex history show
|
|
98
|
+
fastvex history clean --keep 10
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Python API
|
|
102
|
+
|
|
103
|
+
`fastvex` 也提供命令级 Python API,方便测试或脚本复用:
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
from fastvex.services import UploadRequest, upload_slots, validate_project
|
|
107
|
+
|
|
108
|
+
report = validate_project(config="D:/path/to/robot/fastvex.yaml")
|
|
109
|
+
assert report.warnings == []
|
|
110
|
+
|
|
111
|
+
upload_report = upload_slots(
|
|
112
|
+
UploadRequest(slots="3", dry_run=True),
|
|
113
|
+
config="D:/path/to/robot/fastvex.yaml",
|
|
114
|
+
)
|
|
115
|
+
assert upload_report.failed_slots == []
|
|
116
|
+
```
|
fastvex-0.0.1/README.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# fastvex
|
|
2
|
+
|
|
3
|
+
`fastvex` 是面向 VEX V5 / PROS 机器人项目的槽位构建与上传工具。
|
|
4
|
+
|
|
5
|
+
它会读取机器人项目根目录中的 `fastvex.yaml`,根据 slot、role、route 配置生成构建参数和程序名,调用 PROS 完成构建/上传,并把本机运行状态写入 `.fastvex/state.json`。
|
|
6
|
+
|
|
7
|
+
## 安装与运行
|
|
8
|
+
|
|
9
|
+
临时运行:
|
|
10
|
+
|
|
11
|
+
```powershell
|
|
12
|
+
uvx fastvex validate
|
|
13
|
+
uvx fastvex upload --slots 1,3 -y
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
全局安装:
|
|
17
|
+
|
|
18
|
+
```powershell
|
|
19
|
+
pipx install fastvex
|
|
20
|
+
fastvex show
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
本仓库本地开发时:
|
|
24
|
+
|
|
25
|
+
```powershell
|
|
26
|
+
uv sync
|
|
27
|
+
uv run pytest
|
|
28
|
+
uv run ruff check .
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## 在机器人项目中使用
|
|
32
|
+
|
|
33
|
+
在包含 `fastvex.yaml` 的机器人仓库根目录运行:
|
|
34
|
+
|
|
35
|
+
```powershell
|
|
36
|
+
fastvex validate
|
|
37
|
+
fastvex show
|
|
38
|
+
fastvex upload --slots 1,3 -y
|
|
39
|
+
fastvex route show
|
|
40
|
+
fastvex route set red r1
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
也可以在机器人项目子目录中运行,`fastvex` 会向上查找 `fastvex.yaml`。
|
|
44
|
+
|
|
45
|
+
如果需要显式指定配置:
|
|
46
|
+
|
|
47
|
+
```powershell
|
|
48
|
+
fastvex validate --config D:\path\to\robot\fastvex.yaml
|
|
49
|
+
fastvex --config D:\path\to\robot\fastvex.yaml validate
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## 配置与状态
|
|
53
|
+
|
|
54
|
+
- `fastvex.yaml`:机器人项目配置,建议提交到机器人代码仓库。
|
|
55
|
+
- `.fastvex/state.json`:本机状态与上传历史,建议加入机器人仓库的 `.gitignore`。
|
|
56
|
+
- `vex_upload_config.yaml`:旧配置文件名仍可读取,但新项目建议迁移到 `fastvex.yaml`。
|
|
57
|
+
|
|
58
|
+
## 常用命令
|
|
59
|
+
|
|
60
|
+
```powershell
|
|
61
|
+
# 初始化 fastvex.yaml 和 .fastvex/state.json,不覆盖已有文件
|
|
62
|
+
fastvex init
|
|
63
|
+
|
|
64
|
+
# 校验配置
|
|
65
|
+
fastvex validate
|
|
66
|
+
|
|
67
|
+
# 展示槽位映射、当前状态和历史
|
|
68
|
+
fastvex show
|
|
69
|
+
|
|
70
|
+
# 预览上传,不执行 PROS 构建/上传
|
|
71
|
+
fastvex upload --slots 3 --dry-run
|
|
72
|
+
|
|
73
|
+
# 上传指定槽位
|
|
74
|
+
fastvex upload --slots 1,3 -y
|
|
75
|
+
|
|
76
|
+
# 按配置中的分组上传
|
|
77
|
+
fastvex upload --group all-enabled -y
|
|
78
|
+
|
|
79
|
+
# 查看和切换当前路线
|
|
80
|
+
fastvex route show
|
|
81
|
+
fastvex route set red r1
|
|
82
|
+
|
|
83
|
+
# 查看和清理历史
|
|
84
|
+
fastvex history show
|
|
85
|
+
fastvex history clean --keep 10
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Python API
|
|
89
|
+
|
|
90
|
+
`fastvex` 也提供命令级 Python API,方便测试或脚本复用:
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
from fastvex.services import UploadRequest, upload_slots, validate_project
|
|
94
|
+
|
|
95
|
+
report = validate_project(config="D:/path/to/robot/fastvex.yaml")
|
|
96
|
+
assert report.warnings == []
|
|
97
|
+
|
|
98
|
+
upload_report = upload_slots(
|
|
99
|
+
UploadRequest(slots="3", dry_run=True),
|
|
100
|
+
config="D:/path/to/robot/fastvex.yaml",
|
|
101
|
+
)
|
|
102
|
+
assert upload_report.failed_slots == []
|
|
103
|
+
```
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "fastvex"
|
|
3
|
+
version = "0.0.1"
|
|
4
|
+
description = "Fast VEX V5 slot-oriented build and upload CLI"
|
|
5
|
+
license = "MIT"
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
requires-python = ">=3.11"
|
|
8
|
+
dependencies = [
|
|
9
|
+
"PyYAML>=6.0.2",
|
|
10
|
+
"pydantic>=2.11",
|
|
11
|
+
"rich>=13.0",
|
|
12
|
+
"typer>=0.16.0",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[project.scripts]
|
|
16
|
+
fastvex = "fastvex.cli:main"
|
|
17
|
+
|
|
18
|
+
[dependency-groups]
|
|
19
|
+
dev = [
|
|
20
|
+
"pytest>=8.3.5",
|
|
21
|
+
"ruff>=0.11.2",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[tool.ruff]
|
|
25
|
+
line-length = 100
|
|
26
|
+
|
|
27
|
+
[build-system]
|
|
28
|
+
requires = ["hatchling>=1.27.0"]
|
|
29
|
+
build-backend = "hatchling.build"
|
|
30
|
+
|
|
31
|
+
[tool.hatch.build.targets.wheel]
|
|
32
|
+
packages = ["src/fastvex"]
|
|
33
|
+
|
|
34
|
+
[tool.hatch.build.targets.sdist]
|
|
35
|
+
include = [
|
|
36
|
+
"src/fastvex",
|
|
37
|
+
"tests",
|
|
38
|
+
"LICENSE",
|
|
39
|
+
"README.md",
|
|
40
|
+
]
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from typing import Annotated
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from .display import print_execution_result, print_history, print_show, print_upload_plan
|
|
10
|
+
from .services import (
|
|
11
|
+
HistoryCleanReport,
|
|
12
|
+
UploadRequest,
|
|
13
|
+
clean_history,
|
|
14
|
+
get_history,
|
|
15
|
+
init_project,
|
|
16
|
+
plan_upload,
|
|
17
|
+
set_route,
|
|
18
|
+
show_project,
|
|
19
|
+
show_routes,
|
|
20
|
+
upload_slots,
|
|
21
|
+
validate_project,
|
|
22
|
+
)
|
|
23
|
+
from .storage import ValidationError
|
|
24
|
+
from .theme import FAIL, INFO, OK, WARN, confirm, console
|
|
25
|
+
|
|
26
|
+
CommonConfig = Annotated[str | None, typer.Option("--config", help="Config file path.")]
|
|
27
|
+
CommonState = Annotated[str | None, typer.Option("--state", help="State file path.")]
|
|
28
|
+
|
|
29
|
+
app = typer.Typer(
|
|
30
|
+
name="fastvex",
|
|
31
|
+
help="Fast VEX slot-oriented build/upload manager.",
|
|
32
|
+
invoke_without_command=True,
|
|
33
|
+
)
|
|
34
|
+
history_app = typer.Typer(help="Show or clean history.")
|
|
35
|
+
route_app = typer.Typer(help="Show or set active route by route set.")
|
|
36
|
+
app.add_typer(history_app, name="history")
|
|
37
|
+
app.add_typer(route_app, name="route")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def rprint(*args, **kwargs) -> None:
|
|
41
|
+
"""Rich-aware print: uses console.print unless an explicit file= is given."""
|
|
42
|
+
file = kwargs.pop("file", None)
|
|
43
|
+
if file is not None:
|
|
44
|
+
kwargs.setdefault("sep", " ")
|
|
45
|
+
kwargs.setdefault("end", "\n")
|
|
46
|
+
kwargs.setdefault("flush", False)
|
|
47
|
+
print(*args, file=file, **kwargs)
|
|
48
|
+
else:
|
|
49
|
+
console.print(*args, **kwargs)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _ctx_options(ctx: typer.Context, config: str | None, state: str | None) -> dict[str, str | None]:
|
|
53
|
+
obj = ctx.obj or {}
|
|
54
|
+
return {
|
|
55
|
+
"config": config if config is not None else obj.get("config"),
|
|
56
|
+
"state": state if state is not None else obj.get("state"),
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _print_legacy_warning(legacy_config: bool, config_path: object) -> None:
|
|
61
|
+
if legacy_config:
|
|
62
|
+
rprint(f" [yellow]{WARN} using legacy config name:[/yellow] {config_path}")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _finish(code: int) -> None:
|
|
66
|
+
if code:
|
|
67
|
+
raise typer.Exit(code)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _upload_request(
|
|
71
|
+
*,
|
|
72
|
+
slots: str | None = None,
|
|
73
|
+
group: str | None = None,
|
|
74
|
+
all_enabled: bool = False,
|
|
75
|
+
robot_name: str | None = None,
|
|
76
|
+
port: str | None = None,
|
|
77
|
+
clean: bool = False,
|
|
78
|
+
quiet: bool = False,
|
|
79
|
+
dry_run: bool = False,
|
|
80
|
+
yes: bool = False,
|
|
81
|
+
) -> UploadRequest:
|
|
82
|
+
return UploadRequest(
|
|
83
|
+
slots=slots,
|
|
84
|
+
group=group,
|
|
85
|
+
all_enabled=all_enabled,
|
|
86
|
+
robot_name=robot_name,
|
|
87
|
+
port=port,
|
|
88
|
+
clean=clean,
|
|
89
|
+
quiet=quiet,
|
|
90
|
+
dry_run=dry_run,
|
|
91
|
+
yes=yes,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def run_default_interactive(config: str | None = None, state: str | None = None) -> int:
|
|
96
|
+
report = show_project(config=config, state=state)
|
|
97
|
+
_print_legacy_warning(report.paths.legacy_config, report.paths.config)
|
|
98
|
+
print_show(report.config, report.state)
|
|
99
|
+
|
|
100
|
+
rprint(" [bold cyan]Select upload target:[/bold cyan]")
|
|
101
|
+
rprint(" [dim]slot list[/dim] e.g. [green]1,3,5[/green]")
|
|
102
|
+
rprint(" [dim]group:name[/dim] e.g. [green]group:comp-default[/green]")
|
|
103
|
+
rprint(" [dim]all[/dim] upload all enabled slots")
|
|
104
|
+
rprint(" [dim]q[/dim] quit")
|
|
105
|
+
rprint()
|
|
106
|
+
|
|
107
|
+
raw = console.input(" [cyan]target[/cyan]> ").strip()
|
|
108
|
+
if not raw or raw.lower() in {"q", "quit", "exit"}:
|
|
109
|
+
rprint("\n [blue]bye[/blue]\n")
|
|
110
|
+
return 0
|
|
111
|
+
|
|
112
|
+
request = _upload_request()
|
|
113
|
+
if raw.lower() == "all":
|
|
114
|
+
request = _upload_request(all_enabled=True)
|
|
115
|
+
elif raw.lower().startswith("group:"):
|
|
116
|
+
request = _upload_request(group=raw.split(":", 1)[1].strip())
|
|
117
|
+
else:
|
|
118
|
+
request = _upload_request(slots=raw)
|
|
119
|
+
|
|
120
|
+
planned = plan_upload(request, config=config, state=state)
|
|
121
|
+
paths, loaded_config, _, slots, _, _ = planned
|
|
122
|
+
_print_legacy_warning(paths.legacy_config, paths.config)
|
|
123
|
+
print_upload_plan(loaded_config, slots)
|
|
124
|
+
if not confirm(
|
|
125
|
+
" [yellow]Continue upload?[/yellow] [[green]Y[/green]/[red]n[/red]] (Enter for 'Y'): ",
|
|
126
|
+
default_yes=True,
|
|
127
|
+
):
|
|
128
|
+
rprint(f"\n [yellow]{WARN} aborted[/yellow]\n")
|
|
129
|
+
return 0
|
|
130
|
+
|
|
131
|
+
report = upload_slots(request, config=config, state=state)
|
|
132
|
+
if report.execution:
|
|
133
|
+
print_execution_result(report.execution)
|
|
134
|
+
return 1 if report.failed_slots else 0
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@app.callback()
|
|
138
|
+
def root(
|
|
139
|
+
ctx: typer.Context,
|
|
140
|
+
config: CommonConfig = None,
|
|
141
|
+
state: CommonState = None,
|
|
142
|
+
) -> None:
|
|
143
|
+
ctx.obj = {"config": config, "state": state}
|
|
144
|
+
if ctx.invoked_subcommand is None:
|
|
145
|
+
_finish(run_default_interactive(config=config, state=state))
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@app.command("init")
|
|
149
|
+
def init_command(
|
|
150
|
+
ctx: typer.Context,
|
|
151
|
+
config: CommonConfig = None,
|
|
152
|
+
state: CommonState = None,
|
|
153
|
+
) -> None:
|
|
154
|
+
options = _ctx_options(ctx, config, state)
|
|
155
|
+
report = init_project(**options)
|
|
156
|
+
|
|
157
|
+
if report.config_exists:
|
|
158
|
+
rprint(f" [cyan]config exists:[/cyan] {report.paths.config}")
|
|
159
|
+
elif report.legacy_config_exists:
|
|
160
|
+
rprint(f" [yellow]{WARN} legacy config exists:[/yellow] {report.paths.root / 'vex_upload_config.yaml'}")
|
|
161
|
+
rprint(" [dim]fastvex init will not migrate or overwrite configs.[/dim]")
|
|
162
|
+
elif report.config_created:
|
|
163
|
+
rprint(f" [green]created config:[/green] {report.paths.config}")
|
|
164
|
+
|
|
165
|
+
if report.state_exists:
|
|
166
|
+
rprint(f" [cyan]state exists:[/cyan] {report.paths.state}")
|
|
167
|
+
elif report.state_created:
|
|
168
|
+
rprint(f" [green]created state:[/green] {report.paths.state}")
|
|
169
|
+
|
|
170
|
+
rprint(f"\n [bold green]{OK} init ok[/bold green]\n")
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@app.command("show")
|
|
174
|
+
def show_command(
|
|
175
|
+
ctx: typer.Context,
|
|
176
|
+
config: CommonConfig = None,
|
|
177
|
+
state: CommonState = None,
|
|
178
|
+
) -> None:
|
|
179
|
+
report = show_project(**_ctx_options(ctx, config, state))
|
|
180
|
+
_print_legacy_warning(report.paths.legacy_config, report.paths.config)
|
|
181
|
+
print_show(report.config, report.state)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@app.command("validate")
|
|
185
|
+
def validate_command(
|
|
186
|
+
ctx: typer.Context,
|
|
187
|
+
config: CommonConfig = None,
|
|
188
|
+
state: CommonState = None,
|
|
189
|
+
) -> None:
|
|
190
|
+
report = validate_project(**_ctx_options(ctx, config, state))
|
|
191
|
+
_print_legacy_warning(report.paths.legacy_config, report.paths.config)
|
|
192
|
+
for warning in report.warnings:
|
|
193
|
+
rprint(f" {WARN} {warning}")
|
|
194
|
+
rprint(f"\n [bold green]{OK} validate ok[/bold green]\n")
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
@app.command("upload")
|
|
198
|
+
def upload_command(
|
|
199
|
+
ctx: typer.Context,
|
|
200
|
+
config: CommonConfig = None,
|
|
201
|
+
state: CommonState = None,
|
|
202
|
+
slots: Annotated[str | None, typer.Option("--slots", help="Slot list, e.g. '1,3,5'.")] = None,
|
|
203
|
+
group: Annotated[str | None, typer.Option("--group", help="Group name defined in config.")] = None,
|
|
204
|
+
all_enabled: Annotated[
|
|
205
|
+
bool,
|
|
206
|
+
typer.Option("--all-enabled", help="Target all configured slots."),
|
|
207
|
+
] = False,
|
|
208
|
+
robot_name: Annotated[str | None, typer.Option("--robot-name", help="Override robot name.")] = None,
|
|
209
|
+
port: Annotated[str | None, typer.Option("--port", help="Override port. Empty means auto.")] = None,
|
|
210
|
+
clean: Annotated[bool, typer.Option("--clean", help="Run make clean before build.")] = False,
|
|
211
|
+
quiet: Annotated[bool, typer.Option("--quiet", help="Capture build/upload output.")] = False,
|
|
212
|
+
dry_run: Annotated[bool, typer.Option("--dry-run", help="Plan without build/upload.")] = False,
|
|
213
|
+
yes: Annotated[bool, typer.Option("-y", "--yes", help="Skip confirm prompt.")] = False,
|
|
214
|
+
) -> None:
|
|
215
|
+
options = _ctx_options(ctx, config, state)
|
|
216
|
+
request = _upload_request(
|
|
217
|
+
slots=slots,
|
|
218
|
+
group=group,
|
|
219
|
+
all_enabled=all_enabled,
|
|
220
|
+
robot_name=robot_name,
|
|
221
|
+
port=port,
|
|
222
|
+
clean=clean,
|
|
223
|
+
quiet=quiet,
|
|
224
|
+
dry_run=dry_run,
|
|
225
|
+
yes=yes,
|
|
226
|
+
)
|
|
227
|
+
paths, loaded_config, _, selected_slots, _, _ = plan_upload(request, **options)
|
|
228
|
+
_print_legacy_warning(paths.legacy_config, paths.config)
|
|
229
|
+
print_upload_plan(loaded_config, selected_slots)
|
|
230
|
+
|
|
231
|
+
if not yes and not dry_run:
|
|
232
|
+
if not confirm(
|
|
233
|
+
" [yellow]Continue upload?[/yellow] [[green]Y[/green]/[red]n[/red]] (Enter for 'Y'): ",
|
|
234
|
+
default_yes=True,
|
|
235
|
+
):
|
|
236
|
+
rprint(f"\n [yellow]{WARN} aborted[/yellow]\n")
|
|
237
|
+
return
|
|
238
|
+
|
|
239
|
+
report = upload_slots(request, **options)
|
|
240
|
+
if report.execution:
|
|
241
|
+
print_execution_result(report.execution)
|
|
242
|
+
_finish(1 if report.failed_slots else 0)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
@history_app.callback()
|
|
246
|
+
def history_root(
|
|
247
|
+
ctx: typer.Context,
|
|
248
|
+
config: CommonConfig = None,
|
|
249
|
+
state: CommonState = None,
|
|
250
|
+
) -> None:
|
|
251
|
+
parent = ctx.parent.obj if ctx.parent and ctx.parent.obj else {}
|
|
252
|
+
ctx.obj = {
|
|
253
|
+
"config": config if config is not None else parent.get("config"),
|
|
254
|
+
"state": state if state is not None else parent.get("state"),
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
@history_app.command("show")
|
|
259
|
+
def history_show_command(
|
|
260
|
+
ctx: typer.Context,
|
|
261
|
+
config: CommonConfig = None,
|
|
262
|
+
state: CommonState = None,
|
|
263
|
+
) -> None:
|
|
264
|
+
report = get_history(**_ctx_options(ctx, config, state))
|
|
265
|
+
_print_legacy_warning(report.paths.legacy_config, report.paths.config)
|
|
266
|
+
rprint()
|
|
267
|
+
if not report.state.history:
|
|
268
|
+
rprint(" [dim](empty)[/dim]")
|
|
269
|
+
else:
|
|
270
|
+
print_history(report.state.history)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
@history_app.command("clean")
|
|
274
|
+
def history_clean_command(
|
|
275
|
+
ctx: typer.Context,
|
|
276
|
+
config: CommonConfig = None,
|
|
277
|
+
state: CommonState = None,
|
|
278
|
+
keep: Annotated[int, typer.Option("--keep", help="Number of entries to keep.")] = 10,
|
|
279
|
+
) -> None:
|
|
280
|
+
report: HistoryCleanReport = clean_history(**_ctx_options(ctx, config, state), keep=keep)
|
|
281
|
+
_print_legacy_warning(report.paths.legacy_config, report.paths.config)
|
|
282
|
+
if report.removed_count == 0:
|
|
283
|
+
rprint(f" [dim]history has {report.kept_count} entries, no cleanup needed (keep={keep})[/dim]")
|
|
284
|
+
else:
|
|
285
|
+
rprint(
|
|
286
|
+
f" [green]{OK} cleaned[/green] {report.removed_count} "
|
|
287
|
+
f"[dim]old entries, kept last[/dim] {report.kept_count}"
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
@route_app.callback()
|
|
292
|
+
def route_root(
|
|
293
|
+
ctx: typer.Context,
|
|
294
|
+
config: CommonConfig = None,
|
|
295
|
+
state: CommonState = None,
|
|
296
|
+
) -> None:
|
|
297
|
+
parent = ctx.parent.obj if ctx.parent and ctx.parent.obj else {}
|
|
298
|
+
ctx.obj = {
|
|
299
|
+
"config": config if config is not None else parent.get("config"),
|
|
300
|
+
"state": state if state is not None else parent.get("state"),
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
@route_app.command("show")
|
|
305
|
+
def route_show_command(
|
|
306
|
+
ctx: typer.Context,
|
|
307
|
+
config: CommonConfig = None,
|
|
308
|
+
state: CommonState = None,
|
|
309
|
+
) -> None:
|
|
310
|
+
from .theme import role_tone
|
|
311
|
+
|
|
312
|
+
report = show_routes(**_ctx_options(ctx, config, state))
|
|
313
|
+
_print_legacy_warning(report.paths.legacy_config, report.paths.config)
|
|
314
|
+
loaded_config = report.config
|
|
315
|
+
|
|
316
|
+
rprint()
|
|
317
|
+
route_items = []
|
|
318
|
+
for route_set in sorted(loaded_config.active_route.keys()):
|
|
319
|
+
color, _ = role_tone(route_set, "COMP")
|
|
320
|
+
key = loaded_config.active_route[route_set]
|
|
321
|
+
route_items.append(f"[bold {color}]{route_set}[/bold {color}]:[{color}]{key}[/{color}]")
|
|
322
|
+
|
|
323
|
+
rprint(" [bold cyan]Active Routes[/bold cyan]")
|
|
324
|
+
rprint(f" {' '.join(route_items)}")
|
|
325
|
+
rprint()
|
|
326
|
+
|
|
327
|
+
rprint(" [bold cyan]Available Routes[/bold cyan]")
|
|
328
|
+
for route_set in sorted(loaded_config.routes.keys()):
|
|
329
|
+
color, _ = role_tone(route_set, "COMP")
|
|
330
|
+
active_key = loaded_config.active_route.get(route_set)
|
|
331
|
+
|
|
332
|
+
rprint(f"\n [bold {color}]{route_set}[/bold {color}]")
|
|
333
|
+
for key, opt in loaded_config.routes[route_set].items():
|
|
334
|
+
is_active = key == active_key
|
|
335
|
+
marker = f"[green]{OK}[/green] " if is_active else " "
|
|
336
|
+
active_tag = " [green](active)[/green]" if is_active else ""
|
|
337
|
+
rprint(
|
|
338
|
+
f" {marker}[cyan]{key}[/cyan] "
|
|
339
|
+
f"route={opt.route} routeName={opt.route_name}{active_tag}"
|
|
340
|
+
)
|
|
341
|
+
rprint()
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
@route_app.command("set")
|
|
345
|
+
def route_set_command(
|
|
346
|
+
ctx: typer.Context,
|
|
347
|
+
route_set: str,
|
|
348
|
+
route_key: str,
|
|
349
|
+
config: CommonConfig = None,
|
|
350
|
+
state: CommonState = None,
|
|
351
|
+
) -> None:
|
|
352
|
+
from .theme import role_tone
|
|
353
|
+
|
|
354
|
+
report = set_route(route_set, route_key, **_ctx_options(ctx, config, state))
|
|
355
|
+
_print_legacy_warning(report.paths.legacy_config, report.paths.config)
|
|
356
|
+
if not report.changed:
|
|
357
|
+
rprint(f"\n [dim]{INFO} route unchanged:[/dim] {report.route_set}={report.new_key}\n")
|
|
358
|
+
return
|
|
359
|
+
|
|
360
|
+
color, _ = role_tone(report.route_set, "COMP")
|
|
361
|
+
rprint(
|
|
362
|
+
f"\n [bold green]{OK} updated active route:[/bold green] "
|
|
363
|
+
f"[bold {color}]{report.route_set}[/bold {color}] [dim]{report.old_key}[/dim] "
|
|
364
|
+
f"[cyan]→[/cyan] [bold {color}]{report.new_key}[/bold {color}]\n"
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def main(argv: list[str] | None = None) -> int:
|
|
369
|
+
try:
|
|
370
|
+
app(args=argv, prog_name="fastvex", standalone_mode=False)
|
|
371
|
+
return 0
|
|
372
|
+
except click.exceptions.Exit as exc:
|
|
373
|
+
return int(exc.exit_code)
|
|
374
|
+
except ValidationError as exc:
|
|
375
|
+
print(f"\n [bold red]{FAIL} validation error:[/bold red] {exc}\n", file=sys.stderr)
|
|
376
|
+
return 2
|
|
377
|
+
except KeyboardInterrupt:
|
|
378
|
+
print(f"\n [yellow]{WARN} interrupted[/yellow]\n", file=sys.stderr)
|
|
379
|
+
return 130
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
if __name__ == "__main__":
|
|
383
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from .storage import ValidationError
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def replace_active_route_in_text(text: str, route_set: str, route_key: str) -> str:
|
|
7
|
+
"""Text-based replacement to keep YAML comments/formatting intact."""
|
|
8
|
+
lines = text.splitlines()
|
|
9
|
+
in_active = False
|
|
10
|
+
found = False
|
|
11
|
+
for idx, line in enumerate(lines):
|
|
12
|
+
stripped = line.strip()
|
|
13
|
+
if stripped == "activeRoute:":
|
|
14
|
+
in_active = True
|
|
15
|
+
continue
|
|
16
|
+
if in_active:
|
|
17
|
+
if stripped and not line.startswith(" "):
|
|
18
|
+
break
|
|
19
|
+
key_prefix = f" {route_set}:"
|
|
20
|
+
if line.startswith(key_prefix):
|
|
21
|
+
lines[idx] = f" {route_set}: {route_key}"
|
|
22
|
+
found = True
|
|
23
|
+
break
|
|
24
|
+
if not found:
|
|
25
|
+
raise ValidationError(f"failed to update activeRoute.{route_set} in config file")
|
|
26
|
+
return "\n".join(lines) + "\n"
|