claude-relay 2.3.1 → 2.4.1
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 +20 -5
- package/bin/cli.js +261 -20
- package/lib/cli-sessions.js +270 -0
- package/lib/daemon.js +146 -37
- package/lib/project.js +129 -1
- package/lib/public/app.js +403 -76
- package/lib/public/css/base.css +41 -7
- package/lib/public/css/diff.css +6 -6
- package/lib/public/css/filebrowser.css +41 -56
- package/lib/public/css/highlight.css +144 -0
- package/lib/public/css/input.css +9 -9
- package/lib/public/css/menus.css +82 -23
- package/lib/public/css/messages.css +178 -34
- package/lib/public/css/overlays.css +184 -51
- package/lib/public/css/rewind.css +17 -17
- package/lib/public/css/sidebar.css +225 -145
- package/lib/public/index.html +75 -43
- package/lib/public/modules/filebrowser.js +30 -12
- package/lib/public/modules/fileicons.js +172 -0
- package/lib/public/modules/markdown.js +10 -10
- package/lib/public/modules/notifications.js +52 -2
- package/lib/public/modules/sidebar.js +109 -31
- package/lib/public/modules/terminal.js +11 -23
- package/lib/public/modules/theme.js +622 -0
- package/lib/public/modules/tools.js +245 -4
- package/lib/public/modules/utils.js +21 -5
- package/lib/public/style.css +1 -0
- package/lib/sdk-bridge.js +95 -0
- package/lib/server.js +42 -1
- package/lib/sessions.js +57 -5
- package/lib/themes/ayu-light.json +9 -0
- package/lib/themes/catppuccin-latte.json +9 -0
- package/lib/themes/catppuccin-mocha.json +9 -0
- package/lib/themes/claude-light.json +9 -0
- package/lib/themes/claude.json +9 -0
- package/lib/themes/dracula.json +9 -0
- package/lib/themes/everforest-light.json +9 -0
- package/lib/themes/everforest.json +9 -0
- package/lib/themes/github-light.json +9 -0
- package/lib/themes/gruvbox-dark.json +9 -0
- package/lib/themes/gruvbox-light.json +9 -0
- package/lib/themes/monokai.json +9 -0
- package/lib/themes/nord-light.json +9 -0
- package/lib/themes/nord.json +9 -0
- package/lib/themes/one-dark.json +9 -0
- package/lib/themes/one-light.json +9 -0
- package/lib/themes/rose-pine-dawn.json +9 -0
- package/lib/themes/rose-pine.json +9 -0
- package/lib/themes/solarized-dark.json +9 -0
- package/lib/themes/solarized-light.json +9 -0
- package/lib/themes/tokyo-night-light.json +9 -0
- package/lib/themes/tokyo-night.json +9 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
<h3 align="center">Web UI for Claude Code. Any device. Push notifications.</h3>
|
|
8
8
|
|
|
9
|
-
[](https://www.npmjs.com/package/claude-relay) [](https://www.npmjs.com/package/claude-relay) [](https://github.com/chadbyte/claude-relay)
|
|
9
|
+
[](https://www.npmjs.com/package/claude-relay) [](https://www.npmjs.com/package/claude-relay) [](https://github.com/chadbyte/claude-relay) [](https://github.com/chadbyte/claude-relay/blob/main/LICENSE)
|
|
10
10
|
|
|
11
11
|
Claude Code. Anywhere.
|
|
12
12
|
Same session. Same files. Same machine.
|
|
@@ -86,7 +86,7 @@ Scan the QR code with your phone to connect instantly, or open the URL displayed
|
|
|
86
86
|
## Key Features
|
|
87
87
|
|
|
88
88
|
* **Push Approvals** - Approve or reject from your phone while away, so Claude Code does not get stuck waiting.
|
|
89
|
-
* **Multi Project Daemon** - Manage all projects via a single port.
|
|
89
|
+
* **Multi Project Daemon** - Manage all projects via a single port. Add and remove projects from the browser.
|
|
90
90
|
* **Usage and Model Switching** - View token usage, rate limit bars, and switch models from the browser.
|
|
91
91
|
* **Session Search** - Full-text search across all session messages with hit timeline.
|
|
92
92
|
* **Auto Session Logs (JSONL)** - Conversations and execution history are always saved locally. No data loss on crashes or restarts. Location: `./.claude-relay/sessions/`
|
|
@@ -105,10 +105,10 @@ Scan the QR code with your phone to connect instantly, or open the URL displayed
|
|
|
105
105
|
|
|
106
106
|
**Projects and Sessions**
|
|
107
107
|
|
|
108
|
-
* **Multi Project** - Single port management for all projects.
|
|
108
|
+
* **Multi Project** - Single port management for all projects. Add and remove projects from the browser with path autocomplete.
|
|
109
109
|
* **Project Names** - Custom names make it easy to distinguish tabs.
|
|
110
110
|
* **Session Persistence** - Sessions survive server restarts, browser crashes, and network drops.
|
|
111
|
-
* **Session Handoff** - Start in the terminal, continue on your phone, pass back to desktop.
|
|
111
|
+
* **Session Handoff** - Start in the terminal, continue on your phone, pass back to desktop. Browse and resume CLI sessions directly from the web UI.
|
|
112
112
|
* **Session Search** - Full-text search across all session content with highlighted results and a rewind-style hit timeline.
|
|
113
113
|
* **Rewind (Native Claude Code)** - Accessible directly from the browser UI.
|
|
114
114
|
* **Draft Persistence** - Unsent messages are saved per session and restored when you switch back.
|
|
@@ -183,7 +183,7 @@ If push registration fails: check whether your browser trusts HTTPS and whether
|
|
|
183
183
|
```bash
|
|
184
184
|
npx claude-relay # Default (port 2633)
|
|
185
185
|
npx claude-relay -p 8080 # Specify port
|
|
186
|
-
npx claude-relay
|
|
186
|
+
npx claude-relay --yes # Skip interactive prompts (accept defaults)
|
|
187
187
|
npx claude-relay -y --pin 123456
|
|
188
188
|
# Non-interactive with PIN (for scripts/CI)
|
|
189
189
|
npx claude-relay --no-https # Disable HTTPS
|
|
@@ -196,6 +196,7 @@ npx claude-relay --list # List registered projects
|
|
|
196
196
|
npx claude-relay --shutdown # Stop the running daemon
|
|
197
197
|
npx claude-relay --dangerously-skip-permissions
|
|
198
198
|
# Bypass all permission prompts (PIN required during setup)
|
|
199
|
+
npx claude-relay --dev # Development mode (foreground, auto-restart on lib/ changes, port 2635)
|
|
199
200
|
```
|
|
200
201
|
|
|
201
202
|
## Requirements
|
|
@@ -205,6 +206,20 @@ npx claude-relay --dangerously-skip-permissions
|
|
|
205
206
|
* [mkcert](https://github.com/FiloSottile/mkcert) - For push notifications (optional)
|
|
206
207
|
* [Tailscale](https://tailscale.com) - For remote access (optional)
|
|
207
208
|
|
|
209
|
+
## Why claude-relay?
|
|
210
|
+
|
|
211
|
+
**Why not just use tmux + Termius?**
|
|
212
|
+
You can monitor the terminal remotely, but there are no push notifications and no way to approve permission requests without switching back to the terminal. On a phone, navigating a raw terminal session is clunky. You end up checking manually instead of getting notified, and the experience never feels native.
|
|
213
|
+
|
|
214
|
+
**Why not just add hooks to send notifications?**
|
|
215
|
+
Hooks with ntfy or Pushover can get you push notifications, but you still need shell scripts, config files, and third-party accounts just to get alerts. And once you get the notification, there is no UI to approve or reject from. You are back to opening a terminal. claude-relay gives you notifications, a one-tap approval UI, and a full web interface with a single command.
|
|
216
|
+
|
|
217
|
+
**Why not use Claude Code Desktop?**
|
|
218
|
+
The desktop app works great on your computer, but there is no mobile version. To access sessions from your phone, you need remote sessions on Anthropic's cloud, which requires pushing your code to GitHub first. claude-relay runs entirely on your machine and lets you connect from any device on your network.
|
|
219
|
+
|
|
220
|
+
**Why not use Happy Coder?**
|
|
221
|
+
Happy Coder requires installing a native app and routes through its own relay server with end-to-end encryption. claude-relay *is* the relay server, running on your machine. Open a URL and you are in. No app install, no signup, nothing leaves your network.
|
|
222
|
+
|
|
208
223
|
## Architecture
|
|
209
224
|
|
|
210
225
|
claude-relay is not a wrapper that intercepts standard input/output.
|
package/bin/cli.js
CHANGED
|
@@ -8,7 +8,7 @@ var qrcode = require("qrcode-terminal");
|
|
|
8
8
|
var net = require("net");
|
|
9
9
|
|
|
10
10
|
// Detect dev mode before loading config (env must be set before require)
|
|
11
|
-
var _isDev = process.argv[1] && path.basename(process.argv[1]) === "claude-relay-dev";
|
|
11
|
+
var _isDev = (process.argv[1] && path.basename(process.argv[1]) === "claude-relay-dev") || process.argv.includes("--dev");
|
|
12
12
|
if (_isDev) {
|
|
13
13
|
process.env.CLAUDE_RELAY_HOME = path.join(os.homedir(), ".claude-relay-dev");
|
|
14
14
|
}
|
|
@@ -40,6 +40,8 @@ var addPath = null;
|
|
|
40
40
|
var removePath = null;
|
|
41
41
|
var listMode = false;
|
|
42
42
|
var dangerouslySkipPermissions = false;
|
|
43
|
+
var headlessMode = false;
|
|
44
|
+
var watchMode = false;
|
|
43
45
|
|
|
44
46
|
for (var i = 0; i < args.length; i++) {
|
|
45
47
|
if (args[i] === "-p" || args[i] === "--port") {
|
|
@@ -53,6 +55,10 @@ for (var i = 0; i < args.length; i++) {
|
|
|
53
55
|
useHttps = false;
|
|
54
56
|
} else if (args[i] === "--no-update" || args[i] === "--skip-update") {
|
|
55
57
|
skipUpdate = true;
|
|
58
|
+
} else if (args[i] === "--dev") {
|
|
59
|
+
// Already handled above for CLAUDE_RELAY_HOME, just skip
|
|
60
|
+
} else if (args[i] === "--watch" || args[i] === "-w") {
|
|
61
|
+
watchMode = true;
|
|
56
62
|
} else if (args[i] === "--debug") {
|
|
57
63
|
debugMode = true;
|
|
58
64
|
} else if (args[i] === "-y" || args[i] === "--yes") {
|
|
@@ -70,6 +76,9 @@ for (var i = 0; i < args.length; i++) {
|
|
|
70
76
|
i++;
|
|
71
77
|
} else if (args[i] === "--list") {
|
|
72
78
|
listMode = true;
|
|
79
|
+
} else if (args[i] === "--headless") {
|
|
80
|
+
headlessMode = true;
|
|
81
|
+
autoYes = true;
|
|
73
82
|
} else if (args[i] === "--dangerously-skip-permissions") {
|
|
74
83
|
dangerouslySkipPermissions = true;
|
|
75
84
|
} else if (args[i] === "-h" || args[i] === "--help") {
|
|
@@ -89,12 +98,19 @@ for (var i = 0; i < args.length; i++) {
|
|
|
89
98
|
console.log(" --add <path> Add a project directory (use '.' for current)");
|
|
90
99
|
console.log(" --remove <path> Remove a project directory");
|
|
91
100
|
console.log(" --list List all registered projects");
|
|
101
|
+
console.log(" --headless Start daemon and exit immediately (implies --yes)");
|
|
92
102
|
console.log(" --dangerously-skip-permissions");
|
|
93
103
|
console.log(" Bypass all permission prompts (requires --pin)");
|
|
94
104
|
process.exit(0);
|
|
95
105
|
}
|
|
96
106
|
}
|
|
97
107
|
|
|
108
|
+
// Dev mode implies debug + skip update
|
|
109
|
+
if (_isDev) {
|
|
110
|
+
debugMode = true;
|
|
111
|
+
skipUpdate = true;
|
|
112
|
+
}
|
|
113
|
+
|
|
98
114
|
// --- Handle --shutdown before anything else ---
|
|
99
115
|
if (shutdownMode) {
|
|
100
116
|
var shutdownConfig = loadConfig();
|
|
@@ -393,9 +409,13 @@ async function restartDaemonFromConfig() {
|
|
|
393
409
|
newConfig.pid = child.pid;
|
|
394
410
|
saveConfig(newConfig);
|
|
395
411
|
|
|
396
|
-
// Wait and verify
|
|
397
|
-
|
|
398
|
-
var
|
|
412
|
+
// Wait and verify (retry up to 5 seconds)
|
|
413
|
+
var alive = false;
|
|
414
|
+
for (var rc = 0; rc < 10; rc++) {
|
|
415
|
+
await new Promise(function (resolve) { setTimeout(resolve, 500); });
|
|
416
|
+
alive = await isDaemonAliveAsync(newConfig);
|
|
417
|
+
if (alive) break;
|
|
418
|
+
}
|
|
399
419
|
if (!alive) {
|
|
400
420
|
log(a.red + " Restart failed. Check logs: " + a.reset + logFile);
|
|
401
421
|
process.exit(1);
|
|
@@ -1175,7 +1195,7 @@ function setup(callback) {
|
|
|
1175
1195
|
// ==============================
|
|
1176
1196
|
// Fork the daemon process
|
|
1177
1197
|
// ==============================
|
|
1178
|
-
async function forkDaemon(pin, keepAwake, extraProjects) {
|
|
1198
|
+
async function forkDaemon(pin, keepAwake, extraProjects, addCwd) {
|
|
1179
1199
|
var ip = getLocalIP();
|
|
1180
1200
|
var hasTls = false;
|
|
1181
1201
|
|
|
@@ -1197,12 +1217,18 @@ async function forkDaemon(pin, keepAwake, extraProjects) {
|
|
|
1197
1217
|
return;
|
|
1198
1218
|
}
|
|
1199
1219
|
|
|
1200
|
-
var
|
|
1201
|
-
var
|
|
1220
|
+
var allProjects = [];
|
|
1221
|
+
var usedSlugs = [];
|
|
1222
|
+
|
|
1223
|
+
// Only include cwd if explicitly requested
|
|
1224
|
+
if (addCwd) {
|
|
1225
|
+
var slug = generateSlug(cwd, []);
|
|
1226
|
+
allProjects.push({ path: cwd, slug: slug, addedAt: Date.now() });
|
|
1227
|
+
usedSlugs.push(slug);
|
|
1228
|
+
}
|
|
1202
1229
|
|
|
1203
1230
|
// Add restored projects (from ~/.clayrc)
|
|
1204
1231
|
if (extraProjects && extraProjects.length > 0) {
|
|
1205
|
-
var usedSlugs = [slug];
|
|
1206
1232
|
for (var ep = 0; ep < extraProjects.length; ep++) {
|
|
1207
1233
|
var rp = extraProjects[ep];
|
|
1208
1234
|
if (rp.path === cwd) continue; // skip if same as cwd
|
|
@@ -1247,11 +1273,13 @@ async function forkDaemon(pin, keepAwake, extraProjects) {
|
|
|
1247
1273
|
config.pid = child.pid;
|
|
1248
1274
|
saveConfig(config);
|
|
1249
1275
|
|
|
1250
|
-
// Wait for daemon to start
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1276
|
+
// Wait for daemon to start (retry up to 5 seconds)
|
|
1277
|
+
var alive = false;
|
|
1278
|
+
for (var attempt = 0; attempt < 10; attempt++) {
|
|
1279
|
+
await new Promise(function (resolve) { setTimeout(resolve, 500); });
|
|
1280
|
+
alive = await isDaemonAliveAsync(config);
|
|
1281
|
+
if (alive) break;
|
|
1282
|
+
}
|
|
1255
1283
|
if (!alive) {
|
|
1256
1284
|
log(a.red + "Failed to start daemon. Check logs:" + a.reset);
|
|
1257
1285
|
log(a.dim + logFile + a.reset);
|
|
@@ -1260,10 +1288,181 @@ async function forkDaemon(pin, keepAwake, extraProjects) {
|
|
|
1260
1288
|
return;
|
|
1261
1289
|
}
|
|
1262
1290
|
|
|
1291
|
+
// Headless mode — print status and exit immediately
|
|
1292
|
+
if (headlessMode) {
|
|
1293
|
+
var protocol = config.tls ? "https" : "http";
|
|
1294
|
+
var url = protocol + "://" + ip + ":" + config.port;
|
|
1295
|
+
console.log(" " + sym.done + " Daemon started (PID " + config.pid + ")");
|
|
1296
|
+
console.log(" " + sym.done + " " + url);
|
|
1297
|
+
console.log(" " + sym.done + " Headless mode — exiting CLI");
|
|
1298
|
+
process.exit(0);
|
|
1299
|
+
return;
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1263
1302
|
// Show success + QR
|
|
1264
1303
|
showServerStarted(config, ip);
|
|
1265
1304
|
}
|
|
1266
1305
|
|
|
1306
|
+
// ==============================
|
|
1307
|
+
// Dev mode — foreground daemon with file watching
|
|
1308
|
+
// ==============================
|
|
1309
|
+
async function devMode(pin, keepAwake, existingPinHash) {
|
|
1310
|
+
var ip = getLocalIP();
|
|
1311
|
+
var hasTls = false;
|
|
1312
|
+
|
|
1313
|
+
if (useHttps) {
|
|
1314
|
+
var certPaths = ensureCerts(ip);
|
|
1315
|
+
if (certPaths) hasTls = true;
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
var portFree = await isPortFree(port);
|
|
1319
|
+
if (!portFree) {
|
|
1320
|
+
console.log("\x1b[31m[dev] Port " + port + " is already in use.\x1b[0m");
|
|
1321
|
+
process.exit(1);
|
|
1322
|
+
return;
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
var slug = generateSlug(cwd, []);
|
|
1326
|
+
var allProjects = [{ path: cwd, slug: slug, addedAt: Date.now() }];
|
|
1327
|
+
|
|
1328
|
+
// Restore previous projects
|
|
1329
|
+
var rc = loadClayrc();
|
|
1330
|
+
var restorable = (rc.recentProjects || []).filter(function (p) {
|
|
1331
|
+
return p.path !== cwd && fs.existsSync(p.path);
|
|
1332
|
+
});
|
|
1333
|
+
var usedSlugs = [slug];
|
|
1334
|
+
for (var ri = 0; ri < restorable.length; ri++) {
|
|
1335
|
+
var rp = restorable[ri];
|
|
1336
|
+
var rpSlug = generateSlug(rp.path, usedSlugs);
|
|
1337
|
+
usedSlugs.push(rpSlug);
|
|
1338
|
+
allProjects.push({ path: rp.path, slug: rpSlug, title: rp.title || undefined, addedAt: rp.addedAt || Date.now() });
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
var config = {
|
|
1342
|
+
pid: null,
|
|
1343
|
+
port: port,
|
|
1344
|
+
pinHash: existingPinHash || (pin ? generateAuthToken(pin) : null),
|
|
1345
|
+
tls: hasTls,
|
|
1346
|
+
debug: true,
|
|
1347
|
+
keepAwake: keepAwake || false,
|
|
1348
|
+
dangerouslySkipPermissions: dangerouslySkipPermissions,
|
|
1349
|
+
projects: allProjects,
|
|
1350
|
+
};
|
|
1351
|
+
|
|
1352
|
+
ensureConfigDir();
|
|
1353
|
+
saveConfig(config);
|
|
1354
|
+
|
|
1355
|
+
var daemonScript = path.join(__dirname, "..", "lib", "daemon.js");
|
|
1356
|
+
var libDir = path.join(__dirname, "..", "lib");
|
|
1357
|
+
var child = null;
|
|
1358
|
+
var intentionalKill = false;
|
|
1359
|
+
var debounceTimer = null;
|
|
1360
|
+
|
|
1361
|
+
function spawnDaemon() {
|
|
1362
|
+
child = spawn(process.execPath, [daemonScript], {
|
|
1363
|
+
stdio: ["ignore", "inherit", "inherit"],
|
|
1364
|
+
env: Object.assign({}, process.env, {
|
|
1365
|
+
CLAUDE_RELAY_CONFIG: configPath(),
|
|
1366
|
+
}),
|
|
1367
|
+
});
|
|
1368
|
+
|
|
1369
|
+
child.on("exit", function (code) {
|
|
1370
|
+
child = null;
|
|
1371
|
+
if (intentionalKill) {
|
|
1372
|
+
intentionalKill = false;
|
|
1373
|
+
return;
|
|
1374
|
+
}
|
|
1375
|
+
// Exit code 120 = update restart — respawn daemon with current dev code
|
|
1376
|
+
if (code === 120) {
|
|
1377
|
+
console.log("\x1b[36m[dev]\x1b[0m Update restart — respawning daemon...");
|
|
1378
|
+
console.log("");
|
|
1379
|
+
setTimeout(spawnDaemon, 500);
|
|
1380
|
+
return;
|
|
1381
|
+
}
|
|
1382
|
+
// Unexpected exit — auto restart
|
|
1383
|
+
console.log("\x1b[33m[dev] Daemon exited (code " + code + "), restarting...\x1b[0m");
|
|
1384
|
+
setTimeout(spawnDaemon, 500);
|
|
1385
|
+
});
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
function restartDaemon() {
|
|
1389
|
+
intentionalKill = true;
|
|
1390
|
+
if (child) {
|
|
1391
|
+
child.kill("SIGTERM");
|
|
1392
|
+
// Give it a moment to shut down, then spawn
|
|
1393
|
+
setTimeout(spawnDaemon, 300);
|
|
1394
|
+
} else {
|
|
1395
|
+
intentionalKill = false;
|
|
1396
|
+
spawnDaemon();
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
console.log("\x1b[36m[dev]\x1b[0m Starting relay on port " + port + "...");
|
|
1401
|
+
if (watchMode) {
|
|
1402
|
+
console.log("\x1b[36m[dev]\x1b[0m Watching lib/ for changes (excluding lib/public/)");
|
|
1403
|
+
}
|
|
1404
|
+
console.log("");
|
|
1405
|
+
|
|
1406
|
+
spawnDaemon();
|
|
1407
|
+
|
|
1408
|
+
// Wait for daemon to be ready, then show CLI menu
|
|
1409
|
+
config.pid = child ? child.pid : null;
|
|
1410
|
+
saveConfig(config);
|
|
1411
|
+
|
|
1412
|
+
var daemonReady = false;
|
|
1413
|
+
for (var da = 0; da < 10; da++) {
|
|
1414
|
+
await new Promise(function (resolve) { setTimeout(resolve, 500); });
|
|
1415
|
+
daemonReady = await isDaemonAliveAsync(config);
|
|
1416
|
+
if (daemonReady) break;
|
|
1417
|
+
}
|
|
1418
|
+
if (daemonReady) {
|
|
1419
|
+
showServerStarted(config, ip);
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
// Watch lib/ for server-side file changes (only with --watch)
|
|
1423
|
+
var watcher = null;
|
|
1424
|
+
if (watchMode) {
|
|
1425
|
+
watcher = fs.watch(libDir, { recursive: true }, function (eventType, filename) {
|
|
1426
|
+
if (!filename) return;
|
|
1427
|
+
// Skip client-side files — they're served from disk
|
|
1428
|
+
if (filename.startsWith("public" + path.sep) || filename.startsWith("public/")) return;
|
|
1429
|
+
// Skip non-JS files
|
|
1430
|
+
if (!filename.endsWith(".js")) return;
|
|
1431
|
+
|
|
1432
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
1433
|
+
debounceTimer = setTimeout(function () {
|
|
1434
|
+
console.log("\x1b[36m[dev]\x1b[0m File changed: lib/" + filename);
|
|
1435
|
+
console.log("\x1b[36m[dev]\x1b[0m Restarting...");
|
|
1436
|
+
console.log("");
|
|
1437
|
+
restartDaemon();
|
|
1438
|
+
}, 300);
|
|
1439
|
+
});
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
// Clean exit on Ctrl+C
|
|
1443
|
+
var shuttingDown = false;
|
|
1444
|
+
process.on("SIGINT", function () {
|
|
1445
|
+
if (shuttingDown) return;
|
|
1446
|
+
shuttingDown = true;
|
|
1447
|
+
console.log("\n\x1b[36m[dev]\x1b[0m Shutting down...");
|
|
1448
|
+
if (watcher) watcher.close();
|
|
1449
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
1450
|
+
intentionalKill = true;
|
|
1451
|
+
if (child) {
|
|
1452
|
+
child.kill("SIGTERM");
|
|
1453
|
+
child.on("exit", function () {
|
|
1454
|
+
clearStaleConfig();
|
|
1455
|
+
process.exit(0);
|
|
1456
|
+
});
|
|
1457
|
+
// Force kill after 3s
|
|
1458
|
+
setTimeout(function () { process.exit(0); }, 3000);
|
|
1459
|
+
} else {
|
|
1460
|
+
clearStaleConfig();
|
|
1461
|
+
process.exit(0);
|
|
1462
|
+
}
|
|
1463
|
+
});
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1267
1466
|
// ==============================
|
|
1268
1467
|
// Restart daemon with TLS enabled
|
|
1269
1468
|
// ==============================
|
|
@@ -1324,9 +1523,12 @@ async function restartDaemonWithTLS(config, callback) {
|
|
|
1324
1523
|
newConfig.pid = child.pid;
|
|
1325
1524
|
saveConfig(newConfig);
|
|
1326
1525
|
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1526
|
+
var alive = false;
|
|
1527
|
+
for (var ra = 0; ra < 10; ra++) {
|
|
1528
|
+
await new Promise(function (resolve) { setTimeout(resolve, 500); });
|
|
1529
|
+
alive = await isDaemonAliveAsync(newConfig);
|
|
1530
|
+
if (alive) break;
|
|
1531
|
+
}
|
|
1330
1532
|
if (!alive) {
|
|
1331
1533
|
log(sym.warn + " " + a.yellow + "Failed to restart with HTTPS, falling back to HTTP..." + a.reset);
|
|
1332
1534
|
// Re-fork without TLS so the server is at least running
|
|
@@ -1345,7 +1547,11 @@ async function restartDaemonWithTLS(config, callback) {
|
|
|
1345
1547
|
fs.closeSync(logFd2);
|
|
1346
1548
|
newConfig.pid = child2.pid;
|
|
1347
1549
|
saveConfig(newConfig);
|
|
1348
|
-
|
|
1550
|
+
for (var rb = 0; rb < 10; rb++) {
|
|
1551
|
+
await new Promise(function (resolve) { setTimeout(resolve, 500); });
|
|
1552
|
+
var retryAlive = await isDaemonAliveAsync(newConfig);
|
|
1553
|
+
if (retryAlive) break;
|
|
1554
|
+
}
|
|
1349
1555
|
startDaemonWatcher();
|
|
1350
1556
|
callback(newConfig);
|
|
1351
1557
|
return;
|
|
@@ -1998,6 +2204,28 @@ var currentVersion = require("../package.json").version;
|
|
|
1998
2204
|
var updated = await checkAndUpdate(currentVersion, skipUpdate);
|
|
1999
2205
|
if (updated) return;
|
|
2000
2206
|
|
|
2207
|
+
// Dev mode — foreground daemon with file watching
|
|
2208
|
+
if (_isDev) {
|
|
2209
|
+
var devConfig = loadConfig();
|
|
2210
|
+
var devAlive = devConfig ? await isDaemonAliveAsync(devConfig) : false;
|
|
2211
|
+
if (devAlive) {
|
|
2212
|
+
console.log("\x1b[36m[dev]\x1b[0m Shutting down existing daemon...");
|
|
2213
|
+
await sendIPCCommand(socketPath(), { cmd: "shutdown" });
|
|
2214
|
+
clearStaleConfig();
|
|
2215
|
+
await new Promise(function (resolve) { setTimeout(resolve, 500); });
|
|
2216
|
+
}
|
|
2217
|
+
// First run — go through setup (disclaimer, port, PIN, etc.)
|
|
2218
|
+
if (!devConfig) {
|
|
2219
|
+
setup(function (pin, keepAwake) {
|
|
2220
|
+
devMode(pin, keepAwake, null);
|
|
2221
|
+
});
|
|
2222
|
+
} else {
|
|
2223
|
+
// Reuse existing PIN hash from previous config
|
|
2224
|
+
await devMode(cliPin || null, devConfig.keepAwake || false, devConfig.pinHash || null);
|
|
2225
|
+
}
|
|
2226
|
+
return;
|
|
2227
|
+
}
|
|
2228
|
+
|
|
2001
2229
|
var config = loadConfig();
|
|
2002
2230
|
var alive = config ? await isDaemonAliveAsync(config) : false;
|
|
2003
2231
|
|
|
@@ -2008,6 +2236,17 @@ var currentVersion = require("../package.json").version;
|
|
|
2008
2236
|
}
|
|
2009
2237
|
|
|
2010
2238
|
if (alive) {
|
|
2239
|
+
// Headless mode — daemon already running, just report and exit
|
|
2240
|
+
if (headlessMode) {
|
|
2241
|
+
var protocol = config.tls ? "https" : "http";
|
|
2242
|
+
var ip = getLocalIP();
|
|
2243
|
+
var url = protocol + "://" + ip + ":" + config.port;
|
|
2244
|
+
console.log(" " + sym.done + " Daemon already running (PID " + config.pid + ")");
|
|
2245
|
+
console.log(" " + sym.done + " " + url);
|
|
2246
|
+
process.exit(0);
|
|
2247
|
+
return;
|
|
2248
|
+
}
|
|
2249
|
+
|
|
2011
2250
|
// Daemon is running — auto-add cwd if needed, then show menu
|
|
2012
2251
|
var ip = getLocalIP();
|
|
2013
2252
|
|
|
@@ -2079,7 +2318,8 @@ var currentVersion = require("../package.json").version;
|
|
|
2079
2318
|
if (autoRestorable.length > 0) {
|
|
2080
2319
|
console.log(" " + sym.done + " Restoring " + autoRestorable.length + " previous project(s)");
|
|
2081
2320
|
}
|
|
2082
|
-
|
|
2321
|
+
var hasRestorable = autoRestorable.length > 0;
|
|
2322
|
+
await forkDaemon(pin, false, hasRestorable ? autoRestorable : undefined, !hasRestorable);
|
|
2083
2323
|
} else {
|
|
2084
2324
|
setup(function (pin, keepAwake) {
|
|
2085
2325
|
if (dangerouslySkipPermissions && !pin) {
|
|
@@ -2088,6 +2328,7 @@ var currentVersion = require("../package.json").version;
|
|
|
2088
2328
|
process.exit(1);
|
|
2089
2329
|
return;
|
|
2090
2330
|
}
|
|
2331
|
+
|
|
2091
2332
|
// Check ~/.clayrc for previous projects to restore
|
|
2092
2333
|
var rc = loadClayrc();
|
|
2093
2334
|
var restorable = (rc.recentProjects || []).filter(function (p) {
|
|
@@ -2096,13 +2337,13 @@ var currentVersion = require("../package.json").version;
|
|
|
2096
2337
|
|
|
2097
2338
|
if (restorable.length > 0) {
|
|
2098
2339
|
promptRestoreProjects(restorable, function (selected) {
|
|
2099
|
-
forkDaemon(pin, keepAwake, selected);
|
|
2340
|
+
forkDaemon(pin, keepAwake, selected, false);
|
|
2100
2341
|
});
|
|
2101
2342
|
} else {
|
|
2102
2343
|
log(sym.bar);
|
|
2103
2344
|
log(sym.end + " " + a.dim + "Starting relay..." + a.reset);
|
|
2104
2345
|
log("");
|
|
2105
|
-
forkDaemon(pin, keepAwake);
|
|
2346
|
+
forkDaemon(pin, keepAwake, undefined, true);
|
|
2106
2347
|
}
|
|
2107
2348
|
});
|
|
2108
2349
|
}
|