vssh 4.2.0__tar.gz → 4.2.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.
- {vssh-4.2.0 → vssh-4.2.1}/CHANGELOG.md +17 -0
- {vssh-4.2.0 → vssh-4.2.1}/HELP.md +2 -0
- {vssh-4.2.0 → vssh-4.2.1}/Makefile +1 -1
- {vssh-4.2.0 → vssh-4.2.1}/PKG-INFO +13 -9
- {vssh-4.2.0 → vssh-4.2.1}/README.md +12 -8
- vssh-4.2.1/cmd/vssh/doctor.go +297 -0
- {vssh-4.2.0 → vssh-4.2.1}/cmd/vssh/main.go +4 -1
- {vssh-4.2.0 → vssh-4.2.1}/cmd/vssh/mcp.go +112 -63
- {vssh-4.2.0 → vssh-4.2.1}/cmd/vssh/mcp_test.go +40 -2
- {vssh-4.2.0 → vssh-4.2.1}/internal/server/auth.go +18 -2
- {vssh-4.2.0 → vssh-4.2.1}/pyproject.toml +1 -1
- {vssh-4.2.0 → vssh-4.2.1}/src/vssh/client.py +6 -0
- {vssh-4.2.0 → vssh-4.2.1}/tests/test_python_sdk.py +9 -0
- {vssh-4.2.0 → vssh-4.2.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {vssh-4.2.0 → vssh-4.2.1}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {vssh-4.2.0 → vssh-4.2.1}/.github/workflows/ci.yml +0 -0
- {vssh-4.2.0 → vssh-4.2.1}/.github/workflows/release.yml +0 -0
- {vssh-4.2.0 → vssh-4.2.1}/.gitignore +0 -0
- {vssh-4.2.0 → vssh-4.2.1}/CONTRIBUTING.md +0 -0
- {vssh-4.2.0 → vssh-4.2.1}/README.ko.md +0 -0
- {vssh-4.2.0 → vssh-4.2.1}/SECURITY.md +0 -0
- {vssh-4.2.0 → vssh-4.2.1}/cmd/vssh/fanout_test.go +0 -0
- {vssh-4.2.0 → vssh-4.2.1}/docs/AI_NATIVE_CAPABILITIES.ko.md +0 -0
- {vssh-4.2.0 → vssh-4.2.1}/docs/CODEX_ORCHESTRATION.ko.md +0 -0
- {vssh-4.2.0 → vssh-4.2.1}/docs/CODEX_ORCHESTRATION.md +0 -0
- {vssh-4.2.0 → vssh-4.2.1}/docs/DIRECTION.md +0 -0
- {vssh-4.2.0 → vssh-4.2.1}/docs/DISTRIBUTION.ko.md +0 -0
- {vssh-4.2.0 → vssh-4.2.1}/docs/NETWORK_TRAVERSAL_AUDIT.ko.md +0 -0
- {vssh-4.2.0 → vssh-4.2.1}/docs/PERFORMANCE.ko.md +0 -0
- {vssh-4.2.0 → vssh-4.2.1}/docs/PUBLISHING_AUDIT.ko.md +0 -0
- {vssh-4.2.0 → vssh-4.2.1}/docs/PUBLISHING_AUDIT.md +0 -0
- {vssh-4.2.0 → vssh-4.2.1}/docs/PYTHON_SDK.ko.md +0 -0
- {vssh-4.2.0 → vssh-4.2.1}/docs/WHY_VSSH.ko.md +0 -0
- {vssh-4.2.0 → vssh-4.2.1}/docs/WHY_VSSH.md +0 -0
- {vssh-4.2.0 → vssh-4.2.1}/go.mod +0 -0
- {vssh-4.2.0 → vssh-4.2.1}/go.sum +0 -0
- {vssh-4.2.0 → vssh-4.2.1}/install.sh +0 -0
- {vssh-4.2.0 → vssh-4.2.1}/internal/adapter/vssh.go +0 -0
- {vssh-4.2.0 → vssh-4.2.1}/internal/agent/agent.go +0 -0
- {vssh-4.2.0 → vssh-4.2.1}/internal/agent/api.go +0 -0
- {vssh-4.2.0 → vssh-4.2.1}/internal/config/config.go +0 -0
- {vssh-4.2.0 → vssh-4.2.1}/internal/event/event.go +0 -0
- {vssh-4.2.0 → vssh-4.2.1}/internal/server/artifact_test.go +0 -0
- {vssh-4.2.0 → vssh-4.2.1}/internal/server/auth_test.go +0 -0
- {vssh-4.2.0 → vssh-4.2.1}/internal/server/client.go +0 -0
- {vssh-4.2.0 → vssh-4.2.1}/internal/server/exec_test.go +0 -0
- {vssh-4.2.0 → vssh-4.2.1}/internal/server/jobs.go +0 -0
- {vssh-4.2.0 → vssh-4.2.1}/internal/server/jobs_test.go +0 -0
- {vssh-4.2.0 → vssh-4.2.1}/internal/server/pty_darwin.go +0 -0
- {vssh-4.2.0 → vssh-4.2.1}/internal/server/pty_linux.go +0 -0
- {vssh-4.2.0 → vssh-4.2.1}/internal/server/relay.go +0 -0
- {vssh-4.2.0 → vssh-4.2.1}/internal/server/rpc.go +0 -0
- {vssh-4.2.0 → vssh-4.2.1}/internal/server/server.go +0 -0
- {vssh-4.2.0 → vssh-4.2.1}/internal/server/sync.go +0 -0
- {vssh-4.2.0 → vssh-4.2.1}/internal/server/transfer.go +0 -0
- {vssh-4.2.0 → vssh-4.2.1}/internal/server/transfer_advanced.go +0 -0
- {vssh-4.2.0 → vssh-4.2.1}/internal/server/transfer_test.go +0 -0
- {vssh-4.2.0 → vssh-4.2.1}/internal/ssh/ssh.go +0 -0
- {vssh-4.2.0 → vssh-4.2.1}/internal/ssh/ssh_test.go +0 -0
- {vssh-4.2.0 → vssh-4.2.1}/src/vssh/__init__.py +0 -0
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [v0.7.5] - 2026-05-22
|
|
6
|
+
|
|
7
|
+
### Runtime
|
|
8
|
+
|
|
9
|
+
- Add `vssh doctor` / `vssh setup-check` for AI-operator setup diagnostics:
|
|
10
|
+
effective binary, stale binary conflicts, secret source, Wire config, and peer
|
|
11
|
+
counts.
|
|
12
|
+
- Add MCP `vssh_doctor` so Codex, Claude, Cursor, and other MCP clients can
|
|
13
|
+
diagnose VSSH before attempting execution or facts calls.
|
|
14
|
+
- Prefer MCP-safe underscore tool names and deduplicate exposed tools.
|
|
15
|
+
- Read native secrets from `/etc/vssh/secret` and `~/.vssh/secret` before
|
|
16
|
+
falling back to Wire-derived secrets.
|
|
17
|
+
|
|
18
|
+
### Python SDK
|
|
19
|
+
|
|
20
|
+
- Add `VSSH.doctor()` to call `vssh doctor --json`.
|
|
21
|
+
|
|
5
22
|
## [v0.7.4] - 2026-05-16
|
|
6
23
|
|
|
7
24
|
### Runtime
|
|
@@ -27,6 +27,7 @@ vssh server
|
|
|
27
27
|
export VSSH_SECRET=your-secret
|
|
28
28
|
vssh shell hostname # Interactive shell
|
|
29
29
|
vssh run hostname "cmd" # Execute command
|
|
30
|
+
vssh doctor --json # Diagnose setup before MCP/AI use
|
|
30
31
|
```
|
|
31
32
|
|
|
32
33
|
## CLI Commands
|
|
@@ -47,6 +48,7 @@ vssh get <host:path> <local> # Download
|
|
|
47
48
|
```bash
|
|
48
49
|
vssh status # Show dashboard
|
|
49
50
|
vssh list # List all peers
|
|
51
|
+
vssh doctor # Diagnose binary, secret, config, and peers
|
|
50
52
|
vssh version # Show version
|
|
51
53
|
```
|
|
52
54
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: vssh
|
|
3
|
-
Version: 4.2.
|
|
3
|
+
Version: 4.2.1
|
|
4
4
|
Summary: Python SDK for the vssh AI-native remote execution daemon
|
|
5
5
|
Project-URL: Homepage, https://github.com/meshpop/vssh
|
|
6
6
|
Project-URL: Repository, https://github.com/meshpop/vssh
|
|
@@ -99,6 +99,7 @@ VSSH_SECRET=change-me vssh run web1 "df -h"
|
|
|
99
99
|
# Show status
|
|
100
100
|
vssh # Dashboard
|
|
101
101
|
vssh list # List all nodes
|
|
102
|
+
vssh doctor --json # Diagnose binary, secret, config, peers, and MCP readiness
|
|
102
103
|
```
|
|
103
104
|
|
|
104
105
|
## Commands
|
|
@@ -123,6 +124,7 @@ vssh get <src> <dst> Download through native daemon
|
|
|
123
124
|
vssh exec <node> <cmd> Alias for native run
|
|
124
125
|
vssh list List all nodes
|
|
125
126
|
vssh status Show dashboard
|
|
127
|
+
vssh doctor [--json] Diagnose local setup
|
|
126
128
|
vssh version Show version
|
|
127
129
|
vssh help Show help
|
|
128
130
|
```
|
|
@@ -269,6 +271,7 @@ facts = client.facts("web1")
|
|
|
269
271
|
fleet = client.facts_many(["web1", "db1"])
|
|
270
272
|
results = client.exec_many(["web1", "db1"], "uptime")
|
|
271
273
|
job = client.job_start("web1", "long-running-command")
|
|
274
|
+
doctor = client.doctor()
|
|
272
275
|
```
|
|
273
276
|
|
|
274
277
|
The Python SDK is a client layer for AI agents and automation. It calls the
|
|
@@ -278,13 +281,14 @@ Agent-facing MCP tools:
|
|
|
278
281
|
|
|
279
282
|
| Tool | Purpose |
|
|
280
283
|
|------|---------|
|
|
281
|
-
| `
|
|
282
|
-
| `
|
|
283
|
-
| `
|
|
284
|
-
| `
|
|
285
|
-
| `
|
|
286
|
-
| `
|
|
287
|
-
| `
|
|
284
|
+
| `vssh_doctor` | Diagnose binary, secret source, wire config, peers, and MCP readiness |
|
|
285
|
+
| `vssh_hosts_list` | List known hosts for routing |
|
|
286
|
+
| `vssh_exec_safe` | Execute read/diagnostic commands with policy blocking |
|
|
287
|
+
| `vssh_exec` | Execute with policy checks and optional `allow_dangerous` |
|
|
288
|
+
| `vssh_route_select` | Select the best host by capability, tag, and health |
|
|
289
|
+
| `vssh_exec_routed` | Route first, then execute with policy/evidence |
|
|
290
|
+
| `vssh_policy_check` | Classify a command before execution |
|
|
291
|
+
| `vssh_status`, `vssh_list` | Status and peer inventory |
|
|
288
292
|
|
|
289
293
|
Commands matching destructive/service-impacting patterns such as `rm -rf`,
|
|
290
294
|
`shutdown`, `reboot`, `docker rm`, `kubectl delete`, and `systemctl restart`
|
|
@@ -292,7 +296,7 @@ are blocked unless the caller sets `allow_dangerous: true` after explicit human
|
|
|
292
296
|
approval. Every execution response is an evidence envelope with timestamps,
|
|
293
297
|
policy decision, target, command, timeout, and the structured execution result.
|
|
294
298
|
|
|
295
|
-
`
|
|
299
|
+
`vssh_hosts_list` returns agent-friendly records:
|
|
296
300
|
|
|
297
301
|
```json
|
|
298
302
|
{
|
|
@@ -78,6 +78,7 @@ VSSH_SECRET=change-me vssh run web1 "df -h"
|
|
|
78
78
|
# Show status
|
|
79
79
|
vssh # Dashboard
|
|
80
80
|
vssh list # List all nodes
|
|
81
|
+
vssh doctor --json # Diagnose binary, secret, config, peers, and MCP readiness
|
|
81
82
|
```
|
|
82
83
|
|
|
83
84
|
## Commands
|
|
@@ -102,6 +103,7 @@ vssh get <src> <dst> Download through native daemon
|
|
|
102
103
|
vssh exec <node> <cmd> Alias for native run
|
|
103
104
|
vssh list List all nodes
|
|
104
105
|
vssh status Show dashboard
|
|
106
|
+
vssh doctor [--json] Diagnose local setup
|
|
105
107
|
vssh version Show version
|
|
106
108
|
vssh help Show help
|
|
107
109
|
```
|
|
@@ -248,6 +250,7 @@ facts = client.facts("web1")
|
|
|
248
250
|
fleet = client.facts_many(["web1", "db1"])
|
|
249
251
|
results = client.exec_many(["web1", "db1"], "uptime")
|
|
250
252
|
job = client.job_start("web1", "long-running-command")
|
|
253
|
+
doctor = client.doctor()
|
|
251
254
|
```
|
|
252
255
|
|
|
253
256
|
The Python SDK is a client layer for AI agents and automation. It calls the
|
|
@@ -257,13 +260,14 @@ Agent-facing MCP tools:
|
|
|
257
260
|
|
|
258
261
|
| Tool | Purpose |
|
|
259
262
|
|------|---------|
|
|
260
|
-
| `
|
|
261
|
-
| `
|
|
262
|
-
| `
|
|
263
|
-
| `
|
|
264
|
-
| `
|
|
265
|
-
| `
|
|
266
|
-
| `
|
|
263
|
+
| `vssh_doctor` | Diagnose binary, secret source, wire config, peers, and MCP readiness |
|
|
264
|
+
| `vssh_hosts_list` | List known hosts for routing |
|
|
265
|
+
| `vssh_exec_safe` | Execute read/diagnostic commands with policy blocking |
|
|
266
|
+
| `vssh_exec` | Execute with policy checks and optional `allow_dangerous` |
|
|
267
|
+
| `vssh_route_select` | Select the best host by capability, tag, and health |
|
|
268
|
+
| `vssh_exec_routed` | Route first, then execute with policy/evidence |
|
|
269
|
+
| `vssh_policy_check` | Classify a command before execution |
|
|
270
|
+
| `vssh_status`, `vssh_list` | Status and peer inventory |
|
|
267
271
|
|
|
268
272
|
Commands matching destructive/service-impacting patterns such as `rm -rf`,
|
|
269
273
|
`shutdown`, `reboot`, `docker rm`, `kubectl delete`, and `systemctl restart`
|
|
@@ -271,7 +275,7 @@ are blocked unless the caller sets `allow_dangerous: true` after explicit human
|
|
|
271
275
|
approval. Every execution response is an evidence envelope with timestamps,
|
|
272
276
|
policy decision, target, command, timeout, and the structured execution result.
|
|
273
277
|
|
|
274
|
-
`
|
|
278
|
+
`vssh_hosts_list` returns agent-friendly records:
|
|
275
279
|
|
|
276
280
|
```json
|
|
277
281
|
{
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"encoding/json"
|
|
5
|
+
"fmt"
|
|
6
|
+
"os"
|
|
7
|
+
"os/exec"
|
|
8
|
+
"path/filepath"
|
|
9
|
+
"sort"
|
|
10
|
+
"strings"
|
|
11
|
+
"time"
|
|
12
|
+
|
|
13
|
+
"github.com/meshpop/vssh/internal/config"
|
|
14
|
+
"github.com/meshpop/vssh/internal/ssh"
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
type DoctorReport struct {
|
|
18
|
+
Kind string `json:"kind"`
|
|
19
|
+
Status string `json:"status"`
|
|
20
|
+
Checks []DoctorCheck `json:"checks"`
|
|
21
|
+
NextActions []string `json:"next_actions"`
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
type DoctorCheck struct {
|
|
25
|
+
Name string `json:"name"`
|
|
26
|
+
Status string `json:"status"`
|
|
27
|
+
Detail string `json:"detail,omitempty"`
|
|
28
|
+
Next string `json:"next,omitempty"`
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
func cmdDoctor(args []string) {
|
|
32
|
+
jsonOut := false
|
|
33
|
+
filtered := make([]string, 0, len(args))
|
|
34
|
+
for _, arg := range args {
|
|
35
|
+
if arg == "--json" {
|
|
36
|
+
jsonOut = true
|
|
37
|
+
continue
|
|
38
|
+
}
|
|
39
|
+
filtered = append(filtered, arg)
|
|
40
|
+
}
|
|
41
|
+
if len(filtered) != 0 {
|
|
42
|
+
fmt.Fprintln(os.Stderr, "Usage: vssh doctor [--json]")
|
|
43
|
+
os.Exit(1)
|
|
44
|
+
}
|
|
45
|
+
report := runDoctor()
|
|
46
|
+
if jsonOut {
|
|
47
|
+
writeJSON(report)
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
fmt.Print(formatDoctor(report))
|
|
51
|
+
if report.Status == "fail" {
|
|
52
|
+
os.Exit(1)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
func runDoctor() DoctorReport {
|
|
57
|
+
report := DoctorReport{Kind: "vssh_doctor", Status: "ok"}
|
|
58
|
+
add := func(name, status, detail, next string) {
|
|
59
|
+
report.Checks = append(report.Checks, DoctorCheck{Name: name, Status: status, Detail: detail, Next: next})
|
|
60
|
+
if status == "fail" {
|
|
61
|
+
report.Status = "fail"
|
|
62
|
+
} else if status == "warn" && report.Status == "ok" {
|
|
63
|
+
report.Status = "warn"
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if exe, err := os.Executable(); err == nil {
|
|
68
|
+
add("vssh_binary", "ok", exe, "")
|
|
69
|
+
} else {
|
|
70
|
+
add("vssh_binary", "fail", err.Error(), "reinstall vssh or run the expected binary explicitly")
|
|
71
|
+
}
|
|
72
|
+
add("vssh_version", "ok", fmt.Sprintf("vssh %s", version), "")
|
|
73
|
+
checkBinaryConflicts(add)
|
|
74
|
+
checkSecretSource(add)
|
|
75
|
+
checkWireConfig(add)
|
|
76
|
+
checkPeers(add)
|
|
77
|
+
|
|
78
|
+
switch report.Status {
|
|
79
|
+
case "ok":
|
|
80
|
+
report.NextActions = []string{
|
|
81
|
+
"Run vssh status.",
|
|
82
|
+
"Run vssh facts-many <hosts> before connecting AI operators.",
|
|
83
|
+
"Connect MCP clients to vssh mcp only when direct transport tools are needed.",
|
|
84
|
+
}
|
|
85
|
+
case "warn":
|
|
86
|
+
report.NextActions = []string{
|
|
87
|
+
"Review warning checks before relying on native execution.",
|
|
88
|
+
"Pin MCP clients to the intended vssh binary and shared secret.",
|
|
89
|
+
}
|
|
90
|
+
default:
|
|
91
|
+
report.NextActions = []string{
|
|
92
|
+
"Fix failed checks before starting vsshd or exposing vssh MCP.",
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return report
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
func checkBinaryConflicts(add func(name, status, detail, next string)) {
|
|
99
|
+
current, _ := os.Executable()
|
|
100
|
+
candidates := vsshBinaryCandidates(current)
|
|
101
|
+
versions := map[string]string{}
|
|
102
|
+
for _, candidate := range candidates {
|
|
103
|
+
versions[candidate] = vsshBinaryVersion(candidate)
|
|
104
|
+
}
|
|
105
|
+
if len(versions) == 0 {
|
|
106
|
+
add("vssh_binary_conflict", "warn", "no executable vssh candidates found", "install vssh or add it to PATH")
|
|
107
|
+
return
|
|
108
|
+
}
|
|
109
|
+
if hasVersionConflict(versions) {
|
|
110
|
+
add("vssh_binary_conflict", "warn", formatVersions(versions), "remove stale vssh binaries or pin MCP clients to the intended binary")
|
|
111
|
+
return
|
|
112
|
+
}
|
|
113
|
+
add("vssh_binary_conflict", "ok", formatVersions(versions), "")
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
func checkSecretSource(add func(name, status, detail, next string)) {
|
|
117
|
+
source := secretSource()
|
|
118
|
+
if source == "" {
|
|
119
|
+
add("vssh_secret", "fail", "no VSSH_SECRET, secret file, or wire-derived secret", "set VSSH_SECRET or create ~/.vssh/secret on client and daemon")
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
secret := getSecret()
|
|
123
|
+
if strings.TrimSpace(secret) == "" {
|
|
124
|
+
add("vssh_secret", "fail", "secret source exists but no effective secret was produced", "verify secret file contents or wire config")
|
|
125
|
+
return
|
|
126
|
+
}
|
|
127
|
+
add("vssh_secret", "ok", source+"; value not displayed", "")
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
func checkWireConfig(add func(name, status, detail, next string)) {
|
|
131
|
+
cfg, err := config.LoadWireConfig()
|
|
132
|
+
if err != nil {
|
|
133
|
+
add("wire_config", "warn", "wire config not found", "standalone vssh can still work; configure peers or use explicit host:port")
|
|
134
|
+
return
|
|
135
|
+
}
|
|
136
|
+
detail := "node=" + cfg.NodeName
|
|
137
|
+
if cfg.ServerURL != "" {
|
|
138
|
+
detail += " server_url=set"
|
|
139
|
+
}
|
|
140
|
+
add("wire_config", "ok", detail, "")
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
func checkPeers(add func(name, status, detail, next string)) {
|
|
144
|
+
connector, err := ssh.NewConnector("default")
|
|
145
|
+
if err != nil {
|
|
146
|
+
add("peers", "warn", err.Error(), "verify wire/tailscale peer configuration or use explicit host:port targets")
|
|
147
|
+
return
|
|
148
|
+
}
|
|
149
|
+
peers := connector.ListPeers()
|
|
150
|
+
online := 0
|
|
151
|
+
now := time.Now().Unix()
|
|
152
|
+
for _, peer := range peers {
|
|
153
|
+
if peer.NodeName == "" {
|
|
154
|
+
continue
|
|
155
|
+
}
|
|
156
|
+
if peerOnline(peer, now) {
|
|
157
|
+
online++
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if len(peers) == 0 {
|
|
161
|
+
add("peers", "warn", "peers=0", "configure peers before using routing, facts-many, or MCP host selection")
|
|
162
|
+
return
|
|
163
|
+
}
|
|
164
|
+
add("peers", "ok", fmt.Sprintf("peers=%d online=%d", len(peers), online), "")
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
func peerOnline(peer config.Peer, now int64) bool {
|
|
168
|
+
if peer.Online != nil {
|
|
169
|
+
return *peer.Online
|
|
170
|
+
}
|
|
171
|
+
if peer.LastSeen == nil {
|
|
172
|
+
return false
|
|
173
|
+
}
|
|
174
|
+
switch v := peer.LastSeen.(type) {
|
|
175
|
+
case float64:
|
|
176
|
+
return now-int64(v) < 60
|
|
177
|
+
case int64:
|
|
178
|
+
return now-v < 60
|
|
179
|
+
case string:
|
|
180
|
+
if t, err := time.Parse(time.RFC3339, v); err == nil {
|
|
181
|
+
return time.Since(t) < 60*time.Second
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return false
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
func secretSource() string {
|
|
188
|
+
if strings.TrimSpace(os.Getenv("VSSH_SECRET")) != "" {
|
|
189
|
+
return "env:VSSH_SECRET"
|
|
190
|
+
}
|
|
191
|
+
for _, path := range []string{"/etc/vssh/secret", filepath.Join(homeDir(), ".vssh", "secret")} {
|
|
192
|
+
if st, err := os.Stat(path); err == nil && !st.IsDir() {
|
|
193
|
+
return path
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if strings.TrimSpace(os.Getenv("WIRE_SERVER_URL")) != "" {
|
|
197
|
+
return "env:WIRE_SERVER_URL derived"
|
|
198
|
+
}
|
|
199
|
+
if _, err := config.LoadWireConfig(); err == nil {
|
|
200
|
+
return "wire config derived"
|
|
201
|
+
}
|
|
202
|
+
return ""
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
func homeDir() string {
|
|
206
|
+
home, _ := os.UserHomeDir()
|
|
207
|
+
return home
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
func vsshBinaryCandidates(current string) []string {
|
|
211
|
+
seen := map[string]bool{}
|
|
212
|
+
add := func(path string) {
|
|
213
|
+
if path == "" || seen[path] {
|
|
214
|
+
return
|
|
215
|
+
}
|
|
216
|
+
if st, err := os.Stat(path); err == nil && !st.IsDir() && st.Mode()&0111 != 0 {
|
|
217
|
+
seen[path] = true
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
add(current)
|
|
221
|
+
if path, err := exec.LookPath("vssh"); err == nil {
|
|
222
|
+
add(path)
|
|
223
|
+
}
|
|
224
|
+
add(filepath.Join(homeDir(), "bin", "vssh"))
|
|
225
|
+
add("/usr/local/bin/vssh")
|
|
226
|
+
add("/opt/homebrew/bin/vssh")
|
|
227
|
+
for _, dir := range filepath.SplitList(os.Getenv("PATH")) {
|
|
228
|
+
add(filepath.Join(dir, "vssh"))
|
|
229
|
+
}
|
|
230
|
+
out := make([]string, 0, len(seen))
|
|
231
|
+
for path := range seen {
|
|
232
|
+
out = append(out, path)
|
|
233
|
+
}
|
|
234
|
+
sort.Strings(out)
|
|
235
|
+
return out
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
func vsshBinaryVersion(path string) string {
|
|
239
|
+
out, err := exec.Command(path, "--version").CombinedOutput()
|
|
240
|
+
if err != nil {
|
|
241
|
+
return "unknown"
|
|
242
|
+
}
|
|
243
|
+
return strings.TrimSpace(string(out))
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
func hasVersionConflict(versions map[string]string) bool {
|
|
247
|
+
seen := map[string]bool{}
|
|
248
|
+
for _, version := range versions {
|
|
249
|
+
if version == "" || version == "unknown" {
|
|
250
|
+
continue
|
|
251
|
+
}
|
|
252
|
+
seen[version] = true
|
|
253
|
+
}
|
|
254
|
+
return len(seen) > 1
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
func formatVersions(versions map[string]string) string {
|
|
258
|
+
paths := make([]string, 0, len(versions))
|
|
259
|
+
for path := range versions {
|
|
260
|
+
paths = append(paths, path)
|
|
261
|
+
}
|
|
262
|
+
sort.Strings(paths)
|
|
263
|
+
parts := make([]string, 0, len(paths))
|
|
264
|
+
for _, path := range paths {
|
|
265
|
+
parts = append(parts, path+"="+versions[path])
|
|
266
|
+
}
|
|
267
|
+
return strings.Join(parts, "; ")
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
func formatDoctor(report DoctorReport) string {
|
|
271
|
+
var b strings.Builder
|
|
272
|
+
fmt.Fprintf(&b, "vssh doctor status=%s\n", report.Status)
|
|
273
|
+
for _, check := range report.Checks {
|
|
274
|
+
fmt.Fprintf(&b, "- %s: %s", check.Name, check.Status)
|
|
275
|
+
if check.Detail != "" {
|
|
276
|
+
fmt.Fprintf(&b, " | %s", check.Detail)
|
|
277
|
+
}
|
|
278
|
+
if check.Next != "" {
|
|
279
|
+
fmt.Fprintf(&b, " | next: %s", check.Next)
|
|
280
|
+
}
|
|
281
|
+
b.WriteByte('\n')
|
|
282
|
+
}
|
|
283
|
+
if len(report.NextActions) > 0 {
|
|
284
|
+
b.WriteString("next_actions:\n")
|
|
285
|
+
for _, action := range report.NextActions {
|
|
286
|
+
fmt.Fprintf(&b, "- %s\n", action)
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return b.String()
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
func doctorJSON(report DoctorReport) map[string]interface{} {
|
|
293
|
+
payload, _ := json.Marshal(report)
|
|
294
|
+
out := map[string]interface{}{}
|
|
295
|
+
json.Unmarshal(payload, &out)
|
|
296
|
+
return out
|
|
297
|
+
}
|
|
@@ -19,7 +19,7 @@ import (
|
|
|
19
19
|
)
|
|
20
20
|
|
|
21
21
|
var (
|
|
22
|
-
version = "0.7.
|
|
22
|
+
version = "0.7.5"
|
|
23
23
|
buildTime = ""
|
|
24
24
|
)
|
|
25
25
|
|
|
@@ -65,6 +65,8 @@ func main() {
|
|
|
65
65
|
cmdArtifactCollect(os.Args[2:])
|
|
66
66
|
case "setup":
|
|
67
67
|
cmdSetup()
|
|
68
|
+
case "doctor", "setup-check", "install-check":
|
|
69
|
+
cmdDoctor(os.Args[2:])
|
|
68
70
|
case "status":
|
|
69
71
|
cmdStatus()
|
|
70
72
|
case "list", "ls":
|
|
@@ -115,6 +117,7 @@ Usage:
|
|
|
115
117
|
vssh bench <host> [count] Measure native exec latency
|
|
116
118
|
|
|
117
119
|
vssh status Show connection status
|
|
120
|
+
vssh doctor [--json] Diagnose local binary, secret, peers, and MCP readiness
|
|
118
121
|
vssh list List all peers
|
|
119
122
|
vssh agent Run monitoring agent
|
|
120
123
|
vssh mcp Run MCP JSON-RPC server
|