clay-server 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +281 -0
- package/bin/cli.js +2385 -0
- package/lib/cli-sessions.js +270 -0
- package/lib/config.js +237 -0
- package/lib/daemon.js +489 -0
- package/lib/ipc.js +112 -0
- package/lib/notes.js +120 -0
- package/lib/pages.js +664 -0
- package/lib/project.js +1433 -0
- package/lib/public/app.js +2795 -0
- package/lib/public/apple-touch-icon-dark.png +0 -0
- package/lib/public/apple-touch-icon.png +0 -0
- package/lib/public/css/base.css +264 -0
- package/lib/public/css/diff.css +128 -0
- package/lib/public/css/filebrowser.css +1114 -0
- package/lib/public/css/highlight.css +144 -0
- package/lib/public/css/icon-strip.css +296 -0
- package/lib/public/css/input.css +573 -0
- package/lib/public/css/menus.css +856 -0
- package/lib/public/css/messages.css +1445 -0
- package/lib/public/css/mobile-nav.css +354 -0
- package/lib/public/css/overlays.css +697 -0
- package/lib/public/css/rewind.css +505 -0
- package/lib/public/css/server-settings.css +761 -0
- package/lib/public/css/sidebar.css +936 -0
- package/lib/public/css/sticky-notes.css +358 -0
- package/lib/public/css/title-bar.css +314 -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-mono.svg +1 -0
- package/lib/public/index.html +762 -0
- package/lib/public/manifest.json +27 -0
- package/lib/public/modules/diff.js +398 -0
- package/lib/public/modules/events.js +21 -0
- package/lib/public/modules/filebrowser.js +1411 -0
- package/lib/public/modules/fileicons.js +172 -0
- package/lib/public/modules/icons.js +54 -0
- package/lib/public/modules/input.js +584 -0
- package/lib/public/modules/markdown.js +356 -0
- package/lib/public/modules/notifications.js +649 -0
- package/lib/public/modules/qrcode.js +70 -0
- package/lib/public/modules/rewind.js +345 -0
- package/lib/public/modules/server-settings.js +510 -0
- package/lib/public/modules/sidebar.js +1083 -0
- package/lib/public/modules/state.js +3 -0
- package/lib/public/modules/sticky-notes.js +688 -0
- package/lib/public/modules/terminal.js +697 -0
- package/lib/public/modules/theme.js +738 -0
- package/lib/public/modules/tools.js +1608 -0
- package/lib/public/modules/utils.js +56 -0
- package/lib/public/style.css +15 -0
- package/lib/public/sw.js +75 -0
- package/lib/push.js +124 -0
- package/lib/sdk-bridge.js +989 -0
- package/lib/server.js +582 -0
- package/lib/sessions.js +424 -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/package.json +47 -0
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
var fs = require("fs");
|
|
2
|
+
var path = require("path");
|
|
3
|
+
var os = require("os");
|
|
4
|
+
var readline = require("readline");
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Compute the encoded project directory name used by the Claude CLI.
|
|
8
|
+
* Replaces all "/" with "-", e.g. "/Users/foo/project" -> "-Users-foo-project"
|
|
9
|
+
*/
|
|
10
|
+
function encodeCwd(cwd) {
|
|
11
|
+
return cwd.replace(/\//g, "-");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Parse the first ~20 lines of a CLI session JSONL file to extract metadata.
|
|
16
|
+
* Returns null if the file can't be parsed or has no user messages.
|
|
17
|
+
*/
|
|
18
|
+
function parseSessionFile(filePath, maxLines) {
|
|
19
|
+
if (maxLines == null) maxLines = 20;
|
|
20
|
+
return new Promise(function (resolve) {
|
|
21
|
+
var sessionId = path.basename(filePath, ".jsonl");
|
|
22
|
+
var result = {
|
|
23
|
+
sessionId: sessionId,
|
|
24
|
+
firstPrompt: "",
|
|
25
|
+
model: null,
|
|
26
|
+
gitBranch: null,
|
|
27
|
+
startTime: null,
|
|
28
|
+
lastActivity: null,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
var lineCount = 0;
|
|
32
|
+
var foundUser = false;
|
|
33
|
+
var stream;
|
|
34
|
+
try {
|
|
35
|
+
stream = fs.createReadStream(filePath, { encoding: "utf8" });
|
|
36
|
+
} catch (e) {
|
|
37
|
+
return resolve(null);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
var rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
41
|
+
|
|
42
|
+
rl.on("line", function (line) {
|
|
43
|
+
lineCount++;
|
|
44
|
+
if (lineCount > maxLines) {
|
|
45
|
+
rl.close();
|
|
46
|
+
stream.destroy();
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
var obj;
|
|
51
|
+
try { obj = JSON.parse(line); } catch (e) { return; }
|
|
52
|
+
|
|
53
|
+
// Skip file-history-snapshot, queue-operation, and other non-message records
|
|
54
|
+
if (obj.type === "user" && obj.message && obj.message.role === "user") {
|
|
55
|
+
if (!foundUser) {
|
|
56
|
+
foundUser = true;
|
|
57
|
+
result.sessionId = obj.sessionId || sessionId;
|
|
58
|
+
result.gitBranch = obj.gitBranch || null;
|
|
59
|
+
if (obj.timestamp) result.startTime = obj.timestamp;
|
|
60
|
+
var content = obj.message.content || "";
|
|
61
|
+
if (typeof content === "string") {
|
|
62
|
+
result.firstPrompt = content.substring(0, 100);
|
|
63
|
+
} else if (Array.isArray(content)) {
|
|
64
|
+
for (var i = 0; i < content.length; i++) {
|
|
65
|
+
if (content[i].type === "text" && content[i].text) {
|
|
66
|
+
result.firstPrompt = content[i].text.substring(0, 100);
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// Track latest user timestamp for lastActivity
|
|
73
|
+
if (obj.timestamp) result.lastActivity = obj.timestamp;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Extract model from first assistant message
|
|
77
|
+
if (!result.model && obj.message && obj.message.role === "assistant" && obj.message.model) {
|
|
78
|
+
result.model = obj.message.model;
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
rl.on("close", function () {
|
|
83
|
+
if (!foundUser) return resolve(null);
|
|
84
|
+
|
|
85
|
+
// Use file mtime as fallback for lastActivity, or as a better proxy
|
|
86
|
+
// since we only read the first ~20 lines
|
|
87
|
+
try {
|
|
88
|
+
var stat = fs.statSync(filePath);
|
|
89
|
+
var mtime = stat.mtime.toISOString();
|
|
90
|
+
// File mtime is always more accurate for "last activity" since we
|
|
91
|
+
// don't read the entire file
|
|
92
|
+
result.lastActivity = mtime;
|
|
93
|
+
} catch (e) {}
|
|
94
|
+
|
|
95
|
+
resolve(result);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
rl.on("error", function () {
|
|
99
|
+
resolve(null);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
stream.on("error", function () {
|
|
103
|
+
rl.close();
|
|
104
|
+
resolve(null);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* List CLI sessions for a given project directory.
|
|
111
|
+
* Reads ~/.claude/projects/{encoded-cwd}/ and parses JSONL metadata.
|
|
112
|
+
* Returns array sorted by lastActivity descending (most recent first).
|
|
113
|
+
*/
|
|
114
|
+
function listCliSessions(cwd) {
|
|
115
|
+
var encoded = encodeCwd(cwd);
|
|
116
|
+
var projectDir = path.join(os.homedir(), ".claude", "projects", encoded);
|
|
117
|
+
|
|
118
|
+
return new Promise(function (resolve) {
|
|
119
|
+
fs.readdir(projectDir, { withFileTypes: true }, function (err, entries) {
|
|
120
|
+
if (err) return resolve([]);
|
|
121
|
+
|
|
122
|
+
var jsonlFiles = [];
|
|
123
|
+
for (var i = 0; i < entries.length; i++) {
|
|
124
|
+
if (entries[i].isFile() && entries[i].name.endsWith(".jsonl")) {
|
|
125
|
+
jsonlFiles.push(path.join(projectDir, entries[i].name));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (jsonlFiles.length === 0) return resolve([]);
|
|
130
|
+
|
|
131
|
+
var pending = jsonlFiles.length;
|
|
132
|
+
var results = [];
|
|
133
|
+
|
|
134
|
+
for (var j = 0; j < jsonlFiles.length; j++) {
|
|
135
|
+
parseSessionFile(jsonlFiles[j]).then(function (session) {
|
|
136
|
+
if (session) results.push(session);
|
|
137
|
+
pending--;
|
|
138
|
+
if (pending === 0) {
|
|
139
|
+
results.sort(function (a, b) {
|
|
140
|
+
var ta = a.lastActivity || "";
|
|
141
|
+
var tb = b.lastActivity || "";
|
|
142
|
+
return ta < tb ? 1 : ta > tb ? -1 : 0;
|
|
143
|
+
});
|
|
144
|
+
resolve(results);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Get the most recent CLI session for a given project directory.
|
|
154
|
+
* Returns the session object or null if none found.
|
|
155
|
+
*/
|
|
156
|
+
function getMostRecentCliSession(cwd) {
|
|
157
|
+
return listCliSessions(cwd).then(function (sessions) {
|
|
158
|
+
return sessions.length > 0 ? sessions[0] : null;
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Extract user message text from a CLI JSONL content field.
|
|
164
|
+
* Content can be a string or an array of content blocks.
|
|
165
|
+
*/
|
|
166
|
+
function extractText(content) {
|
|
167
|
+
if (typeof content === "string") return content;
|
|
168
|
+
if (!Array.isArray(content)) return "";
|
|
169
|
+
var parts = [];
|
|
170
|
+
for (var i = 0; i < content.length; i++) {
|
|
171
|
+
if (content[i].type === "text" && content[i].text) {
|
|
172
|
+
parts.push(content[i].text);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return parts.join("");
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Read a full CLI session JSONL file and convert it to relay-compatible
|
|
180
|
+
* history entries (user_message, delta, tool_start, tool_executing, tool_result).
|
|
181
|
+
* Returns a Promise that resolves to an array of history entries.
|
|
182
|
+
*/
|
|
183
|
+
function readCliSessionHistory(cwd, sessionId) {
|
|
184
|
+
var encoded = encodeCwd(cwd);
|
|
185
|
+
var filePath = path.join(os.homedir(), ".claude", "projects", encoded, sessionId + ".jsonl");
|
|
186
|
+
|
|
187
|
+
return new Promise(function (resolve) {
|
|
188
|
+
var history = [];
|
|
189
|
+
var stream;
|
|
190
|
+
try {
|
|
191
|
+
stream = fs.createReadStream(filePath, { encoding: "utf8" });
|
|
192
|
+
} catch (e) {
|
|
193
|
+
return resolve([]);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
var rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
197
|
+
var toolCounter = 0;
|
|
198
|
+
|
|
199
|
+
rl.on("line", function (line) {
|
|
200
|
+
var obj;
|
|
201
|
+
try { obj = JSON.parse(line); } catch (e) { return; }
|
|
202
|
+
|
|
203
|
+
if (!obj.message) return;
|
|
204
|
+
|
|
205
|
+
// User prompt
|
|
206
|
+
if (obj.type === "user" && obj.message.role === "user") {
|
|
207
|
+
// Skip tool_result records (they have type "user" but content is tool results)
|
|
208
|
+
var content = obj.message.content;
|
|
209
|
+
if (Array.isArray(content) && content.length > 0 && content[0].type === "tool_result") {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
var text = extractText(content);
|
|
213
|
+
if (text) {
|
|
214
|
+
history.push({ type: "user_message", text: text });
|
|
215
|
+
}
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Assistant message
|
|
220
|
+
if (obj.message.role === "assistant" && Array.isArray(obj.message.content)) {
|
|
221
|
+
for (var i = 0; i < obj.message.content.length; i++) {
|
|
222
|
+
var block = obj.message.content[i];
|
|
223
|
+
|
|
224
|
+
if (block.type === "text" && block.text) {
|
|
225
|
+
history.push({ type: "delta", text: block.text });
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (block.type === "tool_use") {
|
|
229
|
+
var toolId = "cli-tool-" + (++toolCounter);
|
|
230
|
+
var toolName = block.name || "Tool";
|
|
231
|
+
history.push({ type: "tool_start", id: toolId, name: toolName });
|
|
232
|
+
history.push({
|
|
233
|
+
type: "tool_executing",
|
|
234
|
+
id: toolId,
|
|
235
|
+
name: toolName,
|
|
236
|
+
input: block.input || {},
|
|
237
|
+
});
|
|
238
|
+
// Emit ask_user_answered so the client re-enables input after replaying AskUserQuestion
|
|
239
|
+
if (toolName === "AskUserQuestion") {
|
|
240
|
+
history.push({ type: "ask_user_answered", toolId: toolId });
|
|
241
|
+
}
|
|
242
|
+
history.push({ type: "tool_result", id: toolId, content: "" });
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
rl.on("close", function () {
|
|
249
|
+
resolve(history);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
rl.on("error", function () {
|
|
253
|
+
resolve([]);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
stream.on("error", function () {
|
|
257
|
+
rl.close();
|
|
258
|
+
resolve([]);
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
module.exports = {
|
|
264
|
+
listCliSessions: listCliSessions,
|
|
265
|
+
getMostRecentCliSession: getMostRecentCliSession,
|
|
266
|
+
readCliSessionHistory: readCliSessionHistory,
|
|
267
|
+
parseSessionFile: parseSessionFile,
|
|
268
|
+
encodeCwd: encodeCwd,
|
|
269
|
+
extractText: extractText,
|
|
270
|
+
};
|
package/lib/config.js
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
var fs = require("fs");
|
|
2
|
+
var path = require("path");
|
|
3
|
+
var os = require("os");
|
|
4
|
+
var net = require("net");
|
|
5
|
+
|
|
6
|
+
// v3: ~/.clay/ (v2 was ~/.claude-relay/, v1 was {cwd}/.claude-relay/)
|
|
7
|
+
var CLAY_HOME = process.env.CLAY_HOME || path.join(os.homedir(), ".clay");
|
|
8
|
+
var LEGACY_HOME = path.join(os.homedir(), ".claude-relay");
|
|
9
|
+
|
|
10
|
+
// Auto-migrate v2 -> v3: rename ~/.claude-relay/ to ~/.clay/ (once, before anything reads)
|
|
11
|
+
if (!fs.existsSync(CLAY_HOME) && fs.existsSync(LEGACY_HOME)) {
|
|
12
|
+
try {
|
|
13
|
+
fs.renameSync(LEGACY_HOME, CLAY_HOME);
|
|
14
|
+
console.log("[config] Migrated " + LEGACY_HOME + " → " + CLAY_HOME);
|
|
15
|
+
} catch (e) {
|
|
16
|
+
// rename failed (cross-device?), fall through — individual files will be read from old path
|
|
17
|
+
console.error("[config] Migration rename failed:", e.message);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
var CONFIG_DIR = CLAY_HOME;
|
|
22
|
+
var CLAYRC_PATH = path.join(os.homedir(), ".clayrc");
|
|
23
|
+
var CRASH_INFO_PATH = path.join(CONFIG_DIR, "crash.json");
|
|
24
|
+
|
|
25
|
+
function configPath() {
|
|
26
|
+
return path.join(CONFIG_DIR, "daemon.json");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function socketPath() {
|
|
30
|
+
if (process.platform === "win32") {
|
|
31
|
+
var pipeName = process.env.CLAY_HOME ? "clay-dev-daemon" : "clay-daemon";
|
|
32
|
+
return "\\\\.\\pipe\\" + pipeName;
|
|
33
|
+
}
|
|
34
|
+
return path.join(CONFIG_DIR, "daemon.sock");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function logPath() {
|
|
38
|
+
return path.join(CONFIG_DIR, "daemon.log");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function ensureConfigDir() {
|
|
42
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function loadConfig() {
|
|
46
|
+
try {
|
|
47
|
+
var data = fs.readFileSync(configPath(), "utf8");
|
|
48
|
+
return JSON.parse(data);
|
|
49
|
+
} catch (e) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function saveConfig(config) {
|
|
55
|
+
ensureConfigDir();
|
|
56
|
+
var tmpPath = configPath() + ".tmp";
|
|
57
|
+
fs.writeFileSync(tmpPath, JSON.stringify(config, null, 2));
|
|
58
|
+
fs.renameSync(tmpPath, configPath());
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function isPidAlive(pid) {
|
|
62
|
+
try {
|
|
63
|
+
process.kill(pid, 0);
|
|
64
|
+
return true;
|
|
65
|
+
} catch (e) {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function isDaemonAlive(config) {
|
|
71
|
+
if (!config || !config.pid) return false;
|
|
72
|
+
if (!isPidAlive(config.pid)) return false;
|
|
73
|
+
// Named pipes on Windows can't be stat'd, just check PID
|
|
74
|
+
if (process.platform === "win32") return true;
|
|
75
|
+
try {
|
|
76
|
+
fs.statSync(socketPath());
|
|
77
|
+
return true;
|
|
78
|
+
} catch (e) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function isDaemonAliveAsync(config) {
|
|
84
|
+
return new Promise(function (resolve) {
|
|
85
|
+
if (!config || !config.pid) return resolve(false);
|
|
86
|
+
if (!isPidAlive(config.pid)) return resolve(false);
|
|
87
|
+
|
|
88
|
+
var sock = socketPath();
|
|
89
|
+
var client = net.connect(sock);
|
|
90
|
+
var timer = setTimeout(function () {
|
|
91
|
+
client.destroy();
|
|
92
|
+
resolve(false);
|
|
93
|
+
}, 1000);
|
|
94
|
+
|
|
95
|
+
client.on("connect", function () {
|
|
96
|
+
clearTimeout(timer);
|
|
97
|
+
client.destroy();
|
|
98
|
+
resolve(true);
|
|
99
|
+
});
|
|
100
|
+
client.on("error", function () {
|
|
101
|
+
clearTimeout(timer);
|
|
102
|
+
resolve(false);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function generateSlug(projectPath, existingSlugs) {
|
|
108
|
+
var base = path.basename(projectPath).toLowerCase().replace(/[^a-z0-9_-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
109
|
+
if (!base) base = "project";
|
|
110
|
+
if (!existingSlugs || existingSlugs.indexOf(base) === -1) return base;
|
|
111
|
+
for (var i = 2; i < 100; i++) {
|
|
112
|
+
var candidate = base + "-" + i;
|
|
113
|
+
if (existingSlugs.indexOf(candidate) === -1) return candidate;
|
|
114
|
+
}
|
|
115
|
+
return base + "-" + Date.now();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function clearStaleConfig() {
|
|
119
|
+
try { fs.unlinkSync(configPath()); } catch (e) {}
|
|
120
|
+
if (process.platform !== "win32") {
|
|
121
|
+
try { fs.unlinkSync(socketPath()); } catch (e) {}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// --- Crash info ---
|
|
126
|
+
|
|
127
|
+
function crashInfoPath() {
|
|
128
|
+
return CRASH_INFO_PATH;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function writeCrashInfo(info) {
|
|
132
|
+
try {
|
|
133
|
+
ensureConfigDir();
|
|
134
|
+
fs.writeFileSync(CRASH_INFO_PATH, JSON.stringify(info));
|
|
135
|
+
} catch (e) {}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function readCrashInfo() {
|
|
139
|
+
try {
|
|
140
|
+
var data = fs.readFileSync(CRASH_INFO_PATH, "utf8");
|
|
141
|
+
return JSON.parse(data);
|
|
142
|
+
} catch (e) {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function clearCrashInfo() {
|
|
148
|
+
try { fs.unlinkSync(CRASH_INFO_PATH); } catch (e) {}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// --- ~/.clayrc (recent projects persistence) ---
|
|
152
|
+
|
|
153
|
+
function clayrcPath() {
|
|
154
|
+
return CLAYRC_PATH;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function loadClayrc() {
|
|
158
|
+
try {
|
|
159
|
+
var data = fs.readFileSync(CLAYRC_PATH, "utf8");
|
|
160
|
+
return JSON.parse(data);
|
|
161
|
+
} catch (e) {
|
|
162
|
+
return { recentProjects: [] };
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function saveClayrc(rc) {
|
|
167
|
+
var tmpPath = CLAYRC_PATH + ".tmp";
|
|
168
|
+
fs.writeFileSync(tmpPath, JSON.stringify(rc, null, 2) + "\n");
|
|
169
|
+
fs.renameSync(tmpPath, CLAYRC_PATH);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Update ~/.clayrc with the current project list from daemon config.
|
|
174
|
+
* Merges with existing entries (preserves addedAt, updates lastUsed).
|
|
175
|
+
*/
|
|
176
|
+
function syncClayrc(projects) {
|
|
177
|
+
var rc = loadClayrc();
|
|
178
|
+
var existing = rc.recentProjects || [];
|
|
179
|
+
|
|
180
|
+
// Build a map by path for quick lookup
|
|
181
|
+
var byPath = {};
|
|
182
|
+
for (var i = 0; i < existing.length; i++) {
|
|
183
|
+
byPath[existing[i].path] = existing[i];
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Update/add current projects
|
|
187
|
+
for (var j = 0; j < projects.length; j++) {
|
|
188
|
+
var p = projects[j];
|
|
189
|
+
if (byPath[p.path]) {
|
|
190
|
+
// Update existing entry
|
|
191
|
+
byPath[p.path].slug = p.slug;
|
|
192
|
+
byPath[p.path].lastUsed = Date.now();
|
|
193
|
+
if (p.title) byPath[p.path].title = p.title;
|
|
194
|
+
else delete byPath[p.path].title;
|
|
195
|
+
} else {
|
|
196
|
+
// New entry
|
|
197
|
+
byPath[p.path] = {
|
|
198
|
+
path: p.path,
|
|
199
|
+
slug: p.slug,
|
|
200
|
+
title: p.title || undefined,
|
|
201
|
+
addedAt: p.addedAt || Date.now(),
|
|
202
|
+
lastUsed: Date.now(),
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Rebuild array, sorted by lastUsed descending
|
|
208
|
+
var all = Object.keys(byPath).map(function (k) { return byPath[k]; });
|
|
209
|
+
all.sort(function (a, b) { return (b.lastUsed || 0) - (a.lastUsed || 0); });
|
|
210
|
+
|
|
211
|
+
// Keep at most 20 recent projects
|
|
212
|
+
rc.recentProjects = all.slice(0, 20);
|
|
213
|
+
saveClayrc(rc);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
module.exports = {
|
|
217
|
+
CONFIG_DIR: CONFIG_DIR,
|
|
218
|
+
configPath: configPath,
|
|
219
|
+
socketPath: socketPath,
|
|
220
|
+
logPath: logPath,
|
|
221
|
+
ensureConfigDir: ensureConfigDir,
|
|
222
|
+
loadConfig: loadConfig,
|
|
223
|
+
saveConfig: saveConfig,
|
|
224
|
+
isPidAlive: isPidAlive,
|
|
225
|
+
isDaemonAlive: isDaemonAlive,
|
|
226
|
+
isDaemonAliveAsync: isDaemonAliveAsync,
|
|
227
|
+
generateSlug: generateSlug,
|
|
228
|
+
clearStaleConfig: clearStaleConfig,
|
|
229
|
+
crashInfoPath: crashInfoPath,
|
|
230
|
+
writeCrashInfo: writeCrashInfo,
|
|
231
|
+
readCrashInfo: readCrashInfo,
|
|
232
|
+
clearCrashInfo: clearCrashInfo,
|
|
233
|
+
clayrcPath: clayrcPath,
|
|
234
|
+
loadClayrc: loadClayrc,
|
|
235
|
+
saveClayrc: saveClayrc,
|
|
236
|
+
syncClayrc: syncClayrc,
|
|
237
|
+
};
|