leglas 0.7.1 → 0.7.3
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 +449 -44
- package/dist/index.js +449 -44
- 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/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
|
}
|
|
@@ -2624,18 +2624,23 @@ async function freePort() {
|
|
|
2624
2624
|
});
|
|
2625
2625
|
});
|
|
2626
2626
|
}
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2627
|
+
var LOOPBACK = ["127.0.0.1", "::1"];
|
|
2628
|
+
function forUrl(host) {
|
|
2629
|
+
return host.includes(":") ? `[${host}]` : host;
|
|
2630
|
+
}
|
|
2631
|
+
async function answeringHost(port) {
|
|
2632
|
+
const reached = await Promise.all(LOOPBACK.map((host) => new Promise((resolve5) => {
|
|
2633
|
+
const socket = net2.connect({ port, host });
|
|
2630
2634
|
const settle = (value) => {
|
|
2631
2635
|
socket.destroy();
|
|
2632
2636
|
resolve5(value);
|
|
2633
2637
|
};
|
|
2634
2638
|
socket.setTimeout(400);
|
|
2635
|
-
socket.once("connect", () => settle(
|
|
2636
|
-
socket.once("timeout", () => settle(
|
|
2637
|
-
socket.once("error", () => settle(
|
|
2638
|
-
});
|
|
2639
|
+
socket.once("connect", () => settle(host));
|
|
2640
|
+
socket.once("timeout", () => settle(null));
|
|
2641
|
+
socket.once("error", () => settle(null));
|
|
2642
|
+
})));
|
|
2643
|
+
return reached.find((host) => host !== null) ?? null;
|
|
2639
2644
|
}
|
|
2640
2645
|
async function startWorktree(options) {
|
|
2641
2646
|
const readyTimeoutMs = options.readyTimeoutMs ?? 9e4;
|
|
@@ -2730,8 +2735,9 @@ async function startAppProcess(options) {
|
|
|
2730
2735
|
if (exited !== null) {
|
|
2731
2736
|
throw new Error(`${options.label} did not start: its dev command exited with code ${exited}.`);
|
|
2732
2737
|
}
|
|
2733
|
-
|
|
2734
|
-
|
|
2738
|
+
const host = await answeringHost(port);
|
|
2739
|
+
if (host !== null) {
|
|
2740
|
+
return { port, url: `http://${forUrl(host)}:${port}`, stop };
|
|
2735
2741
|
}
|
|
2736
2742
|
await new Promise((resolve5) => setTimeout(resolve5, 250));
|
|
2737
2743
|
}
|
|
@@ -4226,6 +4232,10 @@ function startRunner(options) {
|
|
|
4226
4232
|
stopping: false,
|
|
4227
4233
|
waiting: null
|
|
4228
4234
|
};
|
|
4235
|
+
const setState = (next) => {
|
|
4236
|
+
state = typeof next === "function" ? next(state) : next;
|
|
4237
|
+
options.onChange?.();
|
|
4238
|
+
};
|
|
4229
4239
|
let stopped = false;
|
|
4230
4240
|
let ticking = null;
|
|
4231
4241
|
let pendingNudges = 0;
|
|
@@ -4277,7 +4287,7 @@ function startRunner(options) {
|
|
|
4277
4287
|
return session !== void 0 && session.turns < SESSION_TURNS_CAP ? session.id : null;
|
|
4278
4288
|
};
|
|
4279
4289
|
const idle = () => {
|
|
4280
|
-
|
|
4290
|
+
setState({
|
|
4281
4291
|
running: false,
|
|
4282
4292
|
requestId: null,
|
|
4283
4293
|
agent: null,
|
|
@@ -4285,7 +4295,7 @@ function startRunner(options) {
|
|
|
4285
4295
|
startedAt: null,
|
|
4286
4296
|
stopping: false,
|
|
4287
4297
|
waiting: null
|
|
4288
|
-
};
|
|
4298
|
+
});
|
|
4289
4299
|
};
|
|
4290
4300
|
const rememberLine = (lines2, line) => {
|
|
4291
4301
|
lines2.push(line);
|
|
@@ -4393,14 +4403,15 @@ function startRunner(options) {
|
|
|
4393
4403
|
if (retry !== null) {
|
|
4394
4404
|
observed.retry = retry;
|
|
4395
4405
|
if (active === current)
|
|
4396
|
-
|
|
4406
|
+
setState((value) => ({ ...value, waiting: retry }));
|
|
4397
4407
|
}
|
|
4398
4408
|
const activity = activityFrom(resolved.agent, line, options.cwd);
|
|
4399
4409
|
if (activity !== null) {
|
|
4400
4410
|
if (activity.startsWith("editing"))
|
|
4401
4411
|
observed.edited = true;
|
|
4402
|
-
if (active === current)
|
|
4403
|
-
|
|
4412
|
+
if (active === current) {
|
|
4413
|
+
setState((value) => ({ ...value, activity, waiting: null }));
|
|
4414
|
+
}
|
|
4404
4415
|
}
|
|
4405
4416
|
});
|
|
4406
4417
|
const stderrFlush = lineReader(child.stderr, (line) => rememberLine(lines2, line));
|
|
@@ -4447,7 +4458,7 @@ function startRunner(options) {
|
|
|
4447
4458
|
await reportFailure(request, classifyFailure({ agent: resolved.name, error: "stopped by shutdown" }), []);
|
|
4448
4459
|
return;
|
|
4449
4460
|
}
|
|
4450
|
-
|
|
4461
|
+
setState({
|
|
4451
4462
|
running: true,
|
|
4452
4463
|
requestId: request.id,
|
|
4453
4464
|
agent: resolved.name,
|
|
@@ -4455,7 +4466,7 @@ function startRunner(options) {
|
|
|
4455
4466
|
startedAt: Date.now(),
|
|
4456
4467
|
stopping: false,
|
|
4457
4468
|
waiting: null
|
|
4458
|
-
};
|
|
4469
|
+
});
|
|
4459
4470
|
const observed = {
|
|
4460
4471
|
sessionId: null,
|
|
4461
4472
|
edited: false,
|
|
@@ -4486,7 +4497,7 @@ function startRunner(options) {
|
|
|
4486
4497
|
resolved = cold;
|
|
4487
4498
|
observed.sessionId = null;
|
|
4488
4499
|
observed.retry = null;
|
|
4489
|
-
|
|
4500
|
+
setState((value) => ({ ...value, activity: null, waiting: null }));
|
|
4490
4501
|
outcome = await runChild(request, resolved, lines2, observed);
|
|
4491
4502
|
failure = verdict();
|
|
4492
4503
|
}
|
|
@@ -4560,7 +4571,7 @@ function startRunner(options) {
|
|
|
4560
4571
|
const current = active;
|
|
4561
4572
|
current.cancelled = true;
|
|
4562
4573
|
failed.add(current.requestId);
|
|
4563
|
-
|
|
4574
|
+
setState((value) => ({ ...value, stopping: true, waiting: null }));
|
|
4564
4575
|
current.controller.abort();
|
|
4565
4576
|
try {
|
|
4566
4577
|
current.child?.kill("SIGTERM");
|
|
@@ -4603,6 +4614,193 @@ function startRunner(options) {
|
|
|
4603
4614
|
};
|
|
4604
4615
|
}
|
|
4605
4616
|
|
|
4617
|
+
// ../server/dist/live.js
|
|
4618
|
+
import { createHash } from "crypto";
|
|
4619
|
+
var LIVE_PATH = "/leglas/api/live";
|
|
4620
|
+
var WEBSOCKET_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
|
4621
|
+
function encodeFrame(opcode, payload) {
|
|
4622
|
+
const body = typeof payload === "string" ? Buffer.from(payload) : payload;
|
|
4623
|
+
let header;
|
|
4624
|
+
if (body.length < 126) {
|
|
4625
|
+
header = Buffer.allocUnsafe(2);
|
|
4626
|
+
header[1] = body.length;
|
|
4627
|
+
} else if (body.length <= 65535) {
|
|
4628
|
+
header = Buffer.allocUnsafe(4);
|
|
4629
|
+
header[1] = 126;
|
|
4630
|
+
header.writeUInt16BE(body.length, 2);
|
|
4631
|
+
} else {
|
|
4632
|
+
header = Buffer.allocUnsafe(10);
|
|
4633
|
+
header[1] = 127;
|
|
4634
|
+
header.writeBigUInt64BE(BigInt(body.length), 2);
|
|
4635
|
+
}
|
|
4636
|
+
header[0] = 128 | opcode & 15;
|
|
4637
|
+
return Buffer.concat([header, body], header.length + body.length);
|
|
4638
|
+
}
|
|
4639
|
+
var LIVE_DEBOUNCE_MS = 50;
|
|
4640
|
+
function createCoalescer(emit, options = {}) {
|
|
4641
|
+
const windowMs = options.windowMs ?? LIVE_DEBOUNCE_MS;
|
|
4642
|
+
const setLater = options.setTimeout ?? ((callback, ms) => {
|
|
4643
|
+
const timer = setTimeout(callback, ms);
|
|
4644
|
+
timer.unref?.();
|
|
4645
|
+
return timer;
|
|
4646
|
+
});
|
|
4647
|
+
const clearLater = options.clearTimeout ?? ((handle) => clearTimeout(handle));
|
|
4648
|
+
const pending = /* @__PURE__ */ new Map();
|
|
4649
|
+
let closed = false;
|
|
4650
|
+
return {
|
|
4651
|
+
schedule(change) {
|
|
4652
|
+
if (closed)
|
|
4653
|
+
return;
|
|
4654
|
+
const waiting = pending.get(change);
|
|
4655
|
+
if (waiting !== void 0)
|
|
4656
|
+
clearLater(waiting);
|
|
4657
|
+
pending.set(change, setLater(() => {
|
|
4658
|
+
pending.delete(change);
|
|
4659
|
+
if (!closed)
|
|
4660
|
+
emit(change);
|
|
4661
|
+
}, windowMs));
|
|
4662
|
+
},
|
|
4663
|
+
close() {
|
|
4664
|
+
closed = true;
|
|
4665
|
+
for (const handle of pending.values())
|
|
4666
|
+
clearLater(handle);
|
|
4667
|
+
pending.clear();
|
|
4668
|
+
}
|
|
4669
|
+
};
|
|
4670
|
+
}
|
|
4671
|
+
function createLiveHub(_options = {}) {
|
|
4672
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
4673
|
+
const drop = (listener) => {
|
|
4674
|
+
listeners.delete(listener);
|
|
4675
|
+
};
|
|
4676
|
+
const write2 = (listener, opcode, payload) => {
|
|
4677
|
+
if (listener.socket.destroyed || !listener.socket.writable) {
|
|
4678
|
+
drop(listener);
|
|
4679
|
+
return false;
|
|
4680
|
+
}
|
|
4681
|
+
try {
|
|
4682
|
+
listener.socket.write(encodeFrame(opcode, payload));
|
|
4683
|
+
return true;
|
|
4684
|
+
} catch {
|
|
4685
|
+
drop(listener);
|
|
4686
|
+
listener.socket.destroy();
|
|
4687
|
+
return false;
|
|
4688
|
+
}
|
|
4689
|
+
};
|
|
4690
|
+
const read = (listener, chunk) => {
|
|
4691
|
+
const incoming = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
4692
|
+
listener.buffered = listener.buffered.length === 0 ? incoming : Buffer.concat([listener.buffered, incoming]);
|
|
4693
|
+
while (listener.buffered.length >= 2) {
|
|
4694
|
+
const first = listener.buffered[0] ?? 0;
|
|
4695
|
+
const second = listener.buffered[1] ?? 0;
|
|
4696
|
+
let length = second & 127;
|
|
4697
|
+
let offset = 2;
|
|
4698
|
+
if (length === 126) {
|
|
4699
|
+
if (listener.buffered.length < 4)
|
|
4700
|
+
return;
|
|
4701
|
+
length = listener.buffered.readUInt16BE(2);
|
|
4702
|
+
offset = 4;
|
|
4703
|
+
} else if (length === 127) {
|
|
4704
|
+
if (listener.buffered.length < 10)
|
|
4705
|
+
return;
|
|
4706
|
+
const wide = listener.buffered.readBigUInt64BE(2);
|
|
4707
|
+
if (wide > BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
4708
|
+
drop(listener);
|
|
4709
|
+
listener.socket.destroy();
|
|
4710
|
+
return;
|
|
4711
|
+
}
|
|
4712
|
+
length = Number(wide);
|
|
4713
|
+
offset = 10;
|
|
4714
|
+
}
|
|
4715
|
+
const masked = (second & 128) !== 0;
|
|
4716
|
+
if (!masked) {
|
|
4717
|
+
drop(listener);
|
|
4718
|
+
listener.socket.destroy();
|
|
4719
|
+
return;
|
|
4720
|
+
}
|
|
4721
|
+
if (listener.buffered.length < offset + 4 + length)
|
|
4722
|
+
return;
|
|
4723
|
+
const mask = listener.buffered.subarray(offset, offset + 4);
|
|
4724
|
+
offset += 4;
|
|
4725
|
+
const payload = Buffer.from(listener.buffered.subarray(offset, offset + length));
|
|
4726
|
+
for (let index = 0; index < payload.length; index += 1) {
|
|
4727
|
+
payload[index] = (payload[index] ?? 0) ^ (mask[index % 4] ?? 0);
|
|
4728
|
+
}
|
|
4729
|
+
listener.buffered = listener.buffered.subarray(offset + length);
|
|
4730
|
+
const opcode = first & 15;
|
|
4731
|
+
if (opcode === 8) {
|
|
4732
|
+
write2(listener, 8, payload);
|
|
4733
|
+
drop(listener);
|
|
4734
|
+
listener.socket.destroy();
|
|
4735
|
+
return;
|
|
4736
|
+
}
|
|
4737
|
+
if (opcode === 9)
|
|
4738
|
+
write2(listener, 10, payload);
|
|
4739
|
+
}
|
|
4740
|
+
};
|
|
4741
|
+
return {
|
|
4742
|
+
nudge: (change) => {
|
|
4743
|
+
if (listeners.size === 0)
|
|
4744
|
+
return;
|
|
4745
|
+
const frame = encodeFrame(1, JSON.stringify({ changed: change }));
|
|
4746
|
+
for (const listener of [...listeners]) {
|
|
4747
|
+
if (listener.socket.destroyed || !listener.socket.writable) {
|
|
4748
|
+
drop(listener);
|
|
4749
|
+
continue;
|
|
4750
|
+
}
|
|
4751
|
+
try {
|
|
4752
|
+
listener.socket.write(frame);
|
|
4753
|
+
} catch {
|
|
4754
|
+
drop(listener);
|
|
4755
|
+
listener.socket.destroy();
|
|
4756
|
+
}
|
|
4757
|
+
}
|
|
4758
|
+
},
|
|
4759
|
+
upgrade: (req, socket, head) => {
|
|
4760
|
+
const path = (req.url ?? "/").split("?")[0] ?? "/";
|
|
4761
|
+
if (req.method !== "GET" || path !== LIVE_PATH)
|
|
4762
|
+
return false;
|
|
4763
|
+
const upgrade = req.headers.upgrade;
|
|
4764
|
+
const key = req.headers["sec-websocket-key"];
|
|
4765
|
+
if (typeof upgrade !== "string" || upgrade.toLowerCase() !== "websocket" || typeof key !== "string" || key.trim() === "") {
|
|
4766
|
+
socket.destroy();
|
|
4767
|
+
return false;
|
|
4768
|
+
}
|
|
4769
|
+
const accept = createHash("sha1").update(key + WEBSOCKET_GUID).digest("base64");
|
|
4770
|
+
try {
|
|
4771
|
+
socket.write(`HTTP/1.1 101 Switching Protocols\r
|
|
4772
|
+
Upgrade: websocket\r
|
|
4773
|
+
Connection: Upgrade\r
|
|
4774
|
+
Sec-WebSocket-Accept: ${accept}\r
|
|
4775
|
+
\r
|
|
4776
|
+
`);
|
|
4777
|
+
} catch {
|
|
4778
|
+
socket.destroy();
|
|
4779
|
+
return false;
|
|
4780
|
+
}
|
|
4781
|
+
const listener = { socket, buffered: Buffer.alloc(0) };
|
|
4782
|
+
listeners.add(listener);
|
|
4783
|
+
socket.on("data", (chunk) => read(listener, chunk));
|
|
4784
|
+
socket.once("error", () => drop(listener));
|
|
4785
|
+
socket.once("end", () => drop(listener));
|
|
4786
|
+
socket.once("close", () => drop(listener));
|
|
4787
|
+
if (head.length > 0)
|
|
4788
|
+
read(listener, head);
|
|
4789
|
+
return true;
|
|
4790
|
+
},
|
|
4791
|
+
close: async () => {
|
|
4792
|
+
for (const listener of [...listeners]) {
|
|
4793
|
+
write2(listener, 8, Buffer.alloc(0));
|
|
4794
|
+
drop(listener);
|
|
4795
|
+
listener.socket.destroy();
|
|
4796
|
+
}
|
|
4797
|
+
},
|
|
4798
|
+
get listening() {
|
|
4799
|
+
return listeners.size;
|
|
4800
|
+
}
|
|
4801
|
+
};
|
|
4802
|
+
}
|
|
4803
|
+
|
|
4606
4804
|
// ../server/dist/renames.js
|
|
4607
4805
|
import { mkdir as mkdir6, readFile as readFile10, writeFile as writeFile6 } from "fs/promises";
|
|
4608
4806
|
import { dirname as dirname6, join as join10 } from "path";
|
|
@@ -4636,11 +4834,11 @@ function resolveTitle(input, titles, renames) {
|
|
|
4636
4834
|
}
|
|
4637
4835
|
|
|
4638
4836
|
// ../server/dist/server.js
|
|
4639
|
-
import { createReadStream, existsSync as existsSync3, statSync } from "fs";
|
|
4837
|
+
import { createReadStream, existsSync as existsSync3, statSync, unwatchFile, watch as watchFs, watchFile } from "fs";
|
|
4640
4838
|
import { mkdir as mkdir8, readdir as readdir3, writeFile as writeFile8 } from "fs/promises";
|
|
4641
4839
|
import http2 from "http";
|
|
4642
4840
|
import net3 from "net";
|
|
4643
|
-
import { extname as extname3, join as join12, normalize, relative as relative3 } from "path";
|
|
4841
|
+
import { basename as basename3, dirname as dirname8, extname as extname3, join as join12, normalize, relative as relative3 } from "path";
|
|
4644
4842
|
|
|
4645
4843
|
// ../server/dist/server-info.js
|
|
4646
4844
|
import { lstat as lstat2, mkdir as mkdir7, readFile as readFile11, rename as rename2, rm as rm4, writeFile as writeFile7 } from "fs/promises";
|
|
@@ -4825,6 +5023,204 @@ function serveShellFile(res, shellDir, urlPath) {
|
|
|
4825
5023
|
const isRoot = relative6 === "" || relative6 === "." || relative6 === "/";
|
|
4826
5024
|
return serveFrom(res, shellDir, isRoot ? "index.html" : relative6);
|
|
4827
5025
|
}
|
|
5026
|
+
var HEALTH_PROBE_MS = 3e3;
|
|
5027
|
+
function fileStamp(path) {
|
|
5028
|
+
try {
|
|
5029
|
+
const stat5 = statSync(path);
|
|
5030
|
+
return `${stat5.dev}:${stat5.ino}:${stat5.size}:${stat5.mtimeMs}:${stat5.ctimeMs}`;
|
|
5031
|
+
} catch {
|
|
5032
|
+
return null;
|
|
5033
|
+
}
|
|
5034
|
+
}
|
|
5035
|
+
function watchLiveFiles(cwd, configPath, live) {
|
|
5036
|
+
const leglasDir = join12(cwd, ".leglas");
|
|
5037
|
+
const targets = [
|
|
5038
|
+
{ path: join12(cwd, LOCAL_PREVIEWS_PATH), change: "config" },
|
|
5039
|
+
{ path: join12(cwd, REQUESTS_PATH), change: "requests" },
|
|
5040
|
+
{ path: join12(cwd, ANNOTATIONS_PATH), change: "requests" }
|
|
5041
|
+
];
|
|
5042
|
+
const byName = new Map(targets.map((target) => [basename3(target.path), target]));
|
|
5043
|
+
const known = new Map(targets.map((target) => [target.path, fileStamp(target.path)]));
|
|
5044
|
+
const coalescer = createCoalescer((change) => live.nudge(change));
|
|
5045
|
+
const watchers = /* @__PURE__ */ new Set();
|
|
5046
|
+
const fallback = /* @__PURE__ */ new Map();
|
|
5047
|
+
let leglasWatcher = null;
|
|
5048
|
+
let retry = null;
|
|
5049
|
+
let closed = false;
|
|
5050
|
+
const nudgeSoon = (change) => coalescer.schedule(change);
|
|
5051
|
+
const scanLeglas = (notify) => {
|
|
5052
|
+
for (const target of targets) {
|
|
5053
|
+
const next = fileStamp(target.path);
|
|
5054
|
+
if (next === known.get(target.path))
|
|
5055
|
+
continue;
|
|
5056
|
+
known.set(target.path, next);
|
|
5057
|
+
if (notify)
|
|
5058
|
+
nudgeSoon(target.change);
|
|
5059
|
+
}
|
|
5060
|
+
};
|
|
5061
|
+
const fallbackWatch = (target) => {
|
|
5062
|
+
if (closed || fallback.has(target.path))
|
|
5063
|
+
return;
|
|
5064
|
+
const listener = () => {
|
|
5065
|
+
const next = fileStamp(target.path);
|
|
5066
|
+
if (next === known.get(target.path))
|
|
5067
|
+
return;
|
|
5068
|
+
known.set(target.path, next);
|
|
5069
|
+
nudgeSoon(target.change);
|
|
5070
|
+
};
|
|
5071
|
+
fallback.set(target.path, listener);
|
|
5072
|
+
watchFile(target.path, { persistent: false, interval: 250 }, listener);
|
|
5073
|
+
};
|
|
5074
|
+
const fallbackLeglas = () => {
|
|
5075
|
+
for (const target of targets)
|
|
5076
|
+
fallbackWatch(target);
|
|
5077
|
+
};
|
|
5078
|
+
const retryLeglas = () => {
|
|
5079
|
+
if (closed || retry !== null)
|
|
5080
|
+
return;
|
|
5081
|
+
retry = setTimeout(() => {
|
|
5082
|
+
retry = null;
|
|
5083
|
+
armLeglas(true);
|
|
5084
|
+
}, 250);
|
|
5085
|
+
retry.unref?.();
|
|
5086
|
+
};
|
|
5087
|
+
const armLeglas = (notify) => {
|
|
5088
|
+
if (closed)
|
|
5089
|
+
return;
|
|
5090
|
+
scanLeglas(notify);
|
|
5091
|
+
let directory = false;
|
|
5092
|
+
try {
|
|
5093
|
+
directory = statSync(leglasDir).isDirectory();
|
|
5094
|
+
} catch {
|
|
5095
|
+
directory = false;
|
|
5096
|
+
}
|
|
5097
|
+
if (!directory) {
|
|
5098
|
+
if (leglasWatcher !== null) {
|
|
5099
|
+
watchers.delete(leglasWatcher);
|
|
5100
|
+
leglasWatcher.close();
|
|
5101
|
+
leglasWatcher = null;
|
|
5102
|
+
}
|
|
5103
|
+
retryLeglas();
|
|
5104
|
+
return;
|
|
5105
|
+
}
|
|
5106
|
+
if (leglasWatcher !== null)
|
|
5107
|
+
return;
|
|
5108
|
+
try {
|
|
5109
|
+
const watcher = watchFs(leglasDir, { persistent: false }, (_event, filename) => {
|
|
5110
|
+
if (filename === null) {
|
|
5111
|
+
scanLeglas(true);
|
|
5112
|
+
return;
|
|
5113
|
+
}
|
|
5114
|
+
const name = Buffer.isBuffer(filename) ? filename.toString() : filename;
|
|
5115
|
+
const target = byName.get(name);
|
|
5116
|
+
if (target === void 0)
|
|
5117
|
+
return;
|
|
5118
|
+
known.set(target.path, fileStamp(target.path));
|
|
5119
|
+
nudgeSoon(target.change);
|
|
5120
|
+
});
|
|
5121
|
+
watcher.on("error", () => {
|
|
5122
|
+
if (leglasWatcher !== watcher)
|
|
5123
|
+
return;
|
|
5124
|
+
watchers.delete(watcher);
|
|
5125
|
+
watcher.close();
|
|
5126
|
+
leglasWatcher = null;
|
|
5127
|
+
fallbackLeglas();
|
|
5128
|
+
retryLeglas();
|
|
5129
|
+
});
|
|
5130
|
+
leglasWatcher = watcher;
|
|
5131
|
+
watchers.add(watcher);
|
|
5132
|
+
} catch {
|
|
5133
|
+
fallbackLeglas();
|
|
5134
|
+
retryLeglas();
|
|
5135
|
+
}
|
|
5136
|
+
};
|
|
5137
|
+
try {
|
|
5138
|
+
const watcher = watchFs(cwd, { persistent: false }, (_event, filename) => {
|
|
5139
|
+
const name = filename === null ? null : Buffer.isBuffer(filename) ? filename.toString() : filename;
|
|
5140
|
+
if (name === null || name === ".leglas")
|
|
5141
|
+
armLeglas(true);
|
|
5142
|
+
});
|
|
5143
|
+
watcher.on("error", () => {
|
|
5144
|
+
watchers.delete(watcher);
|
|
5145
|
+
watcher.close();
|
|
5146
|
+
fallbackLeglas();
|
|
5147
|
+
});
|
|
5148
|
+
watchers.add(watcher);
|
|
5149
|
+
} catch {
|
|
5150
|
+
fallbackLeglas();
|
|
5151
|
+
}
|
|
5152
|
+
if (configPath !== null) {
|
|
5153
|
+
try {
|
|
5154
|
+
const directory = dirname8(configPath);
|
|
5155
|
+
const name = basename3(configPath);
|
|
5156
|
+
const target = { path: configPath, change: "config" };
|
|
5157
|
+
known.set(configPath, fileStamp(configPath));
|
|
5158
|
+
const watcher = watchFs(directory, { persistent: false }, (_event, filename) => {
|
|
5159
|
+
const changed = filename === null ? null : Buffer.isBuffer(filename) ? filename.toString() : filename;
|
|
5160
|
+
if (changed === null || changed === name)
|
|
5161
|
+
nudgeSoon("config");
|
|
5162
|
+
});
|
|
5163
|
+
watcher.on("error", () => {
|
|
5164
|
+
watchers.delete(watcher);
|
|
5165
|
+
watcher.close();
|
|
5166
|
+
fallbackWatch(target);
|
|
5167
|
+
});
|
|
5168
|
+
watchers.add(watcher);
|
|
5169
|
+
} catch {
|
|
5170
|
+
fallbackWatch({ path: configPath, change: "config" });
|
|
5171
|
+
}
|
|
5172
|
+
}
|
|
5173
|
+
armLeglas(false);
|
|
5174
|
+
return {
|
|
5175
|
+
close: () => {
|
|
5176
|
+
if (closed)
|
|
5177
|
+
return;
|
|
5178
|
+
closed = true;
|
|
5179
|
+
if (retry !== null)
|
|
5180
|
+
clearTimeout(retry);
|
|
5181
|
+
coalescer.close();
|
|
5182
|
+
for (const watcher of watchers)
|
|
5183
|
+
watcher.close();
|
|
5184
|
+
watchers.clear();
|
|
5185
|
+
for (const [path, listener] of fallback)
|
|
5186
|
+
unwatchFile(path, listener);
|
|
5187
|
+
fallback.clear();
|
|
5188
|
+
leglasWatcher = null;
|
|
5189
|
+
}
|
|
5190
|
+
};
|
|
5191
|
+
}
|
|
5192
|
+
function watchHealth(target, live) {
|
|
5193
|
+
let previous = null;
|
|
5194
|
+
let probing = false;
|
|
5195
|
+
let closed = false;
|
|
5196
|
+
const timer = setInterval(() => {
|
|
5197
|
+
if (live.listening === 0) {
|
|
5198
|
+
previous = null;
|
|
5199
|
+
return;
|
|
5200
|
+
}
|
|
5201
|
+
if (probing)
|
|
5202
|
+
return;
|
|
5203
|
+
probing = true;
|
|
5204
|
+
void probe(target).then((reachable) => {
|
|
5205
|
+
if (closed || live.listening === 0) {
|
|
5206
|
+
previous = null;
|
|
5207
|
+
return;
|
|
5208
|
+
}
|
|
5209
|
+
if (previous !== null && previous !== reachable)
|
|
5210
|
+
live.nudge("health");
|
|
5211
|
+
previous = reachable;
|
|
5212
|
+
}).finally(() => {
|
|
5213
|
+
probing = false;
|
|
5214
|
+
});
|
|
5215
|
+
}, HEALTH_PROBE_MS);
|
|
5216
|
+
timer.unref?.();
|
|
5217
|
+
return {
|
|
5218
|
+
close: () => {
|
|
5219
|
+
closed = true;
|
|
5220
|
+
clearInterval(timer);
|
|
5221
|
+
}
|
|
5222
|
+
};
|
|
5223
|
+
}
|
|
4828
5224
|
function snapshotConfig(cwd) {
|
|
4829
5225
|
const path = findConfigFile(cwd);
|
|
4830
5226
|
if (path === null)
|
|
@@ -4895,12 +5291,14 @@ async function bind(server, requested) {
|
|
|
4895
5291
|
async function startServer(options) {
|
|
4896
5292
|
const { config, configErrors = [], configWarnings = [], shellDir = null, project = "", cwd = process.cwd(), leglasCommand = "npx -y leglas", fileMounts = /* @__PURE__ */ new Map(), detect = () => detectAgents() } = options;
|
|
4897
5293
|
const browserPool = options.pool ?? createBrowserPool();
|
|
5294
|
+
const live = options.live ?? createLiveHub();
|
|
4898
5295
|
if (options.pool === void 0) {
|
|
4899
5296
|
void reapOrphanedBrowsers().catch(() => {
|
|
4900
5297
|
});
|
|
4901
5298
|
}
|
|
4902
5299
|
const target = config?.devServer ?? "http://localhost:3000";
|
|
4903
5300
|
const proxy = createProxyHandler({ target });
|
|
5301
|
+
const bootConfigPath = findConfigFile(cwd);
|
|
4904
5302
|
const bootConfigSnapshot = snapshotConfig(cwd);
|
|
4905
5303
|
let lastSeen = null;
|
|
4906
5304
|
const externallyAttached = () => lastSeen !== null && Date.now() - lastSeen < ATTACHED_WINDOW_MS;
|
|
@@ -5125,14 +5523,14 @@ async function startServer(options) {
|
|
|
5125
5523
|
return sendJson(res, 400, { ok: false, error: "Unknown preview, or empty request." });
|
|
5126
5524
|
}
|
|
5127
5525
|
const intent = (parsed2.intent ?? "").trim();
|
|
5128
|
-
const
|
|
5526
|
+
const live2 = (await readRequests(cwd).catch(() => [])).filter((entry) => entry.status === "queued" || entry.status === "picked-up");
|
|
5129
5527
|
const sameNotes = (entry) => {
|
|
5130
5528
|
const before = [...entry.notes ?? []].sort().join(",");
|
|
5131
5529
|
return before === notes.map((note) => note.id).sort().join(",");
|
|
5132
5530
|
};
|
|
5133
5531
|
const compare = typeof parsed2.compare === "string" && parsed2.compare !== preview.title ? previews.find((entry) => entry.title === parsed2.compare) ?? null : null;
|
|
5134
5532
|
const sameContext = (entry) => (entry.compare ?? null) === (compare?.title ?? null) && [...entry.references ?? []].sort().join(",") === [...references].sort().join(",");
|
|
5135
|
-
if (
|
|
5533
|
+
if (live2.some((entry) => entry.title === preview.title && entry.intent === intent && // The same words in the other mode are not the same request:
|
|
5136
5534
|
// one forks the direction and the other rewrites it. Only a
|
|
5137
5535
|
// genuine repeat is refused.
|
|
5138
5536
|
(entry.mode ?? "replace") === mode && sameNotes(entry) && sameContext(entry))) {
|
|
@@ -5643,12 +6041,16 @@ async function startServer(options) {
|
|
|
5643
6041
|
socket.once("close", () => sockets.delete(socket));
|
|
5644
6042
|
});
|
|
5645
6043
|
server.on("upgrade", (req, socket, head) => {
|
|
6044
|
+
if (live.upgrade(req, socket, head))
|
|
6045
|
+
return;
|
|
5646
6046
|
const path = (req.url ?? "/").split("?")[0] ?? "/";
|
|
5647
6047
|
if (path.startsWith(`${LEGLAS_PREFIX}/`))
|
|
5648
6048
|
return socket.destroy();
|
|
5649
6049
|
proxy.upgrade(req, socket, head);
|
|
5650
6050
|
});
|
|
5651
6051
|
const port = await bind(server, options.port ?? DEFAULT_PORT);
|
|
6052
|
+
const liveFiles = watchLiveFiles(cwd, bootConfigPath, live);
|
|
6053
|
+
const liveHealth = watchHealth(target, live);
|
|
5652
6054
|
await pruneCaptures(cwd, (await readRequests(cwd).catch(() => [])).map((request) => request.id)).catch(() => {
|
|
5653
6055
|
});
|
|
5654
6056
|
await writeServerInfo(cwd, {
|
|
@@ -5660,6 +6062,7 @@ async function startServer(options) {
|
|
|
5660
6062
|
runner = startRunner({
|
|
5661
6063
|
cwd,
|
|
5662
6064
|
externallyAttached,
|
|
6065
|
+
onChange: () => live.nudge("requests"),
|
|
5663
6066
|
leglasCommand,
|
|
5664
6067
|
...options.codexAppServer === void 0 ? {} : { codexAppServer: options.codexAppServer },
|
|
5665
6068
|
...options.claudeAgentSession === void 0 ? {} : { claudeAgentSession: options.claudeAgentSession }
|
|
@@ -5671,7 +6074,9 @@ async function startServer(options) {
|
|
|
5671
6074
|
close: () => {
|
|
5672
6075
|
if (closePromise !== null)
|
|
5673
6076
|
return closePromise;
|
|
5674
|
-
|
|
6077
|
+
liveFiles.close();
|
|
6078
|
+
liveHealth.close();
|
|
6079
|
+
closePromise = Promise.all([runner.stop(), browserPool.close(), live.close()]).then(() => new Promise((done) => {
|
|
5675
6080
|
for (const socket of sockets)
|
|
5676
6081
|
socket.destroy();
|
|
5677
6082
|
sockets.clear();
|
|
@@ -6144,10 +6549,10 @@ async function runInit(options, deps) {
|
|
|
6144
6549
|
// src/run-keep.ts
|
|
6145
6550
|
import { existsSync as existsSync4 } from "fs";
|
|
6146
6551
|
import { mkdir as mkdir9, readFile as readFile13, rm as rm5, writeFile as writeFile10 } from "fs/promises";
|
|
6147
|
-
import { dirname as
|
|
6552
|
+
import { dirname as dirname9, join as join15 } from "path";
|
|
6148
6553
|
|
|
6149
6554
|
// src/keep.ts
|
|
6150
|
-
import { basename as
|
|
6555
|
+
import { basename as basename4, extname as extname4, normalize as normalize2 } from "path";
|
|
6151
6556
|
function surfaceOf(url) {
|
|
6152
6557
|
if (!url.startsWith("/") || !url.includes("?")) return null;
|
|
6153
6558
|
for (const pair of url.slice(url.indexOf("?") + 1).split("&")) {
|
|
@@ -6157,7 +6562,7 @@ function surfaceOf(url) {
|
|
|
6157
6562
|
return null;
|
|
6158
6563
|
}
|
|
6159
6564
|
function exportNameFor(to) {
|
|
6160
|
-
const stem =
|
|
6565
|
+
const stem = basename4(to, extname4(to));
|
|
6161
6566
|
return stem.split(/[^a-zA-Z0-9]+/).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
|
|
6162
6567
|
}
|
|
6163
6568
|
function planKeep(options) {
|
|
@@ -6252,7 +6657,7 @@ async function runKeep(options, deps) {
|
|
|
6252
6657
|
return fail(`${plan.move.to} already exists. Choose another destination or move it aside.`);
|
|
6253
6658
|
}
|
|
6254
6659
|
const source = await readFile13(from, "utf8");
|
|
6255
|
-
await mkdir9(
|
|
6660
|
+
await mkdir9(dirname9(to), { recursive: true });
|
|
6256
6661
|
await writeFile10(to, renameExport(source, plan.exportName), "utf8");
|
|
6257
6662
|
await rm5(join15(options.cwd, plan.removeDir), { recursive: true, force: true });
|
|
6258
6663
|
const dropped = await dropLocalPreviews(options.cwd, plan.dropTitles);
|
|
@@ -6290,7 +6695,7 @@ async function runKeep(options, deps) {
|
|
|
6290
6695
|
// src/run-new.ts
|
|
6291
6696
|
import { existsSync as existsSync5 } from "fs";
|
|
6292
6697
|
import { mkdir as mkdir10, readFile as readFile14, writeFile as writeFile11 } from "fs/promises";
|
|
6293
|
-
import { dirname as
|
|
6698
|
+
import { dirname as dirname10, join as join16 } from "path";
|
|
6294
6699
|
async function readIfPresent2(path) {
|
|
6295
6700
|
try {
|
|
6296
6701
|
return await readFile14(path, "utf8");
|
|
@@ -6343,7 +6748,7 @@ async function runNew(options, deps) {
|
|
|
6343
6748
|
const written = [];
|
|
6344
6749
|
for (const write2 of plan.writes) {
|
|
6345
6750
|
const target = join16(options.cwd, write2.path);
|
|
6346
|
-
await mkdir10(
|
|
6751
|
+
await mkdir10(dirname10(target), { recursive: true });
|
|
6347
6752
|
await writeFile11(target, write2.contents, "utf8");
|
|
6348
6753
|
written.push(write2.path);
|
|
6349
6754
|
}
|
|
@@ -6706,7 +7111,7 @@ async function runShow(options, deps) {
|
|
|
6706
7111
|
// src/run-watch.ts
|
|
6707
7112
|
import { spawn as spawn3 } from "child_process";
|
|
6708
7113
|
import { mkdir as mkdir11, readFile as readFile16, writeFile as writeFile13 } from "fs/promises";
|
|
6709
|
-
import { dirname as
|
|
7114
|
+
import { dirname as dirname11, join as join18 } from "path";
|
|
6710
7115
|
var POLL_MS2 = 2e3;
|
|
6711
7116
|
var HEARTBEAT_TIMEOUT_MS = 1e3;
|
|
6712
7117
|
async function saveTemplate(cwd, run4) {
|
|
@@ -6720,7 +7125,7 @@ async function saveTemplate(cwd, run4) {
|
|
|
6720
7125
|
} catch {
|
|
6721
7126
|
}
|
|
6722
7127
|
config.run = run4;
|
|
6723
|
-
await mkdir11(
|
|
7128
|
+
await mkdir11(dirname11(path), { recursive: true });
|
|
6724
7129
|
await writeFile13(path, `${JSON.stringify(config, null, 2)}
|
|
6725
7130
|
`, "utf8");
|
|
6726
7131
|
}
|
|
@@ -6862,13 +7267,13 @@ async function runWatch(options, deps) {
|
|
|
6862
7267
|
import { existsSync as existsSync6 } from "fs";
|
|
6863
7268
|
import { realpath as realpath4 } from "fs/promises";
|
|
6864
7269
|
import { createRequire } from "module";
|
|
6865
|
-
import { basename as
|
|
7270
|
+
import { basename as basename6, dirname as dirname12, join as join19, relative as relative5, resolve as resolve4 } from "path";
|
|
6866
7271
|
import { fileURLToPath } from "url";
|
|
6867
7272
|
|
|
6868
7273
|
// src/dev-server-owner.ts
|
|
6869
7274
|
import { execFile as execFile2 } from "child_process";
|
|
6870
7275
|
import { realpath as realpath3 } from "fs/promises";
|
|
6871
|
-
import { basename as
|
|
7276
|
+
import { basename as basename5, isAbsolute as isAbsolute2, relative as relative4, resolve as resolve3 } from "path";
|
|
6872
7277
|
import { promisify as promisify2 } from "util";
|
|
6873
7278
|
var run2 = promisify2(execFile2);
|
|
6874
7279
|
var LOCAL_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1", "[::1]"]);
|
|
@@ -6941,18 +7346,18 @@ function devServerOwnerWarning(origin, projectRoot, owners) {
|
|
|
6941
7346
|
const port = localDevServerPort(origin);
|
|
6942
7347
|
if (port === null || owners.length === 0) return null;
|
|
6943
7348
|
if (owners.some((owner2) => isInside(projectRoot, owner2.cwd))) return null;
|
|
6944
|
-
const owner =
|
|
6945
|
-
const project =
|
|
7349
|
+
const owner = basename5(resolve3(owners[0]?.cwd ?? "")) || "another project";
|
|
7350
|
+
const project = basename5(resolve3(projectRoot)) || "this project";
|
|
6946
7351
|
return `Port ${port} appears to be served from ${owner}, outside this project (${project}). Check devServer in your Leglas config or use --user-port.`;
|
|
6947
7352
|
}
|
|
6948
7353
|
|
|
6949
7354
|
// src/run.ts
|
|
6950
7355
|
function findShellDir() {
|
|
6951
|
-
const bundled = join19(
|
|
7356
|
+
const bundled = join19(dirname12(fileURLToPath(import.meta.url)), "shell");
|
|
6952
7357
|
if (existsSync6(join19(bundled, "index.html"))) return bundled;
|
|
6953
7358
|
try {
|
|
6954
7359
|
const require2 = createRequire(import.meta.url);
|
|
6955
|
-
return
|
|
7360
|
+
return dirname12(require2.resolve("@leglas/shell/dist/index.html"));
|
|
6956
7361
|
} catch {
|
|
6957
7362
|
return null;
|
|
6958
7363
|
}
|
|
@@ -6963,7 +7368,7 @@ function shellWord(value) {
|
|
|
6963
7368
|
return `'${value.replaceAll("'", `'\\''`)}'`;
|
|
6964
7369
|
}
|
|
6965
7370
|
function embeddedLeglasCommand() {
|
|
6966
|
-
const entry = join19(
|
|
7371
|
+
const entry = join19(dirname12(fileURLToPath(import.meta.url)), "bin.js");
|
|
6967
7372
|
if (!existsSync6(entry)) return "npx -y leglas";
|
|
6968
7373
|
return [process.execPath, entry].map(shellWord).join(" ");
|
|
6969
7374
|
}
|
|
@@ -7007,10 +7412,10 @@ async function run3(options, deps) {
|
|
|
7007
7412
|
for (let suffix = 2; fileMounts.has(slug); suffix += 1) {
|
|
7008
7413
|
slug = `${worktreeSlug(preview.title) || "file"}-${suffix}`;
|
|
7009
7414
|
}
|
|
7010
|
-
fileMounts.set(slug,
|
|
7415
|
+
fileMounts.set(slug, dirname12(absolute));
|
|
7011
7416
|
previews.push({
|
|
7012
7417
|
...preview,
|
|
7013
|
-
url: `${FILES_PREFIX}/${slug}/${encodeURIComponent(
|
|
7418
|
+
url: `${FILES_PREFIX}/${slug}/${encodeURIComponent(basename6(absolute))}`
|
|
7014
7419
|
});
|
|
7015
7420
|
continue;
|
|
7016
7421
|
}
|
|
@@ -7040,8 +7445,8 @@ async function run3(options, deps) {
|
|
|
7040
7445
|
}
|
|
7041
7446
|
const config = merged === null ? null : { ...merged, previews };
|
|
7042
7447
|
const configWarnings = [];
|
|
7043
|
-
const projectRoot = await realpath4(loaded.path === null ? options.cwd :
|
|
7044
|
-
() => resolve4(loaded.path === null ? options.cwd :
|
|
7448
|
+
const projectRoot = await realpath4(loaded.path === null ? options.cwd : dirname12(loaded.path)).catch(
|
|
7449
|
+
() => resolve4(loaded.path === null ? options.cwd : dirname12(loaded.path))
|
|
7045
7450
|
);
|
|
7046
7451
|
const ownerWarning = needsApp && app === null ? inspectLocalDevServer(devServer).then((owners) => devServerOwnerWarning(devServer, projectRoot, owners)).catch(() => null) : Promise.resolve(null);
|
|
7047
7452
|
const serverPromise = startServer({
|