dsh-claude-design 2.7.1 → 2.8.0
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/NOTICE.md +2 -1
- package/lib/client.js +1 -1
- package/lib/index.js +1 -1
- package/package.json +1 -1
- package/skills/claude-design/SKILL.md +5 -4
- package/skills/claude-design/scripts/artifact.mjs +359 -85
- package/skills/claude-design/scripts/editor/assets/{index-DyKwtQBI.js → index-CYLuwRoc.js} +178 -178
- package/skills/claude-design/scripts/editor/index.html +1 -1
- package/skills/claude-design/scripts/server.mjs +1146 -408
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// claude-design: local editor server (prebuilt; do not edit).
|
|
3
3
|
|
|
4
|
-
import
|
|
5
|
-
import
|
|
4
|
+
import fs13 from "node:fs";
|
|
5
|
+
import path13 from "node:path";
|
|
6
6
|
|
|
7
|
-
import { createHash as
|
|
8
|
-
import
|
|
7
|
+
import { createHash as createHash5, randomBytes as randomBytes2, randomUUID } from "node:crypto";
|
|
8
|
+
import fs12 from "node:fs";
|
|
9
9
|
import fsp4 from "node:fs/promises";
|
|
10
|
-
import
|
|
11
|
-
import path10 from "node:path";
|
|
10
|
+
import path12 from "node:path";
|
|
12
11
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
13
12
|
|
|
14
13
|
import { spawn } from "node:child_process";
|
|
@@ -768,6 +767,7 @@ data: ${JSON.stringify(snap)}
|
|
|
768
767
|
return { handle, presence, deliver, sendRequest, requestStates, close };
|
|
769
768
|
}
|
|
770
769
|
|
|
770
|
+
import { createHash } from "node:crypto";
|
|
771
771
|
import fs3 from "node:fs";
|
|
772
772
|
import net from "node:net";
|
|
773
773
|
import os2 from "node:os";
|
|
@@ -800,17 +800,15 @@ async function runningServer(root, kind, probe) {
|
|
|
800
800
|
return null;
|
|
801
801
|
}
|
|
802
802
|
}
|
|
803
|
-
function
|
|
804
|
-
|
|
805
|
-
fs3.writeFileSync(file, `${JSON.stringify(state, null, 2)}
|
|
803
|
+
function writeState(state) {
|
|
804
|
+
fs3.writeFileSync(path2.join(state.root, STATE_FILE), `${JSON.stringify(state, null, 2)}
|
|
806
805
|
`);
|
|
807
806
|
writeGitignore(state.root, state.kind);
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
};
|
|
813
|
-
for (const signal of signals) process.on(signal, shutdown);
|
|
807
|
+
}
|
|
808
|
+
function removeState(root) {
|
|
809
|
+
const state = readState(root);
|
|
810
|
+
if (state && state.pid !== process.pid && alive(state.pid)) return;
|
|
811
|
+
fs3.rmSync(path2.join(root, STATE_FILE), { force: true });
|
|
814
812
|
}
|
|
815
813
|
var GITIGNORE_FILE = ".gitignore";
|
|
816
814
|
var gitignoreLines = (kind) => [STATE_FILE, LOG_FILE, ".sharing.json", ...kind === "docs" ? [] : ["history/"], ".*.tmp", ".DS_Store"];
|
|
@@ -909,6 +907,7 @@ function serverOrigin(host, port) {
|
|
|
909
907
|
const shown2 = host === "0.0.0.0" || host === "::" ? "127.0.0.1" : host.includes(":") ? `[${host}]` : host;
|
|
910
908
|
return `http://${shown2}:${port}`;
|
|
911
909
|
}
|
|
910
|
+
var pagePathOf = (kind, root) => `/${kind}/${createHash("sha256").update(path2.resolve(root)).digest("base64url").slice(0, 22)}/`;
|
|
912
911
|
function sendJson2(response, status, body, headers = {}) {
|
|
913
912
|
const text = JSON.stringify(body);
|
|
914
913
|
response.writeHead(status, {
|
|
@@ -1496,9 +1495,911 @@ ${sections || '<p class="muted">This copy has no tokens, components or guide.</p
|
|
|
1496
1495
|
`;
|
|
1497
1496
|
}
|
|
1498
1497
|
|
|
1499
|
-
import {
|
|
1500
|
-
import
|
|
1501
|
-
import
|
|
1498
|
+
import { EventEmitter } from "node:events";
|
|
1499
|
+
import fs6 from "node:fs";
|
|
1500
|
+
import http from "node:http";
|
|
1501
|
+
import os3 from "node:os";
|
|
1502
|
+
import path6 from "node:path";
|
|
1503
|
+
|
|
1504
|
+
import crypto from "node:crypto";
|
|
1505
|
+
import fs5 from "node:fs";
|
|
1506
|
+
import path5 from "node:path";
|
|
1507
|
+
var GUEST_ROLES = ["editor", "commenter", "viewer"];
|
|
1508
|
+
var isGuestRole = (value) => typeof value === "string" && GUEST_ROLES.includes(value);
|
|
1509
|
+
var SHARING_FILE = ".sharing.json";
|
|
1510
|
+
var MAX_GUESTS = 200;
|
|
1511
|
+
var MAX_NAME = 40;
|
|
1512
|
+
var token = (bytes = 18) => crypto.randomBytes(bytes).toString("base64url");
|
|
1513
|
+
var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1514
|
+
function fresh() {
|
|
1515
|
+
return { lan: false, links: { editor: { token: token(), on: true }, commenter: { token: token(), on: true }, viewer: { token: token(), on: true } }, guests: [] };
|
|
1516
|
+
}
|
|
1517
|
+
function cleanName(raw) {
|
|
1518
|
+
const name = typeof raw === "string" ? raw.replace(/[\u0000-\u001f\u007f@<>]/g, "").replace(/\s+/g, " ").trim() : "";
|
|
1519
|
+
return [...name].slice(0, MAX_NAME).join("");
|
|
1520
|
+
}
|
|
1521
|
+
function guestUser(guest) {
|
|
1522
|
+
return { id: `guest-${guest.id}`, name: guest.name, email: `${guest.name}@${guest.id}.guest` };
|
|
1523
|
+
}
|
|
1524
|
+
function isLoopback(address) {
|
|
1525
|
+
if (!address) return false;
|
|
1526
|
+
const a = address.startsWith("::ffff:") ? address.slice(7) : address;
|
|
1527
|
+
return a === "::1" || a.startsWith("127.");
|
|
1528
|
+
}
|
|
1529
|
+
var Sharing = class _Sharing {
|
|
1530
|
+
file;
|
|
1531
|
+
state;
|
|
1532
|
+
constructor(root) {
|
|
1533
|
+
this.file = path5.join(root, SHARING_FILE);
|
|
1534
|
+
this.state = _Sharing.read(this.file);
|
|
1535
|
+
}
|
|
1536
|
+
static read(file) {
|
|
1537
|
+
let raw;
|
|
1538
|
+
try {
|
|
1539
|
+
raw = JSON.parse(fs5.readFileSync(file, "utf8"));
|
|
1540
|
+
} catch {
|
|
1541
|
+
return fresh();
|
|
1542
|
+
}
|
|
1543
|
+
const state = fresh();
|
|
1544
|
+
if (!isRecord2(raw)) return state;
|
|
1545
|
+
state.lan = raw.lan === true;
|
|
1546
|
+
if (typeof raw.port === "number" && Number.isInteger(raw.port) && raw.port > 0 && raw.port < 65536) state.port = raw.port;
|
|
1547
|
+
if (isRecord2(raw.links)) {
|
|
1548
|
+
for (const role of GUEST_ROLES) {
|
|
1549
|
+
const link = raw.links[role];
|
|
1550
|
+
if (isRecord2(link) && typeof link.token === "string" && link.token.length >= 16) state.links[role] = { token: link.token, on: link.on !== false };
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
if (Array.isArray(raw.guests)) {
|
|
1554
|
+
for (const g of raw.guests) {
|
|
1555
|
+
if (!isRecord2(g) || typeof g.id !== "string" || typeof g.secret !== "string" || !isGuestRole(g.role) || typeof g.link !== "string") continue;
|
|
1556
|
+
state.guests.push({
|
|
1557
|
+
id: g.id,
|
|
1558
|
+
secret: g.secret,
|
|
1559
|
+
name: cleanName(g.name) || "Guest",
|
|
1560
|
+
role: g.role,
|
|
1561
|
+
link: g.link,
|
|
1562
|
+
joined: typeof g.joined === "string" ? g.joined : (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
1563
|
+
...typeof g.seen === "string" ? { seen: g.seen } : {}
|
|
1564
|
+
});
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
return state;
|
|
1568
|
+
}
|
|
1569
|
+
save() {
|
|
1570
|
+
const tmp = `${this.file}.${process.pid}.tmp`;
|
|
1571
|
+
fs5.writeFileSync(tmp, `${JSON.stringify(this.state, null, 2)}
|
|
1572
|
+
`, { mode: 384 });
|
|
1573
|
+
fs5.renameSync(tmp, this.file);
|
|
1574
|
+
}
|
|
1575
|
+
/** The role whose link has this token, while that link is on. */
|
|
1576
|
+
linkRole(tokenGiven) {
|
|
1577
|
+
for (const role of GUEST_ROLES) {
|
|
1578
|
+
const link = this.state.links[role];
|
|
1579
|
+
if (link.on && tokenGiven.length === link.token.length && crypto.timingSafeEqual(Buffer.from(tokenGiven), Buffer.from(link.token))) return role;
|
|
1580
|
+
}
|
|
1581
|
+
return null;
|
|
1582
|
+
}
|
|
1583
|
+
/** The guest a cookie names, if they still have access (their link on and not reset). */
|
|
1584
|
+
guest(secret) {
|
|
1585
|
+
if (!secret) return null;
|
|
1586
|
+
const guest = this.state.guests.find((g) => g.secret === secret);
|
|
1587
|
+
return guest && this.valid(guest) ? guest : null;
|
|
1588
|
+
}
|
|
1589
|
+
valid(guest) {
|
|
1590
|
+
const link = this.state.links[guest.role];
|
|
1591
|
+
return link.on && link.token === guest.link && this.state.guests.includes(guest);
|
|
1592
|
+
}
|
|
1593
|
+
join(role, name) {
|
|
1594
|
+
const guest = { id: token(4), secret: token(24), name: cleanName(name) || "Guest", role, link: this.state.links[role].token, joined: (/* @__PURE__ */ new Date()).toISOString() };
|
|
1595
|
+
this.state.guests.push(guest);
|
|
1596
|
+
if (this.state.guests.length > MAX_GUESTS) this.state.guests.splice(0, this.state.guests.length - MAX_GUESTS);
|
|
1597
|
+
this.save();
|
|
1598
|
+
return guest;
|
|
1599
|
+
}
|
|
1600
|
+
/** Marks a guest seen now (saved at most once a minute per guest). */
|
|
1601
|
+
seen(guest) {
|
|
1602
|
+
const now = /* @__PURE__ */ new Date();
|
|
1603
|
+
if (guest.seen && now.getTime() - Date.parse(guest.seen) < 6e4) return;
|
|
1604
|
+
guest.seen = now.toISOString();
|
|
1605
|
+
this.save();
|
|
1606
|
+
}
|
|
1607
|
+
setLan(on, port) {
|
|
1608
|
+
this.state.lan = on;
|
|
1609
|
+
if (on && port) this.state.port = port;
|
|
1610
|
+
this.save();
|
|
1611
|
+
}
|
|
1612
|
+
setLink(role, on) {
|
|
1613
|
+
this.state.links[role].on = on;
|
|
1614
|
+
this.save();
|
|
1615
|
+
}
|
|
1616
|
+
/** A new token for the link: the old link stops working and the guests who came through it are dropped. */
|
|
1617
|
+
resetLink(role) {
|
|
1618
|
+
const old = this.state.links[role].token;
|
|
1619
|
+
this.state.links[role] = { token: token(), on: this.state.links[role].on };
|
|
1620
|
+
this.state.guests = this.state.guests.filter((g) => g.link !== old);
|
|
1621
|
+
this.save();
|
|
1622
|
+
}
|
|
1623
|
+
/**
|
|
1624
|
+
* Gives a guest another role. They now hang on that role's link (turning it off or resetting it drops them), so a role
|
|
1625
|
+
* whose link is off would shut them out: false then, as for a guest who isn't there.
|
|
1626
|
+
*/
|
|
1627
|
+
setRole(id, role) {
|
|
1628
|
+
const guest = this.state.guests.find((g) => g.id === id);
|
|
1629
|
+
if (!guest || !this.state.links[role].on) return false;
|
|
1630
|
+
guest.role = role;
|
|
1631
|
+
guest.link = this.state.links[role].token;
|
|
1632
|
+
this.save();
|
|
1633
|
+
return true;
|
|
1634
|
+
}
|
|
1635
|
+
removeGuest(id) {
|
|
1636
|
+
const before = this.state.guests.length;
|
|
1637
|
+
this.state.guests = this.state.guests.filter((g) => g.id !== id);
|
|
1638
|
+
if (this.state.guests.length === before) return false;
|
|
1639
|
+
this.save();
|
|
1640
|
+
return true;
|
|
1641
|
+
}
|
|
1642
|
+
};
|
|
1643
|
+
|
|
1644
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
1645
|
+
var GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
|
1646
|
+
var MAX_MESSAGE = 64 * 1024 * 1024;
|
|
1647
|
+
var WebSocketConnection = class {
|
|
1648
|
+
socket;
|
|
1649
|
+
buffer = Buffer.alloc(0);
|
|
1650
|
+
parts = [];
|
|
1651
|
+
partOpcode = 0;
|
|
1652
|
+
closed = false;
|
|
1653
|
+
handlers = {};
|
|
1654
|
+
constructor(socket, head) {
|
|
1655
|
+
this.socket = socket;
|
|
1656
|
+
socket.on("data", (chunk) => this.receive(chunk));
|
|
1657
|
+
socket.on("close", () => this.finish());
|
|
1658
|
+
socket.on("error", () => this.finish());
|
|
1659
|
+
if (head.length) this.receive(head);
|
|
1660
|
+
}
|
|
1661
|
+
get open() {
|
|
1662
|
+
return !this.closed;
|
|
1663
|
+
}
|
|
1664
|
+
sendText(text) {
|
|
1665
|
+
this.send(1, Buffer.from(text, "utf8"));
|
|
1666
|
+
}
|
|
1667
|
+
sendBinary(data) {
|
|
1668
|
+
this.send(2, data);
|
|
1669
|
+
}
|
|
1670
|
+
close(code = 1e3, reason = "") {
|
|
1671
|
+
if (this.closed) return;
|
|
1672
|
+
const body = Buffer.alloc(2 + Buffer.byteLength(reason));
|
|
1673
|
+
body.writeUInt16BE(code, 0);
|
|
1674
|
+
body.write(reason, 2);
|
|
1675
|
+
this.send(8, body);
|
|
1676
|
+
this.socket.end();
|
|
1677
|
+
this.finish();
|
|
1678
|
+
}
|
|
1679
|
+
finish() {
|
|
1680
|
+
if (this.closed) return;
|
|
1681
|
+
this.closed = true;
|
|
1682
|
+
this.socket.destroy();
|
|
1683
|
+
this.handlers.close?.();
|
|
1684
|
+
}
|
|
1685
|
+
send(opcode, payload) {
|
|
1686
|
+
if (this.closed) return;
|
|
1687
|
+
const length = payload.length;
|
|
1688
|
+
let header;
|
|
1689
|
+
if (length < 126) {
|
|
1690
|
+
header = Buffer.from([128 | opcode, length]);
|
|
1691
|
+
} else if (length < 65536) {
|
|
1692
|
+
header = Buffer.alloc(4);
|
|
1693
|
+
header[0] = 128 | opcode;
|
|
1694
|
+
header[1] = 126;
|
|
1695
|
+
header.writeUInt16BE(length, 2);
|
|
1696
|
+
} else {
|
|
1697
|
+
header = Buffer.alloc(10);
|
|
1698
|
+
header[0] = 128 | opcode;
|
|
1699
|
+
header[1] = 127;
|
|
1700
|
+
header.writeBigUInt64BE(BigInt(length), 2);
|
|
1701
|
+
}
|
|
1702
|
+
this.socket.write(Buffer.concat([header, payload]));
|
|
1703
|
+
}
|
|
1704
|
+
receive(chunk) {
|
|
1705
|
+
this.buffer = this.buffer.length ? Buffer.concat([this.buffer, chunk]) : chunk;
|
|
1706
|
+
for (; ; ) {
|
|
1707
|
+
if (this.buffer.length < 2) return;
|
|
1708
|
+
const first = this.buffer[0];
|
|
1709
|
+
const second = this.buffer[1];
|
|
1710
|
+
const fin = (first & 128) !== 0;
|
|
1711
|
+
const opcode = first & 15;
|
|
1712
|
+
const masked = (second & 128) !== 0;
|
|
1713
|
+
let length = second & 127;
|
|
1714
|
+
let offset = 2;
|
|
1715
|
+
if (length === 126) {
|
|
1716
|
+
if (this.buffer.length < 4) return;
|
|
1717
|
+
length = this.buffer.readUInt16BE(2);
|
|
1718
|
+
offset = 4;
|
|
1719
|
+
} else if (length === 127) {
|
|
1720
|
+
if (this.buffer.length < 10) return;
|
|
1721
|
+
const big = this.buffer.readBigUInt64BE(2);
|
|
1722
|
+
if (big > BigInt(MAX_MESSAGE)) return this.close(1009, "message too big");
|
|
1723
|
+
length = Number(big);
|
|
1724
|
+
offset = 10;
|
|
1725
|
+
}
|
|
1726
|
+
const maskLength = masked ? 4 : 0;
|
|
1727
|
+
if (this.buffer.length < offset + maskLength + length) return;
|
|
1728
|
+
const mask = masked ? this.buffer.subarray(offset, offset + 4) : null;
|
|
1729
|
+
const payload = Buffer.from(this.buffer.subarray(offset + maskLength, offset + maskLength + length));
|
|
1730
|
+
if (mask) for (let index = 0; index < payload.length; index++) payload[index] = payload[index] ^ mask[index & 3];
|
|
1731
|
+
this.buffer = this.buffer.subarray(offset + maskLength + length);
|
|
1732
|
+
this.frame(fin, opcode, payload);
|
|
1733
|
+
if (this.closed) return;
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
frame(fin, opcode, payload) {
|
|
1737
|
+
switch (opcode) {
|
|
1738
|
+
case 8:
|
|
1739
|
+
this.close(payload.length >= 2 ? payload.readUInt16BE(0) : 1e3);
|
|
1740
|
+
return;
|
|
1741
|
+
case 9:
|
|
1742
|
+
this.send(10, payload);
|
|
1743
|
+
return;
|
|
1744
|
+
case 10:
|
|
1745
|
+
return;
|
|
1746
|
+
case 0:
|
|
1747
|
+
case 1:
|
|
1748
|
+
case 2: {
|
|
1749
|
+
if (opcode !== 0) {
|
|
1750
|
+
this.parts = [];
|
|
1751
|
+
this.partOpcode = opcode;
|
|
1752
|
+
}
|
|
1753
|
+
this.parts.push(payload);
|
|
1754
|
+
if (this.parts.reduce((total, part) => total + part.length, 0) > MAX_MESSAGE) return this.close(1009, "message too big");
|
|
1755
|
+
if (!fin) return;
|
|
1756
|
+
const message = Buffer.concat(this.parts);
|
|
1757
|
+
this.parts = [];
|
|
1758
|
+
if (this.partOpcode === 1) this.handlers.text?.(message.toString("utf8"));
|
|
1759
|
+
else this.handlers.binary?.(message);
|
|
1760
|
+
return;
|
|
1761
|
+
}
|
|
1762
|
+
default:
|
|
1763
|
+
this.close(1002, "unknown opcode");
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
};
|
|
1767
|
+
function acceptWebSocket(request, socket, head, protocol) {
|
|
1768
|
+
const key = request.headers["sec-websocket-key"];
|
|
1769
|
+
if (typeof key !== "string" || String(request.headers.upgrade).toLowerCase() !== "websocket") {
|
|
1770
|
+
socket.end("HTTP/1.1 400 Bad Request\r\n\r\n");
|
|
1771
|
+
return null;
|
|
1772
|
+
}
|
|
1773
|
+
const accept = createHash2("sha1").update(key + GUID).digest("base64");
|
|
1774
|
+
socket.write(["HTTP/1.1 101 Switching Protocols", "Upgrade: websocket", "Connection: Upgrade", `Sec-WebSocket-Accept: ${accept}`, ...protocol ? [`Sec-WebSocket-Protocol: ${protocol}`] : [], "", ""].join("\r\n"));
|
|
1775
|
+
return new WebSocketConnection(socket, head);
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
var HUB_ROUTE = "/__hub";
|
|
1779
|
+
var STREAM_PROTOCOL = "baoyu-sse";
|
|
1780
|
+
var STREAM_REFUSED = 4e3;
|
|
1781
|
+
function buildOf(script2) {
|
|
1782
|
+
const real = fs6.realpathSync(script2);
|
|
1783
|
+
const { mtimeMs, size } = fs6.statSync(real);
|
|
1784
|
+
return `${real}:${mtimeMs}:${size}`;
|
|
1785
|
+
}
|
|
1786
|
+
var SiteClash = class extends Error {
|
|
1787
|
+
};
|
|
1788
|
+
var StreamResponse = class extends EventEmitter {
|
|
1789
|
+
status = 200;
|
|
1790
|
+
headersSent = false;
|
|
1791
|
+
writableEnded = false;
|
|
1792
|
+
destroyed = false;
|
|
1793
|
+
ws;
|
|
1794
|
+
constructor(ws) {
|
|
1795
|
+
super();
|
|
1796
|
+
this.ws = ws;
|
|
1797
|
+
}
|
|
1798
|
+
writeHead(status) {
|
|
1799
|
+
this.status = status;
|
|
1800
|
+
this.headersSent = true;
|
|
1801
|
+
return this;
|
|
1802
|
+
}
|
|
1803
|
+
setHeader() {
|
|
1804
|
+
return this;
|
|
1805
|
+
}
|
|
1806
|
+
flushHeaders() {
|
|
1807
|
+
}
|
|
1808
|
+
write(chunk) {
|
|
1809
|
+
if (this.status === 200 && !this.writableEnded) this.ws.sendText(String(chunk));
|
|
1810
|
+
return true;
|
|
1811
|
+
}
|
|
1812
|
+
end(chunk) {
|
|
1813
|
+
if (this.writableEnded) return this;
|
|
1814
|
+
if (chunk !== void 0) this.write(chunk);
|
|
1815
|
+
this.writableEnded = true;
|
|
1816
|
+
this.ws.close(this.status === 200 ? 1e3 : STREAM_REFUSED + Math.min(this.status, 999));
|
|
1817
|
+
return this;
|
|
1818
|
+
}
|
|
1819
|
+
destroy() {
|
|
1820
|
+
this.destroyed = true;
|
|
1821
|
+
return this.end();
|
|
1822
|
+
}
|
|
1823
|
+
closed() {
|
|
1824
|
+
this.writableEnded = true;
|
|
1825
|
+
this.emit("close");
|
|
1826
|
+
}
|
|
1827
|
+
};
|
|
1828
|
+
var Hub = class {
|
|
1829
|
+
port = 0;
|
|
1830
|
+
kind;
|
|
1831
|
+
host;
|
|
1832
|
+
build;
|
|
1833
|
+
factory;
|
|
1834
|
+
/** Sites' logs go to its stderr too: it runs in someone's terminal, not in the background. */
|
|
1835
|
+
echo;
|
|
1836
|
+
server;
|
|
1837
|
+
/** page → site. */
|
|
1838
|
+
sites = /* @__PURE__ */ new Map();
|
|
1839
|
+
adding = /* @__PURE__ */ new Map();
|
|
1840
|
+
startedAt = /* @__PURE__ */ new Map();
|
|
1841
|
+
hosts = /* @__PURE__ */ new Set();
|
|
1842
|
+
/** One listener per LAN address while any site is open to the network. */
|
|
1843
|
+
lanServers = /* @__PURE__ */ new Map();
|
|
1844
|
+
network;
|
|
1845
|
+
constructor(options) {
|
|
1846
|
+
this.echo = options.echo;
|
|
1847
|
+
this.kind = options.kind;
|
|
1848
|
+
this.build = options.build;
|
|
1849
|
+
this.host = options.host;
|
|
1850
|
+
this.factory = options.site;
|
|
1851
|
+
this.server = this.createServer();
|
|
1852
|
+
const hub = this;
|
|
1853
|
+
this.network = {
|
|
1854
|
+
get port() {
|
|
1855
|
+
return hub.port;
|
|
1856
|
+
},
|
|
1857
|
+
addresses: () => [...this.lanServers.keys()],
|
|
1858
|
+
open: () => this.openLan(),
|
|
1859
|
+
release: () => this.releaseLan()
|
|
1860
|
+
};
|
|
1861
|
+
}
|
|
1862
|
+
createServer() {
|
|
1863
|
+
const server2 = http.createServer((request, response) => {
|
|
1864
|
+
this.onRequest(request, response).catch((error) => {
|
|
1865
|
+
process.stderr.write(`[${(/* @__PURE__ */ new Date()).toISOString()}] request failed: ${error instanceof Error ? error.stack ?? error.message : String(error)}
|
|
1866
|
+
`);
|
|
1867
|
+
if (!response.headersSent) sendJson2(response, 500, { code: "internal", message: "internal error" });
|
|
1868
|
+
else response.destroy();
|
|
1869
|
+
});
|
|
1870
|
+
});
|
|
1871
|
+
server2.on("upgrade", (request, socket, head) => this.onUpgrade(request, socket, head));
|
|
1872
|
+
return server2;
|
|
1873
|
+
}
|
|
1874
|
+
/** Listens at the first free port from `port`; `first` is tried before them. */
|
|
1875
|
+
async listen(port, first) {
|
|
1876
|
+
this.port = await listenFree(this.server, port, this.host, first);
|
|
1877
|
+
const named = this.host === "0.0.0.0" || this.host === "::" ? [] : [this.host.includes(":") ? `[${this.host}]:${this.port}` : `${this.host}:${this.port}`];
|
|
1878
|
+
this.hosts = /* @__PURE__ */ new Set([`127.0.0.1:${this.port}`, `localhost:${this.port}`, `[::1]:${this.port}`, ...named]);
|
|
1879
|
+
return this.port;
|
|
1880
|
+
}
|
|
1881
|
+
/** Ends every site and the listeners; what the folders' state files say goes with them. */
|
|
1882
|
+
close() {
|
|
1883
|
+
for (const site of [...this.sites.values()]) this.drop(site);
|
|
1884
|
+
this.closeLan();
|
|
1885
|
+
this.server.close();
|
|
1886
|
+
this.server.closeAllConnections?.();
|
|
1887
|
+
}
|
|
1888
|
+
/** On a signal (or a stray error, logged): every site's record goes and the process exits. */
|
|
1889
|
+
ownProcess() {
|
|
1890
|
+
const shutdown = () => {
|
|
1891
|
+
this.close();
|
|
1892
|
+
process.exit(0);
|
|
1893
|
+
};
|
|
1894
|
+
for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) process.on(signal, shutdown);
|
|
1895
|
+
const stray = (error) => process.stderr.write(`[${(/* @__PURE__ */ new Date()).toISOString()}] uncaught: ${error instanceof Error ? error.stack ?? error.message : String(error)}
|
|
1896
|
+
`);
|
|
1897
|
+
process.on("uncaughtException", stray);
|
|
1898
|
+
process.on("unhandledRejection", stray);
|
|
1899
|
+
}
|
|
1900
|
+
// ---- the sites -------------------------------------------------------------------------------------------------
|
|
1901
|
+
urlOf(site) {
|
|
1902
|
+
return `${serverOrigin(this.host, this.port)}${site.page}`;
|
|
1903
|
+
}
|
|
1904
|
+
stateOf(site) {
|
|
1905
|
+
const lan = site.share.lanUrls();
|
|
1906
|
+
return { pid: process.pid, url: this.urlOf(site), port: this.port, root: site.root, kind: this.kind, startedAt: this.startedAt.get(site.page) ?? (/* @__PURE__ */ new Date()).toISOString(), ...lan.length ? { lan } : {} };
|
|
1907
|
+
}
|
|
1908
|
+
/** Mounts a folder (already mounted: the same site), writing its <root>/.artifact-server.json. */
|
|
1909
|
+
add(root, options) {
|
|
1910
|
+
const mounted = [...this.sites.values()].find((site) => site.root === root);
|
|
1911
|
+
if (mounted) return (options.lan && !mounted.share.shared ? mounted.share.setLan(true) : Promise.resolve()).then(() => this.stateOf(mounted));
|
|
1912
|
+
const adding = this.adding.get(root);
|
|
1913
|
+
if (adding) return adding;
|
|
1914
|
+
const work = this.mount(root, options).finally(() => this.adding.delete(root));
|
|
1915
|
+
this.adding.set(root, work);
|
|
1916
|
+
return work;
|
|
1917
|
+
}
|
|
1918
|
+
async mount(root, { agentHook, lan = false }) {
|
|
1919
|
+
const file = path6.join(root, LOG_FILE);
|
|
1920
|
+
const log = (line) => {
|
|
1921
|
+
const text = `[${(/* @__PURE__ */ new Date()).toISOString()}] ${line}
|
|
1922
|
+
`;
|
|
1923
|
+
try {
|
|
1924
|
+
fs6.appendFileSync(file, text);
|
|
1925
|
+
} catch {
|
|
1926
|
+
}
|
|
1927
|
+
if (this.echo) process.stderr.write(text);
|
|
1928
|
+
};
|
|
1929
|
+
const site = await this.factory(root, { log, network: this.network, ...agentHook ? { agentHook } : {} });
|
|
1930
|
+
const other = this.sites.get(site.page);
|
|
1931
|
+
if (other) {
|
|
1932
|
+
site.close();
|
|
1933
|
+
throw new SiteClash(`${other.root} is already open at ${site.page}.`);
|
|
1934
|
+
}
|
|
1935
|
+
this.sites.set(site.page, site);
|
|
1936
|
+
this.startedAt.set(site.page, (/* @__PURE__ */ new Date()).toISOString());
|
|
1937
|
+
try {
|
|
1938
|
+
await site.share.start(lan);
|
|
1939
|
+
} catch (error) {
|
|
1940
|
+
this.drop(site);
|
|
1941
|
+
throw error;
|
|
1942
|
+
}
|
|
1943
|
+
const state = this.stateOf(site);
|
|
1944
|
+
const { lan: _lan, ...record } = state;
|
|
1945
|
+
writeState(record);
|
|
1946
|
+
log(`serving ${root} at ${state.url}${agentHook ? `; each comment sent to the agent runs: ${agentHook}` : ""}`);
|
|
1947
|
+
return state;
|
|
1948
|
+
}
|
|
1949
|
+
drop(site) {
|
|
1950
|
+
this.sites.delete(site.page);
|
|
1951
|
+
this.startedAt.delete(site.page);
|
|
1952
|
+
removeState(site.root);
|
|
1953
|
+
site.close();
|
|
1954
|
+
}
|
|
1955
|
+
/** Ends one site; true when one was mounted there. The process ends with its last site. */
|
|
1956
|
+
remove(root) {
|
|
1957
|
+
const site = [...this.sites.values()].find((candidate) => candidate.root === root);
|
|
1958
|
+
if (site) this.drop(site);
|
|
1959
|
+
return site !== void 0;
|
|
1960
|
+
}
|
|
1961
|
+
// ---- the network side ------------------------------------------------------------------------------------------
|
|
1962
|
+
async openLan() {
|
|
1963
|
+
for (const address of lanAddresses()) {
|
|
1964
|
+
if (this.lanServers.has(address)) continue;
|
|
1965
|
+
const server2 = this.createServer();
|
|
1966
|
+
try {
|
|
1967
|
+
await listen(server2, this.port, address);
|
|
1968
|
+
this.lanServers.set(address, server2);
|
|
1969
|
+
} catch (error) {
|
|
1970
|
+
server2.close();
|
|
1971
|
+
process.stderr.write(`[${(/* @__PURE__ */ new Date()).toISOString()}] not listening on ${address}:${this.port}: ${errorText(error)}
|
|
1972
|
+
`);
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
return [...this.lanServers.keys()];
|
|
1976
|
+
}
|
|
1977
|
+
/** A site stopped sharing: the listeners stay while another is shared. */
|
|
1978
|
+
releaseLan() {
|
|
1979
|
+
if (![...this.sites.values()].some((site) => site.share.shared)) this.closeLan();
|
|
1980
|
+
}
|
|
1981
|
+
closeLan() {
|
|
1982
|
+
for (const server2 of this.lanServers.values()) {
|
|
1983
|
+
server2.close();
|
|
1984
|
+
server2.closeAllConnections?.();
|
|
1985
|
+
}
|
|
1986
|
+
this.lanServers.clear();
|
|
1987
|
+
}
|
|
1988
|
+
/** A Host header this hub answers: its own names, and while something is shared, its LAN addresses and machine name. */
|
|
1989
|
+
hostAllowed(request) {
|
|
1990
|
+
const host = request.headers.host ?? "";
|
|
1991
|
+
if (this.hosts.has(host)) return true;
|
|
1992
|
+
if (!this.lanServers.size) return false;
|
|
1993
|
+
const suffix = `:${this.port}`;
|
|
1994
|
+
if (!host.endsWith(suffix)) return false;
|
|
1995
|
+
const name = host.slice(0, -suffix.length).toLowerCase();
|
|
1996
|
+
const machine = os3.hostname().toLowerCase();
|
|
1997
|
+
return this.lanServers.has(name) || name === machine || name === `${machine.replace(/\.local$/, "")}.local`;
|
|
1998
|
+
}
|
|
1999
|
+
// ---- requests --------------------------------------------------------------------------------------------------
|
|
2000
|
+
/** The site whose address `pathname` is under, with the path below it. */
|
|
2001
|
+
under(pathname) {
|
|
2002
|
+
const [, first, second] = pathname.split("/");
|
|
2003
|
+
const site = first && second ? this.sites.get(`/${first}/${second}/`) : void 0;
|
|
2004
|
+
if (site) return { site, rest: pathname.slice(site.page.length - 1) };
|
|
2005
|
+
for (const candidate of this.sites.values()) if (candidate.claims?.(pathname)) return { site: candidate, rest: pathname };
|
|
2006
|
+
return null;
|
|
2007
|
+
}
|
|
2008
|
+
/** The site a page at this request's Referer belongs to. */
|
|
2009
|
+
referred(request) {
|
|
2010
|
+
const referer = request.headers.referer;
|
|
2011
|
+
if (typeof referer !== "string") return null;
|
|
2012
|
+
try {
|
|
2013
|
+
const at = new URL(referer);
|
|
2014
|
+
return at.host === request.headers.host ? this.under(at.pathname)?.site ?? null : null;
|
|
2015
|
+
} catch {
|
|
2016
|
+
return null;
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
/** The sites this request's person may see. */
|
|
2020
|
+
seenBy(request) {
|
|
2021
|
+
return [...this.sites.values()].filter((site) => site.share.viewerOf(request));
|
|
2022
|
+
}
|
|
2023
|
+
async onRequest(request, response) {
|
|
2024
|
+
if (!this.hostAllowed(request)) {
|
|
2025
|
+
request.resume();
|
|
2026
|
+
response.writeHead(421, { "Content-Type": "text/plain; charset=utf-8", "Cache-Control": "no-store" }).end("Misdirected request");
|
|
2027
|
+
return;
|
|
2028
|
+
}
|
|
2029
|
+
const url = new URL(request.url ?? "/", "http://local");
|
|
2030
|
+
const pathname = url.pathname;
|
|
2031
|
+
if (pathname === HUB_ROUTE) return this.control(request, response);
|
|
2032
|
+
if (pathname === "/favicon.ico") {
|
|
2033
|
+
response.writeHead(204).end();
|
|
2034
|
+
return;
|
|
2035
|
+
}
|
|
2036
|
+
if (pathname === "/" || pathname === "/index.html") return this.landing(request, response, url.search);
|
|
2037
|
+
const at = this.under(pathname);
|
|
2038
|
+
if (at) {
|
|
2039
|
+
if (at.rest === "") {
|
|
2040
|
+
request.resume();
|
|
2041
|
+
response.writeHead(308, { Location: `${pathname}/${url.search}`, "Cache-Control": "no-store" }).end();
|
|
2042
|
+
return;
|
|
2043
|
+
}
|
|
2044
|
+
request.url = `${at.rest}${url.search}`;
|
|
2045
|
+
return at.site.handle(request, response);
|
|
2046
|
+
}
|
|
2047
|
+
const site = await this.answering(request, pathname);
|
|
2048
|
+
if (site) return site.handle(request, response);
|
|
2049
|
+
request.resume();
|
|
2050
|
+
response.writeHead(404, { "Content-Type": "text/plain; charset=utf-8", "Cache-Control": "no-store" }).end("Not found");
|
|
2051
|
+
}
|
|
2052
|
+
/** The site that answers a path outside every site's address. */
|
|
2053
|
+
async answering(request, pathname) {
|
|
2054
|
+
const link = /^\/s\/([A-Za-z0-9_-]{16,64})$/.exec(pathname);
|
|
2055
|
+
if (link) return [...this.sites.values()].find((site) => site.share.sharing.linkRole(link[1])) ?? null;
|
|
2056
|
+
const blob = /^\/_blob\/([0-9a-f]{32})$/.exec(pathname);
|
|
2057
|
+
if (blob) {
|
|
2058
|
+
const referred = this.referred(request);
|
|
2059
|
+
const seen = this.seenBy(request);
|
|
2060
|
+
for (const site of referred && seen.includes(referred) ? [referred, ...seen] : seen) if (await site.hasBlob?.(blob[1])) return site;
|
|
2061
|
+
return null;
|
|
2062
|
+
}
|
|
2063
|
+
for (const site of this.sites.values()) if (site.common?.(pathname)) return site;
|
|
2064
|
+
return this.referred(request);
|
|
2065
|
+
}
|
|
2066
|
+
/** `/`: where this person goes. */
|
|
2067
|
+
landing(request, response, search) {
|
|
2068
|
+
const seen = this.seenBy(request);
|
|
2069
|
+
const only = seen[0];
|
|
2070
|
+
if (seen.length === 1 && only) {
|
|
2071
|
+
request.resume();
|
|
2072
|
+
response.writeHead(302, { Location: `${only.page}${search}`, "Cache-Control": "no-store" }).end();
|
|
2073
|
+
return;
|
|
2074
|
+
}
|
|
2075
|
+
const any = only ?? [...this.sites.values()][0];
|
|
2076
|
+
if (!any) {
|
|
2077
|
+
request.resume();
|
|
2078
|
+
response.writeHead(404, { "Content-Type": "text/plain; charset=utf-8", "Cache-Control": "no-store" }).end("Nothing is open here.");
|
|
2079
|
+
return;
|
|
2080
|
+
}
|
|
2081
|
+
if (!only) return any.share.denied(request, response);
|
|
2082
|
+
any.share.sendList(response, seen.map((site) => ({ page: site.page, title: site.share.title() })));
|
|
2083
|
+
}
|
|
2084
|
+
/** The CLIs' route: over loopback, from a process (a browser's POST carries an Origin). */
|
|
2085
|
+
async control(request, response) {
|
|
2086
|
+
const info = { hub: 1, kind: this.kind, build: this.build, host: this.host, pid: process.pid };
|
|
2087
|
+
if (!isLoopback(request.socket.remoteAddress) || request.headers.origin !== void 0) {
|
|
2088
|
+
request.resume();
|
|
2089
|
+
return sendJson2(response, 403, { code: "not_granted", message: "Only a local process can do this." });
|
|
2090
|
+
}
|
|
2091
|
+
if (request.method === "GET") return sendJson2(response, 200, info);
|
|
2092
|
+
if (request.method !== "POST") {
|
|
2093
|
+
request.resume();
|
|
2094
|
+
return sendJson2(response, 405, { code: "method_not_allowed", message: "GET or POST." });
|
|
2095
|
+
}
|
|
2096
|
+
let body;
|
|
2097
|
+
try {
|
|
2098
|
+
body = JSON.parse((await readBody(request, 64 << 10)).toString("utf8"));
|
|
2099
|
+
} catch {
|
|
2100
|
+
return sendJson2(response, 400, { code: "invalid_argument", message: "The call is not JSON." });
|
|
2101
|
+
}
|
|
2102
|
+
if (typeof body.root !== "string" || !path6.isAbsolute(body.root)) return sendJson2(response, 400, { code: "invalid_argument", message: "root: an absolute path." });
|
|
2103
|
+
if (body.action === "mount") {
|
|
2104
|
+
try {
|
|
2105
|
+
const state = await this.add(body.root, { ...typeof body.agentHook === "string" && body.agentHook ? { agentHook: body.agentHook } : {}, lan: body.lan === true });
|
|
2106
|
+
return sendJson2(response, 200, { ...info, state });
|
|
2107
|
+
} catch (error) {
|
|
2108
|
+
sendJson2(response, error instanceof SiteClash ? 409 : 400, { ...info, code: error.code ?? "SERVER_FAILED", message: errorText(error) });
|
|
2109
|
+
return this.endIfEmpty();
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
if (body.action === "unmount") {
|
|
2113
|
+
const removed = this.remove(body.root);
|
|
2114
|
+
const remaining = this.sites.size;
|
|
2115
|
+
sendJson2(response, 200, { ...info, removed, remaining });
|
|
2116
|
+
return this.endIfEmpty();
|
|
2117
|
+
}
|
|
2118
|
+
return sendJson2(response, 400, { code: "invalid_argument", message: "action: mount or unmount." });
|
|
2119
|
+
}
|
|
2120
|
+
/** The process ends with its last site. */
|
|
2121
|
+
endIfEmpty() {
|
|
2122
|
+
if (this.sites.size > 0 || this.adding.size > 0) return;
|
|
2123
|
+
this.close();
|
|
2124
|
+
setImmediate(() => process.exit(0));
|
|
2125
|
+
}
|
|
2126
|
+
onUpgrade(request, socket, head) {
|
|
2127
|
+
socket.on("error", () => {
|
|
2128
|
+
});
|
|
2129
|
+
const url = new URL(request.url ?? "/", "http://local");
|
|
2130
|
+
const at = this.hostAllowed(request) && this.originOk(request) ? this.under(url.pathname) : null;
|
|
2131
|
+
if (!at) {
|
|
2132
|
+
socket.end("HTTP/1.1 403 Forbidden\r\n\r\n");
|
|
2133
|
+
return;
|
|
2134
|
+
}
|
|
2135
|
+
request.url = `${at.rest}${url.search}`;
|
|
2136
|
+
const asked = String(request.headers["sec-websocket-protocol"] ?? "").split(",").map((p) => p.trim());
|
|
2137
|
+
if (asked.includes(STREAM_PROTOCOL)) return this.stream(at.site, request, socket, head, url);
|
|
2138
|
+
if (at.site.upgrade) return at.site.upgrade(request, socket, head);
|
|
2139
|
+
socket.end("HTTP/1.1 404 Not Found\r\n\r\n");
|
|
2140
|
+
}
|
|
2141
|
+
originOk(request) {
|
|
2142
|
+
const origin = request.headers.origin;
|
|
2143
|
+
if (origin === void 0) return true;
|
|
2144
|
+
try {
|
|
2145
|
+
return new URL(origin).host === request.headers.host;
|
|
2146
|
+
} catch {
|
|
2147
|
+
return false;
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
2150
|
+
/** An event stream over a WebSocket: the site's handler answers it as a GET. */
|
|
2151
|
+
stream(site, request, socket, head, url) {
|
|
2152
|
+
const ws = acceptWebSocket(request, socket, head, STREAM_PROTOCOL);
|
|
2153
|
+
if (!ws) return;
|
|
2154
|
+
const last = url.searchParams.get("__lastEventId");
|
|
2155
|
+
if (last !== null) {
|
|
2156
|
+
request.headers["last-event-id"] = last;
|
|
2157
|
+
url.searchParams.delete("__lastEventId");
|
|
2158
|
+
request.url = `${request.url?.split("?")[0]}${url.search}`;
|
|
2159
|
+
}
|
|
2160
|
+
request.method = "GET";
|
|
2161
|
+
const response = new StreamResponse(ws);
|
|
2162
|
+
ws.handlers = {
|
|
2163
|
+
close: () => {
|
|
2164
|
+
response.closed();
|
|
2165
|
+
request.emit("close");
|
|
2166
|
+
}
|
|
2167
|
+
};
|
|
2168
|
+
Promise.resolve(site.handle(request, response)).catch(() => ws.close(STREAM_REFUSED + 500));
|
|
2169
|
+
}
|
|
2170
|
+
};
|
|
2171
|
+
async function hubAt(origin) {
|
|
2172
|
+
try {
|
|
2173
|
+
const response = await fetch(`${origin}${HUB_ROUTE}`, { signal: AbortSignal.timeout(1e3) });
|
|
2174
|
+
if (!response.ok) return null;
|
|
2175
|
+
const info = await response.json();
|
|
2176
|
+
return info.hub === 1 ? info : null;
|
|
2177
|
+
} catch {
|
|
2178
|
+
return null;
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
async function joinHub(call, below = Infinity) {
|
|
2182
|
+
if (call.port === 0) return null;
|
|
2183
|
+
const ports = call.portGiven ? [call.port] : Array.from({ length: PORT_TRIES }, (_, i) => call.port + i).filter((port) => port <= 65535 && port < below);
|
|
2184
|
+
const build = buildOf(call.script);
|
|
2185
|
+
const origins = ports.map((port) => serverOrigin(call.host, port));
|
|
2186
|
+
const infos = await Promise.all(origins.map(hubAt));
|
|
2187
|
+
for (const [i, info] of infos.entries()) {
|
|
2188
|
+
if (!info || info.kind !== call.kind || info.build !== build || info.host !== call.host) continue;
|
|
2189
|
+
const response = await fetch(`${origins[i]}${HUB_ROUTE}`, {
|
|
2190
|
+
method: "POST",
|
|
2191
|
+
headers: { "Content-Type": "application/json" },
|
|
2192
|
+
body: JSON.stringify({ action: "mount", root: call.root, ...call.agentHook ? { agentHook: call.agentHook } : {}, lan: call.lan === true }),
|
|
2193
|
+
signal: AbortSignal.timeout(2e4)
|
|
2194
|
+
}).catch(() => null);
|
|
2195
|
+
if (!response) continue;
|
|
2196
|
+
const body = await response.json().catch(() => ({}));
|
|
2197
|
+
if (response.ok && body.state) return body.state;
|
|
2198
|
+
if (response.status === 409) continue;
|
|
2199
|
+
throw Object.assign(new Error(body.message ?? `the server answered ${response.status}`), { code: body.code ?? "SERVER_FAILED" });
|
|
2200
|
+
}
|
|
2201
|
+
return null;
|
|
2202
|
+
}
|
|
2203
|
+
async function leaveHub(state) {
|
|
2204
|
+
let remaining = 0;
|
|
2205
|
+
const response = await fetch(`${new URL(state.url).origin}${HUB_ROUTE}`, {
|
|
2206
|
+
method: "POST",
|
|
2207
|
+
headers: { "Content-Type": "application/json" },
|
|
2208
|
+
body: JSON.stringify({ action: "unmount", root: state.root }),
|
|
2209
|
+
signal: AbortSignal.timeout(5e3)
|
|
2210
|
+
}).catch(() => null);
|
|
2211
|
+
const body = response ? await response.json().catch(() => null) : null;
|
|
2212
|
+
if (body?.hub === 1) remaining = Number(body.remaining ?? 0);
|
|
2213
|
+
else if (alive(state.pid)) process.kill(state.pid, "SIGTERM");
|
|
2214
|
+
const deadline = Date.now() + 5e3;
|
|
2215
|
+
while (remaining === 0 && alive(state.pid) && Date.now() < deadline) await new Promise((resolve) => setTimeout(resolve, 100));
|
|
2216
|
+
if (remaining === 0 && alive(state.pid)) return false;
|
|
2217
|
+
fs6.rmSync(path6.join(state.root, STATE_FILE), { force: true });
|
|
2218
|
+
return true;
|
|
2219
|
+
}
|
|
2220
|
+
function holdSite(state) {
|
|
2221
|
+
const leave = () => {
|
|
2222
|
+
void leaveHub(state).finally(() => process.exit(0));
|
|
2223
|
+
};
|
|
2224
|
+
for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) process.on(signal, leave);
|
|
2225
|
+
setInterval(() => {
|
|
2226
|
+
if (!alive(state.pid) || readState(state.root)?.pid !== state.pid) process.exit(0);
|
|
2227
|
+
}, 2e3);
|
|
2228
|
+
}
|
|
2229
|
+
async function serveHub(call) {
|
|
2230
|
+
const joined = await joinHub(call);
|
|
2231
|
+
const stayed = (state) => {
|
|
2232
|
+
if (!call.background) holdSite(state);
|
|
2233
|
+
return { ...state, joined: true };
|
|
2234
|
+
};
|
|
2235
|
+
if (joined) return stayed(joined);
|
|
2236
|
+
const hub = new Hub({ kind: call.kind, build: buildOf(call.script), host: call.host, site: call.site, echo: !call.background });
|
|
2237
|
+
const { state: sharing } = new Sharing(call.root);
|
|
2238
|
+
const linksPort = !call.portGiven && (call.lan || sharing.lan) ? sharing.port : void 0;
|
|
2239
|
+
const port = await hub.listen(call.port, linksPort);
|
|
2240
|
+
const lower = call.portGiven ? null : await joinHub(call, port);
|
|
2241
|
+
if (lower) {
|
|
2242
|
+
hub.close();
|
|
2243
|
+
return stayed(lower);
|
|
2244
|
+
}
|
|
2245
|
+
hub.ownProcess();
|
|
2246
|
+
try {
|
|
2247
|
+
return { ...await hub.add(call.root, { ...call.agentHook ? { agentHook: call.agentHook } : {}, lan: call.lan === true }), joined: false };
|
|
2248
|
+
} catch (error) {
|
|
2249
|
+
hub.close();
|
|
2250
|
+
throw error;
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
|
|
2254
|
+
import fs7 from "node:fs";
|
|
2255
|
+
import path7 from "node:path";
|
|
2256
|
+
var FOLDER2 = /^[a-z0-9][a-z0-9_-]{0,63}$/;
|
|
2257
|
+
var MAX_FILES = 2e3;
|
|
2258
|
+
var MAX_FILE_BYTES2 = 64 * 1024 * 1024;
|
|
2259
|
+
var DESIGN_SYSTEM_FILE = "design-system.json";
|
|
2260
|
+
function readRecords(project, indexFile) {
|
|
2261
|
+
let index;
|
|
2262
|
+
try {
|
|
2263
|
+
index = JSON.parse(fs7.readFileSync(path7.join(project, indexFile), "utf8"));
|
|
2264
|
+
} catch {
|
|
2265
|
+
return [];
|
|
2266
|
+
}
|
|
2267
|
+
const list = index?.designSystems;
|
|
2268
|
+
const records = Array.isArray(list) ? list : list && typeof list === "object" ? Object.values(list) : [];
|
|
2269
|
+
const out = [];
|
|
2270
|
+
for (const raw of records) {
|
|
2271
|
+
const r = raw;
|
|
2272
|
+
if (typeof r?.namespace !== "string" || !FOLDER2.test(r.namespace)) continue;
|
|
2273
|
+
out.push({
|
|
2274
|
+
namespace: r.namespace,
|
|
2275
|
+
title: typeof r.title === "string" ? r.title : r.namespace,
|
|
2276
|
+
artifact: typeof r.artifact === "string" ? r.artifact : ""
|
|
2277
|
+
});
|
|
2278
|
+
}
|
|
2279
|
+
return out;
|
|
2280
|
+
}
|
|
2281
|
+
function plain(p, want) {
|
|
2282
|
+
const stat = fs7.lstatSync(p, { throwIfNoEntry: false });
|
|
2283
|
+
return Boolean(stat && !stat.isSymbolicLink() && (want === "file" ? stat.isFile() : stat.isDirectory()));
|
|
2284
|
+
}
|
|
2285
|
+
function walk(dir, prefix = "", out = []) {
|
|
2286
|
+
for (const entry of fs7.readdirSync(path7.join(dir, prefix), { withFileTypes: true })) {
|
|
2287
|
+
if (out.length >= MAX_FILES) break;
|
|
2288
|
+
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
2289
|
+
if (entry.isDirectory()) walk(dir, rel, out);
|
|
2290
|
+
else if (entry.isFile()) out.push(rel);
|
|
2291
|
+
}
|
|
2292
|
+
return out;
|
|
2293
|
+
}
|
|
2294
|
+
var projectOf = (candidate) => path7.basename(candidate) === "project" ? candidate : path7.join(candidate, "project");
|
|
2295
|
+
function sourceProject(artifact, root, from) {
|
|
2296
|
+
const given = from ?? artifact;
|
|
2297
|
+
if (!given) return { reason: "the record names no source folder", kind: "none" };
|
|
2298
|
+
if (!from && /^[a-z][a-z0-9+.-]*:\/\//i.test(given)) return { reason: `its source is ${given}, which is not a folder on this machine`, kind: "remote" };
|
|
2299
|
+
const candidates = path7.isAbsolute(given) ? [given] : [path7.resolve(given), path7.resolve(root, given), path7.resolve(root, "..", given)];
|
|
2300
|
+
for (const candidate of candidates) {
|
|
2301
|
+
const project = projectOf(candidate);
|
|
2302
|
+
if (plain(project, "dir")) return { project };
|
|
2303
|
+
}
|
|
2304
|
+
if (!from && !path7.isAbsolute(given)) {
|
|
2305
|
+
let dir = path7.resolve(root, "..");
|
|
2306
|
+
for (let up = path7.dirname(dir); up !== dir; up = path7.dirname(dir)) {
|
|
2307
|
+
dir = up;
|
|
2308
|
+
const project = projectOf(path7.resolve(dir, given));
|
|
2309
|
+
if (plain(path7.join(project, DESIGN_SYSTEM_FILE), "file")) return { project };
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
return { reason: `its source folder ${given} was not found${from ? "" : ` (tried from ${process.cwd()} and the folders above the artifact); pass --from <folder>`}`, kind: "not-found" };
|
|
2313
|
+
}
|
|
2314
|
+
function compareSystem(project, root, record, from) {
|
|
2315
|
+
const diff = { namespace: record.namespace, title: record.title, changed: [], removed: [] };
|
|
2316
|
+
const found = sourceProject(record.artifact, root, from);
|
|
2317
|
+
if (!found.project) return { ...diff, skipped: found.reason, skippedKind: found.kind };
|
|
2318
|
+
diff.source = path7.dirname(found.project);
|
|
2319
|
+
const copy = path7.join(project, "ds", record.namespace);
|
|
2320
|
+
if (!plain(copy, "dir")) return { ...diff, missing: true };
|
|
2321
|
+
for (const rel of walk(copy)) {
|
|
2322
|
+
const mine = path7.join(copy, rel);
|
|
2323
|
+
const theirs = path7.join(found.project, rel);
|
|
2324
|
+
if (!plain(mine, "file")) continue;
|
|
2325
|
+
if (!plain(theirs, "file")) {
|
|
2326
|
+
diff.removed.push(rel);
|
|
2327
|
+
continue;
|
|
2328
|
+
}
|
|
2329
|
+
const a = fs7.statSync(mine);
|
|
2330
|
+
const b = fs7.statSync(theirs);
|
|
2331
|
+
if (a.size > MAX_FILE_BYTES2 || b.size > MAX_FILE_BYTES2) continue;
|
|
2332
|
+
if (a.size !== b.size || !fs7.readFileSync(mine).equals(fs7.readFileSync(theirs))) diff.changed.push(rel);
|
|
2333
|
+
}
|
|
2334
|
+
return diff;
|
|
2335
|
+
}
|
|
2336
|
+
function systemStatus(project, root, indexFile, namespace) {
|
|
2337
|
+
const record = readRecords(project, indexFile).find((r) => r.namespace === namespace);
|
|
2338
|
+
if (!record) return null;
|
|
2339
|
+
const diff = compareSystem(project, root, record);
|
|
2340
|
+
if (diff.skippedKind) return { state: "unknown", changed: 0, removed: 0, reason: diff.skippedKind };
|
|
2341
|
+
if (diff.missing) return { state: "unknown", changed: 0, removed: 0, reason: "missing" };
|
|
2342
|
+
return { state: diff.changed.length > 0 ? "behind" : "current", changed: diff.changed.length, removed: diff.removed.length };
|
|
2343
|
+
}
|
|
2344
|
+
var DS_SYNC = /^__ds-sync\/([a-z0-9][a-z0-9_-]{0,63})$/;
|
|
2345
|
+
function pick(records, namespace) {
|
|
2346
|
+
if (namespace === void 0) return records;
|
|
2347
|
+
const one = records.filter((r) => r.namespace === namespace);
|
|
2348
|
+
if (one.length === 0) {
|
|
2349
|
+
const names = records.map((r) => r.namespace);
|
|
2350
|
+
throw new Error(`No designSystems record has namespace ${JSON.stringify(namespace)}${names.length ? ` (it has: ${names.join(", ")})` : ""}.`);
|
|
2351
|
+
}
|
|
2352
|
+
return one;
|
|
2353
|
+
}
|
|
2354
|
+
function compareSystems(project, root, indexFile, namespace, from) {
|
|
2355
|
+
return pick(readRecords(project, indexFile), namespace).map((r) => compareSystem(project, root, r, from));
|
|
2356
|
+
}
|
|
2357
|
+
function stamp(project, indexFile, namespaces, now) {
|
|
2358
|
+
const file = path7.join(project, indexFile);
|
|
2359
|
+
const text = fs7.readFileSync(file, "utf8");
|
|
2360
|
+
const index = JSON.parse(text);
|
|
2361
|
+
const list = index.designSystems;
|
|
2362
|
+
const records = Array.isArray(list) ? list : list && typeof list === "object" ? Object.values(list) : [];
|
|
2363
|
+
for (const r of records) {
|
|
2364
|
+
const record = r;
|
|
2365
|
+
if (record && typeof record.namespace === "string" && namespaces.has(record.namespace)) record.copiedAt = now;
|
|
2366
|
+
}
|
|
2367
|
+
const indent = /^\{\n/.test(text) ? /\n( +|\t)"/.exec(text)?.[1] ?? 2 : void 0;
|
|
2368
|
+
const out = JSON.stringify(index, null, indent);
|
|
2369
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
2370
|
+
fs7.writeFileSync(tmp, text.endsWith("\n") ? `${out}
|
|
2371
|
+
` : out);
|
|
2372
|
+
fs7.renameSync(tmp, file);
|
|
2373
|
+
}
|
|
2374
|
+
function syncSystems(project, root, indexFile, opts) {
|
|
2375
|
+
const diffs = compareSystems(project, root, indexFile, opts.namespace, opts.from);
|
|
2376
|
+
const results = [];
|
|
2377
|
+
const synced = /* @__PURE__ */ new Set();
|
|
2378
|
+
const now = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
2379
|
+
for (const diff of diffs) {
|
|
2380
|
+
const result = { ...diff, copied: [] };
|
|
2381
|
+
if (!opts.check && diff.source && !diff.missing) {
|
|
2382
|
+
for (const rel of diff.changed) {
|
|
2383
|
+
const target = path7.join(project, "ds", diff.namespace, rel);
|
|
2384
|
+
const tmp = `${target}.${process.pid}.tmp`;
|
|
2385
|
+
fs7.copyFileSync(path7.join(diff.source, "project", rel), tmp);
|
|
2386
|
+
fs7.renameSync(tmp, target);
|
|
2387
|
+
result.copied.push(rel);
|
|
2388
|
+
}
|
|
2389
|
+
if (diff.changed.length > 0) synced.add(diff.namespace);
|
|
2390
|
+
}
|
|
2391
|
+
results.push(result);
|
|
2392
|
+
}
|
|
2393
|
+
if (synced.size > 0) {
|
|
2394
|
+
stamp(project, indexFile, synced, now);
|
|
2395
|
+
for (const r of results) if (synced.has(r.namespace)) r.copiedAt = now;
|
|
2396
|
+
}
|
|
2397
|
+
return results;
|
|
2398
|
+
}
|
|
2399
|
+
|
|
2400
|
+
import { createHash as createHash3, randomBytes } from "node:crypto";
|
|
2401
|
+
import fs8 from "node:fs";
|
|
2402
|
+
import path8 from "node:path";
|
|
1502
2403
|
var HISTORY_DIR = "history";
|
|
1503
2404
|
var INDEX = "index.json";
|
|
1504
2405
|
var OBJECTS = "objects";
|
|
@@ -1509,34 +2410,34 @@ var MAX_NOTICES = 50;
|
|
|
1509
2410
|
var NOTICE_DAYS = 7;
|
|
1510
2411
|
var AGENT = { by: "agent", user: { id: "agent", name: "Agent" } };
|
|
1511
2412
|
var SYSTEM = { by: "system", user: { id: "system", name: "" } };
|
|
1512
|
-
var sha256 = (bytes) =>
|
|
2413
|
+
var sha256 = (bytes) => createHash3("sha256").update(bytes).digest("hex");
|
|
1513
2414
|
var SHA = /^[0-9a-f]{64}$/;
|
|
1514
|
-
var
|
|
2415
|
+
var isRecord3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1515
2416
|
function writeAtomic2(file, data) {
|
|
1516
|
-
|
|
1517
|
-
const tmp =
|
|
2417
|
+
fs8.mkdirSync(path8.dirname(file), { recursive: true });
|
|
2418
|
+
const tmp = path8.join(path8.dirname(file), `.${path8.basename(file)}.${process.pid}.${randomBytes(4).toString("hex")}.tmp`);
|
|
1518
2419
|
try {
|
|
1519
|
-
|
|
1520
|
-
|
|
2420
|
+
fs8.writeFileSync(tmp, data);
|
|
2421
|
+
fs8.renameSync(tmp, file);
|
|
1521
2422
|
} catch (error) {
|
|
1522
|
-
|
|
2423
|
+
fs8.rmSync(tmp, { force: true });
|
|
1523
2424
|
throw error;
|
|
1524
2425
|
}
|
|
1525
2426
|
}
|
|
1526
2427
|
var emptyIndex = () => ({ format: 1, next: 1, base: {}, versions: [], notices: [] });
|
|
1527
2428
|
function readUser(value) {
|
|
1528
|
-
return
|
|
2429
|
+
return isRecord3(value) && typeof value.id === "string" && typeof value.name === "string" ? { id: value.id, name: value.name } : null;
|
|
1529
2430
|
}
|
|
1530
2431
|
var isAuthor = (value) => value === "human" || value === "agent" || value === "system";
|
|
1531
2432
|
function parseIndex(raw) {
|
|
1532
2433
|
const index = emptyIndex();
|
|
1533
|
-
if (!
|
|
1534
|
-
if (
|
|
2434
|
+
if (!isRecord3(raw)) return index;
|
|
2435
|
+
if (isRecord3(raw.base)) {
|
|
1535
2436
|
for (const [p, s] of Object.entries(raw.base)) if (typeof s === "string" && SHA.test(s)) index.base[p] = s;
|
|
1536
2437
|
}
|
|
1537
2438
|
if (Array.isArray(raw.versions)) {
|
|
1538
2439
|
for (const v of raw.versions) {
|
|
1539
|
-
if (!
|
|
2440
|
+
if (!isRecord3(v) || typeof v.n !== "number" || !isAuthor(v.by) || typeof v.at !== "string" || !isRecord3(v.changes)) continue;
|
|
1540
2441
|
const user = readUser(v.user);
|
|
1541
2442
|
if (!user) continue;
|
|
1542
2443
|
const changes = {};
|
|
@@ -1548,8 +2449,8 @@ function parseIndex(raw) {
|
|
|
1548
2449
|
if (Array.isArray(v.overwrote)) {
|
|
1549
2450
|
const list = [];
|
|
1550
2451
|
for (const o of v.overwrote) {
|
|
1551
|
-
const who =
|
|
1552
|
-
if (!
|
|
2452
|
+
const who = isRecord3(o) ? readUser(o.user) : null;
|
|
2453
|
+
if (!isRecord3(o) || !who || typeof o.path !== "string" || o.lost !== null && typeof o.lost !== "string" || typeof o.n !== "number" || !isAuthor(o.by)) continue;
|
|
1553
2454
|
list.push({
|
|
1554
2455
|
path: o.path,
|
|
1555
2456
|
lost: o.lost,
|
|
@@ -1571,10 +2472,10 @@ function parseIndex(raw) {
|
|
|
1571
2472
|
index.next = Math.max(typeof raw.next === "number" ? raw.next : 1, (last?.n ?? 0) + 1);
|
|
1572
2473
|
if (Array.isArray(raw.notices)) {
|
|
1573
2474
|
for (const n of raw.notices) {
|
|
1574
|
-
if (!
|
|
2475
|
+
if (!isRecord3(n) || typeof n.id !== "string" || typeof n.to !== "string" || typeof n.path !== "string" || typeof n.n !== "number") continue;
|
|
1575
2476
|
if (n.kind !== "overwritten" && n.kind !== "overwrote") continue;
|
|
1576
2477
|
const by = readUser(n.by);
|
|
1577
|
-
const lost =
|
|
2478
|
+
const lost = isRecord3(n.lost) ? n.lost : null;
|
|
1578
2479
|
const lostUser = lost ? readUser(lost.user) : null;
|
|
1579
2480
|
if (!by || !lost || !lostUser || lost.sha !== null && typeof lost.sha !== "string" || typeof lost.n !== "number" || !isAuthor(lost.by)) continue;
|
|
1580
2481
|
index.notices.push({
|
|
@@ -1600,10 +2501,10 @@ var History = class {
|
|
|
1600
2501
|
/** path → sha: what the files hold after the newest version. */
|
|
1601
2502
|
headState;
|
|
1602
2503
|
constructor(root) {
|
|
1603
|
-
this.dir =
|
|
2504
|
+
this.dir = path8.join(root, HISTORY_DIR);
|
|
1604
2505
|
let raw = null;
|
|
1605
2506
|
try {
|
|
1606
|
-
raw = JSON.parse(
|
|
2507
|
+
raw = JSON.parse(fs8.readFileSync(path8.join(this.dir, INDEX), "utf8"));
|
|
1607
2508
|
} catch {
|
|
1608
2509
|
}
|
|
1609
2510
|
this.index = parseIndex(raw);
|
|
@@ -1647,7 +2548,7 @@ var History = class {
|
|
|
1647
2548
|
read(sha) {
|
|
1648
2549
|
if (!SHA.test(sha)) return null;
|
|
1649
2550
|
try {
|
|
1650
|
-
return
|
|
2551
|
+
return fs8.readFileSync(path8.join(this.dir, OBJECTS, sha));
|
|
1651
2552
|
} catch {
|
|
1652
2553
|
return null;
|
|
1653
2554
|
}
|
|
@@ -1661,7 +2562,7 @@ var History = class {
|
|
|
1661
2562
|
size(sha) {
|
|
1662
2563
|
if (!SHA.test(sha)) return null;
|
|
1663
2564
|
try {
|
|
1664
|
-
return
|
|
2565
|
+
return fs8.statSync(path8.join(this.dir, OBJECTS, sha)).size;
|
|
1665
2566
|
} catch {
|
|
1666
2567
|
return null;
|
|
1667
2568
|
}
|
|
@@ -1795,8 +2696,8 @@ var History = class {
|
|
|
1795
2696
|
return out;
|
|
1796
2697
|
}
|
|
1797
2698
|
putObject(sha, bytes) {
|
|
1798
|
-
const file =
|
|
1799
|
-
if (
|
|
2699
|
+
const file = path8.join(this.dir, OBJECTS, sha);
|
|
2700
|
+
if (fs8.existsSync(file)) return;
|
|
1800
2701
|
writeAtomic2(file, bytes);
|
|
1801
2702
|
}
|
|
1802
2703
|
/** Past MAX_VERSIONS the oldest fold into the base; old notices go; contents no one holds are removed. */
|
|
@@ -1833,14 +2734,14 @@ var History = class {
|
|
|
1833
2734
|
}
|
|
1834
2735
|
let names;
|
|
1835
2736
|
try {
|
|
1836
|
-
names =
|
|
2737
|
+
names = fs8.readdirSync(path8.join(this.dir, OBJECTS));
|
|
1837
2738
|
} catch {
|
|
1838
2739
|
return;
|
|
1839
2740
|
}
|
|
1840
|
-
for (const name of names) if (SHA.test(name) && !keep.has(name))
|
|
2741
|
+
for (const name of names) if (SHA.test(name) && !keep.has(name)) fs8.rmSync(path8.join(this.dir, OBJECTS, name), { force: true });
|
|
1841
2742
|
}
|
|
1842
2743
|
save() {
|
|
1843
|
-
writeAtomic2(
|
|
2744
|
+
writeAtomic2(path8.join(this.dir, INDEX), `${JSON.stringify(this.index)}
|
|
1844
2745
|
`);
|
|
1845
2746
|
}
|
|
1846
2747
|
};
|
|
@@ -1909,157 +2810,16 @@ function diffContents(before, after) {
|
|
|
1909
2810
|
return { added, removed, lines, ...truncated ? { truncated: true } : {} };
|
|
1910
2811
|
}
|
|
1911
2812
|
|
|
1912
|
-
import { createHash as
|
|
1913
|
-
import
|
|
1914
|
-
import
|
|
1915
|
-
import
|
|
2813
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
2814
|
+
import fs11 from "node:fs";
|
|
2815
|
+
import os5 from "node:os";
|
|
2816
|
+
import path11 from "node:path";
|
|
1916
2817
|
|
|
1917
|
-
import
|
|
2818
|
+
import fs9 from "node:fs";
|
|
1918
2819
|
import fsp2 from "node:fs/promises";
|
|
1919
|
-
import
|
|
2820
|
+
import path9 from "node:path";
|
|
1920
2821
|
import { promisify } from "node:util";
|
|
1921
2822
|
import zlib from "node:zlib";
|
|
1922
|
-
|
|
1923
|
-
import crypto from "node:crypto";
|
|
1924
|
-
import fs6 from "node:fs";
|
|
1925
|
-
import path6 from "node:path";
|
|
1926
|
-
var GUEST_ROLES = ["editor", "commenter", "viewer"];
|
|
1927
|
-
var isGuestRole = (value) => typeof value === "string" && GUEST_ROLES.includes(value);
|
|
1928
|
-
var SHARING_FILE = ".sharing.json";
|
|
1929
|
-
var MAX_GUESTS = 200;
|
|
1930
|
-
var MAX_NAME = 40;
|
|
1931
|
-
var token = (bytes = 18) => crypto.randomBytes(bytes).toString("base64url");
|
|
1932
|
-
var isRecord3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1933
|
-
function fresh() {
|
|
1934
|
-
return { lan: false, links: { editor: { token: token(), on: true }, commenter: { token: token(), on: true }, viewer: { token: token(), on: true } }, guests: [] };
|
|
1935
|
-
}
|
|
1936
|
-
function cleanName(raw) {
|
|
1937
|
-
const name = typeof raw === "string" ? raw.replace(/[\u0000-\u001f\u007f@<>]/g, "").replace(/\s+/g, " ").trim() : "";
|
|
1938
|
-
return [...name].slice(0, MAX_NAME).join("");
|
|
1939
|
-
}
|
|
1940
|
-
function guestUser(guest) {
|
|
1941
|
-
return { id: `guest-${guest.id}`, name: guest.name, email: `${guest.name}@${guest.id}.guest` };
|
|
1942
|
-
}
|
|
1943
|
-
function isLoopback(address) {
|
|
1944
|
-
if (!address) return false;
|
|
1945
|
-
const a = address.startsWith("::ffff:") ? address.slice(7) : address;
|
|
1946
|
-
return a === "::1" || a.startsWith("127.");
|
|
1947
|
-
}
|
|
1948
|
-
var Sharing = class _Sharing {
|
|
1949
|
-
file;
|
|
1950
|
-
state;
|
|
1951
|
-
constructor(root) {
|
|
1952
|
-
this.file = path6.join(root, SHARING_FILE);
|
|
1953
|
-
this.state = _Sharing.read(this.file);
|
|
1954
|
-
}
|
|
1955
|
-
static read(file) {
|
|
1956
|
-
let raw;
|
|
1957
|
-
try {
|
|
1958
|
-
raw = JSON.parse(fs6.readFileSync(file, "utf8"));
|
|
1959
|
-
} catch {
|
|
1960
|
-
return fresh();
|
|
1961
|
-
}
|
|
1962
|
-
const state = fresh();
|
|
1963
|
-
if (!isRecord3(raw)) return state;
|
|
1964
|
-
state.lan = raw.lan === true;
|
|
1965
|
-
if (typeof raw.port === "number" && Number.isInteger(raw.port) && raw.port > 0 && raw.port < 65536) state.port = raw.port;
|
|
1966
|
-
if (isRecord3(raw.links)) {
|
|
1967
|
-
for (const role of GUEST_ROLES) {
|
|
1968
|
-
const link = raw.links[role];
|
|
1969
|
-
if (isRecord3(link) && typeof link.token === "string" && link.token.length >= 16) state.links[role] = { token: link.token, on: link.on !== false };
|
|
1970
|
-
}
|
|
1971
|
-
}
|
|
1972
|
-
if (Array.isArray(raw.guests)) {
|
|
1973
|
-
for (const g of raw.guests) {
|
|
1974
|
-
if (!isRecord3(g) || typeof g.id !== "string" || typeof g.secret !== "string" || !isGuestRole(g.role) || typeof g.link !== "string") continue;
|
|
1975
|
-
state.guests.push({
|
|
1976
|
-
id: g.id,
|
|
1977
|
-
secret: g.secret,
|
|
1978
|
-
name: cleanName(g.name) || "Guest",
|
|
1979
|
-
role: g.role,
|
|
1980
|
-
link: g.link,
|
|
1981
|
-
joined: typeof g.joined === "string" ? g.joined : (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
1982
|
-
...typeof g.seen === "string" ? { seen: g.seen } : {}
|
|
1983
|
-
});
|
|
1984
|
-
}
|
|
1985
|
-
}
|
|
1986
|
-
return state;
|
|
1987
|
-
}
|
|
1988
|
-
save() {
|
|
1989
|
-
const tmp = `${this.file}.${process.pid}.tmp`;
|
|
1990
|
-
fs6.writeFileSync(tmp, `${JSON.stringify(this.state, null, 2)}
|
|
1991
|
-
`, { mode: 384 });
|
|
1992
|
-
fs6.renameSync(tmp, this.file);
|
|
1993
|
-
}
|
|
1994
|
-
/** The role whose link has this token, while that link is on. */
|
|
1995
|
-
linkRole(tokenGiven) {
|
|
1996
|
-
for (const role of GUEST_ROLES) {
|
|
1997
|
-
const link = this.state.links[role];
|
|
1998
|
-
if (link.on && tokenGiven.length === link.token.length && crypto.timingSafeEqual(Buffer.from(tokenGiven), Buffer.from(link.token))) return role;
|
|
1999
|
-
}
|
|
2000
|
-
return null;
|
|
2001
|
-
}
|
|
2002
|
-
/** The guest a cookie names, if they still have access (their link on and not reset). */
|
|
2003
|
-
guest(secret) {
|
|
2004
|
-
if (!secret) return null;
|
|
2005
|
-
const guest = this.state.guests.find((g) => g.secret === secret);
|
|
2006
|
-
return guest && this.valid(guest) ? guest : null;
|
|
2007
|
-
}
|
|
2008
|
-
valid(guest) {
|
|
2009
|
-
const link = this.state.links[guest.role];
|
|
2010
|
-
return link.on && link.token === guest.link && this.state.guests.includes(guest);
|
|
2011
|
-
}
|
|
2012
|
-
join(role, name) {
|
|
2013
|
-
const guest = { id: token(4), secret: token(24), name: cleanName(name) || "Guest", role, link: this.state.links[role].token, joined: (/* @__PURE__ */ new Date()).toISOString() };
|
|
2014
|
-
this.state.guests.push(guest);
|
|
2015
|
-
if (this.state.guests.length > MAX_GUESTS) this.state.guests.splice(0, this.state.guests.length - MAX_GUESTS);
|
|
2016
|
-
this.save();
|
|
2017
|
-
return guest;
|
|
2018
|
-
}
|
|
2019
|
-
/** Marks a guest seen now (saved at most once a minute per guest). */
|
|
2020
|
-
seen(guest) {
|
|
2021
|
-
const now = /* @__PURE__ */ new Date();
|
|
2022
|
-
if (guest.seen && now.getTime() - Date.parse(guest.seen) < 6e4) return;
|
|
2023
|
-
guest.seen = now.toISOString();
|
|
2024
|
-
this.save();
|
|
2025
|
-
}
|
|
2026
|
-
setLan(on, port) {
|
|
2027
|
-
this.state.lan = on;
|
|
2028
|
-
if (on && port) this.state.port = port;
|
|
2029
|
-
this.save();
|
|
2030
|
-
}
|
|
2031
|
-
setLink(role, on) {
|
|
2032
|
-
this.state.links[role].on = on;
|
|
2033
|
-
this.save();
|
|
2034
|
-
}
|
|
2035
|
-
/** A new token for the link: the old link stops working and the guests who came through it are dropped. */
|
|
2036
|
-
resetLink(role) {
|
|
2037
|
-
const old = this.state.links[role].token;
|
|
2038
|
-
this.state.links[role] = { token: token(), on: this.state.links[role].on };
|
|
2039
|
-
this.state.guests = this.state.guests.filter((g) => g.link !== old);
|
|
2040
|
-
this.save();
|
|
2041
|
-
}
|
|
2042
|
-
/**
|
|
2043
|
-
* Gives a guest another role. They now hang on that role's link (turning it off or resetting it drops them), so a role
|
|
2044
|
-
* whose link is off would shut them out: false then, as for a guest who isn't there.
|
|
2045
|
-
*/
|
|
2046
|
-
setRole(id, role) {
|
|
2047
|
-
const guest = this.state.guests.find((g) => g.id === id);
|
|
2048
|
-
if (!guest || !this.state.links[role].on) return false;
|
|
2049
|
-
guest.role = role;
|
|
2050
|
-
guest.link = this.state.links[role].token;
|
|
2051
|
-
this.save();
|
|
2052
|
-
return true;
|
|
2053
|
-
}
|
|
2054
|
-
removeGuest(id) {
|
|
2055
|
-
const before = this.state.guests.length;
|
|
2056
|
-
this.state.guests = this.state.guests.filter((g) => g.id !== id);
|
|
2057
|
-
if (this.state.guests.length === before) return false;
|
|
2058
|
-
this.save();
|
|
2059
|
-
return true;
|
|
2060
|
-
}
|
|
2061
|
-
};
|
|
2062
|
-
|
|
2063
2823
|
var LOCAL_FILES = /* @__PURE__ */ new Set([STATE_FILE, LOG_FILE, SHARING_FILE]);
|
|
2064
2824
|
var LOCAL_DIRS = /* @__PURE__ */ new Set([EXPORTS_DIR]);
|
|
2065
2825
|
var JUNK_FILES = /* @__PURE__ */ new Set([".DS_Store", "Thumbs.db", "desktop.ini"]);
|
|
@@ -2071,10 +2831,10 @@ var ArchiveTooLarge = class extends Error {
|
|
|
2071
2831
|
code = "TOO_LARGE";
|
|
2072
2832
|
};
|
|
2073
2833
|
async function archiveEntries(root, skip = []) {
|
|
2074
|
-
const folder =
|
|
2075
|
-
const skipped = new Set(skip.map((file) =>
|
|
2834
|
+
const folder = path9.basename(path9.resolve(root)) || "artifact";
|
|
2835
|
+
const skipped = new Set(skip.map((file) => path9.resolve(file)));
|
|
2076
2836
|
const entries = [];
|
|
2077
|
-
const
|
|
2837
|
+
const walk2 = async (dir, rel) => {
|
|
2078
2838
|
let names;
|
|
2079
2839
|
try {
|
|
2080
2840
|
names = await fsp2.readdir(dir, { withFileTypes: true });
|
|
@@ -2085,17 +2845,17 @@ async function archiveEntries(root, skip = []) {
|
|
|
2085
2845
|
for (const dirent of names.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0)) {
|
|
2086
2846
|
const name = dirent.name;
|
|
2087
2847
|
if (JUNK_FILES.has(name) || isTemp(name) || !rel && (LOCAL_FILES.has(name) || LOCAL_DIRS.has(name) && dirent.isDirectory())) continue;
|
|
2088
|
-
const file =
|
|
2848
|
+
const file = path9.join(dir, name);
|
|
2089
2849
|
if (skipped.has(file)) continue;
|
|
2090
2850
|
const inner = rel ? `${rel}/${name}` : name;
|
|
2091
|
-
if (dirent.isDirectory()) await
|
|
2851
|
+
if (dirent.isDirectory()) await walk2(file, inner);
|
|
2092
2852
|
else if (dirent.isFile()) {
|
|
2093
2853
|
const stat = await fsp2.stat(file).catch(() => null);
|
|
2094
2854
|
if (stat?.isFile()) entries.push({ name: `${folder}/${inner}`, file, size: stat.size, mode: stat.mode & 511, mtime: stat.mtime });
|
|
2095
2855
|
}
|
|
2096
2856
|
}
|
|
2097
2857
|
};
|
|
2098
|
-
await
|
|
2858
|
+
await walk2(path9.resolve(root), "");
|
|
2099
2859
|
if (entries.length > MAX_ENTRIES) throw new ArchiveTooLarge(`The folder holds ${entries.length} files, more than a zip holds (${MAX_ENTRIES}); copy the folder itself instead.`);
|
|
2100
2860
|
const bound = entries.reduce((sum, entry) => sum + entry.size + 2 * (46 + Buffer.byteLength(entry.name)), 22);
|
|
2101
2861
|
if (bound > MAX_BYTES) throw new ArchiveTooLarge(`The folder holds ${(bound / 2 ** 30).toFixed(1)} GiB, more than a zip holds (4 GiB); copy the folder itself instead.`);
|
|
@@ -2144,7 +2904,7 @@ async function writeZip(entries, write) {
|
|
|
2144
2904
|
const crc = crc32(data);
|
|
2145
2905
|
let method = 0;
|
|
2146
2906
|
let body = data;
|
|
2147
|
-
if (data.length > 0 && !STORED.has(
|
|
2907
|
+
if (data.length > 0 && !STORED.has(path9.extname(entry.name).toLowerCase())) {
|
|
2148
2908
|
const deflated = await deflateRaw(data);
|
|
2149
2909
|
if (deflated.length < data.length) {
|
|
2150
2910
|
method = 8;
|
|
@@ -2198,12 +2958,12 @@ async function writeZip(entries, write) {
|
|
|
2198
2958
|
}
|
|
2199
2959
|
function archiveFileName(root, now = /* @__PURE__ */ new Date()) {
|
|
2200
2960
|
const day = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
|
|
2201
|
-
return `${
|
|
2961
|
+
return `${path9.basename(path9.resolve(root)) || "artifact"}-${day}.zip`;
|
|
2202
2962
|
}
|
|
2203
2963
|
async function writeArchive(root, out) {
|
|
2204
|
-
const file =
|
|
2964
|
+
const file = path9.resolve(out);
|
|
2205
2965
|
const entries = await archiveEntries(root, [file]);
|
|
2206
|
-
const tmp =
|
|
2966
|
+
const tmp = path9.join(path9.dirname(file), `.${path9.basename(file)}.${process.pid}.tmp`);
|
|
2207
2967
|
const handle = await fsp2.open(tmp, "w");
|
|
2208
2968
|
let bytes;
|
|
2209
2969
|
try {
|
|
@@ -2221,10 +2981,10 @@ async function writeArchive(root, out) {
|
|
|
2221
2981
|
return { file, files: entries.length, bytes };
|
|
2222
2982
|
}
|
|
2223
2983
|
async function archiveCommand(root, out) {
|
|
2224
|
-
const where = out ?
|
|
2225
|
-
const target =
|
|
2984
|
+
const where = out ? path9.resolve(out) : path9.dirname(path9.resolve(root));
|
|
2985
|
+
const target = fs9.statSync(where, { throwIfNoEntry: false })?.isDirectory() ? path9.join(where, archiveFileName(root)) : where;
|
|
2226
2986
|
const { file, files, bytes } = await writeArchive(root, target);
|
|
2227
|
-
return { file, folder:
|
|
2987
|
+
return { file, folder: path9.basename(path9.resolve(root)), files, bytes, leftOut: [...LOCAL_FILES] };
|
|
2228
2988
|
}
|
|
2229
2989
|
function attachment(name) {
|
|
2230
2990
|
const ascii = name.replace(/[^\x20-\x7e]|["\\]/g, "_");
|
|
@@ -2272,31 +3032,31 @@ async function sendArchive(request, response, root) {
|
|
|
2272
3032
|
}
|
|
2273
3033
|
}
|
|
2274
3034
|
|
|
2275
|
-
import
|
|
3035
|
+
import fs10 from "node:fs";
|
|
2276
3036
|
import fsp3 from "node:fs/promises";
|
|
2277
|
-
import
|
|
2278
|
-
import
|
|
3037
|
+
import os4 from "node:os";
|
|
3038
|
+
import path10 from "node:path";
|
|
2279
3039
|
var MAX_BYTES2 = 4294967295;
|
|
2280
3040
|
function shown(file) {
|
|
2281
|
-
const home =
|
|
2282
|
-
return home && (file === home || file.startsWith(home +
|
|
3041
|
+
const home = os4.homedir();
|
|
3042
|
+
return home && (file === home || file.startsWith(home + path10.sep)) ? `~${file.slice(home.length)}` : file;
|
|
2283
3043
|
}
|
|
2284
3044
|
function saved(file, bytes) {
|
|
2285
|
-
return { path: file, name:
|
|
3045
|
+
return { path: file, name: path10.basename(file), folder: path10.dirname(file), shown: shown(file), bytes };
|
|
2286
3046
|
}
|
|
2287
3047
|
function safeFileName(raw) {
|
|
2288
3048
|
let name = (raw.replace(/\\/g, "/").split("/").pop() ?? "").replace(/[\u0000-\u001f\u007f<>:"|?*]/g, "_").replace(/^[.\s]+|[.\s]+$/g, "");
|
|
2289
3049
|
if (name.length > 200) {
|
|
2290
|
-
const ext =
|
|
3050
|
+
const ext = path10.extname(name).slice(0, 16);
|
|
2291
3051
|
name = name.slice(0, 200 - ext.length) + ext;
|
|
2292
3052
|
}
|
|
2293
3053
|
return name || "download";
|
|
2294
3054
|
}
|
|
2295
3055
|
async function openFree(dir, name) {
|
|
2296
|
-
const ext =
|
|
3056
|
+
const ext = path10.extname(name);
|
|
2297
3057
|
const stem = name.slice(0, name.length - ext.length);
|
|
2298
3058
|
for (let n = 0; ; n++) {
|
|
2299
|
-
const file =
|
|
3059
|
+
const file = path10.join(dir, n === 0 ? name : `${stem} (${n})${ext}`);
|
|
2300
3060
|
try {
|
|
2301
3061
|
return { file, handle: await fsp3.open(file, "wx") };
|
|
2302
3062
|
} catch (error) {
|
|
@@ -2315,7 +3075,7 @@ async function saveDownload(request, response, root) {
|
|
|
2315
3075
|
let handle = null;
|
|
2316
3076
|
let bytes = 0;
|
|
2317
3077
|
try {
|
|
2318
|
-
const dir =
|
|
3078
|
+
const dir = path10.join(root, EXPORTS_DIR);
|
|
2319
3079
|
await fsp3.mkdir(dir, { recursive: true });
|
|
2320
3080
|
({ file, handle } = await openFree(dir, name));
|
|
2321
3081
|
for await (const chunk of request) {
|
|
@@ -2340,7 +3100,7 @@ async function saveArchive(request, response, root) {
|
|
|
2340
3100
|
request.resume();
|
|
2341
3101
|
try {
|
|
2342
3102
|
const { file } = await archiveCommand(root, void 0);
|
|
2343
|
-
sendJson2(response, 200, saved(file,
|
|
3103
|
+
sendJson2(response, 200, saved(file, fs10.statSync(file).size));
|
|
2344
3104
|
} catch (error) {
|
|
2345
3105
|
const tooLarge = error instanceof ArchiveTooLarge;
|
|
2346
3106
|
sendJson2(response, tooLarge ? 413 : 500, { code: tooLarge ? "too_large" : "upstream_error", message: error instanceof Error ? error.message : String(error) });
|
|
@@ -2369,108 +3129,73 @@ function commentsViewer(viewer) {
|
|
|
2369
3129
|
var isRecord4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2370
3130
|
var esc2 = (text) => text.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
2371
3131
|
var script = (value) => JSON.stringify(value).replace(/</g, "\\u003c");
|
|
2372
|
-
function ownHosts(host, port) {
|
|
2373
|
-
const named = host === "0.0.0.0" || host === "::" ? [] : [host.includes(":") ? `[${host}]:${port}` : `${host}:${port}`];
|
|
2374
|
-
return [.../* @__PURE__ */ new Set([`127.0.0.1:${port}`, `localhost:${port}`, `[::1]:${port}`, ...named])];
|
|
2375
|
-
}
|
|
2376
3132
|
function localOwner() {
|
|
2377
3133
|
let name = "You";
|
|
2378
3134
|
try {
|
|
2379
|
-
name =
|
|
3135
|
+
name = os5.userInfo().username.trim() || name;
|
|
2380
3136
|
} catch {
|
|
2381
3137
|
}
|
|
2382
3138
|
return { id: "local", name, email: `${name}@localhost` };
|
|
2383
3139
|
}
|
|
2384
|
-
var guestCookieName = (kind, root) => `${kind.replace(/-/g, "_")}_guest_${
|
|
3140
|
+
var guestCookieName = (kind, root) => `${kind.replace(/-/g, "_")}_guest_${createHash4("sha256").update(path11.resolve(root)).digest("hex").slice(0, 12)}`;
|
|
2385
3141
|
var ShareHost = class {
|
|
2386
3142
|
sharing;
|
|
2387
3143
|
options;
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
lanServers = /* @__PURE__ */ new Map();
|
|
3144
|
+
network;
|
|
3145
|
+
page;
|
|
3146
|
+
/** Open to the local network now (the hub's listeners are up, for this artifact too). */
|
|
3147
|
+
on = false;
|
|
2393
3148
|
live = /* @__PURE__ */ new Set();
|
|
2394
3149
|
constructor(options) {
|
|
2395
3150
|
this.options = options;
|
|
3151
|
+
this.network = options.network;
|
|
3152
|
+
this.page = options.page;
|
|
2396
3153
|
this.sharing = new Sharing(options.root);
|
|
2397
3154
|
}
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
async listen(server2, { port, host, lan = false, portGiven = false }) {
|
|
3155
|
+
get port() {
|
|
3156
|
+
return this.network.port;
|
|
3157
|
+
}
|
|
3158
|
+
/** The hub took the artifact: open to the network now with `lan`, or if it was left shared. */
|
|
3159
|
+
async start(lan = false) {
|
|
2404
3160
|
const { state } = this.sharing;
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
else if (!linksPort && port && taken !== port) this.options.log(`port ${port} is in use; took ${taken}`);
|
|
2409
|
-
if (lan && !this.sharing.state.lan) this.sharing.setLan(true, taken);
|
|
2410
|
-
await this.listening(taken, ownHosts(host, taken));
|
|
2411
|
-
return taken;
|
|
3161
|
+
if (!lan && !state.lan) return;
|
|
3162
|
+
if (state.port && state.port !== this.port) this.options.log(`port ${state.port} (the shared links') is not this server's; it is on ${this.port}: give the new links out again`);
|
|
3163
|
+
await this.setLan(true);
|
|
2412
3164
|
}
|
|
2413
3165
|
/** The artifact at each LAN address it is shared on ([] while it is not). */
|
|
2414
3166
|
lanUrls() {
|
|
2415
|
-
return
|
|
2416
|
-
}
|
|
2417
|
-
/** The main listener is up on `port` (answering `hosts`): open to the network now if the artifact was left shared. */
|
|
2418
|
-
async listening(port, hosts) {
|
|
2419
|
-
this.port = port;
|
|
2420
|
-
this.hosts = new Set(hosts);
|
|
2421
|
-
if (this.sharing.state.lan) await this.setLan(true);
|
|
3167
|
+
return this.on ? this.network.addresses().map((a) => `http://${a}:${this.port}${this.page}`) : [];
|
|
2422
3168
|
}
|
|
2423
3169
|
/** Open to the local network now. */
|
|
2424
3170
|
get shared() {
|
|
2425
|
-
return this.
|
|
3171
|
+
return this.on;
|
|
2426
3172
|
}
|
|
2427
|
-
/**
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
if (this.hosts.has(host)) return true;
|
|
2431
|
-
if (!this.lanServers.size) return false;
|
|
2432
|
-
const suffix = `:${this.port}`;
|
|
2433
|
-
if (!host.endsWith(suffix)) return false;
|
|
2434
|
-
const name = host.slice(0, -suffix.length).toLowerCase();
|
|
2435
|
-
const machine = os4.hostname().toLowerCase();
|
|
2436
|
-
return this.lanServers.has(name) || name === machine || name === `${machine.replace(/\.local$/, "")}.local`;
|
|
3173
|
+
/** The artifact's name, for the join page and the list of what is open at the server's root. */
|
|
3174
|
+
title() {
|
|
3175
|
+
return this.options.title();
|
|
2437
3176
|
}
|
|
2438
3177
|
/**
|
|
2439
|
-
* Opens the artifact to the local network
|
|
2440
|
-
*
|
|
3178
|
+
* Opens the artifact to the local network or closes it, and returns the addresses it is on. Guests already in keep
|
|
3179
|
+
* their place for when it opens again.
|
|
2441
3180
|
*/
|
|
2442
3181
|
async setLan(on) {
|
|
2443
3182
|
if (!on) {
|
|
2444
|
-
this.
|
|
3183
|
+
this.on = false;
|
|
3184
|
+
this.network.release();
|
|
2445
3185
|
for (const live of [...this.live]) if (live.viewer.guest) live.close("unshared");
|
|
2446
3186
|
if (this.sharing.state.lan) this.sharing.setLan(false);
|
|
2447
3187
|
return [];
|
|
2448
3188
|
}
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
const server2 = this.options.createServer();
|
|
2452
|
-
try {
|
|
2453
|
-
await listen(server2, this.port, address);
|
|
2454
|
-
this.lanServers.set(address, server2);
|
|
2455
|
-
} catch (error) {
|
|
2456
|
-
server2.close();
|
|
2457
|
-
this.options.log(`not listening on ${address}:${this.port}: ${error.message}`);
|
|
2458
|
-
}
|
|
2459
|
-
}
|
|
3189
|
+
const addresses = await this.network.open();
|
|
3190
|
+
this.on = addresses.length > 0;
|
|
2460
3191
|
if (!this.sharing.state.lan || this.sharing.state.port !== this.port) this.sharing.setLan(true, this.port);
|
|
2461
|
-
this.options.log(`shared on the local network: ${
|
|
2462
|
-
return
|
|
2463
|
-
}
|
|
2464
|
-
closeLan() {
|
|
2465
|
-
for (const server2 of this.lanServers.values()) {
|
|
2466
|
-
server2.close();
|
|
2467
|
-
server2.closeAllConnections?.();
|
|
2468
|
-
}
|
|
2469
|
-
this.lanServers.clear();
|
|
3192
|
+
this.options.log(`shared on the local network: ${addresses.map((a) => `http://${a}:${this.port}${this.page}`).join(", ") || "(no LAN address)"}`);
|
|
3193
|
+
return addresses;
|
|
2470
3194
|
}
|
|
2471
|
-
/**
|
|
3195
|
+
/** The artifact left the hub: it stays shared in .sharing.json, for the next time it opens. */
|
|
2472
3196
|
close() {
|
|
2473
|
-
this.
|
|
3197
|
+
this.on = false;
|
|
3198
|
+
this.network.release();
|
|
2474
3199
|
}
|
|
2475
3200
|
// ---- who is asking ---------------------------------------------------------------------------------------------
|
|
2476
3201
|
cookie(request) {
|
|
@@ -2488,7 +3213,7 @@ var ShareHost = class {
|
|
|
2488
3213
|
*/
|
|
2489
3214
|
viewerOf(request) {
|
|
2490
3215
|
if (isLoopback(request.socket.remoteAddress)) return { role: "owner", user: this.options.owner };
|
|
2491
|
-
const guest = this.
|
|
3216
|
+
const guest = this.on ? this.sharing.guest(this.cookie(request)) : null;
|
|
2492
3217
|
if (!guest) return null;
|
|
2493
3218
|
this.sharing.seen(guest);
|
|
2494
3219
|
return { role: guest.role, user: guestUser(guest), guest };
|
|
@@ -2506,7 +3231,7 @@ var ShareHost = class {
|
|
|
2506
3231
|
}
|
|
2507
3232
|
/** Still allowed in: the owner, or a guest whose link is on and who was not removed. */
|
|
2508
3233
|
stillIn(viewer) {
|
|
2509
|
-
return !viewer.guest || this.
|
|
3234
|
+
return !viewer.guest || this.on && this.sharing.valid(viewer.guest);
|
|
2510
3235
|
}
|
|
2511
3236
|
/** Ends the streams of guests who lost access (a link turned off or reset, a guest removed). */
|
|
2512
3237
|
dropRevoked() {
|
|
@@ -2546,7 +3271,7 @@ var ShareHost = class {
|
|
|
2546
3271
|
}
|
|
2547
3272
|
if (method === "GET" && p === "/__share/invite") return sendJson2(response, 200, this.invites(viewer, request.headers.host ?? "")), true;
|
|
2548
3273
|
if (method === "GET" && p === "/__share/ui.js") {
|
|
2549
|
-
|
|
3274
|
+
fs11.stat(this.options.uiFile, (error, stat) => {
|
|
2550
3275
|
if (error || !stat.isFile()) return sendJson2(response, 404, { code: "not_found", message: "The Share panel is missing." });
|
|
2551
3276
|
sendFile(request, response, this.options.uiFile, { "Content-Type": "text/javascript; charset=utf-8", "Content-Length": stat.size, "Cache-Control": "no-store", "X-Content-Type-Options": "nosniff" });
|
|
2552
3277
|
});
|
|
@@ -2565,8 +3290,8 @@ var ShareHost = class {
|
|
|
2565
3290
|
* ("?…") goes to the boot script, for `globals`.
|
|
2566
3291
|
*/
|
|
2567
3292
|
headTags(_viewer, query = "") {
|
|
2568
|
-
let tags = `<script src="
|
|
2569
|
-
if (
|
|
3293
|
+
let tags = `<script src="${this.page}__share/boot.js${query.replace(/[<>"']/g, "").replace(/&/g, "&")}"></script>`;
|
|
3294
|
+
if (fs11.existsSync(this.options.uiFile)) tags += `<script type="module" src="${this.page}__share/ui.js"></script>`;
|
|
2570
3295
|
return tags;
|
|
2571
3296
|
}
|
|
2572
3297
|
/** `html` with the head tags first in its <head>; null when it has none. */
|
|
@@ -2576,10 +3301,10 @@ var ShareHost = class {
|
|
|
2576
3301
|
const end = html.indexOf(">", at) + 1;
|
|
2577
3302
|
return html.slice(0, end) + this.headTags(viewer, query) + html.slice(end);
|
|
2578
3303
|
}
|
|
2579
|
-
/** What the page runs before anything else (see SECURE_CONTEXT_SHIM and GUEST_WATCH). */
|
|
3304
|
+
/** What the page runs before anything else (see ROOT_SHIM, SECURE_CONTEXT_SHIM and GUEST_WATCH). */
|
|
2580
3305
|
bootScript(viewer, query = new URLSearchParams()) {
|
|
2581
3306
|
const share = { ...this.me(viewer), noun: this.options.noun, title: this.options.title(), ...this.options.reactions ? { reactions: true } : {} };
|
|
2582
|
-
let js =
|
|
3307
|
+
let js = `(function(BASE){${ROOT_SHIM}})(${script(this.page)});${SECURE_CONTEXT_SHIM}window.__baoyuShare=${script(share)};`;
|
|
2583
3308
|
for (const [name, value] of Object.entries(this.options.globals?.(viewer, query) ?? {})) js += `window[${script(name)}]=${script(value)};`;
|
|
2584
3309
|
if (viewer.guest) js += GUEST_WATCH;
|
|
2585
3310
|
return js;
|
|
@@ -2592,6 +3317,11 @@ var ShareHost = class {
|
|
|
2592
3317
|
const t = this.words(request);
|
|
2593
3318
|
this.sendPage(response, 403, t.noAccessTitle, `<h1>${esc2(t.noAccessTitle)}</h1><p>${esc2(t.noAccess)}</p>${this.ownerHint(request, t)}`);
|
|
2594
3319
|
}
|
|
3320
|
+
/** `/` on the server for someone who may see more than one artifact: a page of links. */
|
|
3321
|
+
sendList(response, sites) {
|
|
3322
|
+
const noun = this.options.noun.en;
|
|
3323
|
+
this.sendPage(response, 200, `Open ${noun}s`, `<h1>Open ${esc2(noun)}s</h1><ul>${sites.map((site) => `<li><a href="${esc2(site.page)}">${esc2(site.title)}</a></li>`).join("")}</ul>`);
|
|
3324
|
+
}
|
|
2595
3325
|
/** A small page of our own (the join form, the refusal), in the reader's language. */
|
|
2596
3326
|
sendPage(response, status, title, body, headers = {}) {
|
|
2597
3327
|
const html = `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${esc2(title)}</title><style>${PAGE_CSS}</style></head><body><main>${body}</main></body></html>`;
|
|
@@ -2606,8 +3336,8 @@ var ShareHost = class {
|
|
|
2606
3336
|
/** On the computer that shares the artifact, over its LAN address: the way back to the owner's page. */
|
|
2607
3337
|
ownerHint(request, t) {
|
|
2608
3338
|
const from = (request.socket.remoteAddress ?? "").replace(/^::ffff:/, "");
|
|
2609
|
-
if (!this.
|
|
2610
|
-
const url = `http://127.0.0.1:${this.port}
|
|
3339
|
+
if (!this.network.addresses().includes(from)) return "";
|
|
3340
|
+
const url = `http://127.0.0.1:${this.port}${this.page}`;
|
|
2611
3341
|
return `<p class="note">${esc2(t.ownerHere)} <a href="${esc2(url)}">${esc2(url)}</a></p>`;
|
|
2612
3342
|
}
|
|
2613
3343
|
/** GET /s/<token>: the name form (or straight in, for a guest already in through this link); POST: join. */
|
|
@@ -2617,16 +3347,16 @@ var ShareHost = class {
|
|
|
2617
3347
|
const local = isLoopback(request.socket.remoteAddress);
|
|
2618
3348
|
const form = method === "POST" ? await readBody(request, 8 << 10).then((body) => body.toString("utf8"), () => "") : "";
|
|
2619
3349
|
const title = this.options.title();
|
|
2620
|
-
if (!role || !local && !this.
|
|
3350
|
+
if (!role || !local && !this.on) return this.sendPage(response, 404, t.linkGoneTitle, `<h1>${esc2(t.linkGoneTitle)}</h1><p>${esc2(t.linkGone)}</p>`);
|
|
2621
3351
|
if (local) {
|
|
2622
|
-
const lan =
|
|
2623
|
-
const there = lan ? `http://${lan}:${this.port}
|
|
3352
|
+
const lan = this.on ? this.network.addresses()[0] : void 0;
|
|
3353
|
+
const there = lan ? `http://${lan}:${this.port}${this.page}s/${token2}` : "";
|
|
2624
3354
|
const tryIt = there ? `<p>${esc2(t.tryRole(t.roles[role]))} <a href="${esc2(there)}">${esc2(there)}</a></p>` : "";
|
|
2625
|
-
return this.sendPage(response, 200, title, `<h1>${esc2(title)}</h1><p>${esc2(t.youOwn)} <a href="
|
|
3355
|
+
return this.sendPage(response, 200, title, `<h1>${esc2(title)}</h1><p>${esc2(t.youOwn)} <a href="${esc2(this.page)}">${esc2(t.openAsOwner)}</a></p>${tryIt}`);
|
|
2626
3356
|
}
|
|
2627
3357
|
const current = this.sharing.guest(this.cookie(request));
|
|
2628
3358
|
if (method === "GET" && current && current.link === this.sharing.state.links[role].token) {
|
|
2629
|
-
response.writeHead(303, { Location:
|
|
3359
|
+
response.writeHead(303, { Location: this.page, "Cache-Control": "no-store" }).end();
|
|
2630
3360
|
return;
|
|
2631
3361
|
}
|
|
2632
3362
|
if (method === "GET") {
|
|
@@ -2641,21 +3371,21 @@ var ShareHost = class {
|
|
|
2641
3371
|
const guest = this.sharing.join(role, name);
|
|
2642
3372
|
this.options.log(`share: ${guest.name} joined as ${role} (${request.socket.remoteAddress})`);
|
|
2643
3373
|
const cookie = `${this.options.cookieName}=${guest.secret}; Path=/; HttpOnly; SameSite=Lax; Max-Age=31536000`;
|
|
2644
|
-
response.writeHead(303, { Location:
|
|
3374
|
+
response.writeHead(303, { Location: this.page, "Set-Cookie": cookie, "Cache-Control": "no-store" }).end();
|
|
2645
3375
|
}
|
|
2646
3376
|
/**
|
|
2647
3377
|
* The links this person may pass on: their own role's and the ones below it that are on (the owner: every one on), at
|
|
2648
|
-
* the address they came by (`host`, a Host header
|
|
3378
|
+
* the address they came by (`host`, a Host header the hub let in). A guest could forward the link they joined
|
|
2649
3379
|
* with anyway; a lower one gives no more than that. Who joins through one shows in the owner's panel like anyone else.
|
|
2650
3380
|
*/
|
|
2651
3381
|
invites(viewer, host) {
|
|
2652
3382
|
const { links } = this.sharing.state;
|
|
2653
3383
|
const from = viewer.role === "owner" ? 0 : GUEST_ROLES.indexOf(viewer.role);
|
|
2654
|
-
return { links: GUEST_ROLES.slice(from).filter((role) => links[role].on).map((role) => ({ role, url: `http://${host}
|
|
3384
|
+
return { links: GUEST_ROLES.slice(from).filter((role) => links[role].on).map((role) => ({ role, url: `http://${host}${this.page}s/${links[role].token}` })) };
|
|
2655
3385
|
}
|
|
2656
3386
|
/** Links, addresses and people, for the share panel and `artifact.mjs share`. */
|
|
2657
3387
|
shareView() {
|
|
2658
|
-
const addresses =
|
|
3388
|
+
const addresses = (this.on ? this.network.addresses() : []).sort((a, b) => Number(isTailnet(a)) - Number(isTailnet(b)));
|
|
2659
3389
|
const local = addresses.filter((a) => !isTailnet(a));
|
|
2660
3390
|
const base = local.length ? `http://${local[0]}:${this.port}` : addresses.length ? `http://${addresses[0]}:${this.port}` : `http://127.0.0.1:${this.port}`;
|
|
2661
3391
|
const tailnet = addresses.find(isTailnet);
|
|
@@ -2666,12 +3396,13 @@ var ShareHost = class {
|
|
|
2666
3396
|
const network = lan.length ? `http://${lan[0]}:${this.port}` : base;
|
|
2667
3397
|
return {
|
|
2668
3398
|
title: this.options.title(),
|
|
2669
|
-
lan: this.
|
|
3399
|
+
lan: this.on,
|
|
2670
3400
|
port: this.port,
|
|
3401
|
+
page: this.page,
|
|
2671
3402
|
addresses,
|
|
2672
|
-
local: `http://127.0.0.1:${this.port}
|
|
2673
|
-
links: Object.fromEntries(GUEST_ROLES.map((role) => [role, { on: links[role].on, url: `${base}
|
|
2674
|
-
...tailnet ? { tailnet: { address: tailnet, links: Object.fromEntries(GUEST_ROLES.map((role) => [role, { on: links[role].on, url: `http://${tailnet}:${this.port}
|
|
3403
|
+
local: `http://127.0.0.1:${this.port}${this.page}`,
|
|
3404
|
+
links: Object.fromEntries(GUEST_ROLES.map((role) => [role, { on: links[role].on, url: `${base}${this.page}s/${links[role].token}`, networkUrl: `${network}${this.page}s/${links[role].token}` }])),
|
|
3405
|
+
...tailnet ? { tailnet: { address: tailnet, links: Object.fromEntries(GUEST_ROLES.map((role) => [role, { on: links[role].on, url: `http://${tailnet}:${this.port}${this.page}s/${links[role].token}` }])) } } : {},
|
|
2675
3406
|
guests: guests.filter((g) => this.sharing.valid(g) || links[g.role].token === g.link).map((g) => ({ id: g.id, name: g.name, role: g.role, joined: g.joined, ...g.seen ? { seen: g.seen } : {}, online: online.has(g.id), active: this.sharing.valid(g) }))
|
|
2676
3407
|
};
|
|
2677
3408
|
}
|
|
@@ -2735,6 +3466,31 @@ var ShareHost = class {
|
|
|
2735
3466
|
return (zh ? ZH : EN)(this.options.noun);
|
|
2736
3467
|
}
|
|
2737
3468
|
};
|
|
3469
|
+
var ROOT_SHIM = `
|
|
3470
|
+
var P=location.protocol==='https:'?'wss:':'ws:';
|
|
3471
|
+
function under(u){try{var a=new URL(String(u),location.href);if(a.host===location.host&&/^(https?|wss?):$/.test(a.protocol)&&a.pathname.indexOf(BASE)!==0)a.pathname=BASE+a.pathname.slice(1);return a.href}catch(e){return u}}
|
|
3472
|
+
var F=window.fetch;
|
|
3473
|
+
window.fetch=function(i,o){if(typeof Request==='function'&&i instanceof Request){var r=under(i.url);return F.call(this,r===i.url?i:new Request(r,i),o)}return F.call(this,under(i&&i.href||i),o)};
|
|
3474
|
+
var X=XMLHttpRequest.prototype.open;
|
|
3475
|
+
XMLHttpRequest.prototype.open=function(m,u){var a=[].slice.call(arguments);a[1]=under(u);return X.apply(this,a)};
|
|
3476
|
+
var W=window.WebSocket;
|
|
3477
|
+
function WS(u,p){return p===undefined?new W(under(u)):new W(under(u),p)}
|
|
3478
|
+
WS.prototype=W.prototype;WS.CONNECTING=0;WS.OPEN=1;WS.CLOSING=2;WS.CLOSED=3;
|
|
3479
|
+
window.WebSocket=WS;
|
|
3480
|
+
if(navigator.sendBeacon){var B=navigator.sendBeacon;navigator.sendBeacon=function(u,d){return B.call(navigator,under(u),d)}}
|
|
3481
|
+
class S extends EventTarget{
|
|
3482
|
+
constructor(u){super();this.url=under(u);this.withCredentials=false;this.readyState=0;this.onopen=this.onmessage=this.onerror=null;this.l='';this.c()}
|
|
3483
|
+
c(){var t=this,a=new URL(t.url);a.protocol=P;if(t.l)a.searchParams.set('__lastEventId',t.l);var w=t.w=new W(a.href,'baoyu-sse'),b='';
|
|
3484
|
+
w.onopen=function(){t.readyState=1;t.f(new Event('open'))};
|
|
3485
|
+
w.onmessage=function(e){b+=e.data;var i;while((i=b.indexOf('\\n\\n'))>=0){t.b(b.slice(0,i));b=b.slice(i+2)}};
|
|
3486
|
+
w.onclose=function(e){if(t.readyState===2)return;if(e.code>=4000){t.readyState=2;t.f(new Event('error'));return}t.readyState=0;t.f(new Event('error'));t.r=setTimeout(function(){t.c()},1000)}}
|
|
3487
|
+
b(s){var n='message',d=[],h=false;s.split('\\n').forEach(function(x){if(!x||x[0]===':')return;var i=x.indexOf(':'),k=i<0?x:x.slice(0,i),v=i<0?'':x.slice(i+1).replace(/^ /,'');if(k==='event')n=v;else if(k==='data'){d.push(v);h=true}else if(k==='id')this.l=v}.bind(this));if(h)this.f(new MessageEvent(n,{data:d.join('\\n'),origin:location.origin,lastEventId:this.l}))}
|
|
3488
|
+
f(e){this.dispatchEvent(e);var h=this['on'+e.type];if(typeof h==='function')h.call(this,e)}
|
|
3489
|
+
close(){this.readyState=2;clearTimeout(this.r);if(this.w)this.w.close()}
|
|
3490
|
+
}
|
|
3491
|
+
S.CONNECTING=0;S.OPEN=1;S.CLOSED=2;
|
|
3492
|
+
window.EventSource=S;
|
|
3493
|
+
`;
|
|
2738
3494
|
var SECURE_CONTEXT_SHIM = "if(window.crypto&&!crypto.randomUUID)crypto.randomUUID=function(){var b=crypto.getRandomValues(new Uint8Array(16)),h='',i;b[6]=b[6]&15|64;b[8]=b[8]&63|128;for(i=0;i<16;i++){h+=(b[i]+256).toString(16).slice(1);if(i==3||i==5||i==7||i==9)h+='-'}return h};";
|
|
2739
3495
|
var GUEST_WATCH = "setInterval(function(){fetch('/__share/me',{cache:'no-store'}).then(function(r){if(r.status===403)return location.reload();return r.json().then(function(m){if(m&&m.role&&m.role!==window.__baoyuShare.role)location.reload()})},function(){})},5000);";
|
|
2740
3496
|
var PAGE_CSS = `
|
|
@@ -2958,7 +3714,7 @@ function noticeView(n, label, lostAt, gone) {
|
|
|
2958
3714
|
...n.lost.sha === null ? { deleted: true } : {}
|
|
2959
3715
|
};
|
|
2960
3716
|
}
|
|
2961
|
-
var
|
|
3717
|
+
var plain2 = (text) => ({ en: text, zh: text });
|
|
2962
3718
|
|
|
2963
3719
|
var PEER_ID = /^[A-Za-z0-9_-]{8,64}$/;
|
|
2964
3720
|
function agentWhere(main2, count, label) {
|
|
@@ -3209,7 +3965,7 @@ var OVERWRITE_WINDOW_MS = 6e4;
|
|
|
3209
3965
|
var filesOf = (listing, paths) => Object.fromEntries(paths.map((p) => [p, listing.files[p]?.sha256 ?? null]));
|
|
3210
3966
|
var authorOf = (who) => ({ id: who.user.id, name: who.user.name, ...who.by === "agent" ? { agent: true } : {} });
|
|
3211
3967
|
var CLIENT_ID = /^[A-Za-z0-9_-]{8,64}$/;
|
|
3212
|
-
var sha2562 = (bytes) =>
|
|
3968
|
+
var sha2562 = (bytes) => createHash5("sha256").update(bytes).digest("hex");
|
|
3213
3969
|
function contentTypeOf(file) {
|
|
3214
3970
|
const ext = file.slice(file.lastIndexOf(".") + 1).toLowerCase();
|
|
3215
3971
|
return MIME[ext] ?? "application/octet-stream";
|
|
@@ -3258,7 +4014,7 @@ async function collectFiles(root, limit) {
|
|
|
3258
4014
|
const seen = /* @__PURE__ */ new Set();
|
|
3259
4015
|
let truncated = false;
|
|
3260
4016
|
const visit = async (relDir, descend) => {
|
|
3261
|
-
const dir = relDir === "" ? root :
|
|
4017
|
+
const dir = relDir === "" ? root : path12.join(root, ...relDir.split("/"));
|
|
3262
4018
|
let entries;
|
|
3263
4019
|
try {
|
|
3264
4020
|
entries = await fsp4.readdir(dir, { withFileTypes: true });
|
|
@@ -3289,8 +4045,8 @@ async function collectFiles(root, limit) {
|
|
|
3289
4045
|
return { rels, truncated };
|
|
3290
4046
|
}
|
|
3291
4047
|
async function writeFileAtomic(file, bytes) {
|
|
3292
|
-
await fsp4.mkdir(
|
|
3293
|
-
const tmp =
|
|
4048
|
+
await fsp4.mkdir(path12.dirname(file), { recursive: true });
|
|
4049
|
+
const tmp = path12.join(path12.dirname(file), `.${path12.basename(file)}.${randomUUID()}.tmp`);
|
|
3294
4050
|
try {
|
|
3295
4051
|
await fsp4.writeFile(tmp, bytes);
|
|
3296
4052
|
await fsp4.rename(tmp, file);
|
|
@@ -3346,11 +4102,11 @@ var Store = class {
|
|
|
3346
4102
|
async diskPath(rel) {
|
|
3347
4103
|
const realRoot = await fsp4.realpath(this.root).catch(() => null);
|
|
3348
4104
|
if (realRoot === null) return null;
|
|
3349
|
-
const file =
|
|
3350
|
-
for (let dir =
|
|
4105
|
+
const file = path12.join(realRoot, ...rel.split("/"));
|
|
4106
|
+
for (let dir = path12.dirname(file); ; dir = path12.dirname(dir)) {
|
|
3351
4107
|
const real = await fsp4.realpath(dir).catch(() => null);
|
|
3352
4108
|
if (real === null) {
|
|
3353
|
-
if (
|
|
4109
|
+
if (path12.dirname(dir) === dir) return null;
|
|
3354
4110
|
continue;
|
|
3355
4111
|
}
|
|
3356
4112
|
if (real !== dir) return null;
|
|
@@ -3580,7 +4336,7 @@ var Store = class {
|
|
|
3580
4336
|
}
|
|
3581
4337
|
async findBlobFile(id) {
|
|
3582
4338
|
try {
|
|
3583
|
-
const names = await fsp4.readdir(
|
|
4339
|
+
const names = await fsp4.readdir(path12.join(this.root, LAYOUT.blobDir));
|
|
3584
4340
|
const name = names.find((n) => n.startsWith(`${id}.`) && !n.endsWith(".tmp"));
|
|
3585
4341
|
if (name === void 0) return null;
|
|
3586
4342
|
const file = await this.diskPath(`${LAYOUT.blobDir}/${name}`);
|
|
@@ -3635,13 +4391,13 @@ function watchArtifact(store, onChange, onError) {
|
|
|
3635
4391
|
const directories = /* @__PURE__ */ new Map();
|
|
3636
4392
|
const visit = (rel, descend) => {
|
|
3637
4393
|
if (directories.size >= maxDirectories) return;
|
|
3638
|
-
const dir = rel === "" ? store.root :
|
|
3639
|
-
if (directories.has(dir) || !
|
|
3640
|
-
const info =
|
|
4394
|
+
const dir = rel === "" ? store.root : path12.join(store.root, ...rel.split("/"));
|
|
4395
|
+
if (directories.has(dir) || !fs12.existsSync(dir)) return;
|
|
4396
|
+
const info = fs12.lstatSync(dir);
|
|
3641
4397
|
if (!info.isDirectory()) return;
|
|
3642
4398
|
directories.set(dir, `${info.dev}:${info.ino}`);
|
|
3643
4399
|
if (!descend) return;
|
|
3644
|
-
for (const entry of
|
|
4400
|
+
for (const entry of fs12.readdirSync(dir, { withFileTypes: true })) {
|
|
3645
4401
|
if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
|
|
3646
4402
|
const child = rel === "" ? entry.name : `${rel}/${entry.name}`;
|
|
3647
4403
|
if (LAYOUT.descendInto(child)) visit(child, true);
|
|
@@ -3658,19 +4414,19 @@ function watchArtifact(store, onChange, onError) {
|
|
|
3658
4414
|
if (watchers.has(dir)) continue;
|
|
3659
4415
|
let watcher;
|
|
3660
4416
|
try {
|
|
3661
|
-
watcher =
|
|
4417
|
+
watcher = fs12.watch(dir, (event, name) => {
|
|
3662
4418
|
if (stopped) return;
|
|
3663
4419
|
if (name) {
|
|
3664
|
-
const changed =
|
|
4420
|
+
const changed = path12.join(dir, String(name));
|
|
3665
4421
|
if (event === "rename" && watchers.has(changed)) {
|
|
3666
4422
|
for (const [watchedDir, item] of watchers) {
|
|
3667
|
-
if (watchedDir === changed || watchedDir.startsWith(changed +
|
|
4423
|
+
if (watchedDir === changed || watchedDir.startsWith(changed + path12.sep)) {
|
|
3668
4424
|
item.watcher.close();
|
|
3669
4425
|
watchers.delete(watchedDir);
|
|
3670
4426
|
}
|
|
3671
4427
|
}
|
|
3672
4428
|
}
|
|
3673
|
-
const rel =
|
|
4429
|
+
const rel = path12.relative(store.root, changed).split(path12.sep).join("/");
|
|
3674
4430
|
if (isIgnoredPath(rel) || !LAYOUT.watches(rel)) return;
|
|
3675
4431
|
}
|
|
3676
4432
|
schedule();
|
|
@@ -3706,7 +4462,7 @@ function watchArtifact(store, onChange, onError) {
|
|
|
3706
4462
|
const { rels } = await collectFiles(store.root, LAYOUT.maxFiles);
|
|
3707
4463
|
const parts = [];
|
|
3708
4464
|
for (const rel of rels) {
|
|
3709
|
-
const info = await fsp4.lstat(
|
|
4465
|
+
const info = await fsp4.lstat(path12.join(store.root, ...rel.split("/"))).catch(() => null);
|
|
3710
4466
|
parts.push(info ? `${rel}\0${info.size}\0${info.mtimeMs}\0${info.ino}` : `${rel}\0-`);
|
|
3711
4467
|
}
|
|
3712
4468
|
return parts.join("\n");
|
|
@@ -3735,23 +4491,23 @@ function watchArtifact(store, onChange, onError) {
|
|
|
3735
4491
|
};
|
|
3736
4492
|
}
|
|
3737
4493
|
var publishError = (response, status, code, message) => sendJson2(response, status, { code, message });
|
|
3738
|
-
async function
|
|
3739
|
-
|
|
3740
|
-
const
|
|
3741
|
-
const stamp = (line) => log(`[${(/* @__PURE__ */ new Date()).toISOString()}] ${line}`);
|
|
4494
|
+
async function mountSite(root, { log: stamp2, network, agentHook }) {
|
|
4495
|
+
const project = path12.join(root, "project");
|
|
4496
|
+
const editorPath = pagePathOf(KIND, root);
|
|
3742
4497
|
const projectDir = await fsp4.realpath(project);
|
|
3743
4498
|
const history = new History(root);
|
|
3744
|
-
const store = new Store(projectDir, history,
|
|
4499
|
+
const store = new Store(projectDir, history, stamp2);
|
|
3745
4500
|
const owner = localOwner();
|
|
3746
4501
|
const share = new ShareHost({
|
|
3747
4502
|
root,
|
|
4503
|
+
page: editorPath,
|
|
4504
|
+
network,
|
|
3748
4505
|
cookieName: guestCookieName(KIND, root),
|
|
3749
4506
|
owner,
|
|
3750
4507
|
noun: NOUNS[KIND] ?? { en: NOUN, zh: NOUN, zhThis: NOUN },
|
|
3751
4508
|
title: () => artifactTitle(root, projectDir),
|
|
3752
|
-
log:
|
|
3753
|
-
|
|
3754
|
-
uiFile: path10.join(EDITOR_DIR, "share-ui.js"),
|
|
4509
|
+
log: stamp2,
|
|
4510
|
+
uiFile: path12.join(EDITOR_DIR, "share-ui.js"),
|
|
3755
4511
|
// The History panel's address and what this person may do there; on a version's page (?at=n), which version.
|
|
3756
4512
|
globals: (viewer, query) => {
|
|
3757
4513
|
const at = Number(query.get("at") ?? "");
|
|
@@ -3760,7 +4516,7 @@ async function startServer({ root, port = DEFAULT_PORTS[KIND], host = "127.0.0.1
|
|
|
3760
4516
|
});
|
|
3761
4517
|
const comments = createCommentsService({
|
|
3762
4518
|
root,
|
|
3763
|
-
log:
|
|
4519
|
+
log: stamp2,
|
|
3764
4520
|
locate: commentLocation ? (input) => commentLocation(input, projectDir) : void 0,
|
|
3765
4521
|
agentHook,
|
|
3766
4522
|
userName: owner.name,
|
|
@@ -3770,8 +4526,6 @@ async function startServer({ root, port = DEFAULT_PORTS[KIND], host = "127.0.0.1
|
|
|
3770
4526
|
return viewer ? commentsViewer(viewer) : null;
|
|
3771
4527
|
}
|
|
3772
4528
|
});
|
|
3773
|
-
const grant = randomBytes2(24).toString("base64url");
|
|
3774
|
-
const editorPath = `/${KIND}/${grant}/`;
|
|
3775
4529
|
const boot = randomBytes2(6).toString("hex");
|
|
3776
4530
|
const clients = /* @__PURE__ */ new Map();
|
|
3777
4531
|
const presence = new PresenceHub();
|
|
@@ -3799,7 +4553,7 @@ data: ${JSON.stringify(event)}
|
|
|
3799
4553
|
}
|
|
3800
4554
|
return (p) => {
|
|
3801
4555
|
const rel = p.replace(/^project\//, "");
|
|
3802
|
-
return named?.(rel) ??
|
|
4556
|
+
return named?.(rel) ?? plain2(rel);
|
|
3803
4557
|
};
|
|
3804
4558
|
};
|
|
3805
4559
|
const agentWorked = (paths) => {
|
|
@@ -3830,16 +4584,16 @@ data: ${JSON.stringify(event)}
|
|
|
3830
4584
|
// A notice whose overwrite named the version its saver's edits started from: what was lost goes back by line into
|
|
3831
4585
|
// the file as it is now, the lost lines winning where both changed them; a deleted file gets all of it back.
|
|
3832
4586
|
recover: (_path, base, lost, now) => now === null ? lost : mergeTextBytes(base, lost, now),
|
|
3833
|
-
log:
|
|
4587
|
+
log: stamp2
|
|
3834
4588
|
});
|
|
3835
4589
|
const stopWatch = watchArtifact(
|
|
3836
4590
|
store,
|
|
3837
4591
|
(event) => {
|
|
3838
|
-
|
|
4592
|
+
stamp2(`changed on disk: ${event.paths.join(", ")}${clients.size ? "" : " (no page open)"}`);
|
|
3839
4593
|
changed({ ...event, by: authorOf(AGENT) });
|
|
3840
4594
|
agentWorked(event.paths);
|
|
3841
4595
|
},
|
|
3842
|
-
(error) =>
|
|
4596
|
+
(error) => stamp2(`watching failed: ${errorText(error)}`)
|
|
3843
4597
|
);
|
|
3844
4598
|
const events = async (request, response, viewer) => {
|
|
3845
4599
|
const given = new URL(request.url ?? "/", "http://local").searchParams.get("client") ?? "";
|
|
@@ -3902,7 +4656,7 @@ data: ${JSON.stringify(event)}
|
|
|
3902
4656
|
const parts = rest === "" ? ["index.html"] : rest.split("/");
|
|
3903
4657
|
const invalid = parts.some((p) => p === "" || p === "." || p === ".." || p.startsWith(".") || p.includes("\\")) || parts[0] === "node";
|
|
3904
4658
|
const realDist = invalid ? null : await fsp4.realpath(EDITOR_DIR).catch(() => null);
|
|
3905
|
-
const file = realDist === null ? null :
|
|
4659
|
+
const file = realDist === null ? null : path12.join(realDist, ...parts);
|
|
3906
4660
|
const real = file === null ? null : await fsp4.realpath(file).catch(() => null);
|
|
3907
4661
|
const info = real === null ? null : await fsp4.lstat(real).catch(() => null);
|
|
3908
4662
|
if (file === null || real !== file || !info?.isFile()) {
|
|
@@ -3913,7 +4667,7 @@ data: ${JSON.stringify(event)}
|
|
|
3913
4667
|
response.writeHead(404, { "Cache-Control": "no-store" }).end();
|
|
3914
4668
|
return;
|
|
3915
4669
|
}
|
|
3916
|
-
const ext =
|
|
4670
|
+
const ext = path12.extname(file).toLowerCase();
|
|
3917
4671
|
const type = STATIC_TYPES[ext] ?? "application/octet-stream";
|
|
3918
4672
|
if (ext === ".html") {
|
|
3919
4673
|
const key = parts.join("/");
|
|
@@ -3929,7 +4683,8 @@ data: ${JSON.stringify(event)}
|
|
|
3929
4683
|
"Content-Length": body.length,
|
|
3930
4684
|
"Cache-Control": "no-cache",
|
|
3931
4685
|
"X-Content-Type-Options": "nosniff",
|
|
3932
|
-
|
|
4686
|
+
// A root-relative <link> or <a href> finds its way to this artifact by the page's address (hub.ts).
|
|
4687
|
+
"Referrer-Policy": "same-origin",
|
|
3933
4688
|
"Content-Security-Policy": page.csp
|
|
3934
4689
|
});
|
|
3935
4690
|
response.end(request.method === "HEAD" ? void 0 : body);
|
|
@@ -3968,7 +4723,7 @@ data: ${JSON.stringify(event)}
|
|
|
3968
4723
|
const outcome = await store.publish(files, whoOf(viewer), over, merged, CLIENT_ID.test(page) ? page : "");
|
|
3969
4724
|
const names = Object.keys(files).map((p) => p.replace(/^project\//, ""));
|
|
3970
4725
|
const made = outcome.version ? ` as version ${outcome.version.n}` : "";
|
|
3971
|
-
|
|
4726
|
+
stamp2(`saved from the page by ${viewer.user.name} (${outcome.status}${outcome.body.code ? ` ${outcome.body.code}` : ""})${made}: ${names.join(", ")}`);
|
|
3972
4727
|
sendJson2(response, outcome.status, outcome.body);
|
|
3973
4728
|
if (outcome.status === 200 && clients.size > 0) {
|
|
3974
4729
|
const from = String(request.headers["x-deck-client"] ?? "");
|
|
@@ -3989,7 +4744,7 @@ data: ${JSON.stringify(event)}
|
|
|
3989
4744
|
return;
|
|
3990
4745
|
}
|
|
3991
4746
|
const result = await store.putBlob(bytes, contentType);
|
|
3992
|
-
|
|
4747
|
+
stamp2(`uploaded from the page: ${result.url} (${contentType}, ${bytes.length} bytes)`);
|
|
3993
4748
|
sendJson2(response, 200, result);
|
|
3994
4749
|
};
|
|
3995
4750
|
const session = (viewer) => {
|
|
@@ -4102,6 +4857,24 @@ data: ${JSON.stringify(event)}
|
|
|
4102
4857
|
else await upload(request, response);
|
|
4103
4858
|
return;
|
|
4104
4859
|
}
|
|
4860
|
+
const dsSync = DESIGN_SYSTEMS ? DS_SYNC.exec(rest) : null;
|
|
4861
|
+
if (dsSync) {
|
|
4862
|
+
const folder = dsSync[1];
|
|
4863
|
+
const status = systemStatus(projectDir, root, INDEX_FILE, folder);
|
|
4864
|
+
if (method === "POST") {
|
|
4865
|
+
request.resume();
|
|
4866
|
+
if (!sameOrigin(request)) return publishError(response, 403, "not_writer", "cross-origin request");
|
|
4867
|
+
if (viewer.guest) return publishError(response, 403, "not_owner", "Only the owner of this computer can sync a design system from its source.");
|
|
4868
|
+
if (status === null) return sendJson2(response, 404, { error: "not_found" });
|
|
4869
|
+
const [result] = syncSystems(projectDir, root, INDEX_FILE, { namespace: folder });
|
|
4870
|
+
return sendJson2(response, 200, { copied: result.copied.length, ...systemStatus(projectDir, root, INDEX_FILE, folder), canSync: true });
|
|
4871
|
+
}
|
|
4872
|
+
if (method !== "GET" && method !== "HEAD") {
|
|
4873
|
+
response.writeHead(405, { Allow: "GET, HEAD, POST" }).end();
|
|
4874
|
+
return;
|
|
4875
|
+
}
|
|
4876
|
+
return status === null ? sendJson2(response, 404, { error: "not_found" }, { "Cache-Control": "no-store" }) : sendJson2(response, 200, { ...status, canSync: !viewer.guest }, { "Cache-Control": "no-store" });
|
|
4877
|
+
}
|
|
4105
4878
|
if (method !== "GET" && method !== "HEAD") {
|
|
4106
4879
|
response.writeHead(405, { Allow: "GET, HEAD" }).end();
|
|
4107
4880
|
return;
|
|
@@ -4122,7 +4895,7 @@ data: ${JSON.stringify(event)}
|
|
|
4122
4895
|
"Content-Length": body.length,
|
|
4123
4896
|
"Cache-Control": "no-store",
|
|
4124
4897
|
"X-Content-Type-Options": "nosniff",
|
|
4125
|
-
//
|
|
4898
|
+
// A README may link anywhere, and the page (no script, its own origin) asks for nothing by its address.
|
|
4126
4899
|
"Referrer-Policy": "no-referrer",
|
|
4127
4900
|
"Content-Security-Policy": DATA_CSP
|
|
4128
4901
|
});
|
|
@@ -4157,54 +4930,24 @@ data: ${JSON.stringify(event)}
|
|
|
4157
4930
|
if (!response.headersSent) publishError(response, 413, "too_large", "request body is too large");
|
|
4158
4931
|
return;
|
|
4159
4932
|
}
|
|
4160
|
-
|
|
4933
|
+
stamp2(`request failed: ${error instanceof Error ? error.stack ?? error.message : String(error)}`);
|
|
4161
4934
|
if (!response.headersSent) publishError(response, 500, "upstream_error", "internal error");
|
|
4162
4935
|
else response.destroy();
|
|
4163
4936
|
}
|
|
4164
4937
|
};
|
|
4165
4938
|
async function handle(request, response) {
|
|
4166
|
-
if (!share.hostAllowed(request)) {
|
|
4167
|
-
request.resume();
|
|
4168
|
-
response.writeHead(421, { "Content-Type": "text/plain; charset=utf-8", "Cache-Control": "no-store" }).end("Misdirected request");
|
|
4169
|
-
return;
|
|
4170
|
-
}
|
|
4171
|
-
const url2 = new URL(request.url ?? "/", "http://localhost");
|
|
4172
|
-
const pathname = url2.pathname;
|
|
4173
|
-
if (pathname === "/favicon.ico") {
|
|
4174
|
-
response.writeHead(204).end();
|
|
4175
|
-
return;
|
|
4176
|
-
}
|
|
4177
4939
|
if (await share.route(request, response)) return;
|
|
4178
4940
|
const viewer = share.viewerOf(request);
|
|
4179
4941
|
if (!viewer) return share.denied(request, response);
|
|
4180
|
-
if (pathname === "/" || pathname === "/index.html") {
|
|
4181
|
-
response.writeHead(302, { Location: editorPath, "Cache-Control": "no-store" }).end();
|
|
4182
|
-
return;
|
|
4183
|
-
}
|
|
4184
|
-
const rootBlob = BLOB_PATH.exec(pathname.slice(1));
|
|
4185
|
-
if (rootBlob) {
|
|
4186
|
-
void guard(response, () => serveBlob(request, response, rootBlob[1]));
|
|
4187
|
-
return;
|
|
4188
|
-
}
|
|
4189
|
-
const match = /^\/([a-z-]+)\/([^/]+)(\/.*)?$/.exec(pathname);
|
|
4190
|
-
if (!match || match[1] !== KIND || match[2] !== grant) {
|
|
4191
|
-
response.writeHead(404, { "Content-Type": "text/plain; charset=utf-8", "Cache-Control": "no-store" }).end("Not found");
|
|
4192
|
-
return;
|
|
4193
|
-
}
|
|
4194
|
-
if (match[3] === void 0) {
|
|
4195
|
-
response.writeHead(308, { Location: `${editorPath}${url2.search}`, "Cache-Control": "no-store" }).end();
|
|
4196
|
-
return;
|
|
4197
|
-
}
|
|
4198
4942
|
let rest;
|
|
4199
4943
|
try {
|
|
4200
|
-
rest = decodeURIComponent(
|
|
4944
|
+
rest = decodeURIComponent(new URL(request.url ?? "/", "http://localhost").pathname.slice(1));
|
|
4201
4945
|
} catch {
|
|
4202
4946
|
response.writeHead(400).end();
|
|
4203
4947
|
return;
|
|
4204
4948
|
}
|
|
4205
|
-
|
|
4949
|
+
return route(request, response, rest, viewer);
|
|
4206
4950
|
}
|
|
4207
|
-
const server2 = http.createServer((request, response) => void guard(response, () => handle(request, response)));
|
|
4208
4951
|
const close = () => {
|
|
4209
4952
|
stopWatch();
|
|
4210
4953
|
comments.close();
|
|
@@ -4212,32 +4955,27 @@ data: ${JSON.stringify(event)}
|
|
|
4212
4955
|
presence.close();
|
|
4213
4956
|
for (const client of clients.keys()) client.end();
|
|
4214
4957
|
clients.clear();
|
|
4215
|
-
server2.close();
|
|
4216
|
-
server2.closeAllConnections?.();
|
|
4217
4958
|
};
|
|
4218
|
-
|
|
4219
|
-
|
|
4220
|
-
|
|
4221
|
-
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
|
|
4959
|
+
return {
|
|
4960
|
+
root,
|
|
4961
|
+
page: editorPath,
|
|
4962
|
+
share,
|
|
4963
|
+
handle: (request, response) => guard(response, () => handle(request, response)),
|
|
4964
|
+
hasBlob: async (id) => await store.findBlobFile(id) !== null,
|
|
4965
|
+
close
|
|
4966
|
+
};
|
|
4226
4967
|
}
|
|
4227
4968
|
function artifactTitle(root, projectDir) {
|
|
4228
4969
|
try {
|
|
4229
|
-
const index = JSON.parse(
|
|
4970
|
+
const index = JSON.parse(fs12.readFileSync(path12.join(projectDir, INDEX_FILE), "utf8"));
|
|
4230
4971
|
for (const value of [index.title, index.name]) if (typeof value === "string" && value.trim() && value.trim() !== "project") return value.trim();
|
|
4231
4972
|
} catch {
|
|
4232
4973
|
}
|
|
4233
|
-
return
|
|
4974
|
+
return path12.basename(root);
|
|
4234
4975
|
}
|
|
4235
4976
|
var runningServer2 = (root) => runningServer(root, KIND, ROUTES.session);
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
const state = { pid: process.pid, url: running.url, port: running.port, root, kind: KIND, startedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
4239
|
-
recordServer(state, running.close);
|
|
4240
|
-
return { ...state, ...running.lan ? { lan: running.lan } : {} };
|
|
4977
|
+
function serveForeground(options) {
|
|
4978
|
+
return serveHub({ ...options, kind: KIND, script: SCRIPT, portGiven: options.portGiven === true, site: mountSite });
|
|
4241
4979
|
}
|
|
4242
4980
|
async function main(argv) {
|
|
4243
4981
|
const positional = [];
|
|
@@ -4246,23 +4984,23 @@ async function main(argv) {
|
|
|
4246
4984
|
const arg = argv[i];
|
|
4247
4985
|
if (!arg.startsWith("--")) positional.push(arg);
|
|
4248
4986
|
else if (arg.includes("=")) flags.set(arg.slice(2, arg.indexOf("=")), arg.slice(arg.indexOf("=") + 1));
|
|
4249
|
-
else if (arg === "--lan") flags.set(
|
|
4987
|
+
else if (arg === "--lan" || arg === "--background") flags.set(arg.slice(2), "true");
|
|
4250
4988
|
else flags.set(arg.slice(2), argv[++i] ?? "");
|
|
4251
4989
|
}
|
|
4252
4990
|
if (positional.length !== 1 || flags.has("help")) {
|
|
4253
4991
|
process.stderr.write(
|
|
4254
|
-
`Usage: node ${
|
|
4992
|
+
`Usage: node ${path12.basename(SCRIPT)} <root> [--port n] [--host h] [--lan] [--agent-hook "<command>"]
|
|
4255
4993
|
<root> holds project/${INDEX_FILE}.
|
|
4256
4994
|
`
|
|
4257
4995
|
);
|
|
4258
4996
|
return 2;
|
|
4259
4997
|
}
|
|
4260
|
-
let root =
|
|
4261
|
-
if (
|
|
4262
|
-
root =
|
|
4998
|
+
let root = path12.resolve(positional[0]);
|
|
4999
|
+
if (path12.basename(root) === "project" && fs12.existsSync(path12.join(root, INDEX_FILE)) && !fs12.existsSync(path12.join(root, "project"))) {
|
|
5000
|
+
root = path12.dirname(root);
|
|
4263
5001
|
}
|
|
4264
|
-
if (!
|
|
4265
|
-
process.stderr.write(`${JSON.stringify({ error: "NOT_FOUND", message: `${
|
|
5002
|
+
if (!fs12.statSync(path12.join(root, "project", INDEX_FILE), { throwIfNoEntry: false })?.isFile()) {
|
|
5003
|
+
process.stderr.write(`${JSON.stringify({ error: "NOT_FOUND", message: `${path12.join(root, "project", INDEX_FILE)} does not exist.` })}
|
|
4266
5004
|
`);
|
|
4267
5005
|
return 2;
|
|
4268
5006
|
}
|
|
@@ -4279,14 +5017,14 @@ async function main(argv) {
|
|
|
4279
5017
|
return 0;
|
|
4280
5018
|
}
|
|
4281
5019
|
const agentHook = flags.get("agent-hook") || void 0;
|
|
4282
|
-
const state = await serveForeground({ root, port, host: flags.get("host") ?? "127.0.0.1", agentHook, lan: flags.has("lan"), portGiven: flags.has("port") });
|
|
5020
|
+
const state = await serveForeground({ root, port, host: flags.get("host") ?? "127.0.0.1", agentHook, lan: flags.has("lan"), portGiven: flags.has("port"), background: flags.has("background") });
|
|
4283
5021
|
process.stdout.write(`${JSON.stringify({ url: state.url, root, pid: state.pid, ...state.lan ? { lan: state.lan } : {} }, null, 2)}
|
|
4284
5022
|
`);
|
|
4285
|
-
return -1;
|
|
5023
|
+
return state.joined && flags.has("background") ? 0 : -1;
|
|
4286
5024
|
}
|
|
4287
5025
|
function isMain(moduleUrl) {
|
|
4288
5026
|
try {
|
|
4289
|
-
return Boolean(process.argv[1]) && pathToFileURL(
|
|
5027
|
+
return Boolean(process.argv[1]) && pathToFileURL(fs12.realpathSync(process.argv[1])).href === moduleUrl;
|
|
4290
5028
|
} catch {
|
|
4291
5029
|
return false;
|
|
4292
5030
|
}
|
|
@@ -4303,7 +5041,7 @@ function createArtifactServer(kind, moduleUrl) {
|
|
|
4303
5041
|
kindLabels = kind.labels;
|
|
4304
5042
|
kindAgentContext = kind.agentContext;
|
|
4305
5043
|
SCRIPT = fileURLToPath(moduleUrl);
|
|
4306
|
-
EDITOR_DIR =
|
|
5044
|
+
EDITOR_DIR = path12.join(path12.dirname(SCRIPT), "editor");
|
|
4307
5045
|
const runIfMain = () => {
|
|
4308
5046
|
if (!isMain(moduleUrl)) return;
|
|
4309
5047
|
main(process.argv.slice(2)).then(
|
|
@@ -4318,7 +5056,7 @@ function createArtifactServer(kind, moduleUrl) {
|
|
|
4318
5056
|
}
|
|
4319
5057
|
);
|
|
4320
5058
|
};
|
|
4321
|
-
return {
|
|
5059
|
+
return { mountSite, runningServer: runningServer2, serveForeground, readState, runIfMain };
|
|
4322
5060
|
}
|
|
4323
5061
|
|
|
4324
5062
|
var KIND2 = "design";
|
|
@@ -4353,7 +5091,7 @@ function commentLocation2(input, projectDir) {
|
|
|
4353
5091
|
});
|
|
4354
5092
|
let boards = [];
|
|
4355
5093
|
try {
|
|
4356
|
-
const listed = JSON.parse(
|
|
5094
|
+
const listed = JSON.parse(fs13.readFileSync(path13.join(projectDir, INDEX_FILE2), "utf8")).boards;
|
|
4357
5095
|
if (listed && typeof listed === "object") boards = Object.keys(listed);
|
|
4358
5096
|
} catch {
|
|
4359
5097
|
}
|
|
@@ -4364,7 +5102,7 @@ function commentLocation2(input, projectDir) {
|
|
|
4364
5102
|
function labels(projectDir) {
|
|
4365
5103
|
const titles = /* @__PURE__ */ new Map();
|
|
4366
5104
|
try {
|
|
4367
|
-
const listed = JSON.parse(
|
|
5105
|
+
const listed = JSON.parse(fs13.readFileSync(path13.join(projectDir, INDEX_FILE2), "utf8")).boards;
|
|
4368
5106
|
if (listed && typeof listed === "object") {
|
|
4369
5107
|
for (const [file, board] of Object.entries(listed)) {
|
|
4370
5108
|
const title = board?.title;
|
|
@@ -4398,15 +5136,15 @@ var server = createArtifactServer(
|
|
|
4398
5136
|
},
|
|
4399
5137
|
import.meta.url
|
|
4400
5138
|
);
|
|
4401
|
-
var {
|
|
5139
|
+
var { mountSite: mountSite2, serveForeground: serveForeground2, runningServer: runningServer3, readState: readState2 } = server;
|
|
4402
5140
|
server.runIfMain();
|
|
4403
5141
|
export {
|
|
4404
5142
|
INDEX_FILE2 as INDEX_FILE,
|
|
4405
5143
|
KIND2 as KIND,
|
|
4406
5144
|
LAYOUT2 as LAYOUT,
|
|
4407
5145
|
NOUN2 as NOUN,
|
|
5146
|
+
mountSite2 as mountSite,
|
|
4408
5147
|
readState2 as readState,
|
|
4409
5148
|
runningServer3 as runningServer,
|
|
4410
|
-
serveForeground2 as serveForeground
|
|
4411
|
-
startServer2 as startServer
|
|
5149
|
+
serveForeground2 as serveForeground
|
|
4412
5150
|
};
|