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
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { execFileSync, execFile } from 'child_process';
|
|
2
|
+
import { promisify } from 'util';
|
|
3
|
+
const execFileAsync = promisify(execFile);
|
|
4
|
+
/**
|
|
5
|
+
* Synchronously execute a command with arguments.
|
|
6
|
+
* Uses execFileSync to prevent shell injection.
|
|
7
|
+
* Returns trimmed stdout as a string.
|
|
8
|
+
*/
|
|
9
|
+
export function run(cmd, args) {
|
|
10
|
+
const result = execFileSync(cmd, args, { encoding: 'utf8' });
|
|
11
|
+
return result.trim();
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Asynchronously execute a command with arguments.
|
|
15
|
+
* Uses execFile (promisified) to prevent shell injection.
|
|
16
|
+
* Returns trimmed stdout as a string.
|
|
17
|
+
*/
|
|
18
|
+
export async function runAsync(cmd, args) {
|
|
19
|
+
const { stdout } = await execFileAsync(cmd, args, { encoding: 'utf8' });
|
|
20
|
+
return stdout.trim();
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Check whether a command exists in PATH.
|
|
24
|
+
* Returns true if found, false otherwise.
|
|
25
|
+
*/
|
|
26
|
+
export function which(cmd) {
|
|
27
|
+
try {
|
|
28
|
+
run('which', [cmd]);
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Check whether a given port is currently in use.
|
|
37
|
+
* Uses lsof to inspect open files/sockets.
|
|
38
|
+
*/
|
|
39
|
+
export function isPortInUse(port) {
|
|
40
|
+
try {
|
|
41
|
+
run('lsof', ['-iTCP:' + port, '-sTCP:LISTEN', '-t']);
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check whether tmux is installed in PATH.
|
|
3
|
+
*/
|
|
4
|
+
export declare function isTmuxInstalled(): boolean;
|
|
5
|
+
/**
|
|
6
|
+
* Check whether the devremote tmux session exists.
|
|
7
|
+
*/
|
|
8
|
+
export declare function sessionExists(): boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Create the devremote tmux session in detached mode.
|
|
11
|
+
*/
|
|
12
|
+
export declare function createSession(): void;
|
|
13
|
+
/**
|
|
14
|
+
* Attach to the devremote tmux session.
|
|
15
|
+
* Spawns tmux with stdio: 'inherit' and waits for the child to exit.
|
|
16
|
+
*/
|
|
17
|
+
export declare function attachSession(): void;
|
|
18
|
+
/**
|
|
19
|
+
* Kill a tmux session by name (defaults to devremote).
|
|
20
|
+
*/
|
|
21
|
+
export declare function killSession(name?: string): void;
|
|
22
|
+
/**
|
|
23
|
+
* List all tmux sessions that include the devremote session name.
|
|
24
|
+
* Returns an array of matching session name strings.
|
|
25
|
+
*/
|
|
26
|
+
export declare function listSessions(): string[];
|
|
27
|
+
/**
|
|
28
|
+
* Return the configured session name.
|
|
29
|
+
*/
|
|
30
|
+
export declare function getSessionName(): string;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import { run } from './shell.js';
|
|
3
|
+
import { which } from './shell.js';
|
|
4
|
+
const SESSION_NAME = 'devremote';
|
|
5
|
+
/**
|
|
6
|
+
* Check whether tmux is installed in PATH.
|
|
7
|
+
*/
|
|
8
|
+
export function isTmuxInstalled() {
|
|
9
|
+
return which('tmux');
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Check whether the devremote tmux session exists.
|
|
13
|
+
*/
|
|
14
|
+
export function sessionExists() {
|
|
15
|
+
try {
|
|
16
|
+
run('tmux', ['has-session', '-t', SESSION_NAME]);
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Create the devremote tmux session in detached mode.
|
|
25
|
+
*/
|
|
26
|
+
export function createSession() {
|
|
27
|
+
run('tmux', ['new-session', '-d', '-s', SESSION_NAME]);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Attach to the devremote tmux session.
|
|
31
|
+
* Spawns tmux with stdio: 'inherit' and waits for the child to exit.
|
|
32
|
+
*/
|
|
33
|
+
export function attachSession() {
|
|
34
|
+
const child = spawn('tmux', ['attach-session', '-t', SESSION_NAME], {
|
|
35
|
+
stdio: 'inherit',
|
|
36
|
+
});
|
|
37
|
+
child.on('exit', (code) => {
|
|
38
|
+
process.exit(code ?? 0);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Kill a tmux session by name (defaults to devremote).
|
|
43
|
+
*/
|
|
44
|
+
export function killSession(name) {
|
|
45
|
+
const target = name ?? SESSION_NAME;
|
|
46
|
+
try {
|
|
47
|
+
run('tmux', ['kill-session', '-t', target]);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
// Session may not exist — silently ignore
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* List all tmux sessions that include the devremote session name.
|
|
55
|
+
* Returns an array of matching session name strings.
|
|
56
|
+
*/
|
|
57
|
+
export function listSessions() {
|
|
58
|
+
try {
|
|
59
|
+
const output = run('tmux', ['list-sessions', '-F', '#{session_name}']);
|
|
60
|
+
return output
|
|
61
|
+
.split('\n')
|
|
62
|
+
.map((s) => s.trim())
|
|
63
|
+
.filter((s) => s.includes(SESSION_NAME));
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Return the configured session name.
|
|
71
|
+
*/
|
|
72
|
+
export function getSessionName() {
|
|
73
|
+
return SESSION_NAME;
|
|
74
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check whether the devremote server is currently running.
|
|
3
|
+
*/
|
|
4
|
+
export declare function isServerRunning(): boolean;
|
|
5
|
+
/**
|
|
6
|
+
* Return the PID of the server process, or null.
|
|
7
|
+
*/
|
|
8
|
+
export declare function getServerPid(): number | null;
|
|
9
|
+
/**
|
|
10
|
+
* Check whether the server port is available.
|
|
11
|
+
*/
|
|
12
|
+
export declare function checkPortAvailable(): {
|
|
13
|
+
available: boolean;
|
|
14
|
+
blockingPid?: number;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Start the devremote terminal server as a detached process.
|
|
18
|
+
* Returns the PID.
|
|
19
|
+
*/
|
|
20
|
+
export declare function startServer(): number;
|
|
21
|
+
/**
|
|
22
|
+
* Stop the devremote server.
|
|
23
|
+
*/
|
|
24
|
+
export declare function stopServer(): void;
|
|
25
|
+
export declare const isTtydRunning: typeof isServerRunning;
|
|
26
|
+
export declare const startTtyd: typeof startServer;
|
|
27
|
+
export declare const stopTtyd: typeof stopServer;
|
|
28
|
+
export declare const getTtydPid: typeof getServerPid;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import { resolve, dirname } from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import { isPortInUse } from './shell.js';
|
|
5
|
+
import { findProcessByPort } from './process.js';
|
|
6
|
+
import { readConfig } from './config.js';
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = dirname(__filename);
|
|
9
|
+
/**
|
|
10
|
+
* Check whether the devremote server is currently running.
|
|
11
|
+
*/
|
|
12
|
+
export function isServerRunning() {
|
|
13
|
+
const config = readConfig();
|
|
14
|
+
return isPortInUse(config.ttyd.port);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Return the PID of the server process, or null.
|
|
18
|
+
*/
|
|
19
|
+
export function getServerPid() {
|
|
20
|
+
const config = readConfig();
|
|
21
|
+
return findProcessByPort(config.ttyd.port);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Check whether the server port is available.
|
|
25
|
+
*/
|
|
26
|
+
export function checkPortAvailable() {
|
|
27
|
+
const config = readConfig();
|
|
28
|
+
const pid = findProcessByPort(config.ttyd.port);
|
|
29
|
+
if (pid === null) {
|
|
30
|
+
return { available: true };
|
|
31
|
+
}
|
|
32
|
+
return { available: false, blockingPid: pid };
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Start the devremote terminal server as a detached process.
|
|
36
|
+
* Returns the PID.
|
|
37
|
+
*/
|
|
38
|
+
export function startServer() {
|
|
39
|
+
const serverEntry = resolve(__dirname, '..', 'server', 'index.js');
|
|
40
|
+
const child = spawn('node', [serverEntry], {
|
|
41
|
+
detached: true,
|
|
42
|
+
stdio: 'ignore',
|
|
43
|
+
env: { ...process.env },
|
|
44
|
+
});
|
|
45
|
+
child.unref();
|
|
46
|
+
const pid = child.pid;
|
|
47
|
+
if (pid === undefined) {
|
|
48
|
+
throw new Error('Failed to start devremote server: no PID returned');
|
|
49
|
+
}
|
|
50
|
+
return pid;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Stop the devremote server.
|
|
54
|
+
*/
|
|
55
|
+
export function stopServer() {
|
|
56
|
+
const config = readConfig();
|
|
57
|
+
const pid = findProcessByPort(config.ttyd.port);
|
|
58
|
+
if (pid !== null) {
|
|
59
|
+
try {
|
|
60
|
+
process.kill(pid, 'SIGTERM');
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// Already dead
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// Backwards-compatible aliases
|
|
68
|
+
export const isTtydRunning = isServerRunning;
|
|
69
|
+
export const startTtyd = startServer;
|
|
70
|
+
export const stopTtyd = stopServer;
|
|
71
|
+
export const getTtydPid = getServerPid;
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { randomBytes } from 'crypto';
|
|
2
|
+
import { resolve, dirname } from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import { copyFileSync, mkdirSync, writeFileSync, existsSync } from 'fs';
|
|
5
|
+
import { hashSync } from 'bcryptjs';
|
|
6
|
+
import { ensureDataDir, writeConfig, writeEnv, DOCKER_COMPOSE_PATH, NTFY_CONFIG_PATH, NOTIFY_SCRIPT_PATH, HOOKS_PATH, PIN_HASH_PATH, } from '../services/config.js';
|
|
7
|
+
import { composeUp } from '../services/docker.js';
|
|
8
|
+
import { createAdminUser, sendNotification } from '../services/ntfy.js';
|
|
9
|
+
import { which } from '../services/shell.js';
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = dirname(__filename);
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Broadcast helper
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
export function broadcast(wss, type, data = {}) {
|
|
16
|
+
const message = JSON.stringify({ type, data });
|
|
17
|
+
wss.clients.forEach((client) => {
|
|
18
|
+
if (client.readyState === 1 /* OPEN */) {
|
|
19
|
+
client.send(message);
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Route setup
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
export function setupRoutes(app, wss) {
|
|
27
|
+
// GET /api/prereqs — check system dependencies
|
|
28
|
+
app.get('/api/prereqs', (_req, res) => {
|
|
29
|
+
res.json({
|
|
30
|
+
docker: which('docker'),
|
|
31
|
+
tmux: which('tmux'),
|
|
32
|
+
ttyd: which('ttyd'),
|
|
33
|
+
claude: which('claude'),
|
|
34
|
+
jq: which('jq'),
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
// POST /api/setup — run setup steps
|
|
38
|
+
app.post('/api/setup', async (req, res) => {
|
|
39
|
+
try {
|
|
40
|
+
const { terminalDomain, notifyDomain, tunnelToken, ttydPassword, email, pin, } = req.body;
|
|
41
|
+
// Step 1: Preparation
|
|
42
|
+
broadcast(wss, 'status', { message: 'Preparation des fichiers...' });
|
|
43
|
+
// Step 2: Ensure data directory
|
|
44
|
+
ensureDataDir();
|
|
45
|
+
// Step 3: Generate ntfy credentials
|
|
46
|
+
const ntfyTopic = `devremote-${randomBytes(6).toString('hex')}`;
|
|
47
|
+
const ntfyPassword = randomBytes(8).toString('hex');
|
|
48
|
+
// Step 4: Write config.json
|
|
49
|
+
writeConfig({
|
|
50
|
+
domains: {
|
|
51
|
+
terminal: terminalDomain,
|
|
52
|
+
notify: notifyDomain,
|
|
53
|
+
},
|
|
54
|
+
ttyd: {
|
|
55
|
+
port: 7681,
|
|
56
|
+
},
|
|
57
|
+
ntfy: {
|
|
58
|
+
port: 2586,
|
|
59
|
+
},
|
|
60
|
+
battery: {
|
|
61
|
+
lowThreshold: 20,
|
|
62
|
+
checkInterval: 60,
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
// Step 5: Write .env
|
|
66
|
+
writeEnv({
|
|
67
|
+
CLOUDFLARE_TUNNEL_TOKEN: tunnelToken,
|
|
68
|
+
TERMINAL_DOMAIN: terminalDomain,
|
|
69
|
+
NOTIFY_DOMAIN: notifyDomain,
|
|
70
|
+
TTYD_USER: email,
|
|
71
|
+
TTYD_PASSWORD: ttydPassword,
|
|
72
|
+
NTFY_TOPIC: ntfyTopic,
|
|
73
|
+
NTFY_ADMIN_PASSWORD: ntfyPassword,
|
|
74
|
+
NTFY_PORT: '2586',
|
|
75
|
+
});
|
|
76
|
+
// Step 6: Copy docker-compose.yml
|
|
77
|
+
const repoDockerDir = resolve(__dirname, '..', '..', 'docker');
|
|
78
|
+
const srcCompose = resolve(repoDockerDir, 'docker-compose.yml');
|
|
79
|
+
copyFileSync(srcCompose, DOCKER_COMPOSE_PATH);
|
|
80
|
+
// Step 7: Write ntfy/server.yml with actual domain
|
|
81
|
+
const ntfyDir = resolve(NTFY_CONFIG_PATH, '..');
|
|
82
|
+
if (!existsSync(ntfyDir)) {
|
|
83
|
+
mkdirSync(ntfyDir, { recursive: true });
|
|
84
|
+
}
|
|
85
|
+
const ntfyServerYml = [
|
|
86
|
+
`# ntfy server configuration`,
|
|
87
|
+
`base-url: https://${notifyDomain}`,
|
|
88
|
+
`listen-http: ":80"`,
|
|
89
|
+
`auth-default-access: "deny-all"`,
|
|
90
|
+
`behind-proxy: true`,
|
|
91
|
+
`cache-file: "/var/cache/ntfy/cache.db"`,
|
|
92
|
+
`auth-file: "/var/lib/ntfy/user.db"`,
|
|
93
|
+
`enable-web: false`,
|
|
94
|
+
].join('\n') + '\n';
|
|
95
|
+
writeFileSync(NTFY_CONFIG_PATH, ntfyServerYml, 'utf8');
|
|
96
|
+
// Step 8: Copy notify.sh and chmod 755
|
|
97
|
+
const repoScriptsDir = resolve(__dirname, '..', '..', 'scripts');
|
|
98
|
+
const srcNotify = resolve(repoScriptsDir, 'notify.sh');
|
|
99
|
+
copyFileSync(srcNotify, NOTIFY_SCRIPT_PATH);
|
|
100
|
+
// chmod 755
|
|
101
|
+
const { chmodSync } = await import('fs');
|
|
102
|
+
chmodSync(NOTIFY_SCRIPT_PATH, 0o755);
|
|
103
|
+
// Step 9: Write hooks.json template
|
|
104
|
+
const hooksTemplate = {
|
|
105
|
+
hooks: {
|
|
106
|
+
PreToolUse: [
|
|
107
|
+
{
|
|
108
|
+
matcher: '',
|
|
109
|
+
hooks: [
|
|
110
|
+
{
|
|
111
|
+
type: 'command',
|
|
112
|
+
command: `${NOTIFY_SCRIPT_PATH} tool-use`,
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
},
|
|
116
|
+
],
|
|
117
|
+
Notification: [
|
|
118
|
+
{
|
|
119
|
+
matcher: '',
|
|
120
|
+
hooks: [
|
|
121
|
+
{
|
|
122
|
+
type: 'command',
|
|
123
|
+
command: `${NOTIFY_SCRIPT_PATH} notification`,
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
Stop: [
|
|
129
|
+
{
|
|
130
|
+
matcher: '',
|
|
131
|
+
hooks: [
|
|
132
|
+
{
|
|
133
|
+
type: 'command',
|
|
134
|
+
command: `${NOTIFY_SCRIPT_PATH} stop`,
|
|
135
|
+
},
|
|
136
|
+
],
|
|
137
|
+
},
|
|
138
|
+
],
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
writeFileSync(HOOKS_PATH, JSON.stringify(hooksTemplate, null, 2), 'utf8');
|
|
142
|
+
// Step 10: Hash PIN and save
|
|
143
|
+
const pinHash = hashSync(pin, 10);
|
|
144
|
+
writeFileSync(PIN_HASH_PATH, pinHash, 'utf8');
|
|
145
|
+
// Step 11: Start Docker
|
|
146
|
+
broadcast(wss, 'status', { message: 'Demarrage Docker...' });
|
|
147
|
+
composeUp();
|
|
148
|
+
// Step 12: Wait 5 seconds
|
|
149
|
+
await new Promise((r) => setTimeout(r, 5000));
|
|
150
|
+
// Step 13: Create ntfy admin user
|
|
151
|
+
broadcast(wss, 'status', { message: 'Creation utilisateur ntfy...' });
|
|
152
|
+
createAdminUser(ntfyPassword);
|
|
153
|
+
// Step 14: Done
|
|
154
|
+
broadcast(wss, 'complete', {});
|
|
155
|
+
res.json({ ok: true, ntfyTopic, ntfyPassword, notifyDomain });
|
|
156
|
+
}
|
|
157
|
+
catch (err) {
|
|
158
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
159
|
+
broadcast(wss, 'error', { message });
|
|
160
|
+
res.status(500).json({ ok: false, error: message });
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
// POST /api/test-notification — send a test notification
|
|
164
|
+
app.post('/api/test-notification', async (_req, res) => {
|
|
165
|
+
try {
|
|
166
|
+
const ok = await sendNotification('Test rms-devremote -- les notifications marchent !');
|
|
167
|
+
res.json({ ok });
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
res.json({ ok: false });
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
// POST /api/done — signal setup complete
|
|
174
|
+
app.post('/api/done', (_req, res) => {
|
|
175
|
+
res.json({ ok: true });
|
|
176
|
+
});
|
|
177
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { createServer } from 'http';
|
|
2
|
+
import { resolve, dirname } from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import express from 'express';
|
|
5
|
+
import { WebSocketServer } from 'ws';
|
|
6
|
+
import { setupRoutes } from './routes.js';
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = dirname(__filename);
|
|
9
|
+
const PORT = 3456;
|
|
10
|
+
export function getSetupUrl() {
|
|
11
|
+
return `http://localhost:${PORT}`;
|
|
12
|
+
}
|
|
13
|
+
export async function startSetupServer() {
|
|
14
|
+
const app = express();
|
|
15
|
+
app.use(express.json());
|
|
16
|
+
// Serve static files from src/setup-server/public/
|
|
17
|
+
// When running from dist/, go up two levels to reach the source tree
|
|
18
|
+
const publicDir = resolve(__dirname, '..', '..', 'src', 'setup-server', 'public');
|
|
19
|
+
app.use(express.static(publicDir));
|
|
20
|
+
const httpServer = createServer(app);
|
|
21
|
+
const wss = new WebSocketServer({ server: httpServer });
|
|
22
|
+
setupRoutes(app, wss);
|
|
23
|
+
await new Promise((resolvePromise) => {
|
|
24
|
+
httpServer.listen(PORT, '127.0.0.1', () => resolvePromise());
|
|
25
|
+
});
|
|
26
|
+
return {
|
|
27
|
+
close: () => {
|
|
28
|
+
httpServer.close();
|
|
29
|
+
wss.close();
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
services:
|
|
2
|
+
tunnel:
|
|
3
|
+
image: cloudflare/cloudflared:latest
|
|
4
|
+
container_name: devremote-tunnel
|
|
5
|
+
restart: unless-stopped
|
|
6
|
+
command: tunnel run
|
|
7
|
+
environment:
|
|
8
|
+
- TUNNEL_TOKEN=${CLOUDFLARE_TUNNEL_TOKEN}
|
|
9
|
+
|
|
10
|
+
ntfy:
|
|
11
|
+
image: binwiederhier/ntfy:latest
|
|
12
|
+
container_name: devremote-ntfy
|
|
13
|
+
restart: unless-stopped
|
|
14
|
+
command: serve
|
|
15
|
+
ports:
|
|
16
|
+
- "127.0.0.1:${NTFY_PORT:-2586}:80"
|
|
17
|
+
volumes:
|
|
18
|
+
- ./ntfy/server.yml:/etc/ntfy/server.yml:ro
|
|
19
|
+
- ntfy-cache:/var/cache/ntfy
|
|
20
|
+
- ntfy-auth:/var/lib/ntfy
|
|
21
|
+
|
|
22
|
+
volumes:
|
|
23
|
+
ntfy-cache:
|
|
24
|
+
ntfy-auth:
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "rms-devremote",
|
|
3
|
+
"version": "3.0.0",
|
|
4
|
+
"description": "Control your terminal remotely from your phone — mobile PWA with push notifications and zero open ports",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"rms-devremote": "./dist/index.js",
|
|
8
|
+
"rdr": "./dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist/",
|
|
12
|
+
"docker/",
|
|
13
|
+
"scripts/",
|
|
14
|
+
"src/setup-server/public/"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsc",
|
|
18
|
+
"dev": "tsc --watch",
|
|
19
|
+
"start": "node dist/index.js"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"terminal",
|
|
23
|
+
"remote",
|
|
24
|
+
"tmux",
|
|
25
|
+
"mobile",
|
|
26
|
+
"pwa",
|
|
27
|
+
"xterm",
|
|
28
|
+
"claude",
|
|
29
|
+
"notifications"
|
|
30
|
+
],
|
|
31
|
+
"author": "RMS Tech Studio",
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "https://github.com/RMS-Tech-Studio/claude-remote.git"
|
|
36
|
+
},
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=20.0.0"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@clack/prompts": "^1.1.0",
|
|
42
|
+
"@xterm/addon-fit": "^0.11.0",
|
|
43
|
+
"@xterm/addon-web-links": "^0.12.0",
|
|
44
|
+
"@xterm/xterm": "^6.0.0",
|
|
45
|
+
"bcryptjs": "^3.0.3",
|
|
46
|
+
"chalk": "^5.6.2",
|
|
47
|
+
"commander": "^14.0.3",
|
|
48
|
+
"express": "^5.2.1",
|
|
49
|
+
"jsonwebtoken": "^9.0.3",
|
|
50
|
+
"node-pty": "^1.1.0",
|
|
51
|
+
"ws": "^8.20.0"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@types/bcryptjs": "^3.0.0",
|
|
55
|
+
"@types/express": "^5.0.6",
|
|
56
|
+
"@types/jsonwebtoken": "^9.0.10",
|
|
57
|
+
"@types/node": "^25.5.0",
|
|
58
|
+
"@types/ws": "^8.18.1",
|
|
59
|
+
"typescript": "^6.0.2"
|
|
60
|
+
}
|
|
61
|
+
}
|