pulsos-cli 0.1.0

Cross-platform deployment monitoring CLI
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
# <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Huge Icons by Hugeicons - undefined --><g fill="none" stroke="currentColor" stroke-linecap="round"><path stroke-width="1.5" d="M16.093 13C17.23 12.098 18 10.85 18 9.47C18 6.72 14.945 2 12 2C9.054 2 6 6.72 6 9.47c0 1.38.77 2.628 1.908 3.53"/><path stroke-linejoin="round" stroke-width="2" d="M14.012 10h-.01m-3.99 0h-.01"/><path stroke-linejoin="round" stroke-width="1.5" d="M13.5 14c-.484 4 .4 5.714 3.5 8m-6.5-8c.484 4-.4 5.714-3.5 8m6.5-8c1 2 3.58 4.5 5.547 4.5c1.969 0 2.953-1.231 2.953-2.75S20.898 13 19.54 13m-9.04 1c-1 2-3.58 4.5-5.548 4.5S2 17.269 2 15.75S3.102 13 4.46 13"/></g></svg> PULSOS CLI


[![CI](https://github.com/Vivallo04/pulsos-cli/actions/workflows/ci.yml/badge.svg)](https://github.com/Vivallo04/pulsos-cli/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)

Cross-platform deployment monitoring CLI. Track deployments across GitHub Actions, Railway, and Vercel from a single terminal.

## What is Pulsos

Pulsos is a terminal tool that gives you a unified view of deployments across GitHub Actions, Railway, and Vercel. It runs as a live TUI dashboard, as a persistent background daemon with a native system-tray icon, or as one-shot CLI output. The correlation engine matches deployment events across platforms by commit SHA and timestamp heuristics, so you can see how a single commit flows through your CI/CD pipeline.

## Features

- **Live TUI** with 5 tabs: Unified, Platform, Health, Settings, Logs
- **Cross-platform correlation** — matches events by SHA and timestamp proximity
- **Health scoring** per project (weighted: GitHub 40%, Railway 35%, Vercel 25%)
- **DORA metrics** — Deployment Frequency, Lead Time, Change Failure Rate, Time to Restore
- **Real-time telemetry** — Railway container metrics (CPU/RAM) and blackbox endpoint pings (TTFB/uptime)
- **Background daemon** — `pulsos daemon start` runs a persistent process with an Axum SSE server and native tray icon (macOS / Windows)
- **Auto-discovery** of repos, projects, and services via `repos sync`
- **Saved views** with filters for project, platform, branch, and status
- **ETag caching** with rate-limit-aware adaptive polling
- **Credential storage** — OS keyring with file fallback (`~/.config/pulsos/credentials.toml`)
- **Output formats** — table, compact, JSON
- **Shell completions** for bash, zsh, fish, powershell, elvish

## Install

### From crates.io

```sh
cargo install pulsos-cli
```

### From source

```sh
git clone https://github.com/Vivallo04/pulsos-cli.git
cd pulsos-cli
cargo install --path crates/pulsos-cli
```

### Homebrew (macOS / Linux)

```sh
brew install Vivallo04/tap/pulsos
```

### Requirements

- Rust 1.75+

## Quick Start

```sh
# 1. Authenticate with your platforms
pulsos auth github
pulsos auth railway
pulsos auth vercel

# 2. Discover and track your repos/projects
pulsos repos sync

# 3. Launch the live TUI dashboard
pulsos status --watch

# Or get a one-shot table in your terminal
pulsos status --once

# Or run as a persistent background daemon
pulsos daemon start
```

**What happens at each step:**

1. `pulsos auth <platform>` prompts for a token and stores it in your OS keyring (falls back to `~/.config/pulsos/credentials.toml`).
2. `pulsos repos sync` queries each authenticated platform, discovers available repos/projects/services, lets you pick which ones to track, and auto-generates correlations.
3. `pulsos status` fetches recent deployments from all tracked resources, correlates them, and displays the result. With `--watch` it launches the live TUI; without flags it auto-detects (TUI if interactive terminal, one-shot otherwise).
4. `pulsos daemon start` detaches the daemon to the background. The TUI automatically connects to the daemon's SSE stream when one is running, saving API quota.

## Authentication

### Token Resolution Order

Pulsos resolves tokens in this priority order:

1. **Environment variables** — checked first
2. **OS keyring** — via `keyring` crate (macOS Keychain, Windows Credential Manager, Linux Secret Service)
3. **CLI config detection** — reads tokens from `gh`, `railway`, and `vercel` CLI config files

### Environment Variables

| Platform | Variables |
|----------|-----------|
| GitHub   | `GH_TOKEN`, `GITHUB_TOKEN` |
| Railway  | `RAILWAY_TOKEN`, `RAILWAY_API_TOKEN` |
| Vercel   | `VERCEL_TOKEN` |

### Interactive Setup

```sh
pulsos auth github              # Authenticate with GitHub
pulsos auth railway             # Authenticate with Railway
pulsos auth vercel              # Authenticate with Vercel
```

Each command prompts for a token, validates it against the platform API, and stores it.

### Non-Interactive / CI

```sh
# Store tokens in one shot (CI mode)
pulsos auth --ci --github-token=ghp_xxx --railway-token=xxx --vercel-token=xxx

# Check which tokens are resolved
pulsos auth status

# Re-validate stored tokens; re-prompts if invalid
pulsos auth refresh

# Remove stored tokens
pulsos auth logout
pulsos auth logout --platform github
```

### Token Detection Toggles

You can disable automatic detection of specific sources in `config.toml`:

```toml
[auth.token_detection]
detect_gh_cli = true
detect_railway_cli = true
detect_vercel_cli = true
detect_env_vars = true
```

## Configuration

Config file location: `~/.config/pulsos/config.toml`

Run `pulsos config path` to print the exact path, or `pulsos config edit` to open it in `$EDITOR`.

### Full Example

```toml
[auth]
github_host = "github.mycompany.com"   # Default: "github.com"

[auth.token_detection]
detect_gh_cli = true
detect_railway_cli = false
detect_vercel_cli = true
detect_env_vars = true

[[github.organizations]]
name = "myorg"
include_patterns = ["api-*"]
exclude_patterns = ["*-legacy"]
auto_discover = true

[[railway.workspaces]]
name = "lambda-prod"
include_projects = ["my-saas-api"]
default_environment = "production"      # Default: "production"

[[vercel.teams]]
name = "lambda"
include_projects = ["my-saas-web"]
include_preview_deployments = true

[[correlations]]
name = "my-saas"
github_repo = "myorg/my-saas"
railway_project = "my-saas-api"
vercel_project = "my-saas-web"

[correlations.branch_mapping]
main = "production"
develop = "staging"

[[views]]
name = "production"
description = "Production systems"
projects = ["my-saas", "api-core"]
platforms = ["github", "railway", "vercel"]
branch_filter = "main"
status_filter = ["success", "failure"]
refresh_interval = 5

[[groups]]
name = "backend"
resources = ["api-core", "auth-service"]

[tui]
theme = "dark"                  # "dark" or "light"
fps = 10                        # Render frames per second
refresh_interval = 5            # Seconds between API polls
default_tab = "unified"         # "unified", "platform", "health", "settings", "logs"
show_sparklines = true
unicode = "auto"                # "auto", "always", "never"

[cache]
max_size_mb = 100
```

### Config Sections

| Section | Purpose |
|---------|---------|
| `[auth]` | GitHub Enterprise host, token detection toggles |
| `[[github.organizations]]` | Org name, include/exclude repo patterns |
| `[[railway.workspaces]]` | Workspace name, include/exclude projects, default environment |
| `[[vercel.teams]]` | Team name, include projects, preview deployment toggle |
| `[[correlations]]` | Link a GitHub repo, Railway project, and Vercel project under one name |
| `[[views]]` | Named filter presets (projects, platforms, branch, status) |
| `[[groups]]` | Named collections of resources for UI-level grouping |
| `[tui]` | Theme, FPS, refresh interval, default tab, sparklines |
| `[cache]` | Max cache size in MB |

## CLI Commands

### Status

```sh
pulsos                              # Default: show deployment status
pulsos status                       # Same as above
pulsos status --watch               # Live TUI mode
pulsos status --once                # Force one-shot output
pulsos status --platform github     # Filter by platform
pulsos status --view production     # Use a saved view
pulsos status --branch main         # Filter by branch
pulsos status --format json         # JSON output
pulsos status --format compact      # Compact output
```

### Auth

```sh
pulsos auth github                  # Authenticate with GitHub
pulsos auth railway                 # Authenticate with Railway
pulsos auth vercel                  # Authenticate with Vercel
pulsos auth github --token ghp_xxx  # Non-interactive with token
pulsos auth status                  # Check auth status across platforms
pulsos auth refresh                 # Re-validate tokens, re-prompt if invalid
pulsos auth refresh --platform github  # Refresh a specific platform
pulsos auth logout                  # Remove tokens (prompts for platform)
pulsos auth logout --platform github   # Remove a specific platform token
pulsos auth --ci --github-token=xxx    # CI mode: store tokens non-interactively
pulsos auth --from-env              # Only check env vars (skip keyring/interactive)
```

### Repos

```sh
pulsos repos sync                   # Discover, select, and save (all platforms)
pulsos repos list                   # Show tracked repos/projects
pulsos repos add github:org/repo    # Add a resource
pulsos repos remove github:org/repo # Remove a resource
pulsos repos correlate my-saas      # Edit correlations for a project
pulsos repos groups list            # List resource groups
pulsos repos groups create mygroup -- github:org/repo railway:project
pulsos repos groups delete mygroup  # Delete a group
pulsos repos verify                 # Check access permissions for tracked resources
```

### Views

```sh
pulsos views                        # List all views (default)
pulsos views list                   # List all views
pulsos views show production        # Display view details
pulsos views create                 # Create a view interactively
pulsos views edit production        # Edit a view interactively
pulsos views delete production      # Delete a view
pulsos views templates              # List built-in templates
pulsos views validate production    # Validate view projects against correlations
pulsos views export production -o view.json  # Export to JSON
pulsos views import view.json       # Import from JSON
```

### Config

```sh
pulsos config                       # Print current config as TOML (default)
pulsos config show                  # Print current config as TOML
pulsos config path                  # Print config file path
pulsos config edit                  # Open config in $EDITOR
pulsos config wizard                # Run interactive platform setup wizard
```

### Daemon

```sh
pulsos daemon run                   # Run the daemon in the foreground (owns main thread for tray icon)
pulsos daemon start                 # Start the daemon as a detached background process
pulsos daemon stop                  # Stop the running daemon (sends SIGTERM via PID file)
pulsos daemon status                # Print daemon running state and port
```

**Daemon architecture:**
- `daemon run` starts an Axum SSE server on an ephemeral port, writes the port to `~/.config/pulsos/daemon.port`, generates a random 64-char bearer token at `~/.config/pulsos/daemon.token` (mode 0600 on Unix), and (on macOS/Windows) shows a native tray icon in the menu bar.
- `daemon start` re-execs `daemon run` as a detached process (stdin/stdout/stderr closed).
- `daemon stop` reads `~/.config/pulsos/daemon.pid` and sends SIGTERM (Unix) or `taskkill /F` (Windows), then removes `daemon.pid`, `daemon.port`, and `daemon.token`.
- `daemon status` checks the `/health` endpoint of the running daemon.
- When a daemon is running, `pulsos status --watch` connects to its bearer-auth-protected SSE stream (`/api/stream`) instead of polling platform APIs directly, conserving rate-limit budget. The `/health` endpoint remains public (used by `daemon status`).
- The tray icon shows three states: neutral (all OK), syncing (blue), alert (red — endpoint down or deployment failure on main/master).
- Desktop notifications fire on endpoint state transitions (up→down, down→up) on macOS and Windows.

### Other

```sh
pulsos doctor                       # Run diagnostics (system, auth, connectivity, cache)
pulsos completions bash             # Generate shell completions
pulsos completions zsh
pulsos completions fish
pulsos completions powershell
pulsos completions elvish
```

### Global Flags

| Flag | Description |
|------|-------------|
| `--format <table\|compact\|json>` | Output format (default: table) |
| `--no-color` | Disable color output |
| `--verbose` | Show debug information |
| `--config <path>` | Custom config file path |

## TUI Keyboard Shortcuts

| Key | Action |
|-----|--------|
| `1`–`5` | Switch to tab (Unified, Platform, Health, Settings, Logs) |
| `Tab` / `Shift+Tab` | Cycle tabs forward / backward |
| `j` / `k` or `↑` / `↓` | Navigate rows |
| `/` | Enter search mode |
| `Enter` | Apply search (in search mode) |
| `Esc` | Exit search mode / cancel |
| `r` | Force refresh |
| `s` | Cycle sort order on Unified tab (by time / by platform) |
| `q` / `Ctrl+C` | Quit |

### Platform Tab

| Key | Action |
|-----|--------|
| `←` / `→` | Switch provider subtab (GitHub / Railway / Vercel) |
| `g` | Switch to GitHub subtab |
| `w` | Switch to Railway subtab |
| `v` | Switch to Vercel subtab |
| `d` | Toggle GitHub job/step details panel (GitHub subtab only) |

### Platform Tab — Details Panel (GitHub)

| Key | Action |
|-----|--------|
| `↑` / `↓` or `j` / `k` | Navigate tree / scroll right panel |
| `→` / `Enter` | Expand job or open right panel |
| `←` | Collapse job or focus left tree |
| `PageDown` / `PageUp` | Scroll right panel by 20 lines |
| `Ctrl+D` / `Ctrl+U` | Scroll right panel by 10 lines |
| `Esc` | Close details panel |

### Logs Tab

| Key | Action |
|-----|--------|
| `f` | Cycle log level filter (ALL → ERR → WARN → INFO → ALL) |
| `c` | Copy selected log message to clipboard |

### Settings Tab

| Key | Action |
|-----|--------|
| `t` | Enter token for selected platform |
| `T` | Enter token (force override even with env token) |
| `v` | Validate selected platform |
| `x` | Remove stored token |
| `o` / `Enter` | Start onboarding / discovery flow |

## Project Architecture

```
pulsos-cli/
├── crates/
│   ├── pulsos-core/          # Library crate
│   │   └── src/
│   │       ├── platform/     # GitHub, Railway, Vercel API clients + retry helper
│   │       ├── correlation/  # Event matching engine (SHA + timestamp heuristic)
│   │       ├── domain/       # DeploymentEvent, CorrelatedEvent, DoraMetrics,
│   │       │                 # ResourceMetrics, EndpointHealth, health scoring
│   │       ├── auth/         # Credential resolution, KeyringStore, FileCredentialStore, FallbackStore
│   │       ├── config/       # TOML config loading and saving
│   │       ├── cache/        # sled-based ETag cache
│   │       ├── health/       # Platform health checks (PlatformHealthReport) + PingEngine
│   │       ├── scheduler/    # Polling budget and adaptive scheduler
│   │       ├── analytics/    # DoraCalculator
│   │       └── sync/         # Auto-correlation builder (build_correlations, merge)
│   ├── pulsos-cli/           # Binary crate
│   │   ├── assets/           # Embedded tray icon PNGs (neutral, sync, alert — 16x16)
│   │   └── src/
│   │       ├── main.rs       # CLI entry point (clap); daemon run owns main thread for tray
│   │       ├── commands/     # Command handlers: status, auth, repos, views, config, doctor, daemon
│   │       │   ├── daemon.rs # pulsos daemon [run|start|stop|status]
│   │       │   ├── wizard.rs # First-run config wizard (auth + discovery + re-check)
│   │       │   └── ui/       # Full-screen interactive prompt layer (ScreenSession)
│   │       ├── daemon/       # Background daemon subsystem
│   │       │   ├── engine.rs # Wraps run_poller; broadcasts DataSnapshot via SSE channel
│   │       │   ├── server.rs # Axum SSE server (/api/stream, /health); writes daemon.port
│   │       │   ├── tray.rs   # Native tray icon (macOS/Windows only; tray-icon + tao + muda)
│   │       │   └── notify.rs # Desktop notifications on endpoint state transitions
│   │       ├── tui/          # ratatui live TUI (5 tabs: Unified, Platform, Health, Settings, Logs)
│   │       │   ├── poll.rs   # Background poller; connects to SSE if daemon is running
│   │       │   ├── actions.rs # ActionRequest/ActionResult async channel for settings
│   │       │   └── widgets/  # Per-tab rendering components + log_buffer, settings_flow
│   │       └── output/       # Table, compact, JSON formatters
│   └── pulsos-test/          # Test helpers and builders (EventBuilder, mock servers)
└── Cargo.toml                # Workspace root
```

### Daemon File Locations

| File | Path | Notes |
|------|------|-------|
| Port file | `~/.config/pulsos/daemon.port` | Written by SSE server on bind; mode 0600 on Unix |
| PID file | `~/.config/pulsos/daemon.pid` | Written by `daemon run` on startup |
| Token file | `~/.config/pulsos/daemon.token` | 64-char random hex bearer token; mode 0600 on Unix; removed on stop |
| Credentials | `~/.config/pulsos/credentials.toml` | File-based fallback; mode 0600 on Unix |
| Config | `~/.config/pulsos/config.toml` | Main TOML config |

### Polling Intervals (TUI / Poller)

| Platform | Interval | Notes |
|----------|----------|-------|
| GitHub | 30s (adaptive) | Backs off to 60s/120s as rate limit is consumed |
| Railway | 15s | |
| Vercel | 15s | |
| Health checks | 30s | Per-platform token + connectivity readiness |
| Container metrics | 30s | Railway CPU/RAM via GraphQL |
| Endpoint pings | 8s | Blackbox TTFB/uptime; does not consume platform quota |

## Development

### Prerequisites

- Rust 1.75+

### Build

```sh
cargo build --workspace
```

### Test

```sh
cargo test --workspace
```

### Lint

```sh
cargo fmt --all -- --check
cargo clippy --workspace --all-targets -- -D warnings
```

### Run Locally

```sh
cargo run --bin pulsos -- status
cargo run --bin pulsos -- status --watch
cargo run --bin pulsos -- doctor
cargo run --bin pulsos -- daemon run
```

## License

MIT

---

Last Updated: 2026-02-23