leglas 0.7.1 → 0.7.2
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/bin.js +434 -35
- package/dist/index.js +434 -35
- package/dist/shell/assets/{index-CuUL6FpW.js → index-DbONqSbP.js} +8 -8
- package/dist/shell/index.html +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -783,7 +783,7 @@ ${AGENTS_SECTION}`
|
|
|
783
783
|
}
|
|
784
784
|
|
|
785
785
|
// src/keep.ts
|
|
786
|
-
import { basename as
|
|
786
|
+
import { basename as basename4, extname as extname4, normalize as normalize2 } from "path";
|
|
787
787
|
|
|
788
788
|
// ../server/dist/config.js
|
|
789
789
|
var DEFAULT_DEV_SERVER = "http://localhost:3000";
|
|
@@ -1555,8 +1555,8 @@ function findConfigFile(startDir) {
|
|
|
1555
1555
|
const { root } = parse(startDir);
|
|
1556
1556
|
let dir = startDir;
|
|
1557
1557
|
for (; ; ) {
|
|
1558
|
-
for (const
|
|
1559
|
-
const candidate = join2(dir,
|
|
1558
|
+
for (const basename7 of CONFIG_BASENAMES) {
|
|
1559
|
+
const candidate = join2(dir, basename7);
|
|
1560
1560
|
if (existsSync(candidate))
|
|
1561
1561
|
return candidate;
|
|
1562
1562
|
}
|
|
@@ -4602,6 +4602,10 @@ function startRunner(options) {
|
|
|
4602
4602
|
stopping: false,
|
|
4603
4603
|
waiting: null
|
|
4604
4604
|
};
|
|
4605
|
+
const setState = (next) => {
|
|
4606
|
+
state = typeof next === "function" ? next(state) : next;
|
|
4607
|
+
options.onChange?.();
|
|
4608
|
+
};
|
|
4605
4609
|
let stopped = false;
|
|
4606
4610
|
let ticking = null;
|
|
4607
4611
|
let pendingNudges = 0;
|
|
@@ -4653,7 +4657,7 @@ function startRunner(options) {
|
|
|
4653
4657
|
return session !== void 0 && session.turns < SESSION_TURNS_CAP ? session.id : null;
|
|
4654
4658
|
};
|
|
4655
4659
|
const idle = () => {
|
|
4656
|
-
|
|
4660
|
+
setState({
|
|
4657
4661
|
running: false,
|
|
4658
4662
|
requestId: null,
|
|
4659
4663
|
agent: null,
|
|
@@ -4661,7 +4665,7 @@ function startRunner(options) {
|
|
|
4661
4665
|
startedAt: null,
|
|
4662
4666
|
stopping: false,
|
|
4663
4667
|
waiting: null
|
|
4664
|
-
};
|
|
4668
|
+
});
|
|
4665
4669
|
};
|
|
4666
4670
|
const rememberLine = (lines2, line) => {
|
|
4667
4671
|
lines2.push(line);
|
|
@@ -4769,14 +4773,15 @@ function startRunner(options) {
|
|
|
4769
4773
|
if (retry !== null) {
|
|
4770
4774
|
observed.retry = retry;
|
|
4771
4775
|
if (active === current)
|
|
4772
|
-
|
|
4776
|
+
setState((value) => ({ ...value, waiting: retry }));
|
|
4773
4777
|
}
|
|
4774
4778
|
const activity = activityFrom(resolved.agent, line, options.cwd);
|
|
4775
4779
|
if (activity !== null) {
|
|
4776
4780
|
if (activity.startsWith("editing"))
|
|
4777
4781
|
observed.edited = true;
|
|
4778
|
-
if (active === current)
|
|
4779
|
-
|
|
4782
|
+
if (active === current) {
|
|
4783
|
+
setState((value) => ({ ...value, activity, waiting: null }));
|
|
4784
|
+
}
|
|
4780
4785
|
}
|
|
4781
4786
|
});
|
|
4782
4787
|
const stderrFlush = lineReader(child.stderr, (line) => rememberLine(lines2, line));
|
|
@@ -4823,7 +4828,7 @@ function startRunner(options) {
|
|
|
4823
4828
|
await reportFailure(request, classifyFailure({ agent: resolved.name, error: "stopped by shutdown" }), []);
|
|
4824
4829
|
return;
|
|
4825
4830
|
}
|
|
4826
|
-
|
|
4831
|
+
setState({
|
|
4827
4832
|
running: true,
|
|
4828
4833
|
requestId: request.id,
|
|
4829
4834
|
agent: resolved.name,
|
|
@@ -4831,7 +4836,7 @@ function startRunner(options) {
|
|
|
4831
4836
|
startedAt: Date.now(),
|
|
4832
4837
|
stopping: false,
|
|
4833
4838
|
waiting: null
|
|
4834
|
-
};
|
|
4839
|
+
});
|
|
4835
4840
|
const observed = {
|
|
4836
4841
|
sessionId: null,
|
|
4837
4842
|
edited: false,
|
|
@@ -4862,7 +4867,7 @@ function startRunner(options) {
|
|
|
4862
4867
|
resolved = cold;
|
|
4863
4868
|
observed.sessionId = null;
|
|
4864
4869
|
observed.retry = null;
|
|
4865
|
-
|
|
4870
|
+
setState((value) => ({ ...value, activity: null, waiting: null }));
|
|
4866
4871
|
outcome = await runChild(request, resolved, lines2, observed);
|
|
4867
4872
|
failure = verdict();
|
|
4868
4873
|
}
|
|
@@ -4936,7 +4941,7 @@ function startRunner(options) {
|
|
|
4936
4941
|
const current = active;
|
|
4937
4942
|
current.cancelled = true;
|
|
4938
4943
|
failed.add(current.requestId);
|
|
4939
|
-
|
|
4944
|
+
setState((value) => ({ ...value, stopping: true, waiting: null }));
|
|
4940
4945
|
current.controller.abort();
|
|
4941
4946
|
try {
|
|
4942
4947
|
current.child?.kill("SIGTERM");
|
|
@@ -4979,6 +4984,193 @@ function startRunner(options) {
|
|
|
4979
4984
|
};
|
|
4980
4985
|
}
|
|
4981
4986
|
|
|
4987
|
+
// ../server/dist/live.js
|
|
4988
|
+
import { createHash } from "crypto";
|
|
4989
|
+
var LIVE_PATH = "/leglas/api/live";
|
|
4990
|
+
var WEBSOCKET_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
|
4991
|
+
function encodeFrame(opcode, payload) {
|
|
4992
|
+
const body = typeof payload === "string" ? Buffer.from(payload) : payload;
|
|
4993
|
+
let header;
|
|
4994
|
+
if (body.length < 126) {
|
|
4995
|
+
header = Buffer.allocUnsafe(2);
|
|
4996
|
+
header[1] = body.length;
|
|
4997
|
+
} else if (body.length <= 65535) {
|
|
4998
|
+
header = Buffer.allocUnsafe(4);
|
|
4999
|
+
header[1] = 126;
|
|
5000
|
+
header.writeUInt16BE(body.length, 2);
|
|
5001
|
+
} else {
|
|
5002
|
+
header = Buffer.allocUnsafe(10);
|
|
5003
|
+
header[1] = 127;
|
|
5004
|
+
header.writeBigUInt64BE(BigInt(body.length), 2);
|
|
5005
|
+
}
|
|
5006
|
+
header[0] = 128 | opcode & 15;
|
|
5007
|
+
return Buffer.concat([header, body], header.length + body.length);
|
|
5008
|
+
}
|
|
5009
|
+
var LIVE_DEBOUNCE_MS = 50;
|
|
5010
|
+
function createCoalescer(emit, options = {}) {
|
|
5011
|
+
const windowMs = options.windowMs ?? LIVE_DEBOUNCE_MS;
|
|
5012
|
+
const setLater = options.setTimeout ?? ((callback, ms) => {
|
|
5013
|
+
const timer = setTimeout(callback, ms);
|
|
5014
|
+
timer.unref?.();
|
|
5015
|
+
return timer;
|
|
5016
|
+
});
|
|
5017
|
+
const clearLater = options.clearTimeout ?? ((handle) => clearTimeout(handle));
|
|
5018
|
+
const pending = /* @__PURE__ */ new Map();
|
|
5019
|
+
let closed = false;
|
|
5020
|
+
return {
|
|
5021
|
+
schedule(change) {
|
|
5022
|
+
if (closed)
|
|
5023
|
+
return;
|
|
5024
|
+
const waiting = pending.get(change);
|
|
5025
|
+
if (waiting !== void 0)
|
|
5026
|
+
clearLater(waiting);
|
|
5027
|
+
pending.set(change, setLater(() => {
|
|
5028
|
+
pending.delete(change);
|
|
5029
|
+
if (!closed)
|
|
5030
|
+
emit(change);
|
|
5031
|
+
}, windowMs));
|
|
5032
|
+
},
|
|
5033
|
+
close() {
|
|
5034
|
+
closed = true;
|
|
5035
|
+
for (const handle of pending.values())
|
|
5036
|
+
clearLater(handle);
|
|
5037
|
+
pending.clear();
|
|
5038
|
+
}
|
|
5039
|
+
};
|
|
5040
|
+
}
|
|
5041
|
+
function createLiveHub(_options = {}) {
|
|
5042
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
5043
|
+
const drop = (listener) => {
|
|
5044
|
+
listeners.delete(listener);
|
|
5045
|
+
};
|
|
5046
|
+
const write2 = (listener, opcode, payload) => {
|
|
5047
|
+
if (listener.socket.destroyed || !listener.socket.writable) {
|
|
5048
|
+
drop(listener);
|
|
5049
|
+
return false;
|
|
5050
|
+
}
|
|
5051
|
+
try {
|
|
5052
|
+
listener.socket.write(encodeFrame(opcode, payload));
|
|
5053
|
+
return true;
|
|
5054
|
+
} catch {
|
|
5055
|
+
drop(listener);
|
|
5056
|
+
listener.socket.destroy();
|
|
5057
|
+
return false;
|
|
5058
|
+
}
|
|
5059
|
+
};
|
|
5060
|
+
const read = (listener, chunk) => {
|
|
5061
|
+
const incoming = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
5062
|
+
listener.buffered = listener.buffered.length === 0 ? incoming : Buffer.concat([listener.buffered, incoming]);
|
|
5063
|
+
while (listener.buffered.length >= 2) {
|
|
5064
|
+
const first = listener.buffered[0] ?? 0;
|
|
5065
|
+
const second = listener.buffered[1] ?? 0;
|
|
5066
|
+
let length = second & 127;
|
|
5067
|
+
let offset = 2;
|
|
5068
|
+
if (length === 126) {
|
|
5069
|
+
if (listener.buffered.length < 4)
|
|
5070
|
+
return;
|
|
5071
|
+
length = listener.buffered.readUInt16BE(2);
|
|
5072
|
+
offset = 4;
|
|
5073
|
+
} else if (length === 127) {
|
|
5074
|
+
if (listener.buffered.length < 10)
|
|
5075
|
+
return;
|
|
5076
|
+
const wide = listener.buffered.readBigUInt64BE(2);
|
|
5077
|
+
if (wide > BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
5078
|
+
drop(listener);
|
|
5079
|
+
listener.socket.destroy();
|
|
5080
|
+
return;
|
|
5081
|
+
}
|
|
5082
|
+
length = Number(wide);
|
|
5083
|
+
offset = 10;
|
|
5084
|
+
}
|
|
5085
|
+
const masked = (second & 128) !== 0;
|
|
5086
|
+
if (!masked) {
|
|
5087
|
+
drop(listener);
|
|
5088
|
+
listener.socket.destroy();
|
|
5089
|
+
return;
|
|
5090
|
+
}
|
|
5091
|
+
if (listener.buffered.length < offset + 4 + length)
|
|
5092
|
+
return;
|
|
5093
|
+
const mask = listener.buffered.subarray(offset, offset + 4);
|
|
5094
|
+
offset += 4;
|
|
5095
|
+
const payload = Buffer.from(listener.buffered.subarray(offset, offset + length));
|
|
5096
|
+
for (let index = 0; index < payload.length; index += 1) {
|
|
5097
|
+
payload[index] = (payload[index] ?? 0) ^ (mask[index % 4] ?? 0);
|
|
5098
|
+
}
|
|
5099
|
+
listener.buffered = listener.buffered.subarray(offset + length);
|
|
5100
|
+
const opcode = first & 15;
|
|
5101
|
+
if (opcode === 8) {
|
|
5102
|
+
write2(listener, 8, payload);
|
|
5103
|
+
drop(listener);
|
|
5104
|
+
listener.socket.destroy();
|
|
5105
|
+
return;
|
|
5106
|
+
}
|
|
5107
|
+
if (opcode === 9)
|
|
5108
|
+
write2(listener, 10, payload);
|
|
5109
|
+
}
|
|
5110
|
+
};
|
|
5111
|
+
return {
|
|
5112
|
+
nudge: (change) => {
|
|
5113
|
+
if (listeners.size === 0)
|
|
5114
|
+
return;
|
|
5115
|
+
const frame = encodeFrame(1, JSON.stringify({ changed: change }));
|
|
5116
|
+
for (const listener of [...listeners]) {
|
|
5117
|
+
if (listener.socket.destroyed || !listener.socket.writable) {
|
|
5118
|
+
drop(listener);
|
|
5119
|
+
continue;
|
|
5120
|
+
}
|
|
5121
|
+
try {
|
|
5122
|
+
listener.socket.write(frame);
|
|
5123
|
+
} catch {
|
|
5124
|
+
drop(listener);
|
|
5125
|
+
listener.socket.destroy();
|
|
5126
|
+
}
|
|
5127
|
+
}
|
|
5128
|
+
},
|
|
5129
|
+
upgrade: (req, socket, head) => {
|
|
5130
|
+
const path = (req.url ?? "/").split("?")[0] ?? "/";
|
|
5131
|
+
if (req.method !== "GET" || path !== LIVE_PATH)
|
|
5132
|
+
return false;
|
|
5133
|
+
const upgrade = req.headers.upgrade;
|
|
5134
|
+
const key = req.headers["sec-websocket-key"];
|
|
5135
|
+
if (typeof upgrade !== "string" || upgrade.toLowerCase() !== "websocket" || typeof key !== "string" || key.trim() === "") {
|
|
5136
|
+
socket.destroy();
|
|
5137
|
+
return false;
|
|
5138
|
+
}
|
|
5139
|
+
const accept = createHash("sha1").update(key + WEBSOCKET_GUID).digest("base64");
|
|
5140
|
+
try {
|
|
5141
|
+
socket.write(`HTTP/1.1 101 Switching Protocols\r
|
|
5142
|
+
Upgrade: websocket\r
|
|
5143
|
+
Connection: Upgrade\r
|
|
5144
|
+
Sec-WebSocket-Accept: ${accept}\r
|
|
5145
|
+
\r
|
|
5146
|
+
`);
|
|
5147
|
+
} catch {
|
|
5148
|
+
socket.destroy();
|
|
5149
|
+
return false;
|
|
5150
|
+
}
|
|
5151
|
+
const listener = { socket, buffered: Buffer.alloc(0) };
|
|
5152
|
+
listeners.add(listener);
|
|
5153
|
+
socket.on("data", (chunk) => read(listener, chunk));
|
|
5154
|
+
socket.once("error", () => drop(listener));
|
|
5155
|
+
socket.once("end", () => drop(listener));
|
|
5156
|
+
socket.once("close", () => drop(listener));
|
|
5157
|
+
if (head.length > 0)
|
|
5158
|
+
read(listener, head);
|
|
5159
|
+
return true;
|
|
5160
|
+
},
|
|
5161
|
+
close: async () => {
|
|
5162
|
+
for (const listener of [...listeners]) {
|
|
5163
|
+
write2(listener, 8, Buffer.alloc(0));
|
|
5164
|
+
drop(listener);
|
|
5165
|
+
listener.socket.destroy();
|
|
5166
|
+
}
|
|
5167
|
+
},
|
|
5168
|
+
get listening() {
|
|
5169
|
+
return listeners.size;
|
|
5170
|
+
}
|
|
5171
|
+
};
|
|
5172
|
+
}
|
|
5173
|
+
|
|
4982
5174
|
// ../server/dist/renames.js
|
|
4983
5175
|
import { mkdir as mkdir6, readFile as readFile10, writeFile as writeFile6 } from "fs/promises";
|
|
4984
5176
|
import { dirname as dirname6, join as join10 } from "path";
|
|
@@ -5012,11 +5204,11 @@ function resolveTitle(input, titles, renames) {
|
|
|
5012
5204
|
}
|
|
5013
5205
|
|
|
5014
5206
|
// ../server/dist/server.js
|
|
5015
|
-
import { createReadStream, existsSync as existsSync3, statSync } from "fs";
|
|
5207
|
+
import { createReadStream, existsSync as existsSync3, statSync, unwatchFile, watch as watchFs, watchFile } from "fs";
|
|
5016
5208
|
import { mkdir as mkdir8, readdir as readdir3, writeFile as writeFile8 } from "fs/promises";
|
|
5017
5209
|
import http2 from "http";
|
|
5018
5210
|
import net3 from "net";
|
|
5019
|
-
import { extname as extname3, join as join12, normalize, relative as relative3 } from "path";
|
|
5211
|
+
import { basename as basename3, dirname as dirname8, extname as extname3, join as join12, normalize, relative as relative3 } from "path";
|
|
5020
5212
|
|
|
5021
5213
|
// ../server/dist/server-info.js
|
|
5022
5214
|
import { lstat as lstat2, mkdir as mkdir7, readFile as readFile11, rename as rename2, rm as rm4, writeFile as writeFile7 } from "fs/promises";
|
|
@@ -5201,6 +5393,204 @@ function serveShellFile(res, shellDir, urlPath) {
|
|
|
5201
5393
|
const isRoot = relative6 === "" || relative6 === "." || relative6 === "/";
|
|
5202
5394
|
return serveFrom(res, shellDir, isRoot ? "index.html" : relative6);
|
|
5203
5395
|
}
|
|
5396
|
+
var HEALTH_PROBE_MS = 3e3;
|
|
5397
|
+
function fileStamp(path) {
|
|
5398
|
+
try {
|
|
5399
|
+
const stat5 = statSync(path);
|
|
5400
|
+
return `${stat5.dev}:${stat5.ino}:${stat5.size}:${stat5.mtimeMs}:${stat5.ctimeMs}`;
|
|
5401
|
+
} catch {
|
|
5402
|
+
return null;
|
|
5403
|
+
}
|
|
5404
|
+
}
|
|
5405
|
+
function watchLiveFiles(cwd, configPath, live) {
|
|
5406
|
+
const leglasDir = join12(cwd, ".leglas");
|
|
5407
|
+
const targets = [
|
|
5408
|
+
{ path: join12(cwd, LOCAL_PREVIEWS_PATH), change: "config" },
|
|
5409
|
+
{ path: join12(cwd, REQUESTS_PATH), change: "requests" },
|
|
5410
|
+
{ path: join12(cwd, ANNOTATIONS_PATH), change: "requests" }
|
|
5411
|
+
];
|
|
5412
|
+
const byName = new Map(targets.map((target) => [basename3(target.path), target]));
|
|
5413
|
+
const known = new Map(targets.map((target) => [target.path, fileStamp(target.path)]));
|
|
5414
|
+
const coalescer = createCoalescer((change) => live.nudge(change));
|
|
5415
|
+
const watchers = /* @__PURE__ */ new Set();
|
|
5416
|
+
const fallback = /* @__PURE__ */ new Map();
|
|
5417
|
+
let leglasWatcher = null;
|
|
5418
|
+
let retry = null;
|
|
5419
|
+
let closed = false;
|
|
5420
|
+
const nudgeSoon = (change) => coalescer.schedule(change);
|
|
5421
|
+
const scanLeglas = (notify) => {
|
|
5422
|
+
for (const target of targets) {
|
|
5423
|
+
const next = fileStamp(target.path);
|
|
5424
|
+
if (next === known.get(target.path))
|
|
5425
|
+
continue;
|
|
5426
|
+
known.set(target.path, next);
|
|
5427
|
+
if (notify)
|
|
5428
|
+
nudgeSoon(target.change);
|
|
5429
|
+
}
|
|
5430
|
+
};
|
|
5431
|
+
const fallbackWatch = (target) => {
|
|
5432
|
+
if (closed || fallback.has(target.path))
|
|
5433
|
+
return;
|
|
5434
|
+
const listener = () => {
|
|
5435
|
+
const next = fileStamp(target.path);
|
|
5436
|
+
if (next === known.get(target.path))
|
|
5437
|
+
return;
|
|
5438
|
+
known.set(target.path, next);
|
|
5439
|
+
nudgeSoon(target.change);
|
|
5440
|
+
};
|
|
5441
|
+
fallback.set(target.path, listener);
|
|
5442
|
+
watchFile(target.path, { persistent: false, interval: 250 }, listener);
|
|
5443
|
+
};
|
|
5444
|
+
const fallbackLeglas = () => {
|
|
5445
|
+
for (const target of targets)
|
|
5446
|
+
fallbackWatch(target);
|
|
5447
|
+
};
|
|
5448
|
+
const retryLeglas = () => {
|
|
5449
|
+
if (closed || retry !== null)
|
|
5450
|
+
return;
|
|
5451
|
+
retry = setTimeout(() => {
|
|
5452
|
+
retry = null;
|
|
5453
|
+
armLeglas(true);
|
|
5454
|
+
}, 250);
|
|
5455
|
+
retry.unref?.();
|
|
5456
|
+
};
|
|
5457
|
+
const armLeglas = (notify) => {
|
|
5458
|
+
if (closed)
|
|
5459
|
+
return;
|
|
5460
|
+
scanLeglas(notify);
|
|
5461
|
+
let directory = false;
|
|
5462
|
+
try {
|
|
5463
|
+
directory = statSync(leglasDir).isDirectory();
|
|
5464
|
+
} catch {
|
|
5465
|
+
directory = false;
|
|
5466
|
+
}
|
|
5467
|
+
if (!directory) {
|
|
5468
|
+
if (leglasWatcher !== null) {
|
|
5469
|
+
watchers.delete(leglasWatcher);
|
|
5470
|
+
leglasWatcher.close();
|
|
5471
|
+
leglasWatcher = null;
|
|
5472
|
+
}
|
|
5473
|
+
retryLeglas();
|
|
5474
|
+
return;
|
|
5475
|
+
}
|
|
5476
|
+
if (leglasWatcher !== null)
|
|
5477
|
+
return;
|
|
5478
|
+
try {
|
|
5479
|
+
const watcher = watchFs(leglasDir, { persistent: false }, (_event, filename) => {
|
|
5480
|
+
if (filename === null) {
|
|
5481
|
+
scanLeglas(true);
|
|
5482
|
+
return;
|
|
5483
|
+
}
|
|
5484
|
+
const name = Buffer.isBuffer(filename) ? filename.toString() : filename;
|
|
5485
|
+
const target = byName.get(name);
|
|
5486
|
+
if (target === void 0)
|
|
5487
|
+
return;
|
|
5488
|
+
known.set(target.path, fileStamp(target.path));
|
|
5489
|
+
nudgeSoon(target.change);
|
|
5490
|
+
});
|
|
5491
|
+
watcher.on("error", () => {
|
|
5492
|
+
if (leglasWatcher !== watcher)
|
|
5493
|
+
return;
|
|
5494
|
+
watchers.delete(watcher);
|
|
5495
|
+
watcher.close();
|
|
5496
|
+
leglasWatcher = null;
|
|
5497
|
+
fallbackLeglas();
|
|
5498
|
+
retryLeglas();
|
|
5499
|
+
});
|
|
5500
|
+
leglasWatcher = watcher;
|
|
5501
|
+
watchers.add(watcher);
|
|
5502
|
+
} catch {
|
|
5503
|
+
fallbackLeglas();
|
|
5504
|
+
retryLeglas();
|
|
5505
|
+
}
|
|
5506
|
+
};
|
|
5507
|
+
try {
|
|
5508
|
+
const watcher = watchFs(cwd, { persistent: false }, (_event, filename) => {
|
|
5509
|
+
const name = filename === null ? null : Buffer.isBuffer(filename) ? filename.toString() : filename;
|
|
5510
|
+
if (name === null || name === ".leglas")
|
|
5511
|
+
armLeglas(true);
|
|
5512
|
+
});
|
|
5513
|
+
watcher.on("error", () => {
|
|
5514
|
+
watchers.delete(watcher);
|
|
5515
|
+
watcher.close();
|
|
5516
|
+
fallbackLeglas();
|
|
5517
|
+
});
|
|
5518
|
+
watchers.add(watcher);
|
|
5519
|
+
} catch {
|
|
5520
|
+
fallbackLeglas();
|
|
5521
|
+
}
|
|
5522
|
+
if (configPath !== null) {
|
|
5523
|
+
try {
|
|
5524
|
+
const directory = dirname8(configPath);
|
|
5525
|
+
const name = basename3(configPath);
|
|
5526
|
+
const target = { path: configPath, change: "config" };
|
|
5527
|
+
known.set(configPath, fileStamp(configPath));
|
|
5528
|
+
const watcher = watchFs(directory, { persistent: false }, (_event, filename) => {
|
|
5529
|
+
const changed = filename === null ? null : Buffer.isBuffer(filename) ? filename.toString() : filename;
|
|
5530
|
+
if (changed === null || changed === name)
|
|
5531
|
+
nudgeSoon("config");
|
|
5532
|
+
});
|
|
5533
|
+
watcher.on("error", () => {
|
|
5534
|
+
watchers.delete(watcher);
|
|
5535
|
+
watcher.close();
|
|
5536
|
+
fallbackWatch(target);
|
|
5537
|
+
});
|
|
5538
|
+
watchers.add(watcher);
|
|
5539
|
+
} catch {
|
|
5540
|
+
fallbackWatch({ path: configPath, change: "config" });
|
|
5541
|
+
}
|
|
5542
|
+
}
|
|
5543
|
+
armLeglas(false);
|
|
5544
|
+
return {
|
|
5545
|
+
close: () => {
|
|
5546
|
+
if (closed)
|
|
5547
|
+
return;
|
|
5548
|
+
closed = true;
|
|
5549
|
+
if (retry !== null)
|
|
5550
|
+
clearTimeout(retry);
|
|
5551
|
+
coalescer.close();
|
|
5552
|
+
for (const watcher of watchers)
|
|
5553
|
+
watcher.close();
|
|
5554
|
+
watchers.clear();
|
|
5555
|
+
for (const [path, listener] of fallback)
|
|
5556
|
+
unwatchFile(path, listener);
|
|
5557
|
+
fallback.clear();
|
|
5558
|
+
leglasWatcher = null;
|
|
5559
|
+
}
|
|
5560
|
+
};
|
|
5561
|
+
}
|
|
5562
|
+
function watchHealth(target, live) {
|
|
5563
|
+
let previous = null;
|
|
5564
|
+
let probing = false;
|
|
5565
|
+
let closed = false;
|
|
5566
|
+
const timer = setInterval(() => {
|
|
5567
|
+
if (live.listening === 0) {
|
|
5568
|
+
previous = null;
|
|
5569
|
+
return;
|
|
5570
|
+
}
|
|
5571
|
+
if (probing)
|
|
5572
|
+
return;
|
|
5573
|
+
probing = true;
|
|
5574
|
+
void probe(target).then((reachable) => {
|
|
5575
|
+
if (closed || live.listening === 0) {
|
|
5576
|
+
previous = null;
|
|
5577
|
+
return;
|
|
5578
|
+
}
|
|
5579
|
+
if (previous !== null && previous !== reachable)
|
|
5580
|
+
live.nudge("health");
|
|
5581
|
+
previous = reachable;
|
|
5582
|
+
}).finally(() => {
|
|
5583
|
+
probing = false;
|
|
5584
|
+
});
|
|
5585
|
+
}, HEALTH_PROBE_MS);
|
|
5586
|
+
timer.unref?.();
|
|
5587
|
+
return {
|
|
5588
|
+
close: () => {
|
|
5589
|
+
closed = true;
|
|
5590
|
+
clearInterval(timer);
|
|
5591
|
+
}
|
|
5592
|
+
};
|
|
5593
|
+
}
|
|
5204
5594
|
function snapshotConfig(cwd) {
|
|
5205
5595
|
const path = findConfigFile(cwd);
|
|
5206
5596
|
if (path === null)
|
|
@@ -5271,12 +5661,14 @@ async function bind(server, requested) {
|
|
|
5271
5661
|
async function startServer(options) {
|
|
5272
5662
|
const { config, configErrors = [], configWarnings = [], shellDir = null, project = "", cwd = process.cwd(), leglasCommand = "npx -y leglas", fileMounts = /* @__PURE__ */ new Map(), detect = () => detectAgents() } = options;
|
|
5273
5663
|
const browserPool = options.pool ?? createBrowserPool();
|
|
5664
|
+
const live = options.live ?? createLiveHub();
|
|
5274
5665
|
if (options.pool === void 0) {
|
|
5275
5666
|
void reapOrphanedBrowsers().catch(() => {
|
|
5276
5667
|
});
|
|
5277
5668
|
}
|
|
5278
5669
|
const target = config?.devServer ?? "http://localhost:3000";
|
|
5279
5670
|
const proxy = createProxyHandler({ target });
|
|
5671
|
+
const bootConfigPath = findConfigFile(cwd);
|
|
5280
5672
|
const bootConfigSnapshot = snapshotConfig(cwd);
|
|
5281
5673
|
let lastSeen = null;
|
|
5282
5674
|
const externallyAttached = () => lastSeen !== null && Date.now() - lastSeen < ATTACHED_WINDOW_MS;
|
|
@@ -5501,14 +5893,14 @@ async function startServer(options) {
|
|
|
5501
5893
|
return sendJson(res, 400, { ok: false, error: "Unknown preview, or empty request." });
|
|
5502
5894
|
}
|
|
5503
5895
|
const intent = (parsed.intent ?? "").trim();
|
|
5504
|
-
const
|
|
5896
|
+
const live2 = (await readRequests(cwd).catch(() => [])).filter((entry) => entry.status === "queued" || entry.status === "picked-up");
|
|
5505
5897
|
const sameNotes = (entry) => {
|
|
5506
5898
|
const before = [...entry.notes ?? []].sort().join(",");
|
|
5507
5899
|
return before === notes.map((note) => note.id).sort().join(",");
|
|
5508
5900
|
};
|
|
5509
5901
|
const compare = typeof parsed.compare === "string" && parsed.compare !== preview.title ? previews.find((entry) => entry.title === parsed.compare) ?? null : null;
|
|
5510
5902
|
const sameContext = (entry) => (entry.compare ?? null) === (compare?.title ?? null) && [...entry.references ?? []].sort().join(",") === [...references].sort().join(",");
|
|
5511
|
-
if (
|
|
5903
|
+
if (live2.some((entry) => entry.title === preview.title && entry.intent === intent && // The same words in the other mode are not the same request:
|
|
5512
5904
|
// one forks the direction and the other rewrites it. Only a
|
|
5513
5905
|
// genuine repeat is refused.
|
|
5514
5906
|
(entry.mode ?? "replace") === mode && sameNotes(entry) && sameContext(entry))) {
|
|
@@ -6019,12 +6411,16 @@ async function startServer(options) {
|
|
|
6019
6411
|
socket.once("close", () => sockets.delete(socket));
|
|
6020
6412
|
});
|
|
6021
6413
|
server.on("upgrade", (req, socket, head) => {
|
|
6414
|
+
if (live.upgrade(req, socket, head))
|
|
6415
|
+
return;
|
|
6022
6416
|
const path = (req.url ?? "/").split("?")[0] ?? "/";
|
|
6023
6417
|
if (path.startsWith(`${LEGLAS_PREFIX}/`))
|
|
6024
6418
|
return socket.destroy();
|
|
6025
6419
|
proxy.upgrade(req, socket, head);
|
|
6026
6420
|
});
|
|
6027
6421
|
const port = await bind(server, options.port ?? DEFAULT_PORT);
|
|
6422
|
+
const liveFiles = watchLiveFiles(cwd, bootConfigPath, live);
|
|
6423
|
+
const liveHealth = watchHealth(target, live);
|
|
6028
6424
|
await pruneCaptures(cwd, (await readRequests(cwd).catch(() => [])).map((request) => request.id)).catch(() => {
|
|
6029
6425
|
});
|
|
6030
6426
|
await writeServerInfo(cwd, {
|
|
@@ -6036,6 +6432,7 @@ async function startServer(options) {
|
|
|
6036
6432
|
runner = startRunner({
|
|
6037
6433
|
cwd,
|
|
6038
6434
|
externallyAttached,
|
|
6435
|
+
onChange: () => live.nudge("requests"),
|
|
6039
6436
|
leglasCommand,
|
|
6040
6437
|
...options.codexAppServer === void 0 ? {} : { codexAppServer: options.codexAppServer },
|
|
6041
6438
|
...options.claudeAgentSession === void 0 ? {} : { claudeAgentSession: options.claudeAgentSession }
|
|
@@ -6047,7 +6444,9 @@ async function startServer(options) {
|
|
|
6047
6444
|
close: () => {
|
|
6048
6445
|
if (closePromise !== null)
|
|
6049
6446
|
return closePromise;
|
|
6050
|
-
|
|
6447
|
+
liveFiles.close();
|
|
6448
|
+
liveHealth.close();
|
|
6449
|
+
closePromise = Promise.all([runner.stop(), browserPool.close(), live.close()]).then(() => new Promise((done) => {
|
|
6051
6450
|
for (const socket of sockets)
|
|
6052
6451
|
socket.destroy();
|
|
6053
6452
|
sockets.clear();
|
|
@@ -6070,7 +6469,7 @@ function surfaceOf(url) {
|
|
|
6070
6469
|
return null;
|
|
6071
6470
|
}
|
|
6072
6471
|
function exportNameFor(to) {
|
|
6073
|
-
const stem =
|
|
6472
|
+
const stem = basename4(to, extname4(to));
|
|
6074
6473
|
return stem.split(/[^a-zA-Z0-9]+/).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
|
|
6075
6474
|
}
|
|
6076
6475
|
function planKeep(options) {
|
|
@@ -6159,7 +6558,7 @@ async function runInit(options, deps) {
|
|
|
6159
6558
|
// src/run-keep.ts
|
|
6160
6559
|
import { existsSync as existsSync4 } from "fs";
|
|
6161
6560
|
import { mkdir as mkdir9, readFile as readFile13, rm as rm5, writeFile as writeFile10 } from "fs/promises";
|
|
6162
|
-
import { dirname as
|
|
6561
|
+
import { dirname as dirname9, join as join14 } from "path";
|
|
6163
6562
|
|
|
6164
6563
|
// src/resolve-title.ts
|
|
6165
6564
|
function resolveOrExplain(input, titles, renames) {
|
|
@@ -6212,7 +6611,7 @@ async function runKeep(options, deps) {
|
|
|
6212
6611
|
return fail(`${plan.move.to} already exists. Choose another destination or move it aside.`);
|
|
6213
6612
|
}
|
|
6214
6613
|
const source = await readFile13(from, "utf8");
|
|
6215
|
-
await mkdir9(
|
|
6614
|
+
await mkdir9(dirname9(to), { recursive: true });
|
|
6216
6615
|
await writeFile10(to, renameExport(source, plan.exportName), "utf8");
|
|
6217
6616
|
await rm5(join14(options.cwd, plan.removeDir), { recursive: true, force: true });
|
|
6218
6617
|
const dropped = await dropLocalPreviews(options.cwd, plan.dropTitles);
|
|
@@ -6250,7 +6649,7 @@ async function runKeep(options, deps) {
|
|
|
6250
6649
|
// src/run-new.ts
|
|
6251
6650
|
import { existsSync as existsSync5 } from "fs";
|
|
6252
6651
|
import { mkdir as mkdir10, readFile as readFile14, writeFile as writeFile11 } from "fs/promises";
|
|
6253
|
-
import { dirname as
|
|
6652
|
+
import { dirname as dirname10, join as join15 } from "path";
|
|
6254
6653
|
async function readIfPresent2(path) {
|
|
6255
6654
|
try {
|
|
6256
6655
|
return await readFile14(path, "utf8");
|
|
@@ -6303,7 +6702,7 @@ async function runNew(options, deps) {
|
|
|
6303
6702
|
const written = [];
|
|
6304
6703
|
for (const write2 of plan.writes) {
|
|
6305
6704
|
const target = join15(options.cwd, write2.path);
|
|
6306
|
-
await mkdir10(
|
|
6705
|
+
await mkdir10(dirname10(target), { recursive: true });
|
|
6307
6706
|
await writeFile11(target, write2.contents, "utf8");
|
|
6308
6707
|
written.push(write2.path);
|
|
6309
6708
|
}
|
|
@@ -6664,7 +7063,7 @@ async function runShow(options, deps) {
|
|
|
6664
7063
|
// src/run-watch.ts
|
|
6665
7064
|
import { spawn as spawn3 } from "child_process";
|
|
6666
7065
|
import { mkdir as mkdir11, readFile as readFile16, writeFile as writeFile13 } from "fs/promises";
|
|
6667
|
-
import { dirname as
|
|
7066
|
+
import { dirname as dirname11, join as join17 } from "path";
|
|
6668
7067
|
var POLL_MS2 = 2e3;
|
|
6669
7068
|
var HEARTBEAT_TIMEOUT_MS = 1e3;
|
|
6670
7069
|
async function saveTemplate(cwd, run4) {
|
|
@@ -6678,7 +7077,7 @@ async function saveTemplate(cwd, run4) {
|
|
|
6678
7077
|
} catch {
|
|
6679
7078
|
}
|
|
6680
7079
|
config.run = run4;
|
|
6681
|
-
await mkdir11(
|
|
7080
|
+
await mkdir11(dirname11(path), { recursive: true });
|
|
6682
7081
|
await writeFile13(path, `${JSON.stringify(config, null, 2)}
|
|
6683
7082
|
`, "utf8");
|
|
6684
7083
|
}
|
|
@@ -6852,13 +7251,13 @@ async function runClassify(options, deps) {
|
|
|
6852
7251
|
import { existsSync as existsSync6 } from "fs";
|
|
6853
7252
|
import { realpath as realpath4 } from "fs/promises";
|
|
6854
7253
|
import { createRequire } from "module";
|
|
6855
|
-
import { basename as
|
|
7254
|
+
import { basename as basename6, dirname as dirname12, join as join19, relative as relative5, resolve as resolve4 } from "path";
|
|
6856
7255
|
import { fileURLToPath } from "url";
|
|
6857
7256
|
|
|
6858
7257
|
// src/dev-server-owner.ts
|
|
6859
7258
|
import { execFile as execFile2 } from "child_process";
|
|
6860
7259
|
import { realpath as realpath3 } from "fs/promises";
|
|
6861
|
-
import { basename as
|
|
7260
|
+
import { basename as basename5, isAbsolute as isAbsolute2, relative as relative4, resolve as resolve3 } from "path";
|
|
6862
7261
|
import { promisify as promisify2 } from "util";
|
|
6863
7262
|
var run2 = promisify2(execFile2);
|
|
6864
7263
|
var LOCAL_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1", "[::1]"]);
|
|
@@ -6931,18 +7330,18 @@ function devServerOwnerWarning(origin, projectRoot, owners) {
|
|
|
6931
7330
|
const port = localDevServerPort(origin);
|
|
6932
7331
|
if (port === null || owners.length === 0) return null;
|
|
6933
7332
|
if (owners.some((owner2) => isInside(projectRoot, owner2.cwd))) return null;
|
|
6934
|
-
const owner =
|
|
6935
|
-
const project =
|
|
7333
|
+
const owner = basename5(resolve3(owners[0]?.cwd ?? "")) || "another project";
|
|
7334
|
+
const project = basename5(resolve3(projectRoot)) || "this project";
|
|
6936
7335
|
return `Port ${port} appears to be served from ${owner}, outside this project (${project}). Check devServer in your Leglas config or use --user-port.`;
|
|
6937
7336
|
}
|
|
6938
7337
|
|
|
6939
7338
|
// src/run.ts
|
|
6940
7339
|
function findShellDir() {
|
|
6941
|
-
const bundled = join19(
|
|
7340
|
+
const bundled = join19(dirname12(fileURLToPath(import.meta.url)), "shell");
|
|
6942
7341
|
if (existsSync6(join19(bundled, "index.html"))) return bundled;
|
|
6943
7342
|
try {
|
|
6944
7343
|
const require2 = createRequire(import.meta.url);
|
|
6945
|
-
return
|
|
7344
|
+
return dirname12(require2.resolve("@leglas/shell/dist/index.html"));
|
|
6946
7345
|
} catch {
|
|
6947
7346
|
return null;
|
|
6948
7347
|
}
|
|
@@ -6953,7 +7352,7 @@ function shellWord(value) {
|
|
|
6953
7352
|
return `'${value.replaceAll("'", `'\\''`)}'`;
|
|
6954
7353
|
}
|
|
6955
7354
|
function embeddedLeglasCommand() {
|
|
6956
|
-
const entry = join19(
|
|
7355
|
+
const entry = join19(dirname12(fileURLToPath(import.meta.url)), "bin.js");
|
|
6957
7356
|
if (!existsSync6(entry)) return "npx -y leglas";
|
|
6958
7357
|
return [process.execPath, entry].map(shellWord).join(" ");
|
|
6959
7358
|
}
|
|
@@ -6997,10 +7396,10 @@ async function run3(options, deps) {
|
|
|
6997
7396
|
for (let suffix = 2; fileMounts.has(slug); suffix += 1) {
|
|
6998
7397
|
slug = `${worktreeSlug(preview.title) || "file"}-${suffix}`;
|
|
6999
7398
|
}
|
|
7000
|
-
fileMounts.set(slug,
|
|
7399
|
+
fileMounts.set(slug, dirname12(absolute));
|
|
7001
7400
|
previews.push({
|
|
7002
7401
|
...preview,
|
|
7003
|
-
url: `${FILES_PREFIX}/${slug}/${encodeURIComponent(
|
|
7402
|
+
url: `${FILES_PREFIX}/${slug}/${encodeURIComponent(basename6(absolute))}`
|
|
7004
7403
|
});
|
|
7005
7404
|
continue;
|
|
7006
7405
|
}
|
|
@@ -7030,8 +7429,8 @@ async function run3(options, deps) {
|
|
|
7030
7429
|
}
|
|
7031
7430
|
const config = merged === null ? null : { ...merged, previews };
|
|
7032
7431
|
const configWarnings = [];
|
|
7033
|
-
const projectRoot = await realpath4(loaded.path === null ? options.cwd :
|
|
7034
|
-
() => resolve4(loaded.path === null ? options.cwd :
|
|
7432
|
+
const projectRoot = await realpath4(loaded.path === null ? options.cwd : dirname12(loaded.path)).catch(
|
|
7433
|
+
() => resolve4(loaded.path === null ? options.cwd : dirname12(loaded.path))
|
|
7035
7434
|
);
|
|
7036
7435
|
const ownerWarning = needsApp && app === null ? inspectLocalDevServer(devServer).then((owners) => devServerOwnerWarning(devServer, projectRoot, owners)).catch(() => null) : Promise.resolve(null);
|
|
7037
7436
|
const serverPromise = startServer({
|