claude-relay 2.4.2 → 2.5.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/bin/cli.js +1 -2350
- package/package.json +7 -42
- package/LICENSE +0 -21
- package/README.md +0 -281
- package/lib/cli-sessions.js +0 -270
- package/lib/config.js +0 -222
- package/lib/daemon.js +0 -423
- package/lib/ipc.js +0 -112
- package/lib/pages.js +0 -714
- package/lib/project.js +0 -1224
- package/lib/public/app.js +0 -2157
- package/lib/public/apple-touch-icon.png +0 -0
- package/lib/public/css/base.css +0 -145
- package/lib/public/css/diff.css +0 -128
- package/lib/public/css/filebrowser.css +0 -1076
- package/lib/public/css/highlight.css +0 -144
- package/lib/public/css/input.css +0 -512
- package/lib/public/css/menus.css +0 -683
- package/lib/public/css/messages.css +0 -1159
- package/lib/public/css/overlays.css +0 -731
- package/lib/public/css/rewind.css +0 -529
- package/lib/public/css/sidebar.css +0 -794
- package/lib/public/favicon.svg +0 -26
- package/lib/public/icon-192.png +0 -0
- package/lib/public/icon-512.png +0 -0
- package/lib/public/icon-mono.svg +0 -19
- package/lib/public/index.html +0 -460
- package/lib/public/manifest.json +0 -27
- package/lib/public/modules/diff.js +0 -398
- package/lib/public/modules/events.js +0 -21
- package/lib/public/modules/filebrowser.js +0 -1375
- package/lib/public/modules/fileicons.js +0 -172
- package/lib/public/modules/icons.js +0 -54
- package/lib/public/modules/input.js +0 -578
- package/lib/public/modules/markdown.js +0 -149
- package/lib/public/modules/notifications.js +0 -643
- package/lib/public/modules/qrcode.js +0 -70
- package/lib/public/modules/rewind.js +0 -334
- package/lib/public/modules/sidebar.js +0 -628
- package/lib/public/modules/state.js +0 -3
- package/lib/public/modules/terminal.js +0 -658
- package/lib/public/modules/theme.js +0 -622
- package/lib/public/modules/tools.js +0 -1410
- package/lib/public/modules/utils.js +0 -56
- package/lib/public/style.css +0 -10
- package/lib/public/sw.js +0 -75
- package/lib/push.js +0 -125
- package/lib/sdk-bridge.js +0 -771
- package/lib/server.js +0 -577
- package/lib/sessions.js +0 -402
- package/lib/terminal-manager.js +0 -187
- package/lib/terminal.js +0 -24
- package/lib/themes/ayu-light.json +0 -9
- package/lib/themes/catppuccin-latte.json +0 -9
- package/lib/themes/catppuccin-mocha.json +0 -9
- package/lib/themes/claude-light.json +0 -9
- package/lib/themes/claude.json +0 -9
- package/lib/themes/dracula.json +0 -9
- package/lib/themes/everforest-light.json +0 -9
- package/lib/themes/everforest.json +0 -9
- package/lib/themes/github-light.json +0 -9
- package/lib/themes/gruvbox-dark.json +0 -9
- package/lib/themes/gruvbox-light.json +0 -9
- package/lib/themes/monokai.json +0 -9
- package/lib/themes/nord-light.json +0 -9
- package/lib/themes/nord.json +0 -9
- package/lib/themes/one-dark.json +0 -9
- package/lib/themes/one-light.json +0 -9
- package/lib/themes/rose-pine-dawn.json +0 -9
- package/lib/themes/rose-pine.json +0 -9
- package/lib/themes/solarized-dark.json +0 -9
- package/lib/themes/solarized-light.json +0 -9
- package/lib/themes/tokyo-night-light.json +0 -9
- package/lib/themes/tokyo-night.json +0 -9
- package/lib/updater.js +0 -96
package/bin/cli.js
CHANGED
|
@@ -1,2351 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
var os = require("os");
|
|
4
|
-
var fs = require("fs");
|
|
5
|
-
var path = require("path");
|
|
6
|
-
var { execSync, execFileSync, spawn } = require("child_process");
|
|
7
|
-
var qrcode = require("qrcode-terminal");
|
|
8
|
-
var net = require("net");
|
|
9
|
-
|
|
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") || process.argv.includes("--dev");
|
|
12
|
-
if (_isDev) {
|
|
13
|
-
process.env.CLAUDE_RELAY_HOME = path.join(os.homedir(), ".claude-relay-dev");
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
var { loadConfig, saveConfig, configPath, socketPath, logPath, ensureConfigDir, isDaemonAlive, isDaemonAliveAsync, generateSlug, clearStaleConfig, loadClayrc, saveClayrc, readCrashInfo } = require("../lib/config");
|
|
17
|
-
var { sendIPCCommand } = require("../lib/ipc");
|
|
18
|
-
var { generateAuthToken } = require("../lib/server");
|
|
19
|
-
|
|
20
|
-
function openUrl(url) {
|
|
21
|
-
try {
|
|
22
|
-
if (process.platform === "win32") {
|
|
23
|
-
spawn("cmd", ["/c", "start", "", url], { stdio: "ignore", detached: true, windowsHide: true }).unref();
|
|
24
|
-
} else {
|
|
25
|
-
var cmd = process.platform === "darwin" ? "open" : "xdg-open";
|
|
26
|
-
spawn(cmd, [url], { stdio: "ignore", detached: true }).unref();
|
|
27
|
-
}
|
|
28
|
-
} catch (e) {}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
var args = process.argv.slice(2);
|
|
32
|
-
var port = _isDev ? 2635 : 2633;
|
|
33
|
-
var useHttps = true;
|
|
34
|
-
var skipUpdate = false;
|
|
35
|
-
var debugMode = false;
|
|
36
|
-
var autoYes = false;
|
|
37
|
-
var cliPin = null;
|
|
38
|
-
var shutdownMode = false;
|
|
39
|
-
var addPath = null;
|
|
40
|
-
var removePath = null;
|
|
41
|
-
var listMode = false;
|
|
42
|
-
var dangerouslySkipPermissions = false;
|
|
43
|
-
var headlessMode = false;
|
|
44
|
-
var watchMode = false;
|
|
45
|
-
|
|
46
|
-
for (var i = 0; i < args.length; i++) {
|
|
47
|
-
if (args[i] === "-p" || args[i] === "--port") {
|
|
48
|
-
port = parseInt(args[i + 1], 10);
|
|
49
|
-
if (isNaN(port)) {
|
|
50
|
-
console.error("Invalid port number");
|
|
51
|
-
process.exit(1);
|
|
52
|
-
}
|
|
53
|
-
i++;
|
|
54
|
-
} else if (args[i] === "--no-https") {
|
|
55
|
-
useHttps = false;
|
|
56
|
-
} else if (args[i] === "--no-update" || args[i] === "--skip-update") {
|
|
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;
|
|
62
|
-
} else if (args[i] === "--debug") {
|
|
63
|
-
debugMode = true;
|
|
64
|
-
} else if (args[i] === "-y" || args[i] === "--yes") {
|
|
65
|
-
autoYes = true;
|
|
66
|
-
} else if (args[i] === "--pin") {
|
|
67
|
-
cliPin = args[i + 1] || null;
|
|
68
|
-
i++;
|
|
69
|
-
} else if (args[i] === "--shutdown") {
|
|
70
|
-
shutdownMode = true;
|
|
71
|
-
} else if (args[i] === "--add") {
|
|
72
|
-
addPath = args[i + 1] || ".";
|
|
73
|
-
i++;
|
|
74
|
-
} else if (args[i] === "--remove") {
|
|
75
|
-
removePath = args[i + 1] || null;
|
|
76
|
-
i++;
|
|
77
|
-
} else if (args[i] === "--list") {
|
|
78
|
-
listMode = true;
|
|
79
|
-
} else if (args[i] === "--headless") {
|
|
80
|
-
headlessMode = true;
|
|
81
|
-
autoYes = true;
|
|
82
|
-
} else if (args[i] === "--dangerously-skip-permissions") {
|
|
83
|
-
dangerouslySkipPermissions = true;
|
|
84
|
-
} else if (args[i] === "-h" || args[i] === "--help") {
|
|
85
|
-
console.log("Usage: claude-relay [-p|--port <port>] [--no-https] [--no-update] [--debug] [-y|--yes] [--pin <pin>] [--shutdown]");
|
|
86
|
-
console.log(" claude-relay --add <path> Add a project to the running daemon");
|
|
87
|
-
console.log(" claude-relay --remove <path> Remove a project from the running daemon");
|
|
88
|
-
console.log(" claude-relay --list List registered projects");
|
|
89
|
-
console.log("");
|
|
90
|
-
console.log("Options:");
|
|
91
|
-
console.log(" -p, --port <port> Port to listen on (default: 2633)");
|
|
92
|
-
console.log(" --no-https Disable HTTPS (enabled by default via mkcert)");
|
|
93
|
-
console.log(" --no-update Skip auto-update check on startup");
|
|
94
|
-
console.log(" --debug Enable debug panel in the web UI");
|
|
95
|
-
console.log(" -y, --yes Skip interactive prompts (accept defaults)");
|
|
96
|
-
console.log(" --pin <pin> Set 6-digit PIN (use with --yes)");
|
|
97
|
-
console.log(" --shutdown Shut down the running relay daemon");
|
|
98
|
-
console.log(" --add <path> Add a project directory (use '.' for current)");
|
|
99
|
-
console.log(" --remove <path> Remove a project directory");
|
|
100
|
-
console.log(" --list List all registered projects");
|
|
101
|
-
console.log(" --headless Start daemon and exit immediately (implies --yes)");
|
|
102
|
-
console.log(" --dangerously-skip-permissions");
|
|
103
|
-
console.log(" Bypass all permission prompts (requires --pin)");
|
|
104
|
-
process.exit(0);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// Dev mode implies debug + skip update
|
|
109
|
-
if (_isDev) {
|
|
110
|
-
debugMode = true;
|
|
111
|
-
skipUpdate = true;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// --- Handle --shutdown before anything else ---
|
|
115
|
-
if (shutdownMode) {
|
|
116
|
-
var shutdownConfig = loadConfig();
|
|
117
|
-
isDaemonAliveAsync(shutdownConfig).then(function (alive) {
|
|
118
|
-
if (!alive) {
|
|
119
|
-
console.error("No running daemon found.");
|
|
120
|
-
process.exit(1);
|
|
121
|
-
}
|
|
122
|
-
sendIPCCommand(socketPath(), { cmd: "shutdown" }).then(function () {
|
|
123
|
-
console.log("Server stopped.");
|
|
124
|
-
clearStaleConfig();
|
|
125
|
-
process.exit(0);
|
|
126
|
-
}).catch(function (err) {
|
|
127
|
-
console.error("Shutdown failed:", err.message);
|
|
128
|
-
process.exit(1);
|
|
129
|
-
});
|
|
130
|
-
});
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// --- Handle --add before anything else ---
|
|
135
|
-
if (addPath !== null) {
|
|
136
|
-
var absAdd = path.resolve(addPath);
|
|
137
|
-
try {
|
|
138
|
-
var stat = fs.statSync(absAdd);
|
|
139
|
-
if (!stat.isDirectory()) {
|
|
140
|
-
console.error("Not a directory: " + absAdd);
|
|
141
|
-
process.exit(1);
|
|
142
|
-
}
|
|
143
|
-
} catch (e) {
|
|
144
|
-
console.error("Directory not found: " + absAdd);
|
|
145
|
-
process.exit(1);
|
|
146
|
-
}
|
|
147
|
-
var addConfig = loadConfig();
|
|
148
|
-
isDaemonAliveAsync(addConfig).then(function (alive) {
|
|
149
|
-
if (!alive) {
|
|
150
|
-
console.error("No running daemon. Start with: npx claude-relay");
|
|
151
|
-
process.exit(1);
|
|
152
|
-
}
|
|
153
|
-
sendIPCCommand(socketPath(), { cmd: "add_project", path: absAdd }).then(function (res) {
|
|
154
|
-
if (res.ok) {
|
|
155
|
-
if (res.existing) {
|
|
156
|
-
console.log("Already registered: " + res.slug);
|
|
157
|
-
} else {
|
|
158
|
-
console.log("Added: " + res.slug + " \u2192 " + absAdd);
|
|
159
|
-
}
|
|
160
|
-
process.exit(0);
|
|
161
|
-
} else {
|
|
162
|
-
console.error("Failed: " + (res.error || "unknown error"));
|
|
163
|
-
process.exit(1);
|
|
164
|
-
}
|
|
165
|
-
});
|
|
166
|
-
});
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// --- Handle --remove before anything else ---
|
|
171
|
-
if (removePath !== null) {
|
|
172
|
-
var absRemove = path.resolve(removePath);
|
|
173
|
-
var removeConfig = loadConfig();
|
|
174
|
-
isDaemonAliveAsync(removeConfig).then(function (alive) {
|
|
175
|
-
if (!alive) {
|
|
176
|
-
console.error("No running daemon. Start with: npx claude-relay");
|
|
177
|
-
process.exit(1);
|
|
178
|
-
}
|
|
179
|
-
sendIPCCommand(socketPath(), { cmd: "remove_project", path: absRemove }).then(function (res) {
|
|
180
|
-
if (res.ok) {
|
|
181
|
-
console.log("Removed: " + path.basename(absRemove));
|
|
182
|
-
process.exit(0);
|
|
183
|
-
} else {
|
|
184
|
-
console.error("Failed: " + (res.error || "project not found"));
|
|
185
|
-
process.exit(1);
|
|
186
|
-
}
|
|
187
|
-
});
|
|
188
|
-
});
|
|
189
|
-
return;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// --- Handle --list before anything else ---
|
|
193
|
-
if (listMode) {
|
|
194
|
-
var listConfig = loadConfig();
|
|
195
|
-
isDaemonAliveAsync(listConfig).then(function (alive) {
|
|
196
|
-
if (!alive) {
|
|
197
|
-
console.error("No running daemon. Start with: npx claude-relay");
|
|
198
|
-
process.exit(1);
|
|
199
|
-
}
|
|
200
|
-
sendIPCCommand(socketPath(), { cmd: "get_status" }).then(function (res) {
|
|
201
|
-
if (!res.ok || !res.projects || res.projects.length === 0) {
|
|
202
|
-
console.log("No projects registered.");
|
|
203
|
-
process.exit(0);
|
|
204
|
-
return;
|
|
205
|
-
}
|
|
206
|
-
console.log("Projects (" + res.projects.length + "):\n");
|
|
207
|
-
for (var p = 0; p < res.projects.length; p++) {
|
|
208
|
-
var proj = res.projects[p];
|
|
209
|
-
var label = " " + proj.slug;
|
|
210
|
-
if (proj.title) label += " (" + proj.title + ")";
|
|
211
|
-
label += "\n " + proj.path;
|
|
212
|
-
console.log(label);
|
|
213
|
-
}
|
|
214
|
-
console.log("");
|
|
215
|
-
process.exit(0);
|
|
216
|
-
});
|
|
217
|
-
});
|
|
218
|
-
return;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
var cwd = process.cwd();
|
|
222
|
-
|
|
223
|
-
// --- ANSI helpers ---
|
|
224
|
-
var isBasicTerm = process.env.TERM_PROGRAM === "Apple_Terminal";
|
|
225
|
-
var a = {
|
|
226
|
-
reset: "\x1b[0m",
|
|
227
|
-
bold: "\x1b[1m",
|
|
228
|
-
dim: "\x1b[2m",
|
|
229
|
-
cyan: "\x1b[36m",
|
|
230
|
-
green: "\x1b[32m",
|
|
231
|
-
yellow: "\x1b[33m",
|
|
232
|
-
red: "\x1b[31m",
|
|
233
|
-
};
|
|
234
|
-
|
|
235
|
-
function gradient(text) {
|
|
236
|
-
if (isBasicTerm) {
|
|
237
|
-
return a.yellow + text + a.reset;
|
|
238
|
-
}
|
|
239
|
-
// Orange (#DA7756) → Gold (#D4A574)
|
|
240
|
-
var r0 = 218, g0 = 119, b0 = 86;
|
|
241
|
-
var r1 = 212, g1 = 165, b1 = 116;
|
|
242
|
-
var out = "";
|
|
243
|
-
var len = text.length;
|
|
244
|
-
for (var i = 0; i < len; i++) {
|
|
245
|
-
var t = len > 1 ? i / (len - 1) : 0;
|
|
246
|
-
var r = Math.round(r0 + (r1 - r0) * t);
|
|
247
|
-
var g = Math.round(g0 + (g1 - g0) * t);
|
|
248
|
-
var b = Math.round(b0 + (b1 - b0) * t);
|
|
249
|
-
out += "\x1b[38;2;" + r + ";" + g + ";" + b + "m" + text[i];
|
|
250
|
-
}
|
|
251
|
-
return out + a.reset;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
var sym = {
|
|
255
|
-
pointer: a.cyan + "◆" + a.reset,
|
|
256
|
-
done: a.green + "◇" + a.reset,
|
|
257
|
-
bar: a.dim + "│" + a.reset,
|
|
258
|
-
end: a.dim + "└" + a.reset,
|
|
259
|
-
warn: a.yellow + "▲" + a.reset,
|
|
260
|
-
};
|
|
261
|
-
|
|
262
|
-
function log(s) { console.log(" " + s); }
|
|
263
|
-
|
|
264
|
-
function clearUp(n) {
|
|
265
|
-
for (var i = 0; i < n; i++) {
|
|
266
|
-
process.stdout.write("\x1b[1A\x1b[2K");
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
// --- Daemon watcher ---
|
|
271
|
-
// Polls daemon socket; if connection fails, the server is down.
|
|
272
|
-
var _daemonWatcher = null;
|
|
273
|
-
|
|
274
|
-
function startDaemonWatcher() {
|
|
275
|
-
if (_daemonWatcher) return;
|
|
276
|
-
_daemonWatcher = setInterval(function () {
|
|
277
|
-
var client = net.connect(socketPath());
|
|
278
|
-
var timer = setTimeout(function () {
|
|
279
|
-
client.destroy();
|
|
280
|
-
onDaemonDied();
|
|
281
|
-
}, 1500);
|
|
282
|
-
client.on("connect", function () {
|
|
283
|
-
clearTimeout(timer);
|
|
284
|
-
client.destroy();
|
|
285
|
-
});
|
|
286
|
-
client.on("error", function () {
|
|
287
|
-
clearTimeout(timer);
|
|
288
|
-
client.destroy();
|
|
289
|
-
onDaemonDied();
|
|
290
|
-
});
|
|
291
|
-
}, 3000);
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
function stopDaemonWatcher() {
|
|
295
|
-
if (_daemonWatcher) {
|
|
296
|
-
clearInterval(_daemonWatcher);
|
|
297
|
-
_daemonWatcher = null;
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
var _restartAttempts = 0;
|
|
302
|
-
var MAX_RESTART_ATTEMPTS = 5;
|
|
303
|
-
var _restartBackoffStart = 0;
|
|
304
|
-
|
|
305
|
-
function onDaemonDied() {
|
|
306
|
-
stopDaemonWatcher();
|
|
307
|
-
// Clean up stdin in case a prompt is active
|
|
308
|
-
try {
|
|
309
|
-
process.stdin.setRawMode(false);
|
|
310
|
-
process.stdin.pause();
|
|
311
|
-
process.stdin.removeAllListeners("data");
|
|
312
|
-
} catch (e) {}
|
|
313
|
-
|
|
314
|
-
// Check if this was a crash (crash.json exists) vs intentional shutdown
|
|
315
|
-
var crashInfo = readCrashInfo();
|
|
316
|
-
if (!crashInfo) {
|
|
317
|
-
// Intentional shutdown, no restart
|
|
318
|
-
log("");
|
|
319
|
-
log(sym.warn + " " + a.yellow + "Server has been shut down." + a.reset);
|
|
320
|
-
log(a.dim + " Run " + a.reset + "npx claude-relay" + a.dim + " to start again." + a.reset);
|
|
321
|
-
log("");
|
|
322
|
-
process.exit(0);
|
|
323
|
-
return;
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
// Reset backoff counter if enough time has passed since last restart burst
|
|
327
|
-
var now = Date.now();
|
|
328
|
-
if (_restartBackoffStart && now - _restartBackoffStart > 60000) {
|
|
329
|
-
_restartAttempts = 0;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
_restartAttempts++;
|
|
333
|
-
if (_restartAttempts > MAX_RESTART_ATTEMPTS) {
|
|
334
|
-
log("");
|
|
335
|
-
log(sym.warn + " " + a.red + "Server crashed too many times (" + MAX_RESTART_ATTEMPTS + " attempts). Giving up." + a.reset);
|
|
336
|
-
if (crashInfo.reason) {
|
|
337
|
-
log(a.dim + " " + crashInfo.reason.split("\n")[0] + a.reset);
|
|
338
|
-
}
|
|
339
|
-
log(a.dim + " Check logs: " + a.reset + logPath());
|
|
340
|
-
log("");
|
|
341
|
-
process.exit(1);
|
|
342
|
-
return;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
if (_restartAttempts === 1) _restartBackoffStart = now;
|
|
346
|
-
|
|
347
|
-
log("");
|
|
348
|
-
log(sym.warn + " " + a.yellow + "Server crashed. Restarting... (" + _restartAttempts + "/" + MAX_RESTART_ATTEMPTS + ")" + a.reset);
|
|
349
|
-
if (crashInfo.reason) {
|
|
350
|
-
log(a.dim + " " + crashInfo.reason.split("\n")[0] + a.reset);
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
// Re-fork the daemon from saved config
|
|
354
|
-
restartDaemonFromConfig();
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
async function restartDaemonFromConfig() {
|
|
358
|
-
var lastConfig = loadConfig();
|
|
359
|
-
if (!lastConfig || !lastConfig.projects) {
|
|
360
|
-
log(a.red + " No config found. Cannot restart." + a.reset);
|
|
361
|
-
process.exit(1);
|
|
362
|
-
return;
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
clearStaleConfig();
|
|
366
|
-
|
|
367
|
-
// Wait for port to be released
|
|
368
|
-
var targetPort = lastConfig.port || port;
|
|
369
|
-
var waited = 0;
|
|
370
|
-
while (waited < 3000) {
|
|
371
|
-
var free = await isPortFree(targetPort);
|
|
372
|
-
if (free) break;
|
|
373
|
-
await new Promise(function (resolve) { setTimeout(resolve, 300); });
|
|
374
|
-
waited += 300;
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
// Rebuild config (preserve everything except pid)
|
|
378
|
-
var newConfig = {
|
|
379
|
-
pid: null,
|
|
380
|
-
port: targetPort,
|
|
381
|
-
pinHash: lastConfig.pinHash || null,
|
|
382
|
-
tls: lastConfig.tls !== undefined ? lastConfig.tls : useHttps,
|
|
383
|
-
debug: lastConfig.debug || false,
|
|
384
|
-
keepAwake: lastConfig.keepAwake || false,
|
|
385
|
-
dangerouslySkipPermissions: lastConfig.dangerouslySkipPermissions || false,
|
|
386
|
-
projects: (lastConfig.projects || []).filter(function (p) {
|
|
387
|
-
return fs.existsSync(p.path);
|
|
388
|
-
}),
|
|
389
|
-
};
|
|
390
|
-
|
|
391
|
-
ensureConfigDir();
|
|
392
|
-
saveConfig(newConfig);
|
|
393
|
-
|
|
394
|
-
var daemonScript = path.join(__dirname, "..", "lib", "daemon.js");
|
|
395
|
-
var logFile = logPath();
|
|
396
|
-
var logFd = fs.openSync(logFile, "a");
|
|
397
|
-
|
|
398
|
-
var child = spawn(process.execPath, [daemonScript], {
|
|
399
|
-
detached: true,
|
|
400
|
-
windowsHide: true,
|
|
401
|
-
stdio: ["ignore", logFd, logFd],
|
|
402
|
-
env: Object.assign({}, process.env, {
|
|
403
|
-
CLAUDE_RELAY_CONFIG: configPath(),
|
|
404
|
-
}),
|
|
405
|
-
});
|
|
406
|
-
child.unref();
|
|
407
|
-
fs.closeSync(logFd);
|
|
408
|
-
|
|
409
|
-
newConfig.pid = child.pid;
|
|
410
|
-
saveConfig(newConfig);
|
|
411
|
-
|
|
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
|
-
}
|
|
419
|
-
if (!alive) {
|
|
420
|
-
log(a.red + " Restart failed. Check logs: " + a.reset + logFile);
|
|
421
|
-
process.exit(1);
|
|
422
|
-
return;
|
|
423
|
-
}
|
|
424
|
-
var ip = getLocalIP();
|
|
425
|
-
log(sym.done + " " + a.green + "Server restarted successfully." + a.reset);
|
|
426
|
-
log("");
|
|
427
|
-
showMainMenu(newConfig, ip);
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
// --- Network ---
|
|
431
|
-
function getLocalIP() {
|
|
432
|
-
var interfaces = os.networkInterfaces();
|
|
433
|
-
|
|
434
|
-
// Prefer Tailscale IP
|
|
435
|
-
for (var name in interfaces) {
|
|
436
|
-
if (/^(tailscale|utun)/.test(name)) {
|
|
437
|
-
for (var j = 0; j < interfaces[name].length; j++) {
|
|
438
|
-
var addr = interfaces[name][j];
|
|
439
|
-
if (addr.family === "IPv4" && !addr.internal && addr.address.startsWith("100.")) {
|
|
440
|
-
return addr.address;
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
// All interfaces for Tailscale CGNAT range
|
|
447
|
-
for (var addrs of Object.values(interfaces)) {
|
|
448
|
-
for (var k = 0; k < addrs.length; k++) {
|
|
449
|
-
if (addrs[k].family === "IPv4" && !addrs[k].internal && addrs[k].address.startsWith("100.")) {
|
|
450
|
-
return addrs[k].address;
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
// Fall back to LAN IP
|
|
456
|
-
for (var addrs2 of Object.values(interfaces)) {
|
|
457
|
-
for (var m = 0; m < addrs2.length; m++) {
|
|
458
|
-
if (addrs2[m].family === "IPv4" && !addrs2[m].internal) {
|
|
459
|
-
return addrs2[m].address;
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
return "localhost";
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
// --- Certs ---
|
|
468
|
-
function isRoutableIP(addr) {
|
|
469
|
-
if (addr.startsWith("10.")) return true;
|
|
470
|
-
if (addr.startsWith("192.168.")) return true;
|
|
471
|
-
if (addr.startsWith("100.")) {
|
|
472
|
-
var second = parseInt(addr.split(".")[1], 10);
|
|
473
|
-
return second >= 64 && second <= 127; // CGNAT (Tailscale)
|
|
474
|
-
}
|
|
475
|
-
if (addr.startsWith("172.")) {
|
|
476
|
-
var second = parseInt(addr.split(".")[1], 10);
|
|
477
|
-
return second >= 16 && second <= 31;
|
|
478
|
-
}
|
|
479
|
-
return false;
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
function getAllIPs() {
|
|
483
|
-
var ips = [];
|
|
484
|
-
var ifaces = os.networkInterfaces();
|
|
485
|
-
for (var addrs of Object.values(ifaces)) {
|
|
486
|
-
for (var j = 0; j < addrs.length; j++) {
|
|
487
|
-
if (addrs[j].family === "IPv4" && !addrs[j].internal && isRoutableIP(addrs[j].address)) {
|
|
488
|
-
ips.push(addrs[j].address);
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
return ips;
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
function ensureCerts(ip) {
|
|
496
|
-
var homeDir = os.homedir();
|
|
497
|
-
var certDir = path.join(process.env.CLAUDE_RELAY_HOME || path.join(homeDir, ".claude-relay"), "certs");
|
|
498
|
-
var keyPath = path.join(certDir, "key.pem");
|
|
499
|
-
var certPath = path.join(certDir, "cert.pem");
|
|
500
|
-
|
|
501
|
-
var legacyDir = path.join(cwd, ".claude-relay", "certs");
|
|
502
|
-
var legacyKey = path.join(legacyDir, "key.pem");
|
|
503
|
-
var legacyCert = path.join(legacyDir, "cert.pem");
|
|
504
|
-
if (!fs.existsSync(keyPath) && fs.existsSync(legacyKey) && fs.existsSync(legacyCert)) {
|
|
505
|
-
fs.mkdirSync(certDir, { recursive: true });
|
|
506
|
-
fs.copyFileSync(legacyKey, keyPath);
|
|
507
|
-
fs.copyFileSync(legacyCert, certPath);
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
var caRoot = null;
|
|
511
|
-
try {
|
|
512
|
-
caRoot = path.join(
|
|
513
|
-
execSync("mkcert -CAROOT", { encoding: "utf8" }).trim(),
|
|
514
|
-
"rootCA.pem"
|
|
515
|
-
);
|
|
516
|
-
if (!fs.existsSync(caRoot)) caRoot = null;
|
|
517
|
-
} catch (e) {}
|
|
518
|
-
|
|
519
|
-
// Collect all IPv4 addresses (Tailscale + LAN)
|
|
520
|
-
var allIPs = getAllIPs();
|
|
521
|
-
|
|
522
|
-
if (fs.existsSync(keyPath) && fs.existsSync(certPath)) {
|
|
523
|
-
var needRegen = false;
|
|
524
|
-
try {
|
|
525
|
-
var certText = execFileSync("openssl", ["x509", "-in", certPath, "-text", "-noout"], { encoding: "utf8" });
|
|
526
|
-
for (var i = 0; i < allIPs.length; i++) {
|
|
527
|
-
if (certText.indexOf(allIPs[i]) === -1) {
|
|
528
|
-
needRegen = true;
|
|
529
|
-
break;
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
} catch (e) { needRegen = true; }
|
|
533
|
-
if (!needRegen) return { key: keyPath, cert: certPath, caRoot: caRoot };
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
fs.mkdirSync(certDir, { recursive: true });
|
|
537
|
-
|
|
538
|
-
var domains = ["localhost", "127.0.0.1", "::1"];
|
|
539
|
-
for (var i = 0; i < allIPs.length; i++) {
|
|
540
|
-
if (domains.indexOf(allIPs[i]) === -1) domains.push(allIPs[i]);
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
try {
|
|
544
|
-
var mkcertArgs = ["-key-file", keyPath, "-cert-file", certPath].concat(domains);
|
|
545
|
-
execFileSync("mkcert", mkcertArgs, { stdio: "pipe" });
|
|
546
|
-
} catch (err) {
|
|
547
|
-
return null;
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
return { key: keyPath, cert: certPath, caRoot: caRoot };
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
// --- Logo ---
|
|
554
|
-
function printLogo() {
|
|
555
|
-
var c = isBasicTerm ? a.yellow : "\x1b[38;2;218;119;86m";
|
|
556
|
-
var r = a.reset;
|
|
557
|
-
var lines = [
|
|
558
|
-
" ██████╗ ██╗ █████╗ ██╗ ██╗ ██████╗ ███████╗ ██████╗ ███████╗ ██╗ █████╗ ██╗ ██╗",
|
|
559
|
-
" ██╔════╝ ██║ ██╔══██╗ ██║ ██║ ██╔══██╗ ██╔════╝ ██╔══██╗ ██╔════╝ ██║ ██╔══██╗ ╚██╗ ██╔╝",
|
|
560
|
-
" ██║ ██║ ███████║ ██║ ██║ ██║ ██║ █████╗ ██████╔╝ █████╗ ██║ ███████║ ╚████╔╝ ",
|
|
561
|
-
" ██║ ██║ ██╔══██║ ██║ ██║ ██║ ██║ ██╔══╝ ██╔══██╗ ██╔══╝ ██║ ██╔══██║ ╚██╔╝ ",
|
|
562
|
-
" ╚██████╗ ███████╗ ██║ ██║ ╚██████╔╝ ██████╔╝ ███████╗ ██║ ██║ ███████╗ ███████╗ ██║ ██║ ██║ ",
|
|
563
|
-
" ╚═════╝ ╚══════╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═╝ ╚══════╝ ╚══════╝ ╚═╝ ╚═╝ ╚═╝ ",
|
|
564
|
-
];
|
|
565
|
-
console.log("");
|
|
566
|
-
for (var i = 0; i < lines.length; i++) {
|
|
567
|
-
console.log(c + lines[i] + r);
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
// --- Interactive prompts ---
|
|
572
|
-
function promptToggle(title, desc, defaultValue, callback) {
|
|
573
|
-
var value = defaultValue || false;
|
|
574
|
-
|
|
575
|
-
function renderToggle() {
|
|
576
|
-
var yes = value
|
|
577
|
-
? a.green + a.bold + "● Yes" + a.reset
|
|
578
|
-
: a.dim + "○ Yes" + a.reset;
|
|
579
|
-
var no = !value
|
|
580
|
-
? a.green + a.bold + "● No" + a.reset
|
|
581
|
-
: a.dim + "○ No" + a.reset;
|
|
582
|
-
return yes + a.dim + " / " + a.reset + no;
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
var lines = 2;
|
|
586
|
-
log(sym.pointer + " " + a.bold + title + a.reset);
|
|
587
|
-
if (desc) {
|
|
588
|
-
log(sym.bar + " " + a.dim + desc + a.reset);
|
|
589
|
-
lines = 3;
|
|
590
|
-
}
|
|
591
|
-
process.stdout.write(" " + sym.bar + " " + renderToggle());
|
|
592
|
-
|
|
593
|
-
process.stdin.setRawMode(true);
|
|
594
|
-
process.stdin.resume();
|
|
595
|
-
process.stdin.setEncoding("utf8");
|
|
596
|
-
|
|
597
|
-
process.stdin.on("data", function onToggle(ch) {
|
|
598
|
-
if (ch === "\x1b[D" || ch === "\x1b[C" || ch === "\t") {
|
|
599
|
-
value = !value;
|
|
600
|
-
process.stdout.write("\x1b[2K\r " + sym.bar + " " + renderToggle());
|
|
601
|
-
} else if (ch === "y" || ch === "Y") {
|
|
602
|
-
value = true;
|
|
603
|
-
process.stdout.write("\x1b[2K\r " + sym.bar + " " + renderToggle());
|
|
604
|
-
} else if (ch === "n" || ch === "N") {
|
|
605
|
-
value = false;
|
|
606
|
-
process.stdout.write("\x1b[2K\r " + sym.bar + " " + renderToggle());
|
|
607
|
-
} else if (ch === "\r" || ch === "\n") {
|
|
608
|
-
process.stdin.setRawMode(false);
|
|
609
|
-
process.stdin.pause();
|
|
610
|
-
process.stdin.removeListener("data", onToggle);
|
|
611
|
-
process.stdout.write("\n");
|
|
612
|
-
clearUp(lines);
|
|
613
|
-
var result = value ? a.green + "Yes" + a.reset : a.dim + "No" + a.reset;
|
|
614
|
-
log(sym.done + " " + title + " " + a.dim + "·" + a.reset + " " + result);
|
|
615
|
-
callback(value);
|
|
616
|
-
} else if (ch === "\x03") {
|
|
617
|
-
process.stdout.write("\n");
|
|
618
|
-
clearUp(lines);
|
|
619
|
-
log(sym.end + " " + a.dim + "Cancelled" + a.reset);
|
|
620
|
-
process.exit(0);
|
|
621
|
-
}
|
|
622
|
-
});
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
function promptPin(callback) {
|
|
626
|
-
log(sym.pointer + " " + a.bold + "PIN protection" + a.reset);
|
|
627
|
-
log(sym.bar + " " + a.dim + "Require a 6-digit PIN to access the web UI. Enter to skip." + a.reset);
|
|
628
|
-
process.stdout.write(" " + sym.bar + " ");
|
|
629
|
-
|
|
630
|
-
var pin = "";
|
|
631
|
-
process.stdin.setRawMode(true);
|
|
632
|
-
process.stdin.resume();
|
|
633
|
-
process.stdin.setEncoding("utf8");
|
|
634
|
-
|
|
635
|
-
process.stdin.on("data", function onPin(ch) {
|
|
636
|
-
if (ch === "\r" || ch === "\n") {
|
|
637
|
-
process.stdin.setRawMode(false);
|
|
638
|
-
process.stdin.pause();
|
|
639
|
-
process.stdin.removeListener("data", onPin);
|
|
640
|
-
process.stdout.write("\n");
|
|
641
|
-
|
|
642
|
-
if (pin !== "" && !/^\d{6}$/.test(pin)) {
|
|
643
|
-
clearUp(3);
|
|
644
|
-
log(sym.done + " PIN protection " + a.red + "Must be exactly 6 digits" + a.reset);
|
|
645
|
-
log(sym.end);
|
|
646
|
-
process.exit(1);
|
|
647
|
-
return;
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
clearUp(3);
|
|
651
|
-
if (pin) {
|
|
652
|
-
log(sym.done + " PIN protection " + a.dim + "·" + a.reset + " " + a.green + "Enabled" + a.reset);
|
|
653
|
-
} else {
|
|
654
|
-
log(sym.done + " PIN protection " + a.dim + "· Skipped" + a.reset);
|
|
655
|
-
}
|
|
656
|
-
log(sym.bar);
|
|
657
|
-
callback(pin || null);
|
|
658
|
-
} else if (ch === "\x03") {
|
|
659
|
-
process.stdout.write("\n");
|
|
660
|
-
clearUp(3);
|
|
661
|
-
log(sym.end + " " + a.dim + "Cancelled" + a.reset);
|
|
662
|
-
process.exit(0);
|
|
663
|
-
} else if (ch === "\x7f" || ch === "\b") {
|
|
664
|
-
if (pin.length > 0) {
|
|
665
|
-
pin = pin.slice(0, -1);
|
|
666
|
-
process.stdout.write("\b \b");
|
|
667
|
-
}
|
|
668
|
-
} else if (/\d/.test(ch) && pin.length < 6) {
|
|
669
|
-
pin += ch;
|
|
670
|
-
process.stdout.write(a.cyan + "●" + a.reset);
|
|
671
|
-
}
|
|
672
|
-
});
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
/**
|
|
676
|
-
* Text input prompt with placeholder and Tab directory completion.
|
|
677
|
-
* title: prompt label, placeholder: dimmed hint, callback(value)
|
|
678
|
-
* Enter with empty input returns placeholder value.
|
|
679
|
-
* Tab completes directory paths.
|
|
680
|
-
*/
|
|
681
|
-
function promptText(title, placeholder, callback) {
|
|
682
|
-
var prefix = " " + sym.bar + " ";
|
|
683
|
-
var hintLine = "";
|
|
684
|
-
var lineCount = 2;
|
|
685
|
-
|
|
686
|
-
log(sym.pointer + " " + a.bold + title + a.reset + " " + a.dim + "(esc to go back)" + a.reset);
|
|
687
|
-
process.stdout.write(prefix + a.dim + placeholder + a.reset);
|
|
688
|
-
// Move cursor to start of placeholder
|
|
689
|
-
process.stdout.write("\r" + prefix);
|
|
690
|
-
|
|
691
|
-
var text = "";
|
|
692
|
-
var showingPlaceholder = true;
|
|
693
|
-
process.stdin.setRawMode(true);
|
|
694
|
-
process.stdin.resume();
|
|
695
|
-
process.stdin.setEncoding("utf8");
|
|
696
|
-
|
|
697
|
-
function redrawInput() {
|
|
698
|
-
process.stdout.write("\x1b[2K\r" + prefix + text);
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
function clearHint() {
|
|
702
|
-
if (hintLine) {
|
|
703
|
-
// Erase the hint line below
|
|
704
|
-
process.stdout.write("\n\x1b[2K\x1b[1A");
|
|
705
|
-
hintLine = "";
|
|
706
|
-
lineCount = 2;
|
|
707
|
-
}
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
function showHint(msg) {
|
|
711
|
-
clearHint();
|
|
712
|
-
hintLine = msg;
|
|
713
|
-
lineCount = 3;
|
|
714
|
-
// Print hint below, then move cursor back up
|
|
715
|
-
process.stdout.write("\n" + prefix + a.dim + msg + a.reset + "\x1b[1A");
|
|
716
|
-
redrawInput();
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
function tabComplete() {
|
|
720
|
-
var current = text || "";
|
|
721
|
-
if (!current) current = "/";
|
|
722
|
-
|
|
723
|
-
// Resolve ~ to home
|
|
724
|
-
if (current.charAt(0) === "~") {
|
|
725
|
-
current = os.homedir() + current.substring(1);
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
var resolved = path.resolve(current);
|
|
729
|
-
var dir, partial;
|
|
730
|
-
|
|
731
|
-
try {
|
|
732
|
-
var st = fs.statSync(resolved);
|
|
733
|
-
if (st.isDirectory()) {
|
|
734
|
-
// Current text is a full directory — list its children
|
|
735
|
-
dir = resolved;
|
|
736
|
-
partial = "";
|
|
737
|
-
} else {
|
|
738
|
-
dir = path.dirname(resolved);
|
|
739
|
-
partial = path.basename(resolved);
|
|
740
|
-
}
|
|
741
|
-
} catch (e) {
|
|
742
|
-
// Path doesn't exist — complete from parent
|
|
743
|
-
dir = path.dirname(resolved);
|
|
744
|
-
partial = path.basename(resolved);
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
var entries;
|
|
748
|
-
try {
|
|
749
|
-
entries = fs.readdirSync(dir);
|
|
750
|
-
} catch (e) {
|
|
751
|
-
return; // Can't read directory
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
// Filter to directories only, matching partial prefix
|
|
755
|
-
var matches = [];
|
|
756
|
-
var lowerPartial = partial.toLowerCase();
|
|
757
|
-
for (var i = 0; i < entries.length; i++) {
|
|
758
|
-
if (entries[i].charAt(0) === "." && !partial.startsWith(".")) continue;
|
|
759
|
-
if (lowerPartial && entries[i].toLowerCase().indexOf(lowerPartial) !== 0) continue;
|
|
760
|
-
try {
|
|
761
|
-
var full = path.join(dir, entries[i]);
|
|
762
|
-
if (fs.statSync(full).isDirectory()) {
|
|
763
|
-
matches.push(entries[i]);
|
|
764
|
-
}
|
|
765
|
-
} catch (e) {}
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
if (matches.length === 0) return;
|
|
769
|
-
|
|
770
|
-
if (matches.length === 1) {
|
|
771
|
-
// Single match — complete it
|
|
772
|
-
var completed = path.join(dir, matches[0]) + path.sep;
|
|
773
|
-
text = completed;
|
|
774
|
-
showingPlaceholder = false;
|
|
775
|
-
clearHint();
|
|
776
|
-
redrawInput();
|
|
777
|
-
} else {
|
|
778
|
-
// Multiple matches — find longest common prefix and show candidates
|
|
779
|
-
var common = matches[0];
|
|
780
|
-
for (var m = 1; m < matches.length; m++) {
|
|
781
|
-
var k = 0;
|
|
782
|
-
while (k < common.length && k < matches[m].length && common.charAt(k) === matches[m].charAt(k)) k++;
|
|
783
|
-
common = common.substring(0, k);
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
if (common.length > partial.length) {
|
|
787
|
-
// Extend to common prefix
|
|
788
|
-
text = path.join(dir, common);
|
|
789
|
-
showingPlaceholder = false;
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
// Show candidates as hint
|
|
793
|
-
var display = matches.slice(0, 6).join(" ");
|
|
794
|
-
if (matches.length > 6) display += " " + a.dim + "+" + (matches.length - 6) + " more" + a.reset;
|
|
795
|
-
showHint(display);
|
|
796
|
-
}
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
process.stdin.on("data", function onText(ch) {
|
|
800
|
-
if (ch === "\r" || ch === "\n") {
|
|
801
|
-
process.stdin.setRawMode(false);
|
|
802
|
-
process.stdin.pause();
|
|
803
|
-
process.stdin.removeListener("data", onText);
|
|
804
|
-
var result = text || placeholder;
|
|
805
|
-
clearHint();
|
|
806
|
-
process.stdout.write("\n");
|
|
807
|
-
clearUp(2);
|
|
808
|
-
log(sym.done + " " + title + " " + a.dim + "·" + a.reset + " " + result);
|
|
809
|
-
callback(result);
|
|
810
|
-
} else if (ch === "\x1b" || ch === "\x03") {
|
|
811
|
-
process.stdin.setRawMode(false);
|
|
812
|
-
process.stdin.pause();
|
|
813
|
-
process.stdin.removeListener("data", onText);
|
|
814
|
-
clearHint();
|
|
815
|
-
process.stdout.write("\n");
|
|
816
|
-
clearUp(2);
|
|
817
|
-
if (ch === "\x03") {
|
|
818
|
-
log(sym.end + " " + a.dim + "Cancelled" + a.reset);
|
|
819
|
-
process.exit(0);
|
|
820
|
-
}
|
|
821
|
-
callback(null);
|
|
822
|
-
} else if (ch === "\t") {
|
|
823
|
-
if (showingPlaceholder) {
|
|
824
|
-
// Accept placeholder first
|
|
825
|
-
text = placeholder;
|
|
826
|
-
showingPlaceholder = false;
|
|
827
|
-
redrawInput();
|
|
828
|
-
}
|
|
829
|
-
tabComplete();
|
|
830
|
-
} else if (ch === "\x7f" || ch === "\b") {
|
|
831
|
-
if (text.length > 0) {
|
|
832
|
-
text = text.slice(0, -1);
|
|
833
|
-
clearHint();
|
|
834
|
-
if (text.length === 0) {
|
|
835
|
-
// Re-show placeholder
|
|
836
|
-
showingPlaceholder = true;
|
|
837
|
-
process.stdout.write("\x1b[2K\r" + prefix + a.dim + placeholder + a.reset);
|
|
838
|
-
process.stdout.write("\r" + prefix);
|
|
839
|
-
} else {
|
|
840
|
-
redrawInput();
|
|
841
|
-
}
|
|
842
|
-
}
|
|
843
|
-
} else if (ch >= " ") {
|
|
844
|
-
if (showingPlaceholder) {
|
|
845
|
-
showingPlaceholder = false;
|
|
846
|
-
}
|
|
847
|
-
clearHint();
|
|
848
|
-
text += ch;
|
|
849
|
-
redrawInput();
|
|
850
|
-
}
|
|
851
|
-
});
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
/**
|
|
855
|
-
* Select menu: arrow keys to navigate, enter to select.
|
|
856
|
-
* items: [{ label, value, desc? }]
|
|
857
|
-
*/
|
|
858
|
-
function promptSelect(title, items, callback, opts) {
|
|
859
|
-
var idx = 0;
|
|
860
|
-
// Build hotkeys map: { key: handler }
|
|
861
|
-
var hotkeys = {};
|
|
862
|
-
if (opts && opts.key && opts.onKey) {
|
|
863
|
-
hotkeys[opts.key] = opts.onKey;
|
|
864
|
-
}
|
|
865
|
-
if (opts && opts.keys) {
|
|
866
|
-
for (var ki = 0; ki < opts.keys.length; ki++) {
|
|
867
|
-
hotkeys[opts.keys[ki].key] = opts.keys[ki].onKey;
|
|
868
|
-
}
|
|
869
|
-
}
|
|
870
|
-
var hintLines = null;
|
|
871
|
-
if (opts && opts.hint) {
|
|
872
|
-
hintLines = Array.isArray(opts.hint) ? opts.hint : [opts.hint];
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
function render() {
|
|
876
|
-
var out = "";
|
|
877
|
-
for (var i = 0; i < items.length; i++) {
|
|
878
|
-
var prefix = i === idx
|
|
879
|
-
? a.green + a.bold + " ● " + a.reset
|
|
880
|
-
: a.dim + " ○ " + a.reset;
|
|
881
|
-
out += " " + sym.bar + prefix + items[i].label + "\n";
|
|
882
|
-
}
|
|
883
|
-
return out;
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
log(sym.pointer + " " + a.bold + title + a.reset);
|
|
887
|
-
process.stdout.write(render());
|
|
888
|
-
|
|
889
|
-
// Render hint lines below the menu tree
|
|
890
|
-
var hintBoxLines = 0;
|
|
891
|
-
if (hintLines) {
|
|
892
|
-
log(sym.end);
|
|
893
|
-
for (var h = 0; h < hintLines.length; h++) {
|
|
894
|
-
log(" " + gradient(hintLines[h]));
|
|
895
|
-
}
|
|
896
|
-
hintBoxLines = 1 + hintLines.length; // sym.end + lines
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
var lineCount = items.length + 1 + hintBoxLines;
|
|
900
|
-
|
|
901
|
-
process.stdin.setRawMode(true);
|
|
902
|
-
process.stdin.resume();
|
|
903
|
-
process.stdin.setEncoding("utf8");
|
|
904
|
-
|
|
905
|
-
process.stdin.on("data", function onSelect(ch) {
|
|
906
|
-
if (ch === "\x1b[A") { // up
|
|
907
|
-
if (idx > 0) idx--;
|
|
908
|
-
} else if (ch === "\x1b[B") { // down
|
|
909
|
-
if (idx < items.length - 1) idx++;
|
|
910
|
-
} else if (ch === "\r" || ch === "\n") {
|
|
911
|
-
process.stdin.setRawMode(false);
|
|
912
|
-
process.stdin.pause();
|
|
913
|
-
process.stdin.removeListener("data", onSelect);
|
|
914
|
-
clearUp(lineCount);
|
|
915
|
-
log(sym.done + " " + title + " " + a.dim + "·" + a.reset + " " + items[idx].label);
|
|
916
|
-
callback(items[idx].value);
|
|
917
|
-
return;
|
|
918
|
-
} else if (ch === "\x03") {
|
|
919
|
-
process.stdout.write("\n");
|
|
920
|
-
process.exit(0);
|
|
921
|
-
} else if (hotkeys[ch]) {
|
|
922
|
-
process.stdin.setRawMode(false);
|
|
923
|
-
process.stdin.pause();
|
|
924
|
-
process.stdin.removeListener("data", onSelect);
|
|
925
|
-
clearUp(lineCount);
|
|
926
|
-
hotkeys[ch]();
|
|
927
|
-
return;
|
|
928
|
-
} else if (ch === "\x7f" || ch === "\b") {
|
|
929
|
-
// Backspace — trigger "back" if available
|
|
930
|
-
for (var bi = 0; bi < items.length; bi++) {
|
|
931
|
-
if (items[bi].value === "back") {
|
|
932
|
-
process.stdin.setRawMode(false);
|
|
933
|
-
process.stdin.pause();
|
|
934
|
-
process.stdin.removeListener("data", onSelect);
|
|
935
|
-
clearUp(lineCount);
|
|
936
|
-
log(sym.done + " " + title + " " + a.dim + "·" + a.reset + " " + items[bi].label);
|
|
937
|
-
callback("back");
|
|
938
|
-
return;
|
|
939
|
-
}
|
|
940
|
-
}
|
|
941
|
-
return;
|
|
942
|
-
} else {
|
|
943
|
-
return;
|
|
944
|
-
}
|
|
945
|
-
// Redraw
|
|
946
|
-
clearUp(items.length + hintBoxLines);
|
|
947
|
-
process.stdout.write(render());
|
|
948
|
-
// Re-render hint lines
|
|
949
|
-
if (hintLines) {
|
|
950
|
-
log(sym.end);
|
|
951
|
-
for (var rh = 0; rh < hintLines.length; rh++) {
|
|
952
|
-
log(" " + gradient(hintLines[rh]));
|
|
953
|
-
}
|
|
954
|
-
}
|
|
955
|
-
});
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
/**
|
|
959
|
-
* Multi-select menu: space to toggle, enter to confirm.
|
|
960
|
-
* items: [{ label, value, checked? }]
|
|
961
|
-
* callback(selectedValues[])
|
|
962
|
-
*/
|
|
963
|
-
function promptMultiSelect(title, items, callback) {
|
|
964
|
-
var selected = [];
|
|
965
|
-
for (var si = 0; si < items.length; si++) {
|
|
966
|
-
selected.push(items[si].checked !== false);
|
|
967
|
-
}
|
|
968
|
-
var idx = 0;
|
|
969
|
-
|
|
970
|
-
function render() {
|
|
971
|
-
var out = "";
|
|
972
|
-
for (var i = 0; i < items.length; i++) {
|
|
973
|
-
var cursor = i === idx ? a.cyan + ">" + a.reset : " ";
|
|
974
|
-
var check = selected[i]
|
|
975
|
-
? a.green + a.bold + "■" + a.reset
|
|
976
|
-
: a.dim + "□" + a.reset;
|
|
977
|
-
out += " " + sym.bar + " " + cursor + " " + check + " " + items[i].label + "\n";
|
|
978
|
-
}
|
|
979
|
-
out += " " + sym.bar + " " + a.dim + "space: toggle · enter: confirm" + a.reset + "\n";
|
|
980
|
-
return out;
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
log(sym.pointer + " " + a.bold + title + a.reset);
|
|
984
|
-
process.stdout.write(render());
|
|
985
|
-
|
|
986
|
-
var lineCount = items.length + 2; // title + items + hint
|
|
987
|
-
|
|
988
|
-
process.stdin.setRawMode(true);
|
|
989
|
-
process.stdin.resume();
|
|
990
|
-
process.stdin.setEncoding("utf8");
|
|
991
|
-
|
|
992
|
-
process.stdin.on("data", function onMulti(ch) {
|
|
993
|
-
if (ch === "\x1b[A") { // up
|
|
994
|
-
if (idx > 0) idx--;
|
|
995
|
-
} else if (ch === "\x1b[B") { // down
|
|
996
|
-
if (idx < items.length - 1) idx++;
|
|
997
|
-
} else if (ch === " ") { // toggle
|
|
998
|
-
selected[idx] = !selected[idx];
|
|
999
|
-
} else if (ch === "a" || ch === "A") { // toggle all
|
|
1000
|
-
var allSelected = selected.every(function (s) { return s; });
|
|
1001
|
-
for (var ai = 0; ai < selected.length; ai++) selected[ai] = !allSelected;
|
|
1002
|
-
} else if (ch === "\r" || ch === "\n") {
|
|
1003
|
-
process.stdin.setRawMode(false);
|
|
1004
|
-
process.stdin.pause();
|
|
1005
|
-
process.stdin.removeListener("data", onMulti);
|
|
1006
|
-
clearUp(lineCount);
|
|
1007
|
-
var result = [];
|
|
1008
|
-
var labels = [];
|
|
1009
|
-
for (var ri = 0; ri < items.length; ri++) {
|
|
1010
|
-
if (selected[ri]) {
|
|
1011
|
-
result.push(items[ri].value);
|
|
1012
|
-
labels.push(items[ri].label);
|
|
1013
|
-
}
|
|
1014
|
-
}
|
|
1015
|
-
var summary = result.length === items.length
|
|
1016
|
-
? "All (" + result.length + ")"
|
|
1017
|
-
: result.length + " of " + items.length;
|
|
1018
|
-
log(sym.done + " " + title + " " + a.dim + "·" + a.reset + " " + summary);
|
|
1019
|
-
callback(result);
|
|
1020
|
-
return;
|
|
1021
|
-
} else if (ch === "\x03") {
|
|
1022
|
-
process.stdout.write("\n");
|
|
1023
|
-
process.exit(0);
|
|
1024
|
-
} else if (ch === "\x1b") {
|
|
1025
|
-
// Escape — select none
|
|
1026
|
-
process.stdin.setRawMode(false);
|
|
1027
|
-
process.stdin.pause();
|
|
1028
|
-
process.stdin.removeListener("data", onMulti);
|
|
1029
|
-
clearUp(lineCount);
|
|
1030
|
-
log(sym.done + " " + title + " " + a.dim + "· Skipped" + a.reset);
|
|
1031
|
-
callback([]);
|
|
1032
|
-
return;
|
|
1033
|
-
} else {
|
|
1034
|
-
return;
|
|
1035
|
-
}
|
|
1036
|
-
// Redraw
|
|
1037
|
-
clearUp(items.length + 1); // items + hint (not title)
|
|
1038
|
-
process.stdout.write(render());
|
|
1039
|
-
});
|
|
1040
|
-
}
|
|
1041
|
-
|
|
1042
|
-
// --- Port availability ---
|
|
1043
|
-
|
|
1044
|
-
function isPortFree(p) {
|
|
1045
|
-
return new Promise(function (resolve) {
|
|
1046
|
-
var srv = net.createServer();
|
|
1047
|
-
srv.once("error", function () { resolve(false); });
|
|
1048
|
-
srv.once("listening", function () { srv.close(function () { resolve(true); }); });
|
|
1049
|
-
srv.listen(p);
|
|
1050
|
-
});
|
|
1051
|
-
}
|
|
1052
|
-
|
|
1053
|
-
// --- Detect tools ---
|
|
1054
|
-
function getTailscaleIP() {
|
|
1055
|
-
var interfaces = os.networkInterfaces();
|
|
1056
|
-
for (var name in interfaces) {
|
|
1057
|
-
if (/^(tailscale|utun)/.test(name)) {
|
|
1058
|
-
for (var i = 0; i < interfaces[name].length; i++) {
|
|
1059
|
-
var addr = interfaces[name][i];
|
|
1060
|
-
if (addr.family === "IPv4" && !addr.internal && addr.address.startsWith("100.")) {
|
|
1061
|
-
return addr.address;
|
|
1062
|
-
}
|
|
1063
|
-
}
|
|
1064
|
-
}
|
|
1065
|
-
}
|
|
1066
|
-
for (var addrs of Object.values(interfaces)) {
|
|
1067
|
-
for (var j = 0; j < addrs.length; j++) {
|
|
1068
|
-
if (addrs[j].family === "IPv4" && !addrs[j].internal && addrs[j].address.startsWith("100.")) {
|
|
1069
|
-
return addrs[j].address;
|
|
1070
|
-
}
|
|
1071
|
-
}
|
|
1072
|
-
}
|
|
1073
|
-
return null;
|
|
1074
|
-
}
|
|
1075
|
-
|
|
1076
|
-
function hasTailscale() {
|
|
1077
|
-
return getTailscaleIP() !== null;
|
|
1078
|
-
}
|
|
1079
|
-
|
|
1080
|
-
function hasMkcert() {
|
|
1081
|
-
try {
|
|
1082
|
-
execSync("mkcert -CAROOT", { stdio: "pipe", encoding: "utf8" });
|
|
1083
|
-
return true;
|
|
1084
|
-
} catch (e) { return false; }
|
|
1085
|
-
}
|
|
1086
|
-
|
|
1087
|
-
// ==============================
|
|
1088
|
-
// Restore projects from ~/.clayrc
|
|
1089
|
-
// ==============================
|
|
1090
|
-
function promptRestoreProjects(projects, callback) {
|
|
1091
|
-
log(sym.bar);
|
|
1092
|
-
log(sym.pointer + " " + a.bold + "Previous projects found" + a.reset);
|
|
1093
|
-
log(sym.bar + " " + a.dim + "Restore projects from your last session?" + a.reset);
|
|
1094
|
-
log(sym.bar);
|
|
1095
|
-
|
|
1096
|
-
var items = projects.map(function (p) {
|
|
1097
|
-
var name = p.title || path.basename(p.path);
|
|
1098
|
-
return {
|
|
1099
|
-
label: a.bold + name + a.reset + " " + a.dim + p.path + a.reset,
|
|
1100
|
-
value: p,
|
|
1101
|
-
checked: true,
|
|
1102
|
-
};
|
|
1103
|
-
});
|
|
1104
|
-
|
|
1105
|
-
promptMultiSelect("Restore projects", items, function (selected) {
|
|
1106
|
-
// Remove unselected projects from ~/.clayrc
|
|
1107
|
-
if (selected.length < projects.length) {
|
|
1108
|
-
var selectedPaths = {};
|
|
1109
|
-
for (var si = 0; si < selected.length; si++) {
|
|
1110
|
-
selectedPaths[selected[si].path] = true;
|
|
1111
|
-
}
|
|
1112
|
-
try {
|
|
1113
|
-
var rc = loadClayrc();
|
|
1114
|
-
rc.recentProjects = (rc.recentProjects || []).filter(function (p) {
|
|
1115
|
-
return selectedPaths[p.path];
|
|
1116
|
-
});
|
|
1117
|
-
saveClayrc(rc);
|
|
1118
|
-
} catch (e) {}
|
|
1119
|
-
}
|
|
1120
|
-
|
|
1121
|
-
log(sym.bar);
|
|
1122
|
-
if (selected.length > 0) {
|
|
1123
|
-
log(sym.done + " " + a.green + "Restoring " + selected.length + (selected.length === 1 ? " project" : " projects") + a.reset);
|
|
1124
|
-
} else {
|
|
1125
|
-
log(sym.done + " " + a.dim + "Starting fresh" + a.reset);
|
|
1126
|
-
}
|
|
1127
|
-
log(sym.end + " " + a.dim + "Starting relay..." + a.reset);
|
|
1128
|
-
log("");
|
|
1129
|
-
callback(selected);
|
|
1130
|
-
});
|
|
1131
|
-
}
|
|
1132
|
-
|
|
1133
|
-
// ==============================
|
|
1134
|
-
// First-run setup (no daemon)
|
|
1135
|
-
// ==============================
|
|
1136
|
-
function setup(callback) {
|
|
1137
|
-
console.clear();
|
|
1138
|
-
printLogo();
|
|
1139
|
-
log("");
|
|
1140
|
-
log(sym.pointer + " " + a.bold + "Claude Relay" + a.reset + a.dim + " · Unofficial, open-source project" + a.reset);
|
|
1141
|
-
log(sym.bar);
|
|
1142
|
-
log(sym.bar + " " + a.dim + "Anyone with the URL gets full Claude Code access to this machine." + a.reset);
|
|
1143
|
-
log(sym.bar + " " + a.dim + "Use a private network (Tailscale, VPN)." + a.reset);
|
|
1144
|
-
log(sym.bar + " " + a.dim + "The authors assume no responsibility for any damage or data loss." + a.reset);
|
|
1145
|
-
log(sym.bar);
|
|
1146
|
-
|
|
1147
|
-
promptToggle("Accept and continue", null, true, function (accepted) {
|
|
1148
|
-
if (!accepted) {
|
|
1149
|
-
log(sym.end + " " + a.dim + "Aborted." + a.reset);
|
|
1150
|
-
log("");
|
|
1151
|
-
process.exit(0);
|
|
1152
|
-
return;
|
|
1153
|
-
}
|
|
1154
|
-
log(sym.bar);
|
|
1155
|
-
|
|
1156
|
-
function askPort() {
|
|
1157
|
-
promptText("Port", String(port), function (val) {
|
|
1158
|
-
if (val === null) {
|
|
1159
|
-
log(sym.end + " " + a.dim + "Aborted." + a.reset);
|
|
1160
|
-
log("");
|
|
1161
|
-
process.exit(0);
|
|
1162
|
-
return;
|
|
1163
|
-
}
|
|
1164
|
-
var p = parseInt(val, 10);
|
|
1165
|
-
if (!p || p < 1 || p > 65535) {
|
|
1166
|
-
log(sym.warn + " " + a.red + "Invalid port number" + a.reset);
|
|
1167
|
-
askPort();
|
|
1168
|
-
return;
|
|
1169
|
-
}
|
|
1170
|
-
isPortFree(p).then(function (free) {
|
|
1171
|
-
if (!free) {
|
|
1172
|
-
log(sym.warn + " " + a.yellow + "Port " + p + " is already in use" + a.reset);
|
|
1173
|
-
askPort();
|
|
1174
|
-
return;
|
|
1175
|
-
}
|
|
1176
|
-
port = p;
|
|
1177
|
-
log(sym.bar);
|
|
1178
|
-
|
|
1179
|
-
promptPin(function (pin) {
|
|
1180
|
-
if (process.platform === "darwin") {
|
|
1181
|
-
promptToggle("Keep awake", "Prevent system sleep while relay is running", false, function (keepAwake) {
|
|
1182
|
-
callback(pin, keepAwake);
|
|
1183
|
-
});
|
|
1184
|
-
} else {
|
|
1185
|
-
callback(pin, false);
|
|
1186
|
-
}
|
|
1187
|
-
});
|
|
1188
|
-
});
|
|
1189
|
-
});
|
|
1190
|
-
}
|
|
1191
|
-
askPort();
|
|
1192
|
-
});
|
|
1193
|
-
}
|
|
1194
|
-
|
|
1195
|
-
// ==============================
|
|
1196
|
-
// Fork the daemon process
|
|
1197
|
-
// ==============================
|
|
1198
|
-
async function forkDaemon(pin, keepAwake, extraProjects, addCwd) {
|
|
1199
|
-
var ip = getLocalIP();
|
|
1200
|
-
var hasTls = false;
|
|
1201
|
-
|
|
1202
|
-
if (useHttps) {
|
|
1203
|
-
var certPaths = ensureCerts(ip);
|
|
1204
|
-
if (certPaths) {
|
|
1205
|
-
hasTls = true;
|
|
1206
|
-
} else {
|
|
1207
|
-
log(sym.warn + " " + a.yellow + "HTTPS unavailable" + a.reset + a.dim + " · mkcert not installed" + a.reset);
|
|
1208
|
-
}
|
|
1209
|
-
}
|
|
1210
|
-
|
|
1211
|
-
// Check port availability
|
|
1212
|
-
var portFree = await isPortFree(port);
|
|
1213
|
-
if (!portFree) {
|
|
1214
|
-
log(a.red + "Port " + port + " is already in use." + a.reset);
|
|
1215
|
-
log(a.dim + "Is another Claude Relay daemon running?" + a.reset);
|
|
1216
|
-
process.exit(1);
|
|
1217
|
-
return;
|
|
1218
|
-
}
|
|
1219
|
-
|
|
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
|
-
}
|
|
1229
|
-
|
|
1230
|
-
// Add restored projects (from ~/.clayrc)
|
|
1231
|
-
if (extraProjects && extraProjects.length > 0) {
|
|
1232
|
-
for (var ep = 0; ep < extraProjects.length; ep++) {
|
|
1233
|
-
var rp = extraProjects[ep];
|
|
1234
|
-
if (rp.path === cwd) continue; // skip if same as cwd
|
|
1235
|
-
if (!fs.existsSync(rp.path)) continue; // skip missing directories
|
|
1236
|
-
var rpSlug = generateSlug(rp.path, usedSlugs);
|
|
1237
|
-
usedSlugs.push(rpSlug);
|
|
1238
|
-
allProjects.push({ path: rp.path, slug: rpSlug, title: rp.title || undefined, addedAt: rp.addedAt || Date.now() });
|
|
1239
|
-
}
|
|
1240
|
-
}
|
|
1241
|
-
|
|
1242
|
-
var config = {
|
|
1243
|
-
pid: null,
|
|
1244
|
-
port: port,
|
|
1245
|
-
pinHash: pin ? generateAuthToken(pin) : null,
|
|
1246
|
-
tls: hasTls,
|
|
1247
|
-
debug: debugMode,
|
|
1248
|
-
keepAwake: keepAwake,
|
|
1249
|
-
dangerouslySkipPermissions: dangerouslySkipPermissions,
|
|
1250
|
-
projects: allProjects,
|
|
1251
|
-
};
|
|
1252
|
-
|
|
1253
|
-
ensureConfigDir();
|
|
1254
|
-
saveConfig(config);
|
|
1255
|
-
|
|
1256
|
-
// Fork daemon
|
|
1257
|
-
var daemonScript = path.join(__dirname, "..", "lib", "daemon.js");
|
|
1258
|
-
var logFile = logPath();
|
|
1259
|
-
var logFd = fs.openSync(logFile, "a");
|
|
1260
|
-
|
|
1261
|
-
var child = spawn(process.execPath, [daemonScript], {
|
|
1262
|
-
detached: true,
|
|
1263
|
-
windowsHide: true,
|
|
1264
|
-
stdio: ["ignore", logFd, logFd],
|
|
1265
|
-
env: Object.assign({}, process.env, {
|
|
1266
|
-
CLAUDE_RELAY_CONFIG: configPath(),
|
|
1267
|
-
}),
|
|
1268
|
-
});
|
|
1269
|
-
child.unref();
|
|
1270
|
-
fs.closeSync(logFd);
|
|
1271
|
-
|
|
1272
|
-
// Update config with PID
|
|
1273
|
-
config.pid = child.pid;
|
|
1274
|
-
saveConfig(config);
|
|
1275
|
-
|
|
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
|
-
}
|
|
1283
|
-
if (!alive) {
|
|
1284
|
-
log(a.red + "Failed to start daemon. Check logs:" + a.reset);
|
|
1285
|
-
log(a.dim + logFile + a.reset);
|
|
1286
|
-
clearStaleConfig();
|
|
1287
|
-
process.exit(1);
|
|
1288
|
-
return;
|
|
1289
|
-
}
|
|
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
|
-
|
|
1302
|
-
// Show success + QR
|
|
1303
|
-
showServerStarted(config, ip);
|
|
1304
|
-
}
|
|
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
|
-
|
|
1466
|
-
// ==============================
|
|
1467
|
-
// Restart daemon with TLS enabled
|
|
1468
|
-
// ==============================
|
|
1469
|
-
async function restartDaemonWithTLS(config, callback) {
|
|
1470
|
-
var ip = getLocalIP();
|
|
1471
|
-
var certPaths = ensureCerts(ip);
|
|
1472
|
-
if (!certPaths) {
|
|
1473
|
-
callback(config);
|
|
1474
|
-
return;
|
|
1475
|
-
}
|
|
1476
|
-
|
|
1477
|
-
// Shut down old daemon
|
|
1478
|
-
stopDaemonWatcher();
|
|
1479
|
-
try {
|
|
1480
|
-
await sendIPCCommand(socketPath(), { cmd: "shutdown" });
|
|
1481
|
-
} catch (e) {}
|
|
1482
|
-
|
|
1483
|
-
// Wait for port to be released
|
|
1484
|
-
var waited = 0;
|
|
1485
|
-
while (waited < 5000) {
|
|
1486
|
-
await new Promise(function (resolve) { setTimeout(resolve, 300); });
|
|
1487
|
-
waited += 300;
|
|
1488
|
-
var free = await isPortFree(config.port);
|
|
1489
|
-
if (free) break;
|
|
1490
|
-
}
|
|
1491
|
-
clearStaleConfig();
|
|
1492
|
-
|
|
1493
|
-
// Re-fork with TLS
|
|
1494
|
-
var newConfig = {
|
|
1495
|
-
pid: null,
|
|
1496
|
-
port: config.port,
|
|
1497
|
-
pinHash: config.pinHash || null,
|
|
1498
|
-
tls: true,
|
|
1499
|
-
debug: config.debug || false,
|
|
1500
|
-
keepAwake: config.keepAwake || false,
|
|
1501
|
-
dangerouslySkipPermissions: config.dangerouslySkipPermissions || false,
|
|
1502
|
-
projects: config.projects || [],
|
|
1503
|
-
};
|
|
1504
|
-
|
|
1505
|
-
ensureConfigDir();
|
|
1506
|
-
saveConfig(newConfig);
|
|
1507
|
-
|
|
1508
|
-
var daemonScript = path.join(__dirname, "..", "lib", "daemon.js");
|
|
1509
|
-
var logFile = logPath();
|
|
1510
|
-
var logFd = fs.openSync(logFile, "a");
|
|
1511
|
-
|
|
1512
|
-
var child = spawn(process.execPath, [daemonScript], {
|
|
1513
|
-
detached: true,
|
|
1514
|
-
windowsHide: true,
|
|
1515
|
-
stdio: ["ignore", logFd, logFd],
|
|
1516
|
-
env: Object.assign({}, process.env, {
|
|
1517
|
-
CLAUDE_RELAY_CONFIG: configPath(),
|
|
1518
|
-
}),
|
|
1519
|
-
});
|
|
1520
|
-
child.unref();
|
|
1521
|
-
fs.closeSync(logFd);
|
|
1522
|
-
|
|
1523
|
-
newConfig.pid = child.pid;
|
|
1524
|
-
saveConfig(newConfig);
|
|
1525
|
-
|
|
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
|
-
}
|
|
1532
|
-
if (!alive) {
|
|
1533
|
-
log(sym.warn + " " + a.yellow + "Failed to restart with HTTPS, falling back to HTTP..." + a.reset);
|
|
1534
|
-
// Re-fork without TLS so the server is at least running
|
|
1535
|
-
newConfig.tls = false;
|
|
1536
|
-
saveConfig(newConfig);
|
|
1537
|
-
var logFd2 = fs.openSync(logFile, "a");
|
|
1538
|
-
var child2 = spawn(process.execPath, [daemonScript], {
|
|
1539
|
-
detached: true,
|
|
1540
|
-
windowsHide: true,
|
|
1541
|
-
stdio: ["ignore", logFd2, logFd2],
|
|
1542
|
-
env: Object.assign({}, process.env, {
|
|
1543
|
-
CLAUDE_RELAY_CONFIG: configPath(),
|
|
1544
|
-
}),
|
|
1545
|
-
});
|
|
1546
|
-
child2.unref();
|
|
1547
|
-
fs.closeSync(logFd2);
|
|
1548
|
-
newConfig.pid = child2.pid;
|
|
1549
|
-
saveConfig(newConfig);
|
|
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
|
-
}
|
|
1555
|
-
startDaemonWatcher();
|
|
1556
|
-
callback(newConfig);
|
|
1557
|
-
return;
|
|
1558
|
-
}
|
|
1559
|
-
|
|
1560
|
-
startDaemonWatcher();
|
|
1561
|
-
callback(newConfig);
|
|
1562
|
-
}
|
|
1563
|
-
|
|
1564
|
-
// ==============================
|
|
1565
|
-
// Show server started info
|
|
1566
|
-
// ==============================
|
|
1567
|
-
function showServerStarted(config, ip) {
|
|
1568
|
-
showMainMenu(config, ip);
|
|
1569
|
-
}
|
|
1570
|
-
|
|
1571
|
-
// ==============================
|
|
1572
|
-
// Main management menu
|
|
1573
|
-
// ==============================
|
|
1574
|
-
function showMainMenu(config, ip) {
|
|
1575
|
-
startDaemonWatcher();
|
|
1576
|
-
var protocol = config.tls ? "https" : "http";
|
|
1577
|
-
var url = protocol + "://" + ip + ":" + config.port;
|
|
1578
|
-
|
|
1579
|
-
sendIPCCommand(socketPath(), { cmd: "get_status" }).then(function (status) {
|
|
1580
|
-
var projs = (status && status.projects) || [];
|
|
1581
|
-
var totalSessions = 0;
|
|
1582
|
-
var totalAwaiting = 0;
|
|
1583
|
-
for (var i = 0; i < projs.length; i++) {
|
|
1584
|
-
totalSessions += projs[i].sessions || 0;
|
|
1585
|
-
if (projs[i].isProcessing) totalAwaiting++;
|
|
1586
|
-
}
|
|
1587
|
-
|
|
1588
|
-
console.clear();
|
|
1589
|
-
printLogo();
|
|
1590
|
-
log("");
|
|
1591
|
-
|
|
1592
|
-
function afterQr() {
|
|
1593
|
-
// Status line
|
|
1594
|
-
log(" " + a.dim + "claude-relay" + a.reset + " " + a.dim + "v" + currentVersion + a.reset + a.dim + " — " + url + a.reset);
|
|
1595
|
-
var parts = [];
|
|
1596
|
-
parts.push(a.bold + projs.length + a.reset + a.dim + (projs.length === 1 ? " project" : " projects"));
|
|
1597
|
-
parts.push(a.reset + a.bold + totalSessions + a.reset + a.dim + (totalSessions === 1 ? " session" : " sessions"));
|
|
1598
|
-
if (totalAwaiting > 0) {
|
|
1599
|
-
parts.push(a.reset + a.yellow + a.bold + totalAwaiting + a.reset + a.yellow + " awaiting" + a.reset + a.dim);
|
|
1600
|
-
}
|
|
1601
|
-
log(" " + a.dim + parts.join(a.reset + a.dim + " · ") + a.reset);
|
|
1602
|
-
log(" Press " + a.bold + "o" + a.reset + " to open in browser");
|
|
1603
|
-
log("");
|
|
1604
|
-
|
|
1605
|
-
showMenuItems();
|
|
1606
|
-
}
|
|
1607
|
-
|
|
1608
|
-
if (ip !== "localhost") {
|
|
1609
|
-
qrcode.generate(url, { small: !isBasicTerm }, function (code) {
|
|
1610
|
-
var lines = code.split("\n").map(function (l) { return " " + l; }).join("\n");
|
|
1611
|
-
console.log(lines);
|
|
1612
|
-
afterQr();
|
|
1613
|
-
});
|
|
1614
|
-
} else {
|
|
1615
|
-
log(a.bold + " " + url + a.reset);
|
|
1616
|
-
log("");
|
|
1617
|
-
afterQr();
|
|
1618
|
-
}
|
|
1619
|
-
|
|
1620
|
-
function showMenuItems() {
|
|
1621
|
-
var items = [
|
|
1622
|
-
{ label: "Setup notifications", value: "notifications" },
|
|
1623
|
-
{ label: "Projects", value: "projects" },
|
|
1624
|
-
{ label: "Settings", value: "settings" },
|
|
1625
|
-
{ label: "Shut down server", value: "shutdown" },
|
|
1626
|
-
{ label: "Keep server alive & exit", value: "exit" },
|
|
1627
|
-
];
|
|
1628
|
-
|
|
1629
|
-
promptSelect("What would you like to do?", items, function (choice) {
|
|
1630
|
-
switch (choice) {
|
|
1631
|
-
case "notifications":
|
|
1632
|
-
showSetupGuide(config, ip, function () {
|
|
1633
|
-
config = loadConfig() || config;
|
|
1634
|
-
showMainMenu(config, ip);
|
|
1635
|
-
});
|
|
1636
|
-
break;
|
|
1637
|
-
|
|
1638
|
-
case "projects":
|
|
1639
|
-
showProjectsMenu(config, ip);
|
|
1640
|
-
break;
|
|
1641
|
-
|
|
1642
|
-
case "settings":
|
|
1643
|
-
showSettingsMenu(config, ip);
|
|
1644
|
-
break;
|
|
1645
|
-
|
|
1646
|
-
case "shutdown":
|
|
1647
|
-
log(sym.bar);
|
|
1648
|
-
log(sym.bar + " " + a.yellow + "This will stop the server completely." + a.reset);
|
|
1649
|
-
log(sym.bar + " " + a.dim + "All connected sessions will be disconnected." + a.reset);
|
|
1650
|
-
log(sym.bar);
|
|
1651
|
-
promptSelect("Are you sure?", [
|
|
1652
|
-
{ label: "Cancel", value: "cancel" },
|
|
1653
|
-
{ label: "Shut down", value: "confirm" },
|
|
1654
|
-
], function (confirm) {
|
|
1655
|
-
if (confirm === "confirm") {
|
|
1656
|
-
stopDaemonWatcher();
|
|
1657
|
-
sendIPCCommand(socketPath(), { cmd: "shutdown" }).then(function () {
|
|
1658
|
-
log(sym.done + " " + a.green + "Server stopped." + a.reset);
|
|
1659
|
-
log("");
|
|
1660
|
-
clearStaleConfig();
|
|
1661
|
-
process.exit(0);
|
|
1662
|
-
});
|
|
1663
|
-
} else {
|
|
1664
|
-
showMainMenu(config, ip);
|
|
1665
|
-
}
|
|
1666
|
-
});
|
|
1667
|
-
break;
|
|
1668
|
-
|
|
1669
|
-
case "exit":
|
|
1670
|
-
log("");
|
|
1671
|
-
log(" " + a.bold + "Bye!" + a.reset + " " + a.dim + "Server is still running in background." + a.reset);
|
|
1672
|
-
log(" " + a.dim + "Run " + a.reset + "npx claude-relay" + a.dim + " to come back here." + a.reset);
|
|
1673
|
-
log("");
|
|
1674
|
-
process.exit(0);
|
|
1675
|
-
break;
|
|
1676
|
-
}
|
|
1677
|
-
}, {
|
|
1678
|
-
hint: [
|
|
1679
|
-
"Run npx claude-relay in other directories to add more projects.",
|
|
1680
|
-
"★ github.com/chadbyte/claude-relay — Press s to star the repo",
|
|
1681
|
-
],
|
|
1682
|
-
keys: [
|
|
1683
|
-
{ key: "o", onKey: function () {
|
|
1684
|
-
openUrl(url);
|
|
1685
|
-
showMainMenu(config, ip);
|
|
1686
|
-
}},
|
|
1687
|
-
{ key: "s", onKey: function () {
|
|
1688
|
-
openUrl("https://github.com/chadbyte/claude-relay");
|
|
1689
|
-
showMainMenu(config, ip);
|
|
1690
|
-
}},
|
|
1691
|
-
],
|
|
1692
|
-
});
|
|
1693
|
-
}
|
|
1694
|
-
});
|
|
1695
|
-
}
|
|
1696
|
-
|
|
1697
|
-
// ==============================
|
|
1698
|
-
// Projects sub-menu
|
|
1699
|
-
// ==============================
|
|
1700
|
-
function showProjectsMenu(config, ip) {
|
|
1701
|
-
sendIPCCommand(socketPath(), { cmd: "get_status" }).then(function (status) {
|
|
1702
|
-
if (!status.ok) {
|
|
1703
|
-
log(a.red + "Failed to get status" + a.reset);
|
|
1704
|
-
showMainMenu(config, ip);
|
|
1705
|
-
return;
|
|
1706
|
-
}
|
|
1707
|
-
|
|
1708
|
-
console.clear();
|
|
1709
|
-
printLogo();
|
|
1710
|
-
log("");
|
|
1711
|
-
log(sym.pointer + " " + a.bold + "Projects" + a.reset);
|
|
1712
|
-
log(sym.bar);
|
|
1713
|
-
|
|
1714
|
-
var projs = status.projects || [];
|
|
1715
|
-
for (var i = 0; i < projs.length; i++) {
|
|
1716
|
-
var p = projs[i];
|
|
1717
|
-
var statusIcon = p.isProcessing ? "⚡" : (p.clients > 0 ? "🟢" : "⏸");
|
|
1718
|
-
var sessionLabel = p.sessions === 1 ? "1 session" : p.sessions + " sessions";
|
|
1719
|
-
var projName = p.title || p.project;
|
|
1720
|
-
log(sym.bar + " " + a.bold + projName + a.reset + " " + sessionLabel + " " + statusIcon);
|
|
1721
|
-
log(sym.bar + " " + a.dim + p.path + a.reset);
|
|
1722
|
-
if (i < projs.length - 1) log(sym.bar);
|
|
1723
|
-
}
|
|
1724
|
-
log(sym.bar);
|
|
1725
|
-
|
|
1726
|
-
// Build menu items
|
|
1727
|
-
var items = [];
|
|
1728
|
-
|
|
1729
|
-
// Check if cwd is already registered
|
|
1730
|
-
var cwdRegistered = false;
|
|
1731
|
-
for (var j = 0; j < projs.length; j++) {
|
|
1732
|
-
if (projs[j].path === cwd) {
|
|
1733
|
-
cwdRegistered = true;
|
|
1734
|
-
break;
|
|
1735
|
-
}
|
|
1736
|
-
}
|
|
1737
|
-
if (!cwdRegistered) {
|
|
1738
|
-
items.push({ label: "+ Add " + a.bold + path.basename(cwd) + a.reset + " " + a.dim + "(" + cwd + ")" + a.reset, value: "add_cwd" });
|
|
1739
|
-
}
|
|
1740
|
-
items.push({ label: "+ Add project...", value: "add_other" });
|
|
1741
|
-
|
|
1742
|
-
for (var k = 0; k < projs.length; k++) {
|
|
1743
|
-
var itemLabel = projs[k].title || projs[k].project;
|
|
1744
|
-
items.push({ label: itemLabel, value: "detail:" + projs[k].slug });
|
|
1745
|
-
}
|
|
1746
|
-
items.push({ label: "Back", value: "back" });
|
|
1747
|
-
|
|
1748
|
-
promptSelect("Select", items, function (choice) {
|
|
1749
|
-
if (choice === "back") {
|
|
1750
|
-
console.clear();
|
|
1751
|
-
printLogo();
|
|
1752
|
-
log("");
|
|
1753
|
-
showMainMenu(config, ip);
|
|
1754
|
-
} else if (choice === "add_cwd") {
|
|
1755
|
-
sendIPCCommand(socketPath(), { cmd: "add_project", path: cwd }).then(function (res) {
|
|
1756
|
-
if (res.ok) {
|
|
1757
|
-
log(sym.done + " " + a.green + "Added: " + res.slug + a.reset);
|
|
1758
|
-
config = loadConfig() || config;
|
|
1759
|
-
} else {
|
|
1760
|
-
log(sym.warn + " " + a.yellow + (res.error || "Failed") + a.reset);
|
|
1761
|
-
}
|
|
1762
|
-
log("");
|
|
1763
|
-
showProjectsMenu(config, ip);
|
|
1764
|
-
});
|
|
1765
|
-
} else if (choice === "add_other") {
|
|
1766
|
-
log(sym.bar);
|
|
1767
|
-
promptText("Directory path", cwd, function (dirPath) {
|
|
1768
|
-
if (dirPath === null) {
|
|
1769
|
-
showProjectsMenu(config, ip);
|
|
1770
|
-
return;
|
|
1771
|
-
}
|
|
1772
|
-
var absPath = path.resolve(dirPath);
|
|
1773
|
-
try {
|
|
1774
|
-
var stat = fs.statSync(absPath);
|
|
1775
|
-
if (!stat.isDirectory()) {
|
|
1776
|
-
log(sym.warn + " " + a.red + "Not a directory: " + absPath + a.reset);
|
|
1777
|
-
setTimeout(function () { showProjectsMenu(config, ip); }, 2000);
|
|
1778
|
-
return;
|
|
1779
|
-
}
|
|
1780
|
-
} catch (e) {
|
|
1781
|
-
log(sym.warn + " " + a.red + "Directory not found: " + absPath + a.reset);
|
|
1782
|
-
setTimeout(function () { showProjectsMenu(config, ip); }, 2000);
|
|
1783
|
-
return;
|
|
1784
|
-
}
|
|
1785
|
-
var alreadyExists = false;
|
|
1786
|
-
for (var pi = 0; pi < projs.length; pi++) {
|
|
1787
|
-
if (projs[pi].path === absPath) {
|
|
1788
|
-
alreadyExists = true;
|
|
1789
|
-
break;
|
|
1790
|
-
}
|
|
1791
|
-
}
|
|
1792
|
-
if (alreadyExists) {
|
|
1793
|
-
log(sym.done + " " + a.yellow + "Already added: " + path.basename(absPath) + a.reset + " " + a.dim + "(" + absPath + ")" + a.reset);
|
|
1794
|
-
setTimeout(function () { showProjectsMenu(config, ip); }, 2000);
|
|
1795
|
-
return;
|
|
1796
|
-
}
|
|
1797
|
-
sendIPCCommand(socketPath(), { cmd: "add_project", path: absPath }).then(function (res) {
|
|
1798
|
-
if (res.ok) {
|
|
1799
|
-
log(sym.done + " " + a.green + "Added: " + res.slug + a.reset + " " + a.dim + "(" + absPath + ")" + a.reset);
|
|
1800
|
-
config = loadConfig() || config;
|
|
1801
|
-
} else {
|
|
1802
|
-
log(sym.warn + " " + a.yellow + (res.error || "Failed") + a.reset);
|
|
1803
|
-
}
|
|
1804
|
-
setTimeout(function () { showProjectsMenu(config, ip); }, 2000);
|
|
1805
|
-
});
|
|
1806
|
-
});
|
|
1807
|
-
} else if (choice.startsWith("detail:")) {
|
|
1808
|
-
var detailSlug = choice.substring(7);
|
|
1809
|
-
showProjectDetail(config, ip, detailSlug, projs);
|
|
1810
|
-
}
|
|
1811
|
-
});
|
|
1812
|
-
});
|
|
1813
|
-
}
|
|
1814
|
-
|
|
1815
|
-
// ==============================
|
|
1816
|
-
// Project detail
|
|
1817
|
-
// ==============================
|
|
1818
|
-
function showProjectDetail(config, ip, slug, projects) {
|
|
1819
|
-
var proj = null;
|
|
1820
|
-
for (var i = 0; i < projects.length; i++) {
|
|
1821
|
-
if (projects[i].slug === slug) {
|
|
1822
|
-
proj = projects[i];
|
|
1823
|
-
break;
|
|
1824
|
-
}
|
|
1825
|
-
}
|
|
1826
|
-
if (!proj) {
|
|
1827
|
-
showProjectsMenu(config, ip);
|
|
1828
|
-
return;
|
|
1829
|
-
}
|
|
1830
|
-
|
|
1831
|
-
var displayName = proj.title || proj.project;
|
|
1832
|
-
|
|
1833
|
-
console.clear();
|
|
1834
|
-
printLogo();
|
|
1835
|
-
log("");
|
|
1836
|
-
log(sym.pointer + " " + a.bold + displayName + a.reset + " " + a.dim + proj.slug + " · " + proj.path + a.reset);
|
|
1837
|
-
log(sym.bar);
|
|
1838
|
-
var sessionLabel = proj.sessions === 1 ? "1 session" : proj.sessions + " sessions";
|
|
1839
|
-
var clientLabel = proj.clients === 1 ? "1 client" : proj.clients + " clients";
|
|
1840
|
-
log(sym.bar + " " + sessionLabel + " · " + clientLabel);
|
|
1841
|
-
if (proj.title) {
|
|
1842
|
-
log(sym.bar + " " + a.dim + "Title: " + a.reset + proj.title);
|
|
1843
|
-
}
|
|
1844
|
-
log(sym.bar);
|
|
1845
|
-
|
|
1846
|
-
var items = [
|
|
1847
|
-
{ label: proj.title ? "Change title" : "Set title", value: "title" },
|
|
1848
|
-
{ label: "Remove project", value: "remove" },
|
|
1849
|
-
{ label: "Back", value: "back" },
|
|
1850
|
-
];
|
|
1851
|
-
|
|
1852
|
-
promptSelect("What would you like to do?", items, function (choice) {
|
|
1853
|
-
if (choice === "title") {
|
|
1854
|
-
log(sym.bar);
|
|
1855
|
-
promptText("Project title", proj.title || proj.project, function (newTitle) {
|
|
1856
|
-
if (newTitle === null) {
|
|
1857
|
-
showProjectDetail(config, ip, slug, projects);
|
|
1858
|
-
return;
|
|
1859
|
-
}
|
|
1860
|
-
var titleVal = newTitle.trim();
|
|
1861
|
-
// If same as directory name, clear custom title
|
|
1862
|
-
if (titleVal === proj.project || titleVal === "") {
|
|
1863
|
-
titleVal = null;
|
|
1864
|
-
}
|
|
1865
|
-
sendIPCCommand(socketPath(), { cmd: "set_project_title", slug: slug, title: titleVal }).then(function (res) {
|
|
1866
|
-
if (res.ok) {
|
|
1867
|
-
proj.title = titleVal;
|
|
1868
|
-
config = loadConfig() || config;
|
|
1869
|
-
log(sym.done + " " + a.green + "Title updated" + a.reset);
|
|
1870
|
-
} else {
|
|
1871
|
-
log(sym.warn + " " + a.yellow + (res.error || "Failed") + a.reset);
|
|
1872
|
-
}
|
|
1873
|
-
log("");
|
|
1874
|
-
showProjectDetail(config, ip, slug, projects);
|
|
1875
|
-
});
|
|
1876
|
-
});
|
|
1877
|
-
} else if (choice === "remove") {
|
|
1878
|
-
sendIPCCommand(socketPath(), { cmd: "remove_project", slug: slug }).then(function (res) {
|
|
1879
|
-
if (res.ok) {
|
|
1880
|
-
log(sym.done + " " + a.green + "Removed: " + slug + a.reset);
|
|
1881
|
-
config = loadConfig() || config;
|
|
1882
|
-
} else {
|
|
1883
|
-
log(sym.warn + " " + a.yellow + (res.error || "Failed") + a.reset);
|
|
1884
|
-
}
|
|
1885
|
-
log("");
|
|
1886
|
-
showProjectsMenu(config, ip);
|
|
1887
|
-
});
|
|
1888
|
-
} else {
|
|
1889
|
-
showProjectsMenu(config, ip);
|
|
1890
|
-
}
|
|
1891
|
-
});
|
|
1892
|
-
}
|
|
1893
|
-
|
|
1894
|
-
// ==============================
|
|
1895
|
-
// Setup guide (2x2 toggle flow)
|
|
1896
|
-
// ==============================
|
|
1897
|
-
function showSetupGuide(config, ip, goBack) {
|
|
1898
|
-
var protocol = config.tls ? "https" : "http";
|
|
1899
|
-
var wantRemote = false;
|
|
1900
|
-
var wantPush = false;
|
|
1901
|
-
|
|
1902
|
-
console.clear();
|
|
1903
|
-
printLogo();
|
|
1904
|
-
log("");
|
|
1905
|
-
log(sym.pointer + " " + a.bold + "Setup Notifications" + a.reset);
|
|
1906
|
-
log(sym.bar);
|
|
1907
|
-
|
|
1908
|
-
function redraw(renderFn) {
|
|
1909
|
-
console.clear();
|
|
1910
|
-
printLogo();
|
|
1911
|
-
log("");
|
|
1912
|
-
log(sym.pointer + " " + a.bold + "Setup Notifications" + a.reset);
|
|
1913
|
-
log(sym.bar);
|
|
1914
|
-
if (wantRemote) log(sym.done + " Access from outside your network? " + a.dim + "·" + a.reset + " " + a.green + "Yes" + a.reset);
|
|
1915
|
-
else log(sym.done + " Access from outside your network? " + a.dim + "· No" + a.reset);
|
|
1916
|
-
log(sym.bar);
|
|
1917
|
-
if (wantPush) log(sym.done + " Want push notifications? " + a.dim + "·" + a.reset + " " + a.green + "Yes" + a.reset);
|
|
1918
|
-
else log(sym.done + " Want push notifications? " + a.dim + "· No" + a.reset);
|
|
1919
|
-
log(sym.bar);
|
|
1920
|
-
renderFn();
|
|
1921
|
-
}
|
|
1922
|
-
|
|
1923
|
-
promptToggle("Access from outside your network?", "Requires Tailscale on both devices", false, function (remote) {
|
|
1924
|
-
wantRemote = remote;
|
|
1925
|
-
log(sym.bar);
|
|
1926
|
-
promptToggle("Want push notifications?", "Requires HTTPS (mkcert certificate)", false, function (push) {
|
|
1927
|
-
wantPush = push;
|
|
1928
|
-
log(sym.bar);
|
|
1929
|
-
afterToggles();
|
|
1930
|
-
});
|
|
1931
|
-
});
|
|
1932
|
-
|
|
1933
|
-
function afterToggles() {
|
|
1934
|
-
if (!wantRemote && !wantPush) {
|
|
1935
|
-
log(sym.done + " " + a.green + "All set!" + a.reset + a.dim + " · No additional setup needed." + a.reset);
|
|
1936
|
-
log(sym.end);
|
|
1937
|
-
log("");
|
|
1938
|
-
promptSelect("Back?", [{ label: "Back", value: "back" }], function () {
|
|
1939
|
-
goBack();
|
|
1940
|
-
});
|
|
1941
|
-
return;
|
|
1942
|
-
}
|
|
1943
|
-
if (wantRemote) {
|
|
1944
|
-
renderTailscale();
|
|
1945
|
-
} else {
|
|
1946
|
-
renderHttps();
|
|
1947
|
-
}
|
|
1948
|
-
}
|
|
1949
|
-
|
|
1950
|
-
function renderTailscale() {
|
|
1951
|
-
var tsIP = getTailscaleIP();
|
|
1952
|
-
|
|
1953
|
-
log(sym.pointer + " " + a.bold + "Tailscale Setup" + a.reset);
|
|
1954
|
-
if (tsIP) {
|
|
1955
|
-
log(sym.bar + " " + a.green + "Tailscale is running" + a.reset + a.dim + " · " + tsIP + a.reset);
|
|
1956
|
-
log(sym.bar);
|
|
1957
|
-
log(sym.bar + " On your phone/tablet:");
|
|
1958
|
-
log(sym.bar + " " + a.dim + "1. Install Tailscale (App Store / Google Play)" + a.reset);
|
|
1959
|
-
log(sym.bar + " " + a.dim + "2. Sign in with the same account" + a.reset);
|
|
1960
|
-
log(sym.bar);
|
|
1961
|
-
renderHttps();
|
|
1962
|
-
} else {
|
|
1963
|
-
log(sym.bar + " " + a.yellow + "Tailscale not found on this machine." + a.reset);
|
|
1964
|
-
log(sym.bar + " " + a.dim + "Install: " + a.reset + "https://tailscale.com/download");
|
|
1965
|
-
log(sym.bar + " " + a.dim + "Then run: " + a.reset + "tailscale up");
|
|
1966
|
-
log(sym.bar);
|
|
1967
|
-
log(sym.bar + " On your phone/tablet:");
|
|
1968
|
-
log(sym.bar + " " + a.dim + "1. Install Tailscale (App Store / Google Play)" + a.reset);
|
|
1969
|
-
log(sym.bar + " " + a.dim + "2. Sign in with the same account" + a.reset);
|
|
1970
|
-
log(sym.bar);
|
|
1971
|
-
promptSelect("Select", [
|
|
1972
|
-
{ label: "Re-check", value: "recheck" },
|
|
1973
|
-
{ label: "Back", value: "back" },
|
|
1974
|
-
], function (choice) {
|
|
1975
|
-
if (choice === "recheck") {
|
|
1976
|
-
redraw(renderTailscale);
|
|
1977
|
-
} else {
|
|
1978
|
-
goBack();
|
|
1979
|
-
}
|
|
1980
|
-
});
|
|
1981
|
-
}
|
|
1982
|
-
}
|
|
1983
|
-
|
|
1984
|
-
function renderHttps() {
|
|
1985
|
-
if (!wantPush) {
|
|
1986
|
-
showSetupQR();
|
|
1987
|
-
return;
|
|
1988
|
-
}
|
|
1989
|
-
|
|
1990
|
-
var mcReady = hasMkcert();
|
|
1991
|
-
log(sym.pointer + " " + a.bold + "HTTPS Setup (for push notifications)" + a.reset);
|
|
1992
|
-
if (mcReady) {
|
|
1993
|
-
log(sym.bar + " " + a.green + "mkcert is installed" + a.reset);
|
|
1994
|
-
if (!config.tls) {
|
|
1995
|
-
log(sym.bar + " " + a.dim + "Restarting server with HTTPS..." + a.reset);
|
|
1996
|
-
restartDaemonWithTLS(config, function (newConfig) {
|
|
1997
|
-
config = newConfig;
|
|
1998
|
-
log(sym.bar);
|
|
1999
|
-
showSetupQR();
|
|
2000
|
-
});
|
|
2001
|
-
return;
|
|
2002
|
-
}
|
|
2003
|
-
log(sym.bar);
|
|
2004
|
-
showSetupQR();
|
|
2005
|
-
} else {
|
|
2006
|
-
log(sym.bar + " " + a.yellow + "mkcert not found." + a.reset);
|
|
2007
|
-
var mkcertHint = process.platform === "win32"
|
|
2008
|
-
? "choco install mkcert && mkcert -install"
|
|
2009
|
-
: process.platform === "darwin"
|
|
2010
|
-
? "brew install mkcert && mkcert -install"
|
|
2011
|
-
: "apt install mkcert && mkcert -install";
|
|
2012
|
-
log(sym.bar + " " + a.dim + "Install: " + a.reset + mkcertHint);
|
|
2013
|
-
log(sym.bar);
|
|
2014
|
-
promptSelect("Select", [
|
|
2015
|
-
{ label: "Re-check", value: "recheck" },
|
|
2016
|
-
{ label: "Back", value: "back" },
|
|
2017
|
-
], function (choice) {
|
|
2018
|
-
if (choice === "recheck") {
|
|
2019
|
-
redraw(renderHttps);
|
|
2020
|
-
} else {
|
|
2021
|
-
goBack();
|
|
2022
|
-
}
|
|
2023
|
-
});
|
|
2024
|
-
}
|
|
2025
|
-
}
|
|
2026
|
-
|
|
2027
|
-
function showSetupQR() {
|
|
2028
|
-
var tsIP = getTailscaleIP();
|
|
2029
|
-
var lanIP = null;
|
|
2030
|
-
if (!wantRemote) {
|
|
2031
|
-
var allIPs = getAllIPs();
|
|
2032
|
-
for (var j = 0; j < allIPs.length; j++) {
|
|
2033
|
-
if (!allIPs[j].startsWith("100.")) { lanIP = allIPs[j]; break; }
|
|
2034
|
-
}
|
|
2035
|
-
}
|
|
2036
|
-
var setupIP = wantRemote ? (tsIP || ip) : (lanIP || ip);
|
|
2037
|
-
var setupQuery = wantRemote ? "" : "?mode=lan";
|
|
2038
|
-
// Always use HTTP onboarding URL for QR/setup when TLS is active
|
|
2039
|
-
var setupUrl = config.tls
|
|
2040
|
-
? "http://" + setupIP + ":" + (config.port + 1) + "/setup" + setupQuery
|
|
2041
|
-
: "http://" + setupIP + ":" + config.port + "/setup" + setupQuery;
|
|
2042
|
-
log(sym.pointer + " " + a.bold + "Continue on your device" + a.reset);
|
|
2043
|
-
log(sym.bar + " " + a.dim + "Scan the QR code or open:" + a.reset);
|
|
2044
|
-
log(sym.bar + " " + a.bold + setupUrl + a.reset);
|
|
2045
|
-
log(sym.bar);
|
|
2046
|
-
qrcode.generate(setupUrl, { small: !isBasicTerm }, function (code) {
|
|
2047
|
-
var lines = code.split("\n").map(function (l) { return " " + sym.bar + " " + l; }).join("\n");
|
|
2048
|
-
console.log(lines);
|
|
2049
|
-
log(sym.bar);
|
|
2050
|
-
if (wantRemote) {
|
|
2051
|
-
log(sym.bar + " " + a.dim + "Can't connect? Make sure Tailscale is installed on your phone too." + a.reset);
|
|
2052
|
-
} else {
|
|
2053
|
-
log(sym.bar + " " + a.dim + "Can't connect? Your phone must be on the same Wi-Fi network." + a.reset);
|
|
2054
|
-
}
|
|
2055
|
-
log(sym.bar);
|
|
2056
|
-
log(sym.done + " " + a.dim + "Setup complete." + a.reset);
|
|
2057
|
-
log(sym.end);
|
|
2058
|
-
log("");
|
|
2059
|
-
promptSelect("Back?", [{ label: "Back", value: "back" }], function () {
|
|
2060
|
-
goBack();
|
|
2061
|
-
});
|
|
2062
|
-
});
|
|
2063
|
-
}
|
|
2064
|
-
}
|
|
2065
|
-
|
|
2066
|
-
// ==============================
|
|
2067
|
-
// Settings sub-menu
|
|
2068
|
-
// ==============================
|
|
2069
|
-
function showSettingsMenu(config, ip) {
|
|
2070
|
-
sendIPCCommand(socketPath(), { cmd: "get_status" }).then(function (status) {
|
|
2071
|
-
var isAwake = status && status.keepAwake;
|
|
2072
|
-
|
|
2073
|
-
console.clear();
|
|
2074
|
-
printLogo();
|
|
2075
|
-
log("");
|
|
2076
|
-
log(sym.pointer + " " + a.bold + "Settings" + a.reset);
|
|
2077
|
-
log(sym.bar);
|
|
2078
|
-
|
|
2079
|
-
// Detect current state
|
|
2080
|
-
var tsIP = getTailscaleIP();
|
|
2081
|
-
var tsOk = tsIP !== null;
|
|
2082
|
-
var mcOk = hasMkcert();
|
|
2083
|
-
|
|
2084
|
-
var tsStatus = tsOk
|
|
2085
|
-
? a.green + "Connected" + a.reset + a.dim + " · " + tsIP + a.reset
|
|
2086
|
-
: a.dim + "Not detected" + a.reset;
|
|
2087
|
-
var mcStatus = mcOk
|
|
2088
|
-
? a.green + "Installed" + a.reset
|
|
2089
|
-
: a.dim + "Not found" + a.reset;
|
|
2090
|
-
var tlsStatus = config.tls
|
|
2091
|
-
? a.green + "Enabled" + a.reset
|
|
2092
|
-
: a.dim + "Disabled" + a.reset;
|
|
2093
|
-
var pinStatus = config.pinHash
|
|
2094
|
-
? a.green + "Enabled" + a.reset
|
|
2095
|
-
: a.dim + "Off" + a.reset;
|
|
2096
|
-
var awakeStatus = isAwake
|
|
2097
|
-
? a.green + "On" + a.reset
|
|
2098
|
-
: a.dim + "Off" + a.reset;
|
|
2099
|
-
|
|
2100
|
-
log(sym.bar + " Tailscale " + tsStatus);
|
|
2101
|
-
log(sym.bar + " mkcert " + mcStatus);
|
|
2102
|
-
log(sym.bar + " HTTPS " + tlsStatus);
|
|
2103
|
-
log(sym.bar + " PIN " + pinStatus);
|
|
2104
|
-
if (process.platform === "darwin") {
|
|
2105
|
-
log(sym.bar + " Keep awake " + awakeStatus);
|
|
2106
|
-
}
|
|
2107
|
-
log(sym.bar);
|
|
2108
|
-
|
|
2109
|
-
// Build items
|
|
2110
|
-
var items = [
|
|
2111
|
-
{ label: "Setup notifications", value: "guide" },
|
|
2112
|
-
];
|
|
2113
|
-
|
|
2114
|
-
if (config.pinHash) {
|
|
2115
|
-
items.push({ label: "Change PIN", value: "pin" });
|
|
2116
|
-
items.push({ label: "Remove PIN", value: "remove_pin" });
|
|
2117
|
-
} else {
|
|
2118
|
-
items.push({ label: "Set PIN", value: "pin" });
|
|
2119
|
-
}
|
|
2120
|
-
if (process.platform === "darwin") {
|
|
2121
|
-
items.push({ label: isAwake ? "Disable keep awake" : "Enable keep awake", value: "awake" });
|
|
2122
|
-
}
|
|
2123
|
-
items.push({ label: "View logs", value: "logs" });
|
|
2124
|
-
items.push({ label: "Back", value: "back" });
|
|
2125
|
-
|
|
2126
|
-
promptSelect("Select", items, function (choice) {
|
|
2127
|
-
switch (choice) {
|
|
2128
|
-
case "guide":
|
|
2129
|
-
showSetupGuide(config, ip, function () {
|
|
2130
|
-
config = loadConfig() || config;
|
|
2131
|
-
showSettingsMenu(config, ip);
|
|
2132
|
-
});
|
|
2133
|
-
break;
|
|
2134
|
-
|
|
2135
|
-
case "pin":
|
|
2136
|
-
log(sym.bar);
|
|
2137
|
-
promptPin(function (pin) {
|
|
2138
|
-
if (pin) {
|
|
2139
|
-
var hash = generateAuthToken(pin);
|
|
2140
|
-
sendIPCCommand(socketPath(), { cmd: "set_pin", pinHash: hash }).then(function () {
|
|
2141
|
-
config.pinHash = hash;
|
|
2142
|
-
log(sym.done + " " + a.green + "PIN updated" + a.reset);
|
|
2143
|
-
log("");
|
|
2144
|
-
showSettingsMenu(config, ip);
|
|
2145
|
-
});
|
|
2146
|
-
} else {
|
|
2147
|
-
showSettingsMenu(config, ip);
|
|
2148
|
-
}
|
|
2149
|
-
});
|
|
2150
|
-
break;
|
|
2151
|
-
|
|
2152
|
-
case "remove_pin":
|
|
2153
|
-
sendIPCCommand(socketPath(), { cmd: "set_pin", pinHash: null }).then(function () {
|
|
2154
|
-
config.pinHash = null;
|
|
2155
|
-
log(sym.done + " " + a.dim + "PIN removed" + a.reset);
|
|
2156
|
-
log("");
|
|
2157
|
-
showSettingsMenu(config, ip);
|
|
2158
|
-
});
|
|
2159
|
-
break;
|
|
2160
|
-
|
|
2161
|
-
case "logs":
|
|
2162
|
-
console.clear();
|
|
2163
|
-
log(a.bold + "Daemon logs" + a.reset + " " + a.dim + "(" + logPath() + ")" + a.reset);
|
|
2164
|
-
log("");
|
|
2165
|
-
try {
|
|
2166
|
-
var logContent = fs.readFileSync(logPath(), "utf8");
|
|
2167
|
-
var logLines = logContent.split("\n").slice(-30);
|
|
2168
|
-
for (var li = 0; li < logLines.length; li++) {
|
|
2169
|
-
log(a.dim + logLines[li] + a.reset);
|
|
2170
|
-
}
|
|
2171
|
-
} catch (e) {
|
|
2172
|
-
log(a.dim + "(empty)" + a.reset);
|
|
2173
|
-
}
|
|
2174
|
-
log("");
|
|
2175
|
-
promptSelect("Back?", [{ label: "Back", value: "back" }], function () {
|
|
2176
|
-
showSettingsMenu(config, ip);
|
|
2177
|
-
});
|
|
2178
|
-
break;
|
|
2179
|
-
|
|
2180
|
-
case "awake":
|
|
2181
|
-
sendIPCCommand(socketPath(), { cmd: "set_keep_awake", value: !isAwake }).then(function (res) {
|
|
2182
|
-
if (res.ok) {
|
|
2183
|
-
config.keepAwake = !isAwake;
|
|
2184
|
-
}
|
|
2185
|
-
showSettingsMenu(config, ip);
|
|
2186
|
-
});
|
|
2187
|
-
break;
|
|
2188
|
-
|
|
2189
|
-
case "back":
|
|
2190
|
-
showMainMenu(config, ip);
|
|
2191
|
-
break;
|
|
2192
|
-
}
|
|
2193
|
-
});
|
|
2194
|
-
});
|
|
2195
|
-
}
|
|
2196
|
-
|
|
2197
|
-
// ==============================
|
|
2198
|
-
// Main entry: daemon alive?
|
|
2199
|
-
// ==============================
|
|
2200
|
-
var { checkAndUpdate } = require("../lib/updater");
|
|
2201
|
-
var currentVersion = require("../package.json").version;
|
|
2202
|
-
|
|
2203
|
-
(async function () {
|
|
2204
|
-
var updated = await checkAndUpdate(currentVersion, skipUpdate);
|
|
2205
|
-
if (updated) return;
|
|
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
|
-
|
|
2229
|
-
var config = loadConfig();
|
|
2230
|
-
var alive = config ? await isDaemonAliveAsync(config) : false;
|
|
2231
|
-
|
|
2232
|
-
if (!alive && config && config.pid) {
|
|
2233
|
-
// Stale config
|
|
2234
|
-
clearStaleConfig();
|
|
2235
|
-
config = null;
|
|
2236
|
-
}
|
|
2237
|
-
|
|
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
|
-
|
|
2250
|
-
// Daemon is running — auto-add cwd if needed, then show menu
|
|
2251
|
-
var ip = getLocalIP();
|
|
2252
|
-
|
|
2253
|
-
var status = await sendIPCCommand(socketPath(), { cmd: "get_status" });
|
|
2254
|
-
if (!status.ok) {
|
|
2255
|
-
log(a.red + "Daemon not responding" + a.reset);
|
|
2256
|
-
clearStaleConfig();
|
|
2257
|
-
process.exit(1);
|
|
2258
|
-
return;
|
|
2259
|
-
}
|
|
2260
|
-
|
|
2261
|
-
// Check if cwd needs to be added
|
|
2262
|
-
var projs = status.projects || [];
|
|
2263
|
-
var cwdRegistered = false;
|
|
2264
|
-
for (var j = 0; j < projs.length; j++) {
|
|
2265
|
-
if (projs[j].path === cwd) {
|
|
2266
|
-
cwdRegistered = true;
|
|
2267
|
-
break;
|
|
2268
|
-
}
|
|
2269
|
-
}
|
|
2270
|
-
|
|
2271
|
-
if (!cwdRegistered) {
|
|
2272
|
-
var slug = path.basename(cwd).toLowerCase().replace(/[^a-z0-9_-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") || "project";
|
|
2273
|
-
console.clear();
|
|
2274
|
-
printLogo();
|
|
2275
|
-
log("");
|
|
2276
|
-
log(sym.pointer + " " + a.bold + "Add this project?" + a.reset);
|
|
2277
|
-
log(sym.bar);
|
|
2278
|
-
log(sym.bar + " " + a.dim + cwd + a.reset);
|
|
2279
|
-
log(sym.bar);
|
|
2280
|
-
promptSelect("Add " + a.green + slug + a.reset + " to relay?", [
|
|
2281
|
-
{ label: "Yes", value: "yes" },
|
|
2282
|
-
{ label: "No", value: "no" },
|
|
2283
|
-
], function (answer) {
|
|
2284
|
-
if (answer === "yes") {
|
|
2285
|
-
sendIPCCommand(socketPath(), { cmd: "add_project", path: cwd }).then(function (res) {
|
|
2286
|
-
if (res.ok) {
|
|
2287
|
-
config = loadConfig() || config;
|
|
2288
|
-
log(sym.done + " " + a.green + "Added: " + (res.slug || slug) + a.reset);
|
|
2289
|
-
}
|
|
2290
|
-
log("");
|
|
2291
|
-
showMainMenu(config || { pid: status.pid, port: status.port, tls: status.tls }, ip);
|
|
2292
|
-
});
|
|
2293
|
-
} else {
|
|
2294
|
-
showMainMenu(config || { pid: status.pid, port: status.port, tls: status.tls }, ip);
|
|
2295
|
-
}
|
|
2296
|
-
});
|
|
2297
|
-
} else {
|
|
2298
|
-
showMainMenu(config || { pid: status.pid, port: status.port, tls: status.tls }, ip);
|
|
2299
|
-
}
|
|
2300
|
-
} else {
|
|
2301
|
-
// No daemon running — first-time setup
|
|
2302
|
-
if (autoYes) {
|
|
2303
|
-
var pin = cliPin || null;
|
|
2304
|
-
if (dangerouslySkipPermissions && !pin) {
|
|
2305
|
-
console.error(" " + sym.warn + " " + a.red + "--dangerously-skip-permissions requires --pin <pin>" + a.reset);
|
|
2306
|
-
process.exit(1);
|
|
2307
|
-
return;
|
|
2308
|
-
}
|
|
2309
|
-
console.log(" " + sym.done + " Auto-accepted disclaimer");
|
|
2310
|
-
console.log(" " + sym.done + " PIN: " + (pin ? "Enabled" : "Skipped"));
|
|
2311
|
-
if (dangerouslySkipPermissions) {
|
|
2312
|
-
console.log(" " + sym.warn + " " + a.yellow + "Skip permissions mode enabled" + a.reset);
|
|
2313
|
-
}
|
|
2314
|
-
var autoRc = loadClayrc();
|
|
2315
|
-
var autoRestorable = (autoRc.recentProjects || []).filter(function (p) {
|
|
2316
|
-
return p.path !== cwd && fs.existsSync(p.path);
|
|
2317
|
-
});
|
|
2318
|
-
if (autoRestorable.length > 0) {
|
|
2319
|
-
console.log(" " + sym.done + " Restoring " + autoRestorable.length + " previous project(s)");
|
|
2320
|
-
}
|
|
2321
|
-
var hasRestorable = autoRestorable.length > 0;
|
|
2322
|
-
await forkDaemon(pin, false, hasRestorable ? autoRestorable : undefined, !hasRestorable);
|
|
2323
|
-
} else {
|
|
2324
|
-
setup(function (pin, keepAwake) {
|
|
2325
|
-
if (dangerouslySkipPermissions && !pin) {
|
|
2326
|
-
log(sym.warn + " " + a.red + "--dangerously-skip-permissions requires a PIN." + a.reset);
|
|
2327
|
-
log(a.dim + " Please set a PIN to use skip permissions mode." + a.reset);
|
|
2328
|
-
process.exit(1);
|
|
2329
|
-
return;
|
|
2330
|
-
}
|
|
2331
|
-
|
|
2332
|
-
// Check ~/.clayrc for previous projects to restore
|
|
2333
|
-
var rc = loadClayrc();
|
|
2334
|
-
var restorable = (rc.recentProjects || []).filter(function (p) {
|
|
2335
|
-
return p.path !== cwd && fs.existsSync(p.path);
|
|
2336
|
-
});
|
|
2337
|
-
|
|
2338
|
-
if (restorable.length > 0) {
|
|
2339
|
-
promptRestoreProjects(restorable, function (selected) {
|
|
2340
|
-
forkDaemon(pin, keepAwake, selected, false);
|
|
2341
|
-
});
|
|
2342
|
-
} else {
|
|
2343
|
-
log(sym.bar);
|
|
2344
|
-
log(sym.end + " " + a.dim + "Starting relay..." + a.reset);
|
|
2345
|
-
log("");
|
|
2346
|
-
forkDaemon(pin, keepAwake, undefined, true);
|
|
2347
|
-
}
|
|
2348
|
-
});
|
|
2349
|
-
}
|
|
2350
|
-
}
|
|
2351
|
-
})();
|
|
2
|
+
require("clay-server/bin/cli.js");
|