leglas 0.7.0 → 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 +438 -47
- package/dist/index.js +438 -47
- package/dist/shell/assets/{index-CuUL6FpW.js → index-DbONqSbP.js} +8 -8
- package/dist/shell/index.html +1 -1
- package/package.json +3 -3
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))) {
|
|
@@ -5568,10 +5960,8 @@ async function startServer(options) {
|
|
|
5568
5960
|
let body = "";
|
|
5569
5961
|
req.on("data", (chunk) => body += chunk);
|
|
5570
5962
|
return void req.on("end", async () => {
|
|
5571
|
-
|
|
5572
|
-
|
|
5573
|
-
parsed = JSON.parse(body || "{}");
|
|
5574
|
-
} catch {
|
|
5963
|
+
const parsed = jsonBody(body);
|
|
5964
|
+
if (parsed === null) {
|
|
5575
5965
|
return sendJson(res, 400, { ok: false, error: "Body must be JSON." });
|
|
5576
5966
|
}
|
|
5577
5967
|
if (typeof parsed.title !== "string" || parsed.title === "") {
|
|
@@ -5895,16 +6285,10 @@ async function startServer(options) {
|
|
|
5895
6285
|
let body = "";
|
|
5896
6286
|
req.on("data", (chunk) => body += chunk);
|
|
5897
6287
|
return void req.on("end", async () => {
|
|
5898
|
-
|
|
5899
|
-
|
|
5900
|
-
read = JSON.parse(body || "{}");
|
|
5901
|
-
} catch {
|
|
6288
|
+
const parsed = jsonBody(body);
|
|
6289
|
+
if (parsed === null) {
|
|
5902
6290
|
return sendJson(res, 400, { ok: false, error: "Body must be JSON." });
|
|
5903
6291
|
}
|
|
5904
|
-
if (typeof read !== "object" || read === null || Array.isArray(read)) {
|
|
5905
|
-
return sendJson(res, 400, { ok: false, error: "Body must be a JSON object." });
|
|
5906
|
-
}
|
|
5907
|
-
const parsed = read;
|
|
5908
6292
|
if (typeof parsed.id !== "string" || parsed.id === "") {
|
|
5909
6293
|
return sendJson(res, 400, { ok: false, error: "Body needs the note to reword." });
|
|
5910
6294
|
}
|
|
@@ -6027,12 +6411,16 @@ async function startServer(options) {
|
|
|
6027
6411
|
socket.once("close", () => sockets.delete(socket));
|
|
6028
6412
|
});
|
|
6029
6413
|
server.on("upgrade", (req, socket, head) => {
|
|
6414
|
+
if (live.upgrade(req, socket, head))
|
|
6415
|
+
return;
|
|
6030
6416
|
const path = (req.url ?? "/").split("?")[0] ?? "/";
|
|
6031
6417
|
if (path.startsWith(`${LEGLAS_PREFIX}/`))
|
|
6032
6418
|
return socket.destroy();
|
|
6033
6419
|
proxy.upgrade(req, socket, head);
|
|
6034
6420
|
});
|
|
6035
6421
|
const port = await bind(server, options.port ?? DEFAULT_PORT);
|
|
6422
|
+
const liveFiles = watchLiveFiles(cwd, bootConfigPath, live);
|
|
6423
|
+
const liveHealth = watchHealth(target, live);
|
|
6036
6424
|
await pruneCaptures(cwd, (await readRequests(cwd).catch(() => [])).map((request) => request.id)).catch(() => {
|
|
6037
6425
|
});
|
|
6038
6426
|
await writeServerInfo(cwd, {
|
|
@@ -6044,6 +6432,7 @@ async function startServer(options) {
|
|
|
6044
6432
|
runner = startRunner({
|
|
6045
6433
|
cwd,
|
|
6046
6434
|
externallyAttached,
|
|
6435
|
+
onChange: () => live.nudge("requests"),
|
|
6047
6436
|
leglasCommand,
|
|
6048
6437
|
...options.codexAppServer === void 0 ? {} : { codexAppServer: options.codexAppServer },
|
|
6049
6438
|
...options.claudeAgentSession === void 0 ? {} : { claudeAgentSession: options.claudeAgentSession }
|
|
@@ -6055,7 +6444,9 @@ async function startServer(options) {
|
|
|
6055
6444
|
close: () => {
|
|
6056
6445
|
if (closePromise !== null)
|
|
6057
6446
|
return closePromise;
|
|
6058
|
-
|
|
6447
|
+
liveFiles.close();
|
|
6448
|
+
liveHealth.close();
|
|
6449
|
+
closePromise = Promise.all([runner.stop(), browserPool.close(), live.close()]).then(() => new Promise((done) => {
|
|
6059
6450
|
for (const socket of sockets)
|
|
6060
6451
|
socket.destroy();
|
|
6061
6452
|
sockets.clear();
|
|
@@ -6078,7 +6469,7 @@ function surfaceOf(url) {
|
|
|
6078
6469
|
return null;
|
|
6079
6470
|
}
|
|
6080
6471
|
function exportNameFor(to) {
|
|
6081
|
-
const stem =
|
|
6472
|
+
const stem = basename4(to, extname4(to));
|
|
6082
6473
|
return stem.split(/[^a-zA-Z0-9]+/).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
|
|
6083
6474
|
}
|
|
6084
6475
|
function planKeep(options) {
|
|
@@ -6167,7 +6558,7 @@ async function runInit(options, deps) {
|
|
|
6167
6558
|
// src/run-keep.ts
|
|
6168
6559
|
import { existsSync as existsSync4 } from "fs";
|
|
6169
6560
|
import { mkdir as mkdir9, readFile as readFile13, rm as rm5, writeFile as writeFile10 } from "fs/promises";
|
|
6170
|
-
import { dirname as
|
|
6561
|
+
import { dirname as dirname9, join as join14 } from "path";
|
|
6171
6562
|
|
|
6172
6563
|
// src/resolve-title.ts
|
|
6173
6564
|
function resolveOrExplain(input, titles, renames) {
|
|
@@ -6220,7 +6611,7 @@ async function runKeep(options, deps) {
|
|
|
6220
6611
|
return fail(`${plan.move.to} already exists. Choose another destination or move it aside.`);
|
|
6221
6612
|
}
|
|
6222
6613
|
const source = await readFile13(from, "utf8");
|
|
6223
|
-
await mkdir9(
|
|
6614
|
+
await mkdir9(dirname9(to), { recursive: true });
|
|
6224
6615
|
await writeFile10(to, renameExport(source, plan.exportName), "utf8");
|
|
6225
6616
|
await rm5(join14(options.cwd, plan.removeDir), { recursive: true, force: true });
|
|
6226
6617
|
const dropped = await dropLocalPreviews(options.cwd, plan.dropTitles);
|
|
@@ -6258,7 +6649,7 @@ async function runKeep(options, deps) {
|
|
|
6258
6649
|
// src/run-new.ts
|
|
6259
6650
|
import { existsSync as existsSync5 } from "fs";
|
|
6260
6651
|
import { mkdir as mkdir10, readFile as readFile14, writeFile as writeFile11 } from "fs/promises";
|
|
6261
|
-
import { dirname as
|
|
6652
|
+
import { dirname as dirname10, join as join15 } from "path";
|
|
6262
6653
|
async function readIfPresent2(path) {
|
|
6263
6654
|
try {
|
|
6264
6655
|
return await readFile14(path, "utf8");
|
|
@@ -6311,7 +6702,7 @@ async function runNew(options, deps) {
|
|
|
6311
6702
|
const written = [];
|
|
6312
6703
|
for (const write2 of plan.writes) {
|
|
6313
6704
|
const target = join15(options.cwd, write2.path);
|
|
6314
|
-
await mkdir10(
|
|
6705
|
+
await mkdir10(dirname10(target), { recursive: true });
|
|
6315
6706
|
await writeFile11(target, write2.contents, "utf8");
|
|
6316
6707
|
written.push(write2.path);
|
|
6317
6708
|
}
|
|
@@ -6672,7 +7063,7 @@ async function runShow(options, deps) {
|
|
|
6672
7063
|
// src/run-watch.ts
|
|
6673
7064
|
import { spawn as spawn3 } from "child_process";
|
|
6674
7065
|
import { mkdir as mkdir11, readFile as readFile16, writeFile as writeFile13 } from "fs/promises";
|
|
6675
|
-
import { dirname as
|
|
7066
|
+
import { dirname as dirname11, join as join17 } from "path";
|
|
6676
7067
|
var POLL_MS2 = 2e3;
|
|
6677
7068
|
var HEARTBEAT_TIMEOUT_MS = 1e3;
|
|
6678
7069
|
async function saveTemplate(cwd, run4) {
|
|
@@ -6686,7 +7077,7 @@ async function saveTemplate(cwd, run4) {
|
|
|
6686
7077
|
} catch {
|
|
6687
7078
|
}
|
|
6688
7079
|
config.run = run4;
|
|
6689
|
-
await mkdir11(
|
|
7080
|
+
await mkdir11(dirname11(path), { recursive: true });
|
|
6690
7081
|
await writeFile13(path, `${JSON.stringify(config, null, 2)}
|
|
6691
7082
|
`, "utf8");
|
|
6692
7083
|
}
|
|
@@ -6860,13 +7251,13 @@ async function runClassify(options, deps) {
|
|
|
6860
7251
|
import { existsSync as existsSync6 } from "fs";
|
|
6861
7252
|
import { realpath as realpath4 } from "fs/promises";
|
|
6862
7253
|
import { createRequire } from "module";
|
|
6863
|
-
import { basename as
|
|
7254
|
+
import { basename as basename6, dirname as dirname12, join as join19, relative as relative5, resolve as resolve4 } from "path";
|
|
6864
7255
|
import { fileURLToPath } from "url";
|
|
6865
7256
|
|
|
6866
7257
|
// src/dev-server-owner.ts
|
|
6867
7258
|
import { execFile as execFile2 } from "child_process";
|
|
6868
7259
|
import { realpath as realpath3 } from "fs/promises";
|
|
6869
|
-
import { basename as
|
|
7260
|
+
import { basename as basename5, isAbsolute as isAbsolute2, relative as relative4, resolve as resolve3 } from "path";
|
|
6870
7261
|
import { promisify as promisify2 } from "util";
|
|
6871
7262
|
var run2 = promisify2(execFile2);
|
|
6872
7263
|
var LOCAL_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1", "[::1]"]);
|
|
@@ -6939,18 +7330,18 @@ function devServerOwnerWarning(origin, projectRoot, owners) {
|
|
|
6939
7330
|
const port = localDevServerPort(origin);
|
|
6940
7331
|
if (port === null || owners.length === 0) return null;
|
|
6941
7332
|
if (owners.some((owner2) => isInside(projectRoot, owner2.cwd))) return null;
|
|
6942
|
-
const owner =
|
|
6943
|
-
const project =
|
|
7333
|
+
const owner = basename5(resolve3(owners[0]?.cwd ?? "")) || "another project";
|
|
7334
|
+
const project = basename5(resolve3(projectRoot)) || "this project";
|
|
6944
7335
|
return `Port ${port} appears to be served from ${owner}, outside this project (${project}). Check devServer in your Leglas config or use --user-port.`;
|
|
6945
7336
|
}
|
|
6946
7337
|
|
|
6947
7338
|
// src/run.ts
|
|
6948
7339
|
function findShellDir() {
|
|
6949
|
-
const bundled = join19(
|
|
7340
|
+
const bundled = join19(dirname12(fileURLToPath(import.meta.url)), "shell");
|
|
6950
7341
|
if (existsSync6(join19(bundled, "index.html"))) return bundled;
|
|
6951
7342
|
try {
|
|
6952
7343
|
const require2 = createRequire(import.meta.url);
|
|
6953
|
-
return
|
|
7344
|
+
return dirname12(require2.resolve("@leglas/shell/dist/index.html"));
|
|
6954
7345
|
} catch {
|
|
6955
7346
|
return null;
|
|
6956
7347
|
}
|
|
@@ -6961,7 +7352,7 @@ function shellWord(value) {
|
|
|
6961
7352
|
return `'${value.replaceAll("'", `'\\''`)}'`;
|
|
6962
7353
|
}
|
|
6963
7354
|
function embeddedLeglasCommand() {
|
|
6964
|
-
const entry = join19(
|
|
7355
|
+
const entry = join19(dirname12(fileURLToPath(import.meta.url)), "bin.js");
|
|
6965
7356
|
if (!existsSync6(entry)) return "npx -y leglas";
|
|
6966
7357
|
return [process.execPath, entry].map(shellWord).join(" ");
|
|
6967
7358
|
}
|
|
@@ -7005,10 +7396,10 @@ async function run3(options, deps) {
|
|
|
7005
7396
|
for (let suffix = 2; fileMounts.has(slug); suffix += 1) {
|
|
7006
7397
|
slug = `${worktreeSlug(preview.title) || "file"}-${suffix}`;
|
|
7007
7398
|
}
|
|
7008
|
-
fileMounts.set(slug,
|
|
7399
|
+
fileMounts.set(slug, dirname12(absolute));
|
|
7009
7400
|
previews.push({
|
|
7010
7401
|
...preview,
|
|
7011
|
-
url: `${FILES_PREFIX}/${slug}/${encodeURIComponent(
|
|
7402
|
+
url: `${FILES_PREFIX}/${slug}/${encodeURIComponent(basename6(absolute))}`
|
|
7012
7403
|
});
|
|
7013
7404
|
continue;
|
|
7014
7405
|
}
|
|
@@ -7038,8 +7429,8 @@ async function run3(options, deps) {
|
|
|
7038
7429
|
}
|
|
7039
7430
|
const config = merged === null ? null : { ...merged, previews };
|
|
7040
7431
|
const configWarnings = [];
|
|
7041
|
-
const projectRoot = await realpath4(loaded.path === null ? options.cwd :
|
|
7042
|
-
() => 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))
|
|
7043
7434
|
);
|
|
7044
7435
|
const ownerWarning = needsApp && app === null ? inspectLocalDevServer(devServer).then((owners) => devServerOwnerWarning(devServer, projectRoot, owners)).catch(() => null) : Promise.resolve(null);
|
|
7045
7436
|
const serverPromise = startServer({
|