framer-code-link 0.1.3 → 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 +551 -144
- package/package.json +12 -12
- package/src/controller.test.ts +22 -137
- package/src/controller.ts +242 -106
- package/src/helpers/connection.ts +10 -10
- 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 +7 -4
- 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,23 +115,168 @@ 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
|
|
@@ -73,27 +312,27 @@ function initConnection(port) {
|
|
|
73
312
|
});
|
|
74
313
|
wss.on("listening", () => {
|
|
75
314
|
isReady = true;
|
|
76
|
-
|
|
315
|
+
debug(`WebSocket server listening on port ${port}`);
|
|
77
316
|
wss.on("connection", (ws) => {
|
|
78
317
|
const connId = ++connectionId;
|
|
79
|
-
|
|
318
|
+
debug(`Client connected (conn ${connId})`);
|
|
80
319
|
ws.on("message", (data) => {
|
|
81
320
|
try {
|
|
82
321
|
const message = JSON.parse(data.toString());
|
|
83
322
|
if (message.type === "handshake") {
|
|
84
|
-
|
|
323
|
+
debug(`Received handshake (conn ${connId})`);
|
|
85
324
|
handlers.onHandshake?.(ws, message);
|
|
86
325
|
} else handlers.onMessage?.(message);
|
|
87
326
|
} catch (err) {
|
|
88
|
-
error(`
|
|
327
|
+
error(`Failed to parse message:`, err);
|
|
89
328
|
}
|
|
90
329
|
});
|
|
91
330
|
ws.on("close", (code, reason) => {
|
|
92
|
-
|
|
331
|
+
debug(`Client disconnected (code: ${code}, reason: ${reason?.toString() || "none"})`);
|
|
93
332
|
handlers.onDisconnect?.();
|
|
94
333
|
});
|
|
95
334
|
ws.on("error", (err) => {
|
|
96
|
-
error(`
|
|
335
|
+
error(`WebSocket error:`, err);
|
|
97
336
|
});
|
|
98
337
|
});
|
|
99
338
|
resolve({
|
|
@@ -136,19 +375,75 @@ function sendMessage(socket, message) {
|
|
|
136
375
|
return new Promise((resolve) => {
|
|
137
376
|
if (socket.readyState !== READY_STATE.OPEN) {
|
|
138
377
|
const stateStr = readyStateToString(socket.readyState);
|
|
139
|
-
|
|
378
|
+
debug(`Cannot send ${message.type}: socket is ${stateStr}`);
|
|
140
379
|
resolve(false);
|
|
141
380
|
return;
|
|
142
381
|
}
|
|
143
382
|
socket.send(JSON.stringify(message), (err) => {
|
|
144
383
|
if (err) {
|
|
145
|
-
|
|
384
|
+
debug(`Send error for ${message.type}: ${err.message}`);
|
|
146
385
|
resolve(false);
|
|
147
386
|
} else resolve(true);
|
|
148
387
|
});
|
|
149
388
|
});
|
|
150
389
|
}
|
|
151
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
|
+
|
|
152
447
|
//#endregion
|
|
153
448
|
//#region ../shared/dist/paths.js
|
|
154
449
|
/**
|
|
@@ -252,6 +547,16 @@ function sanitizeFilePath(input, capitalizeReactComponent = true) {
|
|
|
252
547
|
function isSupportedExtension$1(filePath) {
|
|
253
548
|
return /\.(tsx?|jsx?|json)$/i.test(filePath);
|
|
254
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
|
+
}
|
|
255
560
|
|
|
256
561
|
//#endregion
|
|
257
562
|
//#region src/utils/paths.ts
|
|
@@ -297,7 +602,7 @@ function initWatcher(filesDir) {
|
|
|
297
602
|
persistent: true,
|
|
298
603
|
ignoreInitial: false
|
|
299
604
|
});
|
|
300
|
-
|
|
605
|
+
debug(`Watching directory: ${filesDir}`);
|
|
301
606
|
const emitEvent = async (kind, absolutePath) => {
|
|
302
607
|
if (!isSupportedExtension$1(absolutePath)) return;
|
|
303
608
|
const rawRelativePath = normalizePath(getRelativePath(filesDir, absolutePath));
|
|
@@ -309,10 +614,10 @@ function initWatcher(filesDir) {
|
|
|
309
614
|
try {
|
|
310
615
|
await fs.mkdir(path.dirname(newAbsolutePath), { recursive: true });
|
|
311
616
|
await fs.rename(absolutePath, newAbsolutePath);
|
|
312
|
-
|
|
617
|
+
debug(`Renamed ${rawRelativePath} -> ${relativePath}`);
|
|
313
618
|
effectiveAbsolutePath = newAbsolutePath;
|
|
314
619
|
} catch (err) {
|
|
315
|
-
warn(`Failed to rename ${rawRelativePath}
|
|
620
|
+
warn(`Failed to rename ${rawRelativePath}`, err);
|
|
316
621
|
}
|
|
317
622
|
}
|
|
318
623
|
let content;
|
|
@@ -423,6 +728,10 @@ const SUPPORTED_EXTENSIONS = [
|
|
|
423
728
|
];
|
|
424
729
|
const DEFAULT_EXTENSION = ".tsx";
|
|
425
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
|
+
}
|
|
426
735
|
/**
|
|
427
736
|
* Lists all supported files in the files directory
|
|
428
737
|
*/
|
|
@@ -466,19 +775,34 @@ async function detectConflicts(remoteFiles, filesDir, options = {}) {
|
|
|
466
775
|
const detect = options.detectConflicts ?? true;
|
|
467
776
|
const preferRemote = options.preferRemote ?? false;
|
|
468
777
|
const persistedState = options.persistedState;
|
|
778
|
+
const getPersistedState = (fileName) => persistedState?.get(normalizeForComparison(fileName)) ?? persistedState?.get(fileName);
|
|
469
779
|
debug(`Detecting conflicts for ${remoteFiles.length} remote files`);
|
|
470
780
|
const localFiles = await listFiles(filesDir);
|
|
471
|
-
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
|
+
}));
|
|
472
786
|
const processedFiles = /* @__PURE__ */ new Set();
|
|
473
787
|
for (const remote of remoteFiles) {
|
|
474
788
|
const normalized = resolveRemoteReference(filesDir, remote.name);
|
|
475
|
-
const
|
|
476
|
-
|
|
477
|
-
|
|
789
|
+
const normalizedKey = normalizeForComparison(normalized.relativePath);
|
|
790
|
+
const local = localFileMap.get(normalizedKey);
|
|
791
|
+
processedFiles.add(normalizedKey);
|
|
792
|
+
const persisted = getPersistedState(normalized.relativePath);
|
|
478
793
|
const localHash = local ? hashFileContent(local.content) : null;
|
|
479
794
|
const localMatchesPersisted = !!persisted && !!local && localHash === persisted.contentHash;
|
|
480
795
|
if (!local) {
|
|
481
|
-
|
|
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({
|
|
482
806
|
name: normalized.relativePath,
|
|
483
807
|
content: remote.content,
|
|
484
808
|
modifiedAt: remote.modifiedAt
|
|
@@ -505,11 +829,32 @@ async function detectConflicts(remoteFiles, filesDir, options = {}) {
|
|
|
505
829
|
localClean
|
|
506
830
|
});
|
|
507
831
|
}
|
|
508
|
-
for (const local of localFiles)
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
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
|
+
}
|
|
513
858
|
return {
|
|
514
859
|
conflicts,
|
|
515
860
|
writes,
|
|
@@ -525,30 +870,30 @@ function autoResolveConflicts(conflicts, versions, options = {}) {
|
|
|
525
870
|
for (const conflict of conflicts) {
|
|
526
871
|
const latestRemoteVersionMs = versionMap.get(conflict.fileName);
|
|
527
872
|
const lastSyncedAt = conflict.lastSyncedAt;
|
|
528
|
-
|
|
873
|
+
debug(`Auto-resolve checking ${conflict.fileName}`);
|
|
529
874
|
if (!latestRemoteVersionMs) {
|
|
530
|
-
|
|
875
|
+
debug(` No remote version data, keeping conflict`);
|
|
531
876
|
remainingConflicts.push(conflict);
|
|
532
877
|
continue;
|
|
533
878
|
}
|
|
534
879
|
if (!lastSyncedAt) {
|
|
535
|
-
|
|
880
|
+
debug(` No last sync timestamp, keeping conflict`);
|
|
536
881
|
remainingConflicts.push(conflict);
|
|
537
882
|
continue;
|
|
538
883
|
}
|
|
539
|
-
|
|
540
|
-
|
|
884
|
+
debug(` Remote: ${new Date(latestRemoteVersionMs).toISOString()}`);
|
|
885
|
+
debug(` Synced: ${new Date(lastSyncedAt).toISOString()}`);
|
|
541
886
|
const remoteUnchanged = latestRemoteVersionMs <= lastSyncedAt + remoteDriftMs;
|
|
542
887
|
const localClean = conflict.localClean === true;
|
|
543
888
|
if (remoteUnchanged && !localClean) {
|
|
544
|
-
|
|
889
|
+
debug(` Remote unchanged, local changed -> LOCAL`);
|
|
545
890
|
autoResolvedLocal.push(conflict);
|
|
546
891
|
} else if (localClean && !remoteUnchanged) {
|
|
547
|
-
|
|
892
|
+
debug(` Local unchanged, remote changed -> REMOTE`);
|
|
548
893
|
autoResolvedRemote.push(conflict);
|
|
549
|
-
} else if (remoteUnchanged && localClean)
|
|
894
|
+
} else if (remoteUnchanged && localClean) debug(` Both unchanged, skipping`);
|
|
550
895
|
else {
|
|
551
|
-
|
|
896
|
+
debug(` Both changed, real conflict`);
|
|
552
897
|
remainingConflicts.push(conflict);
|
|
553
898
|
}
|
|
554
899
|
}
|
|
@@ -563,7 +908,7 @@ function autoResolveConflicts(conflicts, versions, options = {}) {
|
|
|
563
908
|
* CRITICAL: Update hashTracker BEFORE writing to disk
|
|
564
909
|
*/
|
|
565
910
|
async function writeRemoteFiles(files, filesDir, hashTracker, installer) {
|
|
566
|
-
|
|
911
|
+
debug(`Writing ${files.length} remote files`);
|
|
567
912
|
for (const file of files) try {
|
|
568
913
|
const normalized = resolveRemoteReference(filesDir, file.name);
|
|
569
914
|
const fullPath = normalized.absolutePath;
|
|
@@ -585,12 +930,12 @@ async function deleteLocalFile(fileName, filesDir, hashTracker) {
|
|
|
585
930
|
hashTracker.markDelete(normalized.relativePath);
|
|
586
931
|
await fs.unlink(normalized.absolutePath);
|
|
587
932
|
hashTracker.forget(normalized.relativePath);
|
|
588
|
-
|
|
933
|
+
debug(`Deleted file: ${normalized.relativePath}`);
|
|
589
934
|
} catch (err) {
|
|
590
935
|
const nodeError = err;
|
|
591
936
|
if (nodeError?.code === "ENOENT") {
|
|
592
937
|
hashTracker.forget(normalized.relativePath);
|
|
593
|
-
|
|
938
|
+
debug(`File already deleted: ${normalized.relativePath}`);
|
|
594
939
|
return;
|
|
595
940
|
}
|
|
596
941
|
hashTracker.clearDelete(normalized.relativePath);
|
|
@@ -730,13 +1075,13 @@ var Installer = class {
|
|
|
730
1075
|
} catch {}
|
|
731
1076
|
if (pkgMatch && !seenPackages.has(pkgMatch[1])) {
|
|
732
1077
|
seenPackages.add(pkgMatch[1]);
|
|
733
|
-
|
|
1078
|
+
debug(`📦 Types: ${pkgMatch[1]}`);
|
|
734
1079
|
}
|
|
735
1080
|
await this.writeTypeFile(receivedPath, code);
|
|
736
1081
|
}
|
|
737
1082
|
}
|
|
738
1083
|
});
|
|
739
|
-
|
|
1084
|
+
debug("Type installer initialized");
|
|
740
1085
|
}
|
|
741
1086
|
/**
|
|
742
1087
|
* Ensure the project scaffolding exists (tsconfig, declarations, etc.)
|
|
@@ -784,7 +1129,7 @@ var Installer = class {
|
|
|
784
1129
|
const hash = imports.map((imp) => imp.name).sort().join(",");
|
|
785
1130
|
if (this.processedImports.has(hash)) return;
|
|
786
1131
|
this.processedImports.add(hash);
|
|
787
|
-
|
|
1132
|
+
debug(`Processing imports for ${fileName} (${imports.length} packages)`);
|
|
788
1133
|
try {
|
|
789
1134
|
await this.ata(content);
|
|
790
1135
|
} catch (err) {
|
|
@@ -879,7 +1224,7 @@ var Installer = class {
|
|
|
879
1224
|
include: ["files/**/*", "framer-modules.d.ts"]
|
|
880
1225
|
};
|
|
881
1226
|
await fs.writeFile(tsconfigPath, JSON.stringify(config, null, 2));
|
|
882
|
-
|
|
1227
|
+
debug("Created tsconfig.json");
|
|
883
1228
|
}
|
|
884
1229
|
}
|
|
885
1230
|
async ensurePrettierConfig() {
|
|
@@ -894,7 +1239,7 @@ var Installer = class {
|
|
|
894
1239
|
trailingComma: "es5"
|
|
895
1240
|
};
|
|
896
1241
|
await fs.writeFile(prettierPath, JSON.stringify(config, null, 2));
|
|
897
|
-
|
|
1242
|
+
debug("Created .prettierrc");
|
|
898
1243
|
}
|
|
899
1244
|
}
|
|
900
1245
|
async ensureFramerDeclarations() {
|
|
@@ -911,7 +1256,7 @@ declare module "https://framerusercontent.com/*"
|
|
|
911
1256
|
declare module "*.json"
|
|
912
1257
|
`;
|
|
913
1258
|
await fs.writeFile(declarationsPath, declarations);
|
|
914
|
-
|
|
1259
|
+
debug("Created framer-modules.d.ts");
|
|
915
1260
|
}
|
|
916
1261
|
}
|
|
917
1262
|
async ensurePackageJson() {
|
|
@@ -927,7 +1272,7 @@ declare module "*.json"
|
|
|
927
1272
|
description: "Framer files synced with framer-code-link"
|
|
928
1273
|
};
|
|
929
1274
|
await fs.writeFile(packagePath, JSON.stringify(pkg, null, 2));
|
|
930
|
-
|
|
1275
|
+
debug("Created package.json");
|
|
931
1276
|
}
|
|
932
1277
|
}
|
|
933
1278
|
async ensureReact18Types() {
|
|
@@ -939,9 +1284,9 @@ declare module "*.json"
|
|
|
939
1284
|
"jsx-runtime.d.ts",
|
|
940
1285
|
"jsx-dev-runtime.d.ts"
|
|
941
1286
|
];
|
|
942
|
-
if (await this.hasTypePackage(reactTypesDir, REACT_TYPES_VERSION, reactFiles))
|
|
1287
|
+
if (await this.hasTypePackage(reactTypesDir, REACT_TYPES_VERSION, reactFiles)) debug("📦 React types (from cache)");
|
|
943
1288
|
else {
|
|
944
|
-
|
|
1289
|
+
debug("Downloading React 18 types...");
|
|
945
1290
|
await this.downloadTypePackage("@types/react", REACT_TYPES_VERSION, reactTypesDir, reactFiles);
|
|
946
1291
|
}
|
|
947
1292
|
const reactDomDir = path.join(this.projectDir, "node_modules/@types/react-dom");
|
|
@@ -950,7 +1295,7 @@ declare module "*.json"
|
|
|
950
1295
|
"index.d.ts",
|
|
951
1296
|
"client.d.ts"
|
|
952
1297
|
];
|
|
953
|
-
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)");
|
|
954
1299
|
else await this.downloadTypePackage("@types/react-dom", REACT_DOM_TYPES_VERSION, reactDomDir, reactDomFiles);
|
|
955
1300
|
}
|
|
956
1301
|
async hasTypePackage(destinationDir, version, files) {
|
|
@@ -1015,7 +1360,7 @@ async function fetchWithRetry(url, init, retries = MAX_FETCH_RETRIES) {
|
|
|
1015
1360
|
}
|
|
1016
1361
|
|
|
1017
1362
|
//#endregion
|
|
1018
|
-
//#region src/utils/
|
|
1363
|
+
//#region src/utils/hash-tracker.ts
|
|
1019
1364
|
/**
|
|
1020
1365
|
* Creates a hash tracker instance for echo prevention
|
|
1021
1366
|
*/
|
|
@@ -1057,26 +1402,11 @@ function createHashTracker() {
|
|
|
1057
1402
|
};
|
|
1058
1403
|
}
|
|
1059
1404
|
/**
|
|
1060
|
-
* Computes a hash of file content for comparison
|
|
1405
|
+
* Computes a SHA256 hash of file content for comparison
|
|
1061
1406
|
*/
|
|
1062
1407
|
function hashContent(content) {
|
|
1063
1408
|
return createHash("sha256").update(content).digest("hex");
|
|
1064
1409
|
}
|
|
1065
|
-
/**
|
|
1066
|
-
* Generate a deterministic port number from a project hash
|
|
1067
|
-
* Port range: 3847-4096 (250 possible ports)
|
|
1068
|
-
* Uses simple hash to match client-side implementation
|
|
1069
|
-
*/
|
|
1070
|
-
function getPortFromHash(projectHash) {
|
|
1071
|
-
let hash = 0;
|
|
1072
|
-
for (let i = 0; i < projectHash.length; i++) {
|
|
1073
|
-
const char = projectHash.charCodeAt(i);
|
|
1074
|
-
hash = (hash << 5) - hash + char;
|
|
1075
|
-
hash = hash & hash;
|
|
1076
|
-
}
|
|
1077
|
-
const portOffset = Math.abs(hash) % 250;
|
|
1078
|
-
return 3847 + portOffset;
|
|
1079
|
-
}
|
|
1080
1410
|
|
|
1081
1411
|
//#endregion
|
|
1082
1412
|
//#region src/utils/file-metadata-cache.ts
|
|
@@ -1183,7 +1513,7 @@ var UserActionCoordinator = class {
|
|
|
1183
1513
|
return await confirmationPromise;
|
|
1184
1514
|
} catch (err) {
|
|
1185
1515
|
if (err instanceof PluginDisconnectedError) {
|
|
1186
|
-
|
|
1516
|
+
debug(`Plugin disconnected while waiting for delete confirmation: ${fileName}`);
|
|
1187
1517
|
return false;
|
|
1188
1518
|
}
|
|
1189
1519
|
throw err;
|
|
@@ -1215,7 +1545,7 @@ var UserActionCoordinator = class {
|
|
|
1215
1545
|
return new Map(results);
|
|
1216
1546
|
} catch (err) {
|
|
1217
1547
|
if (err instanceof PluginDisconnectedError) {
|
|
1218
|
-
|
|
1548
|
+
debug("Plugin disconnected while awaiting conflict decisions");
|
|
1219
1549
|
return /* @__PURE__ */ new Map();
|
|
1220
1550
|
}
|
|
1221
1551
|
throw err;
|
|
@@ -1230,7 +1560,7 @@ var UserActionCoordinator = class {
|
|
|
1230
1560
|
resolve,
|
|
1231
1561
|
reject
|
|
1232
1562
|
});
|
|
1233
|
-
|
|
1563
|
+
debug(`Awaiting ${description}: ${actionId}`);
|
|
1234
1564
|
});
|
|
1235
1565
|
}
|
|
1236
1566
|
/**
|
|
@@ -1239,12 +1569,12 @@ var UserActionCoordinator = class {
|
|
|
1239
1569
|
handleConfirmation(actionId, value) {
|
|
1240
1570
|
const pending = this.pendingActions.get(actionId);
|
|
1241
1571
|
if (!pending) {
|
|
1242
|
-
|
|
1572
|
+
debug(`Unexpected confirmation for ${actionId}`);
|
|
1243
1573
|
return false;
|
|
1244
1574
|
}
|
|
1245
1575
|
this.pendingActions.delete(actionId);
|
|
1246
1576
|
pending.resolve(value);
|
|
1247
|
-
|
|
1577
|
+
debug(`Confirmed: ${actionId}`);
|
|
1248
1578
|
return true;
|
|
1249
1579
|
}
|
|
1250
1580
|
/**
|
|
@@ -1253,7 +1583,7 @@ var UserActionCoordinator = class {
|
|
|
1253
1583
|
cleanup() {
|
|
1254
1584
|
for (const [actionId, pending] of this.pendingActions.entries()) {
|
|
1255
1585
|
pending.reject(new PluginDisconnectedError());
|
|
1256
|
-
|
|
1586
|
+
debug(`Cancelled pending action: ${actionId}`);
|
|
1257
1587
|
}
|
|
1258
1588
|
this.pendingActions.clear();
|
|
1259
1589
|
}
|
|
@@ -1301,14 +1631,14 @@ function toPackageName(name) {
|
|
|
1301
1631
|
return name.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/^-+|-+$/g, "").replace(/-+/g, "-");
|
|
1302
1632
|
}
|
|
1303
1633
|
function toDirName(name) {
|
|
1304
|
-
return name.replace(/[^a-zA-Z0-9-]/g, "-").replace(
|
|
1634
|
+
return name.replace(/[^a-zA-Z0-9- ]/g, "-").replace(/^[-\s]+|[-\s]+$/g, "").replace(/-+/g, "-");
|
|
1305
1635
|
}
|
|
1306
1636
|
async function getProjectHashFromCwd() {
|
|
1307
1637
|
try {
|
|
1308
1638
|
const packageJsonPath = path.join(process.cwd(), "package.json");
|
|
1309
1639
|
const content = await fs.readFile(packageJsonPath, "utf-8");
|
|
1310
1640
|
const pkg = JSON.parse(content);
|
|
1311
|
-
return pkg.
|
|
1641
|
+
return pkg.shortProjectHash ?? null;
|
|
1312
1642
|
} catch {
|
|
1313
1643
|
return null;
|
|
1314
1644
|
}
|
|
@@ -1325,13 +1655,15 @@ async function findOrCreateProjectDir(projectHash, projectName, explicitDir) {
|
|
|
1325
1655
|
if (!projectName) throw new Error("Project name is required when creating a new workspace. Pass --name <project name>.");
|
|
1326
1656
|
const dirName = toDirName(projectName);
|
|
1327
1657
|
const pkgName = toPackageName(projectName);
|
|
1328
|
-
const
|
|
1658
|
+
const shortId = shortProjectHash(projectHash);
|
|
1659
|
+
const projectDir = path.join(cwd, dirName || shortId);
|
|
1329
1660
|
await fs.mkdir(path.join(projectDir, "files"), { recursive: true });
|
|
1330
1661
|
const pkg = {
|
|
1331
|
-
name: pkgName ||
|
|
1662
|
+
name: pkgName || shortId,
|
|
1332
1663
|
version: "1.0.0",
|
|
1333
1664
|
private: true,
|
|
1334
|
-
|
|
1665
|
+
shortProjectHash: shortId,
|
|
1666
|
+
framerProjectHash: projectHash,
|
|
1335
1667
|
framerProjectName: projectName
|
|
1336
1668
|
};
|
|
1337
1669
|
await fs.writeFile(path.join(projectDir, "package.json"), JSON.stringify(pkg, null, 2));
|
|
@@ -1352,7 +1684,8 @@ async function matchesProject(packageJsonPath, projectHash) {
|
|
|
1352
1684
|
try {
|
|
1353
1685
|
const content = await fs.readFile(packageJsonPath, "utf-8");
|
|
1354
1686
|
const pkg = JSON.parse(content);
|
|
1355
|
-
|
|
1687
|
+
const inputShort = shortProjectHash(projectHash);
|
|
1688
|
+
return pkg.shortProjectHash === inputShort;
|
|
1356
1689
|
} catch {
|
|
1357
1690
|
return false;
|
|
1358
1691
|
}
|
|
@@ -1400,7 +1733,7 @@ function transition(state, event) {
|
|
|
1400
1733
|
};
|
|
1401
1734
|
}
|
|
1402
1735
|
case "FILE_SYNCED": {
|
|
1403
|
-
effects.push(log("
|
|
1736
|
+
effects.push(log("debug", `Remote confirmed sync: ${event.fileName}`), {
|
|
1404
1737
|
type: "UPDATE_FILE_METADATA",
|
|
1405
1738
|
fileName: event.fileName,
|
|
1406
1739
|
remoteModifiedAt: event.remoteModifiedAt
|
|
@@ -1411,7 +1744,7 @@ function transition(state, event) {
|
|
|
1411
1744
|
};
|
|
1412
1745
|
}
|
|
1413
1746
|
case "DISCONNECT": {
|
|
1414
|
-
effects.push({ type: "PERSIST_STATE" }, log("
|
|
1747
|
+
effects.push({ type: "PERSIST_STATE" }, log("debug", "Disconnected, persisting state"));
|
|
1415
1748
|
if (state.mode === "conflict_resolution") {
|
|
1416
1749
|
const { pendingConflicts: _discarded,...rest } = state;
|
|
1417
1750
|
return {
|
|
@@ -1440,7 +1773,7 @@ function transition(state, event) {
|
|
|
1440
1773
|
effects
|
|
1441
1774
|
};
|
|
1442
1775
|
}
|
|
1443
|
-
effects.push(log("
|
|
1776
|
+
effects.push(log("debug", "Plugin requested file list"), { type: "LIST_LOCAL_FILES" });
|
|
1444
1777
|
return {
|
|
1445
1778
|
state,
|
|
1446
1779
|
effects
|
|
@@ -1454,7 +1787,7 @@ function transition(state, event) {
|
|
|
1454
1787
|
effects
|
|
1455
1788
|
};
|
|
1456
1789
|
}
|
|
1457
|
-
effects.push(log("
|
|
1790
|
+
effects.push(log("debug", `Received file list: ${event.files.length} files`));
|
|
1458
1791
|
effects.push({
|
|
1459
1792
|
type: "DETECT_CONFLICTS",
|
|
1460
1793
|
remoteFiles: event.files
|
|
@@ -1477,12 +1810,13 @@ function transition(state, event) {
|
|
|
1477
1810
|
};
|
|
1478
1811
|
}
|
|
1479
1812
|
const { conflicts, safeWrites, localOnly } = event;
|
|
1480
|
-
if (safeWrites.length > 0) effects.push(log("
|
|
1813
|
+
if (safeWrites.length > 0) effects.push(log("debug", `Applying ${safeWrites.length} safe writes`), {
|
|
1481
1814
|
type: "WRITE_FILES",
|
|
1482
|
-
files: safeWrites
|
|
1815
|
+
files: safeWrites,
|
|
1816
|
+
silent: true
|
|
1483
1817
|
});
|
|
1484
1818
|
if (localOnly.length > 0) {
|
|
1485
|
-
effects.push(log("
|
|
1819
|
+
effects.push(log("debug", `Uploading ${localOnly.length} local-only files`));
|
|
1486
1820
|
for (const file of localOnly) effects.push({
|
|
1487
1821
|
type: "SEND_MESSAGE",
|
|
1488
1822
|
payload: {
|
|
@@ -1493,7 +1827,7 @@ function transition(state, event) {
|
|
|
1493
1827
|
});
|
|
1494
1828
|
}
|
|
1495
1829
|
if (conflicts.length > 0) {
|
|
1496
|
-
effects.push(log("
|
|
1830
|
+
effects.push(log("debug", `${pluralize(conflicts.length, "conflict")} require version check`), {
|
|
1497
1831
|
type: "REQUEST_CONFLICT_VERSIONS",
|
|
1498
1832
|
conflicts
|
|
1499
1833
|
});
|
|
@@ -1506,7 +1840,17 @@ function transition(state, event) {
|
|
|
1506
1840
|
effects
|
|
1507
1841
|
};
|
|
1508
1842
|
}
|
|
1509
|
-
|
|
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
|
+
});
|
|
1510
1854
|
return {
|
|
1511
1855
|
state: {
|
|
1512
1856
|
...state,
|
|
@@ -1535,7 +1879,7 @@ function transition(state, event) {
|
|
|
1535
1879
|
effects
|
|
1536
1880
|
};
|
|
1537
1881
|
}
|
|
1538
|
-
effects.push(log("
|
|
1882
|
+
effects.push(log("debug", `Applying remote change: ${event.file.name}`), {
|
|
1539
1883
|
type: "WRITE_FILES",
|
|
1540
1884
|
files: [event.file]
|
|
1541
1885
|
});
|
|
@@ -1552,7 +1896,7 @@ function transition(state, event) {
|
|
|
1552
1896
|
effects
|
|
1553
1897
|
};
|
|
1554
1898
|
}
|
|
1555
|
-
effects.push(log("
|
|
1899
|
+
effects.push(log("debug", `Remote delete applied: ${event.fileName}`), {
|
|
1556
1900
|
type: "DELETE_LOCAL_FILES",
|
|
1557
1901
|
names: [event.fileName]
|
|
1558
1902
|
}, { type: "PERSIST_STATE" });
|
|
@@ -1562,7 +1906,7 @@ function transition(state, event) {
|
|
|
1562
1906
|
};
|
|
1563
1907
|
}
|
|
1564
1908
|
case "REMOTE_DELETE_CONFIRMED": {
|
|
1565
|
-
effects.push(log("
|
|
1909
|
+
effects.push(log("debug", `Delete confirmed: ${event.fileName}`), {
|
|
1566
1910
|
type: "DELETE_LOCAL_FILES",
|
|
1567
1911
|
names: [event.fileName]
|
|
1568
1912
|
}, { type: "PERSIST_STATE" });
|
|
@@ -1572,7 +1916,7 @@ function transition(state, event) {
|
|
|
1572
1916
|
};
|
|
1573
1917
|
}
|
|
1574
1918
|
case "REMOTE_DELETE_CANCELLED": {
|
|
1575
|
-
effects.push(log("
|
|
1919
|
+
effects.push(log("debug", `Delete cancelled: ${event.fileName}`));
|
|
1576
1920
|
effects.push({
|
|
1577
1921
|
type: "WRITE_FILES",
|
|
1578
1922
|
files: [{
|
|
@@ -1595,18 +1939,28 @@ function transition(state, event) {
|
|
|
1595
1939
|
};
|
|
1596
1940
|
}
|
|
1597
1941
|
if (event.resolution === "remote") {
|
|
1598
|
-
const
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
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({
|
|
1604
1947
|
type: "WRITE_FILES",
|
|
1605
|
-
files:
|
|
1948
|
+
files: [{
|
|
1949
|
+
name: conflict.fileName,
|
|
1950
|
+
content: conflict.remoteContent,
|
|
1951
|
+
modifiedAt: conflict.remoteModifiedAt
|
|
1952
|
+
}]
|
|
1606
1953
|
});
|
|
1954
|
+
effects.push(log("debug", `Applied ${state.pendingConflicts.length} remote versions`));
|
|
1607
1955
|
} else {
|
|
1608
|
-
|
|
1609
|
-
|
|
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({
|
|
1610
1964
|
type: "SEND_MESSAGE",
|
|
1611
1965
|
payload: {
|
|
1612
1966
|
type: "file-change",
|
|
@@ -1614,8 +1968,14 @@ function transition(state, event) {
|
|
|
1614
1968
|
content: conflict.localContent
|
|
1615
1969
|
}
|
|
1616
1970
|
});
|
|
1971
|
+
effects.push(log("debug", `Applied ${state.pendingConflicts.length} local versions`));
|
|
1617
1972
|
}
|
|
1618
|
-
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
|
+
});
|
|
1619
1979
|
const { pendingConflicts: _discarded,...rest } = state;
|
|
1620
1980
|
return {
|
|
1621
1981
|
state: {
|
|
@@ -1644,7 +2004,6 @@ function transition(state, event) {
|
|
|
1644
2004
|
effects
|
|
1645
2005
|
};
|
|
1646
2006
|
}
|
|
1647
|
-
effects.push(log("info", `Local change detected: ${relativePath}`));
|
|
1648
2007
|
effects.push({
|
|
1649
2008
|
type: "SEND_LOCAL_CHANGE",
|
|
1650
2009
|
fileName: relativePath,
|
|
@@ -1653,7 +2012,7 @@ function transition(state, event) {
|
|
|
1653
2012
|
break;
|
|
1654
2013
|
}
|
|
1655
2014
|
case "delete": {
|
|
1656
|
-
effects.push(log("
|
|
2015
|
+
effects.push(log("debug", `Local delete detected: ${relativePath}`), {
|
|
1657
2016
|
type: "REQUEST_LOCAL_DELETE_DECISION",
|
|
1658
2017
|
fileName: relativePath,
|
|
1659
2018
|
requireConfirmation: true
|
|
@@ -1676,23 +2035,37 @@ function transition(state, event) {
|
|
|
1676
2035
|
}
|
|
1677
2036
|
const { autoResolvedLocal, autoResolvedRemote, remainingConflicts } = autoResolveConflicts(state.pendingConflicts, event.versions);
|
|
1678
2037
|
if (autoResolvedLocal.length > 0) {
|
|
1679
|
-
effects.push(log("
|
|
1680
|
-
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({
|
|
1681
2047
|
type: "SEND_LOCAL_CHANGE",
|
|
1682
2048
|
fileName: conflict.fileName,
|
|
1683
2049
|
content: conflict.localContent
|
|
1684
2050
|
});
|
|
1685
2051
|
}
|
|
1686
|
-
if (autoResolvedRemote.length > 0)
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
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
|
+
}
|
|
1694
2067
|
if (remainingConflicts.length > 0) {
|
|
1695
|
-
effects.push(log("warn",
|
|
2068
|
+
effects.push(log("warn", `${pluralize(remainingConflicts.length, "conflict")} require resolution`), {
|
|
1696
2069
|
type: "REQUEST_CONFLICT_DECISIONS",
|
|
1697
2070
|
conflicts: remainingConflicts
|
|
1698
2071
|
});
|
|
@@ -1704,7 +2077,13 @@ function transition(state, event) {
|
|
|
1704
2077
|
effects
|
|
1705
2078
|
};
|
|
1706
2079
|
}
|
|
1707
|
-
|
|
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
|
+
});
|
|
1708
2087
|
const { pendingConflicts: _discarded,...rest } = state;
|
|
1709
2088
|
return {
|
|
1710
2089
|
state: {
|
|
@@ -1736,7 +2115,7 @@ async function executeEffect(effect, context) {
|
|
|
1736
2115
|
const projectName = config.explicitName ?? effect.projectInfo.projectName;
|
|
1737
2116
|
config.projectDir = await findOrCreateProjectDir(config.projectHash, projectName, config.explicitDir);
|
|
1738
2117
|
config.filesDir = `${config.projectDir}/files`;
|
|
1739
|
-
|
|
2118
|
+
debug(`Files directory: ${config.filesDir}`);
|
|
1740
2119
|
await fs.mkdir(config.filesDir, { recursive: true });
|
|
1741
2120
|
}
|
|
1742
2121
|
return [];
|
|
@@ -1744,7 +2123,7 @@ async function executeEffect(effect, context) {
|
|
|
1744
2123
|
case "LOAD_PERSISTED_STATE": {
|
|
1745
2124
|
if (config.projectDir) {
|
|
1746
2125
|
await fileMetadataCache.initialize(config.projectDir);
|
|
1747
|
-
|
|
2126
|
+
debug(`Loaded persisted metadata for ${fileMetadataCache.size()} files`);
|
|
1748
2127
|
}
|
|
1749
2128
|
return [];
|
|
1750
2129
|
}
|
|
@@ -1778,6 +2157,7 @@ async function executeEffect(effect, context) {
|
|
|
1778
2157
|
if (config.filesDir) {
|
|
1779
2158
|
await writeRemoteFiles(effect.files, config.filesDir, hashTracker, installer ?? void 0);
|
|
1780
2159
|
for (const file of effect.files) {
|
|
2160
|
+
if (!effect.silent) fileDown(file.name);
|
|
1781
2161
|
const remoteTimestamp = file.modifiedAt ?? Date.now();
|
|
1782
2162
|
fileMetadataCache.recordRemoteWrite(file.name, file.content, remoteTimestamp);
|
|
1783
2163
|
}
|
|
@@ -1787,6 +2167,7 @@ async function executeEffect(effect, context) {
|
|
|
1787
2167
|
case "DELETE_LOCAL_FILES": {
|
|
1788
2168
|
if (config.filesDir) for (const fileName of effect.names) {
|
|
1789
2169
|
await deleteLocalFile(fileName, config.filesDir, hashTracker);
|
|
2170
|
+
fileDelete(fileName);
|
|
1790
2171
|
fileMetadataCache.recordDelete(fileName);
|
|
1791
2172
|
}
|
|
1792
2173
|
return [];
|
|
@@ -1808,7 +2189,7 @@ async function executeEffect(effect, context) {
|
|
|
1808
2189
|
lastSyncedAt: conflict.lastSyncedAt ?? persisted?.timestamp
|
|
1809
2190
|
};
|
|
1810
2191
|
});
|
|
1811
|
-
|
|
2192
|
+
debug(`Requesting remote version data for ${pluralize(versionRequests.length, "file")}`);
|
|
1812
2193
|
await sendMessage(syncState.socket, {
|
|
1813
2194
|
type: "conflict-version-request",
|
|
1814
2195
|
conflicts: versionRequests
|
|
@@ -1833,17 +2214,27 @@ async function executeEffect(effect, context) {
|
|
|
1833
2214
|
return [];
|
|
1834
2215
|
}
|
|
1835
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
|
+
}
|
|
1836
2223
|
if (hashTracker.shouldSkip(effect.fileName, effect.content)) return [];
|
|
2224
|
+
debug(`Local change detected: ${effect.fileName}`);
|
|
1837
2225
|
try {
|
|
1838
|
-
if (syncState.socket)
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
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
|
+
}
|
|
1843
2234
|
hashTracker.remember(effect.fileName, effect.content);
|
|
1844
2235
|
if (installer) installer.process(effect.fileName, effect.content);
|
|
1845
2236
|
} catch (err) {
|
|
1846
|
-
|
|
2237
|
+
warn(`Failed to push ${effect.fileName}`);
|
|
1847
2238
|
}
|
|
1848
2239
|
return [];
|
|
1849
2240
|
}
|
|
@@ -1875,6 +2266,20 @@ async function executeEffect(effect, context) {
|
|
|
1875
2266
|
await fileMetadataCache.flush();
|
|
1876
2267
|
return [];
|
|
1877
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
|
+
}
|
|
1878
2283
|
case "LOG": {
|
|
1879
2284
|
const logFn = effect.level === "info" ? info : effect.level === "warn" ? warn : debug;
|
|
1880
2285
|
logFn(effect.message);
|
|
@@ -1886,9 +2291,7 @@ async function executeEffect(effect, context) {
|
|
|
1886
2291
|
* Starts the sync controller with the given configuration
|
|
1887
2292
|
*/
|
|
1888
2293
|
async function start(config) {
|
|
1889
|
-
|
|
1890
|
-
info(`Project: ${config.projectHash}`);
|
|
1891
|
-
info(`Port: ${config.port} (auto-selected from project hash)`);
|
|
2294
|
+
status("Waiting for Plugin connection...");
|
|
1892
2295
|
const hashTracker = createHashTracker();
|
|
1893
2296
|
const fileMetadataCache = new FileMetadataCache();
|
|
1894
2297
|
let installer = null;
|
|
@@ -1902,13 +2305,13 @@ async function start(config) {
|
|
|
1902
2305
|
const userActions = new UserActionCoordinator();
|
|
1903
2306
|
async function processEvent(event) {
|
|
1904
2307
|
const socketState = syncState.socket?.readyState;
|
|
1905
|
-
|
|
2308
|
+
debug(`[STATE] Processing event: ${event.type} (mode: ${syncState.mode}, socket: ${socketState ?? "none"})`);
|
|
1906
2309
|
const result = transition(syncState, event);
|
|
1907
2310
|
syncState = result.state;
|
|
1908
|
-
if (result.effects.length > 0)
|
|
2311
|
+
if (result.effects.length > 0) debug(`[STATE] Event produced ${result.effects.length} effects: ${result.effects.map((e) => e.type).join(", ")}`);
|
|
1909
2312
|
for (const effect of result.effects) {
|
|
1910
2313
|
const currentSocketState = syncState.socket?.readyState;
|
|
1911
|
-
if (currentSocketState !== void 0 && currentSocketState !== 1)
|
|
2314
|
+
if (currentSocketState !== void 0 && currentSocketState !== 1) debug(`[STATE] Socket not open (state: ${currentSocketState}) before executing ${effect.type}`);
|
|
1912
2315
|
const followUpEvents = await executeEffect(effect, {
|
|
1913
2316
|
config,
|
|
1914
2317
|
hashTracker,
|
|
@@ -1922,10 +2325,11 @@ async function start(config) {
|
|
|
1922
2325
|
}
|
|
1923
2326
|
const connection = await initConnection(config.port);
|
|
1924
2327
|
connection.on("handshake", async (client, message) => {
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
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}`);
|
|
1929
2333
|
client.close();
|
|
1930
2334
|
return;
|
|
1931
2335
|
}
|
|
@@ -1942,7 +2346,9 @@ async function start(config) {
|
|
|
1942
2346
|
await installer.initialize();
|
|
1943
2347
|
startWatcher();
|
|
1944
2348
|
}
|
|
1945
|
-
|
|
2349
|
+
cancelDisconnectMessage();
|
|
2350
|
+
const wasDisconnected = wasRecentlyDisconnected();
|
|
2351
|
+
if (!wasDisconnected && !didShowDisconnect()) success(`Connected to ${message.projectName}`);
|
|
1946
2352
|
});
|
|
1947
2353
|
async function handleMessage(message) {
|
|
1948
2354
|
if (!config.projectDir || !installer) {
|
|
@@ -1956,7 +2362,7 @@ async function start(config) {
|
|
|
1956
2362
|
break;
|
|
1957
2363
|
case "file-list": {
|
|
1958
2364
|
const totalSize = message.files.reduce((sum, f) => sum + (f.content?.length ?? 0), 0);
|
|
1959
|
-
|
|
2365
|
+
debug(`Received file list: ${message.files.length} files (${(totalSize / 1024).toFixed(1)}KB)`);
|
|
1960
2366
|
event = {
|
|
1961
2367
|
type: "FILE_LIST",
|
|
1962
2368
|
files: message.files
|
|
@@ -2037,10 +2443,11 @@ async function start(config) {
|
|
|
2037
2443
|
}
|
|
2038
2444
|
});
|
|
2039
2445
|
connection.on("disconnect", async () => {
|
|
2040
|
-
|
|
2446
|
+
scheduleDisconnectMessage(() => {
|
|
2447
|
+
status("Disconnected, waiting to reconnect...");
|
|
2448
|
+
});
|
|
2041
2449
|
await processEvent({ type: "DISCONNECT" });
|
|
2042
2450
|
userActions.cleanup();
|
|
2043
|
-
info("Will perform full diff on reconnect");
|
|
2044
2451
|
});
|
|
2045
2452
|
connection.on("error", (err) => {
|
|
2046
2453
|
error("Error on WebSocket connection:", err);
|
|
@@ -2056,10 +2463,9 @@ async function start(config) {
|
|
|
2056
2463
|
});
|
|
2057
2464
|
});
|
|
2058
2465
|
};
|
|
2059
|
-
info("✓ Controller initialized and ready");
|
|
2060
|
-
info(`Waiting for plugin connection on port ${config.port}...`);
|
|
2061
2466
|
process.on("SIGINT", async () => {
|
|
2062
|
-
|
|
2467
|
+
console.log();
|
|
2468
|
+
status("Shutting down...");
|
|
2063
2469
|
if (watcher) await watcher.close();
|
|
2064
2470
|
connection.close();
|
|
2065
2471
|
process.exit(0);
|
|
@@ -2098,6 +2504,7 @@ program.name("code-link").description("Sync Framer code components to your local
|
|
|
2098
2504
|
if (level !== void 0) setLogLevel(level);
|
|
2099
2505
|
} else if (options.verbose || isDev) setLogLevel(LogLevel.DEBUG);
|
|
2100
2506
|
const port = getPortFromHash(projectHash);
|
|
2507
|
+
banner("0.1.3", port);
|
|
2101
2508
|
const config = {
|
|
2102
2509
|
port,
|
|
2103
2510
|
projectHash,
|
|
@@ -2107,7 +2514,7 @@ program.name("code-link").description("Sync Framer code components to your local
|
|
|
2107
2514
|
explicitDir: options.dir,
|
|
2108
2515
|
explicitName: options.name
|
|
2109
2516
|
};
|
|
2110
|
-
if (config.dangerouslyAutoDelete)
|
|
2517
|
+
if (config.dangerouslyAutoDelete) warn("Auto-delete mode enabled - files will be deleted without confirmation");
|
|
2111
2518
|
try {
|
|
2112
2519
|
await start(config);
|
|
2113
2520
|
} catch (err) {
|