framer-code-link 0.1.2 → 0.1.4
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/dist/index.js +670 -168
- package/package.json +12 -12
- package/src/controller.test.ts +22 -137
- package/src/controller.ts +243 -107
- package/src/helpers/connection.ts +100 -60
- package/src/helpers/files.ts +99 -37
- package/src/helpers/installer.ts +11 -11
- package/src/helpers/user-actions.ts +7 -11
- package/src/helpers/watcher.ts +4 -9
- package/src/index.ts +13 -5
- package/src/types.ts +4 -2
- package/src/utils/{hashing.ts → hash-tracker.ts} +1 -17
- package/src/utils/logging.ts +191 -6
- package/src/utils/project.ts +15 -8
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import "node:module";
|
|
2
3
|
import { Command } from "commander";
|
|
3
4
|
import fs from "fs/promises";
|
|
4
5
|
import { WebSocketServer } from "ws";
|
|
@@ -9,10 +10,103 @@ import { createHash } from "crypto";
|
|
|
9
10
|
import { setupTypeAcquisition } from "@typescript/ata";
|
|
10
11
|
import ts from "typescript";
|
|
11
12
|
|
|
13
|
+
//#region rolldown:runtime
|
|
14
|
+
var __create = Object.create;
|
|
15
|
+
var __defProp = Object.defineProperty;
|
|
16
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
17
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
18
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
19
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
20
|
+
var __commonJS = (cb, mod) => function() {
|
|
21
|
+
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
22
|
+
};
|
|
23
|
+
var __copyProps = (to, from, except, desc) => {
|
|
24
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
25
|
+
key = keys[i];
|
|
26
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
27
|
+
get: ((k) => from[k]).bind(null, key),
|
|
28
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
return to;
|
|
32
|
+
};
|
|
33
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
34
|
+
value: mod,
|
|
35
|
+
enumerable: true
|
|
36
|
+
}) : target, mod));
|
|
37
|
+
|
|
38
|
+
//#endregion
|
|
39
|
+
//#region ../../node_modules/picocolors/picocolors.js
|
|
40
|
+
var require_picocolors = __commonJS({ "../../node_modules/picocolors/picocolors.js"(exports, module) {
|
|
41
|
+
let p = process || {}, argv = p.argv || [], env = p.env || {};
|
|
42
|
+
let isColorSupported = !(!!env.NO_COLOR || argv.includes("--no-color")) && (!!env.FORCE_COLOR || argv.includes("--color") || p.platform === "win32" || (p.stdout || {}).isTTY && env.TERM !== "dumb" || !!env.CI);
|
|
43
|
+
let formatter = (open, close, replace = open) => (input) => {
|
|
44
|
+
let string = "" + input, index = string.indexOf(close, open.length);
|
|
45
|
+
return ~index ? open + replaceClose(string, close, replace, index) + close : open + string + close;
|
|
46
|
+
};
|
|
47
|
+
let replaceClose = (string, close, replace, index) => {
|
|
48
|
+
let result = "", cursor = 0;
|
|
49
|
+
do {
|
|
50
|
+
result += string.substring(cursor, index) + replace;
|
|
51
|
+
cursor = index + close.length;
|
|
52
|
+
index = string.indexOf(close, cursor);
|
|
53
|
+
} while (~index);
|
|
54
|
+
return result + string.substring(cursor);
|
|
55
|
+
};
|
|
56
|
+
let createColors = (enabled = isColorSupported) => {
|
|
57
|
+
let f = enabled ? formatter : () => String;
|
|
58
|
+
return {
|
|
59
|
+
isColorSupported: enabled,
|
|
60
|
+
reset: f("\x1B[0m", "\x1B[0m"),
|
|
61
|
+
bold: f("\x1B[1m", "\x1B[22m", "\x1B[22m\x1B[1m"),
|
|
62
|
+
dim: f("\x1B[2m", "\x1B[22m", "\x1B[22m\x1B[2m"),
|
|
63
|
+
italic: f("\x1B[3m", "\x1B[23m"),
|
|
64
|
+
underline: f("\x1B[4m", "\x1B[24m"),
|
|
65
|
+
inverse: f("\x1B[7m", "\x1B[27m"),
|
|
66
|
+
hidden: f("\x1B[8m", "\x1B[28m"),
|
|
67
|
+
strikethrough: f("\x1B[9m", "\x1B[29m"),
|
|
68
|
+
black: f("\x1B[30m", "\x1B[39m"),
|
|
69
|
+
red: f("\x1B[31m", "\x1B[39m"),
|
|
70
|
+
green: f("\x1B[32m", "\x1B[39m"),
|
|
71
|
+
yellow: f("\x1B[33m", "\x1B[39m"),
|
|
72
|
+
blue: f("\x1B[34m", "\x1B[39m"),
|
|
73
|
+
magenta: f("\x1B[35m", "\x1B[39m"),
|
|
74
|
+
cyan: f("\x1B[36m", "\x1B[39m"),
|
|
75
|
+
white: f("\x1B[37m", "\x1B[39m"),
|
|
76
|
+
gray: f("\x1B[90m", "\x1B[39m"),
|
|
77
|
+
bgBlack: f("\x1B[40m", "\x1B[49m"),
|
|
78
|
+
bgRed: f("\x1B[41m", "\x1B[49m"),
|
|
79
|
+
bgGreen: f("\x1B[42m", "\x1B[49m"),
|
|
80
|
+
bgYellow: f("\x1B[43m", "\x1B[49m"),
|
|
81
|
+
bgBlue: f("\x1B[44m", "\x1B[49m"),
|
|
82
|
+
bgMagenta: f("\x1B[45m", "\x1B[49m"),
|
|
83
|
+
bgCyan: f("\x1B[46m", "\x1B[49m"),
|
|
84
|
+
bgWhite: f("\x1B[47m", "\x1B[49m"),
|
|
85
|
+
blackBright: f("\x1B[90m", "\x1B[39m"),
|
|
86
|
+
redBright: f("\x1B[91m", "\x1B[39m"),
|
|
87
|
+
greenBright: f("\x1B[92m", "\x1B[39m"),
|
|
88
|
+
yellowBright: f("\x1B[93m", "\x1B[39m"),
|
|
89
|
+
blueBright: f("\x1B[94m", "\x1B[39m"),
|
|
90
|
+
magentaBright: f("\x1B[95m", "\x1B[39m"),
|
|
91
|
+
cyanBright: f("\x1B[96m", "\x1B[39m"),
|
|
92
|
+
whiteBright: f("\x1B[97m", "\x1B[39m"),
|
|
93
|
+
bgBlackBright: f("\x1B[100m", "\x1B[49m"),
|
|
94
|
+
bgRedBright: f("\x1B[101m", "\x1B[49m"),
|
|
95
|
+
bgGreenBright: f("\x1B[102m", "\x1B[49m"),
|
|
96
|
+
bgYellowBright: f("\x1B[103m", "\x1B[49m"),
|
|
97
|
+
bgBlueBright: f("\x1B[104m", "\x1B[49m"),
|
|
98
|
+
bgMagentaBright: f("\x1B[105m", "\x1B[49m"),
|
|
99
|
+
bgCyanBright: f("\x1B[106m", "\x1B[49m"),
|
|
100
|
+
bgWhiteBright: f("\x1B[107m", "\x1B[49m")
|
|
101
|
+
};
|
|
102
|
+
};
|
|
103
|
+
module.exports = createColors();
|
|
104
|
+
module.exports.createColors = createColors;
|
|
105
|
+
} });
|
|
106
|
+
|
|
107
|
+
//#endregion
|
|
12
108
|
//#region src/utils/logging.ts
|
|
13
|
-
|
|
14
|
-
* Logging utilities for consistent output
|
|
15
|
-
*/
|
|
109
|
+
var import_picocolors = __toESM(require_picocolors(), 1);
|
|
16
110
|
let LogLevel = /* @__PURE__ */ function(LogLevel$1) {
|
|
17
111
|
LogLevel$1[LogLevel$1["DEBUG"] = 0] = "DEBUG";
|
|
18
112
|
LogLevel$1[LogLevel$1["INFO"] = 1] = "INFO";
|
|
@@ -21,77 +115,335 @@ let LogLevel = /* @__PURE__ */ function(LogLevel$1) {
|
|
|
21
115
|
return LogLevel$1;
|
|
22
116
|
}({});
|
|
23
117
|
let currentLevel = LogLevel.INFO;
|
|
118
|
+
let lastMessage = "";
|
|
119
|
+
let lastMessageCount = 0;
|
|
120
|
+
const CLEAR_LINE = "\x1B[2K";
|
|
121
|
+
const MOVE_CURSOR_UP = "\x1B[1A";
|
|
122
|
+
function rewriteLastLine(text) {
|
|
123
|
+
if (process.stdout.isTTY) process.stdout.write(`${MOVE_CURSOR_UP}\r${CLEAR_LINE}${text}\n`);
|
|
124
|
+
else process.stdout.write(`${text}\n`);
|
|
125
|
+
}
|
|
126
|
+
let disconnectTimer = null;
|
|
127
|
+
let isShowingDisconnect = false;
|
|
128
|
+
let hadRecentDisconnect = false;
|
|
129
|
+
const DISCONNECT_DELAY_MS = 2e3;
|
|
24
130
|
function setLogLevel(level) {
|
|
25
131
|
currentLevel = level;
|
|
26
132
|
}
|
|
133
|
+
function dedupeMessage(message, count) {
|
|
134
|
+
rewriteLastLine(`${message} ${import_picocolors.default.dim(`(x${count})`)}`);
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Flush any pending deduplicated message
|
|
138
|
+
*/
|
|
139
|
+
function flushDedupe() {
|
|
140
|
+
if (lastMessageCount > 1) dedupeMessage(lastMessage, lastMessageCount);
|
|
141
|
+
lastMessage = "";
|
|
142
|
+
lastMessageCount = 0;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Log with deduplication - repeated messages within window get counted
|
|
146
|
+
*/
|
|
147
|
+
function logWithDedupe(message, writer) {
|
|
148
|
+
if (message === lastMessage) {
|
|
149
|
+
lastMessageCount++;
|
|
150
|
+
dedupeMessage(message, lastMessageCount);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
lastMessage = message;
|
|
154
|
+
lastMessageCount = 1;
|
|
155
|
+
writer();
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Print the startup banner - one colored line
|
|
159
|
+
*/
|
|
160
|
+
function banner(version, port) {
|
|
161
|
+
console.log();
|
|
162
|
+
let message = ` ${import_picocolors.default.cyan(import_picocolors.default.bold("⚡ Code Link"))} ${import_picocolors.default.dim(`v${version}`)}`;
|
|
163
|
+
if (currentLevel <= LogLevel.DEBUG) message += ` ${import_picocolors.default.dim("Port")} ${import_picocolors.default.yellow(port)}`;
|
|
164
|
+
console.log(message);
|
|
165
|
+
console.log();
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Debug-level logging - only shown with --verbose flag
|
|
169
|
+
*/
|
|
27
170
|
function debug(message, ...args) {
|
|
28
|
-
if (currentLevel <= LogLevel.DEBUG) console.debug(`[DEBUG] ${message}
|
|
171
|
+
if (currentLevel <= LogLevel.DEBUG) console.debug(import_picocolors.default.dim(`[DEBUG] ${message}`), ...args);
|
|
29
172
|
}
|
|
173
|
+
/**
|
|
174
|
+
* Info-level logging - shown by default, no prefix
|
|
175
|
+
*/
|
|
30
176
|
function info(message, ...args) {
|
|
31
|
-
if (currentLevel <= LogLevel.INFO)
|
|
177
|
+
if (currentLevel <= LogLevel.INFO) {
|
|
178
|
+
const formatted = args.length > 0 ? `${message} ${args.join(" ")}` : message;
|
|
179
|
+
logWithDedupe(formatted, () => console.log(formatted));
|
|
180
|
+
}
|
|
32
181
|
}
|
|
182
|
+
/**
|
|
183
|
+
* Warning-level logging
|
|
184
|
+
*/
|
|
33
185
|
function warn(message, ...args) {
|
|
34
|
-
if (currentLevel <= LogLevel.WARN)
|
|
186
|
+
if (currentLevel <= LogLevel.WARN) {
|
|
187
|
+
flushDedupe();
|
|
188
|
+
console.warn(import_picocolors.default.yellow(`⚠ ${message}`), ...args);
|
|
189
|
+
}
|
|
35
190
|
}
|
|
191
|
+
/**
|
|
192
|
+
* Error-level logging
|
|
193
|
+
*/
|
|
36
194
|
function error(message, ...args) {
|
|
37
|
-
if (currentLevel <= LogLevel.ERROR)
|
|
195
|
+
if (currentLevel <= LogLevel.ERROR) {
|
|
196
|
+
flushDedupe();
|
|
197
|
+
console.error(import_picocolors.default.red(`✗ ${message}`), ...args);
|
|
198
|
+
}
|
|
38
199
|
}
|
|
200
|
+
/**
|
|
201
|
+
* Success message with checkmark
|
|
202
|
+
*/
|
|
39
203
|
function success(message, ...args) {
|
|
40
|
-
if (currentLevel <= LogLevel.INFO)
|
|
204
|
+
if (currentLevel <= LogLevel.INFO) {
|
|
205
|
+
flushDedupe();
|
|
206
|
+
console.log(import_picocolors.default.green(`✓ ${message}`), ...args);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* File sync indicators
|
|
211
|
+
*/
|
|
212
|
+
function fileDown(fileName) {
|
|
213
|
+
if (currentLevel <= LogLevel.INFO) {
|
|
214
|
+
const msg = ` ${import_picocolors.default.blue("↓")} ${fileName}`;
|
|
215
|
+
logWithDedupe(msg, () => console.log(msg));
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
function fileUp(fileName) {
|
|
219
|
+
if (currentLevel <= LogLevel.INFO) {
|
|
220
|
+
const msg = ` ${import_picocolors.default.green("↑")} ${fileName}`;
|
|
221
|
+
logWithDedupe(msg, () => console.log(msg));
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
function fileDelete(fileName) {
|
|
225
|
+
if (currentLevel <= LogLevel.INFO) {
|
|
226
|
+
const msg = ` ${import_picocolors.default.red("×")} ${fileName}`;
|
|
227
|
+
logWithDedupe(msg, () => console.log(msg));
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Status message (dimmed, for "watching for changes..." etc)
|
|
232
|
+
*/
|
|
233
|
+
function status(message) {
|
|
234
|
+
if (currentLevel <= LogLevel.INFO) {
|
|
235
|
+
flushDedupe();
|
|
236
|
+
console.log(import_picocolors.default.dim(` ${message}`));
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Schedule a delayed disconnect message.
|
|
241
|
+
* If reconnection happens before the delay, the message is cancelled.
|
|
242
|
+
*/
|
|
243
|
+
function scheduleDisconnectMessage(callback) {
|
|
244
|
+
if (disconnectTimer) clearTimeout(disconnectTimer);
|
|
245
|
+
hadRecentDisconnect = true;
|
|
246
|
+
isShowingDisconnect = false;
|
|
247
|
+
disconnectTimer = setTimeout(() => {
|
|
248
|
+
isShowingDisconnect = true;
|
|
249
|
+
callback();
|
|
250
|
+
disconnectTimer = null;
|
|
251
|
+
}, DISCONNECT_DELAY_MS);
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Cancel pending disconnect message (called on reconnect)
|
|
255
|
+
*/
|
|
256
|
+
function cancelDisconnectMessage() {
|
|
257
|
+
if (disconnectTimer) {
|
|
258
|
+
clearTimeout(disconnectTimer);
|
|
259
|
+
disconnectTimer = null;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Check if we showed a disconnect message (need to show reconnect)
|
|
264
|
+
*/
|
|
265
|
+
function didShowDisconnect() {
|
|
266
|
+
return isShowingDisconnect;
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Check if we recently saw a disconnect (even if the message was suppressed)
|
|
270
|
+
*/
|
|
271
|
+
function wasRecentlyDisconnected() {
|
|
272
|
+
return hadRecentDisconnect;
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Reset disconnect state after successful reconnect
|
|
276
|
+
*/
|
|
277
|
+
function resetDisconnectState() {
|
|
278
|
+
isShowingDisconnect = false;
|
|
279
|
+
hadRecentDisconnect = false;
|
|
41
280
|
}
|
|
42
281
|
|
|
43
282
|
//#endregion
|
|
44
283
|
//#region src/helpers/connection.ts
|
|
45
284
|
/**
|
|
46
285
|
* Initializes a WebSocket server and returns a connection interface
|
|
286
|
+
* Returns a Promise that resolves when the server is ready, or rejects on startup errors
|
|
47
287
|
*/
|
|
48
288
|
function initConnection(port) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
289
|
+
return new Promise((resolve, reject) => {
|
|
290
|
+
const wss = new WebSocketServer({ port });
|
|
291
|
+
const handlers = {};
|
|
292
|
+
let connectionId = 0;
|
|
293
|
+
let isReady = false;
|
|
294
|
+
wss.on("error", (err) => {
|
|
295
|
+
if (!isReady) {
|
|
296
|
+
if (err.code === "EADDRINUSE") {
|
|
297
|
+
error(`Port ${port} is already in use.`);
|
|
298
|
+
error(`This usually means another instance of Code Link is already running.`);
|
|
299
|
+
error(``);
|
|
300
|
+
error(`To fix this:`);
|
|
301
|
+
error(` 1. Close any other terminal running Code Link for this project`);
|
|
302
|
+
error(` 2. Or run: lsof -i :${port} | grep LISTEN`);
|
|
303
|
+
error(` Then kill the process: kill -9 <PID>`);
|
|
304
|
+
reject(new Error(`Port ${port} is already in use`));
|
|
305
|
+
} else {
|
|
306
|
+
error(`Failed to start WebSocket server: ${err.message}`);
|
|
307
|
+
reject(err);
|
|
308
|
+
}
|
|
309
|
+
return;
|
|
61
310
|
}
|
|
311
|
+
error(`WebSocket server error: ${err.message}`);
|
|
62
312
|
});
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
313
|
+
wss.on("listening", () => {
|
|
314
|
+
isReady = true;
|
|
315
|
+
debug(`WebSocket server listening on port ${port}`);
|
|
316
|
+
wss.on("connection", (ws) => {
|
|
317
|
+
const connId = ++connectionId;
|
|
318
|
+
debug(`Client connected (conn ${connId})`);
|
|
319
|
+
ws.on("message", (data) => {
|
|
320
|
+
try {
|
|
321
|
+
const message = JSON.parse(data.toString());
|
|
322
|
+
if (message.type === "handshake") {
|
|
323
|
+
debug(`Received handshake (conn ${connId})`);
|
|
324
|
+
handlers.onHandshake?.(ws, message);
|
|
325
|
+
} else handlers.onMessage?.(message);
|
|
326
|
+
} catch (err) {
|
|
327
|
+
error(`Failed to parse message:`, err);
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
ws.on("close", (code, reason) => {
|
|
331
|
+
debug(`Client disconnected (code: ${code}, reason: ${reason?.toString() || "none"})`);
|
|
332
|
+
handlers.onDisconnect?.();
|
|
333
|
+
});
|
|
334
|
+
ws.on("error", (err) => {
|
|
335
|
+
error(`WebSocket error:`, err);
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
resolve({
|
|
339
|
+
on(event, handler) {
|
|
340
|
+
if (event === "handshake") handlers.onHandshake = handler;
|
|
341
|
+
else if (event === "message") handlers.onMessage = handler;
|
|
342
|
+
else if (event === "disconnect") handlers.onDisconnect = handler;
|
|
343
|
+
else if (event === "error") handlers.onError = handler;
|
|
344
|
+
},
|
|
345
|
+
close() {
|
|
346
|
+
wss.close();
|
|
347
|
+
}
|
|
348
|
+
});
|
|
69
349
|
});
|
|
70
350
|
});
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* WebSocket readyState constants for reference
|
|
354
|
+
*/
|
|
355
|
+
const READY_STATE = {
|
|
356
|
+
CONNECTING: 0,
|
|
357
|
+
OPEN: 1,
|
|
358
|
+
CLOSING: 2,
|
|
359
|
+
CLOSED: 3
|
|
360
|
+
};
|
|
361
|
+
function readyStateToString(state) {
|
|
362
|
+
switch (state) {
|
|
363
|
+
case 0: return "CONNECTING";
|
|
364
|
+
case 1: return "OPEN";
|
|
365
|
+
case 2: return "CLOSING";
|
|
366
|
+
case 3: return "CLOSED";
|
|
367
|
+
default: return `UNKNOWN(${state})`;
|
|
368
|
+
}
|
|
82
369
|
}
|
|
83
370
|
/**
|
|
84
371
|
* Sends a message to a connected socket
|
|
372
|
+
* Returns false if the socket is not open (instead of throwing)
|
|
85
373
|
*/
|
|
86
374
|
function sendMessage(socket, message) {
|
|
87
|
-
return new Promise((resolve
|
|
375
|
+
return new Promise((resolve) => {
|
|
376
|
+
if (socket.readyState !== READY_STATE.OPEN) {
|
|
377
|
+
const stateStr = readyStateToString(socket.readyState);
|
|
378
|
+
debug(`Cannot send ${message.type}: socket is ${stateStr}`);
|
|
379
|
+
resolve(false);
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
88
382
|
socket.send(JSON.stringify(message), (err) => {
|
|
89
|
-
if (err)
|
|
90
|
-
|
|
383
|
+
if (err) {
|
|
384
|
+
debug(`Send error for ${message.type}: ${err.message}`);
|
|
385
|
+
resolve(false);
|
|
386
|
+
} else resolve(true);
|
|
91
387
|
});
|
|
92
388
|
});
|
|
93
389
|
}
|
|
94
390
|
|
|
391
|
+
//#endregion
|
|
392
|
+
//#region ../shared/dist/hash.js
|
|
393
|
+
/**
|
|
394
|
+
* Base58 alphabet (no 0/O/I/l to avoid confusion)
|
|
395
|
+
*/
|
|
396
|
+
const BASE58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
397
|
+
/**
|
|
398
|
+
* Derive a short, deterministic hash from the full Framer project hash.
|
|
399
|
+
* Uses a simple numeric hash encoded in base58 for compactness.
|
|
400
|
+
* Idempotent: if input is already the target length, returns it unchanged.
|
|
401
|
+
*/
|
|
402
|
+
function shortProjectHash(fullHash, length = 8) {
|
|
403
|
+
if (fullHash.length === length) return fullHash;
|
|
404
|
+
let h1 = 0;
|
|
405
|
+
let h2 = 0;
|
|
406
|
+
for (let i = 0; i < fullHash.length; i++) {
|
|
407
|
+
const char = fullHash.charCodeAt(i);
|
|
408
|
+
h1 = Math.imul(h1 ^ char, 2246822507);
|
|
409
|
+
h2 = Math.imul(h2 ^ char, 3266489909);
|
|
410
|
+
}
|
|
411
|
+
h1 ^= h2 >>> 16;
|
|
412
|
+
h2 ^= h1 >>> 13;
|
|
413
|
+
let result = "";
|
|
414
|
+
const combined = [Math.abs(h1), Math.abs(h2)];
|
|
415
|
+
for (const num of combined) {
|
|
416
|
+
let n = num >>> 0;
|
|
417
|
+
while (n > 0 && result.length < length) {
|
|
418
|
+
result += BASE58[n % 58];
|
|
419
|
+
n = Math.floor(n / 58);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
while (result.length < length) result += BASE58[0];
|
|
423
|
+
return result.slice(0, length);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
//#endregion
|
|
427
|
+
//#region ../shared/dist/ports.js
|
|
428
|
+
/**
|
|
429
|
+
* Generate a deterministic port number from a project hash (full or short).
|
|
430
|
+
* Port range: 3847-4096 (250 possible ports)
|
|
431
|
+
* Must match between CLI and plugin.
|
|
432
|
+
*
|
|
433
|
+
* Internally normalizes to the short id so both full and short inputs yield the same port.
|
|
434
|
+
*/
|
|
435
|
+
function getPortFromHash(projectHash) {
|
|
436
|
+
const shortId = shortProjectHash(projectHash);
|
|
437
|
+
let hash = 0;
|
|
438
|
+
for (let i = 0; i < shortId.length; i++) {
|
|
439
|
+
const char = shortId.charCodeAt(i);
|
|
440
|
+
hash = (hash << 5) - hash + char;
|
|
441
|
+
hash = hash & hash;
|
|
442
|
+
}
|
|
443
|
+
const portOffset = Math.abs(hash) % 250;
|
|
444
|
+
return 3847 + portOffset;
|
|
445
|
+
}
|
|
446
|
+
|
|
95
447
|
//#endregion
|
|
96
448
|
//#region ../shared/dist/paths.js
|
|
97
449
|
/**
|
|
@@ -195,6 +547,16 @@ function sanitizeFilePath(input, capitalizeReactComponent = true) {
|
|
|
195
547
|
function isSupportedExtension$1(filePath) {
|
|
196
548
|
return /\.(tsx?|jsx?|json)$/i.test(filePath);
|
|
197
549
|
}
|
|
550
|
+
/**
|
|
551
|
+
* Pluralize a word based on count
|
|
552
|
+
* @example pluralize(1, "file") => "1 file"
|
|
553
|
+
* @example pluralize(3, "file") => "3 files"
|
|
554
|
+
* @example pluralize(0, "conflict") => "0 conflicts"
|
|
555
|
+
*/
|
|
556
|
+
function pluralize(count, singular, plural) {
|
|
557
|
+
const word = count === 1 ? singular : plural ?? `${singular}s`;
|
|
558
|
+
return `${count} ${word}`;
|
|
559
|
+
}
|
|
198
560
|
|
|
199
561
|
//#endregion
|
|
200
562
|
//#region src/utils/paths.ts
|
|
@@ -240,7 +602,7 @@ function initWatcher(filesDir) {
|
|
|
240
602
|
persistent: true,
|
|
241
603
|
ignoreInitial: false
|
|
242
604
|
});
|
|
243
|
-
|
|
605
|
+
debug(`Watching directory: ${filesDir}`);
|
|
244
606
|
const emitEvent = async (kind, absolutePath) => {
|
|
245
607
|
if (!isSupportedExtension$1(absolutePath)) return;
|
|
246
608
|
const rawRelativePath = normalizePath(getRelativePath(filesDir, absolutePath));
|
|
@@ -252,10 +614,10 @@ function initWatcher(filesDir) {
|
|
|
252
614
|
try {
|
|
253
615
|
await fs.mkdir(path.dirname(newAbsolutePath), { recursive: true });
|
|
254
616
|
await fs.rename(absolutePath, newAbsolutePath);
|
|
255
|
-
|
|
617
|
+
debug(`Renamed ${rawRelativePath} -> ${relativePath}`);
|
|
256
618
|
effectiveAbsolutePath = newAbsolutePath;
|
|
257
619
|
} catch (err) {
|
|
258
|
-
warn(`Failed to rename ${rawRelativePath}
|
|
620
|
+
warn(`Failed to rename ${rawRelativePath}`, err);
|
|
259
621
|
}
|
|
260
622
|
}
|
|
261
623
|
let content;
|
|
@@ -366,6 +728,10 @@ const SUPPORTED_EXTENSIONS = [
|
|
|
366
728
|
];
|
|
367
729
|
const DEFAULT_EXTENSION = ".tsx";
|
|
368
730
|
const DEFAULT_REMOTE_DRIFT_MS = 2e3;
|
|
731
|
+
/** Normalize file name for case-insensitive comparison (macOS/Windows compat) */
|
|
732
|
+
function normalizeForComparison(fileName) {
|
|
733
|
+
return fileName.toLowerCase();
|
|
734
|
+
}
|
|
369
735
|
/**
|
|
370
736
|
* Lists all supported files in the files directory
|
|
371
737
|
*/
|
|
@@ -409,19 +775,34 @@ async function detectConflicts(remoteFiles, filesDir, options = {}) {
|
|
|
409
775
|
const detect = options.detectConflicts ?? true;
|
|
410
776
|
const preferRemote = options.preferRemote ?? false;
|
|
411
777
|
const persistedState = options.persistedState;
|
|
778
|
+
const getPersistedState = (fileName) => persistedState?.get(normalizeForComparison(fileName)) ?? persistedState?.get(fileName);
|
|
412
779
|
debug(`Detecting conflicts for ${remoteFiles.length} remote files`);
|
|
413
780
|
const localFiles = await listFiles(filesDir);
|
|
414
|
-
const localFileMap = new Map(localFiles.map((f) => [f.name, f]));
|
|
781
|
+
const localFileMap = new Map(localFiles.map((f) => [normalizeForComparison(f.name), f]));
|
|
782
|
+
const remoteFileMap = new Map(remoteFiles.map((f) => {
|
|
783
|
+
const normalized = resolveRemoteReference(filesDir, f.name);
|
|
784
|
+
return [normalizeForComparison(normalized.relativePath), f];
|
|
785
|
+
}));
|
|
415
786
|
const processedFiles = /* @__PURE__ */ new Set();
|
|
416
787
|
for (const remote of remoteFiles) {
|
|
417
788
|
const normalized = resolveRemoteReference(filesDir, remote.name);
|
|
418
|
-
const
|
|
419
|
-
|
|
420
|
-
|
|
789
|
+
const normalizedKey = normalizeForComparison(normalized.relativePath);
|
|
790
|
+
const local = localFileMap.get(normalizedKey);
|
|
791
|
+
processedFiles.add(normalizedKey);
|
|
792
|
+
const persisted = getPersistedState(normalized.relativePath);
|
|
421
793
|
const localHash = local ? hashFileContent(local.content) : null;
|
|
422
794
|
const localMatchesPersisted = !!persisted && !!local && localHash === persisted.contentHash;
|
|
423
795
|
if (!local) {
|
|
424
|
-
|
|
796
|
+
if (persisted) {
|
|
797
|
+
debug(`Conflict: ${normalized.relativePath} deleted locally while offline`);
|
|
798
|
+
conflicts.push({
|
|
799
|
+
fileName: normalized.relativePath,
|
|
800
|
+
localContent: null,
|
|
801
|
+
remoteContent: remote.content,
|
|
802
|
+
remoteModifiedAt: remote.modifiedAt,
|
|
803
|
+
lastSyncedAt: persisted?.timestamp
|
|
804
|
+
});
|
|
805
|
+
} else writes.push({
|
|
425
806
|
name: normalized.relativePath,
|
|
426
807
|
content: remote.content,
|
|
427
808
|
modifiedAt: remote.modifiedAt
|
|
@@ -448,11 +829,32 @@ async function detectConflicts(remoteFiles, filesDir, options = {}) {
|
|
|
448
829
|
localClean
|
|
449
830
|
});
|
|
450
831
|
}
|
|
451
|
-
for (const local of localFiles)
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
832
|
+
for (const local of localFiles) {
|
|
833
|
+
const localKey = normalizeForComparison(local.name);
|
|
834
|
+
if (!processedFiles.has(localKey)) {
|
|
835
|
+
const persisted = getPersistedState(local.name);
|
|
836
|
+
if (persisted) {
|
|
837
|
+
debug(`Conflict: ${local.name} deleted in Framer while offline`);
|
|
838
|
+
conflicts.push({
|
|
839
|
+
fileName: local.name,
|
|
840
|
+
localContent: local.content,
|
|
841
|
+
remoteContent: null,
|
|
842
|
+
localModifiedAt: local.modifiedAt,
|
|
843
|
+
lastSyncedAt: persisted?.timestamp
|
|
844
|
+
});
|
|
845
|
+
} else localOnly.push({
|
|
846
|
+
name: local.name,
|
|
847
|
+
content: local.content,
|
|
848
|
+
modifiedAt: local.modifiedAt
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
if (persistedState) for (const [fileName] of persistedState) {
|
|
853
|
+
const normalizedKey = normalizeForComparison(fileName);
|
|
854
|
+
const inLocal = localFileMap.has(normalizedKey);
|
|
855
|
+
const inRemote = remoteFileMap.has(normalizedKey);
|
|
856
|
+
if (!inLocal && !inRemote) debug(`[AUTO-RESOLVE] ${fileName}: deleted on both sides, no conflict`);
|
|
857
|
+
}
|
|
456
858
|
return {
|
|
457
859
|
conflicts,
|
|
458
860
|
writes,
|
|
@@ -468,30 +870,30 @@ function autoResolveConflicts(conflicts, versions, options = {}) {
|
|
|
468
870
|
for (const conflict of conflicts) {
|
|
469
871
|
const latestRemoteVersionMs = versionMap.get(conflict.fileName);
|
|
470
872
|
const lastSyncedAt = conflict.lastSyncedAt;
|
|
471
|
-
|
|
873
|
+
debug(`Auto-resolve checking ${conflict.fileName}`);
|
|
472
874
|
if (!latestRemoteVersionMs) {
|
|
473
|
-
|
|
875
|
+
debug(` No remote version data, keeping conflict`);
|
|
474
876
|
remainingConflicts.push(conflict);
|
|
475
877
|
continue;
|
|
476
878
|
}
|
|
477
879
|
if (!lastSyncedAt) {
|
|
478
|
-
|
|
880
|
+
debug(` No last sync timestamp, keeping conflict`);
|
|
479
881
|
remainingConflicts.push(conflict);
|
|
480
882
|
continue;
|
|
481
883
|
}
|
|
482
|
-
|
|
483
|
-
|
|
884
|
+
debug(` Remote: ${new Date(latestRemoteVersionMs).toISOString()}`);
|
|
885
|
+
debug(` Synced: ${new Date(lastSyncedAt).toISOString()}`);
|
|
484
886
|
const remoteUnchanged = latestRemoteVersionMs <= lastSyncedAt + remoteDriftMs;
|
|
485
887
|
const localClean = conflict.localClean === true;
|
|
486
888
|
if (remoteUnchanged && !localClean) {
|
|
487
|
-
|
|
889
|
+
debug(` Remote unchanged, local changed -> LOCAL`);
|
|
488
890
|
autoResolvedLocal.push(conflict);
|
|
489
891
|
} else if (localClean && !remoteUnchanged) {
|
|
490
|
-
|
|
892
|
+
debug(` Local unchanged, remote changed -> REMOTE`);
|
|
491
893
|
autoResolvedRemote.push(conflict);
|
|
492
|
-
} else if (remoteUnchanged && localClean)
|
|
894
|
+
} else if (remoteUnchanged && localClean) debug(` Both unchanged, skipping`);
|
|
493
895
|
else {
|
|
494
|
-
|
|
896
|
+
debug(` Both changed, real conflict`);
|
|
495
897
|
remainingConflicts.push(conflict);
|
|
496
898
|
}
|
|
497
899
|
}
|
|
@@ -506,7 +908,7 @@ function autoResolveConflicts(conflicts, versions, options = {}) {
|
|
|
506
908
|
* CRITICAL: Update hashTracker BEFORE writing to disk
|
|
507
909
|
*/
|
|
508
910
|
async function writeRemoteFiles(files, filesDir, hashTracker, installer) {
|
|
509
|
-
|
|
911
|
+
debug(`Writing ${files.length} remote files`);
|
|
510
912
|
for (const file of files) try {
|
|
511
913
|
const normalized = resolveRemoteReference(filesDir, file.name);
|
|
512
914
|
const fullPath = normalized.absolutePath;
|
|
@@ -528,12 +930,12 @@ async function deleteLocalFile(fileName, filesDir, hashTracker) {
|
|
|
528
930
|
hashTracker.markDelete(normalized.relativePath);
|
|
529
931
|
await fs.unlink(normalized.absolutePath);
|
|
530
932
|
hashTracker.forget(normalized.relativePath);
|
|
531
|
-
|
|
933
|
+
debug(`Deleted file: ${normalized.relativePath}`);
|
|
532
934
|
} catch (err) {
|
|
533
935
|
const nodeError = err;
|
|
534
936
|
if (nodeError?.code === "ENOENT") {
|
|
535
937
|
hashTracker.forget(normalized.relativePath);
|
|
536
|
-
|
|
938
|
+
debug(`File already deleted: ${normalized.relativePath}`);
|
|
537
939
|
return;
|
|
538
940
|
}
|
|
539
941
|
hashTracker.clearDelete(normalized.relativePath);
|
|
@@ -673,13 +1075,13 @@ var Installer = class {
|
|
|
673
1075
|
} catch {}
|
|
674
1076
|
if (pkgMatch && !seenPackages.has(pkgMatch[1])) {
|
|
675
1077
|
seenPackages.add(pkgMatch[1]);
|
|
676
|
-
|
|
1078
|
+
debug(`📦 Types: ${pkgMatch[1]}`);
|
|
677
1079
|
}
|
|
678
1080
|
await this.writeTypeFile(receivedPath, code);
|
|
679
1081
|
}
|
|
680
1082
|
}
|
|
681
1083
|
});
|
|
682
|
-
|
|
1084
|
+
debug("Type installer initialized");
|
|
683
1085
|
}
|
|
684
1086
|
/**
|
|
685
1087
|
* Ensure the project scaffolding exists (tsconfig, declarations, etc.)
|
|
@@ -727,7 +1129,7 @@ var Installer = class {
|
|
|
727
1129
|
const hash = imports.map((imp) => imp.name).sort().join(",");
|
|
728
1130
|
if (this.processedImports.has(hash)) return;
|
|
729
1131
|
this.processedImports.add(hash);
|
|
730
|
-
|
|
1132
|
+
debug(`Processing imports for ${fileName} (${imports.length} packages)`);
|
|
731
1133
|
try {
|
|
732
1134
|
await this.ata(content);
|
|
733
1135
|
} catch (err) {
|
|
@@ -822,7 +1224,7 @@ var Installer = class {
|
|
|
822
1224
|
include: ["files/**/*", "framer-modules.d.ts"]
|
|
823
1225
|
};
|
|
824
1226
|
await fs.writeFile(tsconfigPath, JSON.stringify(config, null, 2));
|
|
825
|
-
|
|
1227
|
+
debug("Created tsconfig.json");
|
|
826
1228
|
}
|
|
827
1229
|
}
|
|
828
1230
|
async ensurePrettierConfig() {
|
|
@@ -837,7 +1239,7 @@ var Installer = class {
|
|
|
837
1239
|
trailingComma: "es5"
|
|
838
1240
|
};
|
|
839
1241
|
await fs.writeFile(prettierPath, JSON.stringify(config, null, 2));
|
|
840
|
-
|
|
1242
|
+
debug("Created .prettierrc");
|
|
841
1243
|
}
|
|
842
1244
|
}
|
|
843
1245
|
async ensureFramerDeclarations() {
|
|
@@ -854,7 +1256,7 @@ declare module "https://framerusercontent.com/*"
|
|
|
854
1256
|
declare module "*.json"
|
|
855
1257
|
`;
|
|
856
1258
|
await fs.writeFile(declarationsPath, declarations);
|
|
857
|
-
|
|
1259
|
+
debug("Created framer-modules.d.ts");
|
|
858
1260
|
}
|
|
859
1261
|
}
|
|
860
1262
|
async ensurePackageJson() {
|
|
@@ -870,7 +1272,7 @@ declare module "*.json"
|
|
|
870
1272
|
description: "Framer files synced with framer-code-link"
|
|
871
1273
|
};
|
|
872
1274
|
await fs.writeFile(packagePath, JSON.stringify(pkg, null, 2));
|
|
873
|
-
|
|
1275
|
+
debug("Created package.json");
|
|
874
1276
|
}
|
|
875
1277
|
}
|
|
876
1278
|
async ensureReact18Types() {
|
|
@@ -882,9 +1284,9 @@ declare module "*.json"
|
|
|
882
1284
|
"jsx-runtime.d.ts",
|
|
883
1285
|
"jsx-dev-runtime.d.ts"
|
|
884
1286
|
];
|
|
885
|
-
if (await this.hasTypePackage(reactTypesDir, REACT_TYPES_VERSION, reactFiles))
|
|
1287
|
+
if (await this.hasTypePackage(reactTypesDir, REACT_TYPES_VERSION, reactFiles)) debug("📦 React types (from cache)");
|
|
886
1288
|
else {
|
|
887
|
-
|
|
1289
|
+
debug("Downloading React 18 types...");
|
|
888
1290
|
await this.downloadTypePackage("@types/react", REACT_TYPES_VERSION, reactTypesDir, reactFiles);
|
|
889
1291
|
}
|
|
890
1292
|
const reactDomDir = path.join(this.projectDir, "node_modules/@types/react-dom");
|
|
@@ -893,7 +1295,7 @@ declare module "*.json"
|
|
|
893
1295
|
"index.d.ts",
|
|
894
1296
|
"client.d.ts"
|
|
895
1297
|
];
|
|
896
|
-
if (await this.hasTypePackage(reactDomDir, REACT_DOM_TYPES_VERSION, reactDomFiles))
|
|
1298
|
+
if (await this.hasTypePackage(reactDomDir, REACT_DOM_TYPES_VERSION, reactDomFiles)) debug("📦 React DOM types (from cache)");
|
|
897
1299
|
else await this.downloadTypePackage("@types/react-dom", REACT_DOM_TYPES_VERSION, reactDomDir, reactDomFiles);
|
|
898
1300
|
}
|
|
899
1301
|
async hasTypePackage(destinationDir, version, files) {
|
|
@@ -958,7 +1360,7 @@ async function fetchWithRetry(url, init, retries = MAX_FETCH_RETRIES) {
|
|
|
958
1360
|
}
|
|
959
1361
|
|
|
960
1362
|
//#endregion
|
|
961
|
-
//#region src/utils/
|
|
1363
|
+
//#region src/utils/hash-tracker.ts
|
|
962
1364
|
/**
|
|
963
1365
|
* Creates a hash tracker instance for echo prevention
|
|
964
1366
|
*/
|
|
@@ -1000,26 +1402,11 @@ function createHashTracker() {
|
|
|
1000
1402
|
};
|
|
1001
1403
|
}
|
|
1002
1404
|
/**
|
|
1003
|
-
* Computes a hash of file content for comparison
|
|
1405
|
+
* Computes a SHA256 hash of file content for comparison
|
|
1004
1406
|
*/
|
|
1005
1407
|
function hashContent(content) {
|
|
1006
1408
|
return createHash("sha256").update(content).digest("hex");
|
|
1007
1409
|
}
|
|
1008
|
-
/**
|
|
1009
|
-
* Generate a deterministic port number from a project hash
|
|
1010
|
-
* Port range: 3847-4096 (250 possible ports)
|
|
1011
|
-
* Uses simple hash to match client-side implementation
|
|
1012
|
-
*/
|
|
1013
|
-
function getPortFromHash(projectHash) {
|
|
1014
|
-
let hash = 0;
|
|
1015
|
-
for (let i = 0; i < projectHash.length; i++) {
|
|
1016
|
-
const char = projectHash.charCodeAt(i);
|
|
1017
|
-
hash = (hash << 5) - hash + char;
|
|
1018
|
-
hash = hash & hash;
|
|
1019
|
-
}
|
|
1020
|
-
const portOffset = Math.abs(hash) % 250;
|
|
1021
|
-
return 3847 + portOffset;
|
|
1022
|
-
}
|
|
1023
1410
|
|
|
1024
1411
|
//#endregion
|
|
1025
1412
|
//#region src/utils/file-metadata-cache.ts
|
|
@@ -1126,7 +1513,7 @@ var UserActionCoordinator = class {
|
|
|
1126
1513
|
return await confirmationPromise;
|
|
1127
1514
|
} catch (err) {
|
|
1128
1515
|
if (err instanceof PluginDisconnectedError) {
|
|
1129
|
-
|
|
1516
|
+
debug(`Plugin disconnected while waiting for delete confirmation: ${fileName}`);
|
|
1130
1517
|
return false;
|
|
1131
1518
|
}
|
|
1132
1519
|
throw err;
|
|
@@ -1158,7 +1545,7 @@ var UserActionCoordinator = class {
|
|
|
1158
1545
|
return new Map(results);
|
|
1159
1546
|
} catch (err) {
|
|
1160
1547
|
if (err instanceof PluginDisconnectedError) {
|
|
1161
|
-
|
|
1548
|
+
debug("Plugin disconnected while awaiting conflict decisions");
|
|
1162
1549
|
return /* @__PURE__ */ new Map();
|
|
1163
1550
|
}
|
|
1164
1551
|
throw err;
|
|
@@ -1173,7 +1560,7 @@ var UserActionCoordinator = class {
|
|
|
1173
1560
|
resolve,
|
|
1174
1561
|
reject
|
|
1175
1562
|
});
|
|
1176
|
-
|
|
1563
|
+
debug(`Awaiting ${description}: ${actionId}`);
|
|
1177
1564
|
});
|
|
1178
1565
|
}
|
|
1179
1566
|
/**
|
|
@@ -1182,12 +1569,12 @@ var UserActionCoordinator = class {
|
|
|
1182
1569
|
handleConfirmation(actionId, value) {
|
|
1183
1570
|
const pending = this.pendingActions.get(actionId);
|
|
1184
1571
|
if (!pending) {
|
|
1185
|
-
|
|
1572
|
+
debug(`Unexpected confirmation for ${actionId}`);
|
|
1186
1573
|
return false;
|
|
1187
1574
|
}
|
|
1188
1575
|
this.pendingActions.delete(actionId);
|
|
1189
1576
|
pending.resolve(value);
|
|
1190
|
-
|
|
1577
|
+
debug(`Confirmed: ${actionId}`);
|
|
1191
1578
|
return true;
|
|
1192
1579
|
}
|
|
1193
1580
|
/**
|
|
@@ -1196,7 +1583,7 @@ var UserActionCoordinator = class {
|
|
|
1196
1583
|
cleanup() {
|
|
1197
1584
|
for (const [actionId, pending] of this.pendingActions.entries()) {
|
|
1198
1585
|
pending.reject(new PluginDisconnectedError());
|
|
1199
|
-
|
|
1586
|
+
debug(`Cancelled pending action: ${actionId}`);
|
|
1200
1587
|
}
|
|
1201
1588
|
this.pendingActions.clear();
|
|
1202
1589
|
}
|
|
@@ -1243,6 +1630,19 @@ function validateIncomingChange(file, fileMeta, currentMode) {
|
|
|
1243
1630
|
function toPackageName(name) {
|
|
1244
1631
|
return name.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/^-+|-+$/g, "").replace(/-+/g, "-");
|
|
1245
1632
|
}
|
|
1633
|
+
function toDirName(name) {
|
|
1634
|
+
return name.replace(/[^a-zA-Z0-9- ]/g, "-").replace(/^[-\s]+|[-\s]+$/g, "").replace(/-+/g, "-");
|
|
1635
|
+
}
|
|
1636
|
+
async function getProjectHashFromCwd() {
|
|
1637
|
+
try {
|
|
1638
|
+
const packageJsonPath = path.join(process.cwd(), "package.json");
|
|
1639
|
+
const content = await fs.readFile(packageJsonPath, "utf-8");
|
|
1640
|
+
const pkg = JSON.parse(content);
|
|
1641
|
+
return pkg.shortProjectHash ?? null;
|
|
1642
|
+
} catch {
|
|
1643
|
+
return null;
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1246
1646
|
async function findOrCreateProjectDir(projectHash, projectName, explicitDir) {
|
|
1247
1647
|
if (explicitDir) {
|
|
1248
1648
|
const resolved = path.resolve(explicitDir);
|
|
@@ -1253,14 +1653,17 @@ async function findOrCreateProjectDir(projectHash, projectName, explicitDir) {
|
|
|
1253
1653
|
const existing = await findExistingProjectDir(cwd, projectHash);
|
|
1254
1654
|
if (existing) return existing;
|
|
1255
1655
|
if (!projectName) throw new Error("Project name is required when creating a new workspace. Pass --name <project name>.");
|
|
1256
|
-
const dirName =
|
|
1257
|
-
const
|
|
1656
|
+
const dirName = toDirName(projectName);
|
|
1657
|
+
const pkgName = toPackageName(projectName);
|
|
1658
|
+
const shortId = shortProjectHash(projectHash);
|
|
1659
|
+
const projectDir = path.join(cwd, dirName || shortId);
|
|
1258
1660
|
await fs.mkdir(path.join(projectDir, "files"), { recursive: true });
|
|
1259
1661
|
const pkg = {
|
|
1260
|
-
name:
|
|
1662
|
+
name: pkgName || shortId,
|
|
1261
1663
|
version: "1.0.0",
|
|
1262
1664
|
private: true,
|
|
1263
|
-
|
|
1665
|
+
shortProjectHash: shortId,
|
|
1666
|
+
framerProjectHash: projectHash,
|
|
1264
1667
|
framerProjectName: projectName
|
|
1265
1668
|
};
|
|
1266
1669
|
await fs.writeFile(path.join(projectDir, "package.json"), JSON.stringify(pkg, null, 2));
|
|
@@ -1281,7 +1684,8 @@ async function matchesProject(packageJsonPath, projectHash) {
|
|
|
1281
1684
|
try {
|
|
1282
1685
|
const content = await fs.readFile(packageJsonPath, "utf-8");
|
|
1283
1686
|
const pkg = JSON.parse(content);
|
|
1284
|
-
|
|
1687
|
+
const inputShort = shortProjectHash(projectHash);
|
|
1688
|
+
return pkg.shortProjectHash === inputShort;
|
|
1285
1689
|
} catch {
|
|
1286
1690
|
return false;
|
|
1287
1691
|
}
|
|
@@ -1329,7 +1733,7 @@ function transition(state, event) {
|
|
|
1329
1733
|
};
|
|
1330
1734
|
}
|
|
1331
1735
|
case "FILE_SYNCED": {
|
|
1332
|
-
effects.push(log("
|
|
1736
|
+
effects.push(log("debug", `Remote confirmed sync: ${event.fileName}`), {
|
|
1333
1737
|
type: "UPDATE_FILE_METADATA",
|
|
1334
1738
|
fileName: event.fileName,
|
|
1335
1739
|
remoteModifiedAt: event.remoteModifiedAt
|
|
@@ -1340,7 +1744,7 @@ function transition(state, event) {
|
|
|
1340
1744
|
};
|
|
1341
1745
|
}
|
|
1342
1746
|
case "DISCONNECT": {
|
|
1343
|
-
effects.push({ type: "PERSIST_STATE" }, log("
|
|
1747
|
+
effects.push({ type: "PERSIST_STATE" }, log("debug", "Disconnected, persisting state"));
|
|
1344
1748
|
if (state.mode === "conflict_resolution") {
|
|
1345
1749
|
const { pendingConflicts: _discarded,...rest } = state;
|
|
1346
1750
|
return {
|
|
@@ -1369,7 +1773,7 @@ function transition(state, event) {
|
|
|
1369
1773
|
effects
|
|
1370
1774
|
};
|
|
1371
1775
|
}
|
|
1372
|
-
effects.push(log("
|
|
1776
|
+
effects.push(log("debug", "Plugin requested file list"), { type: "LIST_LOCAL_FILES" });
|
|
1373
1777
|
return {
|
|
1374
1778
|
state,
|
|
1375
1779
|
effects
|
|
@@ -1383,7 +1787,7 @@ function transition(state, event) {
|
|
|
1383
1787
|
effects
|
|
1384
1788
|
};
|
|
1385
1789
|
}
|
|
1386
|
-
effects.push(log("
|
|
1790
|
+
effects.push(log("debug", `Received file list: ${event.files.length} files`));
|
|
1387
1791
|
effects.push({
|
|
1388
1792
|
type: "DETECT_CONFLICTS",
|
|
1389
1793
|
remoteFiles: event.files
|
|
@@ -1406,12 +1810,13 @@ function transition(state, event) {
|
|
|
1406
1810
|
};
|
|
1407
1811
|
}
|
|
1408
1812
|
const { conflicts, safeWrites, localOnly } = event;
|
|
1409
|
-
if (safeWrites.length > 0) effects.push(log("
|
|
1813
|
+
if (safeWrites.length > 0) effects.push(log("debug", `Applying ${safeWrites.length} safe writes`), {
|
|
1410
1814
|
type: "WRITE_FILES",
|
|
1411
|
-
files: safeWrites
|
|
1815
|
+
files: safeWrites,
|
|
1816
|
+
silent: true
|
|
1412
1817
|
});
|
|
1413
1818
|
if (localOnly.length > 0) {
|
|
1414
|
-
effects.push(log("
|
|
1819
|
+
effects.push(log("debug", `Uploading ${localOnly.length} local-only files`));
|
|
1415
1820
|
for (const file of localOnly) effects.push({
|
|
1416
1821
|
type: "SEND_MESSAGE",
|
|
1417
1822
|
payload: {
|
|
@@ -1422,7 +1827,7 @@ function transition(state, event) {
|
|
|
1422
1827
|
});
|
|
1423
1828
|
}
|
|
1424
1829
|
if (conflicts.length > 0) {
|
|
1425
|
-
effects.push(log("
|
|
1830
|
+
effects.push(log("debug", `${pluralize(conflicts.length, "conflict")} require version check`), {
|
|
1426
1831
|
type: "REQUEST_CONFLICT_VERSIONS",
|
|
1427
1832
|
conflicts
|
|
1428
1833
|
});
|
|
@@ -1435,7 +1840,17 @@ function transition(state, event) {
|
|
|
1435
1840
|
effects
|
|
1436
1841
|
};
|
|
1437
1842
|
}
|
|
1438
|
-
|
|
1843
|
+
const totalSynced = safeWrites.length + localOnly.length;
|
|
1844
|
+
const remoteTotal = state.queuedDiffs.length;
|
|
1845
|
+
const totalCount = remoteTotal + localOnly.length;
|
|
1846
|
+
const updatedCount = safeWrites.length + localOnly.length;
|
|
1847
|
+
const unchangedCount = Math.max(0, remoteTotal - safeWrites.length);
|
|
1848
|
+
effects.push({ type: "PERSIST_STATE" }, {
|
|
1849
|
+
type: "SYNC_COMPLETE",
|
|
1850
|
+
totalCount,
|
|
1851
|
+
updatedCount,
|
|
1852
|
+
unchangedCount
|
|
1853
|
+
});
|
|
1439
1854
|
return {
|
|
1440
1855
|
state: {
|
|
1441
1856
|
...state,
|
|
@@ -1464,7 +1879,7 @@ function transition(state, event) {
|
|
|
1464
1879
|
effects
|
|
1465
1880
|
};
|
|
1466
1881
|
}
|
|
1467
|
-
effects.push(log("
|
|
1882
|
+
effects.push(log("debug", `Applying remote change: ${event.file.name}`), {
|
|
1468
1883
|
type: "WRITE_FILES",
|
|
1469
1884
|
files: [event.file]
|
|
1470
1885
|
});
|
|
@@ -1481,7 +1896,7 @@ function transition(state, event) {
|
|
|
1481
1896
|
effects
|
|
1482
1897
|
};
|
|
1483
1898
|
}
|
|
1484
|
-
effects.push(log("
|
|
1899
|
+
effects.push(log("debug", `Remote delete applied: ${event.fileName}`), {
|
|
1485
1900
|
type: "DELETE_LOCAL_FILES",
|
|
1486
1901
|
names: [event.fileName]
|
|
1487
1902
|
}, { type: "PERSIST_STATE" });
|
|
@@ -1491,7 +1906,7 @@ function transition(state, event) {
|
|
|
1491
1906
|
};
|
|
1492
1907
|
}
|
|
1493
1908
|
case "REMOTE_DELETE_CONFIRMED": {
|
|
1494
|
-
effects.push(log("
|
|
1909
|
+
effects.push(log("debug", `Delete confirmed: ${event.fileName}`), {
|
|
1495
1910
|
type: "DELETE_LOCAL_FILES",
|
|
1496
1911
|
names: [event.fileName]
|
|
1497
1912
|
}, { type: "PERSIST_STATE" });
|
|
@@ -1501,7 +1916,7 @@ function transition(state, event) {
|
|
|
1501
1916
|
};
|
|
1502
1917
|
}
|
|
1503
1918
|
case "REMOTE_DELETE_CANCELLED": {
|
|
1504
|
-
effects.push(log("
|
|
1919
|
+
effects.push(log("debug", `Delete cancelled: ${event.fileName}`));
|
|
1505
1920
|
effects.push({
|
|
1506
1921
|
type: "WRITE_FILES",
|
|
1507
1922
|
files: [{
|
|
@@ -1524,18 +1939,28 @@ function transition(state, event) {
|
|
|
1524
1939
|
};
|
|
1525
1940
|
}
|
|
1526
1941
|
if (event.resolution === "remote") {
|
|
1527
|
-
const
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
effects.push(log("info", `Applying ${remoteFiles.length} remote versions`), {
|
|
1942
|
+
for (const conflict of state.pendingConflicts) if (conflict.remoteContent === null) effects.push({
|
|
1943
|
+
type: "DELETE_LOCAL_FILES",
|
|
1944
|
+
names: [conflict.fileName]
|
|
1945
|
+
});
|
|
1946
|
+
else effects.push({
|
|
1533
1947
|
type: "WRITE_FILES",
|
|
1534
|
-
files:
|
|
1948
|
+
files: [{
|
|
1949
|
+
name: conflict.fileName,
|
|
1950
|
+
content: conflict.remoteContent,
|
|
1951
|
+
modifiedAt: conflict.remoteModifiedAt
|
|
1952
|
+
}]
|
|
1535
1953
|
});
|
|
1954
|
+
effects.push(log("debug", `Applied ${state.pendingConflicts.length} remote versions`));
|
|
1536
1955
|
} else {
|
|
1537
|
-
|
|
1538
|
-
|
|
1956
|
+
for (const conflict of state.pendingConflicts) if (conflict.localContent === null) effects.push({
|
|
1957
|
+
type: "SEND_MESSAGE",
|
|
1958
|
+
payload: {
|
|
1959
|
+
type: "file-delete",
|
|
1960
|
+
fileNames: [conflict.fileName]
|
|
1961
|
+
}
|
|
1962
|
+
});
|
|
1963
|
+
else effects.push({
|
|
1539
1964
|
type: "SEND_MESSAGE",
|
|
1540
1965
|
payload: {
|
|
1541
1966
|
type: "file-change",
|
|
@@ -1543,8 +1968,14 @@ function transition(state, event) {
|
|
|
1543
1968
|
content: conflict.localContent
|
|
1544
1969
|
}
|
|
1545
1970
|
});
|
|
1971
|
+
effects.push(log("debug", `Applied ${state.pendingConflicts.length} local versions`));
|
|
1546
1972
|
}
|
|
1547
|
-
effects.push(
|
|
1973
|
+
effects.push({ type: "PERSIST_STATE" }, {
|
|
1974
|
+
type: "SYNC_COMPLETE",
|
|
1975
|
+
totalCount: state.pendingConflicts.length,
|
|
1976
|
+
updatedCount: state.pendingConflicts.length,
|
|
1977
|
+
unchangedCount: 0
|
|
1978
|
+
});
|
|
1548
1979
|
const { pendingConflicts: _discarded,...rest } = state;
|
|
1549
1980
|
return {
|
|
1550
1981
|
state: {
|
|
@@ -1573,7 +2004,6 @@ function transition(state, event) {
|
|
|
1573
2004
|
effects
|
|
1574
2005
|
};
|
|
1575
2006
|
}
|
|
1576
|
-
effects.push(log("info", `Local change detected: ${relativePath}`));
|
|
1577
2007
|
effects.push({
|
|
1578
2008
|
type: "SEND_LOCAL_CHANGE",
|
|
1579
2009
|
fileName: relativePath,
|
|
@@ -1582,7 +2012,7 @@ function transition(state, event) {
|
|
|
1582
2012
|
break;
|
|
1583
2013
|
}
|
|
1584
2014
|
case "delete": {
|
|
1585
|
-
effects.push(log("
|
|
2015
|
+
effects.push(log("debug", `Local delete detected: ${relativePath}`), {
|
|
1586
2016
|
type: "REQUEST_LOCAL_DELETE_DECISION",
|
|
1587
2017
|
fileName: relativePath,
|
|
1588
2018
|
requireConfirmation: true
|
|
@@ -1605,23 +2035,37 @@ function transition(state, event) {
|
|
|
1605
2035
|
}
|
|
1606
2036
|
const { autoResolvedLocal, autoResolvedRemote, remainingConflicts } = autoResolveConflicts(state.pendingConflicts, event.versions);
|
|
1607
2037
|
if (autoResolvedLocal.length > 0) {
|
|
1608
|
-
effects.push(log("
|
|
1609
|
-
for (const conflict of autoResolvedLocal) effects.push({
|
|
2038
|
+
effects.push(log("debug", `Auto-resolved ${autoResolvedLocal.length} local changes`));
|
|
2039
|
+
for (const conflict of autoResolvedLocal) if (conflict.localContent === null) effects.push({
|
|
2040
|
+
type: "SEND_MESSAGE",
|
|
2041
|
+
payload: {
|
|
2042
|
+
type: "file-delete",
|
|
2043
|
+
fileNames: [conflict.fileName]
|
|
2044
|
+
}
|
|
2045
|
+
});
|
|
2046
|
+
else effects.push({
|
|
1610
2047
|
type: "SEND_LOCAL_CHANGE",
|
|
1611
2048
|
fileName: conflict.fileName,
|
|
1612
2049
|
content: conflict.localContent
|
|
1613
2050
|
});
|
|
1614
2051
|
}
|
|
1615
|
-
if (autoResolvedRemote.length > 0)
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
2052
|
+
if (autoResolvedRemote.length > 0) {
|
|
2053
|
+
effects.push(log("debug", `Auto-resolved ${autoResolvedRemote.length} remote changes`));
|
|
2054
|
+
for (const conflict of autoResolvedRemote) if (conflict.remoteContent === null) effects.push({
|
|
2055
|
+
type: "DELETE_LOCAL_FILES",
|
|
2056
|
+
names: [conflict.fileName]
|
|
2057
|
+
});
|
|
2058
|
+
else effects.push({
|
|
2059
|
+
type: "WRITE_FILES",
|
|
2060
|
+
files: [{
|
|
2061
|
+
name: conflict.fileName,
|
|
2062
|
+
content: conflict.remoteContent,
|
|
2063
|
+
modifiedAt: conflict.remoteModifiedAt ?? Date.now()
|
|
2064
|
+
}]
|
|
2065
|
+
});
|
|
2066
|
+
}
|
|
1623
2067
|
if (remainingConflicts.length > 0) {
|
|
1624
|
-
effects.push(log("warn",
|
|
2068
|
+
effects.push(log("warn", `${pluralize(remainingConflicts.length, "conflict")} require resolution`), {
|
|
1625
2069
|
type: "REQUEST_CONFLICT_DECISIONS",
|
|
1626
2070
|
conflicts: remainingConflicts
|
|
1627
2071
|
});
|
|
@@ -1633,7 +2077,13 @@ function transition(state, event) {
|
|
|
1633
2077
|
effects
|
|
1634
2078
|
};
|
|
1635
2079
|
}
|
|
1636
|
-
|
|
2080
|
+
const resolvedCount = autoResolvedLocal.length + autoResolvedRemote.length;
|
|
2081
|
+
effects.push({ type: "PERSIST_STATE" }, {
|
|
2082
|
+
type: "SYNC_COMPLETE",
|
|
2083
|
+
totalCount: resolvedCount,
|
|
2084
|
+
updatedCount: resolvedCount,
|
|
2085
|
+
unchangedCount: 0
|
|
2086
|
+
});
|
|
1637
2087
|
const { pendingConflicts: _discarded,...rest } = state;
|
|
1638
2088
|
return {
|
|
1639
2089
|
state: {
|
|
@@ -1665,7 +2115,7 @@ async function executeEffect(effect, context) {
|
|
|
1665
2115
|
const projectName = config.explicitName ?? effect.projectInfo.projectName;
|
|
1666
2116
|
config.projectDir = await findOrCreateProjectDir(config.projectHash, projectName, config.explicitDir);
|
|
1667
2117
|
config.filesDir = `${config.projectDir}/files`;
|
|
1668
|
-
|
|
2118
|
+
debug(`Files directory: ${config.filesDir}`);
|
|
1669
2119
|
await fs.mkdir(config.filesDir, { recursive: true });
|
|
1670
2120
|
}
|
|
1671
2121
|
return [];
|
|
@@ -1673,7 +2123,7 @@ async function executeEffect(effect, context) {
|
|
|
1673
2123
|
case "LOAD_PERSISTED_STATE": {
|
|
1674
2124
|
if (config.projectDir) {
|
|
1675
2125
|
await fileMetadataCache.initialize(config.projectDir);
|
|
1676
|
-
|
|
2126
|
+
debug(`Loaded persisted metadata for ${fileMetadataCache.size()} files`);
|
|
1677
2127
|
}
|
|
1678
2128
|
return [];
|
|
1679
2129
|
}
|
|
@@ -1697,13 +2147,17 @@ async function executeEffect(effect, context) {
|
|
|
1697
2147
|
}];
|
|
1698
2148
|
}
|
|
1699
2149
|
case "SEND_MESSAGE": {
|
|
1700
|
-
if (syncState.socket)
|
|
2150
|
+
if (syncState.socket) {
|
|
2151
|
+
const sent = await sendMessage(syncState.socket, effect.payload);
|
|
2152
|
+
if (!sent) warn(`Failed to send message: ${effect.payload.type}`);
|
|
2153
|
+
} else warn(`No socket available to send: ${effect.payload.type}`);
|
|
1701
2154
|
return [];
|
|
1702
2155
|
}
|
|
1703
2156
|
case "WRITE_FILES": {
|
|
1704
2157
|
if (config.filesDir) {
|
|
1705
2158
|
await writeRemoteFiles(effect.files, config.filesDir, hashTracker, installer ?? void 0);
|
|
1706
2159
|
for (const file of effect.files) {
|
|
2160
|
+
if (!effect.silent) fileDown(file.name);
|
|
1707
2161
|
const remoteTimestamp = file.modifiedAt ?? Date.now();
|
|
1708
2162
|
fileMetadataCache.recordRemoteWrite(file.name, file.content, remoteTimestamp);
|
|
1709
2163
|
}
|
|
@@ -1713,6 +2167,7 @@ async function executeEffect(effect, context) {
|
|
|
1713
2167
|
case "DELETE_LOCAL_FILES": {
|
|
1714
2168
|
if (config.filesDir) for (const fileName of effect.names) {
|
|
1715
2169
|
await deleteLocalFile(fileName, config.filesDir, hashTracker);
|
|
2170
|
+
fileDelete(fileName);
|
|
1716
2171
|
fileMetadataCache.recordDelete(fileName);
|
|
1717
2172
|
}
|
|
1718
2173
|
return [];
|
|
@@ -1734,7 +2189,7 @@ async function executeEffect(effect, context) {
|
|
|
1734
2189
|
lastSyncedAt: conflict.lastSyncedAt ?? persisted?.timestamp
|
|
1735
2190
|
};
|
|
1736
2191
|
});
|
|
1737
|
-
|
|
2192
|
+
debug(`Requesting remote version data for ${pluralize(versionRequests.length, "file")}`);
|
|
1738
2193
|
await sendMessage(syncState.socket, {
|
|
1739
2194
|
type: "conflict-version-request",
|
|
1740
2195
|
conflicts: versionRequests
|
|
@@ -1759,17 +2214,27 @@ async function executeEffect(effect, context) {
|
|
|
1759
2214
|
return [];
|
|
1760
2215
|
}
|
|
1761
2216
|
case "SEND_LOCAL_CHANGE": {
|
|
2217
|
+
const contentHash = hashFileContent(effect.content);
|
|
2218
|
+
const metadata = fileMetadataCache.get(effect.fileName);
|
|
2219
|
+
if (metadata?.lastSyncedHash === contentHash) {
|
|
2220
|
+
debug(`Skipping local change for ${effect.fileName}: matches last synced content`);
|
|
2221
|
+
return [];
|
|
2222
|
+
}
|
|
1762
2223
|
if (hashTracker.shouldSkip(effect.fileName, effect.content)) return [];
|
|
2224
|
+
debug(`Local change detected: ${effect.fileName}`);
|
|
1763
2225
|
try {
|
|
1764
|
-
if (syncState.socket)
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
2226
|
+
if (syncState.socket) {
|
|
2227
|
+
await sendMessage(syncState.socket, {
|
|
2228
|
+
type: "file-change",
|
|
2229
|
+
fileName: effect.fileName,
|
|
2230
|
+
content: effect.content
|
|
2231
|
+
});
|
|
2232
|
+
fileUp(effect.fileName);
|
|
2233
|
+
}
|
|
1769
2234
|
hashTracker.remember(effect.fileName, effect.content);
|
|
1770
2235
|
if (installer) installer.process(effect.fileName, effect.content);
|
|
1771
2236
|
} catch (err) {
|
|
1772
|
-
|
|
2237
|
+
warn(`Failed to push ${effect.fileName}`);
|
|
1773
2238
|
}
|
|
1774
2239
|
return [];
|
|
1775
2240
|
}
|
|
@@ -1801,6 +2266,20 @@ async function executeEffect(effect, context) {
|
|
|
1801
2266
|
await fileMetadataCache.flush();
|
|
1802
2267
|
return [];
|
|
1803
2268
|
}
|
|
2269
|
+
case "SYNC_COMPLETE": {
|
|
2270
|
+
const wasDisconnected = wasRecentlyDisconnected();
|
|
2271
|
+
if (wasDisconnected) {
|
|
2272
|
+
if (didShowDisconnect()) {
|
|
2273
|
+
success(`Reconnected, synced ${effect.totalCount} files (${effect.updatedCount} updated, ${effect.unchangedCount} unchanged)`);
|
|
2274
|
+
status("Watching for changes...");
|
|
2275
|
+
}
|
|
2276
|
+
resetDisconnectState();
|
|
2277
|
+
return [];
|
|
2278
|
+
}
|
|
2279
|
+
success(`Synced ${effect.totalCount} files (${effect.updatedCount} updated, ${effect.unchangedCount} unchanged)`);
|
|
2280
|
+
status("Watching for changes...");
|
|
2281
|
+
return [];
|
|
2282
|
+
}
|
|
1804
2283
|
case "LOG": {
|
|
1805
2284
|
const logFn = effect.level === "info" ? info : effect.level === "warn" ? warn : debug;
|
|
1806
2285
|
logFn(effect.message);
|
|
@@ -1812,9 +2291,7 @@ async function executeEffect(effect, context) {
|
|
|
1812
2291
|
* Starts the sync controller with the given configuration
|
|
1813
2292
|
*/
|
|
1814
2293
|
async function start(config) {
|
|
1815
|
-
|
|
1816
|
-
info(`Project: ${config.projectHash}`);
|
|
1817
|
-
info(`Port: ${config.port} (auto-selected from project hash)`);
|
|
2294
|
+
status("Waiting for Plugin connection...");
|
|
1818
2295
|
const hashTracker = createHashTracker();
|
|
1819
2296
|
const fileMetadataCache = new FileMetadataCache();
|
|
1820
2297
|
let installer = null;
|
|
@@ -1827,9 +2304,14 @@ async function start(config) {
|
|
|
1827
2304
|
};
|
|
1828
2305
|
const userActions = new UserActionCoordinator();
|
|
1829
2306
|
async function processEvent(event) {
|
|
2307
|
+
const socketState = syncState.socket?.readyState;
|
|
2308
|
+
debug(`[STATE] Processing event: ${event.type} (mode: ${syncState.mode}, socket: ${socketState ?? "none"})`);
|
|
1830
2309
|
const result = transition(syncState, event);
|
|
1831
2310
|
syncState = result.state;
|
|
2311
|
+
if (result.effects.length > 0) debug(`[STATE] Event produced ${result.effects.length} effects: ${result.effects.map((e) => e.type).join(", ")}`);
|
|
1832
2312
|
for (const effect of result.effects) {
|
|
2313
|
+
const currentSocketState = syncState.socket?.readyState;
|
|
2314
|
+
if (currentSocketState !== void 0 && currentSocketState !== 1) debug(`[STATE] Socket not open (state: ${currentSocketState}) before executing ${effect.type}`);
|
|
1833
2315
|
const followUpEvents = await executeEffect(effect, {
|
|
1834
2316
|
config,
|
|
1835
2317
|
hashTracker,
|
|
@@ -1841,12 +2323,13 @@ async function start(config) {
|
|
|
1841
2323
|
for (const followUpEvent of followUpEvents) await processEvent(followUpEvent);
|
|
1842
2324
|
}
|
|
1843
2325
|
}
|
|
1844
|
-
const connection = initConnection(config.port);
|
|
2326
|
+
const connection = await initConnection(config.port);
|
|
1845
2327
|
connection.on("handshake", async (client, message) => {
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
2328
|
+
debug(`Received handshake: ${message.projectName} (${message.projectId})`);
|
|
2329
|
+
const expectedShort = shortProjectHash(config.projectHash);
|
|
2330
|
+
const receivedShort = shortProjectHash(message.projectId);
|
|
2331
|
+
if (receivedShort !== expectedShort) {
|
|
2332
|
+
warn(`Project ID mismatch: expected ${expectedShort}, got ${receivedShort}`);
|
|
1850
2333
|
client.close();
|
|
1851
2334
|
return;
|
|
1852
2335
|
}
|
|
@@ -1863,7 +2346,9 @@ async function start(config) {
|
|
|
1863
2346
|
await installer.initialize();
|
|
1864
2347
|
startWatcher();
|
|
1865
2348
|
}
|
|
1866
|
-
|
|
2349
|
+
cancelDisconnectMessage();
|
|
2350
|
+
const wasDisconnected = wasRecentlyDisconnected();
|
|
2351
|
+
if (!wasDisconnected && !didShowDisconnect()) success(`Connected to ${message.projectName}`);
|
|
1867
2352
|
});
|
|
1868
2353
|
async function handleMessage(message) {
|
|
1869
2354
|
if (!config.projectDir || !installer) {
|
|
@@ -1875,12 +2360,15 @@ async function start(config) {
|
|
|
1875
2360
|
case "request-files":
|
|
1876
2361
|
event = { type: "REQUEST_FILES" };
|
|
1877
2362
|
break;
|
|
1878
|
-
case "file-list":
|
|
2363
|
+
case "file-list": {
|
|
2364
|
+
const totalSize = message.files.reduce((sum, f) => sum + (f.content?.length ?? 0), 0);
|
|
2365
|
+
debug(`Received file list: ${message.files.length} files (${(totalSize / 1024).toFixed(1)}KB)`);
|
|
1879
2366
|
event = {
|
|
1880
2367
|
type: "FILE_LIST",
|
|
1881
2368
|
files: message.files
|
|
1882
2369
|
};
|
|
1883
2370
|
break;
|
|
2371
|
+
}
|
|
1884
2372
|
case "file-change":
|
|
1885
2373
|
event = {
|
|
1886
2374
|
type: "FILE_CHANGE",
|
|
@@ -1955,10 +2443,11 @@ async function start(config) {
|
|
|
1955
2443
|
}
|
|
1956
2444
|
});
|
|
1957
2445
|
connection.on("disconnect", async () => {
|
|
1958
|
-
|
|
2446
|
+
scheduleDisconnectMessage(() => {
|
|
2447
|
+
status("Disconnected, waiting to reconnect...");
|
|
2448
|
+
});
|
|
1959
2449
|
await processEvent({ type: "DISCONNECT" });
|
|
1960
2450
|
userActions.cleanup();
|
|
1961
|
-
info("Will perform full diff on reconnect");
|
|
1962
2451
|
});
|
|
1963
2452
|
connection.on("error", (err) => {
|
|
1964
2453
|
error("Error on WebSocket connection:", err);
|
|
@@ -1974,10 +2463,9 @@ async function start(config) {
|
|
|
1974
2463
|
});
|
|
1975
2464
|
});
|
|
1976
2465
|
};
|
|
1977
|
-
info("✓ Controller initialized and ready");
|
|
1978
|
-
info(`Waiting for plugin connection on port ${config.port}...`);
|
|
1979
2466
|
process.on("SIGINT", async () => {
|
|
1980
|
-
|
|
2467
|
+
console.log();
|
|
2468
|
+
status("Shutting down...");
|
|
1981
2469
|
if (watcher) await watcher.close();
|
|
1982
2470
|
connection.close();
|
|
1983
2471
|
process.exit(0);
|
|
@@ -1994,7 +2482,16 @@ program.exitOverride((err) => {
|
|
|
1994
2482
|
}
|
|
1995
2483
|
throw err;
|
|
1996
2484
|
});
|
|
1997
|
-
program.name("code-link").description("Sync Framer code components to your local filesystem").version("0.1.0").argument("
|
|
2485
|
+
program.name("code-link").description("Sync Framer code components to your local filesystem").version("0.1.0").argument("[projectHash]", "Framer Project ID Hash (auto-detected from package.json if omitted)").option("-n, --name <name>", "Project name (optional)").option("-d, --dir <directory>", "Explicit project directory").option("-v, --verbose", "Enable verbose logging").option("--log-level <level>", "Set log level (debug, info, warn, error)").option("--dangerously-auto-delete", "Automatically delete remote files without confirmation").action(async (projectHash, options) => {
|
|
2486
|
+
if (!projectHash) {
|
|
2487
|
+
const detected = await getProjectHashFromCwd();
|
|
2488
|
+
if (detected) projectHash = detected;
|
|
2489
|
+
else {
|
|
2490
|
+
console.error("No project ID provided and no framerProjectId found in package.json.");
|
|
2491
|
+
console.error("Either run this command from a project directory or copy the command from the Code Link Plugin.");
|
|
2492
|
+
process.exit(1);
|
|
2493
|
+
}
|
|
2494
|
+
}
|
|
1998
2495
|
const isDev = process.env.NODE_ENV === "development";
|
|
1999
2496
|
if (options.logLevel) {
|
|
2000
2497
|
const levelMap = {
|
|
@@ -2007,6 +2504,7 @@ program.name("code-link").description("Sync Framer code components to your local
|
|
|
2007
2504
|
if (level !== void 0) setLogLevel(level);
|
|
2008
2505
|
} else if (options.verbose || isDev) setLogLevel(LogLevel.DEBUG);
|
|
2009
2506
|
const port = getPortFromHash(projectHash);
|
|
2507
|
+
banner("0.1.3", port);
|
|
2010
2508
|
const config = {
|
|
2011
2509
|
port,
|
|
2012
2510
|
projectHash,
|
|
@@ -2016,8 +2514,12 @@ program.name("code-link").description("Sync Framer code components to your local
|
|
|
2016
2514
|
explicitDir: options.dir,
|
|
2017
2515
|
explicitName: options.name
|
|
2018
2516
|
};
|
|
2019
|
-
if (config.dangerouslyAutoDelete)
|
|
2020
|
-
|
|
2517
|
+
if (config.dangerouslyAutoDelete) warn("Auto-delete mode enabled - files will be deleted without confirmation");
|
|
2518
|
+
try {
|
|
2519
|
+
await start(config);
|
|
2520
|
+
} catch (err) {
|
|
2521
|
+
process.exit(1);
|
|
2522
|
+
}
|
|
2021
2523
|
});
|
|
2022
2524
|
program.parse();
|
|
2023
2525
|
|