Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions .trellis/spec/backend/error-handling.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,17 @@ subprocess.run(f"nssm.exe start {service_name}", shell=True)

## 服务状态错误

- `sing start <name>` 在服务已运行时失败,并提示使用 `sing restart <name>`。
- `sing restart <name>` 停止失败时不得继续启动。
- `sing start <name>` 在服务已运行时失败,并提示使用 `sing restart`。
- `sing restart` 停止失败时不得继续启动。
- `sing stop` 在服务停止失败时返回失败。
- `sing uninstall` 删除服务失败时返回失败。
- 第一版不做隐式重试,不做自动修复服务状态。

## Profile 状态错误

- profile 名不符合命名规则时失败。
- `sing start <name>`、`sing restart <name>`、`sing update <name>` 找不到 profile 名时失败。
- `sing start <name>`、`sing update <name>` 找不到 profile 名时失败。
- `sing restart` 没有 active profile 或 active profile 不存在时失败。
- `sing remove <name>` 删除 active profile 时失败。
- 下载 profile URL 失败、HTTP 状态失败、写入本地文件失败时命令失败。
- `sing add` 和 `sing update` 不主动调用 `sing-box check`;profile 有效性由 `sing start <name>` 启动时的 `sing-box` 行为暴露。
Expand Down
10 changes: 5 additions & 5 deletions .trellis/spec/backend/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ sing install [--bin <path>]
sing uninstall
sing start <name>
sing stop
sing restart <name>
sing restart
sing add <name> <url>
sing remove <name>
sing update <name>
Expand All @@ -49,8 +49,8 @@ sing list

- `sing install` 通过 NSSM 注册 `sing-box` Windows 服务并开启自启;默认使用 `PATH` 中的 `sing-box.exe`,`--bin` 可指定自定义路径。
- `sing install` 不下载、不升级 `sing-box.exe` 或 `nssm.exe`,也不指定或写入业务 profile。
- `sing start <name>` 使用 `<name>` 对应的本地 profile 启动服务;服务已运行时失败并提示使用 `sing restart <name>`。
- `sing restart <name>` 停止当前服务后,用 `<name>` 对应 profile 重新启动。
- `sing start <name>` 使用 `<name>` 对应的本地 profile 启动服务;服务已运行时失败并提示使用 `sing restart`。
- `sing restart` 停止当前服务后,用 active profile 重新启动。
- `sing stop` 停止服务,不需要 profile 名。
- `sing add <name> <url>` 添加命名 profile;URL 返回完整 `sing-box` JSON profile。
- `sing update <name>` 从已保存 URL 重新下载 profile。
Expand All @@ -59,13 +59,13 @@ sing list

## 服务命令行

`sing start <name>` 和 `sing restart <name>` 启动前必须先更新 NSSM 服务参数:
`sing start <name>` 和 `sing restart` 启动前必须先更新 NSSM 服务参数:

```text
nssm.exe set sing-box Application <sing-box.exe>
nssm.exe set sing-box AppParameters "run -c \"<profile-path>\""
```

随后再启动 `sing-box` 服务。Windows 自启时沿用最后一次 `sing start <name>` 或 `sing restart <name>` 写入的 profile。
随后再启动 `sing-box` 服务。Windows 自启时沿用最后一次 `sing start <name>` 或 `sing restart` 写入的 profile。

调用 `nssm.exe` 必须使用参数列表,不使用 shell 字符串拼接。
15 changes: 10 additions & 5 deletions .trellis/spec/backend/quality-guidelines.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ sing install [--bin <path>]
sing uninstall
sing start <name>
sing stop
sing restart <name>
sing restart
sing add <name> <url>
sing remove <name>
sing update <name>
Expand All @@ -33,7 +33,7 @@ sing list

- `sing install` 通过 NSSM 注册服务并开启自启;不处理业务配置。
- `sing start <name>` 负责把 NSSM 服务参数更新到 `<name>` 对应配置,再启动服务。
- `sing restart <name>` 先停止服务,再更新 NSSM 服务参数并启动服务。
- `sing restart` 先停止服务,再用 active 配置更新 NSSM 服务参数并启动服务。
- `sing stop` 不需要配置名。
- `sing list` 必须标出 active 配置。

Expand All @@ -60,7 +60,7 @@ nssm.exe remove sing-box confirm

- `sing install [--bin <path>]` 解析 `sing-box.exe` 后调用 `nssm.exe install`,再设置 `Start` 为 `SERVICE_AUTO_START`。
- `sing start <name>` 启动前设置 `Application` 为已安装的 `sing-box.exe` 路径,设置 `AppParameters` 为 `run -c "<profile-path>"`。
- `sing restart <name>` 必须先停止服务,停止成功后再写入 NSSM 参数并启动服务。
- `sing restart` 必须先停止服务,停止成功后再用 active 配置写入 NSSM 参数并启动服务。
- `nssm.exe` 必须从 `PATH` 解析;项目不下载、不内置 NSSM。
- 外部命令必须用参数列表调用,不通过 shell 字符串执行。
- 捕获外部命令输出时必须显式使用 `encoding="utf-8"` 和 `errors="replace"`,避免 Windows locale 默认编码导致 reader thread 抛出 `UnicodeDecodeError`。
Expand All @@ -73,7 +73,9 @@ nssm.exe remove sing-box confirm
| `nssm.exe` 返回非零退出码 | 命令失败并暴露 stderr 或 stdout 摘要 |
| `nssm.exe` 输出包含当前系统编码无法解码的字节 | 命令不因解码崩溃,输出中的非法字节以替换字符呈现 |
| `nssm.exe status sing-box` 输出 `SERVICE_RUNNING` | `service_is_running()` 返回 `True` |
| `sing start <name>` 发现服务已运行 | 命令失败并提示使用 `sing restart <name>` |
| `sing start <name>` 发现服务已运行 | 命令失败并提示使用 `sing restart` |
| `sing restart` 没有 active 配置 | 命令失败并提示先运行 `sing start <name>` |
| `sing restart` 的 active 配置不存在 | 命令失败并提示 profile 不存在 |

### 5. Good/Base/Bad Cases

Expand Down Expand Up @@ -505,7 +507,10 @@ updates:
- `sing install` 的 PATH 解析、`--bin` 覆盖、找不到二进制失败。
- `nssm.exe` 参数列表构造,不通过 shell 字符串执行。
- `sing start <name>` 服务已运行时失败。
- `sing restart <name>` 停止失败时不继续启动。
- `sing restart` 停止失败时不继续启动。
- `sing restart` 不接受 profile 名参数。
- `sing restart` 使用 active 配置重启。
- `sing restart` 没有 active 配置或 active 配置不存在时失败。
- `state.json` 读写、active 更新和删除 active 配置失败。
- 配置名称校验。
- HTTP 下载失败、写入失败和更新成功路径。
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{"file": ".trellis/spec/backend/index.md", "reason": "Verify backend command contract and docs remain consistent."}
{"file": ".trellis/spec/backend/error-handling.md", "reason": "Verify restart errors remain explicit and user-facing failures are not swallowed."}
{"file": ".trellis/spec/backend/quality-guidelines.md", "reason": "Verify test coverage and required quality commands."}
{"file": ".trellis/spec/backend/logging-guidelines.md", "reason": "Verify CLI output remains concise and separated by success/error path."}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{"file": ".trellis/spec/backend/index.md", "reason": "Backend command contract and project facts for CLI behavior changes."}
{"file": ".trellis/spec/backend/error-handling.md", "reason": "CLI error behavior for service and profile state failures."}
{"file": ".trellis/spec/backend/quality-guidelines.md", "reason": "Required tests and quality commands for Python CLI changes."}
{"file": ".trellis/spec/backend/logging-guidelines.md", "reason": "stdout/stderr output expectations for CLI commands."}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Fix restart active config behavior

## Goal

`sing restart` must restart the Windows service with the currently active profile and must not accept a profile name argument.

## What I already know

- User requirement: `sing restart` does not need parameters.
- User requirement: restart should use the current active configuration.
- Current implementation defines `restart(name: str)` and reconfigures the service with the provided profile.
- `README.md` and backend SPEC currently document `sing restart <name>`.
- Current tests cover service and state helpers but do not cover the Typer command-level restart argument contract.

## Requirements

- Change the CLI command contract from `sing restart <name>` to `sing restart`.
- `sing restart` must load the current state and require `state.active` to be set.
- `sing restart` must require the active profile to still exist.
- `sing restart` must require an installed `sing-box.exe` path.
- `sing restart` must stop the service, configure NSSM with the active profile path, start the service, and keep `state.active` unchanged.
- `sing start <name>` service-running error must point users to `sing restart`.
- Update tests for the no-argument restart behavior.
- Update README and SPEC command descriptions to match the corrected contract.

## Acceptance Criteria

- [x] `sing restart` succeeds with the current active profile.
- [x] `sing restart <name>` is rejected by Typer as an unexpected argument.
- [x] `sing restart` fails clearly when no active profile is recorded.
- [x] `sing restart` fails clearly when the active profile is missing.
- [x] Python lint, type-check, and tests pass.

## Definition of Done

- Tests added or updated for the command behavior.
- `uv run ruff check src` passes.
- `uv run ty check src` passes.
- `uv run pytest` passes.
- Docs and SPEC reflect the finished behavior without extra notes or process commentary.

## Out of Scope

- Backward compatibility for `sing restart <name>`.
- Selecting or switching profiles during restart.
- Changing `sing start`, `sing stop`, `sing update`, `sing remove`, or profile storage beyond the required help/error text updates.

## Technical Notes

- Relevant files inspected:
- `src/sing_cli/cli.py`
- `tests/test_service.py`
- `tests/test_state.py`
- `README.md`
- `.trellis/spec/backend/index.md`
- `.trellis/spec/backend/error-handling.md`
- `.trellis/spec/backend/quality-guidelines.md`
- `.trellis/spec/backend/logging-guidelines.md`
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"id": "fix-restart-active-config",
"name": "fix-restart-active-config",
"title": "Fix restart active config behavior",
"description": "",
"status": "completed",
"dev_type": null,
"scope": null,
"package": null,
"priority": "P2",
"creator": "pixelcola",
"assignee": "pixelcola",
"createdAt": "2026-05-12",
"completedAt": "2026-05-12",
"branch": null,
"base_branch": "main",
"worktree_path": null,
"commit": null,
"pr_url": null,
"subtasks": [],
"children": [],
"parent": null,
"relatedFiles": [],
"notes": "",
"meta": {}
}
5 changes: 3 additions & 2 deletions .trellis/workspace/pixelcola/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

<!-- @@@auto:current-status -->
- **Active File**: `journal-1.md`
- **Total Sessions**: 4
- **Total Sessions**: 5
- **Last Active**: 2026-05-12
<!-- @@@/auto:current-status -->

Expand All @@ -19,7 +19,7 @@
<!-- @@@auto:active-documents -->
| File | Lines | Status |
|------|-------|--------|
| `journal-1.md` | ~139 | Active |
| `journal-1.md` | ~172 | Active |
<!-- @@@/auto:active-documents -->

---
Expand All @@ -29,6 +29,7 @@
<!-- @@@auto:session-history -->
| # | Date | Title | Commits | Branch |
|---|------|-------|---------|--------|
| 5 | 2026-05-12 | Fix restart active profile | `010e59e` | `fix/restart-active-config` |
| 4 | 2026-05-12 | Fix Windows subprocess output decoding | `e90a676` | `fix/windows-subprocess-output-decoding` |
| 3 | 2026-05-12 | Use NSSM for Windows service | `72c1b0d` | `fix/use-nssm-service` |
| 2 | 2026-05-12 | Windows sing-box CLI | `2f18462` | `feature/windows-sing-box-cli` |
Expand Down
33 changes: 33 additions & 0 deletions .trellis/workspace/pixelcola/journal-1.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,3 +137,36 @@ Created PR #4 for the Windows subprocess decoding fix. Updated NSSM subprocess o
### Next Steps

- None - task complete


## Session 5: Fix restart active profile

**Date**: 2026-05-12
**Task**: Fix restart active profile
**Branch**: `fix/restart-active-config`

### Summary

Changed sing restart to use the active profile without accepting a profile argument, updated docs/specs, added CLI regression tests, and opened PR #5.

### Main Changes

(Add details)

### Git Commits

| Hash | Message |
|------|---------|
| `010e59e` | (see git log) |

### Testing

- [OK] (Add test results)

### Status

[OK] **Completed**

### Next Steps

- None - task complete
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Windows CLI for installing and controlling the `sing-box` Windows service.
| `sing uninstall` | Delete the `sing-box` Windows service. |
| `sing start <name>` | Start the service with a saved profile. |
| `sing stop` | Stop the service. |
| `sing restart <name>` | Stop, reconfigure, and start the service with a saved profile. |
| `sing restart` | Stop, reconfigure, and start the service with the active profile. |
| `sing add <name> <url>` | Download a complete `sing-box` JSON profile and save it under a name. |
| `sing remove <name>` | Remove a saved non-active profile. |
| `sing update <name>` | Redownload a saved profile from its URL. |
Expand Down
12 changes: 6 additions & 6 deletions src/sing_cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ def start(name: str) -> None:
entry = require_profile(state, name)
bin_path = require_installed_bin(state)
if service_is_running():
raise SingCliError(f"sing-box service is already running. Use 'sing restart {name}'.")
raise SingCliError("sing-box service is already running. Use 'sing restart'.")
configure_service(bin_path, entry.path)
start_service()
state.active = name
Expand All @@ -110,19 +110,19 @@ def stop() -> None:


@app.command()
def restart(name: str) -> None:
def restart() -> None:
try:
state = load_cli_state()
entry = require_profile(state, name)
if state.active is None:
raise SingCliError("No active profile. Run 'sing start <name>' first.")
entry = require_profile(state, state.active)
bin_path = require_installed_bin(state)
stop_service()
configure_service(bin_path, entry.path)
start_service()
state.active = name
save_cli_state(state)
except SingCliError as error:
fail(error)
typer.echo(f"Restarted {name}")
typer.echo(f"Restarted {state.active}")


@app.command()
Expand Down
Loading