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.
Files changed (68) hide show
  1. package/README.md +154 -0
  2. package/dist/commands/attach.d.ts +2 -0
  3. package/dist/commands/attach.js +10 -0
  4. package/dist/commands/check.d.ts +2 -0
  5. package/dist/commands/check.js +210 -0
  6. package/dist/commands/clean.d.ts +2 -0
  7. package/dist/commands/clean.js +177 -0
  8. package/dist/commands/dashboard.d.ts +2 -0
  9. package/dist/commands/dashboard.js +57 -0
  10. package/dist/commands/link.d.ts +2 -0
  11. package/dist/commands/link.js +112 -0
  12. package/dist/commands/ping.d.ts +2 -0
  13. package/dist/commands/ping.js +21 -0
  14. package/dist/commands/setup.d.ts +2 -0
  15. package/dist/commands/setup.js +54 -0
  16. package/dist/commands/status.d.ts +2 -0
  17. package/dist/commands/status.js +65 -0
  18. package/dist/commands/unlink.d.ts +2 -0
  19. package/dist/commands/unlink.js +53 -0
  20. package/dist/index.d.ts +2 -0
  21. package/dist/index.js +55 -0
  22. package/dist/server/auth.d.ts +6 -0
  23. package/dist/server/auth.js +32 -0
  24. package/dist/server/frontend.d.ts +4 -0
  25. package/dist/server/frontend.js +886 -0
  26. package/dist/server/index.d.ts +1 -0
  27. package/dist/server/index.js +283 -0
  28. package/dist/server/terminal.d.ts +14 -0
  29. package/dist/server/terminal.js +43 -0
  30. package/dist/services/battery-worker.d.ts +1 -0
  31. package/dist/services/battery-worker.js +2 -0
  32. package/dist/services/battery.d.ts +27 -0
  33. package/dist/services/battery.js +152 -0
  34. package/dist/services/config.d.ts +63 -0
  35. package/dist/services/config.js +84 -0
  36. package/dist/services/docker.d.ts +25 -0
  37. package/dist/services/docker.js +75 -0
  38. package/dist/services/hooks.d.ts +15 -0
  39. package/dist/services/hooks.js +111 -0
  40. package/dist/services/ntfy.d.ts +19 -0
  41. package/dist/services/ntfy.js +63 -0
  42. package/dist/services/process.d.ts +30 -0
  43. package/dist/services/process.js +90 -0
  44. package/dist/services/proxy-worker.d.ts +1 -0
  45. package/dist/services/proxy-worker.js +12 -0
  46. package/dist/services/proxy.d.ts +4 -0
  47. package/dist/services/proxy.js +195 -0
  48. package/dist/services/shell.d.ts +22 -0
  49. package/dist/services/shell.js +47 -0
  50. package/dist/services/tmux.d.ts +30 -0
  51. package/dist/services/tmux.js +74 -0
  52. package/dist/services/ttyd.d.ts +28 -0
  53. package/dist/services/ttyd.js +71 -0
  54. package/dist/setup-server/routes.d.ts +4 -0
  55. package/dist/setup-server/routes.js +177 -0
  56. package/dist/setup-server/server.d.ts +4 -0
  57. package/dist/setup-server/server.js +32 -0
  58. package/docker/docker-compose.yml +24 -0
  59. package/docker/ntfy/server.yml +6 -0
  60. package/package.json +61 -0
  61. package/scripts/claude-remote.sh +583 -0
  62. package/scripts/hooks/notify.sh +68 -0
  63. package/scripts/notify.sh +54 -0
  64. package/scripts/startup.sh +29 -0
  65. package/scripts/update-check.sh +25 -0
  66. package/src/setup-server/public/index.html +21 -0
  67. package/src/setup-server/public/setup.css +475 -0
  68. 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,2 @@
1
+ export declare function attach(): Promise<void>;
2
+ export default attach;
@@ -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,2 @@
1
+ export declare function check(): Promise<void>;
2
+ export default check;
@@ -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,2 @@
1
+ export declare function clean(): Promise<void>;
2
+ export default clean;
@@ -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,2 @@
1
+ export declare function dashboard(): Promise<void>;
2
+ export default dashboard;
@@ -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;
@@ -0,0 +1,2 @@
1
+ export declare function link(): Promise<void>;
2
+ export default link;