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
@@ -0,0 +1,112 @@
1
+ import { fork } from 'child_process';
2
+ import { resolve, dirname } from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import chalk from 'chalk';
5
+ import { isSetupDone, readConfig, BATTERY_PID_PATH } from '../services/config.js';
6
+ import { isServerRunning, checkPortAvailable, startServer } from '../services/ttyd.js';
7
+ import { sessionExists, createSession, attachSession } from '../services/tmux.js';
8
+ import { isDockerRunning, isComposeUp, composeUp } from '../services/docker.js';
9
+ import { mergeHooks } from '../services/hooks.js';
10
+ import { inhibitSleep, getBatteryInfo } from '../services/battery.js';
11
+ import { writePid } from '../services/process.js';
12
+ const __filename = fileURLToPath(import.meta.url);
13
+ const __dirname = dirname(__filename);
14
+ export async function link() {
15
+ // 1. Guard: setup must be done
16
+ if (!isSetupDone()) {
17
+ console.error(chalk.red('✖ rms-devremote n\'est pas encore configure. Lance: rms-devremote setup'));
18
+ process.exit(1);
19
+ }
20
+ const config = readConfig();
21
+ // 2. Guard: already linked
22
+ if (isServerRunning()) {
23
+ console.log(chalk.yellow('⚠ Session déjà linkée.'));
24
+ console.log(chalk.white(' URL: ') + chalk.blue(config.domains.terminal));
25
+ console.log(chalk.gray(' → rms-devremote unlink pour stopper'));
26
+ console.log(chalk.gray(' → rms-devremote attach pour rejoindre'));
27
+ return;
28
+ }
29
+ // 3. Check Docker
30
+ if (!isDockerRunning()) {
31
+ console.error(chalk.red('✖ Docker n\'est pas en cours d\'exécution. Démarre Docker et réessaie.'));
32
+ process.exit(1);
33
+ }
34
+ if (!isComposeUp()) {
35
+ console.log(chalk.cyan(' Démarrage du stack Docker Compose…'));
36
+ try {
37
+ composeUp();
38
+ console.log(chalk.green('✔ Docker Compose démarré.'));
39
+ }
40
+ catch (err) {
41
+ console.error(chalk.red('✖ Échec du démarrage Docker Compose.'), err);
42
+ process.exit(1);
43
+ }
44
+ }
45
+ // 4. Check/create tmux session
46
+ if (!sessionExists()) {
47
+ console.log(chalk.cyan(' Création de la session tmux…'));
48
+ createSession();
49
+ console.log(chalk.green('✔ Session tmux créée.'));
50
+ }
51
+ else {
52
+ console.log(chalk.green('✔ Session tmux existante.'));
53
+ }
54
+ // 5. Check port availability
55
+ const portCheck = checkPortAvailable();
56
+ if (!portCheck.available) {
57
+ console.error(chalk.red(`✖ Le port ${config.ttyd.port} est bloqué par le processus PID ${portCheck.blockingPid ?? 'inconnu'}.`));
58
+ console.error(chalk.gray(` Arrête ce processus ou change le port dans la config.`));
59
+ process.exit(1);
60
+ }
61
+ // 6. Start devremote terminal server
62
+ console.log(chalk.cyan(' Démarrage du serveur terminal…'));
63
+ let serverPid;
64
+ try {
65
+ serverPid = startServer();
66
+ console.log(chalk.green(`✔ Serveur terminal démarré (PID ${serverPid}).`));
67
+ }
68
+ catch (err) {
69
+ console.error(chalk.red('✖ Impossible de démarrer le serveur terminal.'), err);
70
+ process.exit(1);
71
+ }
72
+ // 7. Merge hooks
73
+ try {
74
+ mergeHooks();
75
+ console.log(chalk.green('✔ Hooks Claude configurés.'));
76
+ }
77
+ catch {
78
+ console.log(chalk.yellow('⚠ Impossible de configurer les hooks Claude.'));
79
+ }
80
+ // 8. Inhibit sleep
81
+ try {
82
+ inhibitSleep();
83
+ console.log(chalk.green('✔ Mise en veille désactivée.'));
84
+ }
85
+ catch {
86
+ console.log(chalk.yellow('⚠ Impossible de désactiver la mise en veille.'));
87
+ }
88
+ // 9. Battery warning
89
+ const battery = getBatteryInfo();
90
+ if (battery.present && !battery.charging) {
91
+ console.log(chalk.yellow(`⚠ Batterie non branchée — ${battery.percent}% restant. Pense à brancher le chargeur.`));
92
+ }
93
+ // 10. Fork battery worker if battery present
94
+ if (battery.present) {
95
+ const workerPath = resolve(__dirname, '..', 'services', 'battery-worker.js');
96
+ const worker = fork(workerPath, [], { detached: true, stdio: 'ignore' });
97
+ if (worker.pid !== undefined) {
98
+ writePid(BATTERY_PID_PATH, worker.pid);
99
+ }
100
+ worker.unref();
101
+ console.log(chalk.green('✔ Surveillance batterie démarrée.'));
102
+ }
103
+ // 11. Success
104
+ console.log();
105
+ console.log(chalk.bold.green('✔ Terminal linked!'));
106
+ console.log(chalk.white(' URL: ') + chalk.blue(config.domains.terminal));
107
+ console.log();
108
+ // 12. Attach tmux session (blocks until detach/exit)
109
+ // tmux + server continue in background if terminal is closed
110
+ attachSession();
111
+ }
112
+ export default link;
@@ -0,0 +1,2 @@
1
+ export declare function ping(): Promise<void>;
2
+ export default ping;
@@ -0,0 +1,21 @@
1
+ import chalk from 'chalk';
2
+ import { isSetupDone } from '../services/config.js';
3
+ import { sendNotification } from '../services/ntfy.js';
4
+ export async function ping() {
5
+ if (!isSetupDone()) {
6
+ console.log(chalk.yellow('⚠ rms-devremote n\'est pas encore configuré. Lance: rms-devremote setup'));
7
+ return;
8
+ }
9
+ console.log(chalk.cyan(' Envoi d\'une notification de test…'));
10
+ const ok = await sendNotification('rms-devremote ping — connexion OK ✓', 'default');
11
+ if (ok) {
12
+ console.log(chalk.green('✔ Notification envoyée avec succès.'));
13
+ console.log(chalk.gray(' Vérifie ton appareil pour la notification ntfy.'));
14
+ }
15
+ else {
16
+ console.log(chalk.red('✖ Échec de l\'envoi de la notification.'));
17
+ console.log(chalk.gray(' Assure-toi que le conteneur ntfy est en cours d\'exécution.'));
18
+ console.log(chalk.gray(' Lance: rms-devremote status'));
19
+ }
20
+ }
21
+ export default ping;
@@ -0,0 +1,2 @@
1
+ export declare function setup(): Promise<void>;
2
+ export default setup;
@@ -0,0 +1,54 @@
1
+ import chalk from 'chalk';
2
+ import { isSetupDone } from '../services/config.js';
3
+ import { startSetupServer, getSetupUrl } from '../setup-server/server.js';
4
+ import { run } from '../services/shell.js';
5
+ export async function setup() {
6
+ // Guard: already configured
7
+ if (isSetupDone()) {
8
+ console.log(chalk.yellow('Deja configure. Pour reconfigurer, supprime ~/.rms-devremote/ et relance.'));
9
+ return;
10
+ }
11
+ // Start the setup HTTP + WebSocket server
12
+ const { close } = await startSetupServer();
13
+ const url = getSetupUrl();
14
+ // Open browser (ignore errors — may not have a display)
15
+ try {
16
+ const platform = process.platform;
17
+ if (platform === 'darwin') {
18
+ run('open', [url]);
19
+ }
20
+ else {
21
+ run('xdg-open', [url]);
22
+ }
23
+ }
24
+ catch {
25
+ // Silently ignore — user can open manually
26
+ }
27
+ console.log();
28
+ console.log(chalk.bold.cyan('Setup en cours sur ' + url + '...'));
29
+ console.log(chalk.gray('Ouvrez l\'URL ci-dessus dans votre navigateur si elle ne s\'est pas ouverte automatiquement.'));
30
+ console.log(chalk.gray('Ctrl+C pour annuler'));
31
+ console.log();
32
+ // Poll until setup is done (config.json + .env written)
33
+ await new Promise((resolve) => {
34
+ const interval = setInterval(() => {
35
+ if (isSetupDone()) {
36
+ clearInterval(interval);
37
+ resolve();
38
+ }
39
+ }, 1000);
40
+ // Allow Ctrl+C to cancel cleanly
41
+ process.once('SIGINT', () => {
42
+ clearInterval(interval);
43
+ close();
44
+ console.log(chalk.gray('\nSetup annule.'));
45
+ process.exit(0);
46
+ });
47
+ });
48
+ // Teardown server
49
+ close();
50
+ console.log();
51
+ console.log(chalk.bold.green('Setup termine ! Lance: rms-devremote link'));
52
+ console.log();
53
+ }
54
+ export default setup;
@@ -0,0 +1,2 @@
1
+ export declare function status(): Promise<void>;
2
+ export default status;
@@ -0,0 +1,65 @@
1
+ import chalk from 'chalk';
2
+ import { isSetupDone, readConfig } from '../services/config.js';
3
+ import { getContainerStatus } from '../services/docker.js';
4
+ import { healthCheck } from '../services/ntfy.js';
5
+ import { isTtydRunning } from '../services/ttyd.js';
6
+ import { sessionExists } from '../services/tmux.js';
7
+ import { hasDevremoteHooks } from '../services/hooks.js';
8
+ import { getBatteryInfo } from '../services/battery.js';
9
+ function statusIcon(ok) {
10
+ return ok ? chalk.green('●') : chalk.red('●');
11
+ }
12
+ function statusLabel(ok, onLabel, offLabel) {
13
+ return ok ? chalk.green(onLabel) : chalk.red(offLabel);
14
+ }
15
+ export async function status() {
16
+ if (!isSetupDone()) {
17
+ console.log(chalk.yellow('⚠ rms-devremote n\'est pas encore configuré. Lance: rms-devremote setup'));
18
+ return;
19
+ }
20
+ const config = readConfig();
21
+ // Gather status info
22
+ const tunnelStatus = getContainerStatus('devremote-tunnel');
23
+ const ntfyStatus = getContainerStatus('devremote-ntfy');
24
+ const ntfyHealth = await healthCheck();
25
+ const ttydRunning = isTtydRunning();
26
+ const tmuxSession = sessionExists();
27
+ const hooksConfigured = hasDevremoteHooks();
28
+ const battery = getBatteryInfo();
29
+ console.log();
30
+ console.log(chalk.bold.cyan('rms-devremote — Status'));
31
+ console.log(chalk.cyan('─────────────────────────────────────────'));
32
+ // Docker services
33
+ console.log(chalk.bold.white('Docker:'));
34
+ const tunnelOk = tunnelStatus === 'running';
35
+ const ntfyOk = ntfyStatus === 'running';
36
+ console.log(` ${statusIcon(tunnelOk)} Tunnel (Cloudflare) ${statusLabel(tunnelOk, 'running', tunnelStatus)}`);
37
+ console.log(` ${statusIcon(ntfyOk)} ntfy ${statusLabel(ntfyOk, 'running', ntfyStatus)}`);
38
+ console.log(` ${statusIcon(ntfyHealth)} ntfy health ${statusLabel(ntfyHealth, 'healthy', 'unreachable')}`);
39
+ console.log();
40
+ console.log(chalk.bold.white('Services:'));
41
+ console.log(` ${statusIcon(ttydRunning)} terminal server ${statusLabel(ttydRunning, 'running', 'stopped')}`);
42
+ console.log(` ${statusIcon(tmuxSession)} tmux session ${statusLabel(tmuxSession, 'active', 'no session')}`);
43
+ console.log(` ${statusIcon(hooksConfigured)} Claude hooks ${statusLabel(hooksConfigured, 'configured', 'not configured')}`);
44
+ console.log();
45
+ console.log(chalk.bold.white('Battery:'));
46
+ if (battery.present) {
47
+ const chargingIcon = battery.charging ? chalk.green('⚡ charging') : chalk.yellow('🔋 discharging');
48
+ const percentColor = battery.percent > 50
49
+ ? chalk.green(`${battery.percent}%`)
50
+ : battery.percent > 20
51
+ ? chalk.yellow(`${battery.percent}%`)
52
+ : chalk.red(`${battery.percent}%`);
53
+ console.log(` ${percentColor} ${chargingIcon}`);
54
+ }
55
+ else {
56
+ console.log(` ${chalk.gray('No battery detected (desktop or AC only)')}`);
57
+ }
58
+ console.log();
59
+ console.log(chalk.bold.white('Domains:'));
60
+ console.log(` Terminal : ${chalk.blue(config.domains.terminal)}`);
61
+ console.log(` Notify : ${chalk.blue(config.domains.notify)}`);
62
+ console.log(chalk.cyan('─────────────────────────────────────────'));
63
+ console.log();
64
+ }
65
+ export default status;
@@ -0,0 +1,2 @@
1
+ export declare function unlink(): Promise<void>;
2
+ export default unlink;
@@ -0,0 +1,53 @@
1
+ import chalk from 'chalk';
2
+ import { BATTERY_PID_PATH, INHIBIT_PID_PATH } from '../services/config.js';
3
+ import { isTtydRunning, stopTtyd } from '../services/ttyd.js';
4
+ import { sessionExists, attachSession } from '../services/tmux.js';
5
+ import { removeHooks } from '../services/hooks.js';
6
+ import { releaseSleep } from '../services/battery.js';
7
+ import { killByPidFile, cleanupPidFile } from '../services/process.js';
8
+ export async function unlink() {
9
+ // 1. Guard: must be linked
10
+ if (!isTtydRunning()) {
11
+ console.log(chalk.yellow('⚠ Pas de session linkée en cours.'));
12
+ return;
13
+ }
14
+ // 2. Stop ttyd
15
+ console.log(chalk.cyan(' Arrêt de ttyd…'));
16
+ try {
17
+ stopTtyd();
18
+ console.log(chalk.green('✔ ttyd arrêté.'));
19
+ }
20
+ catch {
21
+ console.log(chalk.yellow('⚠ Impossible d\'arrêter ttyd proprement.'));
22
+ }
23
+ // 3. Remove hooks
24
+ try {
25
+ removeHooks();
26
+ console.log(chalk.green('✔ Hooks Claude supprimés.'));
27
+ }
28
+ catch {
29
+ console.log(chalk.yellow('⚠ Impossible de supprimer les hooks Claude.'));
30
+ }
31
+ // 4. Release sleep inhibitor
32
+ try {
33
+ releaseSleep();
34
+ console.log(chalk.green('✔ Mise en veille réactivée.'));
35
+ }
36
+ catch {
37
+ console.log(chalk.yellow('⚠ Impossible de réactiver la mise en veille.'));
38
+ }
39
+ // 5. Kill battery watcher
40
+ killByPidFile(BATTERY_PID_PATH);
41
+ // 6. Clean PID files
42
+ cleanupPidFile(BATTERY_PID_PATH);
43
+ cleanupPidFile(INHIBIT_PID_PATH);
44
+ console.log();
45
+ console.log(chalk.bold.green('✔ Terminal unlinked.'));
46
+ console.log();
47
+ // 7. Attach tmux session if it exists
48
+ if (sessionExists()) {
49
+ console.log(chalk.gray(' La session tmux est toujours active. Connexion…'));
50
+ attachSession();
51
+ }
52
+ }
53
+ export default unlink;
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ const program = new Command();
4
+ program
5
+ .name('rms-devremote')
6
+ .description('Share your local terminal remotely with push notifications')
7
+ .version('2.0.0');
8
+ // Helper: lazy-load a command module by path (relative to dist/)
9
+ async function runCommand(modulePath) {
10
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
11
+ const mod = await import(modulePath);
12
+ await mod.default();
13
+ }
14
+ program
15
+ .command('setup')
16
+ .description('Configure the remote server (install ttyd, nginx, ntfy)')
17
+ .action(() => runCommand('./commands/setup.js'));
18
+ program
19
+ .command('link')
20
+ .description('Link this machine to a remote server')
21
+ .action(() => runCommand('./commands/link.js'));
22
+ program
23
+ .command('unlink')
24
+ .description('Unlink this machine from the remote server')
25
+ .action(() => runCommand('./commands/unlink.js'));
26
+ program
27
+ .command('attach')
28
+ .description('Attach to a remote terminal session')
29
+ .action(() => runCommand('./commands/attach.js'));
30
+ program
31
+ .command('status')
32
+ .description('Show the status of the remote connection')
33
+ .action(() => runCommand('./commands/status.js'));
34
+ program
35
+ .command('ping')
36
+ .description('Ping the remote server to check connectivity')
37
+ .action(() => runCommand('./commands/ping.js'));
38
+ program
39
+ .command('check')
40
+ .description('Check system requirements and dependencies')
41
+ .action(() => runCommand('./commands/check.js'));
42
+ program
43
+ .command('clean')
44
+ .description('Remove all rms-devremote configuration and data')
45
+ .action(() => runCommand('./commands/clean.js'));
46
+ // Default action: show dashboard when no command is provided
47
+ if (process.argv.length <= 2) {
48
+ runCommand('./commands/dashboard.js').catch((err) => {
49
+ console.error(err);
50
+ process.exit(1);
51
+ });
52
+ }
53
+ else {
54
+ program.parse(process.argv);
55
+ }
@@ -0,0 +1,6 @@
1
+ import type { Request, Response, NextFunction } from 'express';
2
+ /**
3
+ * Express middleware: HTTP Basic Authentication.
4
+ * Credentials come from ~/.rms-devremote/.env (TTYD_USER / TTYD_PASSWORD).
5
+ */
6
+ export declare function basicAuth(req: Request, res: Response, next: NextFunction): void;
@@ -0,0 +1,32 @@
1
+ import { readEnv } from '../services/config.js';
2
+ /**
3
+ * Express middleware: HTTP Basic Authentication.
4
+ * Credentials come from ~/.rms-devremote/.env (TTYD_USER / TTYD_PASSWORD).
5
+ */
6
+ export function basicAuth(req, res, next) {
7
+ const authHeader = req.headers.authorization;
8
+ if (!authHeader || !authHeader.startsWith('Basic ')) {
9
+ res.setHeader('WWW-Authenticate', 'Basic realm="devremote"');
10
+ res.status(401).end('Authentication required');
11
+ return;
12
+ }
13
+ const encoded = authHeader.slice(6);
14
+ const decoded = Buffer.from(encoded, 'base64').toString('utf8');
15
+ const colonIdx = decoded.indexOf(':');
16
+ if (colonIdx === -1) {
17
+ res.status(401).end('Invalid credentials');
18
+ return;
19
+ }
20
+ const user = decoded.slice(0, colonIdx);
21
+ const pass = decoded.slice(colonIdx + 1);
22
+ const env = readEnv();
23
+ const expectedUser = env.TTYD_USER ?? 'admin';
24
+ const expectedPass = env.TTYD_PASSWORD ?? 'changeme';
25
+ if (user === expectedUser && pass === expectedPass) {
26
+ next();
27
+ }
28
+ else {
29
+ res.setHeader('WWW-Authenticate', 'Basic realm="devremote"');
30
+ res.status(401).end('Invalid credentials');
31
+ }
32
+ }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Generates the complete frontend HTML for the devremote terminal PWA.
3
+ */
4
+ export declare function buildFrontendHTML(): string;