rms-devremote 3.0.0
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.
- package/README.md +154 -0
- package/dist/commands/attach.d.ts +2 -0
- package/dist/commands/attach.js +10 -0
- package/dist/commands/check.d.ts +2 -0
- package/dist/commands/check.js +210 -0
- package/dist/commands/clean.d.ts +2 -0
- package/dist/commands/clean.js +177 -0
- package/dist/commands/dashboard.d.ts +2 -0
- package/dist/commands/dashboard.js +57 -0
- package/dist/commands/link.d.ts +2 -0
- package/dist/commands/link.js +112 -0
- package/dist/commands/ping.d.ts +2 -0
- package/dist/commands/ping.js +21 -0
- package/dist/commands/setup.d.ts +2 -0
- package/dist/commands/setup.js +54 -0
- package/dist/commands/status.d.ts +2 -0
- package/dist/commands/status.js +65 -0
- package/dist/commands/unlink.d.ts +2 -0
- package/dist/commands/unlink.js +53 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +55 -0
- package/dist/server/auth.d.ts +6 -0
- package/dist/server/auth.js +32 -0
- package/dist/server/frontend.d.ts +4 -0
- package/dist/server/frontend.js +886 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +283 -0
- package/dist/server/terminal.d.ts +14 -0
- package/dist/server/terminal.js +43 -0
- package/dist/services/battery-worker.d.ts +1 -0
- package/dist/services/battery-worker.js +2 -0
- package/dist/services/battery.d.ts +27 -0
- package/dist/services/battery.js +152 -0
- package/dist/services/config.d.ts +63 -0
- package/dist/services/config.js +84 -0
- package/dist/services/docker.d.ts +25 -0
- package/dist/services/docker.js +75 -0
- package/dist/services/hooks.d.ts +15 -0
- package/dist/services/hooks.js +111 -0
- package/dist/services/ntfy.d.ts +19 -0
- package/dist/services/ntfy.js +63 -0
- package/dist/services/process.d.ts +30 -0
- package/dist/services/process.js +90 -0
- package/dist/services/proxy-worker.d.ts +1 -0
- package/dist/services/proxy-worker.js +12 -0
- package/dist/services/proxy.d.ts +4 -0
- package/dist/services/proxy.js +195 -0
- package/dist/services/shell.d.ts +22 -0
- package/dist/services/shell.js +47 -0
- package/dist/services/tmux.d.ts +30 -0
- package/dist/services/tmux.js +74 -0
- package/dist/services/ttyd.d.ts +28 -0
- package/dist/services/ttyd.js +71 -0
- package/dist/setup-server/routes.d.ts +4 -0
- package/dist/setup-server/routes.js +177 -0
- package/dist/setup-server/server.d.ts +4 -0
- package/dist/setup-server/server.js +32 -0
- package/docker/docker-compose.yml +24 -0
- package/docker/ntfy/server.yml +6 -0
- package/package.json +61 -0
- package/scripts/claude-remote.sh +583 -0
- package/scripts/hooks/notify.sh +68 -0
- package/scripts/notify.sh +54 -0
- package/scripts/startup.sh +29 -0
- package/scripts/update-check.sh +25 -0
- package/src/setup-server/public/index.html +21 -0
- package/src/setup-server/public/setup.css +475 -0
- package/src/setup-server/public/setup.js +687 -0
package/README.md
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# rms-devremote
|
|
2
|
+
|
|
3
|
+
Control your terminal remotely from your phone — with push notifications, a mobile-optimized PWA, and zero open ports.
|
|
4
|
+
|
|
5
|
+
## How it works
|
|
6
|
+
|
|
7
|
+
Your machine runs a **tmux session** exposed via a custom **xterm.js terminal server** (WebSocket + node-pty). A **Cloudflare Tunnel** secures the connection with no open ports. When Claude Code needs your approval, **ntfy** sends a push notification to your phone.
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
Phone (PWA)
|
|
11
|
+
|
|
|
12
|
+
├─ terminal.your-domain.com ─► Cloudflare Tunnel ─► Terminal Server (port 7681)
|
|
13
|
+
| └─► node-pty → tmux session
|
|
14
|
+
|
|
|
15
|
+
└─ notify.your-domain.com ─► Cloudflare Tunnel ─► ntfy (Docker)
|
|
16
|
+
▲
|
|
17
|
+
Claude hooks
|
|
18
|
+
(Notification / Stop)
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Mobile PWA Features
|
|
22
|
+
|
|
23
|
+
- **xterm.js v6** terminal with high-contrast OLED dark theme
|
|
24
|
+
- **Touch toolbar** — ← → ↑ ↓ Enter ⌫ Esc Tab Y N ^C
|
|
25
|
+
- **Scroll mode** — tmux copy-mode with progressive acceleration
|
|
26
|
+
- **Input bar** — real-time character forwarding to terminal
|
|
27
|
+
- **Double-tap** = Enter, **swipe gestures** for navigation
|
|
28
|
+
- **Info panel** — tap "devremote" header to see status, path, uptime
|
|
29
|
+
- **Disconnect screen** — clear feedback when server is unreachable
|
|
30
|
+
- **PWA installable** — add to home screen, works offline (cached assets)
|
|
31
|
+
- **Orientation support** — landscape/portrait auto-resize
|
|
32
|
+
|
|
33
|
+
## Security
|
|
34
|
+
|
|
35
|
+
1. **Cloudflare Tunnel** — zero open ports, IP hidden, traffic encrypted
|
|
36
|
+
2. **HTTP Basic Auth** — credentials required for all routes + WebSocket
|
|
37
|
+
3. **Single client** — only one WebSocket connection at a time
|
|
38
|
+
|
|
39
|
+
## Prerequisites
|
|
40
|
+
|
|
41
|
+
- **Docker** installed and running
|
|
42
|
+
- A **Cloudflare** account with a domain and a Tunnel token
|
|
43
|
+
- **tmux** installed on your machine
|
|
44
|
+
- **Node.js 20+** with build tools (`make`, `gcc` for node-pty)
|
|
45
|
+
- The **ntfy** app on your phone
|
|
46
|
+
|
|
47
|
+
## Installation
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
npm install -g rms-devremote
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Commands
|
|
54
|
+
|
|
55
|
+
| Command | Description |
|
|
56
|
+
|---|---|
|
|
57
|
+
| `rms-devremote` | Open interactive dashboard |
|
|
58
|
+
| `rms-devremote setup` | First-time setup wizard |
|
|
59
|
+
| `rms-devremote link` | Start tunnel, terminal server, hooks, tmux |
|
|
60
|
+
| `rms-devremote unlink` | Stop everything, remove hooks |
|
|
61
|
+
| `rms-devremote attach` | Attach locally to the tmux session |
|
|
62
|
+
| `rms-devremote status` | Show live status of all services |
|
|
63
|
+
| `rms-devremote ping` | Send a test notification |
|
|
64
|
+
| `rms-devremote check` | Full system health check |
|
|
65
|
+
| `rms-devremote clean` | Interactive cleanup |
|
|
66
|
+
|
|
67
|
+
## Quick start
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
# 1. First-time setup (run once)
|
|
71
|
+
rms-devremote setup
|
|
72
|
+
|
|
73
|
+
# 2. Link your machine
|
|
74
|
+
rms-devremote link
|
|
75
|
+
|
|
76
|
+
# 3. Open the PWA on your phone
|
|
77
|
+
# https://terminal.your-domain.com
|
|
78
|
+
# Enter credentials → terminal + toolbar
|
|
79
|
+
|
|
80
|
+
# 4. Check status anytime
|
|
81
|
+
rms-devremote status
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Architecture
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
rms-devremote/
|
|
88
|
+
├── src/
|
|
89
|
+
│ ├── index.ts CLI entry point (Commander)
|
|
90
|
+
│ ├── commands/
|
|
91
|
+
│ │ ├── setup.ts Interactive setup wizard
|
|
92
|
+
│ │ ├── link.ts Start tunnel + server + hooks
|
|
93
|
+
│ │ ├── unlink.ts Stop tunnel + remove hooks
|
|
94
|
+
│ │ ├── attach.ts Local tmux attach
|
|
95
|
+
│ │ ├── status.ts Live status dashboard
|
|
96
|
+
│ │ ├── ping.ts Send test notification
|
|
97
|
+
│ │ ├── check.ts System health check
|
|
98
|
+
│ │ ├── clean.ts Interactive cleanup
|
|
99
|
+
│ │ ├── dashboard.ts Default no-arg view
|
|
100
|
+
│ │ └── battery.ts Battery worker entrypoint
|
|
101
|
+
│ ├── server/
|
|
102
|
+
│ │ ├── index.ts Express + WebSocket server
|
|
103
|
+
│ │ ├── terminal.ts node-pty wrapper (tmux attach)
|
|
104
|
+
│ │ ├── auth.ts HTTP Basic Auth middleware
|
|
105
|
+
│ │ └── frontend.ts Inline HTML/CSS/JS generator (PWA)
|
|
106
|
+
│ ├── services/
|
|
107
|
+
│ │ ├── config.ts Config + env file I/O
|
|
108
|
+
│ │ ├── docker.ts Docker daemon + container management
|
|
109
|
+
│ │ ├── ttyd.ts Terminal server start/stop/pid
|
|
110
|
+
│ │ ├── tmux.ts tmux session management
|
|
111
|
+
│ │ ├── ntfy.ts ntfy notifications + health
|
|
112
|
+
│ │ ├── hooks.ts Claude hooks merge/remove
|
|
113
|
+
│ │ ├── battery.ts Battery info + sleep inhibit
|
|
114
|
+
│ │ ├── process.ts PID file management
|
|
115
|
+
│ │ └── shell.ts Shell helpers
|
|
116
|
+
│ └── setup-server/ Setup wizard web UI
|
|
117
|
+
├── docker/ Docker Compose stack (tunnel + ntfy)
|
|
118
|
+
└── scripts/ Hook scripts (notify.sh)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Data directory
|
|
122
|
+
|
|
123
|
+
All runtime data is stored in `~/.rms-devremote/`:
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
~/.rms-devremote/
|
|
127
|
+
├── config.json Domains, ports, battery thresholds
|
|
128
|
+
├── .env Secrets (tunnel token, credentials, ntfy password)
|
|
129
|
+
├── hooks.json Claude hook definitions
|
|
130
|
+
├── pin.hash PIN hash (for future lock screen)
|
|
131
|
+
├── inhibit.pid Sleep inhibitor PID
|
|
132
|
+
├── battery.pid Battery watcher PID
|
|
133
|
+
├── notify.sh Claude notification hook script
|
|
134
|
+
├── docker-compose.yml Generated Docker Compose file
|
|
135
|
+
└── ntfy/server.yml ntfy server configuration
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## How notifications work
|
|
139
|
+
|
|
140
|
+
When Claude Code runs inside the tmux session:
|
|
141
|
+
- **Notification hook** — fires when Claude needs your permission → sends ntfy push
|
|
142
|
+
- **Stop hook** — fires when Claude finishes → sends ntfy push
|
|
143
|
+
|
|
144
|
+
You receive the notification on your phone, open the PWA, and approve/deny using the toolbar buttons.
|
|
145
|
+
|
|
146
|
+
## Platforms
|
|
147
|
+
|
|
148
|
+
- **Linux** — fully supported
|
|
149
|
+
- **macOS** — fully supported (uses `caffeinate` for sleep inhibit)
|
|
150
|
+
- **Windows** — WSL2 required
|
|
151
|
+
|
|
152
|
+
## License
|
|
153
|
+
|
|
154
|
+
MIT
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { sessionExists, attachSession } from '../services/tmux.js';
|
|
3
|
+
export async function attach() {
|
|
4
|
+
if (!sessionExists()) {
|
|
5
|
+
console.error(chalk.red('✖ Aucune session active. Lance \'rms-devremote link\''));
|
|
6
|
+
process.exit(1);
|
|
7
|
+
}
|
|
8
|
+
attachSession();
|
|
9
|
+
}
|
|
10
|
+
export default attach;
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { existsSync } from 'fs';
|
|
3
|
+
import { isDockerRunning, getContainerStatus } from '../services/docker.js';
|
|
4
|
+
import { healthCheck } from '../services/ntfy.js';
|
|
5
|
+
import { listSessions } from '../services/tmux.js';
|
|
6
|
+
import { isTtydRunning } from '../services/ttyd.js';
|
|
7
|
+
import { hasDevremoteHooks } from '../services/hooks.js';
|
|
8
|
+
import { which } from '../services/shell.js';
|
|
9
|
+
import { readPid, isProcessRunning } from '../services/process.js';
|
|
10
|
+
import { getBatteryInfo } from '../services/battery.js';
|
|
11
|
+
import { INHIBIT_PID_PATH, BATTERY_PID_PATH } from '../services/config.js';
|
|
12
|
+
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
13
|
+
function ok(label, detail = '') {
|
|
14
|
+
const suffix = detail ? chalk.gray(` — ${detail}`) : '';
|
|
15
|
+
console.log(` ${chalk.green('✔')} ${label}${suffix}`);
|
|
16
|
+
}
|
|
17
|
+
function warn(label, detail = '') {
|
|
18
|
+
const suffix = detail ? chalk.gray(` — ${detail}`) : '';
|
|
19
|
+
console.log(` ${chalk.yellow('⚠')} ${label}${suffix}`);
|
|
20
|
+
}
|
|
21
|
+
function fail(label, detail = '') {
|
|
22
|
+
const suffix = detail ? chalk.gray(` — ${detail}`) : '';
|
|
23
|
+
console.log(` ${chalk.red('✖')} ${label}${suffix}`);
|
|
24
|
+
}
|
|
25
|
+
function containerLabel(status) {
|
|
26
|
+
if (status === 'running')
|
|
27
|
+
return chalk.green('running');
|
|
28
|
+
if (status === 'stopped')
|
|
29
|
+
return chalk.yellow('stopped');
|
|
30
|
+
return chalk.red('missing');
|
|
31
|
+
}
|
|
32
|
+
// ── Main ───────────────────────────────────────────────────────────────────
|
|
33
|
+
export async function check() {
|
|
34
|
+
let hasProblems = false;
|
|
35
|
+
console.log();
|
|
36
|
+
console.log(chalk.bold.cyan('rms-devremote — System Check'));
|
|
37
|
+
console.log(chalk.cyan('─────────────────────────────────────────'));
|
|
38
|
+
// ── Docker ──────────────────────────────────────────────────────────────
|
|
39
|
+
console.log();
|
|
40
|
+
console.log(chalk.bold.white('Docker:'));
|
|
41
|
+
const dockerRunning = isDockerRunning();
|
|
42
|
+
if (dockerRunning) {
|
|
43
|
+
ok('Docker daemon');
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
fail('Docker daemon', 'not running — start Docker first');
|
|
47
|
+
hasProblems = true;
|
|
48
|
+
}
|
|
49
|
+
const tunnelStatus = getContainerStatus('devremote-tunnel');
|
|
50
|
+
if (tunnelStatus === 'running') {
|
|
51
|
+
ok('devremote-tunnel', containerLabel(tunnelStatus));
|
|
52
|
+
}
|
|
53
|
+
else if (tunnelStatus === 'stopped') {
|
|
54
|
+
warn('devremote-tunnel', `${containerLabel(tunnelStatus)} — run: docker compose up -d`);
|
|
55
|
+
hasProblems = true;
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
fail('devremote-tunnel', `${containerLabel(tunnelStatus)} — run: rms-devremote link`);
|
|
59
|
+
hasProblems = true;
|
|
60
|
+
}
|
|
61
|
+
const ntfyStatus = getContainerStatus('devremote-ntfy');
|
|
62
|
+
if (ntfyStatus === 'running') {
|
|
63
|
+
ok('devremote-ntfy', containerLabel(ntfyStatus));
|
|
64
|
+
}
|
|
65
|
+
else if (ntfyStatus === 'stopped') {
|
|
66
|
+
warn('devremote-ntfy', `${containerLabel(ntfyStatus)} — run: docker compose up -d`);
|
|
67
|
+
hasProblems = true;
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
fail('devremote-ntfy', `${containerLabel(ntfyStatus)} — run: rms-devremote link`);
|
|
71
|
+
hasProblems = true;
|
|
72
|
+
}
|
|
73
|
+
// ── ntfy health ─────────────────────────────────────────────────────────
|
|
74
|
+
const ntfyHealthy = await healthCheck();
|
|
75
|
+
if (ntfyHealthy) {
|
|
76
|
+
ok('ntfy health endpoint', 'healthy');
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
warn('ntfy health endpoint', 'unreachable — notifications may not work');
|
|
80
|
+
hasProblems = true;
|
|
81
|
+
}
|
|
82
|
+
// ── Services ────────────────────────────────────────────────────────────
|
|
83
|
+
console.log();
|
|
84
|
+
console.log(chalk.bold.white('Services:'));
|
|
85
|
+
const ttydRunning = isTtydRunning();
|
|
86
|
+
const sessions = listSessions();
|
|
87
|
+
if (ttydRunning) {
|
|
88
|
+
ok('ttyd', 'running');
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
warn('ttyd', 'not running — terminal not accessible');
|
|
92
|
+
hasProblems = true;
|
|
93
|
+
}
|
|
94
|
+
if (sessions.length > 0) {
|
|
95
|
+
ok('tmux sessions', sessions.join(', '));
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
warn('tmux sessions', 'no active sessions');
|
|
99
|
+
hasProblems = true;
|
|
100
|
+
}
|
|
101
|
+
// Coherence check: ttyd running without tmux is suspicious
|
|
102
|
+
if (ttydRunning && sessions.length === 0) {
|
|
103
|
+
warn('Coherence', 'ttyd is running but no tmux session found — terminal may be broken');
|
|
104
|
+
hasProblems = true;
|
|
105
|
+
}
|
|
106
|
+
// ── Hooks ────────────────────────────────────────────────────────────────
|
|
107
|
+
console.log();
|
|
108
|
+
console.log(chalk.bold.white('Claude Hooks:'));
|
|
109
|
+
const hooksConfigured = hasDevremoteHooks();
|
|
110
|
+
if (hooksConfigured) {
|
|
111
|
+
ok('devremote hooks', 'configured in ~/.claude/settings.json');
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
warn('devremote hooks', 'not configured — run: rms-devremote link');
|
|
115
|
+
hasProblems = true;
|
|
116
|
+
}
|
|
117
|
+
// ── PID files ───────────────────────────────────────────────────────────
|
|
118
|
+
console.log();
|
|
119
|
+
console.log(chalk.bold.white('Background Processes:'));
|
|
120
|
+
hasProblems = checkPidFile('inhibit.pid (sleep inhibitor)', INHIBIT_PID_PATH, hasProblems);
|
|
121
|
+
hasProblems = checkPidFile('battery.pid (battery watcher)', BATTERY_PID_PATH, hasProblems);
|
|
122
|
+
// ── Dependencies ─────────────────────────────────────────────────────────
|
|
123
|
+
console.log();
|
|
124
|
+
console.log(chalk.bold.white('Dependencies:'));
|
|
125
|
+
if (which('jq')) {
|
|
126
|
+
ok('jq', 'installed');
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
warn('jq', 'not installed — some scripts may fail (install: apt/brew install jq)');
|
|
130
|
+
hasProblems = true;
|
|
131
|
+
}
|
|
132
|
+
if (which('tmux')) {
|
|
133
|
+
ok('tmux', 'installed');
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
fail('tmux', 'not installed — required for sessions');
|
|
137
|
+
hasProblems = true;
|
|
138
|
+
}
|
|
139
|
+
if (which('ttyd')) {
|
|
140
|
+
ok('ttyd', 'installed');
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
fail('ttyd', 'not installed — run: rms-devremote setup');
|
|
144
|
+
hasProblems = true;
|
|
145
|
+
}
|
|
146
|
+
if (which('lsof')) {
|
|
147
|
+
ok('lsof', 'installed');
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
warn('lsof', 'not installed — port detection may fail');
|
|
151
|
+
hasProblems = true;
|
|
152
|
+
}
|
|
153
|
+
// ── Battery ───────────────────────────────────────────────────────────────
|
|
154
|
+
console.log();
|
|
155
|
+
console.log(chalk.bold.white('Battery:'));
|
|
156
|
+
const battery = getBatteryInfo();
|
|
157
|
+
if (!battery.present) {
|
|
158
|
+
console.log(` ${chalk.gray('No battery detected (desktop or AC-only machine)')}`);
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
const pct = battery.percent;
|
|
162
|
+
const pctColor = pct > 50
|
|
163
|
+
? chalk.green(`${pct}%`)
|
|
164
|
+
: pct > 20
|
|
165
|
+
? chalk.yellow(`${pct}%`)
|
|
166
|
+
: chalk.red(`${pct}%`);
|
|
167
|
+
const state = battery.charging
|
|
168
|
+
? chalk.green('charging')
|
|
169
|
+
: chalk.yellow('discharging');
|
|
170
|
+
console.log(` ${pctColor} ${state}`);
|
|
171
|
+
if (!battery.charging && pct <= 20) {
|
|
172
|
+
warn('Battery low', 'connect charger to avoid session drop');
|
|
173
|
+
hasProblems = true;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
// ── Summary ───────────────────────────────────────────────────────────────
|
|
177
|
+
console.log();
|
|
178
|
+
console.log(chalk.cyan('─────────────────────────────────────────'));
|
|
179
|
+
if (hasProblems) {
|
|
180
|
+
console.log(chalk.yellow.bold('⚠ Des problemes ont ete detectes — voir les avertissements ci-dessus.'));
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
console.log(chalk.green.bold('✔ Tout est OK — rms-devremote est pret.'));
|
|
184
|
+
}
|
|
185
|
+
console.log();
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Check a PID file: warn if orphaned (file exists but process is dead).
|
|
189
|
+
* Returns the updated hasProblems flag.
|
|
190
|
+
*/
|
|
191
|
+
function checkPidFile(label, pidPath, hasProblems) {
|
|
192
|
+
if (!existsSync(pidPath)) {
|
|
193
|
+
// Not present is normal (process may not be needed right now)
|
|
194
|
+
return hasProblems;
|
|
195
|
+
}
|
|
196
|
+
const pid = readPid(pidPath);
|
|
197
|
+
if (pid === null) {
|
|
198
|
+
warn(label, `PID file present but unreadable at ${pidPath}`);
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
if (isProcessRunning(pid)) {
|
|
202
|
+
ok(label, `running (PID ${pid})`);
|
|
203
|
+
return hasProblems;
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
warn(label, `orphaned PID file — process ${pid} is no longer running`);
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
export default check;
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { multiselect, confirm, outro, intro, spinner, isCancel } from '@clack/prompts';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { existsSync } from 'fs';
|
|
4
|
+
import { listSessions, killSession } from '../services/tmux.js';
|
|
5
|
+
import { getTtydPid, stopTtyd } from '../services/ttyd.js';
|
|
6
|
+
import { killByPidFile, cleanupPidFile, readPid, isProcessRunning } from '../services/process.js';
|
|
7
|
+
import { removeHooks, hasDevremoteHooks } from '../services/hooks.js';
|
|
8
|
+
import { INHIBIT_PID_PATH, BATTERY_PID_PATH } from '../services/config.js';
|
|
9
|
+
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
10
|
+
function pidFileStatus(path) {
|
|
11
|
+
if (!existsSync(path))
|
|
12
|
+
return 'absent';
|
|
13
|
+
const pid = readPid(path);
|
|
14
|
+
if (pid === null)
|
|
15
|
+
return 'orphaned';
|
|
16
|
+
return isProcessRunning(pid) ? 'running' : 'orphaned';
|
|
17
|
+
}
|
|
18
|
+
// ── Main ───────────────────────────────────────────────────────────────────
|
|
19
|
+
export async function clean() {
|
|
20
|
+
intro(chalk.cyan('rms-devremote — Clean'));
|
|
21
|
+
// ── Scan current state ────────────────────────────────────────────────────
|
|
22
|
+
const sessions = listSessions();
|
|
23
|
+
const ttydPid = getTtydPid();
|
|
24
|
+
const inhibitStatus = pidFileStatus(INHIBIT_PID_PATH);
|
|
25
|
+
const batteryStatus = pidFileStatus(BATTERY_PID_PATH);
|
|
26
|
+
const hooksPresent = hasDevremoteHooks();
|
|
27
|
+
// ── Build option list ─────────────────────────────────────────────────────
|
|
28
|
+
const options = [];
|
|
29
|
+
// tmux sessions
|
|
30
|
+
for (const session of sessions) {
|
|
31
|
+
options.push({
|
|
32
|
+
value: `session:${session}`,
|
|
33
|
+
label: `tmux session "${session}"`,
|
|
34
|
+
hint: 'active — will be killed',
|
|
35
|
+
// Active sessions are selected so the user sees them, but must opt-in consciously
|
|
36
|
+
selected: false,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
// ttyd process
|
|
40
|
+
if (ttydPid !== null) {
|
|
41
|
+
options.push({
|
|
42
|
+
value: 'ttyd',
|
|
43
|
+
label: 'ttyd process',
|
|
44
|
+
hint: `running (PID ${ttydPid})`,
|
|
45
|
+
selected: false,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
// inhibit PID file
|
|
49
|
+
if (inhibitStatus !== 'absent') {
|
|
50
|
+
options.push({
|
|
51
|
+
value: 'inhibit-pid',
|
|
52
|
+
label: 'Sleep inhibitor (inhibit.pid)',
|
|
53
|
+
hint: inhibitStatus === 'orphaned' ? 'orphaned PID file' : 'running — will be stopped',
|
|
54
|
+
selected: inhibitStatus === 'orphaned',
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
// battery watcher PID file
|
|
58
|
+
if (batteryStatus !== 'absent') {
|
|
59
|
+
options.push({
|
|
60
|
+
value: 'battery-pid',
|
|
61
|
+
label: 'Battery watcher (battery.pid)',
|
|
62
|
+
hint: batteryStatus === 'orphaned' ? 'orphaned PID file' : 'running — will be stopped',
|
|
63
|
+
selected: batteryStatus === 'orphaned',
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
// Claude hooks
|
|
67
|
+
if (hooksPresent) {
|
|
68
|
+
options.push({
|
|
69
|
+
value: 'hooks',
|
|
70
|
+
label: 'Claude hooks (~/.claude/settings.json)',
|
|
71
|
+
hint: 'devremote hooks will be removed',
|
|
72
|
+
selected: false,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
// ── Nothing to do ─────────────────────────────────────────────────────────
|
|
76
|
+
if (options.length === 0) {
|
|
77
|
+
outro(chalk.green('Nothing to clean — everything looks tidy.'));
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
// ── Multiselect ───────────────────────────────────────────────────────────
|
|
81
|
+
const selected = await multiselect({
|
|
82
|
+
message: 'Select items to clean (space to toggle, enter to confirm):',
|
|
83
|
+
options: options.map((opt) => ({
|
|
84
|
+
value: opt.value,
|
|
85
|
+
label: opt.label,
|
|
86
|
+
hint: opt.hint,
|
|
87
|
+
})),
|
|
88
|
+
initialValues: options.filter((o) => o.selected).map((o) => o.value),
|
|
89
|
+
required: false,
|
|
90
|
+
});
|
|
91
|
+
if (isCancel(selected)) {
|
|
92
|
+
outro(chalk.gray('Cancelled — nothing was changed.'));
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const choices = selected;
|
|
96
|
+
if (choices.length === 0) {
|
|
97
|
+
outro(chalk.gray('Nothing selected — nothing was changed.'));
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
// ── Confirm ───────────────────────────────────────────────────────────────
|
|
101
|
+
const confirmed = await confirm({
|
|
102
|
+
message: `Clean ${choices.length} item(s)? This cannot be undone.`,
|
|
103
|
+
});
|
|
104
|
+
if (isCancel(confirmed) || !confirmed) {
|
|
105
|
+
outro(chalk.gray('Cancelled — nothing was changed.'));
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
// ── Execute ───────────────────────────────────────────────────────────────
|
|
109
|
+
const s = spinner();
|
|
110
|
+
s.start('Cleaning selected items...');
|
|
111
|
+
const results = [];
|
|
112
|
+
for (const choice of choices) {
|
|
113
|
+
if (choice.startsWith('session:')) {
|
|
114
|
+
const sessionName = choice.slice('session:'.length);
|
|
115
|
+
try {
|
|
116
|
+
killSession(sessionName);
|
|
117
|
+
results.push({ label: `tmux session "${sessionName}"`, success: true, detail: 'killed' });
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
results.push({ label: `tmux session "${sessionName}"`, success: false, detail: 'failed to kill' });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (choice === 'ttyd') {
|
|
124
|
+
try {
|
|
125
|
+
stopTtyd();
|
|
126
|
+
results.push({ label: 'ttyd process', success: true, detail: 'stopped' });
|
|
127
|
+
// If ttyd was cleaned, also remove hooks (they reference a now-dead process)
|
|
128
|
+
if (!choices.includes('hooks') && hooksPresent) {
|
|
129
|
+
removeHooks();
|
|
130
|
+
results.push({ label: 'Claude hooks (auto-removed with ttyd)', success: true, detail: 'removed' });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
results.push({ label: 'ttyd process', success: false, detail: 'failed to stop' });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (choice === 'inhibit-pid') {
|
|
138
|
+
killByPidFile(INHIBIT_PID_PATH);
|
|
139
|
+
cleanupPidFile(INHIBIT_PID_PATH);
|
|
140
|
+
results.push({ label: 'Sleep inhibitor', success: true, detail: 'stopped & PID file removed' });
|
|
141
|
+
}
|
|
142
|
+
if (choice === 'battery-pid') {
|
|
143
|
+
killByPidFile(BATTERY_PID_PATH);
|
|
144
|
+
cleanupPidFile(BATTERY_PID_PATH);
|
|
145
|
+
results.push({ label: 'Battery watcher', success: true, detail: 'stopped & PID file removed' });
|
|
146
|
+
}
|
|
147
|
+
if (choice === 'hooks') {
|
|
148
|
+
try {
|
|
149
|
+
removeHooks();
|
|
150
|
+
results.push({ label: 'Claude hooks', success: true, detail: 'removed from settings.json' });
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
results.push({ label: 'Claude hooks', success: false, detail: 'failed to remove' });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
s.stop('Done.');
|
|
158
|
+
// ── Show results ──────────────────────────────────────────────────────────
|
|
159
|
+
console.log();
|
|
160
|
+
for (const r of results) {
|
|
161
|
+
if (r.success) {
|
|
162
|
+
console.log(` ${chalk.green('✔')} ${r.label} ${chalk.gray('— ' + r.detail)}`);
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
console.log(` ${chalk.red('✖')} ${r.label} ${chalk.gray('— ' + r.detail)}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
const allOk = results.every((r) => r.success);
|
|
169
|
+
console.log();
|
|
170
|
+
if (allOk) {
|
|
171
|
+
outro(chalk.green('Clean complete.'));
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
outro(chalk.yellow('Clean finished with some errors — check the output above.'));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
export default clean;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { isSetupDone, readConfig } from '../services/config.js';
|
|
3
|
+
import { isTtydRunning } from '../services/ttyd.js';
|
|
4
|
+
import { sessionExists } from '../services/tmux.js';
|
|
5
|
+
import { getBatteryInfo } from '../services/battery.js';
|
|
6
|
+
export async function dashboard() {
|
|
7
|
+
if (!isSetupDone()) {
|
|
8
|
+
console.log(chalk.yellow('rms-devremote n\'est pas encore configure. Lance: rms-devremote setup'));
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
const config = readConfig();
|
|
12
|
+
const linked = isTtydRunning();
|
|
13
|
+
const tmuxActive = sessionExists();
|
|
14
|
+
const battery = getBatteryInfo();
|
|
15
|
+
const topLine = chalk.bold.cyan('╔══════════════════════════════════════════════╗');
|
|
16
|
+
const botLine = chalk.bold.cyan('╚══════════════════════════════════════════════╝');
|
|
17
|
+
const mid = (text) => chalk.bold.cyan('║') + ' ' + text + ' ' + chalk.bold.cyan('║');
|
|
18
|
+
const pad = (s, len) => s + ' '.repeat(Math.max(0, len - visibleLength(s)));
|
|
19
|
+
// ANSI escape codes are not counted for padding
|
|
20
|
+
function visibleLength(s) {
|
|
21
|
+
// Remove ANSI escape sequences
|
|
22
|
+
return s.replace(/\x1B\[[0-9;]*m/g, '').length;
|
|
23
|
+
}
|
|
24
|
+
const statusText = linked
|
|
25
|
+
? chalk.green('● linked')
|
|
26
|
+
: chalk.red('● unlinked');
|
|
27
|
+
const tmuxText = tmuxActive
|
|
28
|
+
? chalk.green('● active')
|
|
29
|
+
: chalk.yellow('○ no session');
|
|
30
|
+
const batteryText = battery.present
|
|
31
|
+
? (battery.charging ? chalk.green(`${battery.percent}% ⚡`) : chalk.yellow(`${battery.percent}% 🔋`))
|
|
32
|
+
: chalk.gray('N/A');
|
|
33
|
+
const WIDTH = 44;
|
|
34
|
+
const title = chalk.bold.white(' rms-devremote v2.0');
|
|
35
|
+
const separator = chalk.cyan('──────────────────────────────────────────────');
|
|
36
|
+
console.log();
|
|
37
|
+
console.log(topLine);
|
|
38
|
+
console.log(mid(pad(title, WIDTH)));
|
|
39
|
+
console.log(chalk.bold.cyan('║') + ' ' + separator + chalk.bold.cyan('║'));
|
|
40
|
+
console.log(mid(pad(chalk.white('Status : ') + statusText, WIDTH)));
|
|
41
|
+
console.log(mid(pad(chalk.white('URL : ') + chalk.blue(config.domains.terminal), WIDTH)));
|
|
42
|
+
console.log(mid(pad(chalk.white('Tmux : ') + tmuxText, WIDTH)));
|
|
43
|
+
console.log(mid(pad(chalk.white('Battery: ') + batteryText, WIDTH)));
|
|
44
|
+
console.log(chalk.bold.cyan('║') + ' ' + separator + chalk.bold.cyan('║'));
|
|
45
|
+
console.log(mid(pad(chalk.bold.white('Commands:'), WIDTH)));
|
|
46
|
+
console.log(mid(pad(chalk.gray(' setup ') + chalk.white('– configure the server'), WIDTH)));
|
|
47
|
+
console.log(mid(pad(chalk.gray(' link ') + chalk.white('– start remote session'), WIDTH)));
|
|
48
|
+
console.log(mid(pad(chalk.gray(' unlink ') + chalk.white('– stop remote session'), WIDTH)));
|
|
49
|
+
console.log(mid(pad(chalk.gray(' attach ') + chalk.white('– attach to tmux session'), WIDTH)));
|
|
50
|
+
console.log(mid(pad(chalk.gray(' status ') + chalk.white('– detailed status'), WIDTH)));
|
|
51
|
+
console.log(mid(pad(chalk.gray(' ping ') + chalk.white('– send test notification'), WIDTH)));
|
|
52
|
+
console.log(mid(pad(chalk.gray(' check ') + chalk.white('– check dependencies'), WIDTH)));
|
|
53
|
+
console.log(mid(pad(chalk.gray(' clean ') + chalk.white('– remove all config'), WIDTH)));
|
|
54
|
+
console.log(botLine);
|
|
55
|
+
console.log();
|
|
56
|
+
}
|
|
57
|
+
export default dashboard;
|