clay-server 1.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +283 -0
- package/bin/claude-relay.js +6 -0
- package/bin/cli.js +2602 -0
- package/lib/cli-sessions.js +265 -0
- package/lib/config.js +338 -0
- package/lib/daemon.js +802 -0
- package/lib/ipc.js +124 -0
- package/lib/notes.js +121 -0
- package/lib/pages.js +1308 -0
- package/lib/project.js +3172 -0
- package/lib/public/app.js +4795 -0
- package/lib/public/apple-touch-icon-dark.png +0 -0
- package/lib/public/apple-touch-icon.png +0 -0
- package/lib/public/clay-logo.png +0 -0
- package/lib/public/css/admin.css +576 -0
- package/lib/public/css/base.css +284 -0
- package/lib/public/css/diff.css +139 -0
- package/lib/public/css/filebrowser.css +1482 -0
- package/lib/public/css/highlight.css +144 -0
- package/lib/public/css/home-hub.css +455 -0
- package/lib/public/css/icon-strip.css +614 -0
- package/lib/public/css/input.css +654 -0
- package/lib/public/css/loop.css +898 -0
- package/lib/public/css/menus.css +823 -0
- package/lib/public/css/messages.css +1448 -0
- package/lib/public/css/mobile-nav.css +384 -0
- package/lib/public/css/overlays.css +893 -0
- package/lib/public/css/playbook.css +264 -0
- package/lib/public/css/profile.css +268 -0
- package/lib/public/css/rewind.css +528 -0
- package/lib/public/css/scheduler-modal.css +1429 -0
- package/lib/public/css/scheduler.css +1306 -0
- package/lib/public/css/server-settings.css +811 -0
- package/lib/public/css/sidebar.css +1189 -0
- package/lib/public/css/skills.css +789 -0
- package/lib/public/css/sticky-notes.css +848 -0
- package/lib/public/css/stt.css +155 -0
- package/lib/public/css/title-bar.css +517 -0
- package/lib/public/favicon-banded-32.png +0 -0
- package/lib/public/favicon-banded.png +0 -0
- package/lib/public/favicon-dark.svg +1 -0
- package/lib/public/favicon.svg +1 -0
- package/lib/public/icon-192-dark.png +0 -0
- package/lib/public/icon-192.png +0 -0
- package/lib/public/icon-512-dark.png +0 -0
- package/lib/public/icon-512.png +0 -0
- package/lib/public/icon-banded-76.png +0 -0
- package/lib/public/icon-banded-96.png +0 -0
- package/lib/public/icon-mono.svg +1 -0
- package/lib/public/index.html +1437 -0
- package/lib/public/manifest.json +27 -0
- package/lib/public/modules/admin.js +631 -0
- package/lib/public/modules/ascii-logo.js +442 -0
- package/lib/public/modules/diff.js +398 -0
- package/lib/public/modules/events.js +21 -0
- package/lib/public/modules/filebrowser.js +1535 -0
- package/lib/public/modules/fileicons.js +172 -0
- package/lib/public/modules/icons.js +54 -0
- package/lib/public/modules/input.js +661 -0
- package/lib/public/modules/markdown.js +378 -0
- package/lib/public/modules/notifications.js +548 -0
- package/lib/public/modules/playbook.js +578 -0
- package/lib/public/modules/profile.js +378 -0
- package/lib/public/modules/project-settings.js +901 -0
- package/lib/public/modules/qrcode.js +67 -0
- package/lib/public/modules/rewind.js +345 -0
- package/lib/public/modules/scheduler.js +2833 -0
- package/lib/public/modules/server-settings.js +928 -0
- package/lib/public/modules/sidebar.js +2264 -0
- package/lib/public/modules/skills.js +794 -0
- package/lib/public/modules/state.js +3 -0
- package/lib/public/modules/sticky-notes.js +1253 -0
- package/lib/public/modules/stt.js +272 -0
- package/lib/public/modules/terminal.js +736 -0
- package/lib/public/modules/theme.js +720 -0
- package/lib/public/modules/tools.js +1622 -0
- package/lib/public/modules/utils.js +56 -0
- package/lib/public/style.css +24 -0
- package/lib/public/sw.js +154 -0
- package/lib/public/wordmark-banded-20.png +0 -0
- package/lib/public/wordmark-banded-32.png +0 -0
- package/lib/public/wordmark-banded-64.png +0 -0
- package/lib/public/wordmark-banded-80.png +0 -0
- package/lib/push.js +130 -0
- package/lib/scheduler.js +402 -0
- package/lib/sdk-bridge.js +1035 -0
- package/lib/server.js +2055 -0
- package/lib/sessions.js +552 -0
- package/lib/smtp.js +221 -0
- package/lib/terminal-manager.js +187 -0
- package/lib/terminal.js +24 -0
- package/lib/themes/ayu-light.json +9 -0
- package/lib/themes/catppuccin-latte.json +9 -0
- package/lib/themes/catppuccin-mocha.json +9 -0
- package/lib/themes/clay-light.json +10 -0
- package/lib/themes/clay.json +10 -0
- package/lib/themes/dracula.json +9 -0
- package/lib/themes/everforest-light.json +9 -0
- package/lib/themes/everforest.json +9 -0
- package/lib/themes/github-light.json +9 -0
- package/lib/themes/gruvbox-dark.json +9 -0
- package/lib/themes/gruvbox-light.json +9 -0
- package/lib/themes/monokai.json +9 -0
- package/lib/themes/nord-light.json +9 -0
- package/lib/themes/nord.json +9 -0
- package/lib/themes/one-dark.json +9 -0
- package/lib/themes/one-light.json +9 -0
- package/lib/themes/rose-pine-dawn.json +9 -0
- package/lib/themes/rose-pine.json +9 -0
- package/lib/themes/solarized-dark.json +9 -0
- package/lib/themes/solarized-light.json +9 -0
- package/lib/themes/tokyo-night-light.json +9 -0
- package/lib/themes/tokyo-night.json +9 -0
- package/lib/updater.js +97 -0
- package/lib/users.js +459 -0
- package/lib/utils.js +64 -0
- package/package.json +56 -0
package/bin/cli.js
ADDED
|
@@ -0,0 +1,2602 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// --- Node version check (must run before any require that may use Node 20+ features) ---
|
|
4
|
+
var _nodeMajor = parseInt(process.versions.node.split(".")[0], 10);
|
|
5
|
+
if (_nodeMajor < 20) {
|
|
6
|
+
console.error("");
|
|
7
|
+
console.error("\x1b[31m[clay] Node.js 20+ is required (current: " + process.version + ")\x1b[0m");
|
|
8
|
+
console.error("[clay] The Claude Agent SDK 0.2.40+ requires Node 20 for Symbol.dispose support.");
|
|
9
|
+
console.error("[clay] If you cannot upgrade Node, use claude-relay@2.4.3 which supports Node 18.");
|
|
10
|
+
console.error("");
|
|
11
|
+
console.error(" Upgrade Node: nvm install 22 && nvm use 22");
|
|
12
|
+
console.error(" Or use older: npx claude-relay@2.4.3");
|
|
13
|
+
console.error("");
|
|
14
|
+
process.exit(78);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
var os = require("os");
|
|
18
|
+
var fs = require("fs");
|
|
19
|
+
var path = require("path");
|
|
20
|
+
var { execSync, execFileSync, spawn } = require("child_process");
|
|
21
|
+
var qrcode = require("qrcode-terminal");
|
|
22
|
+
var net = require("net");
|
|
23
|
+
|
|
24
|
+
// Detect dev mode — dev and prod use separate daemon files so they can run simultaneously
|
|
25
|
+
var _isDev = (process.argv[1] && path.basename(process.argv[1]) === "clay-dev") || process.argv.includes("--dev");
|
|
26
|
+
if (_isDev) process.env.CLAY_DEV = "1";
|
|
27
|
+
|
|
28
|
+
var { loadConfig, saveConfig, configPath, socketPath, logPath, ensureConfigDir, isDaemonAlive, isDaemonAliveAsync, generateSlug, clearStaleConfig, loadClayrc, saveClayrc, readCrashInfo } = require("../lib/config");
|
|
29
|
+
var { sendIPCCommand } = require("../lib/ipc");
|
|
30
|
+
var { generateAuthToken } = require("../lib/server");
|
|
31
|
+
var { enableMultiUser, disableMultiUser, hasAdmin, isMultiUser } = require("../lib/users");
|
|
32
|
+
|
|
33
|
+
function openUrl(url) {
|
|
34
|
+
try {
|
|
35
|
+
if (process.platform === "win32") {
|
|
36
|
+
spawn("cmd", ["/c", "start", "", url], { stdio: "ignore", detached: true, windowsHide: true }).unref();
|
|
37
|
+
} else {
|
|
38
|
+
var cmd = process.platform === "darwin" ? "open" : "xdg-open";
|
|
39
|
+
spawn(cmd, [url], { stdio: "ignore", detached: true }).unref();
|
|
40
|
+
}
|
|
41
|
+
} catch (e) {}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
var args = process.argv.slice(2);
|
|
45
|
+
var port = _isDev ? 2635 : 2633;
|
|
46
|
+
var useHttps = true;
|
|
47
|
+
var skipUpdate = false;
|
|
48
|
+
var debugMode = false;
|
|
49
|
+
var autoYes = false;
|
|
50
|
+
var cliPin = null;
|
|
51
|
+
var shutdownMode = false;
|
|
52
|
+
var restartMode = false;
|
|
53
|
+
var addPath = null;
|
|
54
|
+
var removePath = null;
|
|
55
|
+
var listMode = false;
|
|
56
|
+
var dangerouslySkipPermissions = false;
|
|
57
|
+
var headlessMode = false;
|
|
58
|
+
var watchMode = false;
|
|
59
|
+
var host = null;
|
|
60
|
+
var multiUserMode = false;
|
|
61
|
+
|
|
62
|
+
for (var i = 0; i < args.length; i++) {
|
|
63
|
+
if (args[i] === "-p" || args[i] === "--port") {
|
|
64
|
+
port = parseInt(args[i + 1], 10);
|
|
65
|
+
if (isNaN(port)) {
|
|
66
|
+
console.error("Invalid port number");
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
i++;
|
|
70
|
+
} else if (args[i] === "--host" || args[i] === "--bind") {
|
|
71
|
+
host = args[i + 1] || null;
|
|
72
|
+
i++;
|
|
73
|
+
} else if (args[i] === "--no-https") {
|
|
74
|
+
useHttps = false;
|
|
75
|
+
} else if (args[i] === "--no-update" || args[i] === "--skip-update") {
|
|
76
|
+
skipUpdate = true;
|
|
77
|
+
} else if (args[i] === "--dev") {
|
|
78
|
+
// Already handled above for CLAY_HOME, just skip
|
|
79
|
+
} else if (args[i] === "--watch" || args[i] === "-w") {
|
|
80
|
+
watchMode = true;
|
|
81
|
+
} else if (args[i] === "--debug") {
|
|
82
|
+
debugMode = true;
|
|
83
|
+
} else if (args[i] === "-y" || args[i] === "--yes") {
|
|
84
|
+
autoYes = true;
|
|
85
|
+
} else if (args[i] === "--pin") {
|
|
86
|
+
cliPin = args[i + 1] || null;
|
|
87
|
+
i++;
|
|
88
|
+
} else if (args[i] === "--shutdown") {
|
|
89
|
+
shutdownMode = true;
|
|
90
|
+
} else if (args[i] === "--restart") {
|
|
91
|
+
restartMode = true;
|
|
92
|
+
} else if (args[i] === "--add") {
|
|
93
|
+
addPath = args[i + 1] || ".";
|
|
94
|
+
i++;
|
|
95
|
+
} else if (args[i] === "--remove") {
|
|
96
|
+
removePath = args[i + 1] || null;
|
|
97
|
+
i++;
|
|
98
|
+
} else if (args[i] === "--list") {
|
|
99
|
+
listMode = true;
|
|
100
|
+
} else if (args[i] === "--headless") {
|
|
101
|
+
headlessMode = true;
|
|
102
|
+
autoYes = true;
|
|
103
|
+
} else if (args[i] === "--dangerously-skip-permissions") {
|
|
104
|
+
dangerouslySkipPermissions = true;
|
|
105
|
+
} else if (args[i] === "--multi-user") {
|
|
106
|
+
multiUserMode = true;
|
|
107
|
+
} else if (args[i] === "-h" || args[i] === "--help") {
|
|
108
|
+
console.log("Usage: clay-server [-p|--port <port>] [--host <address>] [--no-https] [--no-update] [--debug] [-y|--yes] [--pin <pin>] [--shutdown] [--restart]");
|
|
109
|
+
console.log(" clay-server --add <path> Add a project to the running daemon");
|
|
110
|
+
console.log(" clay-server --remove <path> Remove a project from the running daemon");
|
|
111
|
+
console.log(" clay-server --list List registered projects");
|
|
112
|
+
console.log("");
|
|
113
|
+
console.log("Options:");
|
|
114
|
+
console.log(" -p, --port <port> Port to listen on (default: 2633)");
|
|
115
|
+
console.log(" --host <address> Address to bind to (default: 0.0.0.0)");
|
|
116
|
+
console.log(" --no-https Disable HTTPS (enabled by default via mkcert)");
|
|
117
|
+
console.log(" --no-update Skip auto-update check on startup");
|
|
118
|
+
console.log(" --debug Enable debug panel in the web UI");
|
|
119
|
+
console.log(" -y, --yes Skip interactive prompts (accept defaults)");
|
|
120
|
+
console.log(" --pin <pin> Set 6-digit PIN (use with --yes)");
|
|
121
|
+
console.log(" --shutdown Shut down the running relay daemon");
|
|
122
|
+
console.log(" --restart Restart the running relay daemon");
|
|
123
|
+
console.log(" --add <path> Add a project directory (use '.' for current)");
|
|
124
|
+
console.log(" --remove <path> Remove a project directory");
|
|
125
|
+
console.log(" --list List all registered projects");
|
|
126
|
+
console.log(" --headless Start daemon and exit immediately (implies --yes)");
|
|
127
|
+
console.log(" --multi-user Enable multi-user mode (generates setup code)");
|
|
128
|
+
console.log(" --dangerously-skip-permissions");
|
|
129
|
+
console.log(" Bypass all permission prompts");
|
|
130
|
+
process.exit(0);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Dev mode implies debug + skip update
|
|
135
|
+
if (_isDev) {
|
|
136
|
+
debugMode = true;
|
|
137
|
+
skipUpdate = true;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// --- Handle --shutdown before anything else ---
|
|
141
|
+
if (shutdownMode) {
|
|
142
|
+
var shutdownConfig = loadConfig();
|
|
143
|
+
isDaemonAliveAsync(shutdownConfig).then(function (alive) {
|
|
144
|
+
if (!alive) {
|
|
145
|
+
console.error("No running daemon found.");
|
|
146
|
+
process.exit(1);
|
|
147
|
+
}
|
|
148
|
+
sendIPCCommand(socketPath(), { cmd: "shutdown" }).then(function () {
|
|
149
|
+
console.log("Server stopped.");
|
|
150
|
+
clearStaleConfig();
|
|
151
|
+
process.exit(0);
|
|
152
|
+
}).catch(function (err) {
|
|
153
|
+
console.error("Shutdown failed:", err.message);
|
|
154
|
+
process.exit(1);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// --- Handle --restart before anything else ---
|
|
161
|
+
if (restartMode) {
|
|
162
|
+
var restartConfig = loadConfig();
|
|
163
|
+
isDaemonAliveAsync(restartConfig).then(function (alive) {
|
|
164
|
+
if (!alive) {
|
|
165
|
+
console.error("No running daemon found.");
|
|
166
|
+
process.exit(1);
|
|
167
|
+
}
|
|
168
|
+
sendIPCCommand(socketPath(), { cmd: "restart" }).then(function () {
|
|
169
|
+
console.log("Server restarted.");
|
|
170
|
+
process.exit(0);
|
|
171
|
+
}).catch(function (err) {
|
|
172
|
+
console.error("Restart failed:", err.message);
|
|
173
|
+
process.exit(1);
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// --- Handle --add before anything else ---
|
|
180
|
+
if (addPath !== null) {
|
|
181
|
+
var absAdd = path.resolve(addPath);
|
|
182
|
+
try {
|
|
183
|
+
var stat = fs.statSync(absAdd);
|
|
184
|
+
if (!stat.isDirectory()) {
|
|
185
|
+
console.error("Not a directory: " + absAdd);
|
|
186
|
+
process.exit(1);
|
|
187
|
+
}
|
|
188
|
+
} catch (e) {
|
|
189
|
+
console.error("Directory not found: " + absAdd);
|
|
190
|
+
process.exit(1);
|
|
191
|
+
}
|
|
192
|
+
var addConfig = loadConfig();
|
|
193
|
+
isDaemonAliveAsync(addConfig).then(function (alive) {
|
|
194
|
+
if (!alive) {
|
|
195
|
+
console.error("No running daemon. Start with: npx clay-server");
|
|
196
|
+
process.exit(1);
|
|
197
|
+
}
|
|
198
|
+
sendIPCCommand(socketPath(), { cmd: "add_project", path: absAdd }).then(function (res) {
|
|
199
|
+
if (res.ok) {
|
|
200
|
+
if (res.existing) {
|
|
201
|
+
console.log("Already registered: " + res.slug);
|
|
202
|
+
} else {
|
|
203
|
+
console.log("Added: " + res.slug + " \u2192 " + absAdd);
|
|
204
|
+
}
|
|
205
|
+
process.exit(0);
|
|
206
|
+
} else {
|
|
207
|
+
console.error("Failed: " + (res.error || "unknown error"));
|
|
208
|
+
process.exit(1);
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// --- Handle --remove before anything else ---
|
|
216
|
+
if (removePath !== null) {
|
|
217
|
+
var absRemove = path.resolve(removePath);
|
|
218
|
+
var removeConfig = loadConfig();
|
|
219
|
+
isDaemonAliveAsync(removeConfig).then(function (alive) {
|
|
220
|
+
if (!alive) {
|
|
221
|
+
console.error("No running daemon. Start with: npx clay-server");
|
|
222
|
+
process.exit(1);
|
|
223
|
+
}
|
|
224
|
+
sendIPCCommand(socketPath(), { cmd: "remove_project", path: absRemove }).then(function (res) {
|
|
225
|
+
if (res.ok) {
|
|
226
|
+
console.log("Removed: " + path.basename(absRemove));
|
|
227
|
+
process.exit(0);
|
|
228
|
+
} else {
|
|
229
|
+
console.error("Failed: " + (res.error || "project not found"));
|
|
230
|
+
process.exit(1);
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// --- Handle --list before anything else ---
|
|
238
|
+
if (listMode) {
|
|
239
|
+
var listConfig = loadConfig();
|
|
240
|
+
isDaemonAliveAsync(listConfig).then(function (alive) {
|
|
241
|
+
if (!alive) {
|
|
242
|
+
console.error("No running daemon. Start with: npx clay-server");
|
|
243
|
+
process.exit(1);
|
|
244
|
+
}
|
|
245
|
+
sendIPCCommand(socketPath(), { cmd: "get_status" }).then(function (res) {
|
|
246
|
+
if (!res.ok || !res.projects || res.projects.length === 0) {
|
|
247
|
+
console.log("No projects registered.");
|
|
248
|
+
process.exit(0);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
console.log("Projects (" + res.projects.length + "):\n");
|
|
252
|
+
for (var p = 0; p < res.projects.length; p++) {
|
|
253
|
+
var proj = res.projects[p];
|
|
254
|
+
var label = " " + proj.slug;
|
|
255
|
+
if (proj.title) label += " (" + proj.title + ")";
|
|
256
|
+
label += "\n " + proj.path;
|
|
257
|
+
console.log(label);
|
|
258
|
+
}
|
|
259
|
+
console.log("");
|
|
260
|
+
process.exit(0);
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// --- Handle --multi-user before anything else ---
|
|
267
|
+
if (multiUserMode) {
|
|
268
|
+
var muResult = enableMultiUser();
|
|
269
|
+
if (muResult.alreadyEnabled && muResult.hasAdmin) {
|
|
270
|
+
console.log("");
|
|
271
|
+
console.log("Multi-user mode is already enabled and an admin account exists.");
|
|
272
|
+
console.log("No changes made.");
|
|
273
|
+
console.log("");
|
|
274
|
+
} else if (muResult.setupCode) {
|
|
275
|
+
console.log("");
|
|
276
|
+
console.log("\x1b[33m⚠ Experimental Feature\x1b[0m");
|
|
277
|
+
console.log("");
|
|
278
|
+
console.log(" Multi-user mode is experimental and may change in future releases.");
|
|
279
|
+
console.log(" Sharing access to AI-powered tools may be subject to your provider's");
|
|
280
|
+
console.log(" terms of service. Please review the applicable usage policies before");
|
|
281
|
+
console.log(" granting access to other users.");
|
|
282
|
+
console.log("");
|
|
283
|
+
console.log("\x1b[32mMulti-user mode enabled.\x1b[0m");
|
|
284
|
+
console.log("");
|
|
285
|
+
console.log("Setup code: \x1b[1m" + muResult.setupCode + "\x1b[0m");
|
|
286
|
+
console.log("");
|
|
287
|
+
console.log("Open Clay in your browser and enter this code to create the admin account.");
|
|
288
|
+
console.log("The code is single-use and will be cleared once the admin is set up.");
|
|
289
|
+
console.log("");
|
|
290
|
+
}
|
|
291
|
+
process.exit(0);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
var cwd = process.cwd();
|
|
295
|
+
|
|
296
|
+
// --- ANSI helpers ---
|
|
297
|
+
var isBasicTerm = process.env.TERM_PROGRAM === "Apple_Terminal";
|
|
298
|
+
var a = {
|
|
299
|
+
reset: "\x1b[0m",
|
|
300
|
+
bold: "\x1b[1m",
|
|
301
|
+
dim: "\x1b[2m",
|
|
302
|
+
clay: isBasicTerm ? "\x1b[34m" : "\x1b[38;2;88;87;252m", // #5857FC Indigo — active interaction
|
|
303
|
+
green: "\x1b[32m",
|
|
304
|
+
yellow: "\x1b[33m",
|
|
305
|
+
red: "\x1b[31m",
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
function gradient(text) {
|
|
309
|
+
if (isBasicTerm) {
|
|
310
|
+
return a.yellow + text + a.reset;
|
|
311
|
+
}
|
|
312
|
+
// Terracotta (#FE7150) → Warm brown (#D09558) — Clay earthy warmth
|
|
313
|
+
var r0 = 254, g0 = 113, b0 = 80;
|
|
314
|
+
var r1 = 208, g1 = 149, b1 = 88;
|
|
315
|
+
var out = "";
|
|
316
|
+
var len = text.length;
|
|
317
|
+
for (var i = 0; i < len; i++) {
|
|
318
|
+
var t = len > 1 ? i / (len - 1) : 0;
|
|
319
|
+
var r = Math.round(r0 + (r1 - r0) * t);
|
|
320
|
+
var g = Math.round(g0 + (g1 - g0) * t);
|
|
321
|
+
var b = Math.round(b0 + (b1 - b0) * t);
|
|
322
|
+
out += "\x1b[38;2;" + r + ";" + g + ";" + b + "m" + text[i];
|
|
323
|
+
}
|
|
324
|
+
return out + a.reset;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
var sym = {
|
|
328
|
+
pointer: a.clay + "◆" + a.reset,
|
|
329
|
+
done: a.green + "◇" + a.reset,
|
|
330
|
+
bar: a.dim + "│" + a.reset,
|
|
331
|
+
end: a.dim + "└" + a.reset,
|
|
332
|
+
warn: a.yellow + "▲" + a.reset,
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
function log(s) { console.log(" " + s); }
|
|
336
|
+
|
|
337
|
+
function clearUp(n) {
|
|
338
|
+
for (var i = 0; i < n; i++) {
|
|
339
|
+
process.stdout.write("\x1b[1A\x1b[2K");
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// --- Daemon watcher ---
|
|
344
|
+
// Polls daemon socket; if connection fails, the server is down.
|
|
345
|
+
var _daemonWatcher = null;
|
|
346
|
+
|
|
347
|
+
function startDaemonWatcher() {
|
|
348
|
+
if (_daemonWatcher) return;
|
|
349
|
+
_daemonWatcher = setInterval(function () {
|
|
350
|
+
var client = net.connect(socketPath());
|
|
351
|
+
var timer = setTimeout(function () {
|
|
352
|
+
client.destroy();
|
|
353
|
+
onDaemonDied();
|
|
354
|
+
}, 1500);
|
|
355
|
+
client.on("connect", function () {
|
|
356
|
+
clearTimeout(timer);
|
|
357
|
+
client.destroy();
|
|
358
|
+
});
|
|
359
|
+
client.on("error", function () {
|
|
360
|
+
clearTimeout(timer);
|
|
361
|
+
client.destroy();
|
|
362
|
+
onDaemonDied();
|
|
363
|
+
});
|
|
364
|
+
}, 3000);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function stopDaemonWatcher() {
|
|
368
|
+
if (_daemonWatcher) {
|
|
369
|
+
clearInterval(_daemonWatcher);
|
|
370
|
+
_daemonWatcher = null;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
var _restartAttempts = 0;
|
|
375
|
+
var MAX_RESTART_ATTEMPTS = 5;
|
|
376
|
+
var _restartBackoffStart = 0;
|
|
377
|
+
|
|
378
|
+
function onDaemonDied() {
|
|
379
|
+
stopDaemonWatcher();
|
|
380
|
+
// Clean up stdin in case a prompt is active
|
|
381
|
+
try {
|
|
382
|
+
process.stdin.setRawMode(false);
|
|
383
|
+
process.stdin.pause();
|
|
384
|
+
process.stdin.removeAllListeners("data");
|
|
385
|
+
} catch (e) {}
|
|
386
|
+
|
|
387
|
+
// Check if this was a crash (crash.json exists) vs intentional shutdown
|
|
388
|
+
var crashInfo = readCrashInfo();
|
|
389
|
+
if (!crashInfo) {
|
|
390
|
+
// Intentional shutdown, no restart
|
|
391
|
+
log("");
|
|
392
|
+
log(sym.warn + " " + a.yellow + "Server has been shut down." + a.reset);
|
|
393
|
+
log(a.dim + " Run " + a.reset + "npx clay-server" + a.dim + " to start again." + a.reset);
|
|
394
|
+
log("");
|
|
395
|
+
process.exit(0);
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Reset backoff counter if enough time has passed since last restart burst
|
|
400
|
+
var now = Date.now();
|
|
401
|
+
if (_restartBackoffStart && now - _restartBackoffStart > 60000) {
|
|
402
|
+
_restartAttempts = 0;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
_restartAttempts++;
|
|
406
|
+
if (_restartAttempts > MAX_RESTART_ATTEMPTS) {
|
|
407
|
+
log("");
|
|
408
|
+
log(sym.warn + " " + a.red + "Server crashed too many times (" + MAX_RESTART_ATTEMPTS + " attempts). Giving up." + a.reset);
|
|
409
|
+
if (crashInfo.reason) {
|
|
410
|
+
log(a.dim + " " + crashInfo.reason.split("\n")[0] + a.reset);
|
|
411
|
+
}
|
|
412
|
+
log(a.dim + " Check logs: " + a.reset + logPath());
|
|
413
|
+
log("");
|
|
414
|
+
process.exit(1);
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (_restartAttempts === 1) _restartBackoffStart = now;
|
|
419
|
+
|
|
420
|
+
log("");
|
|
421
|
+
log(sym.warn + " " + a.yellow + "Server crashed. Restarting... (" + _restartAttempts + "/" + MAX_RESTART_ATTEMPTS + ")" + a.reset);
|
|
422
|
+
if (crashInfo.reason) {
|
|
423
|
+
log(a.dim + " " + crashInfo.reason.split("\n")[0] + a.reset);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Re-fork the daemon from saved config
|
|
427
|
+
restartDaemonFromConfig();
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
async function restartDaemonFromConfig() {
|
|
431
|
+
var lastConfig = loadConfig();
|
|
432
|
+
if (!lastConfig || !lastConfig.projects) {
|
|
433
|
+
log(a.red + " No config found. Cannot restart." + a.reset);
|
|
434
|
+
process.exit(1);
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
clearStaleConfig();
|
|
439
|
+
|
|
440
|
+
// Wait for port to be released
|
|
441
|
+
var targetPort = lastConfig.port || port;
|
|
442
|
+
var waited = 0;
|
|
443
|
+
while (waited < 3000) {
|
|
444
|
+
var free = await isPortFree(targetPort);
|
|
445
|
+
if (free) break;
|
|
446
|
+
await new Promise(function (resolve) { setTimeout(resolve, 300); });
|
|
447
|
+
waited += 300;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Rebuild config (preserve everything except pid)
|
|
451
|
+
var newConfig = {
|
|
452
|
+
pid: null,
|
|
453
|
+
port: targetPort,
|
|
454
|
+
pinHash: lastConfig.pinHash || null,
|
|
455
|
+
tls: lastConfig.tls !== undefined ? lastConfig.tls : useHttps,
|
|
456
|
+
debug: lastConfig.debug || false,
|
|
457
|
+
keepAwake: lastConfig.keepAwake || false,
|
|
458
|
+
dangerouslySkipPermissions: lastConfig.dangerouslySkipPermissions || false,
|
|
459
|
+
projects: (lastConfig.projects || []).filter(function (p) {
|
|
460
|
+
return fs.existsSync(p.path);
|
|
461
|
+
}),
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
ensureConfigDir();
|
|
465
|
+
saveConfig(newConfig);
|
|
466
|
+
|
|
467
|
+
var daemonScript = path.join(__dirname, "..", "lib", "daemon.js");
|
|
468
|
+
var logFile = logPath();
|
|
469
|
+
var logFd = fs.openSync(logFile, "a");
|
|
470
|
+
|
|
471
|
+
var child = spawn(process.execPath, [daemonScript], {
|
|
472
|
+
detached: true,
|
|
473
|
+
windowsHide: true,
|
|
474
|
+
stdio: ["ignore", logFd, logFd],
|
|
475
|
+
env: Object.assign({}, process.env, {
|
|
476
|
+
CLAY_CONFIG: configPath(),
|
|
477
|
+
}),
|
|
478
|
+
});
|
|
479
|
+
child.unref();
|
|
480
|
+
fs.closeSync(logFd);
|
|
481
|
+
|
|
482
|
+
newConfig.pid = child.pid;
|
|
483
|
+
saveConfig(newConfig);
|
|
484
|
+
|
|
485
|
+
// Wait and verify (retry up to 5 seconds)
|
|
486
|
+
var alive = false;
|
|
487
|
+
for (var rc = 0; rc < 10; rc++) {
|
|
488
|
+
await new Promise(function (resolve) { setTimeout(resolve, 500); });
|
|
489
|
+
alive = await isDaemonAliveAsync(newConfig);
|
|
490
|
+
if (alive) break;
|
|
491
|
+
}
|
|
492
|
+
if (!alive) {
|
|
493
|
+
log(a.red + " Restart failed. Check logs: " + a.reset + logFile);
|
|
494
|
+
process.exit(1);
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
var ip = getLocalIP();
|
|
498
|
+
log(sym.done + " " + a.green + "Server restarted successfully." + a.reset);
|
|
499
|
+
log("");
|
|
500
|
+
showMainMenu(newConfig, ip);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// --- Network ---
|
|
504
|
+
function getLocalIP() {
|
|
505
|
+
var interfaces = os.networkInterfaces();
|
|
506
|
+
|
|
507
|
+
// Prefer Tailscale IP
|
|
508
|
+
for (var name in interfaces) {
|
|
509
|
+
if (/^(tailscale|utun)/.test(name)) {
|
|
510
|
+
for (var j = 0; j < interfaces[name].length; j++) {
|
|
511
|
+
var addr = interfaces[name][j];
|
|
512
|
+
if (addr.family === "IPv4" && !addr.internal && addr.address.startsWith("100.")) {
|
|
513
|
+
return addr.address;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// All interfaces for Tailscale CGNAT range
|
|
520
|
+
for (var addrs of Object.values(interfaces)) {
|
|
521
|
+
for (var k = 0; k < addrs.length; k++) {
|
|
522
|
+
if (addrs[k].family === "IPv4" && !addrs[k].internal && addrs[k].address.startsWith("100.")) {
|
|
523
|
+
return addrs[k].address;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Fall back to LAN IP
|
|
529
|
+
for (var addrs2 of Object.values(interfaces)) {
|
|
530
|
+
for (var m = 0; m < addrs2.length; m++) {
|
|
531
|
+
if (addrs2[m].family === "IPv4" && !addrs2[m].internal) {
|
|
532
|
+
return addrs2[m].address;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
return "localhost";
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// --- Certs ---
|
|
541
|
+
function isRoutableIP(addr) {
|
|
542
|
+
if (addr.startsWith("10.")) return true;
|
|
543
|
+
if (addr.startsWith("192.168.")) return true;
|
|
544
|
+
if (addr.startsWith("100.")) {
|
|
545
|
+
var second = parseInt(addr.split(".")[1], 10);
|
|
546
|
+
return second >= 64 && second <= 127; // CGNAT (Tailscale)
|
|
547
|
+
}
|
|
548
|
+
if (addr.startsWith("172.")) {
|
|
549
|
+
var second = parseInt(addr.split(".")[1], 10);
|
|
550
|
+
return second >= 16 && second <= 31;
|
|
551
|
+
}
|
|
552
|
+
return false;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function getAllIPs() {
|
|
556
|
+
var ips = [];
|
|
557
|
+
var ifaces = os.networkInterfaces();
|
|
558
|
+
for (var addrs of Object.values(ifaces)) {
|
|
559
|
+
for (var j = 0; j < addrs.length; j++) {
|
|
560
|
+
if (addrs[j].family === "IPv4" && !addrs[j].internal && isRoutableIP(addrs[j].address)) {
|
|
561
|
+
ips.push(addrs[j].address);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
return ips;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function ensureCerts(ip) {
|
|
569
|
+
var homeDir = os.homedir();
|
|
570
|
+
var certDir = path.join(process.env.CLAY_HOME || path.join(homeDir, ".clay"), "certs");
|
|
571
|
+
var keyPath = path.join(certDir, "key.pem");
|
|
572
|
+
var certPath = path.join(certDir, "cert.pem");
|
|
573
|
+
|
|
574
|
+
var legacyDir = path.join(cwd, ".claude-relay", "certs");
|
|
575
|
+
var legacyKey = path.join(legacyDir, "key.pem");
|
|
576
|
+
var legacyCert = path.join(legacyDir, "cert.pem");
|
|
577
|
+
if (!fs.existsSync(keyPath) && fs.existsSync(legacyKey) && fs.existsSync(legacyCert)) {
|
|
578
|
+
fs.mkdirSync(certDir, { recursive: true });
|
|
579
|
+
fs.copyFileSync(legacyKey, keyPath);
|
|
580
|
+
fs.copyFileSync(legacyCert, certPath);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
var caRoot = null;
|
|
584
|
+
try {
|
|
585
|
+
caRoot = path.join(
|
|
586
|
+
execSync("mkcert -CAROOT", { encoding: "utf8" }).trim(),
|
|
587
|
+
"rootCA.pem"
|
|
588
|
+
);
|
|
589
|
+
if (!fs.existsSync(caRoot)) caRoot = null;
|
|
590
|
+
} catch (e) {}
|
|
591
|
+
|
|
592
|
+
// Collect all IPv4 addresses (Tailscale + LAN)
|
|
593
|
+
var allIPs = getAllIPs();
|
|
594
|
+
|
|
595
|
+
if (fs.existsSync(keyPath) && fs.existsSync(certPath)) {
|
|
596
|
+
var needRegen = false;
|
|
597
|
+
try {
|
|
598
|
+
var certText = execFileSync("openssl", ["x509", "-in", certPath, "-text", "-noout"], { encoding: "utf8" });
|
|
599
|
+
for (var i = 0; i < allIPs.length; i++) {
|
|
600
|
+
if (certText.indexOf(allIPs[i]) === -1) {
|
|
601
|
+
needRegen = true;
|
|
602
|
+
break;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
} catch (e) { needRegen = true; }
|
|
606
|
+
if (!needRegen) return { key: keyPath, cert: certPath, caRoot: caRoot };
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
fs.mkdirSync(certDir, { recursive: true });
|
|
610
|
+
|
|
611
|
+
var domains = ["localhost", "127.0.0.1", "::1"];
|
|
612
|
+
for (var i = 0; i < allIPs.length; i++) {
|
|
613
|
+
if (domains.indexOf(allIPs[i]) === -1) domains.push(allIPs[i]);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
try {
|
|
617
|
+
var mkcertArgs = ["-key-file", keyPath, "-cert-file", certPath].concat(domains);
|
|
618
|
+
execFileSync("mkcert", mkcertArgs, { stdio: "pipe" });
|
|
619
|
+
} catch (err) {
|
|
620
|
+
return null;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
return { key: keyPath, cert: certPath, caRoot: caRoot };
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// --- Logo ---
|
|
627
|
+
function printLogo() {
|
|
628
|
+
var r = a.reset;
|
|
629
|
+
var lines = [
|
|
630
|
+
"________/\\\\\\\\\\\\\\\\\\__/\\\\\\_________________/\\\\\\\\\\\\\\\\\\_____/\\\\\\________/\\\\\\",
|
|
631
|
+
" _____/\\\\\\////////__\\/\\\\\\_______________/\\\\\\\\\\\\\\\\\\\\\\\\\\__\\///\\\\\\____/\\\\\\/_",
|
|
632
|
+
" ___/\\\\\\/___________\\/\\\\\\______________/\\\\\\/////////\\\\\\___\\///\\\\\\/\\\\\\/___",
|
|
633
|
+
" __/\\\\\\_____________\\/\\\\\\_____________\\/\\\\\\_______\\/\\\\\\_____\\///\\\\\\/_____",
|
|
634
|
+
" _\\/\\\\\\_____________\\/\\\\\\_____________\\/\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\_______\\/\\\\\\______",
|
|
635
|
+
" _\\//\\\\\\____________\\/\\\\\\_____________\\/\\\\\\/////////\\\\\\_______\\/\\\\\\______",
|
|
636
|
+
" __\\///\\\\\\__________\\/\\\\\\_____________\\/\\\\\\_______\\/\\\\\\_______\\/\\\\\\______",
|
|
637
|
+
" ____\\////\\\\\\\\\\\\\\\\\\_\\/\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\_\\/\\\\\\_______\\/\\\\\\_______\\/\\\\\\______",
|
|
638
|
+
" _______\\/////////__\\///////////////__\\///________\\///________\\///_______",
|
|
639
|
+
];
|
|
640
|
+
console.log("");
|
|
641
|
+
if (isBasicTerm) {
|
|
642
|
+
for (var i = 0; i < lines.length; i++) {
|
|
643
|
+
console.log(a.green + lines[i] + r);
|
|
644
|
+
}
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
// Tri-accent vertical gradient: Green (#09E5A3) → Indigo (#5857FC) → Terracotta (#FE7150)
|
|
648
|
+
var stops = [
|
|
649
|
+
[9, 229, 163],
|
|
650
|
+
[88, 87, 252],
|
|
651
|
+
[254, 113, 80],
|
|
652
|
+
];
|
|
653
|
+
for (var i = 0; i < lines.length; i++) {
|
|
654
|
+
var t = lines.length > 1 ? i / (lines.length - 1) : 0;
|
|
655
|
+
var cr, cg, cb;
|
|
656
|
+
if (t <= 0.5) {
|
|
657
|
+
var s = t * 2;
|
|
658
|
+
cr = Math.round(stops[0][0] + (stops[1][0] - stops[0][0]) * s);
|
|
659
|
+
cg = Math.round(stops[0][1] + (stops[1][1] - stops[0][1]) * s);
|
|
660
|
+
cb = Math.round(stops[0][2] + (stops[1][2] - stops[0][2]) * s);
|
|
661
|
+
} else {
|
|
662
|
+
var s = (t - 0.5) * 2;
|
|
663
|
+
cr = Math.round(stops[1][0] + (stops[2][0] - stops[1][0]) * s);
|
|
664
|
+
cg = Math.round(stops[1][1] + (stops[2][1] - stops[1][1]) * s);
|
|
665
|
+
cb = Math.round(stops[1][2] + (stops[2][2] - stops[1][2]) * s);
|
|
666
|
+
}
|
|
667
|
+
console.log("\x1b[38;2;" + cr + ";" + cg + ";" + cb + "m" + lines[i] + r);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// --- Interactive prompts ---
|
|
672
|
+
function promptToggle(title, desc, defaultValue, callback) {
|
|
673
|
+
var value = defaultValue || false;
|
|
674
|
+
|
|
675
|
+
function renderToggle() {
|
|
676
|
+
var yes = value
|
|
677
|
+
? a.green + a.bold + "● Yes" + a.reset
|
|
678
|
+
: a.dim + "○ Yes" + a.reset;
|
|
679
|
+
var no = !value
|
|
680
|
+
? a.green + a.bold + "● No" + a.reset
|
|
681
|
+
: a.dim + "○ No" + a.reset;
|
|
682
|
+
return yes + a.dim + " / " + a.reset + no;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
var lines = 2;
|
|
686
|
+
log(sym.pointer + " " + a.bold + title + a.reset);
|
|
687
|
+
if (desc) {
|
|
688
|
+
log(sym.bar + " " + a.dim + desc + a.reset);
|
|
689
|
+
lines = 3;
|
|
690
|
+
}
|
|
691
|
+
process.stdout.write(" " + sym.bar + " " + renderToggle());
|
|
692
|
+
|
|
693
|
+
process.stdin.setRawMode(true);
|
|
694
|
+
process.stdin.resume();
|
|
695
|
+
process.stdin.setEncoding("utf8");
|
|
696
|
+
|
|
697
|
+
process.stdin.on("data", function onToggle(ch) {
|
|
698
|
+
if (ch === "\x1b[D" || ch === "\x1b[C" || ch === "\t") {
|
|
699
|
+
value = !value;
|
|
700
|
+
process.stdout.write("\x1b[2K\r " + sym.bar + " " + renderToggle());
|
|
701
|
+
} else if (ch === "y" || ch === "Y") {
|
|
702
|
+
value = true;
|
|
703
|
+
process.stdout.write("\x1b[2K\r " + sym.bar + " " + renderToggle());
|
|
704
|
+
} else if (ch === "n" || ch === "N") {
|
|
705
|
+
value = false;
|
|
706
|
+
process.stdout.write("\x1b[2K\r " + sym.bar + " " + renderToggle());
|
|
707
|
+
} else if (ch === "\r" || ch === "\n") {
|
|
708
|
+
process.stdin.setRawMode(false);
|
|
709
|
+
process.stdin.pause();
|
|
710
|
+
process.stdin.removeListener("data", onToggle);
|
|
711
|
+
process.stdout.write("\n");
|
|
712
|
+
clearUp(lines);
|
|
713
|
+
var result = value ? a.green + "Yes" + a.reset : a.dim + "No" + a.reset;
|
|
714
|
+
log(sym.done + " " + title + " " + a.dim + "·" + a.reset + " " + result);
|
|
715
|
+
callback(value);
|
|
716
|
+
} else if (ch === "\x03") {
|
|
717
|
+
process.stdout.write("\n");
|
|
718
|
+
clearUp(lines);
|
|
719
|
+
log(sym.end + " " + a.dim + "Cancelled" + a.reset);
|
|
720
|
+
process.exit(0);
|
|
721
|
+
}
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
function promptPin(callback) {
|
|
726
|
+
log(sym.pointer + " " + a.bold + "PIN protection" + a.reset);
|
|
727
|
+
log(sym.bar + " " + a.dim + "Require a 6-digit PIN to access the web UI. Enter to skip." + a.reset);
|
|
728
|
+
process.stdout.write(" " + sym.bar + " ");
|
|
729
|
+
|
|
730
|
+
var pin = "";
|
|
731
|
+
process.stdin.setRawMode(true);
|
|
732
|
+
process.stdin.resume();
|
|
733
|
+
process.stdin.setEncoding("utf8");
|
|
734
|
+
|
|
735
|
+
process.stdin.on("data", function onPin(ch) {
|
|
736
|
+
if (ch === "\r" || ch === "\n") {
|
|
737
|
+
process.stdin.setRawMode(false);
|
|
738
|
+
process.stdin.pause();
|
|
739
|
+
process.stdin.removeListener("data", onPin);
|
|
740
|
+
process.stdout.write("\n");
|
|
741
|
+
|
|
742
|
+
if (pin !== "" && !/^\d{6}$/.test(pin)) {
|
|
743
|
+
clearUp(3);
|
|
744
|
+
log(sym.done + " PIN protection " + a.red + "Must be exactly 6 digits" + a.reset);
|
|
745
|
+
log(sym.end);
|
|
746
|
+
process.exit(1);
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
clearUp(3);
|
|
751
|
+
if (pin) {
|
|
752
|
+
log(sym.done + " PIN protection " + a.dim + "·" + a.reset + " " + a.green + "Enabled" + a.reset);
|
|
753
|
+
} else {
|
|
754
|
+
log(sym.done + " PIN protection " + a.dim + "· Skipped" + a.reset);
|
|
755
|
+
}
|
|
756
|
+
log(sym.bar);
|
|
757
|
+
callback(pin || null);
|
|
758
|
+
} else if (ch === "\x03") {
|
|
759
|
+
process.stdout.write("\n");
|
|
760
|
+
clearUp(3);
|
|
761
|
+
log(sym.end + " " + a.dim + "Cancelled" + a.reset);
|
|
762
|
+
process.exit(0);
|
|
763
|
+
} else if (ch === "\x7f" || ch === "\b") {
|
|
764
|
+
if (pin.length > 0) {
|
|
765
|
+
pin = pin.slice(0, -1);
|
|
766
|
+
process.stdout.write("\b \b");
|
|
767
|
+
}
|
|
768
|
+
} else if (/\d/.test(ch) && pin.length < 6) {
|
|
769
|
+
pin += ch;
|
|
770
|
+
process.stdout.write(a.clay + "●" + a.reset);
|
|
771
|
+
}
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/**
|
|
776
|
+
* Text input prompt with placeholder and Tab directory completion.
|
|
777
|
+
* title: prompt label, placeholder: dimmed hint, callback(value)
|
|
778
|
+
* Enter with empty input returns placeholder value.
|
|
779
|
+
* Tab completes directory paths.
|
|
780
|
+
*/
|
|
781
|
+
function promptText(title, placeholder, callback) {
|
|
782
|
+
var prefix = " " + sym.bar + " ";
|
|
783
|
+
var hintLine = "";
|
|
784
|
+
var lineCount = 2;
|
|
785
|
+
|
|
786
|
+
log(sym.pointer + " " + a.bold + title + a.reset + " " + a.dim + "(esc to go back)" + a.reset);
|
|
787
|
+
process.stdout.write(prefix + a.dim + placeholder + a.reset);
|
|
788
|
+
// Move cursor to start of placeholder
|
|
789
|
+
process.stdout.write("\r" + prefix);
|
|
790
|
+
|
|
791
|
+
var text = "";
|
|
792
|
+
var showingPlaceholder = true;
|
|
793
|
+
process.stdin.setRawMode(true);
|
|
794
|
+
process.stdin.resume();
|
|
795
|
+
process.stdin.setEncoding("utf8");
|
|
796
|
+
|
|
797
|
+
function redrawInput() {
|
|
798
|
+
process.stdout.write("\x1b[2K\r" + prefix + text);
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function clearHint() {
|
|
802
|
+
if (hintLine) {
|
|
803
|
+
// Erase the hint line below
|
|
804
|
+
process.stdout.write("\n\x1b[2K\x1b[1A");
|
|
805
|
+
hintLine = "";
|
|
806
|
+
lineCount = 2;
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
function showHint(msg) {
|
|
811
|
+
clearHint();
|
|
812
|
+
hintLine = msg;
|
|
813
|
+
lineCount = 3;
|
|
814
|
+
// Print hint below, then move cursor back up
|
|
815
|
+
process.stdout.write("\n" + prefix + a.dim + msg + a.reset + "\x1b[1A");
|
|
816
|
+
redrawInput();
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
function tabComplete() {
|
|
820
|
+
var current = text || "";
|
|
821
|
+
if (!current) current = "/";
|
|
822
|
+
|
|
823
|
+
// Resolve ~ to home
|
|
824
|
+
if (current.charAt(0) === "~") {
|
|
825
|
+
current = os.homedir() + current.substring(1);
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
var resolved = path.resolve(current);
|
|
829
|
+
var dir, partial;
|
|
830
|
+
|
|
831
|
+
try {
|
|
832
|
+
var st = fs.statSync(resolved);
|
|
833
|
+
if (st.isDirectory()) {
|
|
834
|
+
// Current text is a full directory — list its children
|
|
835
|
+
dir = resolved;
|
|
836
|
+
partial = "";
|
|
837
|
+
} else {
|
|
838
|
+
dir = path.dirname(resolved);
|
|
839
|
+
partial = path.basename(resolved);
|
|
840
|
+
}
|
|
841
|
+
} catch (e) {
|
|
842
|
+
// Path doesn't exist — complete from parent
|
|
843
|
+
dir = path.dirname(resolved);
|
|
844
|
+
partial = path.basename(resolved);
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
var entries;
|
|
848
|
+
try {
|
|
849
|
+
entries = fs.readdirSync(dir);
|
|
850
|
+
} catch (e) {
|
|
851
|
+
return; // Can't read directory
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
// Filter to directories only, matching partial prefix
|
|
855
|
+
var matches = [];
|
|
856
|
+
var lowerPartial = partial.toLowerCase();
|
|
857
|
+
for (var i = 0; i < entries.length; i++) {
|
|
858
|
+
if (entries[i].charAt(0) === "." && !partial.startsWith(".")) continue;
|
|
859
|
+
if (lowerPartial && entries[i].toLowerCase().indexOf(lowerPartial) !== 0) continue;
|
|
860
|
+
try {
|
|
861
|
+
var full = path.join(dir, entries[i]);
|
|
862
|
+
if (fs.statSync(full).isDirectory()) {
|
|
863
|
+
matches.push(entries[i]);
|
|
864
|
+
}
|
|
865
|
+
} catch (e) {}
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
if (matches.length === 0) return;
|
|
869
|
+
|
|
870
|
+
if (matches.length === 1) {
|
|
871
|
+
// Single match — complete it
|
|
872
|
+
var completed = path.join(dir, matches[0]) + path.sep;
|
|
873
|
+
text = completed;
|
|
874
|
+
showingPlaceholder = false;
|
|
875
|
+
clearHint();
|
|
876
|
+
redrawInput();
|
|
877
|
+
} else {
|
|
878
|
+
// Multiple matches — find longest common prefix and show candidates
|
|
879
|
+
var common = matches[0];
|
|
880
|
+
for (var m = 1; m < matches.length; m++) {
|
|
881
|
+
var k = 0;
|
|
882
|
+
while (k < common.length && k < matches[m].length && common.charAt(k) === matches[m].charAt(k)) k++;
|
|
883
|
+
common = common.substring(0, k);
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
if (common.length > partial.length) {
|
|
887
|
+
// Extend to common prefix
|
|
888
|
+
text = path.join(dir, common);
|
|
889
|
+
showingPlaceholder = false;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// Show candidates as hint
|
|
893
|
+
var display = matches.slice(0, 6).join(" ");
|
|
894
|
+
if (matches.length > 6) display += " " + a.dim + "+" + (matches.length - 6) + " more" + a.reset;
|
|
895
|
+
showHint(display);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
process.stdin.on("data", function onText(ch) {
|
|
900
|
+
if (ch === "\r" || ch === "\n") {
|
|
901
|
+
process.stdin.setRawMode(false);
|
|
902
|
+
process.stdin.pause();
|
|
903
|
+
process.stdin.removeListener("data", onText);
|
|
904
|
+
var result = text || placeholder;
|
|
905
|
+
clearHint();
|
|
906
|
+
process.stdout.write("\n");
|
|
907
|
+
clearUp(2);
|
|
908
|
+
log(sym.done + " " + title + " " + a.dim + "·" + a.reset + " " + result);
|
|
909
|
+
callback(result);
|
|
910
|
+
} else if (ch === "\x1b" || ch === "\x03") {
|
|
911
|
+
process.stdin.setRawMode(false);
|
|
912
|
+
process.stdin.pause();
|
|
913
|
+
process.stdin.removeListener("data", onText);
|
|
914
|
+
clearHint();
|
|
915
|
+
process.stdout.write("\n");
|
|
916
|
+
clearUp(2);
|
|
917
|
+
if (ch === "\x03") {
|
|
918
|
+
log(sym.end + " " + a.dim + "Cancelled" + a.reset);
|
|
919
|
+
process.exit(0);
|
|
920
|
+
}
|
|
921
|
+
callback(null);
|
|
922
|
+
} else if (ch === "\t") {
|
|
923
|
+
if (showingPlaceholder) {
|
|
924
|
+
// Accept placeholder first
|
|
925
|
+
text = placeholder;
|
|
926
|
+
showingPlaceholder = false;
|
|
927
|
+
redrawInput();
|
|
928
|
+
}
|
|
929
|
+
tabComplete();
|
|
930
|
+
} else if (ch === "\x7f" || ch === "\b") {
|
|
931
|
+
if (text.length > 0) {
|
|
932
|
+
text = text.slice(0, -1);
|
|
933
|
+
clearHint();
|
|
934
|
+
if (text.length === 0) {
|
|
935
|
+
// Re-show placeholder
|
|
936
|
+
showingPlaceholder = true;
|
|
937
|
+
process.stdout.write("\x1b[2K\r" + prefix + a.dim + placeholder + a.reset);
|
|
938
|
+
process.stdout.write("\r" + prefix);
|
|
939
|
+
} else {
|
|
940
|
+
redrawInput();
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
} else if (ch >= " ") {
|
|
944
|
+
if (showingPlaceholder) {
|
|
945
|
+
showingPlaceholder = false;
|
|
946
|
+
}
|
|
947
|
+
clearHint();
|
|
948
|
+
text += ch;
|
|
949
|
+
redrawInput();
|
|
950
|
+
}
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
/**
|
|
955
|
+
* Select menu: arrow keys to navigate, enter to select.
|
|
956
|
+
* items: [{ label, value, desc? }]
|
|
957
|
+
*/
|
|
958
|
+
function promptSelect(title, items, callback, opts) {
|
|
959
|
+
var idx = 0;
|
|
960
|
+
// Build hotkeys map: { key: handler }
|
|
961
|
+
var hotkeys = {};
|
|
962
|
+
if (opts && opts.key && opts.onKey) {
|
|
963
|
+
hotkeys[opts.key] = opts.onKey;
|
|
964
|
+
}
|
|
965
|
+
if (opts && opts.keys) {
|
|
966
|
+
for (var ki = 0; ki < opts.keys.length; ki++) {
|
|
967
|
+
hotkeys[opts.keys[ki].key] = opts.keys[ki].onKey;
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
var hintLines = null;
|
|
971
|
+
if (opts && opts.hint) {
|
|
972
|
+
hintLines = Array.isArray(opts.hint) ? opts.hint : [opts.hint];
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
function render() {
|
|
976
|
+
var out = "";
|
|
977
|
+
for (var i = 0; i < items.length; i++) {
|
|
978
|
+
var prefix = i === idx
|
|
979
|
+
? a.green + a.bold + " ● " + a.reset
|
|
980
|
+
: a.dim + " ○ " + a.reset;
|
|
981
|
+
out += " " + sym.bar + prefix + items[i].label + "\n";
|
|
982
|
+
}
|
|
983
|
+
return out;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
log(sym.pointer + " " + a.bold + title + a.reset);
|
|
987
|
+
process.stdout.write(render());
|
|
988
|
+
|
|
989
|
+
// Render hint lines below the menu tree
|
|
990
|
+
var hintBoxLines = 0;
|
|
991
|
+
if (hintLines) {
|
|
992
|
+
log(sym.end);
|
|
993
|
+
for (var h = 0; h < hintLines.length; h++) {
|
|
994
|
+
log(" " + gradient(hintLines[h]));
|
|
995
|
+
}
|
|
996
|
+
hintBoxLines = 1 + hintLines.length; // sym.end + lines
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
var lineCount = items.length + 1 + hintBoxLines;
|
|
1000
|
+
|
|
1001
|
+
process.stdin.setRawMode(true);
|
|
1002
|
+
process.stdin.resume();
|
|
1003
|
+
process.stdin.setEncoding("utf8");
|
|
1004
|
+
|
|
1005
|
+
process.stdin.on("data", function onSelect(ch) {
|
|
1006
|
+
if (ch === "\x1b[A") { // up
|
|
1007
|
+
if (idx > 0) idx--;
|
|
1008
|
+
} else if (ch === "\x1b[B") { // down
|
|
1009
|
+
if (idx < items.length - 1) idx++;
|
|
1010
|
+
} else if (ch === "\r" || ch === "\n") {
|
|
1011
|
+
process.stdin.setRawMode(false);
|
|
1012
|
+
process.stdin.pause();
|
|
1013
|
+
process.stdin.removeListener("data", onSelect);
|
|
1014
|
+
clearUp(lineCount);
|
|
1015
|
+
log(sym.done + " " + title + " " + a.dim + "·" + a.reset + " " + items[idx].label);
|
|
1016
|
+
callback(items[idx].value);
|
|
1017
|
+
return;
|
|
1018
|
+
} else if (ch === "\x03") {
|
|
1019
|
+
process.stdout.write("\n");
|
|
1020
|
+
process.exit(0);
|
|
1021
|
+
} else if (hotkeys[ch]) {
|
|
1022
|
+
process.stdin.setRawMode(false);
|
|
1023
|
+
process.stdin.pause();
|
|
1024
|
+
process.stdin.removeListener("data", onSelect);
|
|
1025
|
+
clearUp(lineCount);
|
|
1026
|
+
hotkeys[ch]();
|
|
1027
|
+
return;
|
|
1028
|
+
} else if (ch === "\x7f" || ch === "\b") {
|
|
1029
|
+
// Backspace — trigger "back" if available
|
|
1030
|
+
for (var bi = 0; bi < items.length; bi++) {
|
|
1031
|
+
if (items[bi].value === "back") {
|
|
1032
|
+
process.stdin.setRawMode(false);
|
|
1033
|
+
process.stdin.pause();
|
|
1034
|
+
process.stdin.removeListener("data", onSelect);
|
|
1035
|
+
clearUp(lineCount);
|
|
1036
|
+
log(sym.done + " " + title + " " + a.dim + "·" + a.reset + " " + items[bi].label);
|
|
1037
|
+
callback("back");
|
|
1038
|
+
return;
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
return;
|
|
1042
|
+
} else {
|
|
1043
|
+
return;
|
|
1044
|
+
}
|
|
1045
|
+
// Redraw
|
|
1046
|
+
clearUp(items.length + hintBoxLines);
|
|
1047
|
+
process.stdout.write(render());
|
|
1048
|
+
// Re-render hint lines
|
|
1049
|
+
if (hintLines) {
|
|
1050
|
+
log(sym.end);
|
|
1051
|
+
for (var rh = 0; rh < hintLines.length; rh++) {
|
|
1052
|
+
log(" " + gradient(hintLines[rh]));
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
});
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
/**
|
|
1059
|
+
* Multi-select menu: space to toggle, enter to confirm.
|
|
1060
|
+
* items: [{ label, value, checked? }]
|
|
1061
|
+
* callback(selectedValues[])
|
|
1062
|
+
*/
|
|
1063
|
+
function promptMultiSelect(title, items, callback) {
|
|
1064
|
+
var selected = [];
|
|
1065
|
+
for (var si = 0; si < items.length; si++) {
|
|
1066
|
+
selected.push(items[si].checked !== false);
|
|
1067
|
+
}
|
|
1068
|
+
var idx = 0;
|
|
1069
|
+
|
|
1070
|
+
function render() {
|
|
1071
|
+
var out = "";
|
|
1072
|
+
for (var i = 0; i < items.length; i++) {
|
|
1073
|
+
var cursor = i === idx ? a.clay + ">" + a.reset : " ";
|
|
1074
|
+
var check = selected[i]
|
|
1075
|
+
? a.green + a.bold + "■" + a.reset
|
|
1076
|
+
: a.dim + "□" + a.reset;
|
|
1077
|
+
out += " " + sym.bar + " " + cursor + " " + check + " " + items[i].label + "\n";
|
|
1078
|
+
}
|
|
1079
|
+
out += " " + sym.bar + " " + a.dim + "space: toggle · enter: confirm" + a.reset + "\n";
|
|
1080
|
+
return out;
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
log(sym.pointer + " " + a.bold + title + a.reset);
|
|
1084
|
+
process.stdout.write(render());
|
|
1085
|
+
|
|
1086
|
+
var lineCount = items.length + 2; // title + items + hint
|
|
1087
|
+
|
|
1088
|
+
process.stdin.setRawMode(true);
|
|
1089
|
+
process.stdin.resume();
|
|
1090
|
+
process.stdin.setEncoding("utf8");
|
|
1091
|
+
|
|
1092
|
+
process.stdin.on("data", function onMulti(ch) {
|
|
1093
|
+
if (ch === "\x1b[A") { // up
|
|
1094
|
+
if (idx > 0) idx--;
|
|
1095
|
+
} else if (ch === "\x1b[B") { // down
|
|
1096
|
+
if (idx < items.length - 1) idx++;
|
|
1097
|
+
} else if (ch === " ") { // toggle
|
|
1098
|
+
selected[idx] = !selected[idx];
|
|
1099
|
+
} else if (ch === "a" || ch === "A") { // toggle all
|
|
1100
|
+
var allSelected = selected.every(function (s) { return s; });
|
|
1101
|
+
for (var ai = 0; ai < selected.length; ai++) selected[ai] = !allSelected;
|
|
1102
|
+
} else if (ch === "\r" || ch === "\n") {
|
|
1103
|
+
process.stdin.setRawMode(false);
|
|
1104
|
+
process.stdin.pause();
|
|
1105
|
+
process.stdin.removeListener("data", onMulti);
|
|
1106
|
+
clearUp(lineCount);
|
|
1107
|
+
var result = [];
|
|
1108
|
+
var labels = [];
|
|
1109
|
+
for (var ri = 0; ri < items.length; ri++) {
|
|
1110
|
+
if (selected[ri]) {
|
|
1111
|
+
result.push(items[ri].value);
|
|
1112
|
+
labels.push(items[ri].label);
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
var summary = result.length === items.length
|
|
1116
|
+
? "All (" + result.length + ")"
|
|
1117
|
+
: result.length + " of " + items.length;
|
|
1118
|
+
log(sym.done + " " + title + " " + a.dim + "·" + a.reset + " " + summary);
|
|
1119
|
+
callback(result);
|
|
1120
|
+
return;
|
|
1121
|
+
} else if (ch === "\x03") {
|
|
1122
|
+
process.stdout.write("\n");
|
|
1123
|
+
process.exit(0);
|
|
1124
|
+
} else if (ch === "\x1b") {
|
|
1125
|
+
// Escape — select none
|
|
1126
|
+
process.stdin.setRawMode(false);
|
|
1127
|
+
process.stdin.pause();
|
|
1128
|
+
process.stdin.removeListener("data", onMulti);
|
|
1129
|
+
clearUp(lineCount);
|
|
1130
|
+
log(sym.done + " " + title + " " + a.dim + "· Skipped" + a.reset);
|
|
1131
|
+
callback([]);
|
|
1132
|
+
return;
|
|
1133
|
+
} else {
|
|
1134
|
+
return;
|
|
1135
|
+
}
|
|
1136
|
+
// Redraw
|
|
1137
|
+
clearUp(items.length + 1); // items + hint (not title)
|
|
1138
|
+
process.stdout.write(render());
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
// --- Port availability ---
|
|
1143
|
+
|
|
1144
|
+
function isPortFree(p) {
|
|
1145
|
+
return new Promise(function (resolve) {
|
|
1146
|
+
var srv = net.createServer();
|
|
1147
|
+
srv.once("error", function () { resolve(false); });
|
|
1148
|
+
srv.once("listening", function () { srv.close(function () { resolve(true); }); });
|
|
1149
|
+
srv.listen(p);
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
// --- Detect tools ---
|
|
1154
|
+
function getTailscaleIP() {
|
|
1155
|
+
var interfaces = os.networkInterfaces();
|
|
1156
|
+
for (var name in interfaces) {
|
|
1157
|
+
if (/^(tailscale|utun)/.test(name)) {
|
|
1158
|
+
for (var i = 0; i < interfaces[name].length; i++) {
|
|
1159
|
+
var addr = interfaces[name][i];
|
|
1160
|
+
if (addr.family === "IPv4" && !addr.internal && addr.address.startsWith("100.")) {
|
|
1161
|
+
return addr.address;
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
for (var addrs of Object.values(interfaces)) {
|
|
1167
|
+
for (var j = 0; j < addrs.length; j++) {
|
|
1168
|
+
if (addrs[j].family === "IPv4" && !addrs[j].internal && addrs[j].address.startsWith("100.")) {
|
|
1169
|
+
return addrs[j].address;
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
return null;
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
function hasTailscale() {
|
|
1177
|
+
return getTailscaleIP() !== null;
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
function hasMkcert() {
|
|
1181
|
+
try {
|
|
1182
|
+
execSync("mkcert -CAROOT", { stdio: "pipe", encoding: "utf8" });
|
|
1183
|
+
return true;
|
|
1184
|
+
} catch (e) { return false; }
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
// ==============================
|
|
1188
|
+
// Restore projects from ~/.clayrc
|
|
1189
|
+
// ==============================
|
|
1190
|
+
function promptRestoreProjects(projects, callback) {
|
|
1191
|
+
log(sym.bar);
|
|
1192
|
+
log(sym.pointer + " " + a.bold + "Previous projects found" + a.reset);
|
|
1193
|
+
log(sym.bar + " " + a.dim + "Restore projects from your last session?" + a.reset);
|
|
1194
|
+
log(sym.bar);
|
|
1195
|
+
|
|
1196
|
+
var items = projects.map(function (p) {
|
|
1197
|
+
var name = p.title || path.basename(p.path);
|
|
1198
|
+
return {
|
|
1199
|
+
label: a.bold + name + a.reset + " " + a.dim + p.path + a.reset,
|
|
1200
|
+
value: p,
|
|
1201
|
+
checked: true,
|
|
1202
|
+
};
|
|
1203
|
+
});
|
|
1204
|
+
|
|
1205
|
+
promptMultiSelect("Restore projects", items, function (selected) {
|
|
1206
|
+
// Remove unselected projects from ~/.clayrc
|
|
1207
|
+
if (selected.length < projects.length) {
|
|
1208
|
+
var selectedPaths = {};
|
|
1209
|
+
for (var si = 0; si < selected.length; si++) {
|
|
1210
|
+
selectedPaths[selected[si].path] = true;
|
|
1211
|
+
}
|
|
1212
|
+
try {
|
|
1213
|
+
var rc = loadClayrc();
|
|
1214
|
+
rc.recentProjects = (rc.recentProjects || []).filter(function (p) {
|
|
1215
|
+
return selectedPaths[p.path];
|
|
1216
|
+
});
|
|
1217
|
+
saveClayrc(rc);
|
|
1218
|
+
} catch (e) {}
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
log(sym.bar);
|
|
1222
|
+
if (selected.length > 0) {
|
|
1223
|
+
log(sym.done + " " + a.green + "Restoring " + selected.length + (selected.length === 1 ? " project" : " projects") + a.reset);
|
|
1224
|
+
} else {
|
|
1225
|
+
log(sym.done + " " + a.dim + "Starting fresh" + a.reset);
|
|
1226
|
+
}
|
|
1227
|
+
log(sym.end + " " + a.dim + "Starting relay..." + a.reset);
|
|
1228
|
+
log("");
|
|
1229
|
+
callback(selected);
|
|
1230
|
+
});
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
// ==============================
|
|
1234
|
+
// First-run setup (no daemon)
|
|
1235
|
+
// ==============================
|
|
1236
|
+
function setup(callback) {
|
|
1237
|
+
console.clear();
|
|
1238
|
+
printLogo();
|
|
1239
|
+
log("");
|
|
1240
|
+
log(sym.pointer + " " + a.bold + "Clay" + a.reset + a.dim + " · Unofficial, open-source project" + a.reset);
|
|
1241
|
+
log(sym.bar);
|
|
1242
|
+
log(sym.bar + " " + a.dim + "Anyone with the URL gets full Claude Code access to this machine." + a.reset);
|
|
1243
|
+
log(sym.bar + " " + a.dim + "Use a private network (Tailscale, VPN)." + a.reset);
|
|
1244
|
+
log(sym.bar + " " + a.dim + "The authors assume no responsibility for any damage or data loss." + a.reset);
|
|
1245
|
+
log(sym.bar);
|
|
1246
|
+
|
|
1247
|
+
promptToggle("Accept and continue", null, true, function (accepted) {
|
|
1248
|
+
if (!accepted) {
|
|
1249
|
+
log(sym.end + " " + a.dim + "Aborted." + a.reset);
|
|
1250
|
+
log("");
|
|
1251
|
+
process.exit(0);
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1254
|
+
log(sym.bar);
|
|
1255
|
+
|
|
1256
|
+
function askPort() {
|
|
1257
|
+
promptText("Port", String(port), function (val) {
|
|
1258
|
+
if (val === null) {
|
|
1259
|
+
log(sym.end + " " + a.dim + "Aborted." + a.reset);
|
|
1260
|
+
log("");
|
|
1261
|
+
process.exit(0);
|
|
1262
|
+
return;
|
|
1263
|
+
}
|
|
1264
|
+
var p = parseInt(val, 10);
|
|
1265
|
+
if (!p || p < 1 || p > 65535) {
|
|
1266
|
+
log(sym.warn + " " + a.red + "Invalid port number" + a.reset);
|
|
1267
|
+
askPort();
|
|
1268
|
+
return;
|
|
1269
|
+
}
|
|
1270
|
+
isPortFree(p).then(function (free) {
|
|
1271
|
+
if (!free) {
|
|
1272
|
+
log(sym.warn + " " + a.yellow + "Port " + p + " is already in use" + a.reset);
|
|
1273
|
+
askPort();
|
|
1274
|
+
return;
|
|
1275
|
+
}
|
|
1276
|
+
port = p;
|
|
1277
|
+
log(sym.bar);
|
|
1278
|
+
|
|
1279
|
+
function askPin() {
|
|
1280
|
+
promptPin(function (pin) {
|
|
1281
|
+
if (dangerouslySkipPermissions && !pin) {
|
|
1282
|
+
log(sym.bar);
|
|
1283
|
+
log(sym.warn + " " + a.yellow + "WARNING: No PIN + skip permissions = anyone with the URL" + a.reset);
|
|
1284
|
+
log(sym.bar + " " + a.yellow + "can execute any command without approval." + a.reset);
|
|
1285
|
+
log(sym.bar);
|
|
1286
|
+
promptToggle("Continue without PIN?", null, false, function (confirmed) {
|
|
1287
|
+
if (!confirmed) {
|
|
1288
|
+
clearUp(6);
|
|
1289
|
+
log(sym.done + " PIN protection " + a.dim + "·" + a.reset + " " + a.yellow + "Required for skip permissions" + a.reset);
|
|
1290
|
+
log(sym.bar);
|
|
1291
|
+
askPin();
|
|
1292
|
+
return;
|
|
1293
|
+
}
|
|
1294
|
+
afterPin(pin);
|
|
1295
|
+
});
|
|
1296
|
+
} else {
|
|
1297
|
+
afterPin(pin);
|
|
1298
|
+
}
|
|
1299
|
+
});
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
function afterPin(pin) {
|
|
1303
|
+
if (process.platform === "darwin") {
|
|
1304
|
+
promptToggle("Keep awake", "Prevent system sleep while relay is running", false, function (keepAwake) {
|
|
1305
|
+
callback(pin, keepAwake);
|
|
1306
|
+
});
|
|
1307
|
+
} else {
|
|
1308
|
+
callback(pin, false);
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
askPin();
|
|
1313
|
+
});
|
|
1314
|
+
});
|
|
1315
|
+
}
|
|
1316
|
+
askPort();
|
|
1317
|
+
});
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
// ==============================
|
|
1321
|
+
// Fork the daemon process
|
|
1322
|
+
// ==============================
|
|
1323
|
+
async function forkDaemon(pin, keepAwake, extraProjects, addCwd) {
|
|
1324
|
+
var ip = getLocalIP();
|
|
1325
|
+
var hasTls = false;
|
|
1326
|
+
|
|
1327
|
+
if (useHttps) {
|
|
1328
|
+
var certPaths = ensureCerts(ip);
|
|
1329
|
+
if (certPaths) {
|
|
1330
|
+
hasTls = true;
|
|
1331
|
+
} else {
|
|
1332
|
+
log(sym.warn + " " + a.yellow + "HTTPS unavailable" + a.reset + a.dim + " · mkcert not installed" + a.reset);
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
// Check port availability
|
|
1337
|
+
var portFree = await isPortFree(port);
|
|
1338
|
+
if (!portFree) {
|
|
1339
|
+
log(a.red + "Port " + port + " is already in use." + a.reset);
|
|
1340
|
+
log(a.dim + "Is another Clay daemon running?" + a.reset);
|
|
1341
|
+
process.exit(1);
|
|
1342
|
+
return;
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
var allProjects = [];
|
|
1346
|
+
var usedSlugs = [];
|
|
1347
|
+
|
|
1348
|
+
// Load previous config to preserve per-project settings (visibility, allowedUsers)
|
|
1349
|
+
var prevConfig = loadConfig();
|
|
1350
|
+
var prevProjectMap = {};
|
|
1351
|
+
if (prevConfig && prevConfig.projects) {
|
|
1352
|
+
for (var pi = 0; pi < prevConfig.projects.length; pi++) {
|
|
1353
|
+
prevProjectMap[prevConfig.projects[pi].path] = prevConfig.projects[pi];
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
// Only include cwd if explicitly requested
|
|
1358
|
+
if (addCwd) {
|
|
1359
|
+
var slug = generateSlug(cwd, []);
|
|
1360
|
+
var cwdEntry = { path: cwd, slug: slug, addedAt: Date.now() };
|
|
1361
|
+
// Restore title/icon from .clayrc if available
|
|
1362
|
+
var cwdRc = loadClayrc();
|
|
1363
|
+
var cwdRecent = cwdRc.recentProjects || [];
|
|
1364
|
+
for (var cr = 0; cr < cwdRecent.length; cr++) {
|
|
1365
|
+
if (cwdRecent[cr].path === cwd) {
|
|
1366
|
+
if (cwdRecent[cr].title) cwdEntry.title = cwdRecent[cr].title;
|
|
1367
|
+
if (cwdRecent[cr].icon) cwdEntry.icon = cwdRecent[cr].icon;
|
|
1368
|
+
break;
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
// Restore access settings from previous config
|
|
1372
|
+
if (prevProjectMap[cwd]) {
|
|
1373
|
+
if (prevProjectMap[cwd].visibility) cwdEntry.visibility = prevProjectMap[cwd].visibility;
|
|
1374
|
+
if (prevProjectMap[cwd].allowedUsers) cwdEntry.allowedUsers = prevProjectMap[cwd].allowedUsers;
|
|
1375
|
+
}
|
|
1376
|
+
allProjects.push(cwdEntry);
|
|
1377
|
+
usedSlugs.push(slug);
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
// Add restored projects (from ~/.clayrc)
|
|
1381
|
+
if (extraProjects && extraProjects.length > 0) {
|
|
1382
|
+
for (var ep = 0; ep < extraProjects.length; ep++) {
|
|
1383
|
+
var rp = extraProjects[ep];
|
|
1384
|
+
if (rp.path === cwd) continue; // skip if same as cwd
|
|
1385
|
+
if (!fs.existsSync(rp.path)) continue; // skip missing directories
|
|
1386
|
+
var rpSlug = generateSlug(rp.path, usedSlugs);
|
|
1387
|
+
usedSlugs.push(rpSlug);
|
|
1388
|
+
var rpEntry = { path: rp.path, slug: rpSlug, title: rp.title || undefined, icon: rp.icon || undefined, addedAt: rp.addedAt || Date.now() };
|
|
1389
|
+
// Restore access settings from previous config
|
|
1390
|
+
if (prevProjectMap[rp.path]) {
|
|
1391
|
+
if (prevProjectMap[rp.path].visibility) rpEntry.visibility = prevProjectMap[rp.path].visibility;
|
|
1392
|
+
if (prevProjectMap[rp.path].allowedUsers) rpEntry.allowedUsers = prevProjectMap[rp.path].allowedUsers;
|
|
1393
|
+
}
|
|
1394
|
+
allProjects.push(rpEntry);
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
var config = {
|
|
1399
|
+
pid: null,
|
|
1400
|
+
port: port,
|
|
1401
|
+
host: host,
|
|
1402
|
+
pinHash: pin ? generateAuthToken(pin) : null,
|
|
1403
|
+
tls: hasTls,
|
|
1404
|
+
debug: debugMode,
|
|
1405
|
+
keepAwake: keepAwake,
|
|
1406
|
+
dangerouslySkipPermissions: dangerouslySkipPermissions,
|
|
1407
|
+
projects: allProjects,
|
|
1408
|
+
};
|
|
1409
|
+
|
|
1410
|
+
ensureConfigDir();
|
|
1411
|
+
saveConfig(config);
|
|
1412
|
+
|
|
1413
|
+
// Fork daemon
|
|
1414
|
+
var daemonScript = path.join(__dirname, "..", "lib", "daemon.js");
|
|
1415
|
+
var logFile = logPath();
|
|
1416
|
+
var logFd = fs.openSync(logFile, "a");
|
|
1417
|
+
|
|
1418
|
+
var child = spawn(process.execPath, [daemonScript], {
|
|
1419
|
+
detached: true,
|
|
1420
|
+
windowsHide: true,
|
|
1421
|
+
stdio: ["ignore", logFd, logFd],
|
|
1422
|
+
env: Object.assign({}, process.env, {
|
|
1423
|
+
CLAY_CONFIG: configPath(),
|
|
1424
|
+
}),
|
|
1425
|
+
});
|
|
1426
|
+
child.unref();
|
|
1427
|
+
fs.closeSync(logFd);
|
|
1428
|
+
|
|
1429
|
+
// Update config with PID
|
|
1430
|
+
config.pid = child.pid;
|
|
1431
|
+
saveConfig(config);
|
|
1432
|
+
|
|
1433
|
+
// Wait for daemon to start (retry up to 5 seconds)
|
|
1434
|
+
var alive = false;
|
|
1435
|
+
for (var attempt = 0; attempt < 10; attempt++) {
|
|
1436
|
+
await new Promise(function (resolve) { setTimeout(resolve, 500); });
|
|
1437
|
+
alive = await isDaemonAliveAsync(config);
|
|
1438
|
+
if (alive) break;
|
|
1439
|
+
}
|
|
1440
|
+
if (!alive) {
|
|
1441
|
+
log(a.red + "Failed to start daemon. Check logs:" + a.reset);
|
|
1442
|
+
log(a.dim + logFile + a.reset);
|
|
1443
|
+
clearStaleConfig();
|
|
1444
|
+
process.exit(1);
|
|
1445
|
+
return;
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
// Headless mode — print status and exit immediately
|
|
1449
|
+
if (headlessMode) {
|
|
1450
|
+
var protocol = config.tls ? "https" : "http";
|
|
1451
|
+
var url = protocol + "://" + ip + ":" + config.port;
|
|
1452
|
+
console.log(" " + sym.done + " Daemon started (PID " + config.pid + ")");
|
|
1453
|
+
console.log(" " + sym.done + " " + url);
|
|
1454
|
+
console.log(" " + sym.done + " Headless mode — exiting CLI");
|
|
1455
|
+
process.exit(0);
|
|
1456
|
+
return;
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
// Show success + QR
|
|
1460
|
+
showServerStarted(config, ip);
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
// ==============================
|
|
1464
|
+
// Dev mode — foreground daemon with file watching
|
|
1465
|
+
// ==============================
|
|
1466
|
+
async function devMode(pin, keepAwake, existingPinHash) {
|
|
1467
|
+
var ip = getLocalIP();
|
|
1468
|
+
var hasTls = false;
|
|
1469
|
+
|
|
1470
|
+
if (useHttps) {
|
|
1471
|
+
var certPaths = ensureCerts(ip);
|
|
1472
|
+
if (certPaths) hasTls = true;
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
var portFree = await isPortFree(port);
|
|
1476
|
+
if (!portFree) {
|
|
1477
|
+
console.log("\x1b[31m[dev] Port " + port + " is already in use.\x1b[0m");
|
|
1478
|
+
process.exit(1);
|
|
1479
|
+
return;
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
var slug = generateSlug(cwd, []);
|
|
1483
|
+
var cwdDevEntry = { path: cwd, slug: slug, addedAt: Date.now() };
|
|
1484
|
+
|
|
1485
|
+
// Load previous config to preserve per-project settings (visibility, allowedUsers)
|
|
1486
|
+
var prevDevConfig = loadConfig();
|
|
1487
|
+
var prevDevProjectMap = {};
|
|
1488
|
+
if (prevDevConfig && prevDevConfig.projects) {
|
|
1489
|
+
for (var pdi = 0; pdi < prevDevConfig.projects.length; pdi++) {
|
|
1490
|
+
prevDevProjectMap[prevDevConfig.projects[pdi].path] = prevDevConfig.projects[pdi];
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
// Restore previous projects
|
|
1495
|
+
var rc = loadClayrc();
|
|
1496
|
+
var restorable = (rc.recentProjects || []).filter(function (p) {
|
|
1497
|
+
return p.path !== cwd && fs.existsSync(p.path);
|
|
1498
|
+
});
|
|
1499
|
+
// Restore title/icon for cwd from .clayrc
|
|
1500
|
+
var rcAll = rc.recentProjects || [];
|
|
1501
|
+
for (var ci = 0; ci < rcAll.length; ci++) {
|
|
1502
|
+
if (rcAll[ci].path === cwd) {
|
|
1503
|
+
if (rcAll[ci].title) cwdDevEntry.title = rcAll[ci].title;
|
|
1504
|
+
if (rcAll[ci].icon) cwdDevEntry.icon = rcAll[ci].icon;
|
|
1505
|
+
break;
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
// Restore access settings for cwd from previous config
|
|
1509
|
+
if (prevDevProjectMap[cwd]) {
|
|
1510
|
+
if (prevDevProjectMap[cwd].visibility) cwdDevEntry.visibility = prevDevProjectMap[cwd].visibility;
|
|
1511
|
+
if (prevDevProjectMap[cwd].allowedUsers) cwdDevEntry.allowedUsers = prevDevProjectMap[cwd].allowedUsers;
|
|
1512
|
+
}
|
|
1513
|
+
var allProjects = [cwdDevEntry];
|
|
1514
|
+
var usedSlugs = [slug];
|
|
1515
|
+
for (var ri = 0; ri < restorable.length; ri++) {
|
|
1516
|
+
var rp = restorable[ri];
|
|
1517
|
+
var rpSlug = generateSlug(rp.path, usedSlugs);
|
|
1518
|
+
usedSlugs.push(rpSlug);
|
|
1519
|
+
var rpDevEntry = { path: rp.path, slug: rpSlug, title: rp.title || undefined, icon: rp.icon || undefined, addedAt: rp.addedAt || Date.now() };
|
|
1520
|
+
// Restore access settings from previous config
|
|
1521
|
+
if (prevDevProjectMap[rp.path]) {
|
|
1522
|
+
if (prevDevProjectMap[rp.path].visibility) rpDevEntry.visibility = prevDevProjectMap[rp.path].visibility;
|
|
1523
|
+
if (prevDevProjectMap[rp.path].allowedUsers) rpDevEntry.allowedUsers = prevDevProjectMap[rp.path].allowedUsers;
|
|
1524
|
+
}
|
|
1525
|
+
allProjects.push(rpDevEntry);
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
var config = {
|
|
1529
|
+
pid: null,
|
|
1530
|
+
port: port,
|
|
1531
|
+
host: host,
|
|
1532
|
+
pinHash: existingPinHash || (pin ? generateAuthToken(pin) : null),
|
|
1533
|
+
tls: hasTls,
|
|
1534
|
+
debug: true,
|
|
1535
|
+
keepAwake: keepAwake || false,
|
|
1536
|
+
dangerouslySkipPermissions: dangerouslySkipPermissions,
|
|
1537
|
+
projects: allProjects,
|
|
1538
|
+
};
|
|
1539
|
+
|
|
1540
|
+
ensureConfigDir();
|
|
1541
|
+
saveConfig(config);
|
|
1542
|
+
|
|
1543
|
+
var daemonScript = path.join(__dirname, "..", "lib", "daemon.js");
|
|
1544
|
+
var libDir = path.join(__dirname, "..", "lib");
|
|
1545
|
+
var child = null;
|
|
1546
|
+
var intentionalKill = false;
|
|
1547
|
+
var debounceTimer = null;
|
|
1548
|
+
|
|
1549
|
+
function spawnDaemon() {
|
|
1550
|
+
child = spawn(process.execPath, [daemonScript], {
|
|
1551
|
+
stdio: ["ignore", "inherit", "inherit"],
|
|
1552
|
+
env: Object.assign({}, process.env, {
|
|
1553
|
+
CLAY_CONFIG: configPath(),
|
|
1554
|
+
}),
|
|
1555
|
+
});
|
|
1556
|
+
|
|
1557
|
+
child.on("exit", function (code) {
|
|
1558
|
+
child = null;
|
|
1559
|
+
if (intentionalKill) {
|
|
1560
|
+
intentionalKill = false;
|
|
1561
|
+
return;
|
|
1562
|
+
}
|
|
1563
|
+
// Exit code 120 = update restart — respawn daemon with current dev code
|
|
1564
|
+
if (code === 120) {
|
|
1565
|
+
console.log("\x1b[38;2;0;183;133m[dev]\x1b[0m Update restart — respawning daemon...");
|
|
1566
|
+
console.log("");
|
|
1567
|
+
setTimeout(spawnDaemon, 500);
|
|
1568
|
+
return;
|
|
1569
|
+
}
|
|
1570
|
+
// Exit code 78 = fatal config error (e.g. Node version too old) — don't restart
|
|
1571
|
+
if (code === 78) {
|
|
1572
|
+
console.log("\x1b[31m[dev] Daemon exited with fatal error (code 78). Not restarting.\x1b[0m");
|
|
1573
|
+
process.exit(78);
|
|
1574
|
+
return;
|
|
1575
|
+
}
|
|
1576
|
+
// Unexpected exit — auto restart
|
|
1577
|
+
console.log("\x1b[33m[dev] Daemon exited (code " + code + "), restarting...\x1b[0m");
|
|
1578
|
+
setTimeout(spawnDaemon, 500);
|
|
1579
|
+
});
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
function restartDaemon() {
|
|
1583
|
+
intentionalKill = true;
|
|
1584
|
+
if (child) {
|
|
1585
|
+
child.kill("SIGTERM");
|
|
1586
|
+
// Give it a moment to shut down, then spawn
|
|
1587
|
+
setTimeout(spawnDaemon, 300);
|
|
1588
|
+
} else {
|
|
1589
|
+
intentionalKill = false;
|
|
1590
|
+
spawnDaemon();
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
console.log("\x1b[38;2;0;183;133m[dev]\x1b[0m Starting relay on port " + port + "...");
|
|
1595
|
+
if (watchMode) {
|
|
1596
|
+
console.log("\x1b[38;2;0;183;133m[dev]\x1b[0m Watching lib/ for changes (excluding lib/public/)");
|
|
1597
|
+
}
|
|
1598
|
+
console.log("");
|
|
1599
|
+
|
|
1600
|
+
spawnDaemon();
|
|
1601
|
+
|
|
1602
|
+
// Wait for daemon to be ready, then show CLI menu
|
|
1603
|
+
config.pid = child ? child.pid : null;
|
|
1604
|
+
saveConfig(config);
|
|
1605
|
+
|
|
1606
|
+
var daemonReady = false;
|
|
1607
|
+
for (var da = 0; da < 10; da++) {
|
|
1608
|
+
await new Promise(function (resolve) { setTimeout(resolve, 500); });
|
|
1609
|
+
daemonReady = await isDaemonAliveAsync(config);
|
|
1610
|
+
if (daemonReady) break;
|
|
1611
|
+
}
|
|
1612
|
+
if (daemonReady) {
|
|
1613
|
+
showServerStarted(config, ip);
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
// Watch lib/ for server-side file changes (only with --watch)
|
|
1617
|
+
var watcher = null;
|
|
1618
|
+
if (watchMode) {
|
|
1619
|
+
watcher = fs.watch(libDir, { recursive: true }, function (eventType, filename) {
|
|
1620
|
+
if (!filename) return;
|
|
1621
|
+
// Skip client-side files — they're served from disk
|
|
1622
|
+
if (filename.startsWith("public" + path.sep) || filename.startsWith("public/")) return;
|
|
1623
|
+
// Skip non-JS files
|
|
1624
|
+
if (!filename.endsWith(".js")) return;
|
|
1625
|
+
|
|
1626
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
1627
|
+
debounceTimer = setTimeout(function () {
|
|
1628
|
+
console.log("\x1b[38;2;0;183;133m[dev]\x1b[0m File changed: lib/" + filename);
|
|
1629
|
+
console.log("\x1b[38;2;0;183;133m[dev]\x1b[0m Restarting...");
|
|
1630
|
+
console.log("");
|
|
1631
|
+
restartDaemon();
|
|
1632
|
+
}, 300);
|
|
1633
|
+
});
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
// Clean exit on Ctrl+C
|
|
1637
|
+
var shuttingDown = false;
|
|
1638
|
+
process.on("SIGINT", function () {
|
|
1639
|
+
if (shuttingDown) return;
|
|
1640
|
+
shuttingDown = true;
|
|
1641
|
+
console.log("\n\x1b[38;2;0;183;133m[dev]\x1b[0m Shutting down...");
|
|
1642
|
+
if (watcher) watcher.close();
|
|
1643
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
1644
|
+
intentionalKill = true;
|
|
1645
|
+
if (child) {
|
|
1646
|
+
child.kill("SIGTERM");
|
|
1647
|
+
child.on("exit", function () {
|
|
1648
|
+
clearStaleConfig();
|
|
1649
|
+
process.exit(0);
|
|
1650
|
+
});
|
|
1651
|
+
// Force kill after 3s
|
|
1652
|
+
setTimeout(function () { process.exit(0); }, 3000);
|
|
1653
|
+
} else {
|
|
1654
|
+
clearStaleConfig();
|
|
1655
|
+
process.exit(0);
|
|
1656
|
+
}
|
|
1657
|
+
});
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
// ==============================
|
|
1661
|
+
// Restart daemon with TLS enabled
|
|
1662
|
+
// ==============================
|
|
1663
|
+
async function restartDaemonWithTLS(config, callback) {
|
|
1664
|
+
var ip = getLocalIP();
|
|
1665
|
+
var certPaths = ensureCerts(ip);
|
|
1666
|
+
if (!certPaths) {
|
|
1667
|
+
callback(config);
|
|
1668
|
+
return;
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
// Shut down old daemon
|
|
1672
|
+
stopDaemonWatcher();
|
|
1673
|
+
try {
|
|
1674
|
+
await sendIPCCommand(socketPath(), { cmd: "shutdown" });
|
|
1675
|
+
} catch (e) {}
|
|
1676
|
+
|
|
1677
|
+
// Wait for port to be released
|
|
1678
|
+
var waited = 0;
|
|
1679
|
+
while (waited < 5000) {
|
|
1680
|
+
await new Promise(function (resolve) { setTimeout(resolve, 300); });
|
|
1681
|
+
waited += 300;
|
|
1682
|
+
var free = await isPortFree(config.port);
|
|
1683
|
+
if (free) break;
|
|
1684
|
+
}
|
|
1685
|
+
clearStaleConfig();
|
|
1686
|
+
|
|
1687
|
+
// Re-fork with TLS
|
|
1688
|
+
var newConfig = {
|
|
1689
|
+
pid: null,
|
|
1690
|
+
port: config.port,
|
|
1691
|
+
pinHash: config.pinHash || null,
|
|
1692
|
+
tls: true,
|
|
1693
|
+
debug: config.debug || false,
|
|
1694
|
+
keepAwake: config.keepAwake || false,
|
|
1695
|
+
dangerouslySkipPermissions: config.dangerouslySkipPermissions || false,
|
|
1696
|
+
projects: config.projects || [],
|
|
1697
|
+
};
|
|
1698
|
+
|
|
1699
|
+
ensureConfigDir();
|
|
1700
|
+
saveConfig(newConfig);
|
|
1701
|
+
|
|
1702
|
+
var daemonScript = path.join(__dirname, "..", "lib", "daemon.js");
|
|
1703
|
+
var logFile = logPath();
|
|
1704
|
+
var logFd = fs.openSync(logFile, "a");
|
|
1705
|
+
|
|
1706
|
+
var child = spawn(process.execPath, [daemonScript], {
|
|
1707
|
+
detached: true,
|
|
1708
|
+
windowsHide: true,
|
|
1709
|
+
stdio: ["ignore", logFd, logFd],
|
|
1710
|
+
env: Object.assign({}, process.env, {
|
|
1711
|
+
CLAY_CONFIG: configPath(),
|
|
1712
|
+
}),
|
|
1713
|
+
});
|
|
1714
|
+
child.unref();
|
|
1715
|
+
fs.closeSync(logFd);
|
|
1716
|
+
|
|
1717
|
+
newConfig.pid = child.pid;
|
|
1718
|
+
saveConfig(newConfig);
|
|
1719
|
+
|
|
1720
|
+
var alive = false;
|
|
1721
|
+
for (var ra = 0; ra < 10; ra++) {
|
|
1722
|
+
await new Promise(function (resolve) { setTimeout(resolve, 500); });
|
|
1723
|
+
alive = await isDaemonAliveAsync(newConfig);
|
|
1724
|
+
if (alive) break;
|
|
1725
|
+
}
|
|
1726
|
+
if (!alive) {
|
|
1727
|
+
log(sym.warn + " " + a.yellow + "Failed to restart with HTTPS, falling back to HTTP..." + a.reset);
|
|
1728
|
+
// Re-fork without TLS so the server is at least running
|
|
1729
|
+
newConfig.tls = false;
|
|
1730
|
+
saveConfig(newConfig);
|
|
1731
|
+
var logFd2 = fs.openSync(logFile, "a");
|
|
1732
|
+
var child2 = spawn(process.execPath, [daemonScript], {
|
|
1733
|
+
detached: true,
|
|
1734
|
+
windowsHide: true,
|
|
1735
|
+
stdio: ["ignore", logFd2, logFd2],
|
|
1736
|
+
env: Object.assign({}, process.env, {
|
|
1737
|
+
CLAY_CONFIG: configPath(),
|
|
1738
|
+
}),
|
|
1739
|
+
});
|
|
1740
|
+
child2.unref();
|
|
1741
|
+
fs.closeSync(logFd2);
|
|
1742
|
+
newConfig.pid = child2.pid;
|
|
1743
|
+
saveConfig(newConfig);
|
|
1744
|
+
for (var rb = 0; rb < 10; rb++) {
|
|
1745
|
+
await new Promise(function (resolve) { setTimeout(resolve, 500); });
|
|
1746
|
+
var retryAlive = await isDaemonAliveAsync(newConfig);
|
|
1747
|
+
if (retryAlive) break;
|
|
1748
|
+
}
|
|
1749
|
+
startDaemonWatcher();
|
|
1750
|
+
callback(newConfig);
|
|
1751
|
+
return;
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
startDaemonWatcher();
|
|
1755
|
+
callback(newConfig);
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
// ==============================
|
|
1759
|
+
// Show server started info
|
|
1760
|
+
// ==============================
|
|
1761
|
+
function showServerStarted(config, ip) {
|
|
1762
|
+
showMainMenu(config, ip);
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
// ==============================
|
|
1766
|
+
// Main management menu
|
|
1767
|
+
// ==============================
|
|
1768
|
+
function showMainMenu(config, ip) {
|
|
1769
|
+
startDaemonWatcher();
|
|
1770
|
+
var protocol = config.tls ? "https" : "http";
|
|
1771
|
+
var url = protocol + "://" + ip + ":" + config.port;
|
|
1772
|
+
|
|
1773
|
+
sendIPCCommand(socketPath(), { cmd: "get_status" }).then(function (status) {
|
|
1774
|
+
var projs = (status && status.projects) || [];
|
|
1775
|
+
var totalSessions = 0;
|
|
1776
|
+
var totalAwaiting = 0;
|
|
1777
|
+
for (var i = 0; i < projs.length; i++) {
|
|
1778
|
+
totalSessions += projs[i].sessions || 0;
|
|
1779
|
+
if (projs[i].isProcessing) totalAwaiting++;
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
console.clear();
|
|
1783
|
+
printLogo();
|
|
1784
|
+
log("");
|
|
1785
|
+
|
|
1786
|
+
function afterQr() {
|
|
1787
|
+
// Status line
|
|
1788
|
+
log(" " + a.dim + "clay" + a.reset + " " + a.dim + "v" + currentVersion + a.reset + a.dim + " — " + url + a.reset);
|
|
1789
|
+
var parts = [];
|
|
1790
|
+
parts.push(a.bold + projs.length + a.reset + a.dim + (projs.length === 1 ? " project" : " projects"));
|
|
1791
|
+
parts.push(a.reset + a.bold + totalSessions + a.reset + a.dim + (totalSessions === 1 ? " session" : " sessions"));
|
|
1792
|
+
if (totalAwaiting > 0) {
|
|
1793
|
+
parts.push(a.reset + a.yellow + a.bold + totalAwaiting + a.reset + a.yellow + " awaiting" + a.reset + a.dim);
|
|
1794
|
+
}
|
|
1795
|
+
log(" " + a.dim + parts.join(a.reset + a.dim + " · ") + a.reset);
|
|
1796
|
+
log(" Press " + a.bold + "o" + a.reset + " to open in browser");
|
|
1797
|
+
log("");
|
|
1798
|
+
|
|
1799
|
+
showMenuItems();
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
if (ip !== "localhost") {
|
|
1803
|
+
qrcode.generate(url, { small: !isBasicTerm }, function (code) {
|
|
1804
|
+
var lines = code.split("\n").map(function (l) { return " " + l; }).join("\n");
|
|
1805
|
+
console.log(lines);
|
|
1806
|
+
afterQr();
|
|
1807
|
+
});
|
|
1808
|
+
} else {
|
|
1809
|
+
log(a.bold + " " + url + a.reset);
|
|
1810
|
+
log("");
|
|
1811
|
+
afterQr();
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
function showMenuItems() {
|
|
1815
|
+
var items = [
|
|
1816
|
+
{ label: "Setup notifications", value: "notifications" },
|
|
1817
|
+
{ label: "Projects", value: "projects" },
|
|
1818
|
+
{ label: "Settings", value: "settings" },
|
|
1819
|
+
{ label: "Shut down server", value: "shutdown" },
|
|
1820
|
+
{ label: "Keep server alive & exit", value: "exit" },
|
|
1821
|
+
];
|
|
1822
|
+
|
|
1823
|
+
promptSelect("What would you like to do?", items, function (choice) {
|
|
1824
|
+
switch (choice) {
|
|
1825
|
+
case "notifications":
|
|
1826
|
+
showSetupGuide(config, ip, function () {
|
|
1827
|
+
config = loadConfig() || config;
|
|
1828
|
+
showMainMenu(config, ip);
|
|
1829
|
+
});
|
|
1830
|
+
break;
|
|
1831
|
+
|
|
1832
|
+
case "projects":
|
|
1833
|
+
showProjectsMenu(config, ip);
|
|
1834
|
+
break;
|
|
1835
|
+
|
|
1836
|
+
case "settings":
|
|
1837
|
+
showSettingsMenu(config, ip);
|
|
1838
|
+
break;
|
|
1839
|
+
|
|
1840
|
+
case "shutdown":
|
|
1841
|
+
log(sym.bar);
|
|
1842
|
+
log(sym.bar + " " + a.yellow + "This will stop the server completely." + a.reset);
|
|
1843
|
+
log(sym.bar + " " + a.dim + "All connected sessions will be disconnected." + a.reset);
|
|
1844
|
+
log(sym.bar);
|
|
1845
|
+
promptSelect("Are you sure?", [
|
|
1846
|
+
{ label: "Cancel", value: "cancel" },
|
|
1847
|
+
{ label: "Shut down", value: "confirm" },
|
|
1848
|
+
], function (confirm) {
|
|
1849
|
+
if (confirm === "confirm") {
|
|
1850
|
+
stopDaemonWatcher();
|
|
1851
|
+
sendIPCCommand(socketPath(), { cmd: "shutdown" }).then(function () {
|
|
1852
|
+
log(sym.done + " " + a.green + "Server stopped." + a.reset);
|
|
1853
|
+
log("");
|
|
1854
|
+
clearStaleConfig();
|
|
1855
|
+
process.exit(0);
|
|
1856
|
+
});
|
|
1857
|
+
} else {
|
|
1858
|
+
showMainMenu(config, ip);
|
|
1859
|
+
}
|
|
1860
|
+
});
|
|
1861
|
+
break;
|
|
1862
|
+
|
|
1863
|
+
case "exit":
|
|
1864
|
+
log("");
|
|
1865
|
+
log(" " + a.bold + "Bye!" + a.reset + " " + a.dim + "Server is still running in background." + a.reset);
|
|
1866
|
+
log(" " + a.dim + "Run " + a.reset + "npx clay-server" + a.dim + " to come back here." + a.reset);
|
|
1867
|
+
log("");
|
|
1868
|
+
process.exit(0);
|
|
1869
|
+
break;
|
|
1870
|
+
}
|
|
1871
|
+
}, {
|
|
1872
|
+
hint: [
|
|
1873
|
+
"claude-relay has been renamed to clay-server · npx clay-server",
|
|
1874
|
+
"Run npx clay-server in other directories to add more projects.",
|
|
1875
|
+
"★ github.com/chadbyte/claude-relay — Press s to star the repo",
|
|
1876
|
+
],
|
|
1877
|
+
keys: [
|
|
1878
|
+
{ key: "o", onKey: function () {
|
|
1879
|
+
openUrl(url);
|
|
1880
|
+
showMainMenu(config, ip);
|
|
1881
|
+
}},
|
|
1882
|
+
{ key: "s", onKey: function () {
|
|
1883
|
+
openUrl("https://github.com/chadbyte/claude-relay");
|
|
1884
|
+
showMainMenu(config, ip);
|
|
1885
|
+
}},
|
|
1886
|
+
],
|
|
1887
|
+
});
|
|
1888
|
+
}
|
|
1889
|
+
});
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
// ==============================
|
|
1893
|
+
// Projects sub-menu
|
|
1894
|
+
// ==============================
|
|
1895
|
+
function showProjectsMenu(config, ip) {
|
|
1896
|
+
sendIPCCommand(socketPath(), { cmd: "get_status" }).then(function (status) {
|
|
1897
|
+
if (!status.ok) {
|
|
1898
|
+
log(a.red + "Failed to get status" + a.reset);
|
|
1899
|
+
showMainMenu(config, ip);
|
|
1900
|
+
return;
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
console.clear();
|
|
1904
|
+
printLogo();
|
|
1905
|
+
log("");
|
|
1906
|
+
log(sym.pointer + " " + a.bold + "Projects" + a.reset);
|
|
1907
|
+
log(sym.bar);
|
|
1908
|
+
|
|
1909
|
+
var projs = status.projects || [];
|
|
1910
|
+
for (var i = 0; i < projs.length; i++) {
|
|
1911
|
+
var p = projs[i];
|
|
1912
|
+
var statusIcon = p.isProcessing ? "⚡" : (p.clients > 0 ? "🟢" : "⏸");
|
|
1913
|
+
var sessionLabel = p.sessions === 1 ? "1 session" : p.sessions + " sessions";
|
|
1914
|
+
var projName = p.title || p.project;
|
|
1915
|
+
log(sym.bar + " " + a.bold + projName + a.reset + " " + sessionLabel + " " + statusIcon);
|
|
1916
|
+
log(sym.bar + " " + a.dim + p.path + a.reset);
|
|
1917
|
+
if (i < projs.length - 1) log(sym.bar);
|
|
1918
|
+
}
|
|
1919
|
+
log(sym.bar);
|
|
1920
|
+
|
|
1921
|
+
// Build menu items
|
|
1922
|
+
var items = [];
|
|
1923
|
+
|
|
1924
|
+
// Check if cwd is already registered
|
|
1925
|
+
var cwdRegistered = false;
|
|
1926
|
+
for (var j = 0; j < projs.length; j++) {
|
|
1927
|
+
if (projs[j].path === cwd) {
|
|
1928
|
+
cwdRegistered = true;
|
|
1929
|
+
break;
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
if (!cwdRegistered) {
|
|
1933
|
+
items.push({ label: "+ Add " + a.bold + path.basename(cwd) + a.reset + " " + a.dim + "(" + cwd + ")" + a.reset, value: "add_cwd" });
|
|
1934
|
+
}
|
|
1935
|
+
items.push({ label: "+ Add project...", value: "add_other" });
|
|
1936
|
+
|
|
1937
|
+
for (var k = 0; k < projs.length; k++) {
|
|
1938
|
+
var itemLabel = projs[k].title || projs[k].project;
|
|
1939
|
+
items.push({ label: itemLabel, value: "detail:" + projs[k].slug });
|
|
1940
|
+
}
|
|
1941
|
+
items.push({ label: "Back", value: "back" });
|
|
1942
|
+
|
|
1943
|
+
promptSelect("Select", items, function (choice) {
|
|
1944
|
+
if (choice === "back") {
|
|
1945
|
+
console.clear();
|
|
1946
|
+
printLogo();
|
|
1947
|
+
log("");
|
|
1948
|
+
showMainMenu(config, ip);
|
|
1949
|
+
} else if (choice === "add_cwd") {
|
|
1950
|
+
sendIPCCommand(socketPath(), { cmd: "add_project", path: cwd }).then(function (res) {
|
|
1951
|
+
if (res.ok) {
|
|
1952
|
+
log(sym.done + " " + a.green + "Added: " + res.slug + a.reset);
|
|
1953
|
+
config = loadConfig() || config;
|
|
1954
|
+
} else {
|
|
1955
|
+
log(sym.warn + " " + a.yellow + (res.error || "Failed") + a.reset);
|
|
1956
|
+
}
|
|
1957
|
+
log("");
|
|
1958
|
+
showProjectsMenu(config, ip);
|
|
1959
|
+
});
|
|
1960
|
+
} else if (choice === "add_other") {
|
|
1961
|
+
log(sym.bar);
|
|
1962
|
+
promptText("Directory path", cwd, function (dirPath) {
|
|
1963
|
+
if (dirPath === null) {
|
|
1964
|
+
showProjectsMenu(config, ip);
|
|
1965
|
+
return;
|
|
1966
|
+
}
|
|
1967
|
+
var absPath = path.resolve(dirPath);
|
|
1968
|
+
try {
|
|
1969
|
+
var stat = fs.statSync(absPath);
|
|
1970
|
+
if (!stat.isDirectory()) {
|
|
1971
|
+
log(sym.warn + " " + a.red + "Not a directory: " + absPath + a.reset);
|
|
1972
|
+
setTimeout(function () { showProjectsMenu(config, ip); }, 2000);
|
|
1973
|
+
return;
|
|
1974
|
+
}
|
|
1975
|
+
} catch (e) {
|
|
1976
|
+
log(sym.warn + " " + a.red + "Directory not found: " + absPath + a.reset);
|
|
1977
|
+
setTimeout(function () { showProjectsMenu(config, ip); }, 2000);
|
|
1978
|
+
return;
|
|
1979
|
+
}
|
|
1980
|
+
var alreadyExists = false;
|
|
1981
|
+
for (var pi = 0; pi < projs.length; pi++) {
|
|
1982
|
+
if (projs[pi].path === absPath) {
|
|
1983
|
+
alreadyExists = true;
|
|
1984
|
+
break;
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
if (alreadyExists) {
|
|
1988
|
+
log(sym.done + " " + a.yellow + "Already added: " + path.basename(absPath) + a.reset + " " + a.dim + "(" + absPath + ")" + a.reset);
|
|
1989
|
+
setTimeout(function () { showProjectsMenu(config, ip); }, 2000);
|
|
1990
|
+
return;
|
|
1991
|
+
}
|
|
1992
|
+
sendIPCCommand(socketPath(), { cmd: "add_project", path: absPath }).then(function (res) {
|
|
1993
|
+
if (res.ok) {
|
|
1994
|
+
log(sym.done + " " + a.green + "Added: " + res.slug + a.reset + " " + a.dim + "(" + absPath + ")" + a.reset);
|
|
1995
|
+
config = loadConfig() || config;
|
|
1996
|
+
} else {
|
|
1997
|
+
log(sym.warn + " " + a.yellow + (res.error || "Failed") + a.reset);
|
|
1998
|
+
}
|
|
1999
|
+
setTimeout(function () { showProjectsMenu(config, ip); }, 2000);
|
|
2000
|
+
});
|
|
2001
|
+
});
|
|
2002
|
+
} else if (choice.startsWith("detail:")) {
|
|
2003
|
+
var detailSlug = choice.substring(7);
|
|
2004
|
+
showProjectDetail(config, ip, detailSlug, projs);
|
|
2005
|
+
}
|
|
2006
|
+
});
|
|
2007
|
+
});
|
|
2008
|
+
}
|
|
2009
|
+
|
|
2010
|
+
// ==============================
|
|
2011
|
+
// Project detail
|
|
2012
|
+
// ==============================
|
|
2013
|
+
function showProjectDetail(config, ip, slug, projects) {
|
|
2014
|
+
var proj = null;
|
|
2015
|
+
for (var i = 0; i < projects.length; i++) {
|
|
2016
|
+
if (projects[i].slug === slug) {
|
|
2017
|
+
proj = projects[i];
|
|
2018
|
+
break;
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
if (!proj) {
|
|
2022
|
+
showProjectsMenu(config, ip);
|
|
2023
|
+
return;
|
|
2024
|
+
}
|
|
2025
|
+
|
|
2026
|
+
var displayName = proj.title || proj.project;
|
|
2027
|
+
|
|
2028
|
+
console.clear();
|
|
2029
|
+
printLogo();
|
|
2030
|
+
log("");
|
|
2031
|
+
log(sym.pointer + " " + a.bold + displayName + a.reset + " " + a.dim + proj.slug + " · " + proj.path + a.reset);
|
|
2032
|
+
log(sym.bar);
|
|
2033
|
+
var sessionLabel = proj.sessions === 1 ? "1 session" : proj.sessions + " sessions";
|
|
2034
|
+
var clientLabel = proj.clients === 1 ? "1 client" : proj.clients + " clients";
|
|
2035
|
+
log(sym.bar + " " + sessionLabel + " · " + clientLabel);
|
|
2036
|
+
if (proj.title) {
|
|
2037
|
+
log(sym.bar + " " + a.dim + "Title: " + a.reset + proj.title);
|
|
2038
|
+
}
|
|
2039
|
+
log(sym.bar);
|
|
2040
|
+
|
|
2041
|
+
var items = [
|
|
2042
|
+
{ label: proj.title ? "Change title" : "Set title", value: "title" },
|
|
2043
|
+
{ label: "Remove project", value: "remove" },
|
|
2044
|
+
{ label: "Back", value: "back" },
|
|
2045
|
+
];
|
|
2046
|
+
|
|
2047
|
+
promptSelect("What would you like to do?", items, function (choice) {
|
|
2048
|
+
if (choice === "title") {
|
|
2049
|
+
log(sym.bar);
|
|
2050
|
+
promptText("Project title", proj.title || proj.project, function (newTitle) {
|
|
2051
|
+
if (newTitle === null) {
|
|
2052
|
+
showProjectDetail(config, ip, slug, projects);
|
|
2053
|
+
return;
|
|
2054
|
+
}
|
|
2055
|
+
var titleVal = newTitle.trim();
|
|
2056
|
+
// If same as directory name, clear custom title
|
|
2057
|
+
if (titleVal === proj.project || titleVal === "") {
|
|
2058
|
+
titleVal = null;
|
|
2059
|
+
}
|
|
2060
|
+
sendIPCCommand(socketPath(), { cmd: "set_project_title", slug: slug, title: titleVal }).then(function (res) {
|
|
2061
|
+
if (res.ok) {
|
|
2062
|
+
proj.title = titleVal;
|
|
2063
|
+
config = loadConfig() || config;
|
|
2064
|
+
log(sym.done + " " + a.green + "Title updated" + a.reset);
|
|
2065
|
+
} else {
|
|
2066
|
+
log(sym.warn + " " + a.yellow + (res.error || "Failed") + a.reset);
|
|
2067
|
+
}
|
|
2068
|
+
log("");
|
|
2069
|
+
showProjectDetail(config, ip, slug, projects);
|
|
2070
|
+
});
|
|
2071
|
+
});
|
|
2072
|
+
} else if (choice === "remove") {
|
|
2073
|
+
sendIPCCommand(socketPath(), { cmd: "remove_project", slug: slug }).then(function (res) {
|
|
2074
|
+
if (res.ok) {
|
|
2075
|
+
log(sym.done + " " + a.green + "Removed: " + slug + a.reset);
|
|
2076
|
+
config = loadConfig() || config;
|
|
2077
|
+
} else {
|
|
2078
|
+
log(sym.warn + " " + a.yellow + (res.error || "Failed") + a.reset);
|
|
2079
|
+
}
|
|
2080
|
+
log("");
|
|
2081
|
+
showProjectsMenu(config, ip);
|
|
2082
|
+
});
|
|
2083
|
+
} else {
|
|
2084
|
+
showProjectsMenu(config, ip);
|
|
2085
|
+
}
|
|
2086
|
+
});
|
|
2087
|
+
}
|
|
2088
|
+
|
|
2089
|
+
// ==============================
|
|
2090
|
+
// Setup guide (2x2 toggle flow)
|
|
2091
|
+
// ==============================
|
|
2092
|
+
function showSetupGuide(config, ip, goBack) {
|
|
2093
|
+
var protocol = config.tls ? "https" : "http";
|
|
2094
|
+
var wantRemote = false;
|
|
2095
|
+
var wantPush = false;
|
|
2096
|
+
|
|
2097
|
+
console.clear();
|
|
2098
|
+
printLogo();
|
|
2099
|
+
log("");
|
|
2100
|
+
log(sym.pointer + " " + a.bold + "Setup Notifications" + a.reset);
|
|
2101
|
+
log(sym.bar);
|
|
2102
|
+
|
|
2103
|
+
function redraw(renderFn) {
|
|
2104
|
+
console.clear();
|
|
2105
|
+
printLogo();
|
|
2106
|
+
log("");
|
|
2107
|
+
log(sym.pointer + " " + a.bold + "Setup Notifications" + a.reset);
|
|
2108
|
+
log(sym.bar);
|
|
2109
|
+
if (wantRemote) log(sym.done + " Access from outside your network? " + a.dim + "·" + a.reset + " " + a.green + "Yes" + a.reset);
|
|
2110
|
+
else log(sym.done + " Access from outside your network? " + a.dim + "· No" + a.reset);
|
|
2111
|
+
log(sym.bar);
|
|
2112
|
+
if (wantPush) log(sym.done + " Want push notifications? " + a.dim + "·" + a.reset + " " + a.green + "Yes" + a.reset);
|
|
2113
|
+
else log(sym.done + " Want push notifications? " + a.dim + "· No" + a.reset);
|
|
2114
|
+
log(sym.bar);
|
|
2115
|
+
renderFn();
|
|
2116
|
+
}
|
|
2117
|
+
|
|
2118
|
+
promptToggle("Access from outside your network?", "Requires Tailscale on both devices", false, function (remote) {
|
|
2119
|
+
wantRemote = remote;
|
|
2120
|
+
log(sym.bar);
|
|
2121
|
+
promptToggle("Want push notifications?", "Requires HTTPS (mkcert certificate)", false, function (push) {
|
|
2122
|
+
wantPush = push;
|
|
2123
|
+
log(sym.bar);
|
|
2124
|
+
afterToggles();
|
|
2125
|
+
});
|
|
2126
|
+
});
|
|
2127
|
+
|
|
2128
|
+
function afterToggles() {
|
|
2129
|
+
if (!wantRemote && !wantPush) {
|
|
2130
|
+
log(sym.done + " " + a.green + "All set!" + a.reset + a.dim + " · No additional setup needed." + a.reset);
|
|
2131
|
+
log(sym.end);
|
|
2132
|
+
log("");
|
|
2133
|
+
promptSelect("Back?", [{ label: "Back", value: "back" }], function () {
|
|
2134
|
+
goBack();
|
|
2135
|
+
});
|
|
2136
|
+
return;
|
|
2137
|
+
}
|
|
2138
|
+
if (wantRemote) {
|
|
2139
|
+
renderTailscale();
|
|
2140
|
+
} else {
|
|
2141
|
+
renderHttps();
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2145
|
+
function renderTailscale() {
|
|
2146
|
+
var tsIP = getTailscaleIP();
|
|
2147
|
+
|
|
2148
|
+
log(sym.pointer + " " + a.bold + "Tailscale Setup" + a.reset);
|
|
2149
|
+
if (tsIP) {
|
|
2150
|
+
log(sym.bar + " " + a.green + "Tailscale is running" + a.reset + a.dim + " · " + tsIP + a.reset);
|
|
2151
|
+
log(sym.bar);
|
|
2152
|
+
log(sym.bar + " On your phone/tablet:");
|
|
2153
|
+
log(sym.bar + " " + a.dim + "1. Install Tailscale (App Store / Google Play)" + a.reset);
|
|
2154
|
+
log(sym.bar + " " + a.dim + "2. Sign in with the same account" + a.reset);
|
|
2155
|
+
log(sym.bar);
|
|
2156
|
+
renderHttps();
|
|
2157
|
+
} else {
|
|
2158
|
+
log(sym.bar + " " + a.yellow + "Tailscale not found on this machine." + a.reset);
|
|
2159
|
+
log(sym.bar + " " + a.dim + "Install: " + a.reset + "https://tailscale.com/download");
|
|
2160
|
+
log(sym.bar + " " + a.dim + "Then run: " + a.reset + "tailscale up");
|
|
2161
|
+
log(sym.bar);
|
|
2162
|
+
log(sym.bar + " On your phone/tablet:");
|
|
2163
|
+
log(sym.bar + " " + a.dim + "1. Install Tailscale (App Store / Google Play)" + a.reset);
|
|
2164
|
+
log(sym.bar + " " + a.dim + "2. Sign in with the same account" + a.reset);
|
|
2165
|
+
log(sym.bar);
|
|
2166
|
+
promptSelect("Select", [
|
|
2167
|
+
{ label: "Re-check", value: "recheck" },
|
|
2168
|
+
{ label: "Back", value: "back" },
|
|
2169
|
+
], function (choice) {
|
|
2170
|
+
if (choice === "recheck") {
|
|
2171
|
+
redraw(renderTailscale);
|
|
2172
|
+
} else {
|
|
2173
|
+
goBack();
|
|
2174
|
+
}
|
|
2175
|
+
});
|
|
2176
|
+
}
|
|
2177
|
+
}
|
|
2178
|
+
|
|
2179
|
+
function renderHttps() {
|
|
2180
|
+
if (!wantPush) {
|
|
2181
|
+
showSetupQR();
|
|
2182
|
+
return;
|
|
2183
|
+
}
|
|
2184
|
+
|
|
2185
|
+
var mcReady = hasMkcert();
|
|
2186
|
+
log(sym.pointer + " " + a.bold + "HTTPS Setup (for push notifications)" + a.reset);
|
|
2187
|
+
if (mcReady) {
|
|
2188
|
+
log(sym.bar + " " + a.green + "mkcert is installed" + a.reset);
|
|
2189
|
+
if (!config.tls) {
|
|
2190
|
+
log(sym.bar + " " + a.dim + "Restarting server with HTTPS..." + a.reset);
|
|
2191
|
+
restartDaemonWithTLS(config, function (newConfig) {
|
|
2192
|
+
config = newConfig;
|
|
2193
|
+
log(sym.bar);
|
|
2194
|
+
showSetupQR();
|
|
2195
|
+
});
|
|
2196
|
+
return;
|
|
2197
|
+
}
|
|
2198
|
+
log(sym.bar);
|
|
2199
|
+
showSetupQR();
|
|
2200
|
+
} else {
|
|
2201
|
+
log(sym.bar + " " + a.yellow + "mkcert not found." + a.reset);
|
|
2202
|
+
var mkcertHint = process.platform === "win32"
|
|
2203
|
+
? "choco install mkcert && mkcert -install"
|
|
2204
|
+
: process.platform === "darwin"
|
|
2205
|
+
? "brew install mkcert && mkcert -install"
|
|
2206
|
+
: "apt install mkcert && mkcert -install";
|
|
2207
|
+
log(sym.bar + " " + a.dim + "Install: " + a.reset + mkcertHint);
|
|
2208
|
+
log(sym.bar);
|
|
2209
|
+
promptSelect("Select", [
|
|
2210
|
+
{ label: "Re-check", value: "recheck" },
|
|
2211
|
+
{ label: "Back", value: "back" },
|
|
2212
|
+
], function (choice) {
|
|
2213
|
+
if (choice === "recheck") {
|
|
2214
|
+
redraw(renderHttps);
|
|
2215
|
+
} else {
|
|
2216
|
+
goBack();
|
|
2217
|
+
}
|
|
2218
|
+
});
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
|
|
2222
|
+
function showSetupQR() {
|
|
2223
|
+
var tsIP = getTailscaleIP();
|
|
2224
|
+
var lanIP = null;
|
|
2225
|
+
if (!wantRemote) {
|
|
2226
|
+
var allIPs = getAllIPs();
|
|
2227
|
+
for (var j = 0; j < allIPs.length; j++) {
|
|
2228
|
+
if (!allIPs[j].startsWith("100.")) { lanIP = allIPs[j]; break; }
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2231
|
+
var setupIP = wantRemote ? (tsIP || ip) : (lanIP || ip);
|
|
2232
|
+
var setupQuery = wantRemote ? "" : "?mode=lan";
|
|
2233
|
+
// Always use HTTP onboarding URL for QR/setup when TLS is active
|
|
2234
|
+
var setupUrl = config.tls
|
|
2235
|
+
? "http://" + setupIP + ":" + (config.port + 1) + "/setup" + setupQuery
|
|
2236
|
+
: "http://" + setupIP + ":" + config.port + "/setup" + setupQuery;
|
|
2237
|
+
log(sym.pointer + " " + a.bold + "Continue on your device" + a.reset);
|
|
2238
|
+
log(sym.bar + " " + a.dim + "Scan the QR code or open:" + a.reset);
|
|
2239
|
+
log(sym.bar + " " + a.bold + setupUrl + a.reset);
|
|
2240
|
+
log(sym.bar);
|
|
2241
|
+
qrcode.generate(setupUrl, { small: !isBasicTerm }, function (code) {
|
|
2242
|
+
var lines = code.split("\n").map(function (l) { return " " + sym.bar + " " + l; }).join("\n");
|
|
2243
|
+
console.log(lines);
|
|
2244
|
+
log(sym.bar);
|
|
2245
|
+
if (wantRemote) {
|
|
2246
|
+
log(sym.bar + " " + a.dim + "Can't connect? Make sure Tailscale is installed on your phone too." + a.reset);
|
|
2247
|
+
} else {
|
|
2248
|
+
log(sym.bar + " " + a.dim + "Can't connect? Your phone must be on the same Wi-Fi network." + a.reset);
|
|
2249
|
+
}
|
|
2250
|
+
log(sym.bar);
|
|
2251
|
+
log(sym.done + " " + a.dim + "Setup complete." + a.reset);
|
|
2252
|
+
log(sym.end);
|
|
2253
|
+
log("");
|
|
2254
|
+
promptSelect("Back?", [{ label: "Back", value: "back" }], function () {
|
|
2255
|
+
goBack();
|
|
2256
|
+
});
|
|
2257
|
+
});
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
|
|
2261
|
+
// ==============================
|
|
2262
|
+
// Settings sub-menu
|
|
2263
|
+
// ==============================
|
|
2264
|
+
function showSettingsMenu(config, ip) {
|
|
2265
|
+
sendIPCCommand(socketPath(), { cmd: "get_status" }).then(function (status) {
|
|
2266
|
+
var isAwake = status && status.keepAwake;
|
|
2267
|
+
|
|
2268
|
+
console.clear();
|
|
2269
|
+
printLogo();
|
|
2270
|
+
log("");
|
|
2271
|
+
log(sym.pointer + " " + a.bold + "Settings" + a.reset);
|
|
2272
|
+
log(sym.bar);
|
|
2273
|
+
|
|
2274
|
+
// Detect current state
|
|
2275
|
+
var tsIP = getTailscaleIP();
|
|
2276
|
+
var tsOk = tsIP !== null;
|
|
2277
|
+
var mcOk = hasMkcert();
|
|
2278
|
+
|
|
2279
|
+
var tsStatus = tsOk
|
|
2280
|
+
? a.green + "Connected" + a.reset + a.dim + " · " + tsIP + a.reset
|
|
2281
|
+
: a.dim + "Not detected" + a.reset;
|
|
2282
|
+
var mcStatus = mcOk
|
|
2283
|
+
? a.green + "Installed" + a.reset
|
|
2284
|
+
: a.dim + "Not found" + a.reset;
|
|
2285
|
+
var tlsStatus = config.tls
|
|
2286
|
+
? a.green + "Enabled" + a.reset
|
|
2287
|
+
: a.dim + "Disabled" + a.reset;
|
|
2288
|
+
var pinStatus = config.pinHash
|
|
2289
|
+
? a.green + "Enabled" + a.reset
|
|
2290
|
+
: a.dim + "Off" + a.reset;
|
|
2291
|
+
var awakeStatus = isAwake
|
|
2292
|
+
? a.green + "On" + a.reset
|
|
2293
|
+
: a.dim + "Off" + a.reset;
|
|
2294
|
+
|
|
2295
|
+
log(sym.bar + " Tailscale " + tsStatus);
|
|
2296
|
+
log(sym.bar + " mkcert " + mcStatus);
|
|
2297
|
+
log(sym.bar + " HTTPS " + tlsStatus);
|
|
2298
|
+
var muEnabled = isMultiUser();
|
|
2299
|
+
var muStatus = muEnabled
|
|
2300
|
+
? a.green + "Enabled" + a.reset
|
|
2301
|
+
: a.dim + "Off" + a.reset;
|
|
2302
|
+
|
|
2303
|
+
log(sym.bar + " PIN " + pinStatus);
|
|
2304
|
+
log(sym.bar + " Multi-user " + muStatus);
|
|
2305
|
+
if (process.platform === "darwin") {
|
|
2306
|
+
log(sym.bar + " Keep awake " + awakeStatus);
|
|
2307
|
+
}
|
|
2308
|
+
log(sym.bar);
|
|
2309
|
+
|
|
2310
|
+
// Build items
|
|
2311
|
+
var items = [
|
|
2312
|
+
{ label: "Setup notifications", value: "guide" },
|
|
2313
|
+
];
|
|
2314
|
+
|
|
2315
|
+
if (config.pinHash) {
|
|
2316
|
+
items.push({ label: "Change PIN", value: "pin" });
|
|
2317
|
+
items.push({ label: "Remove PIN", value: "remove_pin" });
|
|
2318
|
+
} else {
|
|
2319
|
+
items.push({ label: "Set PIN", value: "pin" });
|
|
2320
|
+
}
|
|
2321
|
+
if (muEnabled) {
|
|
2322
|
+
items.push({ label: "Disable multi-user mode", value: "disable_multi_user" });
|
|
2323
|
+
} else {
|
|
2324
|
+
items.push({ label: "Enable multi-user mode", value: "multi_user" });
|
|
2325
|
+
}
|
|
2326
|
+
if (process.platform === "darwin") {
|
|
2327
|
+
items.push({ label: isAwake ? "Disable keep awake" : "Enable keep awake", value: "awake" });
|
|
2328
|
+
}
|
|
2329
|
+
items.push({ label: "View logs", value: "logs" });
|
|
2330
|
+
items.push({ label: "Back", value: "back" });
|
|
2331
|
+
|
|
2332
|
+
promptSelect("Select", items, function (choice) {
|
|
2333
|
+
switch (choice) {
|
|
2334
|
+
case "guide":
|
|
2335
|
+
showSetupGuide(config, ip, function () {
|
|
2336
|
+
config = loadConfig() || config;
|
|
2337
|
+
showSettingsMenu(config, ip);
|
|
2338
|
+
});
|
|
2339
|
+
break;
|
|
2340
|
+
|
|
2341
|
+
case "pin":
|
|
2342
|
+
log(sym.bar);
|
|
2343
|
+
promptPin(function (pin) {
|
|
2344
|
+
if (pin) {
|
|
2345
|
+
var hash = generateAuthToken(pin);
|
|
2346
|
+
sendIPCCommand(socketPath(), { cmd: "set_pin", pinHash: hash }).then(function () {
|
|
2347
|
+
config.pinHash = hash;
|
|
2348
|
+
log(sym.done + " " + a.green + "PIN updated" + a.reset);
|
|
2349
|
+
log("");
|
|
2350
|
+
showSettingsMenu(config, ip);
|
|
2351
|
+
});
|
|
2352
|
+
} else {
|
|
2353
|
+
showSettingsMenu(config, ip);
|
|
2354
|
+
}
|
|
2355
|
+
});
|
|
2356
|
+
break;
|
|
2357
|
+
|
|
2358
|
+
case "remove_pin":
|
|
2359
|
+
sendIPCCommand(socketPath(), { cmd: "set_pin", pinHash: null }).then(function () {
|
|
2360
|
+
config.pinHash = null;
|
|
2361
|
+
log(sym.done + " " + a.dim + "PIN removed" + a.reset);
|
|
2362
|
+
log("");
|
|
2363
|
+
showSettingsMenu(config, ip);
|
|
2364
|
+
});
|
|
2365
|
+
break;
|
|
2366
|
+
|
|
2367
|
+
case "multi_user":
|
|
2368
|
+
var muResult = enableMultiUser();
|
|
2369
|
+
log(sym.bar);
|
|
2370
|
+
log(sym.bar + " " + a.yellow + sym.warn + " Experimental Feature" + a.reset);
|
|
2371
|
+
log(sym.bar);
|
|
2372
|
+
log(sym.bar + " " + a.dim + "Multi-user mode is experimental and may change in future releases." + a.reset);
|
|
2373
|
+
log(sym.bar + " " + a.dim + "Sharing access to AI-powered tools may be subject to your provider's" + a.reset);
|
|
2374
|
+
log(sym.bar + " " + a.dim + "terms of service. Please review the applicable usage policies before" + a.reset);
|
|
2375
|
+
log(sym.bar + " " + a.dim + "granting access to other users." + a.reset);
|
|
2376
|
+
log(sym.bar);
|
|
2377
|
+
if (muResult.setupCode) {
|
|
2378
|
+
log(sym.bar + " " + a.green + "Multi-user mode enabled." + a.reset);
|
|
2379
|
+
log(sym.bar);
|
|
2380
|
+
log(sym.bar + " Setup code: " + a.bold + muResult.setupCode + a.reset);
|
|
2381
|
+
log(sym.bar);
|
|
2382
|
+
log(sym.bar + " " + a.dim + "Open Clay in your browser and enter this code to create the admin account." + a.reset);
|
|
2383
|
+
log(sym.bar + " " + a.dim + "The code is single-use and will be cleared once the admin is set up." + a.reset);
|
|
2384
|
+
} else {
|
|
2385
|
+
log(sym.bar + " " + a.dim + "Multi-user mode is already enabled." + a.reset);
|
|
2386
|
+
}
|
|
2387
|
+
log(sym.bar);
|
|
2388
|
+
promptSelect("Back?", [{ label: "Back", value: "back" }], function () {
|
|
2389
|
+
showSettingsMenu(config, ip);
|
|
2390
|
+
});
|
|
2391
|
+
break;
|
|
2392
|
+
|
|
2393
|
+
case "disable_multi_user":
|
|
2394
|
+
log(sym.bar);
|
|
2395
|
+
log(sym.bar + " " + a.yellow + sym.warn + " Disable multi-user mode?" + a.reset);
|
|
2396
|
+
log(sym.bar);
|
|
2397
|
+
log(sym.bar + " " + a.dim + "Sessions created by other users will no longer be visible." + a.reset);
|
|
2398
|
+
log(sym.bar + " " + a.dim + "User accounts will be preserved and restored if re-enabled." + a.reset);
|
|
2399
|
+
log(sym.bar);
|
|
2400
|
+
promptSelect("Confirm", [
|
|
2401
|
+
{ label: "Disable multi-user mode", value: "confirm" },
|
|
2402
|
+
{ label: "Cancel", value: "cancel" },
|
|
2403
|
+
], function (confirmChoice) {
|
|
2404
|
+
if (confirmChoice === "confirm") {
|
|
2405
|
+
disableMultiUser();
|
|
2406
|
+
log(sym.bar);
|
|
2407
|
+
log(sym.done + " " + a.green + "Multi-user mode disabled." + a.reset);
|
|
2408
|
+
log(sym.bar + " " + a.dim + "Restart the daemon for changes to take full effect." + a.reset);
|
|
2409
|
+
log(sym.bar);
|
|
2410
|
+
}
|
|
2411
|
+
showSettingsMenu(config, ip);
|
|
2412
|
+
});
|
|
2413
|
+
break;
|
|
2414
|
+
|
|
2415
|
+
case "logs":
|
|
2416
|
+
console.clear();
|
|
2417
|
+
log(a.bold + "Daemon logs" + a.reset + " " + a.dim + "(" + logPath() + ")" + a.reset);
|
|
2418
|
+
log("");
|
|
2419
|
+
try {
|
|
2420
|
+
var logContent = fs.readFileSync(logPath(), "utf8");
|
|
2421
|
+
var logLines = logContent.split("\n").slice(-30);
|
|
2422
|
+
for (var li = 0; li < logLines.length; li++) {
|
|
2423
|
+
log(a.dim + logLines[li] + a.reset);
|
|
2424
|
+
}
|
|
2425
|
+
} catch (e) {
|
|
2426
|
+
log(a.dim + "(empty)" + a.reset);
|
|
2427
|
+
}
|
|
2428
|
+
log("");
|
|
2429
|
+
promptSelect("Back?", [{ label: "Back", value: "back" }], function () {
|
|
2430
|
+
showSettingsMenu(config, ip);
|
|
2431
|
+
});
|
|
2432
|
+
break;
|
|
2433
|
+
|
|
2434
|
+
case "awake":
|
|
2435
|
+
sendIPCCommand(socketPath(), { cmd: "set_keep_awake", value: !isAwake }).then(function (res) {
|
|
2436
|
+
if (res.ok) {
|
|
2437
|
+
config.keepAwake = !isAwake;
|
|
2438
|
+
}
|
|
2439
|
+
showSettingsMenu(config, ip);
|
|
2440
|
+
});
|
|
2441
|
+
break;
|
|
2442
|
+
|
|
2443
|
+
case "back":
|
|
2444
|
+
showMainMenu(config, ip);
|
|
2445
|
+
break;
|
|
2446
|
+
}
|
|
2447
|
+
});
|
|
2448
|
+
});
|
|
2449
|
+
}
|
|
2450
|
+
|
|
2451
|
+
// ==============================
|
|
2452
|
+
// Main entry: daemon alive?
|
|
2453
|
+
// ==============================
|
|
2454
|
+
var { checkAndUpdate } = require("../lib/updater");
|
|
2455
|
+
var currentVersion = require("../package.json").version;
|
|
2456
|
+
|
|
2457
|
+
(async function () {
|
|
2458
|
+
var updated = await checkAndUpdate(currentVersion, skipUpdate);
|
|
2459
|
+
if (updated) return;
|
|
2460
|
+
|
|
2461
|
+
// Dev mode — foreground daemon with file watching
|
|
2462
|
+
if (_isDev) {
|
|
2463
|
+
var devConfig = loadConfig();
|
|
2464
|
+
var devAlive = devConfig ? await isDaemonAliveAsync(devConfig) : false;
|
|
2465
|
+
if (devAlive) {
|
|
2466
|
+
console.log("\x1b[38;2;0;183;133m[dev]\x1b[0m Shutting down existing daemon...");
|
|
2467
|
+
await sendIPCCommand(socketPath(), { cmd: "shutdown" });
|
|
2468
|
+
clearStaleConfig();
|
|
2469
|
+
await new Promise(function (resolve) { setTimeout(resolve, 500); });
|
|
2470
|
+
}
|
|
2471
|
+
// No running daemon — clear config so setup runs fresh
|
|
2472
|
+
if (!devAlive && devConfig) {
|
|
2473
|
+
if (devConfig.pid) clearStaleConfig();
|
|
2474
|
+
devConfig = null;
|
|
2475
|
+
}
|
|
2476
|
+
// No config — go through setup (disclaimer, port, PIN, etc.)
|
|
2477
|
+
if (!devConfig) {
|
|
2478
|
+
setup(function (pin, keepAwake) {
|
|
2479
|
+
devMode(pin, keepAwake, null);
|
|
2480
|
+
});
|
|
2481
|
+
} else {
|
|
2482
|
+
// Reuse existing PIN hash from previous config
|
|
2483
|
+
await devMode(cliPin || null, devConfig.keepAwake || false, devConfig.pinHash || null);
|
|
2484
|
+
}
|
|
2485
|
+
return;
|
|
2486
|
+
}
|
|
2487
|
+
|
|
2488
|
+
var config = loadConfig();
|
|
2489
|
+
var alive = config ? await isDaemonAliveAsync(config) : false;
|
|
2490
|
+
|
|
2491
|
+
if (!alive && config && config.pid) {
|
|
2492
|
+
// Stale config
|
|
2493
|
+
clearStaleConfig();
|
|
2494
|
+
config = null;
|
|
2495
|
+
}
|
|
2496
|
+
|
|
2497
|
+
if (alive) {
|
|
2498
|
+
// Headless mode — daemon already running, just report and exit
|
|
2499
|
+
if (headlessMode) {
|
|
2500
|
+
var protocol = config.tls ? "https" : "http";
|
|
2501
|
+
var ip = getLocalIP();
|
|
2502
|
+
var url = protocol + "://" + ip + ":" + config.port;
|
|
2503
|
+
console.log(" " + sym.done + " Daemon already running (PID " + config.pid + ")");
|
|
2504
|
+
console.log(" " + sym.done + " " + url);
|
|
2505
|
+
process.exit(0);
|
|
2506
|
+
return;
|
|
2507
|
+
}
|
|
2508
|
+
|
|
2509
|
+
// Daemon is running — auto-add cwd if needed, then show menu
|
|
2510
|
+
var ip = getLocalIP();
|
|
2511
|
+
|
|
2512
|
+
var status = await sendIPCCommand(socketPath(), { cmd: "get_status" });
|
|
2513
|
+
if (!status.ok) {
|
|
2514
|
+
log(a.red + "Daemon not responding" + a.reset);
|
|
2515
|
+
clearStaleConfig();
|
|
2516
|
+
process.exit(1);
|
|
2517
|
+
return;
|
|
2518
|
+
}
|
|
2519
|
+
|
|
2520
|
+
// Check if cwd needs to be added
|
|
2521
|
+
var projs = status.projects || [];
|
|
2522
|
+
var cwdRegistered = false;
|
|
2523
|
+
for (var j = 0; j < projs.length; j++) {
|
|
2524
|
+
if (projs[j].path === cwd) {
|
|
2525
|
+
cwdRegistered = true;
|
|
2526
|
+
break;
|
|
2527
|
+
}
|
|
2528
|
+
}
|
|
2529
|
+
|
|
2530
|
+
if (!cwdRegistered) {
|
|
2531
|
+
var slug = path.basename(cwd).toLowerCase().replace(/[^a-z0-9_-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") || "project";
|
|
2532
|
+
console.clear();
|
|
2533
|
+
printLogo();
|
|
2534
|
+
log("");
|
|
2535
|
+
log(sym.pointer + " " + a.bold + "Add this project?" + a.reset);
|
|
2536
|
+
log(sym.bar);
|
|
2537
|
+
log(sym.bar + " " + a.dim + cwd + a.reset);
|
|
2538
|
+
log(sym.bar);
|
|
2539
|
+
promptSelect("Add " + a.green + slug + a.reset + " to relay?", [
|
|
2540
|
+
{ label: "Yes", value: "yes" },
|
|
2541
|
+
{ label: "No", value: "no" },
|
|
2542
|
+
], function (answer) {
|
|
2543
|
+
if (answer === "yes") {
|
|
2544
|
+
sendIPCCommand(socketPath(), { cmd: "add_project", path: cwd }).then(function (res) {
|
|
2545
|
+
if (res.ok) {
|
|
2546
|
+
config = loadConfig() || config;
|
|
2547
|
+
log(sym.done + " " + a.green + "Added: " + (res.slug || slug) + a.reset);
|
|
2548
|
+
}
|
|
2549
|
+
log("");
|
|
2550
|
+
showMainMenu(config || { pid: status.pid, port: status.port, tls: status.tls }, ip);
|
|
2551
|
+
});
|
|
2552
|
+
} else {
|
|
2553
|
+
showMainMenu(config || { pid: status.pid, port: status.port, tls: status.tls }, ip);
|
|
2554
|
+
}
|
|
2555
|
+
});
|
|
2556
|
+
} else {
|
|
2557
|
+
showMainMenu(config || { pid: status.pid, port: status.port, tls: status.tls }, ip);
|
|
2558
|
+
}
|
|
2559
|
+
} else {
|
|
2560
|
+
// No daemon running — first-time setup
|
|
2561
|
+
if (autoYes) {
|
|
2562
|
+
var pin = cliPin || null;
|
|
2563
|
+
console.log(" " + sym.done + " Auto-accepted disclaimer");
|
|
2564
|
+
console.log(" " + sym.done + " PIN: " + (pin ? "Enabled" : "Skipped"));
|
|
2565
|
+
if (dangerouslySkipPermissions) {
|
|
2566
|
+
console.log(" " + sym.warn + " " + a.yellow + "Skip permissions mode enabled" + (pin ? "" : " (no PIN)") + a.reset);
|
|
2567
|
+
}
|
|
2568
|
+
var autoRc = loadClayrc();
|
|
2569
|
+
var autoRestorable = (autoRc.recentProjects || []).filter(function (p) {
|
|
2570
|
+
return p.path !== cwd && fs.existsSync(p.path);
|
|
2571
|
+
});
|
|
2572
|
+
if (autoRestorable.length > 0) {
|
|
2573
|
+
console.log(" " + sym.done + " Restoring " + autoRestorable.length + " previous project(s)");
|
|
2574
|
+
}
|
|
2575
|
+
// Add cwd if it has history in .clayrc, or if there are no other projects to restore
|
|
2576
|
+
var cwdInRc = (autoRc.recentProjects || []).some(function (p) {
|
|
2577
|
+
return p.path === cwd;
|
|
2578
|
+
});
|
|
2579
|
+
var addCwd = cwdInRc || autoRestorable.length === 0;
|
|
2580
|
+
await forkDaemon(pin, false, autoRestorable.length > 0 ? autoRestorable : undefined, addCwd);
|
|
2581
|
+
} else {
|
|
2582
|
+
setup(function (pin, keepAwake) {
|
|
2583
|
+
// Check ~/.clayrc for previous projects to restore
|
|
2584
|
+
var rc = loadClayrc();
|
|
2585
|
+
var restorable = (rc.recentProjects || []).filter(function (p) {
|
|
2586
|
+
return p.path !== cwd && fs.existsSync(p.path);
|
|
2587
|
+
});
|
|
2588
|
+
|
|
2589
|
+
if (restorable.length > 0) {
|
|
2590
|
+
promptRestoreProjects(restorable, function (selected) {
|
|
2591
|
+
forkDaemon(pin, keepAwake, selected, false);
|
|
2592
|
+
});
|
|
2593
|
+
} else {
|
|
2594
|
+
log(sym.bar);
|
|
2595
|
+
log(sym.end + " " + a.dim + "Starting relay..." + a.reset);
|
|
2596
|
+
log("");
|
|
2597
|
+
forkDaemon(pin, keepAwake, undefined, true);
|
|
2598
|
+
}
|
|
2599
|
+
});
|
|
2600
|
+
}
|
|
2601
|
+
}
|
|
2602
|
+
})();
|