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/bin.js
CHANGED
|
@@ -1179,8 +1179,8 @@ function findConfigFile(startDir) {
|
|
|
1179
1179
|
const { root } = parse(startDir);
|
|
1180
1180
|
let dir = startDir;
|
|
1181
1181
|
for (; ; ) {
|
|
1182
|
-
for (const
|
|
1183
|
-
const candidate = join2(dir,
|
|
1182
|
+
for (const basename7 of CONFIG_BASENAMES) {
|
|
1183
|
+
const candidate = join2(dir, basename7);
|
|
1184
1184
|
if (existsSync(candidate))
|
|
1185
1185
|
return candidate;
|
|
1186
1186
|
}
|
|
@@ -4226,6 +4226,10 @@ function startRunner(options) {
|
|
|
4226
4226
|
stopping: false,
|
|
4227
4227
|
waiting: null
|
|
4228
4228
|
};
|
|
4229
|
+
const setState = (next) => {
|
|
4230
|
+
state = typeof next === "function" ? next(state) : next;
|
|
4231
|
+
options.onChange?.();
|
|
4232
|
+
};
|
|
4229
4233
|
let stopped = false;
|
|
4230
4234
|
let ticking = null;
|
|
4231
4235
|
let pendingNudges = 0;
|
|
@@ -4277,7 +4281,7 @@ function startRunner(options) {
|
|
|
4277
4281
|
return session !== void 0 && session.turns < SESSION_TURNS_CAP ? session.id : null;
|
|
4278
4282
|
};
|
|
4279
4283
|
const idle = () => {
|
|
4280
|
-
|
|
4284
|
+
setState({
|
|
4281
4285
|
running: false,
|
|
4282
4286
|
requestId: null,
|
|
4283
4287
|
agent: null,
|
|
@@ -4285,7 +4289,7 @@ function startRunner(options) {
|
|
|
4285
4289
|
startedAt: null,
|
|
4286
4290
|
stopping: false,
|
|
4287
4291
|
waiting: null
|
|
4288
|
-
};
|
|
4292
|
+
});
|
|
4289
4293
|
};
|
|
4290
4294
|
const rememberLine = (lines2, line) => {
|
|
4291
4295
|
lines2.push(line);
|
|
@@ -4393,14 +4397,15 @@ function startRunner(options) {
|
|
|
4393
4397
|
if (retry !== null) {
|
|
4394
4398
|
observed.retry = retry;
|
|
4395
4399
|
if (active === current)
|
|
4396
|
-
|
|
4400
|
+
setState((value) => ({ ...value, waiting: retry }));
|
|
4397
4401
|
}
|
|
4398
4402
|
const activity = activityFrom(resolved.agent, line, options.cwd);
|
|
4399
4403
|
if (activity !== null) {
|
|
4400
4404
|
if (activity.startsWith("editing"))
|
|
4401
4405
|
observed.edited = true;
|
|
4402
|
-
if (active === current)
|
|
4403
|
-
|
|
4406
|
+
if (active === current) {
|
|
4407
|
+
setState((value) => ({ ...value, activity, waiting: null }));
|
|
4408
|
+
}
|
|
4404
4409
|
}
|
|
4405
4410
|
});
|
|
4406
4411
|
const stderrFlush = lineReader(child.stderr, (line) => rememberLine(lines2, line));
|
|
@@ -4447,7 +4452,7 @@ function startRunner(options) {
|
|
|
4447
4452
|
await reportFailure(request, classifyFailure({ agent: resolved.name, error: "stopped by shutdown" }), []);
|
|
4448
4453
|
return;
|
|
4449
4454
|
}
|
|
4450
|
-
|
|
4455
|
+
setState({
|
|
4451
4456
|
running: true,
|
|
4452
4457
|
requestId: request.id,
|
|
4453
4458
|
agent: resolved.name,
|
|
@@ -4455,7 +4460,7 @@ function startRunner(options) {
|
|
|
4455
4460
|
startedAt: Date.now(),
|
|
4456
4461
|
stopping: false,
|
|
4457
4462
|
waiting: null
|
|
4458
|
-
};
|
|
4463
|
+
});
|
|
4459
4464
|
const observed = {
|
|
4460
4465
|
sessionId: null,
|
|
4461
4466
|
edited: false,
|
|
@@ -4486,7 +4491,7 @@ function startRunner(options) {
|
|
|
4486
4491
|
resolved = cold;
|
|
4487
4492
|
observed.sessionId = null;
|
|
4488
4493
|
observed.retry = null;
|
|
4489
|
-
|
|
4494
|
+
setState((value) => ({ ...value, activity: null, waiting: null }));
|
|
4490
4495
|
outcome = await runChild(request, resolved, lines2, observed);
|
|
4491
4496
|
failure = verdict();
|
|
4492
4497
|
}
|
|
@@ -4560,7 +4565,7 @@ function startRunner(options) {
|
|
|
4560
4565
|
const current = active;
|
|
4561
4566
|
current.cancelled = true;
|
|
4562
4567
|
failed.add(current.requestId);
|
|
4563
|
-
|
|
4568
|
+
setState((value) => ({ ...value, stopping: true, waiting: null }));
|
|
4564
4569
|
current.controller.abort();
|
|
4565
4570
|
try {
|
|
4566
4571
|
current.child?.kill("SIGTERM");
|
|
@@ -4603,6 +4608,193 @@ function startRunner(options) {
|
|
|
4603
4608
|
};
|
|
4604
4609
|
}
|
|
4605
4610
|
|
|
4611
|
+
// ../server/dist/live.js
|
|
4612
|
+
import { createHash } from "crypto";
|
|
4613
|
+
var LIVE_PATH = "/leglas/api/live";
|
|
4614
|
+
var WEBSOCKET_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
|
4615
|
+
function encodeFrame(opcode, payload) {
|
|
4616
|
+
const body = typeof payload === "string" ? Buffer.from(payload) : payload;
|
|
4617
|
+
let header;
|
|
4618
|
+
if (body.length < 126) {
|
|
4619
|
+
header = Buffer.allocUnsafe(2);
|
|
4620
|
+
header[1] = body.length;
|
|
4621
|
+
} else if (body.length <= 65535) {
|
|
4622
|
+
header = Buffer.allocUnsafe(4);
|
|
4623
|
+
header[1] = 126;
|
|
4624
|
+
header.writeUInt16BE(body.length, 2);
|
|
4625
|
+
} else {
|
|
4626
|
+
header = Buffer.allocUnsafe(10);
|
|
4627
|
+
header[1] = 127;
|
|
4628
|
+
header.writeBigUInt64BE(BigInt(body.length), 2);
|
|
4629
|
+
}
|
|
4630
|
+
header[0] = 128 | opcode & 15;
|
|
4631
|
+
return Buffer.concat([header, body], header.length + body.length);
|
|
4632
|
+
}
|
|
4633
|
+
var LIVE_DEBOUNCE_MS = 50;
|
|
4634
|
+
function createCoalescer(emit, options = {}) {
|
|
4635
|
+
const windowMs = options.windowMs ?? LIVE_DEBOUNCE_MS;
|
|
4636
|
+
const setLater = options.setTimeout ?? ((callback, ms) => {
|
|
4637
|
+
const timer = setTimeout(callback, ms);
|
|
4638
|
+
timer.unref?.();
|
|
4639
|
+
return timer;
|
|
4640
|
+
});
|
|
4641
|
+
const clearLater = options.clearTimeout ?? ((handle) => clearTimeout(handle));
|
|
4642
|
+
const pending = /* @__PURE__ */ new Map();
|
|
4643
|
+
let closed = false;
|
|
4644
|
+
return {
|
|
4645
|
+
schedule(change) {
|
|
4646
|
+
if (closed)
|
|
4647
|
+
return;
|
|
4648
|
+
const waiting = pending.get(change);
|
|
4649
|
+
if (waiting !== void 0)
|
|
4650
|
+
clearLater(waiting);
|
|
4651
|
+
pending.set(change, setLater(() => {
|
|
4652
|
+
pending.delete(change);
|
|
4653
|
+
if (!closed)
|
|
4654
|
+
emit(change);
|
|
4655
|
+
}, windowMs));
|
|
4656
|
+
},
|
|
4657
|
+
close() {
|
|
4658
|
+
closed = true;
|
|
4659
|
+
for (const handle of pending.values())
|
|
4660
|
+
clearLater(handle);
|
|
4661
|
+
pending.clear();
|
|
4662
|
+
}
|
|
4663
|
+
};
|
|
4664
|
+
}
|
|
4665
|
+
function createLiveHub(_options = {}) {
|
|
4666
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
4667
|
+
const drop = (listener) => {
|
|
4668
|
+
listeners.delete(listener);
|
|
4669
|
+
};
|
|
4670
|
+
const write2 = (listener, opcode, payload) => {
|
|
4671
|
+
if (listener.socket.destroyed || !listener.socket.writable) {
|
|
4672
|
+
drop(listener);
|
|
4673
|
+
return false;
|
|
4674
|
+
}
|
|
4675
|
+
try {
|
|
4676
|
+
listener.socket.write(encodeFrame(opcode, payload));
|
|
4677
|
+
return true;
|
|
4678
|
+
} catch {
|
|
4679
|
+
drop(listener);
|
|
4680
|
+
listener.socket.destroy();
|
|
4681
|
+
return false;
|
|
4682
|
+
}
|
|
4683
|
+
};
|
|
4684
|
+
const read = (listener, chunk) => {
|
|
4685
|
+
const incoming = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
4686
|
+
listener.buffered = listener.buffered.length === 0 ? incoming : Buffer.concat([listener.buffered, incoming]);
|
|
4687
|
+
while (listener.buffered.length >= 2) {
|
|
4688
|
+
const first = listener.buffered[0] ?? 0;
|
|
4689
|
+
const second = listener.buffered[1] ?? 0;
|
|
4690
|
+
let length = second & 127;
|
|
4691
|
+
let offset = 2;
|
|
4692
|
+
if (length === 126) {
|
|
4693
|
+
if (listener.buffered.length < 4)
|
|
4694
|
+
return;
|
|
4695
|
+
length = listener.buffered.readUInt16BE(2);
|
|
4696
|
+
offset = 4;
|
|
4697
|
+
} else if (length === 127) {
|
|
4698
|
+
if (listener.buffered.length < 10)
|
|
4699
|
+
return;
|
|
4700
|
+
const wide = listener.buffered.readBigUInt64BE(2);
|
|
4701
|
+
if (wide > BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
4702
|
+
drop(listener);
|
|
4703
|
+
listener.socket.destroy();
|
|
4704
|
+
return;
|
|
4705
|
+
}
|
|
4706
|
+
length = Number(wide);
|
|
4707
|
+
offset = 10;
|
|
4708
|
+
}
|
|
4709
|
+
const masked = (second & 128) !== 0;
|
|
4710
|
+
if (!masked) {
|
|
4711
|
+
drop(listener);
|
|
4712
|
+
listener.socket.destroy();
|
|
4713
|
+
return;
|
|
4714
|
+
}
|
|
4715
|
+
if (listener.buffered.length < offset + 4 + length)
|
|
4716
|
+
return;
|
|
4717
|
+
const mask = listener.buffered.subarray(offset, offset + 4);
|
|
4718
|
+
offset += 4;
|
|
4719
|
+
const payload = Buffer.from(listener.buffered.subarray(offset, offset + length));
|
|
4720
|
+
for (let index = 0; index < payload.length; index += 1) {
|
|
4721
|
+
payload[index] = (payload[index] ?? 0) ^ (mask[index % 4] ?? 0);
|
|
4722
|
+
}
|
|
4723
|
+
listener.buffered = listener.buffered.subarray(offset + length);
|
|
4724
|
+
const opcode = first & 15;
|
|
4725
|
+
if (opcode === 8) {
|
|
4726
|
+
write2(listener, 8, payload);
|
|
4727
|
+
drop(listener);
|
|
4728
|
+
listener.socket.destroy();
|
|
4729
|
+
return;
|
|
4730
|
+
}
|
|
4731
|
+
if (opcode === 9)
|
|
4732
|
+
write2(listener, 10, payload);
|
|
4733
|
+
}
|
|
4734
|
+
};
|
|
4735
|
+
return {
|
|
4736
|
+
nudge: (change) => {
|
|
4737
|
+
if (listeners.size === 0)
|
|
4738
|
+
return;
|
|
4739
|
+
const frame = encodeFrame(1, JSON.stringify({ changed: change }));
|
|
4740
|
+
for (const listener of [...listeners]) {
|
|
4741
|
+
if (listener.socket.destroyed || !listener.socket.writable) {
|
|
4742
|
+
drop(listener);
|
|
4743
|
+
continue;
|
|
4744
|
+
}
|
|
4745
|
+
try {
|
|
4746
|
+
listener.socket.write(frame);
|
|
4747
|
+
} catch {
|
|
4748
|
+
drop(listener);
|
|
4749
|
+
listener.socket.destroy();
|
|
4750
|
+
}
|
|
4751
|
+
}
|
|
4752
|
+
},
|
|
4753
|
+
upgrade: (req, socket, head) => {
|
|
4754
|
+
const path = (req.url ?? "/").split("?")[0] ?? "/";
|
|
4755
|
+
if (req.method !== "GET" || path !== LIVE_PATH)
|
|
4756
|
+
return false;
|
|
4757
|
+
const upgrade = req.headers.upgrade;
|
|
4758
|
+
const key = req.headers["sec-websocket-key"];
|
|
4759
|
+
if (typeof upgrade !== "string" || upgrade.toLowerCase() !== "websocket" || typeof key !== "string" || key.trim() === "") {
|
|
4760
|
+
socket.destroy();
|
|
4761
|
+
return false;
|
|
4762
|
+
}
|
|
4763
|
+
const accept = createHash("sha1").update(key + WEBSOCKET_GUID).digest("base64");
|
|
4764
|
+
try {
|
|
4765
|
+
socket.write(`HTTP/1.1 101 Switching Protocols\r
|
|
4766
|
+
Upgrade: websocket\r
|
|
4767
|
+
Connection: Upgrade\r
|
|
4768
|
+
Sec-WebSocket-Accept: ${accept}\r
|
|
4769
|
+
\r
|
|
4770
|
+
`);
|
|
4771
|
+
} catch {
|
|
4772
|
+
socket.destroy();
|
|
4773
|
+
return false;
|
|
4774
|
+
}
|
|
4775
|
+
const listener = { socket, buffered: Buffer.alloc(0) };
|
|
4776
|
+
listeners.add(listener);
|
|
4777
|
+
socket.on("data", (chunk) => read(listener, chunk));
|
|
4778
|
+
socket.once("error", () => drop(listener));
|
|
4779
|
+
socket.once("end", () => drop(listener));
|
|
4780
|
+
socket.once("close", () => drop(listener));
|
|
4781
|
+
if (head.length > 0)
|
|
4782
|
+
read(listener, head);
|
|
4783
|
+
return true;
|
|
4784
|
+
},
|
|
4785
|
+
close: async () => {
|
|
4786
|
+
for (const listener of [...listeners]) {
|
|
4787
|
+
write2(listener, 8, Buffer.alloc(0));
|
|
4788
|
+
drop(listener);
|
|
4789
|
+
listener.socket.destroy();
|
|
4790
|
+
}
|
|
4791
|
+
},
|
|
4792
|
+
get listening() {
|
|
4793
|
+
return listeners.size;
|
|
4794
|
+
}
|
|
4795
|
+
};
|
|
4796
|
+
}
|
|
4797
|
+
|
|
4606
4798
|
// ../server/dist/renames.js
|
|
4607
4799
|
import { mkdir as mkdir6, readFile as readFile10, writeFile as writeFile6 } from "fs/promises";
|
|
4608
4800
|
import { dirname as dirname6, join as join10 } from "path";
|
|
@@ -4636,11 +4828,11 @@ function resolveTitle(input, titles, renames) {
|
|
|
4636
4828
|
}
|
|
4637
4829
|
|
|
4638
4830
|
// ../server/dist/server.js
|
|
4639
|
-
import { createReadStream, existsSync as existsSync3, statSync } from "fs";
|
|
4831
|
+
import { createReadStream, existsSync as existsSync3, statSync, unwatchFile, watch as watchFs, watchFile } from "fs";
|
|
4640
4832
|
import { mkdir as mkdir8, readdir as readdir3, writeFile as writeFile8 } from "fs/promises";
|
|
4641
4833
|
import http2 from "http";
|
|
4642
4834
|
import net3 from "net";
|
|
4643
|
-
import { extname as extname3, join as join12, normalize, relative as relative3 } from "path";
|
|
4835
|
+
import { basename as basename3, dirname as dirname8, extname as extname3, join as join12, normalize, relative as relative3 } from "path";
|
|
4644
4836
|
|
|
4645
4837
|
// ../server/dist/server-info.js
|
|
4646
4838
|
import { lstat as lstat2, mkdir as mkdir7, readFile as readFile11, rename as rename2, rm as rm4, writeFile as writeFile7 } from "fs/promises";
|
|
@@ -4825,6 +5017,204 @@ function serveShellFile(res, shellDir, urlPath) {
|
|
|
4825
5017
|
const isRoot = relative6 === "" || relative6 === "." || relative6 === "/";
|
|
4826
5018
|
return serveFrom(res, shellDir, isRoot ? "index.html" : relative6);
|
|
4827
5019
|
}
|
|
5020
|
+
var HEALTH_PROBE_MS = 3e3;
|
|
5021
|
+
function fileStamp(path) {
|
|
5022
|
+
try {
|
|
5023
|
+
const stat5 = statSync(path);
|
|
5024
|
+
return `${stat5.dev}:${stat5.ino}:${stat5.size}:${stat5.mtimeMs}:${stat5.ctimeMs}`;
|
|
5025
|
+
} catch {
|
|
5026
|
+
return null;
|
|
5027
|
+
}
|
|
5028
|
+
}
|
|
5029
|
+
function watchLiveFiles(cwd, configPath, live) {
|
|
5030
|
+
const leglasDir = join12(cwd, ".leglas");
|
|
5031
|
+
const targets = [
|
|
5032
|
+
{ path: join12(cwd, LOCAL_PREVIEWS_PATH), change: "config" },
|
|
5033
|
+
{ path: join12(cwd, REQUESTS_PATH), change: "requests" },
|
|
5034
|
+
{ path: join12(cwd, ANNOTATIONS_PATH), change: "requests" }
|
|
5035
|
+
];
|
|
5036
|
+
const byName = new Map(targets.map((target) => [basename3(target.path), target]));
|
|
5037
|
+
const known = new Map(targets.map((target) => [target.path, fileStamp(target.path)]));
|
|
5038
|
+
const coalescer = createCoalescer((change) => live.nudge(change));
|
|
5039
|
+
const watchers = /* @__PURE__ */ new Set();
|
|
5040
|
+
const fallback = /* @__PURE__ */ new Map();
|
|
5041
|
+
let leglasWatcher = null;
|
|
5042
|
+
let retry = null;
|
|
5043
|
+
let closed = false;
|
|
5044
|
+
const nudgeSoon = (change) => coalescer.schedule(change);
|
|
5045
|
+
const scanLeglas = (notify) => {
|
|
5046
|
+
for (const target of targets) {
|
|
5047
|
+
const next = fileStamp(target.path);
|
|
5048
|
+
if (next === known.get(target.path))
|
|
5049
|
+
continue;
|
|
5050
|
+
known.set(target.path, next);
|
|
5051
|
+
if (notify)
|
|
5052
|
+
nudgeSoon(target.change);
|
|
5053
|
+
}
|
|
5054
|
+
};
|
|
5055
|
+
const fallbackWatch = (target) => {
|
|
5056
|
+
if (closed || fallback.has(target.path))
|
|
5057
|
+
return;
|
|
5058
|
+
const listener = () => {
|
|
5059
|
+
const next = fileStamp(target.path);
|
|
5060
|
+
if (next === known.get(target.path))
|
|
5061
|
+
return;
|
|
5062
|
+
known.set(target.path, next);
|
|
5063
|
+
nudgeSoon(target.change);
|
|
5064
|
+
};
|
|
5065
|
+
fallback.set(target.path, listener);
|
|
5066
|
+
watchFile(target.path, { persistent: false, interval: 250 }, listener);
|
|
5067
|
+
};
|
|
5068
|
+
const fallbackLeglas = () => {
|
|
5069
|
+
for (const target of targets)
|
|
5070
|
+
fallbackWatch(target);
|
|
5071
|
+
};
|
|
5072
|
+
const retryLeglas = () => {
|
|
5073
|
+
if (closed || retry !== null)
|
|
5074
|
+
return;
|
|
5075
|
+
retry = setTimeout(() => {
|
|
5076
|
+
retry = null;
|
|
5077
|
+
armLeglas(true);
|
|
5078
|
+
}, 250);
|
|
5079
|
+
retry.unref?.();
|
|
5080
|
+
};
|
|
5081
|
+
const armLeglas = (notify) => {
|
|
5082
|
+
if (closed)
|
|
5083
|
+
return;
|
|
5084
|
+
scanLeglas(notify);
|
|
5085
|
+
let directory = false;
|
|
5086
|
+
try {
|
|
5087
|
+
directory = statSync(leglasDir).isDirectory();
|
|
5088
|
+
} catch {
|
|
5089
|
+
directory = false;
|
|
5090
|
+
}
|
|
5091
|
+
if (!directory) {
|
|
5092
|
+
if (leglasWatcher !== null) {
|
|
5093
|
+
watchers.delete(leglasWatcher);
|
|
5094
|
+
leglasWatcher.close();
|
|
5095
|
+
leglasWatcher = null;
|
|
5096
|
+
}
|
|
5097
|
+
retryLeglas();
|
|
5098
|
+
return;
|
|
5099
|
+
}
|
|
5100
|
+
if (leglasWatcher !== null)
|
|
5101
|
+
return;
|
|
5102
|
+
try {
|
|
5103
|
+
const watcher = watchFs(leglasDir, { persistent: false }, (_event, filename) => {
|
|
5104
|
+
if (filename === null) {
|
|
5105
|
+
scanLeglas(true);
|
|
5106
|
+
return;
|
|
5107
|
+
}
|
|
5108
|
+
const name = Buffer.isBuffer(filename) ? filename.toString() : filename;
|
|
5109
|
+
const target = byName.get(name);
|
|
5110
|
+
if (target === void 0)
|
|
5111
|
+
return;
|
|
5112
|
+
known.set(target.path, fileStamp(target.path));
|
|
5113
|
+
nudgeSoon(target.change);
|
|
5114
|
+
});
|
|
5115
|
+
watcher.on("error", () => {
|
|
5116
|
+
if (leglasWatcher !== watcher)
|
|
5117
|
+
return;
|
|
5118
|
+
watchers.delete(watcher);
|
|
5119
|
+
watcher.close();
|
|
5120
|
+
leglasWatcher = null;
|
|
5121
|
+
fallbackLeglas();
|
|
5122
|
+
retryLeglas();
|
|
5123
|
+
});
|
|
5124
|
+
leglasWatcher = watcher;
|
|
5125
|
+
watchers.add(watcher);
|
|
5126
|
+
} catch {
|
|
5127
|
+
fallbackLeglas();
|
|
5128
|
+
retryLeglas();
|
|
5129
|
+
}
|
|
5130
|
+
};
|
|
5131
|
+
try {
|
|
5132
|
+
const watcher = watchFs(cwd, { persistent: false }, (_event, filename) => {
|
|
5133
|
+
const name = filename === null ? null : Buffer.isBuffer(filename) ? filename.toString() : filename;
|
|
5134
|
+
if (name === null || name === ".leglas")
|
|
5135
|
+
armLeglas(true);
|
|
5136
|
+
});
|
|
5137
|
+
watcher.on("error", () => {
|
|
5138
|
+
watchers.delete(watcher);
|
|
5139
|
+
watcher.close();
|
|
5140
|
+
fallbackLeglas();
|
|
5141
|
+
});
|
|
5142
|
+
watchers.add(watcher);
|
|
5143
|
+
} catch {
|
|
5144
|
+
fallbackLeglas();
|
|
5145
|
+
}
|
|
5146
|
+
if (configPath !== null) {
|
|
5147
|
+
try {
|
|
5148
|
+
const directory = dirname8(configPath);
|
|
5149
|
+
const name = basename3(configPath);
|
|
5150
|
+
const target = { path: configPath, change: "config" };
|
|
5151
|
+
known.set(configPath, fileStamp(configPath));
|
|
5152
|
+
const watcher = watchFs(directory, { persistent: false }, (_event, filename) => {
|
|
5153
|
+
const changed = filename === null ? null : Buffer.isBuffer(filename) ? filename.toString() : filename;
|
|
5154
|
+
if (changed === null || changed === name)
|
|
5155
|
+
nudgeSoon("config");
|
|
5156
|
+
});
|
|
5157
|
+
watcher.on("error", () => {
|
|
5158
|
+
watchers.delete(watcher);
|
|
5159
|
+
watcher.close();
|
|
5160
|
+
fallbackWatch(target);
|
|
5161
|
+
});
|
|
5162
|
+
watchers.add(watcher);
|
|
5163
|
+
} catch {
|
|
5164
|
+
fallbackWatch({ path: configPath, change: "config" });
|
|
5165
|
+
}
|
|
5166
|
+
}
|
|
5167
|
+
armLeglas(false);
|
|
5168
|
+
return {
|
|
5169
|
+
close: () => {
|
|
5170
|
+
if (closed)
|
|
5171
|
+
return;
|
|
5172
|
+
closed = true;
|
|
5173
|
+
if (retry !== null)
|
|
5174
|
+
clearTimeout(retry);
|
|
5175
|
+
coalescer.close();
|
|
5176
|
+
for (const watcher of watchers)
|
|
5177
|
+
watcher.close();
|
|
5178
|
+
watchers.clear();
|
|
5179
|
+
for (const [path, listener] of fallback)
|
|
5180
|
+
unwatchFile(path, listener);
|
|
5181
|
+
fallback.clear();
|
|
5182
|
+
leglasWatcher = null;
|
|
5183
|
+
}
|
|
5184
|
+
};
|
|
5185
|
+
}
|
|
5186
|
+
function watchHealth(target, live) {
|
|
5187
|
+
let previous = null;
|
|
5188
|
+
let probing = false;
|
|
5189
|
+
let closed = false;
|
|
5190
|
+
const timer = setInterval(() => {
|
|
5191
|
+
if (live.listening === 0) {
|
|
5192
|
+
previous = null;
|
|
5193
|
+
return;
|
|
5194
|
+
}
|
|
5195
|
+
if (probing)
|
|
5196
|
+
return;
|
|
5197
|
+
probing = true;
|
|
5198
|
+
void probe(target).then((reachable) => {
|
|
5199
|
+
if (closed || live.listening === 0) {
|
|
5200
|
+
previous = null;
|
|
5201
|
+
return;
|
|
5202
|
+
}
|
|
5203
|
+
if (previous !== null && previous !== reachable)
|
|
5204
|
+
live.nudge("health");
|
|
5205
|
+
previous = reachable;
|
|
5206
|
+
}).finally(() => {
|
|
5207
|
+
probing = false;
|
|
5208
|
+
});
|
|
5209
|
+
}, HEALTH_PROBE_MS);
|
|
5210
|
+
timer.unref?.();
|
|
5211
|
+
return {
|
|
5212
|
+
close: () => {
|
|
5213
|
+
closed = true;
|
|
5214
|
+
clearInterval(timer);
|
|
5215
|
+
}
|
|
5216
|
+
};
|
|
5217
|
+
}
|
|
4828
5218
|
function snapshotConfig(cwd) {
|
|
4829
5219
|
const path = findConfigFile(cwd);
|
|
4830
5220
|
if (path === null)
|
|
@@ -4895,12 +5285,14 @@ async function bind(server, requested) {
|
|
|
4895
5285
|
async function startServer(options) {
|
|
4896
5286
|
const { config, configErrors = [], configWarnings = [], shellDir = null, project = "", cwd = process.cwd(), leglasCommand = "npx -y leglas", fileMounts = /* @__PURE__ */ new Map(), detect = () => detectAgents() } = options;
|
|
4897
5287
|
const browserPool = options.pool ?? createBrowserPool();
|
|
5288
|
+
const live = options.live ?? createLiveHub();
|
|
4898
5289
|
if (options.pool === void 0) {
|
|
4899
5290
|
void reapOrphanedBrowsers().catch(() => {
|
|
4900
5291
|
});
|
|
4901
5292
|
}
|
|
4902
5293
|
const target = config?.devServer ?? "http://localhost:3000";
|
|
4903
5294
|
const proxy = createProxyHandler({ target });
|
|
5295
|
+
const bootConfigPath = findConfigFile(cwd);
|
|
4904
5296
|
const bootConfigSnapshot = snapshotConfig(cwd);
|
|
4905
5297
|
let lastSeen = null;
|
|
4906
5298
|
const externallyAttached = () => lastSeen !== null && Date.now() - lastSeen < ATTACHED_WINDOW_MS;
|
|
@@ -5125,14 +5517,14 @@ async function startServer(options) {
|
|
|
5125
5517
|
return sendJson(res, 400, { ok: false, error: "Unknown preview, or empty request." });
|
|
5126
5518
|
}
|
|
5127
5519
|
const intent = (parsed2.intent ?? "").trim();
|
|
5128
|
-
const
|
|
5520
|
+
const live2 = (await readRequests(cwd).catch(() => [])).filter((entry) => entry.status === "queued" || entry.status === "picked-up");
|
|
5129
5521
|
const sameNotes = (entry) => {
|
|
5130
5522
|
const before = [...entry.notes ?? []].sort().join(",");
|
|
5131
5523
|
return before === notes.map((note) => note.id).sort().join(",");
|
|
5132
5524
|
};
|
|
5133
5525
|
const compare = typeof parsed2.compare === "string" && parsed2.compare !== preview.title ? previews.find((entry) => entry.title === parsed2.compare) ?? null : null;
|
|
5134
5526
|
const sameContext = (entry) => (entry.compare ?? null) === (compare?.title ?? null) && [...entry.references ?? []].sort().join(",") === [...references].sort().join(",");
|
|
5135
|
-
if (
|
|
5527
|
+
if (live2.some((entry) => entry.title === preview.title && entry.intent === intent && // The same words in the other mode are not the same request:
|
|
5136
5528
|
// one forks the direction and the other rewrites it. Only a
|
|
5137
5529
|
// genuine repeat is refused.
|
|
5138
5530
|
(entry.mode ?? "replace") === mode && sameNotes(entry) && sameContext(entry))) {
|
|
@@ -5192,10 +5584,8 @@ async function startServer(options) {
|
|
|
5192
5584
|
let body = "";
|
|
5193
5585
|
req.on("data", (chunk) => body += chunk);
|
|
5194
5586
|
return void req.on("end", async () => {
|
|
5195
|
-
|
|
5196
|
-
|
|
5197
|
-
parsed2 = JSON.parse(body || "{}");
|
|
5198
|
-
} catch {
|
|
5587
|
+
const parsed2 = jsonBody(body);
|
|
5588
|
+
if (parsed2 === null) {
|
|
5199
5589
|
return sendJson(res, 400, { ok: false, error: "Body must be JSON." });
|
|
5200
5590
|
}
|
|
5201
5591
|
if (typeof parsed2.title !== "string" || parsed2.title === "") {
|
|
@@ -5519,16 +5909,10 @@ async function startServer(options) {
|
|
|
5519
5909
|
let body = "";
|
|
5520
5910
|
req.on("data", (chunk) => body += chunk);
|
|
5521
5911
|
return void req.on("end", async () => {
|
|
5522
|
-
|
|
5523
|
-
|
|
5524
|
-
read = JSON.parse(body || "{}");
|
|
5525
|
-
} catch {
|
|
5912
|
+
const parsed2 = jsonBody(body);
|
|
5913
|
+
if (parsed2 === null) {
|
|
5526
5914
|
return sendJson(res, 400, { ok: false, error: "Body must be JSON." });
|
|
5527
5915
|
}
|
|
5528
|
-
if (typeof read !== "object" || read === null || Array.isArray(read)) {
|
|
5529
|
-
return sendJson(res, 400, { ok: false, error: "Body must be a JSON object." });
|
|
5530
|
-
}
|
|
5531
|
-
const parsed2 = read;
|
|
5532
5916
|
if (typeof parsed2.id !== "string" || parsed2.id === "") {
|
|
5533
5917
|
return sendJson(res, 400, { ok: false, error: "Body needs the note to reword." });
|
|
5534
5918
|
}
|
|
@@ -5651,12 +6035,16 @@ async function startServer(options) {
|
|
|
5651
6035
|
socket.once("close", () => sockets.delete(socket));
|
|
5652
6036
|
});
|
|
5653
6037
|
server.on("upgrade", (req, socket, head) => {
|
|
6038
|
+
if (live.upgrade(req, socket, head))
|
|
6039
|
+
return;
|
|
5654
6040
|
const path = (req.url ?? "/").split("?")[0] ?? "/";
|
|
5655
6041
|
if (path.startsWith(`${LEGLAS_PREFIX}/`))
|
|
5656
6042
|
return socket.destroy();
|
|
5657
6043
|
proxy.upgrade(req, socket, head);
|
|
5658
6044
|
});
|
|
5659
6045
|
const port = await bind(server, options.port ?? DEFAULT_PORT);
|
|
6046
|
+
const liveFiles = watchLiveFiles(cwd, bootConfigPath, live);
|
|
6047
|
+
const liveHealth = watchHealth(target, live);
|
|
5660
6048
|
await pruneCaptures(cwd, (await readRequests(cwd).catch(() => [])).map((request) => request.id)).catch(() => {
|
|
5661
6049
|
});
|
|
5662
6050
|
await writeServerInfo(cwd, {
|
|
@@ -5668,6 +6056,7 @@ async function startServer(options) {
|
|
|
5668
6056
|
runner = startRunner({
|
|
5669
6057
|
cwd,
|
|
5670
6058
|
externallyAttached,
|
|
6059
|
+
onChange: () => live.nudge("requests"),
|
|
5671
6060
|
leglasCommand,
|
|
5672
6061
|
...options.codexAppServer === void 0 ? {} : { codexAppServer: options.codexAppServer },
|
|
5673
6062
|
...options.claudeAgentSession === void 0 ? {} : { claudeAgentSession: options.claudeAgentSession }
|
|
@@ -5679,7 +6068,9 @@ async function startServer(options) {
|
|
|
5679
6068
|
close: () => {
|
|
5680
6069
|
if (closePromise !== null)
|
|
5681
6070
|
return closePromise;
|
|
5682
|
-
|
|
6071
|
+
liveFiles.close();
|
|
6072
|
+
liveHealth.close();
|
|
6073
|
+
closePromise = Promise.all([runner.stop(), browserPool.close(), live.close()]).then(() => new Promise((done) => {
|
|
5683
6074
|
for (const socket of sockets)
|
|
5684
6075
|
socket.destroy();
|
|
5685
6076
|
sockets.clear();
|
|
@@ -6152,10 +6543,10 @@ async function runInit(options, deps) {
|
|
|
6152
6543
|
// src/run-keep.ts
|
|
6153
6544
|
import { existsSync as existsSync4 } from "fs";
|
|
6154
6545
|
import { mkdir as mkdir9, readFile as readFile13, rm as rm5, writeFile as writeFile10 } from "fs/promises";
|
|
6155
|
-
import { dirname as
|
|
6546
|
+
import { dirname as dirname9, join as join15 } from "path";
|
|
6156
6547
|
|
|
6157
6548
|
// src/keep.ts
|
|
6158
|
-
import { basename as
|
|
6549
|
+
import { basename as basename4, extname as extname4, normalize as normalize2 } from "path";
|
|
6159
6550
|
function surfaceOf(url) {
|
|
6160
6551
|
if (!url.startsWith("/") || !url.includes("?")) return null;
|
|
6161
6552
|
for (const pair of url.slice(url.indexOf("?") + 1).split("&")) {
|
|
@@ -6165,7 +6556,7 @@ function surfaceOf(url) {
|
|
|
6165
6556
|
return null;
|
|
6166
6557
|
}
|
|
6167
6558
|
function exportNameFor(to) {
|
|
6168
|
-
const stem =
|
|
6559
|
+
const stem = basename4(to, extname4(to));
|
|
6169
6560
|
return stem.split(/[^a-zA-Z0-9]+/).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
|
|
6170
6561
|
}
|
|
6171
6562
|
function planKeep(options) {
|
|
@@ -6260,7 +6651,7 @@ async function runKeep(options, deps) {
|
|
|
6260
6651
|
return fail(`${plan.move.to} already exists. Choose another destination or move it aside.`);
|
|
6261
6652
|
}
|
|
6262
6653
|
const source = await readFile13(from, "utf8");
|
|
6263
|
-
await mkdir9(
|
|
6654
|
+
await mkdir9(dirname9(to), { recursive: true });
|
|
6264
6655
|
await writeFile10(to, renameExport(source, plan.exportName), "utf8");
|
|
6265
6656
|
await rm5(join15(options.cwd, plan.removeDir), { recursive: true, force: true });
|
|
6266
6657
|
const dropped = await dropLocalPreviews(options.cwd, plan.dropTitles);
|
|
@@ -6298,7 +6689,7 @@ async function runKeep(options, deps) {
|
|
|
6298
6689
|
// src/run-new.ts
|
|
6299
6690
|
import { existsSync as existsSync5 } from "fs";
|
|
6300
6691
|
import { mkdir as mkdir10, readFile as readFile14, writeFile as writeFile11 } from "fs/promises";
|
|
6301
|
-
import { dirname as
|
|
6692
|
+
import { dirname as dirname10, join as join16 } from "path";
|
|
6302
6693
|
async function readIfPresent2(path) {
|
|
6303
6694
|
try {
|
|
6304
6695
|
return await readFile14(path, "utf8");
|
|
@@ -6351,7 +6742,7 @@ async function runNew(options, deps) {
|
|
|
6351
6742
|
const written = [];
|
|
6352
6743
|
for (const write2 of plan.writes) {
|
|
6353
6744
|
const target = join16(options.cwd, write2.path);
|
|
6354
|
-
await mkdir10(
|
|
6745
|
+
await mkdir10(dirname10(target), { recursive: true });
|
|
6355
6746
|
await writeFile11(target, write2.contents, "utf8");
|
|
6356
6747
|
written.push(write2.path);
|
|
6357
6748
|
}
|
|
@@ -6714,7 +7105,7 @@ async function runShow(options, deps) {
|
|
|
6714
7105
|
// src/run-watch.ts
|
|
6715
7106
|
import { spawn as spawn3 } from "child_process";
|
|
6716
7107
|
import { mkdir as mkdir11, readFile as readFile16, writeFile as writeFile13 } from "fs/promises";
|
|
6717
|
-
import { dirname as
|
|
7108
|
+
import { dirname as dirname11, join as join18 } from "path";
|
|
6718
7109
|
var POLL_MS2 = 2e3;
|
|
6719
7110
|
var HEARTBEAT_TIMEOUT_MS = 1e3;
|
|
6720
7111
|
async function saveTemplate(cwd, run4) {
|
|
@@ -6728,7 +7119,7 @@ async function saveTemplate(cwd, run4) {
|
|
|
6728
7119
|
} catch {
|
|
6729
7120
|
}
|
|
6730
7121
|
config.run = run4;
|
|
6731
|
-
await mkdir11(
|
|
7122
|
+
await mkdir11(dirname11(path), { recursive: true });
|
|
6732
7123
|
await writeFile13(path, `${JSON.stringify(config, null, 2)}
|
|
6733
7124
|
`, "utf8");
|
|
6734
7125
|
}
|
|
@@ -6870,13 +7261,13 @@ async function runWatch(options, deps) {
|
|
|
6870
7261
|
import { existsSync as existsSync6 } from "fs";
|
|
6871
7262
|
import { realpath as realpath4 } from "fs/promises";
|
|
6872
7263
|
import { createRequire } from "module";
|
|
6873
|
-
import { basename as
|
|
7264
|
+
import { basename as basename6, dirname as dirname12, join as join19, relative as relative5, resolve as resolve4 } from "path";
|
|
6874
7265
|
import { fileURLToPath } from "url";
|
|
6875
7266
|
|
|
6876
7267
|
// src/dev-server-owner.ts
|
|
6877
7268
|
import { execFile as execFile2 } from "child_process";
|
|
6878
7269
|
import { realpath as realpath3 } from "fs/promises";
|
|
6879
|
-
import { basename as
|
|
7270
|
+
import { basename as basename5, isAbsolute as isAbsolute2, relative as relative4, resolve as resolve3 } from "path";
|
|
6880
7271
|
import { promisify as promisify2 } from "util";
|
|
6881
7272
|
var run2 = promisify2(execFile2);
|
|
6882
7273
|
var LOCAL_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1", "[::1]"]);
|
|
@@ -6949,18 +7340,18 @@ function devServerOwnerWarning(origin, projectRoot, owners) {
|
|
|
6949
7340
|
const port = localDevServerPort(origin);
|
|
6950
7341
|
if (port === null || owners.length === 0) return null;
|
|
6951
7342
|
if (owners.some((owner2) => isInside(projectRoot, owner2.cwd))) return null;
|
|
6952
|
-
const owner =
|
|
6953
|
-
const project =
|
|
7343
|
+
const owner = basename5(resolve3(owners[0]?.cwd ?? "")) || "another project";
|
|
7344
|
+
const project = basename5(resolve3(projectRoot)) || "this project";
|
|
6954
7345
|
return `Port ${port} appears to be served from ${owner}, outside this project (${project}). Check devServer in your Leglas config or use --user-port.`;
|
|
6955
7346
|
}
|
|
6956
7347
|
|
|
6957
7348
|
// src/run.ts
|
|
6958
7349
|
function findShellDir() {
|
|
6959
|
-
const bundled = join19(
|
|
7350
|
+
const bundled = join19(dirname12(fileURLToPath(import.meta.url)), "shell");
|
|
6960
7351
|
if (existsSync6(join19(bundled, "index.html"))) return bundled;
|
|
6961
7352
|
try {
|
|
6962
7353
|
const require2 = createRequire(import.meta.url);
|
|
6963
|
-
return
|
|
7354
|
+
return dirname12(require2.resolve("@leglas/shell/dist/index.html"));
|
|
6964
7355
|
} catch {
|
|
6965
7356
|
return null;
|
|
6966
7357
|
}
|
|
@@ -6971,7 +7362,7 @@ function shellWord(value) {
|
|
|
6971
7362
|
return `'${value.replaceAll("'", `'\\''`)}'`;
|
|
6972
7363
|
}
|
|
6973
7364
|
function embeddedLeglasCommand() {
|
|
6974
|
-
const entry = join19(
|
|
7365
|
+
const entry = join19(dirname12(fileURLToPath(import.meta.url)), "bin.js");
|
|
6975
7366
|
if (!existsSync6(entry)) return "npx -y leglas";
|
|
6976
7367
|
return [process.execPath, entry].map(shellWord).join(" ");
|
|
6977
7368
|
}
|
|
@@ -7015,10 +7406,10 @@ async function run3(options, deps) {
|
|
|
7015
7406
|
for (let suffix = 2; fileMounts.has(slug); suffix += 1) {
|
|
7016
7407
|
slug = `${worktreeSlug(preview.title) || "file"}-${suffix}`;
|
|
7017
7408
|
}
|
|
7018
|
-
fileMounts.set(slug,
|
|
7409
|
+
fileMounts.set(slug, dirname12(absolute));
|
|
7019
7410
|
previews.push({
|
|
7020
7411
|
...preview,
|
|
7021
|
-
url: `${FILES_PREFIX}/${slug}/${encodeURIComponent(
|
|
7412
|
+
url: `${FILES_PREFIX}/${slug}/${encodeURIComponent(basename6(absolute))}`
|
|
7022
7413
|
});
|
|
7023
7414
|
continue;
|
|
7024
7415
|
}
|
|
@@ -7048,8 +7439,8 @@ async function run3(options, deps) {
|
|
|
7048
7439
|
}
|
|
7049
7440
|
const config = merged === null ? null : { ...merged, previews };
|
|
7050
7441
|
const configWarnings = [];
|
|
7051
|
-
const projectRoot = await realpath4(loaded.path === null ? options.cwd :
|
|
7052
|
-
() => resolve4(loaded.path === null ? options.cwd :
|
|
7442
|
+
const projectRoot = await realpath4(loaded.path === null ? options.cwd : dirname12(loaded.path)).catch(
|
|
7443
|
+
() => resolve4(loaded.path === null ? options.cwd : dirname12(loaded.path))
|
|
7053
7444
|
);
|
|
7054
7445
|
const ownerWarning = needsApp && app === null ? inspectLocalDevServer(devServer).then((owners) => devServerOwnerWarning(devServer, projectRoot, owners)).catch(() => null) : Promise.resolve(null);
|
|
7055
7446
|
const serverPromise = startServer({
|