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/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
|
}
|
|
@@ -3000,18 +3000,23 @@ async function freePort() {
|
|
|
3000
3000
|
});
|
|
3001
3001
|
});
|
|
3002
3002
|
}
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3003
|
+
var LOOPBACK = ["127.0.0.1", "::1"];
|
|
3004
|
+
function forUrl(host) {
|
|
3005
|
+
return host.includes(":") ? `[${host}]` : host;
|
|
3006
|
+
}
|
|
3007
|
+
async function answeringHost(port) {
|
|
3008
|
+
const reached = await Promise.all(LOOPBACK.map((host) => new Promise((resolve5) => {
|
|
3009
|
+
const socket = net2.connect({ port, host });
|
|
3006
3010
|
const settle = (value) => {
|
|
3007
3011
|
socket.destroy();
|
|
3008
3012
|
resolve5(value);
|
|
3009
3013
|
};
|
|
3010
3014
|
socket.setTimeout(400);
|
|
3011
|
-
socket.once("connect", () => settle(
|
|
3012
|
-
socket.once("timeout", () => settle(
|
|
3013
|
-
socket.once("error", () => settle(
|
|
3014
|
-
});
|
|
3015
|
+
socket.once("connect", () => settle(host));
|
|
3016
|
+
socket.once("timeout", () => settle(null));
|
|
3017
|
+
socket.once("error", () => settle(null));
|
|
3018
|
+
})));
|
|
3019
|
+
return reached.find((host) => host !== null) ?? null;
|
|
3015
3020
|
}
|
|
3016
3021
|
async function startWorktree(options) {
|
|
3017
3022
|
const readyTimeoutMs = options.readyTimeoutMs ?? 9e4;
|
|
@@ -3106,8 +3111,9 @@ async function startAppProcess(options) {
|
|
|
3106
3111
|
if (exited !== null) {
|
|
3107
3112
|
throw new Error(`${options.label} did not start: its dev command exited with code ${exited}.`);
|
|
3108
3113
|
}
|
|
3109
|
-
|
|
3110
|
-
|
|
3114
|
+
const host = await answeringHost(port);
|
|
3115
|
+
if (host !== null) {
|
|
3116
|
+
return { port, url: `http://${forUrl(host)}:${port}`, stop };
|
|
3111
3117
|
}
|
|
3112
3118
|
await new Promise((resolve5) => setTimeout(resolve5, 250));
|
|
3113
3119
|
}
|
|
@@ -4602,6 +4608,10 @@ function startRunner(options) {
|
|
|
4602
4608
|
stopping: false,
|
|
4603
4609
|
waiting: null
|
|
4604
4610
|
};
|
|
4611
|
+
const setState = (next) => {
|
|
4612
|
+
state = typeof next === "function" ? next(state) : next;
|
|
4613
|
+
options.onChange?.();
|
|
4614
|
+
};
|
|
4605
4615
|
let stopped = false;
|
|
4606
4616
|
let ticking = null;
|
|
4607
4617
|
let pendingNudges = 0;
|
|
@@ -4653,7 +4663,7 @@ function startRunner(options) {
|
|
|
4653
4663
|
return session !== void 0 && session.turns < SESSION_TURNS_CAP ? session.id : null;
|
|
4654
4664
|
};
|
|
4655
4665
|
const idle = () => {
|
|
4656
|
-
|
|
4666
|
+
setState({
|
|
4657
4667
|
running: false,
|
|
4658
4668
|
requestId: null,
|
|
4659
4669
|
agent: null,
|
|
@@ -4661,7 +4671,7 @@ function startRunner(options) {
|
|
|
4661
4671
|
startedAt: null,
|
|
4662
4672
|
stopping: false,
|
|
4663
4673
|
waiting: null
|
|
4664
|
-
};
|
|
4674
|
+
});
|
|
4665
4675
|
};
|
|
4666
4676
|
const rememberLine = (lines2, line) => {
|
|
4667
4677
|
lines2.push(line);
|
|
@@ -4769,14 +4779,15 @@ function startRunner(options) {
|
|
|
4769
4779
|
if (retry !== null) {
|
|
4770
4780
|
observed.retry = retry;
|
|
4771
4781
|
if (active === current)
|
|
4772
|
-
|
|
4782
|
+
setState((value) => ({ ...value, waiting: retry }));
|
|
4773
4783
|
}
|
|
4774
4784
|
const activity = activityFrom(resolved.agent, line, options.cwd);
|
|
4775
4785
|
if (activity !== null) {
|
|
4776
4786
|
if (activity.startsWith("editing"))
|
|
4777
4787
|
observed.edited = true;
|
|
4778
|
-
if (active === current)
|
|
4779
|
-
|
|
4788
|
+
if (active === current) {
|
|
4789
|
+
setState((value) => ({ ...value, activity, waiting: null }));
|
|
4790
|
+
}
|
|
4780
4791
|
}
|
|
4781
4792
|
});
|
|
4782
4793
|
const stderrFlush = lineReader(child.stderr, (line) => rememberLine(lines2, line));
|
|
@@ -4823,7 +4834,7 @@ function startRunner(options) {
|
|
|
4823
4834
|
await reportFailure(request, classifyFailure({ agent: resolved.name, error: "stopped by shutdown" }), []);
|
|
4824
4835
|
return;
|
|
4825
4836
|
}
|
|
4826
|
-
|
|
4837
|
+
setState({
|
|
4827
4838
|
running: true,
|
|
4828
4839
|
requestId: request.id,
|
|
4829
4840
|
agent: resolved.name,
|
|
@@ -4831,7 +4842,7 @@ function startRunner(options) {
|
|
|
4831
4842
|
startedAt: Date.now(),
|
|
4832
4843
|
stopping: false,
|
|
4833
4844
|
waiting: null
|
|
4834
|
-
};
|
|
4845
|
+
});
|
|
4835
4846
|
const observed = {
|
|
4836
4847
|
sessionId: null,
|
|
4837
4848
|
edited: false,
|
|
@@ -4862,7 +4873,7 @@ function startRunner(options) {
|
|
|
4862
4873
|
resolved = cold;
|
|
4863
4874
|
observed.sessionId = null;
|
|
4864
4875
|
observed.retry = null;
|
|
4865
|
-
|
|
4876
|
+
setState((value) => ({ ...value, activity: null, waiting: null }));
|
|
4866
4877
|
outcome = await runChild(request, resolved, lines2, observed);
|
|
4867
4878
|
failure = verdict();
|
|
4868
4879
|
}
|
|
@@ -4936,7 +4947,7 @@ function startRunner(options) {
|
|
|
4936
4947
|
const current = active;
|
|
4937
4948
|
current.cancelled = true;
|
|
4938
4949
|
failed.add(current.requestId);
|
|
4939
|
-
|
|
4950
|
+
setState((value) => ({ ...value, stopping: true, waiting: null }));
|
|
4940
4951
|
current.controller.abort();
|
|
4941
4952
|
try {
|
|
4942
4953
|
current.child?.kill("SIGTERM");
|
|
@@ -4979,6 +4990,193 @@ function startRunner(options) {
|
|
|
4979
4990
|
};
|
|
4980
4991
|
}
|
|
4981
4992
|
|
|
4993
|
+
// ../server/dist/live.js
|
|
4994
|
+
import { createHash } from "crypto";
|
|
4995
|
+
var LIVE_PATH = "/leglas/api/live";
|
|
4996
|
+
var WEBSOCKET_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
|
4997
|
+
function encodeFrame(opcode, payload) {
|
|
4998
|
+
const body = typeof payload === "string" ? Buffer.from(payload) : payload;
|
|
4999
|
+
let header;
|
|
5000
|
+
if (body.length < 126) {
|
|
5001
|
+
header = Buffer.allocUnsafe(2);
|
|
5002
|
+
header[1] = body.length;
|
|
5003
|
+
} else if (body.length <= 65535) {
|
|
5004
|
+
header = Buffer.allocUnsafe(4);
|
|
5005
|
+
header[1] = 126;
|
|
5006
|
+
header.writeUInt16BE(body.length, 2);
|
|
5007
|
+
} else {
|
|
5008
|
+
header = Buffer.allocUnsafe(10);
|
|
5009
|
+
header[1] = 127;
|
|
5010
|
+
header.writeBigUInt64BE(BigInt(body.length), 2);
|
|
5011
|
+
}
|
|
5012
|
+
header[0] = 128 | opcode & 15;
|
|
5013
|
+
return Buffer.concat([header, body], header.length + body.length);
|
|
5014
|
+
}
|
|
5015
|
+
var LIVE_DEBOUNCE_MS = 50;
|
|
5016
|
+
function createCoalescer(emit, options = {}) {
|
|
5017
|
+
const windowMs = options.windowMs ?? LIVE_DEBOUNCE_MS;
|
|
5018
|
+
const setLater = options.setTimeout ?? ((callback, ms) => {
|
|
5019
|
+
const timer = setTimeout(callback, ms);
|
|
5020
|
+
timer.unref?.();
|
|
5021
|
+
return timer;
|
|
5022
|
+
});
|
|
5023
|
+
const clearLater = options.clearTimeout ?? ((handle) => clearTimeout(handle));
|
|
5024
|
+
const pending = /* @__PURE__ */ new Map();
|
|
5025
|
+
let closed = false;
|
|
5026
|
+
return {
|
|
5027
|
+
schedule(change) {
|
|
5028
|
+
if (closed)
|
|
5029
|
+
return;
|
|
5030
|
+
const waiting = pending.get(change);
|
|
5031
|
+
if (waiting !== void 0)
|
|
5032
|
+
clearLater(waiting);
|
|
5033
|
+
pending.set(change, setLater(() => {
|
|
5034
|
+
pending.delete(change);
|
|
5035
|
+
if (!closed)
|
|
5036
|
+
emit(change);
|
|
5037
|
+
}, windowMs));
|
|
5038
|
+
},
|
|
5039
|
+
close() {
|
|
5040
|
+
closed = true;
|
|
5041
|
+
for (const handle of pending.values())
|
|
5042
|
+
clearLater(handle);
|
|
5043
|
+
pending.clear();
|
|
5044
|
+
}
|
|
5045
|
+
};
|
|
5046
|
+
}
|
|
5047
|
+
function createLiveHub(_options = {}) {
|
|
5048
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
5049
|
+
const drop = (listener) => {
|
|
5050
|
+
listeners.delete(listener);
|
|
5051
|
+
};
|
|
5052
|
+
const write2 = (listener, opcode, payload) => {
|
|
5053
|
+
if (listener.socket.destroyed || !listener.socket.writable) {
|
|
5054
|
+
drop(listener);
|
|
5055
|
+
return false;
|
|
5056
|
+
}
|
|
5057
|
+
try {
|
|
5058
|
+
listener.socket.write(encodeFrame(opcode, payload));
|
|
5059
|
+
return true;
|
|
5060
|
+
} catch {
|
|
5061
|
+
drop(listener);
|
|
5062
|
+
listener.socket.destroy();
|
|
5063
|
+
return false;
|
|
5064
|
+
}
|
|
5065
|
+
};
|
|
5066
|
+
const read = (listener, chunk) => {
|
|
5067
|
+
const incoming = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
5068
|
+
listener.buffered = listener.buffered.length === 0 ? incoming : Buffer.concat([listener.buffered, incoming]);
|
|
5069
|
+
while (listener.buffered.length >= 2) {
|
|
5070
|
+
const first = listener.buffered[0] ?? 0;
|
|
5071
|
+
const second = listener.buffered[1] ?? 0;
|
|
5072
|
+
let length = second & 127;
|
|
5073
|
+
let offset = 2;
|
|
5074
|
+
if (length === 126) {
|
|
5075
|
+
if (listener.buffered.length < 4)
|
|
5076
|
+
return;
|
|
5077
|
+
length = listener.buffered.readUInt16BE(2);
|
|
5078
|
+
offset = 4;
|
|
5079
|
+
} else if (length === 127) {
|
|
5080
|
+
if (listener.buffered.length < 10)
|
|
5081
|
+
return;
|
|
5082
|
+
const wide = listener.buffered.readBigUInt64BE(2);
|
|
5083
|
+
if (wide > BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
5084
|
+
drop(listener);
|
|
5085
|
+
listener.socket.destroy();
|
|
5086
|
+
return;
|
|
5087
|
+
}
|
|
5088
|
+
length = Number(wide);
|
|
5089
|
+
offset = 10;
|
|
5090
|
+
}
|
|
5091
|
+
const masked = (second & 128) !== 0;
|
|
5092
|
+
if (!masked) {
|
|
5093
|
+
drop(listener);
|
|
5094
|
+
listener.socket.destroy();
|
|
5095
|
+
return;
|
|
5096
|
+
}
|
|
5097
|
+
if (listener.buffered.length < offset + 4 + length)
|
|
5098
|
+
return;
|
|
5099
|
+
const mask = listener.buffered.subarray(offset, offset + 4);
|
|
5100
|
+
offset += 4;
|
|
5101
|
+
const payload = Buffer.from(listener.buffered.subarray(offset, offset + length));
|
|
5102
|
+
for (let index = 0; index < payload.length; index += 1) {
|
|
5103
|
+
payload[index] = (payload[index] ?? 0) ^ (mask[index % 4] ?? 0);
|
|
5104
|
+
}
|
|
5105
|
+
listener.buffered = listener.buffered.subarray(offset + length);
|
|
5106
|
+
const opcode = first & 15;
|
|
5107
|
+
if (opcode === 8) {
|
|
5108
|
+
write2(listener, 8, payload);
|
|
5109
|
+
drop(listener);
|
|
5110
|
+
listener.socket.destroy();
|
|
5111
|
+
return;
|
|
5112
|
+
}
|
|
5113
|
+
if (opcode === 9)
|
|
5114
|
+
write2(listener, 10, payload);
|
|
5115
|
+
}
|
|
5116
|
+
};
|
|
5117
|
+
return {
|
|
5118
|
+
nudge: (change) => {
|
|
5119
|
+
if (listeners.size === 0)
|
|
5120
|
+
return;
|
|
5121
|
+
const frame = encodeFrame(1, JSON.stringify({ changed: change }));
|
|
5122
|
+
for (const listener of [...listeners]) {
|
|
5123
|
+
if (listener.socket.destroyed || !listener.socket.writable) {
|
|
5124
|
+
drop(listener);
|
|
5125
|
+
continue;
|
|
5126
|
+
}
|
|
5127
|
+
try {
|
|
5128
|
+
listener.socket.write(frame);
|
|
5129
|
+
} catch {
|
|
5130
|
+
drop(listener);
|
|
5131
|
+
listener.socket.destroy();
|
|
5132
|
+
}
|
|
5133
|
+
}
|
|
5134
|
+
},
|
|
5135
|
+
upgrade: (req, socket, head) => {
|
|
5136
|
+
const path = (req.url ?? "/").split("?")[0] ?? "/";
|
|
5137
|
+
if (req.method !== "GET" || path !== LIVE_PATH)
|
|
5138
|
+
return false;
|
|
5139
|
+
const upgrade = req.headers.upgrade;
|
|
5140
|
+
const key = req.headers["sec-websocket-key"];
|
|
5141
|
+
if (typeof upgrade !== "string" || upgrade.toLowerCase() !== "websocket" || typeof key !== "string" || key.trim() === "") {
|
|
5142
|
+
socket.destroy();
|
|
5143
|
+
return false;
|
|
5144
|
+
}
|
|
5145
|
+
const accept = createHash("sha1").update(key + WEBSOCKET_GUID).digest("base64");
|
|
5146
|
+
try {
|
|
5147
|
+
socket.write(`HTTP/1.1 101 Switching Protocols\r
|
|
5148
|
+
Upgrade: websocket\r
|
|
5149
|
+
Connection: Upgrade\r
|
|
5150
|
+
Sec-WebSocket-Accept: ${accept}\r
|
|
5151
|
+
\r
|
|
5152
|
+
`);
|
|
5153
|
+
} catch {
|
|
5154
|
+
socket.destroy();
|
|
5155
|
+
return false;
|
|
5156
|
+
}
|
|
5157
|
+
const listener = { socket, buffered: Buffer.alloc(0) };
|
|
5158
|
+
listeners.add(listener);
|
|
5159
|
+
socket.on("data", (chunk) => read(listener, chunk));
|
|
5160
|
+
socket.once("error", () => drop(listener));
|
|
5161
|
+
socket.once("end", () => drop(listener));
|
|
5162
|
+
socket.once("close", () => drop(listener));
|
|
5163
|
+
if (head.length > 0)
|
|
5164
|
+
read(listener, head);
|
|
5165
|
+
return true;
|
|
5166
|
+
},
|
|
5167
|
+
close: async () => {
|
|
5168
|
+
for (const listener of [...listeners]) {
|
|
5169
|
+
write2(listener, 8, Buffer.alloc(0));
|
|
5170
|
+
drop(listener);
|
|
5171
|
+
listener.socket.destroy();
|
|
5172
|
+
}
|
|
5173
|
+
},
|
|
5174
|
+
get listening() {
|
|
5175
|
+
return listeners.size;
|
|
5176
|
+
}
|
|
5177
|
+
};
|
|
5178
|
+
}
|
|
5179
|
+
|
|
4982
5180
|
// ../server/dist/renames.js
|
|
4983
5181
|
import { mkdir as mkdir6, readFile as readFile10, writeFile as writeFile6 } from "fs/promises";
|
|
4984
5182
|
import { dirname as dirname6, join as join10 } from "path";
|
|
@@ -5012,11 +5210,11 @@ function resolveTitle(input, titles, renames) {
|
|
|
5012
5210
|
}
|
|
5013
5211
|
|
|
5014
5212
|
// ../server/dist/server.js
|
|
5015
|
-
import { createReadStream, existsSync as existsSync3, statSync } from "fs";
|
|
5213
|
+
import { createReadStream, existsSync as existsSync3, statSync, unwatchFile, watch as watchFs, watchFile } from "fs";
|
|
5016
5214
|
import { mkdir as mkdir8, readdir as readdir3, writeFile as writeFile8 } from "fs/promises";
|
|
5017
5215
|
import http2 from "http";
|
|
5018
5216
|
import net3 from "net";
|
|
5019
|
-
import { extname as extname3, join as join12, normalize, relative as relative3 } from "path";
|
|
5217
|
+
import { basename as basename3, dirname as dirname8, extname as extname3, join as join12, normalize, relative as relative3 } from "path";
|
|
5020
5218
|
|
|
5021
5219
|
// ../server/dist/server-info.js
|
|
5022
5220
|
import { lstat as lstat2, mkdir as mkdir7, readFile as readFile11, rename as rename2, rm as rm4, writeFile as writeFile7 } from "fs/promises";
|
|
@@ -5201,6 +5399,204 @@ function serveShellFile(res, shellDir, urlPath) {
|
|
|
5201
5399
|
const isRoot = relative6 === "" || relative6 === "." || relative6 === "/";
|
|
5202
5400
|
return serveFrom(res, shellDir, isRoot ? "index.html" : relative6);
|
|
5203
5401
|
}
|
|
5402
|
+
var HEALTH_PROBE_MS = 3e3;
|
|
5403
|
+
function fileStamp(path) {
|
|
5404
|
+
try {
|
|
5405
|
+
const stat5 = statSync(path);
|
|
5406
|
+
return `${stat5.dev}:${stat5.ino}:${stat5.size}:${stat5.mtimeMs}:${stat5.ctimeMs}`;
|
|
5407
|
+
} catch {
|
|
5408
|
+
return null;
|
|
5409
|
+
}
|
|
5410
|
+
}
|
|
5411
|
+
function watchLiveFiles(cwd, configPath, live) {
|
|
5412
|
+
const leglasDir = join12(cwd, ".leglas");
|
|
5413
|
+
const targets = [
|
|
5414
|
+
{ path: join12(cwd, LOCAL_PREVIEWS_PATH), change: "config" },
|
|
5415
|
+
{ path: join12(cwd, REQUESTS_PATH), change: "requests" },
|
|
5416
|
+
{ path: join12(cwd, ANNOTATIONS_PATH), change: "requests" }
|
|
5417
|
+
];
|
|
5418
|
+
const byName = new Map(targets.map((target) => [basename3(target.path), target]));
|
|
5419
|
+
const known = new Map(targets.map((target) => [target.path, fileStamp(target.path)]));
|
|
5420
|
+
const coalescer = createCoalescer((change) => live.nudge(change));
|
|
5421
|
+
const watchers = /* @__PURE__ */ new Set();
|
|
5422
|
+
const fallback = /* @__PURE__ */ new Map();
|
|
5423
|
+
let leglasWatcher = null;
|
|
5424
|
+
let retry = null;
|
|
5425
|
+
let closed = false;
|
|
5426
|
+
const nudgeSoon = (change) => coalescer.schedule(change);
|
|
5427
|
+
const scanLeglas = (notify) => {
|
|
5428
|
+
for (const target of targets) {
|
|
5429
|
+
const next = fileStamp(target.path);
|
|
5430
|
+
if (next === known.get(target.path))
|
|
5431
|
+
continue;
|
|
5432
|
+
known.set(target.path, next);
|
|
5433
|
+
if (notify)
|
|
5434
|
+
nudgeSoon(target.change);
|
|
5435
|
+
}
|
|
5436
|
+
};
|
|
5437
|
+
const fallbackWatch = (target) => {
|
|
5438
|
+
if (closed || fallback.has(target.path))
|
|
5439
|
+
return;
|
|
5440
|
+
const listener = () => {
|
|
5441
|
+
const next = fileStamp(target.path);
|
|
5442
|
+
if (next === known.get(target.path))
|
|
5443
|
+
return;
|
|
5444
|
+
known.set(target.path, next);
|
|
5445
|
+
nudgeSoon(target.change);
|
|
5446
|
+
};
|
|
5447
|
+
fallback.set(target.path, listener);
|
|
5448
|
+
watchFile(target.path, { persistent: false, interval: 250 }, listener);
|
|
5449
|
+
};
|
|
5450
|
+
const fallbackLeglas = () => {
|
|
5451
|
+
for (const target of targets)
|
|
5452
|
+
fallbackWatch(target);
|
|
5453
|
+
};
|
|
5454
|
+
const retryLeglas = () => {
|
|
5455
|
+
if (closed || retry !== null)
|
|
5456
|
+
return;
|
|
5457
|
+
retry = setTimeout(() => {
|
|
5458
|
+
retry = null;
|
|
5459
|
+
armLeglas(true);
|
|
5460
|
+
}, 250);
|
|
5461
|
+
retry.unref?.();
|
|
5462
|
+
};
|
|
5463
|
+
const armLeglas = (notify) => {
|
|
5464
|
+
if (closed)
|
|
5465
|
+
return;
|
|
5466
|
+
scanLeglas(notify);
|
|
5467
|
+
let directory = false;
|
|
5468
|
+
try {
|
|
5469
|
+
directory = statSync(leglasDir).isDirectory();
|
|
5470
|
+
} catch {
|
|
5471
|
+
directory = false;
|
|
5472
|
+
}
|
|
5473
|
+
if (!directory) {
|
|
5474
|
+
if (leglasWatcher !== null) {
|
|
5475
|
+
watchers.delete(leglasWatcher);
|
|
5476
|
+
leglasWatcher.close();
|
|
5477
|
+
leglasWatcher = null;
|
|
5478
|
+
}
|
|
5479
|
+
retryLeglas();
|
|
5480
|
+
return;
|
|
5481
|
+
}
|
|
5482
|
+
if (leglasWatcher !== null)
|
|
5483
|
+
return;
|
|
5484
|
+
try {
|
|
5485
|
+
const watcher = watchFs(leglasDir, { persistent: false }, (_event, filename) => {
|
|
5486
|
+
if (filename === null) {
|
|
5487
|
+
scanLeglas(true);
|
|
5488
|
+
return;
|
|
5489
|
+
}
|
|
5490
|
+
const name = Buffer.isBuffer(filename) ? filename.toString() : filename;
|
|
5491
|
+
const target = byName.get(name);
|
|
5492
|
+
if (target === void 0)
|
|
5493
|
+
return;
|
|
5494
|
+
known.set(target.path, fileStamp(target.path));
|
|
5495
|
+
nudgeSoon(target.change);
|
|
5496
|
+
});
|
|
5497
|
+
watcher.on("error", () => {
|
|
5498
|
+
if (leglasWatcher !== watcher)
|
|
5499
|
+
return;
|
|
5500
|
+
watchers.delete(watcher);
|
|
5501
|
+
watcher.close();
|
|
5502
|
+
leglasWatcher = null;
|
|
5503
|
+
fallbackLeglas();
|
|
5504
|
+
retryLeglas();
|
|
5505
|
+
});
|
|
5506
|
+
leglasWatcher = watcher;
|
|
5507
|
+
watchers.add(watcher);
|
|
5508
|
+
} catch {
|
|
5509
|
+
fallbackLeglas();
|
|
5510
|
+
retryLeglas();
|
|
5511
|
+
}
|
|
5512
|
+
};
|
|
5513
|
+
try {
|
|
5514
|
+
const watcher = watchFs(cwd, { persistent: false }, (_event, filename) => {
|
|
5515
|
+
const name = filename === null ? null : Buffer.isBuffer(filename) ? filename.toString() : filename;
|
|
5516
|
+
if (name === null || name === ".leglas")
|
|
5517
|
+
armLeglas(true);
|
|
5518
|
+
});
|
|
5519
|
+
watcher.on("error", () => {
|
|
5520
|
+
watchers.delete(watcher);
|
|
5521
|
+
watcher.close();
|
|
5522
|
+
fallbackLeglas();
|
|
5523
|
+
});
|
|
5524
|
+
watchers.add(watcher);
|
|
5525
|
+
} catch {
|
|
5526
|
+
fallbackLeglas();
|
|
5527
|
+
}
|
|
5528
|
+
if (configPath !== null) {
|
|
5529
|
+
try {
|
|
5530
|
+
const directory = dirname8(configPath);
|
|
5531
|
+
const name = basename3(configPath);
|
|
5532
|
+
const target = { path: configPath, change: "config" };
|
|
5533
|
+
known.set(configPath, fileStamp(configPath));
|
|
5534
|
+
const watcher = watchFs(directory, { persistent: false }, (_event, filename) => {
|
|
5535
|
+
const changed = filename === null ? null : Buffer.isBuffer(filename) ? filename.toString() : filename;
|
|
5536
|
+
if (changed === null || changed === name)
|
|
5537
|
+
nudgeSoon("config");
|
|
5538
|
+
});
|
|
5539
|
+
watcher.on("error", () => {
|
|
5540
|
+
watchers.delete(watcher);
|
|
5541
|
+
watcher.close();
|
|
5542
|
+
fallbackWatch(target);
|
|
5543
|
+
});
|
|
5544
|
+
watchers.add(watcher);
|
|
5545
|
+
} catch {
|
|
5546
|
+
fallbackWatch({ path: configPath, change: "config" });
|
|
5547
|
+
}
|
|
5548
|
+
}
|
|
5549
|
+
armLeglas(false);
|
|
5550
|
+
return {
|
|
5551
|
+
close: () => {
|
|
5552
|
+
if (closed)
|
|
5553
|
+
return;
|
|
5554
|
+
closed = true;
|
|
5555
|
+
if (retry !== null)
|
|
5556
|
+
clearTimeout(retry);
|
|
5557
|
+
coalescer.close();
|
|
5558
|
+
for (const watcher of watchers)
|
|
5559
|
+
watcher.close();
|
|
5560
|
+
watchers.clear();
|
|
5561
|
+
for (const [path, listener] of fallback)
|
|
5562
|
+
unwatchFile(path, listener);
|
|
5563
|
+
fallback.clear();
|
|
5564
|
+
leglasWatcher = null;
|
|
5565
|
+
}
|
|
5566
|
+
};
|
|
5567
|
+
}
|
|
5568
|
+
function watchHealth(target, live) {
|
|
5569
|
+
let previous = null;
|
|
5570
|
+
let probing = false;
|
|
5571
|
+
let closed = false;
|
|
5572
|
+
const timer = setInterval(() => {
|
|
5573
|
+
if (live.listening === 0) {
|
|
5574
|
+
previous = null;
|
|
5575
|
+
return;
|
|
5576
|
+
}
|
|
5577
|
+
if (probing)
|
|
5578
|
+
return;
|
|
5579
|
+
probing = true;
|
|
5580
|
+
void probe(target).then((reachable) => {
|
|
5581
|
+
if (closed || live.listening === 0) {
|
|
5582
|
+
previous = null;
|
|
5583
|
+
return;
|
|
5584
|
+
}
|
|
5585
|
+
if (previous !== null && previous !== reachable)
|
|
5586
|
+
live.nudge("health");
|
|
5587
|
+
previous = reachable;
|
|
5588
|
+
}).finally(() => {
|
|
5589
|
+
probing = false;
|
|
5590
|
+
});
|
|
5591
|
+
}, HEALTH_PROBE_MS);
|
|
5592
|
+
timer.unref?.();
|
|
5593
|
+
return {
|
|
5594
|
+
close: () => {
|
|
5595
|
+
closed = true;
|
|
5596
|
+
clearInterval(timer);
|
|
5597
|
+
}
|
|
5598
|
+
};
|
|
5599
|
+
}
|
|
5204
5600
|
function snapshotConfig(cwd) {
|
|
5205
5601
|
const path = findConfigFile(cwd);
|
|
5206
5602
|
if (path === null)
|
|
@@ -5271,12 +5667,14 @@ async function bind(server, requested) {
|
|
|
5271
5667
|
async function startServer(options) {
|
|
5272
5668
|
const { config, configErrors = [], configWarnings = [], shellDir = null, project = "", cwd = process.cwd(), leglasCommand = "npx -y leglas", fileMounts = /* @__PURE__ */ new Map(), detect = () => detectAgents() } = options;
|
|
5273
5669
|
const browserPool = options.pool ?? createBrowserPool();
|
|
5670
|
+
const live = options.live ?? createLiveHub();
|
|
5274
5671
|
if (options.pool === void 0) {
|
|
5275
5672
|
void reapOrphanedBrowsers().catch(() => {
|
|
5276
5673
|
});
|
|
5277
5674
|
}
|
|
5278
5675
|
const target = config?.devServer ?? "http://localhost:3000";
|
|
5279
5676
|
const proxy = createProxyHandler({ target });
|
|
5677
|
+
const bootConfigPath = findConfigFile(cwd);
|
|
5280
5678
|
const bootConfigSnapshot = snapshotConfig(cwd);
|
|
5281
5679
|
let lastSeen = null;
|
|
5282
5680
|
const externallyAttached = () => lastSeen !== null && Date.now() - lastSeen < ATTACHED_WINDOW_MS;
|
|
@@ -5501,14 +5899,14 @@ async function startServer(options) {
|
|
|
5501
5899
|
return sendJson(res, 400, { ok: false, error: "Unknown preview, or empty request." });
|
|
5502
5900
|
}
|
|
5503
5901
|
const intent = (parsed.intent ?? "").trim();
|
|
5504
|
-
const
|
|
5902
|
+
const live2 = (await readRequests(cwd).catch(() => [])).filter((entry) => entry.status === "queued" || entry.status === "picked-up");
|
|
5505
5903
|
const sameNotes = (entry) => {
|
|
5506
5904
|
const before = [...entry.notes ?? []].sort().join(",");
|
|
5507
5905
|
return before === notes.map((note) => note.id).sort().join(",");
|
|
5508
5906
|
};
|
|
5509
5907
|
const compare = typeof parsed.compare === "string" && parsed.compare !== preview.title ? previews.find((entry) => entry.title === parsed.compare) ?? null : null;
|
|
5510
5908
|
const sameContext = (entry) => (entry.compare ?? null) === (compare?.title ?? null) && [...entry.references ?? []].sort().join(",") === [...references].sort().join(",");
|
|
5511
|
-
if (
|
|
5909
|
+
if (live2.some((entry) => entry.title === preview.title && entry.intent === intent && // The same words in the other mode are not the same request:
|
|
5512
5910
|
// one forks the direction and the other rewrites it. Only a
|
|
5513
5911
|
// genuine repeat is refused.
|
|
5514
5912
|
(entry.mode ?? "replace") === mode && sameNotes(entry) && sameContext(entry))) {
|
|
@@ -6019,12 +6417,16 @@ async function startServer(options) {
|
|
|
6019
6417
|
socket.once("close", () => sockets.delete(socket));
|
|
6020
6418
|
});
|
|
6021
6419
|
server.on("upgrade", (req, socket, head) => {
|
|
6420
|
+
if (live.upgrade(req, socket, head))
|
|
6421
|
+
return;
|
|
6022
6422
|
const path = (req.url ?? "/").split("?")[0] ?? "/";
|
|
6023
6423
|
if (path.startsWith(`${LEGLAS_PREFIX}/`))
|
|
6024
6424
|
return socket.destroy();
|
|
6025
6425
|
proxy.upgrade(req, socket, head);
|
|
6026
6426
|
});
|
|
6027
6427
|
const port = await bind(server, options.port ?? DEFAULT_PORT);
|
|
6428
|
+
const liveFiles = watchLiveFiles(cwd, bootConfigPath, live);
|
|
6429
|
+
const liveHealth = watchHealth(target, live);
|
|
6028
6430
|
await pruneCaptures(cwd, (await readRequests(cwd).catch(() => [])).map((request) => request.id)).catch(() => {
|
|
6029
6431
|
});
|
|
6030
6432
|
await writeServerInfo(cwd, {
|
|
@@ -6036,6 +6438,7 @@ async function startServer(options) {
|
|
|
6036
6438
|
runner = startRunner({
|
|
6037
6439
|
cwd,
|
|
6038
6440
|
externallyAttached,
|
|
6441
|
+
onChange: () => live.nudge("requests"),
|
|
6039
6442
|
leglasCommand,
|
|
6040
6443
|
...options.codexAppServer === void 0 ? {} : { codexAppServer: options.codexAppServer },
|
|
6041
6444
|
...options.claudeAgentSession === void 0 ? {} : { claudeAgentSession: options.claudeAgentSession }
|
|
@@ -6047,7 +6450,9 @@ async function startServer(options) {
|
|
|
6047
6450
|
close: () => {
|
|
6048
6451
|
if (closePromise !== null)
|
|
6049
6452
|
return closePromise;
|
|
6050
|
-
|
|
6453
|
+
liveFiles.close();
|
|
6454
|
+
liveHealth.close();
|
|
6455
|
+
closePromise = Promise.all([runner.stop(), browserPool.close(), live.close()]).then(() => new Promise((done) => {
|
|
6051
6456
|
for (const socket of sockets)
|
|
6052
6457
|
socket.destroy();
|
|
6053
6458
|
sockets.clear();
|
|
@@ -6070,7 +6475,7 @@ function surfaceOf(url) {
|
|
|
6070
6475
|
return null;
|
|
6071
6476
|
}
|
|
6072
6477
|
function exportNameFor(to) {
|
|
6073
|
-
const stem =
|
|
6478
|
+
const stem = basename4(to, extname4(to));
|
|
6074
6479
|
return stem.split(/[^a-zA-Z0-9]+/).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
|
|
6075
6480
|
}
|
|
6076
6481
|
function planKeep(options) {
|
|
@@ -6159,7 +6564,7 @@ async function runInit(options, deps) {
|
|
|
6159
6564
|
// src/run-keep.ts
|
|
6160
6565
|
import { existsSync as existsSync4 } from "fs";
|
|
6161
6566
|
import { mkdir as mkdir9, readFile as readFile13, rm as rm5, writeFile as writeFile10 } from "fs/promises";
|
|
6162
|
-
import { dirname as
|
|
6567
|
+
import { dirname as dirname9, join as join14 } from "path";
|
|
6163
6568
|
|
|
6164
6569
|
// src/resolve-title.ts
|
|
6165
6570
|
function resolveOrExplain(input, titles, renames) {
|
|
@@ -6212,7 +6617,7 @@ async function runKeep(options, deps) {
|
|
|
6212
6617
|
return fail(`${plan.move.to} already exists. Choose another destination or move it aside.`);
|
|
6213
6618
|
}
|
|
6214
6619
|
const source = await readFile13(from, "utf8");
|
|
6215
|
-
await mkdir9(
|
|
6620
|
+
await mkdir9(dirname9(to), { recursive: true });
|
|
6216
6621
|
await writeFile10(to, renameExport(source, plan.exportName), "utf8");
|
|
6217
6622
|
await rm5(join14(options.cwd, plan.removeDir), { recursive: true, force: true });
|
|
6218
6623
|
const dropped = await dropLocalPreviews(options.cwd, plan.dropTitles);
|
|
@@ -6250,7 +6655,7 @@ async function runKeep(options, deps) {
|
|
|
6250
6655
|
// src/run-new.ts
|
|
6251
6656
|
import { existsSync as existsSync5 } from "fs";
|
|
6252
6657
|
import { mkdir as mkdir10, readFile as readFile14, writeFile as writeFile11 } from "fs/promises";
|
|
6253
|
-
import { dirname as
|
|
6658
|
+
import { dirname as dirname10, join as join15 } from "path";
|
|
6254
6659
|
async function readIfPresent2(path) {
|
|
6255
6660
|
try {
|
|
6256
6661
|
return await readFile14(path, "utf8");
|
|
@@ -6303,7 +6708,7 @@ async function runNew(options, deps) {
|
|
|
6303
6708
|
const written = [];
|
|
6304
6709
|
for (const write2 of plan.writes) {
|
|
6305
6710
|
const target = join15(options.cwd, write2.path);
|
|
6306
|
-
await mkdir10(
|
|
6711
|
+
await mkdir10(dirname10(target), { recursive: true });
|
|
6307
6712
|
await writeFile11(target, write2.contents, "utf8");
|
|
6308
6713
|
written.push(write2.path);
|
|
6309
6714
|
}
|
|
@@ -6664,7 +7069,7 @@ async function runShow(options, deps) {
|
|
|
6664
7069
|
// src/run-watch.ts
|
|
6665
7070
|
import { spawn as spawn3 } from "child_process";
|
|
6666
7071
|
import { mkdir as mkdir11, readFile as readFile16, writeFile as writeFile13 } from "fs/promises";
|
|
6667
|
-
import { dirname as
|
|
7072
|
+
import { dirname as dirname11, join as join17 } from "path";
|
|
6668
7073
|
var POLL_MS2 = 2e3;
|
|
6669
7074
|
var HEARTBEAT_TIMEOUT_MS = 1e3;
|
|
6670
7075
|
async function saveTemplate(cwd, run4) {
|
|
@@ -6678,7 +7083,7 @@ async function saveTemplate(cwd, run4) {
|
|
|
6678
7083
|
} catch {
|
|
6679
7084
|
}
|
|
6680
7085
|
config.run = run4;
|
|
6681
|
-
await mkdir11(
|
|
7086
|
+
await mkdir11(dirname11(path), { recursive: true });
|
|
6682
7087
|
await writeFile13(path, `${JSON.stringify(config, null, 2)}
|
|
6683
7088
|
`, "utf8");
|
|
6684
7089
|
}
|
|
@@ -6852,13 +7257,13 @@ async function runClassify(options, deps) {
|
|
|
6852
7257
|
import { existsSync as existsSync6 } from "fs";
|
|
6853
7258
|
import { realpath as realpath4 } from "fs/promises";
|
|
6854
7259
|
import { createRequire } from "module";
|
|
6855
|
-
import { basename as
|
|
7260
|
+
import { basename as basename6, dirname as dirname12, join as join19, relative as relative5, resolve as resolve4 } from "path";
|
|
6856
7261
|
import { fileURLToPath } from "url";
|
|
6857
7262
|
|
|
6858
7263
|
// src/dev-server-owner.ts
|
|
6859
7264
|
import { execFile as execFile2 } from "child_process";
|
|
6860
7265
|
import { realpath as realpath3 } from "fs/promises";
|
|
6861
|
-
import { basename as
|
|
7266
|
+
import { basename as basename5, isAbsolute as isAbsolute2, relative as relative4, resolve as resolve3 } from "path";
|
|
6862
7267
|
import { promisify as promisify2 } from "util";
|
|
6863
7268
|
var run2 = promisify2(execFile2);
|
|
6864
7269
|
var LOCAL_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1", "[::1]"]);
|
|
@@ -6931,18 +7336,18 @@ function devServerOwnerWarning(origin, projectRoot, owners) {
|
|
|
6931
7336
|
const port = localDevServerPort(origin);
|
|
6932
7337
|
if (port === null || owners.length === 0) return null;
|
|
6933
7338
|
if (owners.some((owner2) => isInside(projectRoot, owner2.cwd))) return null;
|
|
6934
|
-
const owner =
|
|
6935
|
-
const project =
|
|
7339
|
+
const owner = basename5(resolve3(owners[0]?.cwd ?? "")) || "another project";
|
|
7340
|
+
const project = basename5(resolve3(projectRoot)) || "this project";
|
|
6936
7341
|
return `Port ${port} appears to be served from ${owner}, outside this project (${project}). Check devServer in your Leglas config or use --user-port.`;
|
|
6937
7342
|
}
|
|
6938
7343
|
|
|
6939
7344
|
// src/run.ts
|
|
6940
7345
|
function findShellDir() {
|
|
6941
|
-
const bundled = join19(
|
|
7346
|
+
const bundled = join19(dirname12(fileURLToPath(import.meta.url)), "shell");
|
|
6942
7347
|
if (existsSync6(join19(bundled, "index.html"))) return bundled;
|
|
6943
7348
|
try {
|
|
6944
7349
|
const require2 = createRequire(import.meta.url);
|
|
6945
|
-
return
|
|
7350
|
+
return dirname12(require2.resolve("@leglas/shell/dist/index.html"));
|
|
6946
7351
|
} catch {
|
|
6947
7352
|
return null;
|
|
6948
7353
|
}
|
|
@@ -6953,7 +7358,7 @@ function shellWord(value) {
|
|
|
6953
7358
|
return `'${value.replaceAll("'", `'\\''`)}'`;
|
|
6954
7359
|
}
|
|
6955
7360
|
function embeddedLeglasCommand() {
|
|
6956
|
-
const entry = join19(
|
|
7361
|
+
const entry = join19(dirname12(fileURLToPath(import.meta.url)), "bin.js");
|
|
6957
7362
|
if (!existsSync6(entry)) return "npx -y leglas";
|
|
6958
7363
|
return [process.execPath, entry].map(shellWord).join(" ");
|
|
6959
7364
|
}
|
|
@@ -6997,10 +7402,10 @@ async function run3(options, deps) {
|
|
|
6997
7402
|
for (let suffix = 2; fileMounts.has(slug); suffix += 1) {
|
|
6998
7403
|
slug = `${worktreeSlug(preview.title) || "file"}-${suffix}`;
|
|
6999
7404
|
}
|
|
7000
|
-
fileMounts.set(slug,
|
|
7405
|
+
fileMounts.set(slug, dirname12(absolute));
|
|
7001
7406
|
previews.push({
|
|
7002
7407
|
...preview,
|
|
7003
|
-
url: `${FILES_PREFIX}/${slug}/${encodeURIComponent(
|
|
7408
|
+
url: `${FILES_PREFIX}/${slug}/${encodeURIComponent(basename6(absolute))}`
|
|
7004
7409
|
});
|
|
7005
7410
|
continue;
|
|
7006
7411
|
}
|
|
@@ -7030,8 +7435,8 @@ async function run3(options, deps) {
|
|
|
7030
7435
|
}
|
|
7031
7436
|
const config = merged === null ? null : { ...merged, previews };
|
|
7032
7437
|
const configWarnings = [];
|
|
7033
|
-
const projectRoot = await realpath4(loaded.path === null ? options.cwd :
|
|
7034
|
-
() => resolve4(loaded.path === null ? options.cwd :
|
|
7438
|
+
const projectRoot = await realpath4(loaded.path === null ? options.cwd : dirname12(loaded.path)).catch(
|
|
7439
|
+
() => resolve4(loaded.path === null ? options.cwd : dirname12(loaded.path))
|
|
7035
7440
|
);
|
|
7036
7441
|
const ownerWarning = needsApp && app === null ? inspectLocalDevServer(devServer).then((owners) => devServerOwnerWarning(devServer, projectRoot, owners)).catch(() => null) : Promise.resolve(null);
|
|
7037
7442
|
const serverPromise = startServer({
|