@teammy/cli 0.0.9 → 0.0.10
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/README.md +4 -4
- package/dist/main.js +615 -256
- package/package.json +1 -1
package/dist/main.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// apps/client/main.ts
|
|
2
2
|
import { parseArgs as parseArgs4 } from "node:util";
|
|
3
|
-
import { resolve as
|
|
3
|
+
import { resolve as resolve9 } from "node:path";
|
|
4
4
|
|
|
5
5
|
// packages/protocol/project.ts
|
|
6
6
|
import { z as z3 } from "zod";
|
|
@@ -518,10 +518,10 @@ var HubClient = class {
|
|
|
518
518
|
};
|
|
519
519
|
|
|
520
520
|
// apps/client/sessions/command.ts
|
|
521
|
-
import { createHash as
|
|
521
|
+
import { createHash as createHash7 } from "node:crypto";
|
|
522
522
|
import { realpath as realpath4 } from "node:fs/promises";
|
|
523
|
-
import { homedir } from "node:os";
|
|
524
|
-
import { dirname as
|
|
523
|
+
import { homedir as homedir2 } from "node:os";
|
|
524
|
+
import { dirname as dirname5, join as join7, resolve as resolve4 } from "node:path";
|
|
525
525
|
import { parseArgs } from "node:util";
|
|
526
526
|
|
|
527
527
|
// packages/state/sqlite.ts
|
|
@@ -1513,6 +1513,28 @@ var sessionClientMigration = String.raw`
|
|
|
1513
1513
|
root TEXT NOT NULL, path TEXT NOT NULL, signature TEXT NOT NULL, PRIMARY KEY(root, path)
|
|
1514
1514
|
);
|
|
1515
1515
|
`;
|
|
1516
|
+
var sessionSourcesMigration = String.raw`
|
|
1517
|
+
CREATE TABLE session_hook_receipts (root TEXT NOT NULL, agent TEXT NOT NULL, received_at INTEGER NOT NULL, PRIMARY KEY(root,agent));
|
|
1518
|
+
CREATE TABLE session_publications (
|
|
1519
|
+
root TEXT NOT NULL REFERENCES connections(root), session_key TEXT NOT NULL,
|
|
1520
|
+
revision INTEGER NOT NULL DEFAULT 0, hash TEXT, selected_path TEXT,
|
|
1521
|
+
pending_json TEXT, pending_signature TEXT, pending_path TEXT,
|
|
1522
|
+
attempts INTEGER NOT NULL DEFAULT 0, retry_at INTEGER NOT NULL DEFAULT 0,
|
|
1523
|
+
PRIMARY KEY(root,session_key)
|
|
1524
|
+
);
|
|
1525
|
+
INSERT INTO session_publications(root,session_key,revision,hash,selected_path,pending_json,pending_signature,pending_path,attempts,retry_at)
|
|
1526
|
+
SELECT root,session_key,revision,hash,path,pending_json,pending_signature,path,attempts,retry_at FROM session_sources WHERE session_key IS NOT NULL;
|
|
1527
|
+
ALTER TABLE session_sources RENAME TO session_sources_old;
|
|
1528
|
+
DROP INDEX session_source_scheduling;
|
|
1529
|
+
CREATE TABLE session_sources (
|
|
1530
|
+
root TEXT NOT NULL REFERENCES connections(root), path TEXT NOT NULL, agent TEXT NOT NULL, project_dir TEXT NOT NULL,
|
|
1531
|
+
session_key TEXT, signature TEXT, checked_at INTEGER NOT NULL DEFAULT 0, verified_at INTEGER NOT NULL DEFAULT 0,
|
|
1532
|
+
last_error TEXT, adopt_existing INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(root,path)
|
|
1533
|
+
);
|
|
1534
|
+
INSERT INTO session_sources SELECT root,path,agent,project_dir,session_key,signature,checked_at,verified_at,last_error,adopt_existing FROM session_sources_old;
|
|
1535
|
+
DROP TABLE session_sources_old;
|
|
1536
|
+
CREATE INDEX session_source_scheduling ON session_sources(root,checked_at,path);
|
|
1537
|
+
`;
|
|
1516
1538
|
var SessionState = class {
|
|
1517
1539
|
constructor(state, root) {
|
|
1518
1540
|
this.state = state;
|
|
@@ -1534,23 +1556,27 @@ var SessionState = class {
|
|
|
1534
1556
|
}
|
|
1535
1557
|
sources(path, limit = 2147483647) {
|
|
1536
1558
|
return this.state.db.prepare(
|
|
1537
|
-
`SELECT
|
|
1559
|
+
`SELECT s.*, p.revision,p.hash,p.selected_path,p.pending_json,p.pending_signature,p.pending_path,p.attempts,p.retry_at
|
|
1560
|
+
FROM session_sources s LEFT JOIN session_publications p USING(root,session_key)
|
|
1561
|
+
WHERE s.root=? ${path === void 0 ? "" : "AND s.path=?"} ORDER BY s.checked_at, s.path LIMIT ?`
|
|
1538
1562
|
).all(this.root, ...path === void 0 ? [] : [path], limit).map((row) => ({
|
|
1539
1563
|
path: String(row.path),
|
|
1540
1564
|
agent: row.agent,
|
|
1541
1565
|
projectDir: String(row.project_dir),
|
|
1542
1566
|
key: row.session_key,
|
|
1543
|
-
revision: Number(row.revision),
|
|
1544
|
-
hash: row.hash,
|
|
1567
|
+
revision: Number(row.revision ?? 0),
|
|
1568
|
+
hash: row.hash ?? null,
|
|
1545
1569
|
signature: row.signature,
|
|
1546
1570
|
checkedAt: Number(row.checked_at),
|
|
1547
1571
|
verifiedAt: Number(row.verified_at),
|
|
1548
1572
|
lastError: row.last_error,
|
|
1549
1573
|
adoptExisting: Boolean(row.adopt_existing),
|
|
1550
1574
|
pending: row.pending_json ? SessionCommitSchema.parse(JSON.parse(String(row.pending_json))) : null,
|
|
1551
|
-
pendingSignature: row.pending_signature,
|
|
1552
|
-
|
|
1553
|
-
|
|
1575
|
+
pendingSignature: row.pending_signature ?? null,
|
|
1576
|
+
pendingPath: row.pending_path ?? null,
|
|
1577
|
+
selectedPath: row.selected_path ?? null,
|
|
1578
|
+
attempts: Number(row.attempts ?? 0),
|
|
1579
|
+
retryAt: Number(row.retry_at ?? 0)
|
|
1554
1580
|
}));
|
|
1555
1581
|
}
|
|
1556
1582
|
status() {
|
|
@@ -1560,32 +1586,61 @@ var SessionState = class {
|
|
|
1560
1586
|
}));
|
|
1561
1587
|
}
|
|
1562
1588
|
untrack(path) {
|
|
1563
|
-
this.state.db
|
|
1589
|
+
transaction(this.state.db, () => {
|
|
1590
|
+
const source = this.get(path);
|
|
1591
|
+
this.state.db.prepare("DELETE FROM session_sources WHERE root=? AND path=?").run(this.root, path);
|
|
1592
|
+
if (!source?.key) return;
|
|
1593
|
+
if (!this.state.db.prepare("SELECT 1 FROM session_sources WHERE root=? AND session_key=?").get(this.root, source.key)) {
|
|
1594
|
+
this.state.db.prepare("DELETE FROM session_publications WHERE root=? AND session_key=?").run(this.root, source.key);
|
|
1595
|
+
} else {
|
|
1596
|
+
this.state.db.prepare(
|
|
1597
|
+
"UPDATE session_publications SET selected_path=NULL WHERE root=? AND session_key=? AND selected_path=?"
|
|
1598
|
+
).run(this.root, source.key, path);
|
|
1599
|
+
this.state.db.prepare(
|
|
1600
|
+
"UPDATE session_publications SET pending_json=NULL,pending_signature=NULL,pending_path=NULL,attempts=0,retry_at=0 WHERE root=? AND session_key=? AND pending_path=?"
|
|
1601
|
+
).run(this.root, source.key, path);
|
|
1602
|
+
}
|
|
1603
|
+
});
|
|
1564
1604
|
}
|
|
1565
1605
|
checked(path, error = null) {
|
|
1566
|
-
this.state.db.prepare("UPDATE session_sources SET checked_at=?,
|
|
1606
|
+
this.state.db.prepare("UPDATE session_sources SET checked_at=?,last_error=? WHERE root=? AND path=?").run(Date.now(), error, this.root, path);
|
|
1567
1607
|
}
|
|
1568
1608
|
stage(path, request, signature2) {
|
|
1569
1609
|
this.state.db.prepare(
|
|
1570
|
-
|
|
1571
|
-
|
|
1610
|
+
`UPDATE session_publications SET pending_json=?,pending_signature=?,pending_path=?,attempts=0,retry_at=0
|
|
1611
|
+
WHERE root=? AND session_key=? AND pending_json IS NULL`
|
|
1612
|
+
).run(JSON.stringify(request), signature2, path, this.root, request.sessionKey);
|
|
1572
1613
|
}
|
|
1573
1614
|
acknowledge(path, revision, hash2, signature2) {
|
|
1615
|
+
const source = this.get(path);
|
|
1616
|
+
this.state.db.prepare(
|
|
1617
|
+
`UPDATE session_publications SET revision=?,hash=?,selected_path=?,pending_json=NULL,pending_signature=NULL,pending_path=NULL,attempts=0,retry_at=0 WHERE root=? AND session_key=?`
|
|
1618
|
+
).run(revision, hash2, path, this.root, source.key);
|
|
1619
|
+
this.observed(path, signature2);
|
|
1620
|
+
}
|
|
1621
|
+
observed(path, signature2) {
|
|
1574
1622
|
this.state.db.prepare(
|
|
1575
|
-
|
|
1576
|
-
).run(
|
|
1623
|
+
"UPDATE session_sources SET signature=?,verified_at=?,last_error=NULL,adopt_existing=0 WHERE root=? AND path=?"
|
|
1624
|
+
).run(signature2, Date.now(), this.root, path);
|
|
1577
1625
|
}
|
|
1578
1626
|
baseline(path, key, revision) {
|
|
1579
|
-
this.state.db.prepare(
|
|
1627
|
+
this.state.db.prepare(
|
|
1628
|
+
"INSERT OR IGNORE INTO session_publications(root,session_key,revision) VALUES (?,?,?)"
|
|
1629
|
+
).run(this.root, key, revision);
|
|
1630
|
+
this.state.db.prepare("UPDATE session_sources SET session_key=? WHERE root=? AND path=?").run(key, this.root, path);
|
|
1631
|
+
}
|
|
1632
|
+
publication(key) {
|
|
1633
|
+
return this.state.db.prepare("SELECT 1 FROM session_publications WHERE root=? AND session_key=?").get(this.root, key);
|
|
1580
1634
|
}
|
|
1581
1635
|
retry() {
|
|
1582
|
-
this.state.db.prepare("UPDATE
|
|
1636
|
+
this.state.db.prepare("UPDATE session_publications SET retry_at=0 WHERE root=?").run(this.root);
|
|
1583
1637
|
}
|
|
1584
1638
|
postpone(source, error, blocked) {
|
|
1585
1639
|
const at = blocked ? Number.MAX_SAFE_INTEGER : Date.now() + Math.min(9e5, 6e4 * 2 ** Math.min(source.attempts, 4));
|
|
1586
1640
|
this.state.db.prepare(
|
|
1587
|
-
"UPDATE
|
|
1588
|
-
).run(at,
|
|
1641
|
+
"UPDATE session_publications SET retry_at=?,attempts=attempts+1 WHERE root=? AND session_key=?"
|
|
1642
|
+
).run(at, this.root, source.key);
|
|
1643
|
+
this.checked(source.path, error);
|
|
1589
1644
|
}
|
|
1590
1645
|
discover(agent, directory, projectDir) {
|
|
1591
1646
|
this.state.db.prepare(
|
|
@@ -1593,6 +1648,14 @@ var SessionState = class {
|
|
|
1593
1648
|
).run(this.root, directory, agent, projectDir);
|
|
1594
1649
|
this.state.db.prepare("DELETE FROM session_discovery_cache WHERE root=?").run(this.root);
|
|
1595
1650
|
}
|
|
1651
|
+
receivedHook(agent) {
|
|
1652
|
+
this.state.db.prepare(
|
|
1653
|
+
"INSERT INTO session_hook_receipts VALUES (?,?,?) ON CONFLICT(root,agent) DO UPDATE SET received_at=excluded.received_at"
|
|
1654
|
+
).run(this.root, agent, Date.now());
|
|
1655
|
+
}
|
|
1656
|
+
hooks() {
|
|
1657
|
+
return this.state.db.prepare("SELECT agent,received_at FROM session_hook_receipts WHERE root=? ORDER BY agent").all(this.root);
|
|
1658
|
+
}
|
|
1596
1659
|
directories() {
|
|
1597
1660
|
return this.state.db.prepare("SELECT * FROM session_discovery WHERE root=? ORDER BY directory").all(this.root).map((row) => ({
|
|
1598
1661
|
directory: String(row.directory),
|
|
@@ -1624,7 +1687,8 @@ var ClientState = class extends FilePublicationState {
|
|
|
1624
1687
|
projectClientMigration,
|
|
1625
1688
|
controlClientMigration,
|
|
1626
1689
|
runtimeMigration,
|
|
1627
|
-
projectFilesClientMigration
|
|
1690
|
+
projectFilesClientMigration,
|
|
1691
|
+
sessionSourcesMigration
|
|
1628
1692
|
]);
|
|
1629
1693
|
super(db);
|
|
1630
1694
|
}
|
|
@@ -1687,11 +1751,14 @@ async function stateLocation(root, directory) {
|
|
|
1687
1751
|
return canonical;
|
|
1688
1752
|
}
|
|
1689
1753
|
|
|
1690
|
-
// apps/client/sessions/
|
|
1691
|
-
import {
|
|
1692
|
-
import {
|
|
1693
|
-
import {
|
|
1694
|
-
import {
|
|
1754
|
+
// apps/client/sessions/setup.ts
|
|
1755
|
+
import { execFile as execFile2 } from "node:child_process";
|
|
1756
|
+
import { promisify as promisify2 } from "node:util";
|
|
1757
|
+
import { access, mkdir as mkdir4, readFile, writeFile } from "node:fs/promises";
|
|
1758
|
+
import { constants as constants2 } from "node:fs";
|
|
1759
|
+
import { homedir } from "node:os";
|
|
1760
|
+
import { dirname as dirname4, join as join5, resolve as resolve3 } from "node:path";
|
|
1761
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
1695
1762
|
|
|
1696
1763
|
// apps/client/sessions/normalize.ts
|
|
1697
1764
|
function object(value) {
|
|
@@ -1702,91 +1769,104 @@ function textContent(value) {
|
|
|
1702
1769
|
if (!Array.isArray(value)) return "";
|
|
1703
1770
|
return value.flatMap((block) => {
|
|
1704
1771
|
const part = object(block);
|
|
1705
|
-
return ["text", "input_text", "output_text"].includes(String(part.type)) && typeof part.text === "string" ? [part.text] : [];
|
|
1772
|
+
return ["text", "Text", "input_text", "output_text"].includes(String(part.type)) && typeof part.text === "string" ? [part.text] : [];
|
|
1706
1773
|
}).join("\n");
|
|
1707
1774
|
}
|
|
1708
|
-
function
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
if (!line.trim()) continue;
|
|
1714
|
-
if (Buffer.byteLength(line) > 8 * 1024 * 1024) throw new Error("Session record exceeds 8 MiB.");
|
|
1715
|
-
let parsed;
|
|
1716
|
-
try {
|
|
1717
|
-
parsed = JSON.parse(line);
|
|
1718
|
-
} catch {
|
|
1719
|
-
if (index === lines.length - 1 && !text.endsWith("\n")) return { rows, incomplete: true };
|
|
1720
|
-
throw new Error(`Invalid session JSON at line ${index + 1}.`);
|
|
1721
|
-
}
|
|
1722
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
1723
|
-
throw new Error(`Invalid session record at line ${index + 1}.`);
|
|
1724
|
-
rows.push({ line: index + 1, value: object(parsed) });
|
|
1775
|
+
function parseRecord(line) {
|
|
1776
|
+
try {
|
|
1777
|
+
const value = JSON.parse(line);
|
|
1778
|
+
if (value && typeof value === "object" && !Array.isArray(value)) return object(value);
|
|
1779
|
+
} catch {
|
|
1725
1780
|
}
|
|
1726
|
-
return
|
|
1781
|
+
return void 0;
|
|
1727
1782
|
}
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1783
|
+
var JsonlNormalizer = class {
|
|
1784
|
+
constructor(agent, cursor) {
|
|
1785
|
+
this.agent = agent;
|
|
1786
|
+
if (agent === "cursor" && cursor) {
|
|
1787
|
+
this.nativeId = cursor.nativeId;
|
|
1788
|
+
this.cwd = cursor.cwd;
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
agent;
|
|
1792
|
+
nativeId;
|
|
1793
|
+
cwd;
|
|
1794
|
+
turn = "legacy";
|
|
1795
|
+
candidates = [];
|
|
1796
|
+
add(role, content, id, line, visible = false) {
|
|
1735
1797
|
if (role !== "user" && role !== "assistant") return;
|
|
1736
|
-
const
|
|
1737
|
-
if (
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
for (const { line, value: row } of rows) {
|
|
1744
|
-
if (agent === "claude-code") {
|
|
1745
|
-
if (row.isSidechain === true) continue;
|
|
1798
|
+
const text = textContent(content);
|
|
1799
|
+
if (text.trim())
|
|
1800
|
+
this.candidates.push({ role, text, id, line, group: `${this.turn}:${role}`, visible });
|
|
1801
|
+
}
|
|
1802
|
+
consume(row, line) {
|
|
1803
|
+
if (this.agent === "claude-code") {
|
|
1804
|
+
if (row.isSidechain === true) return;
|
|
1746
1805
|
if (row.sessionId) {
|
|
1747
|
-
if (nativeId && nativeId !== row.sessionId)
|
|
1806
|
+
if (this.nativeId && this.nativeId !== row.sessionId)
|
|
1748
1807
|
throw new Error("Mixed Claude session identities.");
|
|
1749
|
-
nativeId = row.sessionId;
|
|
1808
|
+
this.nativeId = row.sessionId;
|
|
1750
1809
|
}
|
|
1751
|
-
if (row.cwd && !cwd) cwd = row.cwd;
|
|
1752
|
-
if (row.type === "user" || row.type === "assistant") {
|
|
1810
|
+
if (row.cwd && !this.cwd) this.cwd = row.cwd;
|
|
1811
|
+
if ((row.type === "user" || row.type === "assistant") && row.isMeta !== true) {
|
|
1753
1812
|
const message2 = object(row.message);
|
|
1754
|
-
add(message2.role ?? row.type, message2.content, row.uuid, line);
|
|
1813
|
+
this.add(message2.role ?? row.type, message2.content, row.uuid, line);
|
|
1755
1814
|
}
|
|
1756
|
-
} else if (agent === "codex") {
|
|
1815
|
+
} else if (this.agent === "codex") {
|
|
1757
1816
|
const payload = object(row.payload);
|
|
1758
1817
|
if (row.type === "session_meta") {
|
|
1759
|
-
if (nativeId && nativeId !== payload.id)
|
|
1760
|
-
|
|
1761
|
-
|
|
1818
|
+
if (this.nativeId && this.nativeId !== payload.id)
|
|
1819
|
+
throw new Error("Mixed Codex session identities.");
|
|
1820
|
+
this.nativeId = payload.id;
|
|
1821
|
+
this.cwd = payload.cwd;
|
|
1762
1822
|
}
|
|
1763
|
-
if (
|
|
1764
|
-
|
|
1765
|
-
|
|
1823
|
+
if (typeof payload.turn_id === "string") this.turn = payload.turn_id;
|
|
1824
|
+
if (row.type === "event_msg" && payload.type === "item_completed") {
|
|
1825
|
+
const item = object(payload.item);
|
|
1826
|
+
if (item.type === "UserMessage" || item.type === "AgentMessage")
|
|
1827
|
+
this.add(
|
|
1828
|
+
item.type === "UserMessage" ? "user" : "assistant",
|
|
1829
|
+
item.content,
|
|
1830
|
+
item.id,
|
|
1831
|
+
line,
|
|
1832
|
+
true
|
|
1833
|
+
);
|
|
1834
|
+
}
|
|
1835
|
+
if (row.type === "response_item" && payload.type === "message" && payload.channel !== "analysis") {
|
|
1836
|
+
const kinds = object(payload.internal_chat_message_metadata_passthrough).content_item_kinds;
|
|
1837
|
+
if (payload.role === "user" && Array.isArray(kinds) && !kinds.some((k) => String(k).startsWith("user.")))
|
|
1838
|
+
return;
|
|
1839
|
+
this.add(payload.role, payload.content, payload.id, line);
|
|
1840
|
+
}
|
|
1841
|
+
} else if (this.agent === "cursor") {
|
|
1842
|
+
const message2 = object(row.message);
|
|
1843
|
+
if (row.isSidechain === true) return;
|
|
1844
|
+
this.add(row.role ?? message2.role, message2.content ?? row.content, row.id ?? row.uuid, line);
|
|
1845
|
+
} else if (this.agent === "pi") {
|
|
1766
1846
|
if (row.type === "session") {
|
|
1767
1847
|
if (row.version !== 3) throw new Error("Unsupported Pi session version (expected 3).");
|
|
1768
|
-
if (nativeId) throw new Error("Multiple Pi session headers.");
|
|
1769
|
-
nativeId = row.id;
|
|
1770
|
-
cwd = row.cwd;
|
|
1848
|
+
if (this.nativeId) throw new Error("Multiple Pi session headers.");
|
|
1849
|
+
this.nativeId = row.id;
|
|
1850
|
+
this.cwd = row.cwd;
|
|
1771
1851
|
}
|
|
1772
1852
|
if (row.type === "message") {
|
|
1773
1853
|
const message2 = object(row.message);
|
|
1774
|
-
add(message2.role, message2.content, row.id, line);
|
|
1854
|
+
this.add(message2.role, message2.content, row.id, line);
|
|
1775
1855
|
}
|
|
1776
1856
|
} else {
|
|
1777
1857
|
if (row.type === "session") {
|
|
1778
1858
|
if (row.version !== 0) throw new Error("Unsupported DSH session version (expected 0).");
|
|
1779
|
-
if (nativeId) throw new Error("Multiple DSH session headers.");
|
|
1859
|
+
if (this.nativeId) throw new Error("Multiple DSH session headers.");
|
|
1780
1860
|
if (Number(row.delegationDepth ?? 0) > 0)
|
|
1781
1861
|
throw new Error("DSH delegated sessions are not collected.");
|
|
1782
|
-
nativeId = row.id;
|
|
1783
|
-
cwd = row.cwd;
|
|
1862
|
+
this.nativeId = row.id;
|
|
1863
|
+
this.cwd = row.cwd;
|
|
1784
1864
|
}
|
|
1785
1865
|
if (row.type === "user/message" || row.type === "assistant/message") {
|
|
1786
1866
|
const data = object(row.data);
|
|
1787
1867
|
const message2 = row.type === "assistant/message" ? object(data.message) : data;
|
|
1788
|
-
if (object(message2.source).kind === "plugin")
|
|
1789
|
-
add(
|
|
1868
|
+
if (object(message2.source).kind === "plugin") return;
|
|
1869
|
+
this.add(
|
|
1790
1870
|
row.type === "user/message" ? "user" : "assistant",
|
|
1791
1871
|
message2.content,
|
|
1792
1872
|
`seq:${row.seq}`,
|
|
@@ -1795,35 +1875,205 @@ function normalizeJsonl(agent, text) {
|
|
|
1795
1875
|
}
|
|
1796
1876
|
}
|
|
1797
1877
|
}
|
|
1798
|
-
|
|
1799
|
-
|
|
1878
|
+
finish() {
|
|
1879
|
+
const visibleGroups = new Set(this.candidates.filter((m) => m.visible).map((m) => m.group));
|
|
1880
|
+
const ids = /* @__PURE__ */ new Map();
|
|
1881
|
+
const messages = this.candidates.filter((m) => m.visible || !visibleGroups.has(m.group)).map((m) => {
|
|
1882
|
+
const base = typeof m.id === "string" && m.id ? m.id : `line:${m.line}`;
|
|
1883
|
+
const count = (ids.get(base) ?? 0) + 1;
|
|
1884
|
+
ids.set(base, count);
|
|
1885
|
+
return { id: `${base}#${count}`, role: m.role, text: m.text, source: `line:${m.line}` };
|
|
1886
|
+
});
|
|
1887
|
+
return SessionSnapshotSchema.parse({
|
|
1800
1888
|
formatVersion: 1,
|
|
1801
1889
|
adapterVersion: 1,
|
|
1802
|
-
agent,
|
|
1803
|
-
nativeId,
|
|
1804
|
-
cwd,
|
|
1890
|
+
agent: this.agent,
|
|
1891
|
+
nativeId: this.nativeId,
|
|
1892
|
+
cwd: this.cwd,
|
|
1805
1893
|
messages
|
|
1806
|
-
})
|
|
1807
|
-
|
|
1808
|
-
|
|
1894
|
+
});
|
|
1895
|
+
}
|
|
1896
|
+
};
|
|
1897
|
+
|
|
1898
|
+
// apps/client/sessions/setup.ts
|
|
1899
|
+
var exec2 = promisify2(execFile2);
|
|
1900
|
+
var quote = (value) => "'" + value.replace(/'/g, "'\\''") + "'";
|
|
1901
|
+
async function exists(path, mode = constants2.F_OK) {
|
|
1902
|
+
try {
|
|
1903
|
+
await access(path, mode);
|
|
1904
|
+
return true;
|
|
1905
|
+
} catch {
|
|
1906
|
+
return false;
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
async function setupSessions(state, root, directory, options = {}) {
|
|
1910
|
+
const home = options.home ?? homedir(), env = options.env ?? process.env;
|
|
1911
|
+
const project = dirname4(root), sessions = new SessionState(state, root);
|
|
1912
|
+
const defaults = [
|
|
1913
|
+
{
|
|
1914
|
+
agent: "claude-code",
|
|
1915
|
+
executables: ["claude"],
|
|
1916
|
+
storage: env.CLAUDE_CONFIG_DIR ?? join5(home, ".claude"),
|
|
1917
|
+
directories: ["projects"],
|
|
1918
|
+
config: ".claude/settings.local.json",
|
|
1919
|
+
events: ["SessionStart", "UserPromptSubmit", "Stop", "SessionEnd"]
|
|
1920
|
+
},
|
|
1921
|
+
{
|
|
1922
|
+
agent: "codex",
|
|
1923
|
+
executables: ["codex"],
|
|
1924
|
+
storage: env.CODEX_HOME ?? join5(home, ".codex"),
|
|
1925
|
+
directories: ["sessions"],
|
|
1926
|
+
config: ".codex/hooks.json",
|
|
1927
|
+
events: ["SessionStart", "UserPromptSubmit", "Stop", "SessionEnd"]
|
|
1928
|
+
},
|
|
1929
|
+
{
|
|
1930
|
+
agent: "cursor",
|
|
1931
|
+
executables: ["agent", "cursor-agent"],
|
|
1932
|
+
storage: env.CURSOR_CONFIG_DIR ?? join5(home, ".cursor"),
|
|
1933
|
+
directories: ["chats", "projects"],
|
|
1934
|
+
config: ".cursor/hooks.json",
|
|
1935
|
+
events: ["sessionStart", "postToolUse", "stop", "sessionEnd"]
|
|
1936
|
+
},
|
|
1937
|
+
{
|
|
1938
|
+
agent: "pi",
|
|
1939
|
+
executables: ["pi"],
|
|
1940
|
+
storage: env.PI_CODING_AGENT_DIR ?? join5(home, ".pi", "agent"),
|
|
1941
|
+
directories: ["sessions"]
|
|
1942
|
+
}
|
|
1943
|
+
];
|
|
1944
|
+
const issues = [], configured = [];
|
|
1945
|
+
let gitRoot = project;
|
|
1946
|
+
try {
|
|
1947
|
+
gitRoot = (await exec2("git", ["-C", project, "rev-parse", "--show-toplevel"])).stdout.trim();
|
|
1948
|
+
} catch {
|
|
1949
|
+
}
|
|
1950
|
+
for (const adapter of defaults) {
|
|
1951
|
+
const installed = options.agents ? options.agents.includes(adapter.agent) : await exists(adapter.storage) || (await Promise.all(
|
|
1952
|
+
(env.PATH ?? "").split(":").flatMap(
|
|
1953
|
+
(dir) => adapter.executables.map((name2) => exists(join5(dir, name2), constants2.X_OK))
|
|
1954
|
+
)
|
|
1955
|
+
)).some(Boolean);
|
|
1956
|
+
if (!installed) continue;
|
|
1957
|
+
for (const sub of adapter.directories)
|
|
1958
|
+
sessions.discover(adapter.agent, resolve3(adapter.storage, sub), project);
|
|
1959
|
+
if (adapter.agent === "cursor")
|
|
1960
|
+
sessions.discover("cursor", join5(home, ".cursor", "projects"), project);
|
|
1961
|
+
configured.push(adapter.agent);
|
|
1962
|
+
if (!adapter.config) continue;
|
|
1963
|
+
const configRoot = adapter.agent === "cursor" ? gitRoot : project;
|
|
1964
|
+
const path = join5(configRoot, adapter.config);
|
|
1965
|
+
try {
|
|
1966
|
+
await mkdir4(dirname4(path), { recursive: true });
|
|
1967
|
+
let original = null;
|
|
1968
|
+
try {
|
|
1969
|
+
original = await readStable(configRoot, adapter.config);
|
|
1970
|
+
} catch (error) {
|
|
1971
|
+
if (!missing(error)) throw error;
|
|
1972
|
+
}
|
|
1973
|
+
const config = original ? JSON.parse(original.bytes.toString("utf8")) : {};
|
|
1974
|
+
if (!config || typeof config !== "object" || Array.isArray(config))
|
|
1975
|
+
throw new Error("Hook configuration must be a JSON object.");
|
|
1976
|
+
if (config.hooks !== void 0 && (!config.hooks || typeof config.hooks !== "object" || Array.isArray(config.hooks)))
|
|
1977
|
+
throw new Error("Invalid existing hooks object.");
|
|
1978
|
+
const hooks = object(config.hooks);
|
|
1979
|
+
const marker = `teammy-session-${rootKey(root)}`;
|
|
1980
|
+
const command = [
|
|
1981
|
+
process.execPath,
|
|
1982
|
+
options.entry ?? resolve3(process.argv[1]),
|
|
1983
|
+
"session",
|
|
1984
|
+
"hook",
|
|
1985
|
+
"--agent",
|
|
1986
|
+
adapter.agent,
|
|
1987
|
+
"--root",
|
|
1988
|
+
root,
|
|
1989
|
+
"--state-dir",
|
|
1990
|
+
directory,
|
|
1991
|
+
"--project-dir",
|
|
1992
|
+
project
|
|
1993
|
+
].map(quote).join(" ") + ` # ${marker}`;
|
|
1994
|
+
for (const event of adapter.events) {
|
|
1995
|
+
const entries = hooks[event] ?? [];
|
|
1996
|
+
if (!Array.isArray(entries)) throw new Error(`Invalid ${event} hooks.`);
|
|
1997
|
+
const retained = entries.flatMap((entry) => {
|
|
1998
|
+
const value = object(entry);
|
|
1999
|
+
if (adapter.agent === "cursor")
|
|
2000
|
+
return typeof value.command === "string" && value.command.includes(marker) ? [] : [entry];
|
|
2001
|
+
if (!Array.isArray(value.hooks)) return [entry];
|
|
2002
|
+
const nested = value.hooks.filter(
|
|
2003
|
+
(hook) => !(typeof object(hook).command === "string" && String(object(hook).command).includes(marker))
|
|
2004
|
+
);
|
|
2005
|
+
return nested.length ? [{ ...value, hooks: nested }] : [];
|
|
2006
|
+
});
|
|
2007
|
+
hooks[event] = [
|
|
2008
|
+
...retained,
|
|
2009
|
+
adapter.agent === "cursor" ? { command, timeout: 3 } : { hooks: [{ type: "command", command, timeout: 3 }] }
|
|
2010
|
+
];
|
|
2011
|
+
}
|
|
2012
|
+
config.hooks = hooks;
|
|
2013
|
+
if (adapter.agent === "cursor") config.version ??= 1;
|
|
2014
|
+
const bytes = Buffer.from(JSON.stringify(config, null, 2) + "\n");
|
|
2015
|
+
if (original?.bytes.equals(bytes)) continue;
|
|
2016
|
+
if (original) {
|
|
2017
|
+
const backupDir = join5(directory, "hook-backups", rootKey(root));
|
|
2018
|
+
await mkdir4(backupDir, { recursive: true, mode: 448 });
|
|
2019
|
+
const hash2 = createHash5("sha256").update(original.bytes).digest("hex");
|
|
2020
|
+
await writeFile(join5(backupDir, `${adapter.agent}-${hash2}.json`), original.bytes, {
|
|
2021
|
+
mode: 384
|
|
2022
|
+
});
|
|
2023
|
+
}
|
|
2024
|
+
await replaceFile(configRoot, adapter.config, bytes, original?.signature ?? null);
|
|
2025
|
+
try {
|
|
2026
|
+
const relativePath = path.slice(gitRoot.length + 1);
|
|
2027
|
+
const tracked = (await exec2("git", ["-C", gitRoot, "ls-files", "--", relativePath])).stdout;
|
|
2028
|
+
if (!tracked) {
|
|
2029
|
+
const exclude = (await exec2("git", [
|
|
2030
|
+
"-C",
|
|
2031
|
+
gitRoot,
|
|
2032
|
+
"rev-parse",
|
|
2033
|
+
"--path-format=absolute",
|
|
2034
|
+
"--git-path",
|
|
2035
|
+
"info/exclude"
|
|
2036
|
+
])).stdout.trim();
|
|
2037
|
+
const old = await readFile(exclude, "utf8").catch((e) => {
|
|
2038
|
+
if (missing(e)) return "";
|
|
2039
|
+
throw e;
|
|
2040
|
+
});
|
|
2041
|
+
const rule = "/" + relativePath.replace(/[\\*?[\]#! ]/g, "\\$&");
|
|
2042
|
+
if (!old.split(/\r?\n/).includes(rule)) {
|
|
2043
|
+
await mkdir4(dirname4(exclude), { recursive: true });
|
|
2044
|
+
await writeFile(exclude, old + (old && !old.endsWith("\n") ? "\n" : "") + rule + "\n");
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
} catch {
|
|
2048
|
+
}
|
|
2049
|
+
} catch (error) {
|
|
2050
|
+
issues.push(`${path}: ${error instanceof Error ? error.message : "Hook setup failed."}`);
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
return { configured, issues };
|
|
1809
2054
|
}
|
|
1810
2055
|
|
|
1811
2056
|
// apps/client/sessions/hooks.ts
|
|
2057
|
+
import { createHash as createHash6, randomUUID as randomUUID4 } from "node:crypto";
|
|
2058
|
+
import { mkdir as mkdir5, open as open2, opendir, rename as rename2, unlink as unlink2 } from "node:fs/promises";
|
|
2059
|
+
import { isAbsolute as isAbsolute3, join as join6 } from "node:path";
|
|
2060
|
+
import { z as z10 } from "zod";
|
|
1812
2061
|
var HintSchema = z10.object({
|
|
1813
2062
|
agent: AgentSchema,
|
|
1814
|
-
path: z10.string().min(1).max(4096).refine(isAbsolute3),
|
|
2063
|
+
path: z10.string().min(1).max(4096).refine(isAbsolute3).optional(),
|
|
2064
|
+
nativeId: z10.string().min(1).max(512).optional(),
|
|
1815
2065
|
projectDir: z10.string().min(1).max(4096).refine(isAbsolute3)
|
|
1816
2066
|
});
|
|
1817
2067
|
function inbox(directory, root) {
|
|
1818
|
-
return
|
|
2068
|
+
return join6(directory, "session-hints", createHash6("sha256").update(root).digest("hex"));
|
|
1819
2069
|
}
|
|
1820
2070
|
async function recordHint(directory, root, input) {
|
|
1821
2071
|
const hint = HintSchema.parse(input);
|
|
1822
2072
|
const targetDir = inbox(directory, root);
|
|
1823
|
-
await
|
|
1824
|
-
const target =
|
|
2073
|
+
await mkdir5(targetDir, { recursive: true, mode: 448 });
|
|
2074
|
+
const target = join6(
|
|
1825
2075
|
targetDir,
|
|
1826
|
-
`${
|
|
2076
|
+
`${createHash6("sha256").update(JSON.stringify(hint)).digest("hex")}.json`
|
|
1827
2077
|
);
|
|
1828
2078
|
const temporary = `${target}.${randomUUID4()}.tmp`;
|
|
1829
2079
|
const handle = await open2(temporary, "wx", 384);
|
|
@@ -1834,7 +2084,7 @@ async function recordHint(directory, root, input) {
|
|
|
1834
2084
|
}
|
|
1835
2085
|
await rename2(temporary, target);
|
|
1836
2086
|
}
|
|
1837
|
-
async function consumeHints(directory, root, sessions) {
|
|
2087
|
+
async function consumeHints(directory, root, sessions, refresh) {
|
|
1838
2088
|
const issues = [];
|
|
1839
2089
|
let dir;
|
|
1840
2090
|
try {
|
|
@@ -1847,7 +2097,7 @@ async function consumeHints(directory, root, sessions) {
|
|
|
1847
2097
|
for await (const entry of dir) {
|
|
1848
2098
|
if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
|
|
1849
2099
|
if (count++ >= 50) break;
|
|
1850
|
-
const path =
|
|
2100
|
+
const path = join6(inbox(directory, root), entry.name);
|
|
1851
2101
|
try {
|
|
1852
2102
|
const handle = await open2(path, "r");
|
|
1853
2103
|
let body;
|
|
@@ -1858,7 +2108,9 @@ async function consumeHints(directory, root, sessions) {
|
|
|
1858
2108
|
await handle.close();
|
|
1859
2109
|
}
|
|
1860
2110
|
const hint = HintSchema.parse(JSON.parse(body));
|
|
1861
|
-
sessions.
|
|
2111
|
+
sessions.receivedHook(hint.agent);
|
|
2112
|
+
if (hint.path) sessions.track(hint.agent, hint.path, hint.projectDir);
|
|
2113
|
+
if (hint.nativeId) await refresh?.();
|
|
1862
2114
|
await unlink2(path);
|
|
1863
2115
|
} catch (error) {
|
|
1864
2116
|
issues.push(
|
|
@@ -1876,7 +2128,7 @@ async function hookPayload() {
|
|
|
1876
2128
|
for await (const part of process.stdin) {
|
|
1877
2129
|
const bytes = Buffer.from(part);
|
|
1878
2130
|
size += bytes.length;
|
|
1879
|
-
if (size >
|
|
2131
|
+
if (size > 4 * 1024 * 1024) throw new Error("Hook payload too large.");
|
|
1880
2132
|
parts.push(bytes);
|
|
1881
2133
|
}
|
|
1882
2134
|
return object(JSON.parse(Buffer.concat(parts).toString("utf8")));
|
|
@@ -1888,6 +2140,7 @@ async function hookPayload() {
|
|
|
1888
2140
|
// apps/client/sessions/command.ts
|
|
1889
2141
|
var help = `Usage: teammy session COMMAND [options]
|
|
1890
2142
|
|
|
2143
|
+
setup Configure project hooks and native discovery (also done by connect)
|
|
1891
2144
|
track --agent AGENT --file PATH [--project-dir DIR] [--adopt-existing]
|
|
1892
2145
|
untrack --file PATH (does not delete server history)
|
|
1893
2146
|
discover --agent AGENT [--directory DIR] [--project-dir DIR]
|
|
@@ -1913,8 +2166,8 @@ async function runSessionCommand(args) {
|
|
|
1913
2166
|
const { values } = parseArgs({
|
|
1914
2167
|
args: args.slice(1),
|
|
1915
2168
|
options: {
|
|
1916
|
-
root: { type: "string", default:
|
|
1917
|
-
"state-dir": { type: "string", default:
|
|
2169
|
+
root: { type: "string", default: resolve4("workspaces") },
|
|
2170
|
+
"state-dir": { type: "string", default: join7(homedir2(), ".teammy", "client") },
|
|
1918
2171
|
agent: { type: "string" },
|
|
1919
2172
|
file: { type: "string" },
|
|
1920
2173
|
directory: { type: "string" },
|
|
@@ -1933,16 +2186,18 @@ async function runSessionCommand(args) {
|
|
|
1933
2186
|
process.stdout.write(help);
|
|
1934
2187
|
return 0;
|
|
1935
2188
|
}
|
|
1936
|
-
const root = await realpath4(
|
|
2189
|
+
const root = await realpath4(resolve4(values.root));
|
|
1937
2190
|
const directory = await stateLocation(root, values["state-dir"]);
|
|
1938
|
-
const projectDir = await realpath4(
|
|
2191
|
+
const projectDir = await realpath4(resolve4(values["project-dir"] ?? dirname5(root)));
|
|
1939
2192
|
if (command === "hook") {
|
|
1940
2193
|
const payload = values.file ? {} : await hookPayload();
|
|
1941
2194
|
const file = values.file ?? payload.transcript_path ?? payload.session_file ?? payload.sessionFile;
|
|
1942
|
-
|
|
2195
|
+
const nativeId = payload.session_id ?? payload.conversation_id;
|
|
2196
|
+
if (typeof file === "string" && file || typeof nativeId === "string")
|
|
1943
2197
|
await recordHint(directory, root, {
|
|
1944
2198
|
agent: AgentSchema.parse(values.agent),
|
|
1945
|
-
path:
|
|
2199
|
+
...typeof file === "string" && file ? { path: resolve4(file) } : {},
|
|
2200
|
+
...typeof nativeId === "string" ? { nativeId } : {},
|
|
1946
2201
|
projectDir
|
|
1947
2202
|
});
|
|
1948
2203
|
return 0;
|
|
@@ -1954,9 +2209,10 @@ async function runSessionCommand(args) {
|
|
|
1954
2209
|
const sessions = new SessionState(state, root);
|
|
1955
2210
|
const client = new HubClient(binding.serverUrl, 1e4, binding);
|
|
1956
2211
|
let output;
|
|
1957
|
-
if (command === "
|
|
2212
|
+
if (command === "setup") output = await setupSessions(state, root, directory);
|
|
2213
|
+
else if (command === "track" || command === "untrack") {
|
|
1958
2214
|
if (!values.file) throw new Error("--file is required.");
|
|
1959
|
-
const path =
|
|
2215
|
+
const path = resolve4(values.file);
|
|
1960
2216
|
if (command === "track")
|
|
1961
2217
|
sessions.track(
|
|
1962
2218
|
AgentSchema.parse(values.agent),
|
|
@@ -1969,27 +2225,34 @@ async function runSessionCommand(args) {
|
|
|
1969
2225
|
} else if (command === "discover" || command === "undiscover") {
|
|
1970
2226
|
if (command === "undiscover") {
|
|
1971
2227
|
if (!values.directory) throw new Error("--directory is required.");
|
|
1972
|
-
state.db.prepare("DELETE FROM session_discovery WHERE root=? AND directory=?").run(root, await realpath4(
|
|
2228
|
+
state.db.prepare("DELETE FROM session_discovery WHERE root=? AND directory=?").run(root, await realpath4(resolve4(values.directory)));
|
|
1973
2229
|
} else {
|
|
1974
2230
|
const agent = AgentSchema.parse(values.agent);
|
|
1975
2231
|
const defaults = {
|
|
1976
|
-
"claude-code":
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
2232
|
+
"claude-code": join7(
|
|
2233
|
+
process.env.CLAUDE_CONFIG_DIR ?? join7(homedir2(), ".claude"),
|
|
2234
|
+
"projects"
|
|
2235
|
+
),
|
|
2236
|
+
codex: join7(process.env.CODEX_HOME ?? join7(homedir2(), ".codex"), "sessions"),
|
|
2237
|
+
cursor: join7(
|
|
2238
|
+
process.env.CURSOR_CONFIG_DIR ?? join7(homedir2(), ".cursor"),
|
|
1980
2239
|
"chats",
|
|
1981
|
-
|
|
2240
|
+
createHash7("md5").update(projectDir).digest("hex")
|
|
1982
2241
|
),
|
|
1983
|
-
pi:
|
|
2242
|
+
pi: join7(process.env.PI_CODING_AGENT_DIR ?? join7(homedir2(), ".pi", "agent"), "sessions"),
|
|
1984
2243
|
dsh: void 0
|
|
1985
2244
|
};
|
|
1986
2245
|
const path = values.directory ?? defaults[agent];
|
|
1987
2246
|
if (!path) throw new Error("DSH requires its configured persistence --directory.");
|
|
1988
|
-
sessions.discover(agent, await realpath4(
|
|
2247
|
+
sessions.discover(agent, await realpath4(resolve4(path)), projectDir);
|
|
1989
2248
|
}
|
|
1990
2249
|
output = sessions.directories();
|
|
1991
2250
|
} else if (command === "sources")
|
|
1992
|
-
output = {
|
|
2251
|
+
output = {
|
|
2252
|
+
sources: sessions.status(),
|
|
2253
|
+
discovery: sessions.directories(),
|
|
2254
|
+
hooks: sessions.hooks()
|
|
2255
|
+
};
|
|
1993
2256
|
else {
|
|
1994
2257
|
const after = Number(values.after);
|
|
1995
2258
|
const limit = Number(values.limit);
|
|
@@ -2025,22 +2288,22 @@ import { setTimeout as setTimeout4 } from "node:timers/promises";
|
|
|
2025
2288
|
|
|
2026
2289
|
// apps/client/client-command.ts
|
|
2027
2290
|
import { parseArgs as parseArgs2 } from "node:util";
|
|
2028
|
-
import { join as
|
|
2029
|
-
import { homedir as
|
|
2291
|
+
import { join as join12, resolve as resolve6 } from "node:path";
|
|
2292
|
+
import { homedir as homedir4 } from "node:os";
|
|
2030
2293
|
import { createInterface as createInterface2 } from "node:readline/promises";
|
|
2031
2294
|
|
|
2032
2295
|
// apps/client/connection.ts
|
|
2033
|
-
import { readFile, mkdir as
|
|
2296
|
+
import { readFile as readFile2, mkdir as mkdir6 } from "node:fs/promises";
|
|
2034
2297
|
import { createInterface } from "node:readline/promises";
|
|
2035
2298
|
|
|
2036
2299
|
// apps/client/root-lock.ts
|
|
2037
|
-
import { createHash as
|
|
2038
|
-
import { join as
|
|
2300
|
+
import { createHash as createHash8 } from "node:crypto";
|
|
2301
|
+
import { join as join8 } from "node:path";
|
|
2039
2302
|
function acquireRootLock(root, stateDirectory) {
|
|
2040
2303
|
let lock;
|
|
2041
2304
|
try {
|
|
2042
2305
|
lock = openDatabase(
|
|
2043
|
-
|
|
2306
|
+
join8(stateDirectory, "locks", `${createHash8("sha256").update(root).digest("hex")}.sqlite3`),
|
|
2044
2307
|
[],
|
|
2045
2308
|
0
|
|
2046
2309
|
);
|
|
@@ -2055,9 +2318,9 @@ function acquireRootLock(root, stateDirectory) {
|
|
|
2055
2318
|
}
|
|
2056
2319
|
|
|
2057
2320
|
// apps/client/local-config.ts
|
|
2058
|
-
import { constants as
|
|
2059
|
-
import { open as open3, writeFile } from "node:fs/promises";
|
|
2060
|
-
import { join as
|
|
2321
|
+
import { constants as constants3 } from "node:fs";
|
|
2322
|
+
import { open as open3, writeFile as writeFile2 } from "node:fs/promises";
|
|
2323
|
+
import { join as join9 } from "node:path";
|
|
2061
2324
|
import { z as z11 } from "zod";
|
|
2062
2325
|
import ignore from "ignore";
|
|
2063
2326
|
var LOCAL_CONFIG_NAME = ".teammy-local.json";
|
|
@@ -2066,8 +2329,8 @@ var LocalConfigSchema = z11.object({
|
|
|
2066
2329
|
}).strict();
|
|
2067
2330
|
async function ensureLocalConfig(root) {
|
|
2068
2331
|
try {
|
|
2069
|
-
await
|
|
2070
|
-
|
|
2332
|
+
await writeFile2(
|
|
2333
|
+
join9(root, LOCAL_CONFIG_NAME),
|
|
2071
2334
|
JSON.stringify({ files: { ignore: [] } }, null, 2) + "\n",
|
|
2072
2335
|
{ flag: "wx", mode: 384 }
|
|
2073
2336
|
);
|
|
@@ -2079,8 +2342,8 @@ async function loadLocalRules(root) {
|
|
|
2079
2342
|
let rules = [];
|
|
2080
2343
|
try {
|
|
2081
2344
|
const file = await open3(
|
|
2082
|
-
|
|
2083
|
-
|
|
2345
|
+
join9(root, LOCAL_CONFIG_NAME),
|
|
2346
|
+
constants3.O_RDONLY | constants3.O_NOFOLLOW | constants3.O_NONBLOCK
|
|
2084
2347
|
);
|
|
2085
2348
|
try {
|
|
2086
2349
|
const before = await file.stat();
|
|
@@ -2136,7 +2399,7 @@ async function connectProject(state, root, directory, options) {
|
|
|
2136
2399
|
const client2 = new HubClient(existing.serverUrl, 1e4, existing);
|
|
2137
2400
|
await client2.members();
|
|
2138
2401
|
} else if (options.tokenFile) {
|
|
2139
|
-
const token = CredentialSchema.parse((await
|
|
2402
|
+
const token = CredentialSchema.parse((await readFile2(options.tokenFile, "utf8")).trim());
|
|
2140
2403
|
const deviceId = state.deviceFor(root);
|
|
2141
2404
|
const client2 = new HubClient(options.url, 1e4, { token, deviceId });
|
|
2142
2405
|
const binding2 = await client2.bind(deviceId, options.project ?? existing?.workspaceId);
|
|
@@ -2185,7 +2448,7 @@ async function connectProject(state, root, directory, options) {
|
|
|
2185
2448
|
}
|
|
2186
2449
|
const own = await metadata(root, binding.memberId);
|
|
2187
2450
|
if (own && !own.directory) throw new Error("Member path is not a directory.");
|
|
2188
|
-
if (!own) await
|
|
2451
|
+
if (!own) await mkdir6(await safePath(root, binding.memberId));
|
|
2189
2452
|
await ensureLocalConfig(root);
|
|
2190
2453
|
const { token: _, ...visible } = binding;
|
|
2191
2454
|
return { ...visible, configRevision: project.cached()?.config.configRevision ?? null };
|
|
@@ -2196,23 +2459,23 @@ async function connectProject(state, root, directory, options) {
|
|
|
2196
2459
|
|
|
2197
2460
|
// apps/client/runtime/ipc.ts
|
|
2198
2461
|
import { createConnection } from "node:net";
|
|
2199
|
-
import { mkdir as
|
|
2200
|
-
import { join as
|
|
2462
|
+
import { mkdir as mkdir7, lstat as lstat3, chmod } from "node:fs/promises";
|
|
2463
|
+
import { join as join10 } from "node:path";
|
|
2201
2464
|
import { z as z12 } from "zod";
|
|
2202
2465
|
var RequestSchema = z12.object({
|
|
2203
2466
|
method: z12.enum(["ping", "wake", "status", "once"]),
|
|
2204
2467
|
root: z12.string().max(4096).optional()
|
|
2205
2468
|
}).strict();
|
|
2206
2469
|
function socketPath(directory) {
|
|
2207
|
-
return
|
|
2470
|
+
return join10(
|
|
2208
2471
|
"/tmp",
|
|
2209
2472
|
`teammy-${process.getuid?.() ?? "user"}-${rootKey(directory)}`,
|
|
2210
2473
|
"control.sock"
|
|
2211
2474
|
);
|
|
2212
2475
|
}
|
|
2213
2476
|
async function prepareSocketDirectory(directory) {
|
|
2214
|
-
const path =
|
|
2215
|
-
await
|
|
2477
|
+
const path = join10(socketPath(directory), "..");
|
|
2478
|
+
await mkdir7(path, { recursive: true, mode: 448 });
|
|
2216
2479
|
const stat2 = await lstat3(path);
|
|
2217
2480
|
if (!stat2.isDirectory() || stat2.isSymbolicLink() || stat2.uid !== process.getuid?.())
|
|
2218
2481
|
throw new Error("Unsafe daemon socket directory.");
|
|
@@ -2220,7 +2483,7 @@ async function prepareSocketDirectory(directory) {
|
|
|
2220
2483
|
}
|
|
2221
2484
|
async function daemonRequest(directory, input, timeout = 15e3) {
|
|
2222
2485
|
RequestSchema.parse(input);
|
|
2223
|
-
return new Promise((
|
|
2486
|
+
return new Promise((resolve10, reject) => {
|
|
2224
2487
|
const socket = createConnection(socketPath(directory));
|
|
2225
2488
|
socket.setEncoding("utf8");
|
|
2226
2489
|
let body = "";
|
|
@@ -2230,7 +2493,7 @@ async function daemonRequest(directory, input, timeout = 15e3) {
|
|
|
2230
2493
|
done = true;
|
|
2231
2494
|
socket.destroy();
|
|
2232
2495
|
if (error) reject(error);
|
|
2233
|
-
else
|
|
2496
|
+
else resolve10(result2);
|
|
2234
2497
|
};
|
|
2235
2498
|
socket.setTimeout(
|
|
2236
2499
|
timeout,
|
|
@@ -2271,20 +2534,20 @@ async function runningDaemon(directory) {
|
|
|
2271
2534
|
}
|
|
2272
2535
|
|
|
2273
2536
|
// apps/client/runtime/service.ts
|
|
2274
|
-
import { execFile as
|
|
2275
|
-
import { promisify as
|
|
2276
|
-
import { homedir as
|
|
2277
|
-
import { dirname as
|
|
2278
|
-
import { mkdir as
|
|
2537
|
+
import { execFile as execFile3 } from "node:child_process";
|
|
2538
|
+
import { promisify as promisify3 } from "node:util";
|
|
2539
|
+
import { homedir as homedir3 } from "node:os";
|
|
2540
|
+
import { dirname as dirname6, join as join11, resolve as resolve5 } from "node:path";
|
|
2541
|
+
import { mkdir as mkdir8, readFile as readFile3, writeFile as writeFile3, rename as rename3 } from "node:fs/promises";
|
|
2279
2542
|
import { setTimeout as setTimeout3 } from "node:timers/promises";
|
|
2280
|
-
var execute =
|
|
2281
|
-
var
|
|
2543
|
+
var execute = promisify3(execFile3);
|
|
2544
|
+
var exec3 = (file, args, options = {}) => execute(file, args, { timeout: 45e3, ...options });
|
|
2282
2545
|
var xml = (s) => s.replace(
|
|
2283
2546
|
/[<>&"']/g,
|
|
2284
2547
|
(c) => ({ "<": "<", ">": ">", "&": "&", '"': """, "'": "'" })[c]
|
|
2285
2548
|
);
|
|
2286
2549
|
var unit = (s) => '"' + s.replace(/%/g, "%%").replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\$/g, () => "$$") + '"';
|
|
2287
|
-
function serviceDefinition(directory, executable, entry, platform = process.platform, userHome =
|
|
2550
|
+
function serviceDefinition(directory, executable, entry, platform = process.platform, userHome = homedir3()) {
|
|
2288
2551
|
for (const value of [directory, executable, entry])
|
|
2289
2552
|
if (/[\r\n\0]/.test(value)) throw new Error("Invalid service path.");
|
|
2290
2553
|
const id = `com.teammy.client.${rootKey(directory)}`;
|
|
@@ -2292,7 +2555,7 @@ function serviceDefinition(directory, executable, entry, platform = process.plat
|
|
|
2292
2555
|
if (platform === "darwin")
|
|
2293
2556
|
return {
|
|
2294
2557
|
id,
|
|
2295
|
-
path:
|
|
2558
|
+
path: join11(userHome, "Library", "LaunchAgents", id + ".plist"),
|
|
2296
2559
|
content: `<?xml version="1.0" encoding="UTF-8"?>
|
|
2297
2560
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
2298
2561
|
<plist version="1.0"><dict><key>Label</key><string>${id}</string><key>ProgramArguments</key><array>${args.map((a) => `<string>${xml(a)}</string>`).join("")}</array><key>RunAtLoad</key><true/><key>KeepAlive</key><true/><key>ThrottleInterval</key><integer>30</integer><key>WorkingDirectory</key><string>${xml(userHome)}</string></dict></plist>
|
|
@@ -2301,7 +2564,7 @@ function serviceDefinition(directory, executable, entry, platform = process.plat
|
|
|
2301
2564
|
if (platform === "linux")
|
|
2302
2565
|
return {
|
|
2303
2566
|
id: id + ".service",
|
|
2304
|
-
path:
|
|
2567
|
+
path: join11(userHome, ".config", "systemd", "user", id + ".service"),
|
|
2305
2568
|
content: `[Unit]
|
|
2306
2569
|
Description=Teammy background synchronization
|
|
2307
2570
|
[Service]
|
|
@@ -2323,10 +2586,10 @@ WantedBy=default.target
|
|
|
2323
2586
|
async function checkServiceSupport() {
|
|
2324
2587
|
try {
|
|
2325
2588
|
if (process.platform === "darwin")
|
|
2326
|
-
await
|
|
2589
|
+
await exec3("launchctl", ["print", `gui/${process.getuid()}`], {
|
|
2327
2590
|
maxBuffer: 4 * 1024 * 1024
|
|
2328
2591
|
});
|
|
2329
|
-
else if (process.platform === "linux") await
|
|
2592
|
+
else if (process.platform === "linux") await exec3("systemctl", ["--user", "show-environment"]);
|
|
2330
2593
|
else throw new Error("unsupported");
|
|
2331
2594
|
} catch {
|
|
2332
2595
|
throw new Error(
|
|
@@ -2338,7 +2601,7 @@ async function bootstrapLaunchAgent(path) {
|
|
|
2338
2601
|
const deadline = Date.now() + 1e4;
|
|
2339
2602
|
for (; ; ) {
|
|
2340
2603
|
try {
|
|
2341
|
-
await
|
|
2604
|
+
await exec3("launchctl", ["bootstrap", `gui/${process.getuid()}`, path]);
|
|
2342
2605
|
return;
|
|
2343
2606
|
} catch (error) {
|
|
2344
2607
|
if (error.code !== 5 || Date.now() >= deadline) throw error;
|
|
@@ -2347,40 +2610,40 @@ async function bootstrapLaunchAgent(path) {
|
|
|
2347
2610
|
}
|
|
2348
2611
|
}
|
|
2349
2612
|
async function ensureDaemon(directory, version) {
|
|
2350
|
-
const definition = serviceDefinition(directory, process.execPath,
|
|
2613
|
+
const definition = serviceDefinition(directory, process.execPath, resolve5(process.argv[1]));
|
|
2351
2614
|
let old = "";
|
|
2352
2615
|
try {
|
|
2353
|
-
old = await
|
|
2616
|
+
old = await readFile3(definition.path, "utf8");
|
|
2354
2617
|
} catch (e) {
|
|
2355
2618
|
if (e.code !== "ENOENT") throw e;
|
|
2356
2619
|
}
|
|
2357
2620
|
const alive = await runningDaemon(directory);
|
|
2358
2621
|
if (alive && old === definition.content && alive.version === version) return;
|
|
2359
2622
|
await checkServiceSupport();
|
|
2360
|
-
await
|
|
2623
|
+
await mkdir8(dirname6(definition.path), { recursive: true });
|
|
2361
2624
|
if (old !== definition.content) {
|
|
2362
2625
|
const temp = definition.path + `.${process.pid}.tmp`;
|
|
2363
|
-
await
|
|
2626
|
+
await writeFile3(temp, definition.content, { mode: 384 });
|
|
2364
2627
|
await rename3(temp, definition.path);
|
|
2365
2628
|
}
|
|
2366
2629
|
if (process.platform === "darwin") {
|
|
2367
2630
|
const domain = `gui/${process.getuid()}`, target = `${domain}/${definition.id}`;
|
|
2368
2631
|
let loaded = false;
|
|
2369
2632
|
try {
|
|
2370
|
-
await
|
|
2633
|
+
await exec3("launchctl", ["print", target]);
|
|
2371
2634
|
loaded = true;
|
|
2372
2635
|
} catch {
|
|
2373
2636
|
}
|
|
2374
2637
|
if (loaded && (old !== definition.content || alive?.version !== version)) {
|
|
2375
|
-
await
|
|
2638
|
+
await exec3("launchctl", ["bootout", target]);
|
|
2376
2639
|
loaded = false;
|
|
2377
2640
|
}
|
|
2378
2641
|
if (!loaded) await bootstrapLaunchAgent(definition.path);
|
|
2379
|
-
await
|
|
2642
|
+
await exec3("launchctl", ["kickstart", target]);
|
|
2380
2643
|
} else {
|
|
2381
|
-
await
|
|
2382
|
-
await
|
|
2383
|
-
await
|
|
2644
|
+
await exec3("systemctl", ["--user", "daemon-reload"]);
|
|
2645
|
+
await exec3("systemctl", ["--user", "enable", definition.id]);
|
|
2646
|
+
await exec3("systemctl", [
|
|
2384
2647
|
"--user",
|
|
2385
2648
|
old !== definition.content || alive?.version !== version ? "restart" : "start",
|
|
2386
2649
|
definition.id
|
|
@@ -2419,8 +2682,8 @@ function json(value) {
|
|
|
2419
2682
|
|
|
2420
2683
|
// apps/client/client-command.ts
|
|
2421
2684
|
var locationOptions = {
|
|
2422
|
-
root: { type: "string", default:
|
|
2423
|
-
"state-dir": { type: "string", default:
|
|
2685
|
+
root: { type: "string", default: resolve6("workspaces") },
|
|
2686
|
+
"state-dir": { type: "string", default: join12(homedir4(), ".teammy", "client") },
|
|
2424
2687
|
json: { type: "boolean" },
|
|
2425
2688
|
help: { type: "boolean", short: "h" }
|
|
2426
2689
|
};
|
|
@@ -2523,12 +2786,16 @@ async function runClientCommand(command, args, version) {
|
|
|
2523
2786
|
tokenFile: values["token-file"],
|
|
2524
2787
|
reconnect: values.reconnect
|
|
2525
2788
|
});
|
|
2789
|
+
const collection = await setupSessions(state, root, directory);
|
|
2790
|
+
for (const issue of collection.issues) log.write("sessions.setup.failed", root, issue);
|
|
2526
2791
|
completePreparation(state, root);
|
|
2527
2792
|
runtime.enable(root, !values["no-start"]);
|
|
2528
2793
|
log.write("workspace.connected", root);
|
|
2529
2794
|
if (!values["no-start"]) await wakeDaemon(directory, version);
|
|
2530
|
-
if (values.json) json({ ...connected, syncEnabled: !values["no-start"] });
|
|
2795
|
+
if (values.json) json({ ...connected, collection, syncEnabled: !values["no-start"] });
|
|
2531
2796
|
else result(values["no-start"] ? "\u9879\u76EE\u5DF2\u8FDE\u63A5" : "\u5DF2\u8FDE\u63A5\uFF0C\u540E\u53F0\u540C\u6B65\u5DF2\u542F\u52A8", root);
|
|
2797
|
+
if (collection.issues.length && !values.json)
|
|
2798
|
+
result("\u90E8\u5206\u4F1A\u8BDD Hook \u672A\u914D\u7F6E", "\u540E\u53F0\u53D1\u73B0\u4ECD\u53EF\u7528\uFF0C\u8BE6\u60C5\u89C1 teammy logs");
|
|
2532
2799
|
return 0;
|
|
2533
2800
|
} catch (e) {
|
|
2534
2801
|
log.write(`${command}.failed`, root, diagnostic(e));
|
|
@@ -2541,16 +2808,16 @@ async function runClientCommand(command, args, version) {
|
|
|
2541
2808
|
// apps/client/files/synchronizer.ts
|
|
2542
2809
|
import { randomUUID as randomUUID6 } from "node:crypto";
|
|
2543
2810
|
import { watch } from "node:fs";
|
|
2544
|
-
import { lstat as lstat4, mkdir as
|
|
2545
|
-
import { join as
|
|
2811
|
+
import { lstat as lstat4, mkdir as mkdir10 } from "node:fs/promises";
|
|
2812
|
+
import { join as join14, sep as sep4 } from "node:path";
|
|
2546
2813
|
import { performance } from "node:perf_hooks";
|
|
2547
2814
|
|
|
2548
2815
|
// packages/storage/local-blob-store.ts
|
|
2549
|
-
import { createHash as
|
|
2550
|
-
import { link, mkdir as
|
|
2551
|
-
import { dirname as
|
|
2816
|
+
import { createHash as createHash9, randomUUID as randomUUID5 } from "node:crypto";
|
|
2817
|
+
import { link, mkdir as mkdir9, open as open4, readFile as readFile4, unlink as unlink3 } from "node:fs/promises";
|
|
2818
|
+
import { dirname as dirname7, join as join13, resolve as resolve7 } from "node:path";
|
|
2552
2819
|
function hash(content) {
|
|
2553
|
-
return
|
|
2820
|
+
return createHash9("sha256").update(content).digest("hex");
|
|
2554
2821
|
}
|
|
2555
2822
|
function hasCode(error, code) {
|
|
2556
2823
|
return error instanceof Error && "code" in error && error.code === code;
|
|
@@ -2566,13 +2833,13 @@ var BlobIntegrityError = class extends Error {
|
|
|
2566
2833
|
var LocalBlobStore = class {
|
|
2567
2834
|
root;
|
|
2568
2835
|
constructor(root) {
|
|
2569
|
-
this.root =
|
|
2836
|
+
this.root = resolve7(root);
|
|
2570
2837
|
}
|
|
2571
2838
|
async put(content) {
|
|
2572
2839
|
const bytes = Buffer.from(content);
|
|
2573
2840
|
const sha256 = hash(bytes);
|
|
2574
2841
|
const target = this.pathFor(sha256);
|
|
2575
|
-
await
|
|
2842
|
+
await mkdir9(dirname7(target), { recursive: true, mode: 448 });
|
|
2576
2843
|
const temporary = `${target}.${randomUUID5()}.tmp`;
|
|
2577
2844
|
const file = await open4(temporary, "wx", 384);
|
|
2578
2845
|
try {
|
|
@@ -2589,7 +2856,7 @@ var LocalBlobStore = class {
|
|
|
2589
2856
|
await this.get(sha256);
|
|
2590
2857
|
}
|
|
2591
2858
|
if (process.platform !== "win32") {
|
|
2592
|
-
let directory =
|
|
2859
|
+
let directory = dirname7(target);
|
|
2593
2860
|
for (; ; ) {
|
|
2594
2861
|
const handle = await open4(directory, "r");
|
|
2595
2862
|
try {
|
|
@@ -2597,8 +2864,8 @@ var LocalBlobStore = class {
|
|
|
2597
2864
|
} finally {
|
|
2598
2865
|
await handle.close();
|
|
2599
2866
|
}
|
|
2600
|
-
if (directory ===
|
|
2601
|
-
directory =
|
|
2867
|
+
if (directory === dirname7(this.root)) break;
|
|
2868
|
+
directory = dirname7(directory);
|
|
2602
2869
|
}
|
|
2603
2870
|
}
|
|
2604
2871
|
} finally {
|
|
@@ -2610,7 +2877,7 @@ var LocalBlobStore = class {
|
|
|
2610
2877
|
const target = this.pathFor(sha256);
|
|
2611
2878
|
let content;
|
|
2612
2879
|
try {
|
|
2613
|
-
content = await
|
|
2880
|
+
content = await readFile4(target);
|
|
2614
2881
|
} catch (error) {
|
|
2615
2882
|
if (hasCode(error, "ENOENT")) return void 0;
|
|
2616
2883
|
throw error;
|
|
@@ -2620,7 +2887,7 @@ var LocalBlobStore = class {
|
|
|
2620
2887
|
}
|
|
2621
2888
|
pathFor(sha256) {
|
|
2622
2889
|
if (!/^[a-f0-9]{64}$/.test(sha256)) throw new Error("Invalid SHA-256 content identifier.");
|
|
2623
|
-
return
|
|
2890
|
+
return join13(this.root, "sha256", sha256.slice(0, 2), sha256);
|
|
2624
2891
|
}
|
|
2625
2892
|
};
|
|
2626
2893
|
|
|
@@ -2680,7 +2947,7 @@ var FileSynchronizer = class _FileSynchronizer {
|
|
|
2680
2947
|
if (!binding) throw new Error("Connect this root before synchronizing.");
|
|
2681
2948
|
this.binding = binding;
|
|
2682
2949
|
this.client = new HubClient(binding.serverUrl, 3e4, binding, this.abort.signal);
|
|
2683
|
-
this.blobs = new LocalBlobStore(
|
|
2950
|
+
this.blobs = new LocalBlobStore(join14(stateDirectory, "blobs"));
|
|
2684
2951
|
this.projectScope = options.scope === "project";
|
|
2685
2952
|
this.owner = this.projectScope ? PROJECT_FILE_OWNER : binding.memberId;
|
|
2686
2953
|
this.publication = new FilePublicationState(state.db, options.scope);
|
|
@@ -2690,7 +2957,7 @@ var FileSynchronizer = class _FileSynchronizer {
|
|
|
2690
2957
|
commitFiles: (batch) => this.projectScope ? this.client.commitSharedFiles(batch, this.sharedConfig.revision) : this.client.commitFiles(batch)
|
|
2691
2958
|
});
|
|
2692
2959
|
this.index = new SyncState(state, root, options.scope);
|
|
2693
|
-
this.ownRoot = this.projectScope ? root :
|
|
2960
|
+
this.ownRoot = this.projectScope ? root : join14(root, binding.memberId);
|
|
2694
2961
|
this.common = this.projectScope ? void 0 : new _FileSynchronizer(state, root, stateDirectory, { ...options, scope: "project" });
|
|
2695
2962
|
const started = performance.now();
|
|
2696
2963
|
const epoch = Date.now();
|
|
@@ -2889,7 +3156,7 @@ var FileSynchronizer = class _FileSynchronizer {
|
|
|
2889
3156
|
}
|
|
2890
3157
|
}
|
|
2891
3158
|
const ownerPath = this.projectScope ? this.root : await safePath(this.root, this.owner);
|
|
2892
|
-
await
|
|
3159
|
+
await mkdir10(ownerPath).catch((error) => {
|
|
2893
3160
|
if (error.code !== "EEXIST") throw error;
|
|
2894
3161
|
});
|
|
2895
3162
|
this.publication.initializePublication(this.root, manifest.revision, manifest.files);
|
|
@@ -3075,7 +3342,7 @@ var FileSynchronizer = class _FileSynchronizer {
|
|
|
3075
3342
|
}
|
|
3076
3343
|
const job = this.index.job(owner);
|
|
3077
3344
|
const ownerRoot = this.projectScope ? this.root : await safePath(this.root, owner);
|
|
3078
|
-
await
|
|
3345
|
+
await mkdir10(ownerRoot).catch((error) => {
|
|
3079
3346
|
if (error.code !== "EEXIST") throw error;
|
|
3080
3347
|
});
|
|
3081
3348
|
const ownerStat = await lstat4(ownerRoot);
|
|
@@ -3131,22 +3398,24 @@ function message(error) {
|
|
|
3131
3398
|
|
|
3132
3399
|
// apps/client/sessions/synchronizer.ts
|
|
3133
3400
|
import { randomUUID as randomUUID7 } from "node:crypto";
|
|
3134
|
-
import { join as
|
|
3401
|
+
import { join as join17 } from "node:path";
|
|
3135
3402
|
|
|
3136
3403
|
// packages/protocol/session-key.ts
|
|
3137
|
-
import { createHash as
|
|
3404
|
+
import { createHash as createHash10 } from "node:crypto";
|
|
3138
3405
|
function sessionKey(agent, nativeId) {
|
|
3139
|
-
return
|
|
3406
|
+
return createHash10("sha256").update(JSON.stringify([agent, nativeId])).digest("hex");
|
|
3140
3407
|
}
|
|
3141
3408
|
|
|
3142
3409
|
// apps/client/sessions/source.ts
|
|
3143
|
-
import {
|
|
3144
|
-
import {
|
|
3410
|
+
import { createReadStream, realpathSync } from "node:fs";
|
|
3411
|
+
import { stat, realpath as realpath5, readdir as readdir2 } from "node:fs/promises";
|
|
3412
|
+
import { basename as basename3, dirname as dirname9, relative as relative4, isAbsolute as isAbsolute4, sep as sep5, join as join15 } from "node:path";
|
|
3413
|
+
import { createInterface as createInterface3 } from "node:readline";
|
|
3145
3414
|
import * as zlib from "node:zlib";
|
|
3146
3415
|
|
|
3147
3416
|
// apps/client/sessions/cursor.ts
|
|
3148
|
-
import { createHash as
|
|
3149
|
-
import { basename as basename2, dirname as
|
|
3417
|
+
import { createHash as createHash11 } from "node:crypto";
|
|
3418
|
+
import { basename as basename2, dirname as dirname8, resolve as resolve8 } from "node:path";
|
|
3150
3419
|
import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
|
|
3151
3420
|
import { fileURLToPath } from "node:url";
|
|
3152
3421
|
function fields(bytes) {
|
|
@@ -3184,7 +3453,7 @@ function fields(bytes) {
|
|
|
3184
3453
|
}
|
|
3185
3454
|
return result2;
|
|
3186
3455
|
}
|
|
3187
|
-
function readCursor(path, projectDir) {
|
|
3456
|
+
function readCursor(path, projectDir, checkCwd) {
|
|
3188
3457
|
const db = new DatabaseSync2(path, { readOnly: true });
|
|
3189
3458
|
try {
|
|
3190
3459
|
db.exec("PRAGMA query_only=ON; PRAGMA busy_timeout=100; BEGIN");
|
|
@@ -3195,7 +3464,7 @@ function readCursor(path, projectDir) {
|
|
|
3195
3464
|
if (typeof hex !== "string" || !/^(?:[a-fA-F0-9]{2})+$/.test(hex))
|
|
3196
3465
|
throw new Error("Invalid Cursor metadata.");
|
|
3197
3466
|
const meta = object(JSON.parse(Buffer.from(hex, "hex").toString("utf8")));
|
|
3198
|
-
if (typeof meta.agentId !== "string" || meta.agentId !== basename2(
|
|
3467
|
+
if (typeof meta.agentId !== "string" || meta.agentId !== basename2(dirname8(path)) || meta.subagentInfo != null)
|
|
3199
3468
|
throw new Error("Invalid or delegated Cursor session identity.");
|
|
3200
3469
|
let budget = 64 * 1024 * 1024;
|
|
3201
3470
|
const blob = (id) => {
|
|
@@ -3205,13 +3474,13 @@ function readCursor(path, projectDir) {
|
|
|
3205
3474
|
throw new Error("Cursor history missing or exceeds 64 MiB.");
|
|
3206
3475
|
const bytes = Buffer.from(value);
|
|
3207
3476
|
budget -= bytes.length;
|
|
3208
|
-
if (
|
|
3477
|
+
if (createHash11("sha256").update(bytes).digest("hex") !== id)
|
|
3209
3478
|
throw new Error("Cursor blob failed integrity verification.");
|
|
3210
3479
|
return bytes;
|
|
3211
3480
|
};
|
|
3212
3481
|
const root = fields(blob(String(meta.latestRootBlobId)));
|
|
3213
|
-
const bucket = basename2(
|
|
3214
|
-
const md5 = (cwd2) =>
|
|
3482
|
+
const bucket = basename2(dirname8(dirname8(path)));
|
|
3483
|
+
const md5 = (cwd2) => createHash11("md5").update(resolve8(cwd2)).digest("hex");
|
|
3215
3484
|
let cwd = md5(projectDir) === bucket ? projectDir : void 0;
|
|
3216
3485
|
for (const uri of root.get(9) ?? []) {
|
|
3217
3486
|
try {
|
|
@@ -3224,6 +3493,7 @@ function readCursor(path, projectDir) {
|
|
|
3224
3493
|
}
|
|
3225
3494
|
}
|
|
3226
3495
|
if (!cwd) throw new Error("Cursor workspace cannot be verified against its storage bucket.");
|
|
3496
|
+
checkCwd?.(cwd);
|
|
3227
3497
|
const refs = [];
|
|
3228
3498
|
for (const archive of root.get(13) ?? root.get(11) ?? [])
|
|
3229
3499
|
refs.push(...fields(blob(archive.toString("hex"))).get(1) ?? []);
|
|
@@ -3246,7 +3516,7 @@ function readCursor(path, projectDir) {
|
|
|
3246
3516
|
if (start >= 0 && end > start) text = text.slice(start + 12, end).trim();
|
|
3247
3517
|
}
|
|
3248
3518
|
if (!text.trim()) continue;
|
|
3249
|
-
const id =
|
|
3519
|
+
const id = createHash11("sha256").update(JSON.stringify([message2.role, cursor.requestId ?? "", text])).digest("hex");
|
|
3250
3520
|
const count = (counts.get(id) ?? 0) + 1;
|
|
3251
3521
|
counts.set(id, count);
|
|
3252
3522
|
messages.push({
|
|
@@ -3270,9 +3540,14 @@ function readCursor(path, projectDir) {
|
|
|
3270
3540
|
}
|
|
3271
3541
|
|
|
3272
3542
|
// apps/client/sessions/source.ts
|
|
3273
|
-
|
|
3543
|
+
function within(project, cwd) {
|
|
3544
|
+
const rel = relative4(project, cwd);
|
|
3545
|
+
return rel !== ".." && !rel.startsWith(`..${sep5}`) && !isAbsolute4(rel);
|
|
3546
|
+
}
|
|
3547
|
+
var cursorEncoded = (cwd) => cwd.replace(/^[/\\]+/, "").replace(/[/\\]/g, "-");
|
|
3548
|
+
var cursorDatabase = (path) => basename3(path) === "store.db";
|
|
3274
3549
|
async function sourceSignature(path, agent) {
|
|
3275
|
-
const paths = agent === "cursor" ? [path, `${path}-wal`] : [path];
|
|
3550
|
+
const paths = agent === "cursor" && cursorDatabase(path) ? [path, `${path}-wal`] : [path];
|
|
3276
3551
|
const signatures = [];
|
|
3277
3552
|
for (const file of paths) {
|
|
3278
3553
|
try {
|
|
@@ -3293,60 +3568,109 @@ async function sourceSignature(path, agent) {
|
|
|
3293
3568
|
}
|
|
3294
3569
|
return JSON.stringify(signatures);
|
|
3295
3570
|
}
|
|
3571
|
+
async function cursorScope(path, project) {
|
|
3572
|
+
let container = dirname9(path);
|
|
3573
|
+
while (basename3(container) !== "agent-transcripts" && dirname9(container) !== container)
|
|
3574
|
+
container = dirname9(container);
|
|
3575
|
+
if (basename3(container) !== "agent-transcripts")
|
|
3576
|
+
throw new Error("Cursor transcript workspace cannot be verified.");
|
|
3577
|
+
const encoded = basename3(dirname9(container));
|
|
3578
|
+
const matches = [];
|
|
3579
|
+
const visit = async (directory) => {
|
|
3580
|
+
const prefix = cursorEncoded(directory);
|
|
3581
|
+
if (encoded === prefix) {
|
|
3582
|
+
matches.push(directory);
|
|
3583
|
+
return;
|
|
3584
|
+
}
|
|
3585
|
+
if (!encoded.startsWith(prefix + "-")) return;
|
|
3586
|
+
for (const entry of await readdir2(directory, { withFileTypes: true })) {
|
|
3587
|
+
if (entry.isDirectory() && !entry.isSymbolicLink()) await visit(join15(directory, entry.name));
|
|
3588
|
+
}
|
|
3589
|
+
};
|
|
3590
|
+
await visit(project);
|
|
3591
|
+
if (matches.length !== 1)
|
|
3592
|
+
throw new Error("Session cwd is outside the registered project or ambiguous.");
|
|
3593
|
+
return matches[0];
|
|
3594
|
+
}
|
|
3296
3595
|
async function readSource(agent, path, projectDir) {
|
|
3297
3596
|
const signature2 = await sourceSignature(path, agent);
|
|
3298
|
-
|
|
3299
|
-
if (agent === "cursor"
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3597
|
+
const project = await realpath5(projectDir);
|
|
3598
|
+
if (agent === "cursor" && cursorDatabase(path)) {
|
|
3599
|
+
const snapshot2 = readCursor(path, projectDir, (cwd) => {
|
|
3600
|
+
if (!within(project, realpathSync(cwd)))
|
|
3601
|
+
throw new Error("Session cwd is outside the registered project.");
|
|
3602
|
+
});
|
|
3603
|
+
return { snapshot: snapshot2, incomplete: false, skipped: 0, signature: signature2 };
|
|
3604
|
+
}
|
|
3605
|
+
const cursor = agent === "cursor" ? { nativeId: basename3(path, ".jsonl"), cwd: await cursorScope(path, projectDir) } : void 0;
|
|
3606
|
+
const normalizer = new JsonlNormalizer(agent, cursor);
|
|
3607
|
+
const stream = createReadStream(path, {
|
|
3608
|
+
end: Math.max(0, Number(JSON.parse(signature2)[0][2]) - 1)
|
|
3609
|
+
});
|
|
3610
|
+
let input = stream;
|
|
3611
|
+
if (agent === "dsh" && path.endsWith(".zstd")) {
|
|
3612
|
+
const decompress = zlib.createZstdDecompress;
|
|
3613
|
+
if (!decompress) {
|
|
3614
|
+
stream.destroy();
|
|
3615
|
+
throw new Error("Compressed DSH sessions require Node.js with zstd support (22.15+ or 24+).");
|
|
3317
3616
|
}
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3617
|
+
const decoder = decompress();
|
|
3618
|
+
stream.on("error", (e) => decoder.destroy(e));
|
|
3619
|
+
input = stream.pipe(decoder);
|
|
3620
|
+
}
|
|
3621
|
+
const lines = createInterface3({ input, crlfDelay: Infinity });
|
|
3622
|
+
const checkedCwds = /* @__PURE__ */ new Set();
|
|
3623
|
+
let line = 0, skipped = 0, lastBad = false;
|
|
3624
|
+
try {
|
|
3625
|
+
for await (const text of lines) {
|
|
3626
|
+
line++;
|
|
3627
|
+
if (!text.trim()) continue;
|
|
3628
|
+
const row = parseRecord(text);
|
|
3629
|
+
lastBad = !row;
|
|
3630
|
+
if (!row) {
|
|
3631
|
+
skipped++;
|
|
3632
|
+
continue;
|
|
3633
|
+
}
|
|
3634
|
+
const cwd = row.cwd ?? (row.type === "session_meta" ? object(row.payload).cwd : void 0);
|
|
3635
|
+
if (typeof cwd === "string" && !checkedCwds.has(cwd)) {
|
|
3636
|
+
const actual = await realpath5(cwd).catch((error) => {
|
|
3637
|
+
if (error.code === "ENOENT" && !within(project, cwd))
|
|
3638
|
+
throw new Error("Session cwd is outside the registered project.");
|
|
3639
|
+
throw error;
|
|
3640
|
+
});
|
|
3641
|
+
if (!within(project, actual))
|
|
3642
|
+
throw new Error("Session cwd is outside the registered project.");
|
|
3643
|
+
checkedCwds.add(cwd);
|
|
3644
|
+
}
|
|
3645
|
+
normalizer.consume(row, line);
|
|
3325
3646
|
}
|
|
3326
|
-
|
|
3647
|
+
} finally {
|
|
3648
|
+
lines.close();
|
|
3649
|
+
stream.destroy();
|
|
3650
|
+
if (input !== stream) input.destroy();
|
|
3327
3651
|
}
|
|
3328
|
-
|
|
3652
|
+
const before = JSON.parse(signature2)[0];
|
|
3653
|
+
const after = JSON.parse(await sourceSignature(path, agent))[0];
|
|
3654
|
+
if (before[0] !== after[0] || before[1] !== after[1] || Number(after[2]) < Number(before[2]) || before[2] === after[2] && before.join() !== after.join())
|
|
3329
3655
|
throw new Error("Session source changed while reading; retry next cycle.");
|
|
3330
|
-
const
|
|
3331
|
-
|
|
3332
|
-
const rel = relative4(project, cwd);
|
|
3333
|
-
if (rel === ".." || rel.startsWith(`..${sep5}`) || isAbsolute4(rel))
|
|
3656
|
+
const snapshot = normalizer.finish();
|
|
3657
|
+
if (!within(project, await realpath5(snapshot.cwd)))
|
|
3334
3658
|
throw new Error("Session cwd is outside the registered project.");
|
|
3335
|
-
return {
|
|
3659
|
+
return { snapshot, incomplete: lastBad, skipped, signature: signature2 };
|
|
3336
3660
|
}
|
|
3337
3661
|
|
|
3338
3662
|
// apps/client/sessions/discovery.ts
|
|
3339
3663
|
import { opendir as opendir2 } from "node:fs/promises";
|
|
3340
|
-
import { join as
|
|
3664
|
+
import { join as join16 } from "node:path";
|
|
3341
3665
|
async function* candidates(directory, agent) {
|
|
3342
3666
|
const queue = [directory];
|
|
3343
3667
|
while (queue.length) {
|
|
3344
3668
|
const current = queue.shift();
|
|
3345
3669
|
const dir = await opendir2(current);
|
|
3346
3670
|
for await (const entry of dir) {
|
|
3347
|
-
const path =
|
|
3671
|
+
const path = join16(current, entry.name);
|
|
3348
3672
|
if (entry.isDirectory()) queue.push(path);
|
|
3349
|
-
if (entry.isFile() && (agent === "cursor" ? entry.name === "store.db" : entry.name.endsWith(".jsonl") || agent === "dsh" && entry.name.endsWith(".jsonl.zstd")))
|
|
3673
|
+
if (entry.isFile() && (agent === "cursor" ? entry.name === "store.db" || entry.name.endsWith(".jsonl") : entry.name.endsWith(".jsonl") || agent === "dsh" && entry.name.endsWith(".jsonl.zstd")))
|
|
3350
3674
|
yield path;
|
|
3351
3675
|
else yield null;
|
|
3352
3676
|
}
|
|
@@ -3393,25 +3717,27 @@ var SessionDiscovery = class {
|
|
|
3393
3717
|
"SELECT 1 FROM session_discovery_cache WHERE root=? AND path=? AND signature=?"
|
|
3394
3718
|
).get(this.state.root, path, signature2))
|
|
3395
3719
|
continue;
|
|
3396
|
-
reads--;
|
|
3397
3720
|
try {
|
|
3398
3721
|
const result2 = await readSource(config.agent, path, config.projectDir);
|
|
3399
|
-
if (result2.
|
|
3722
|
+
if (!result2.snapshot.messages.length) continue;
|
|
3723
|
+
reads--;
|
|
3400
3724
|
this.state.track(config.agent, path, config.projectDir);
|
|
3401
3725
|
} catch (error) {
|
|
3402
3726
|
const message2 = error instanceof Error ? error.message : "Discovery failed.";
|
|
3403
3727
|
if (!message2.includes("outside the registered project"))
|
|
3404
3728
|
issues.push(`${path}: ${message2}`);
|
|
3405
|
-
if (
|
|
3729
|
+
if (error.code === "ENOENT" || /locked|busy|changed while reading/i.test(message2))
|
|
3730
|
+
continue;
|
|
3406
3731
|
}
|
|
3407
3732
|
db.prepare(
|
|
3408
3733
|
"INSERT INTO session_discovery_cache VALUES (?,?,?) ON CONFLICT(root,path) DO UPDATE SET signature=excluded.signature"
|
|
3409
3734
|
).run(this.state.root, path, signature2);
|
|
3410
3735
|
}
|
|
3411
3736
|
} catch (error) {
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3737
|
+
if (error.code !== "ENOENT")
|
|
3738
|
+
issues.push(
|
|
3739
|
+
`${config.directory}: ${error instanceof Error ? error.message : "Discovery failed."}`
|
|
3740
|
+
);
|
|
3415
3741
|
await scan.iterator.return(void 0);
|
|
3416
3742
|
scan.iterator = candidates(config.directory, config.agent);
|
|
3417
3743
|
scan.nextAt = Date.now() + 3e5;
|
|
@@ -3419,6 +3745,9 @@ var SessionDiscovery = class {
|
|
|
3419
3745
|
}
|
|
3420
3746
|
return issues;
|
|
3421
3747
|
}
|
|
3748
|
+
async refresh() {
|
|
3749
|
+
for (const scan of this.scans.values()) scan.nextAt = 0;
|
|
3750
|
+
}
|
|
3422
3751
|
async close() {
|
|
3423
3752
|
for (const scan of this.scans.values()) await scan.iterator.return(void 0);
|
|
3424
3753
|
}
|
|
@@ -3432,7 +3761,7 @@ var SessionSynchronizer = class {
|
|
|
3432
3761
|
const binding = state.binding(root);
|
|
3433
3762
|
if (!binding) throw new Error("Connect this root before collecting sessions.");
|
|
3434
3763
|
this.client = new HubClient(binding.serverUrl, 1e4, binding, signal);
|
|
3435
|
-
this.blobs = new LocalBlobStore(
|
|
3764
|
+
this.blobs = new LocalBlobStore(join17(directory, "blobs"));
|
|
3436
3765
|
this.index = new SessionState(state, root);
|
|
3437
3766
|
this.discovery = new SessionDiscovery(this.index);
|
|
3438
3767
|
}
|
|
@@ -3444,9 +3773,16 @@ var SessionSynchronizer = class {
|
|
|
3444
3773
|
discovery;
|
|
3445
3774
|
capable = false;
|
|
3446
3775
|
async cycle() {
|
|
3447
|
-
const report = { published: 0, checked: 0, issues: [] };
|
|
3448
|
-
report.issues.push(
|
|
3449
|
-
|
|
3776
|
+
const report = { published: 0, checked: 0, issues: [], warnings: [] };
|
|
3777
|
+
report.issues.push(
|
|
3778
|
+
...await consumeHints(
|
|
3779
|
+
this.directory,
|
|
3780
|
+
this.root,
|
|
3781
|
+
this.index,
|
|
3782
|
+
() => this.discovery.refresh()
|
|
3783
|
+
)
|
|
3784
|
+
);
|
|
3785
|
+
report.warnings.push(...await this.discovery.cycle());
|
|
3450
3786
|
const sources = this.index.sources(void 0, 200);
|
|
3451
3787
|
if (sources.length && !this.capable) {
|
|
3452
3788
|
const info = await this.client.serverInfo();
|
|
@@ -3458,12 +3794,14 @@ var SessionSynchronizer = class {
|
|
|
3458
3794
|
}
|
|
3459
3795
|
let reads = 5;
|
|
3460
3796
|
let uploads = 5;
|
|
3461
|
-
for (const
|
|
3797
|
+
for (const scheduled of sources) {
|
|
3798
|
+
let source = this.index.get(scheduled.path);
|
|
3462
3799
|
if (reads <= 0 || uploads <= 0 || report.checked >= 200) break;
|
|
3463
3800
|
report.checked++;
|
|
3464
3801
|
this.index.checked(source.path, source.lastError);
|
|
3465
3802
|
try {
|
|
3466
3803
|
if (source.pending) {
|
|
3804
|
+
if (source.pendingPath !== source.path) continue;
|
|
3467
3805
|
if (source.retryAt > Date.now()) {
|
|
3468
3806
|
if (source.lastError)
|
|
3469
3807
|
report.issues.push(`${source.path}: ${source.lastError} (retry deferred)`);
|
|
@@ -3481,12 +3819,24 @@ var SessionSynchronizer = class {
|
|
|
3481
3819
|
}
|
|
3482
3820
|
reads--;
|
|
3483
3821
|
const observed = await readSource(source.agent, source.path, source.projectDir);
|
|
3484
|
-
if (observed.incomplete)
|
|
3485
|
-
throw new Error("Final session record is incomplete; waiting for the next save.");
|
|
3486
3822
|
if (!observed.snapshot.messages.length) {
|
|
3487
3823
|
this.index.checked(source.path);
|
|
3488
3824
|
continue;
|
|
3489
3825
|
}
|
|
3826
|
+
if (observed.skipped)
|
|
3827
|
+
report.warnings.push(
|
|
3828
|
+
`${source.path}: skipped ${observed.skipped} unreadable record(s); retained complete dialogue.`
|
|
3829
|
+
);
|
|
3830
|
+
if (observed.skipped && source.hash) {
|
|
3831
|
+
const previous = await this.blobs.get(source.hash);
|
|
3832
|
+
if (previous && JSON.parse(Buffer.from(previous).toString("utf8")).messages.length > observed.snapshot.messages.length) {
|
|
3833
|
+
this.index.observed(source.path, observed.signature);
|
|
3834
|
+
report.warnings.push(
|
|
3835
|
+
`${source.path}: kept the previous snapshot because the damaged copy contains fewer messages.`
|
|
3836
|
+
);
|
|
3837
|
+
continue;
|
|
3838
|
+
}
|
|
3839
|
+
}
|
|
3490
3840
|
const key = sessionKey(source.agent, observed.snapshot.nativeId);
|
|
3491
3841
|
if (source.key && source.key !== key)
|
|
3492
3842
|
throw new Error(
|
|
@@ -3497,7 +3847,7 @@ var SessionSynchronizer = class {
|
|
|
3497
3847
|
throw new Error("Normalized session exceeds the 10 MiB snapshot limit.");
|
|
3498
3848
|
const blob = await this.blobs.put(bytes);
|
|
3499
3849
|
let revision = source.revision;
|
|
3500
|
-
if (!source.key) {
|
|
3850
|
+
if (!source.key && !this.index.publication(key)) {
|
|
3501
3851
|
try {
|
|
3502
3852
|
const owner = this.index.state.binding(this.root).memberId;
|
|
3503
3853
|
const current = await this.client.session(owner, key);
|
|
@@ -3510,7 +3860,14 @@ var SessionSynchronizer = class {
|
|
|
3510
3860
|
if (!(error instanceof HubRequestError && error.code === "SESSION_NOT_FOUND"))
|
|
3511
3861
|
throw error;
|
|
3512
3862
|
}
|
|
3513
|
-
|
|
3863
|
+
}
|
|
3864
|
+
this.index.baseline(source.path, key, revision);
|
|
3865
|
+
source = this.index.get(source.path);
|
|
3866
|
+
revision = source.revision;
|
|
3867
|
+
if (source.pending) continue;
|
|
3868
|
+
if (source.agent === "cursor" && source.selectedPath && cursorDatabase(source.selectedPath) && !cursorDatabase(source.path)) {
|
|
3869
|
+
this.index.observed(source.path, observed.signature);
|
|
3870
|
+
continue;
|
|
3514
3871
|
}
|
|
3515
3872
|
if (blob.sha256 === source.hash) {
|
|
3516
3873
|
this.index.acknowledge(source.path, revision, blob.sha256, observed.signature);
|
|
@@ -3670,7 +4027,7 @@ var SyncJob = class {
|
|
|
3670
4027
|
pendingPublication: false,
|
|
3671
4028
|
issues: []
|
|
3672
4029
|
};
|
|
3673
|
-
let sessions = { published: 0, checked: 0, issues: [] };
|
|
4030
|
+
let sessions = { published: 0, checked: 0, issues: [], warnings: [] };
|
|
3674
4031
|
try {
|
|
3675
4032
|
await verifyWorkspace(this.state, this.root);
|
|
3676
4033
|
if (!this.controlStarted) {
|
|
@@ -3680,6 +4037,8 @@ var SyncJob = class {
|
|
|
3680
4037
|
}
|
|
3681
4038
|
if (this.files) report = await this.files.cycle();
|
|
3682
4039
|
sessions = await this.sessions.cycle();
|
|
4040
|
+
for (const warning of sessions.warnings)
|
|
4041
|
+
this.log.write("sessions.partial", this.root, warning);
|
|
3683
4042
|
} catch (e) {
|
|
3684
4043
|
report.issues.push(diagnostic(e));
|
|
3685
4044
|
}
|
|
@@ -3730,7 +4089,7 @@ var SyncJob = class {
|
|
|
3730
4089
|
);
|
|
3731
4090
|
}
|
|
3732
4091
|
this.lastIssue = issueKey;
|
|
3733
|
-
this.nextAt = now + (
|
|
4092
|
+
this.nextAt = now + (report.issues.length || this.control.enabled && (this.control.status.configError || this.control.status.reportError) ? Math.min(9e5, Math.max(this.intervalMs, 1e4) * 2 ** Math.min(this.issueCount - 1, 5)) : pendingDownloads ? 250 : this.intervalMs);
|
|
3734
4093
|
this.runtime.save(this.root, {
|
|
3735
4094
|
state: this.stopped ? "stopped" : issue ? "waiting" : "idle",
|
|
3736
4095
|
checkedAt,
|
|
@@ -3996,8 +4355,8 @@ async function runDaemon(directory, version) {
|
|
|
3996
4355
|
let timer;
|
|
3997
4356
|
let resolveStop = () => {
|
|
3998
4357
|
};
|
|
3999
|
-
const stopped = new Promise((
|
|
4000
|
-
resolveStop =
|
|
4358
|
+
const stopped = new Promise((resolve10) => {
|
|
4359
|
+
resolveStop = resolve10;
|
|
4001
4360
|
});
|
|
4002
4361
|
const stop = () => {
|
|
4003
4362
|
closing = true;
|
|
@@ -4008,11 +4367,11 @@ async function runDaemon(directory, version) {
|
|
|
4008
4367
|
await unlink4(socketPath(directory)).catch((e) => {
|
|
4009
4368
|
if (e.code !== "ENOENT") throw e;
|
|
4010
4369
|
});
|
|
4011
|
-
await new Promise((
|
|
4370
|
+
await new Promise((resolve10, reject) => {
|
|
4012
4371
|
server.once("error", reject);
|
|
4013
4372
|
server.listen(socketPath(directory), () => {
|
|
4014
4373
|
server.off("error", reject);
|
|
4015
|
-
|
|
4374
|
+
resolve10();
|
|
4016
4375
|
});
|
|
4017
4376
|
});
|
|
4018
4377
|
await chmod2(socketPath(directory), 384);
|
|
@@ -4035,7 +4394,7 @@ async function runDaemon(directory, version) {
|
|
|
4035
4394
|
await Promise.all([...jobs.values(), ...temporaryJobs].map((job) => job.close()));
|
|
4036
4395
|
await Promise.allSettled(oneShots.values());
|
|
4037
4396
|
for (const socket of sockets) socket.destroy();
|
|
4038
|
-
await new Promise((
|
|
4397
|
+
await new Promise((resolve10) => server.close(() => resolve10()));
|
|
4039
4398
|
await unlink4(socketPath(directory)).catch(() => {
|
|
4040
4399
|
});
|
|
4041
4400
|
log.write("daemon.stopped");
|
|
@@ -4094,7 +4453,7 @@ async function runCli(args, version) {
|
|
|
4094
4453
|
options: { "state-dir": { type: "string" } }
|
|
4095
4454
|
});
|
|
4096
4455
|
if (!values["state-dir"]) throw new Error("Missing daemon state directory.");
|
|
4097
|
-
const directory =
|
|
4456
|
+
const directory = resolve9(values["state-dir"]);
|
|
4098
4457
|
try {
|
|
4099
4458
|
await runDaemon(directory, version);
|
|
4100
4459
|
return 0;
|