@mattstack/rt-client 0.24.0 → 0.25.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/dist/index.d.ts +1 -0
- package/dist/index.js +60 -21
- package/dist/test-isolation.d.ts +7 -0
- package/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/test-isolation.ts +36 -0
- package/src/transport.ts +20 -0
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { rtCommand, DEFAULT_SOCK } from "./transport.ts";
|
|
2
2
|
export type { RtResponse, RtClientOptions } from "./transport.ts";
|
|
3
|
+
export { guardTestDaemonEnv } from "./test-isolation.ts";
|
|
3
4
|
export { readProjectMRs, readDiscussions, readMrsByBranch, readBranchCache, resolveForgeToken, listRuns, getRun, abandonRun, chatJoin, chatLeave, chatAck, chatClaim, chatRelease, chatPost, chatRead, chatRooms, chatWho, chatMark, chatMessages, chatSignIn, chatSignOut, chatAway, chatBack, chatBuddies, chatDm, chatArchive, chatDmOpen, eventsHead, eventsEmit, eventsWait, eventsList, agentStart, agentResume, agentGet, agentList, paneList, panePeek, paneSpawn, paneAccounts, paneDirectories, chatInvite, paneSend, paneFocus, reconcilerStatus, reconcilerClear, gateOpen, gateAsk, gateAnswer, gateWait, gateList, gatePark, gateClose, gateSubscribe, gateUnsubscribe, gateSubscriptions, herdStart, herdSpawn, herdAsk, herdMilestone, herdAnswer, herdReport, herdGates, herdStatus, herdList, herdResume, herdClose, herdAttend, herdWrapUp, herdStopHidden, bgEnsure, bgStatus, bgStop, bgRelease, } from "./client.ts";
|
|
4
5
|
export { COMMAND_NAMES, GATE_BY_PANE, gateOptionValue, gateOptionLabel } from "./commands.ts";
|
|
5
6
|
export { GATE_FORM_OPTION_CAP, gatePresentation } from "./gate-presentation.ts";
|
package/dist/index.js
CHANGED
|
@@ -2,14 +2,52 @@ import { createRequire } from "node:module";
|
|
|
2
2
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
3
3
|
|
|
4
4
|
// src/transport.ts
|
|
5
|
+
import { homedir as homedir2 } from "os";
|
|
6
|
+
import { join as join2 } from "path";
|
|
7
|
+
|
|
8
|
+
// src/test-isolation.ts
|
|
5
9
|
import { homedir } from "os";
|
|
6
10
|
import { join } from "path";
|
|
11
|
+
function parseForbidSocks(raw) {
|
|
12
|
+
const parsed = JSON.parse(raw);
|
|
13
|
+
if (!Array.isArray(parsed) || parsed.some((path) => typeof path !== "string")) {
|
|
14
|
+
throw new Error("rt-client: RT_TEST_FORBID_SOCKS must be a JSON string array of socket paths");
|
|
15
|
+
}
|
|
16
|
+
return parsed;
|
|
17
|
+
}
|
|
18
|
+
function guardTestDaemonEnv(env = process.env) {
|
|
19
|
+
const forbidden = new Set;
|
|
20
|
+
if (env.RT_TEST_FORBID_SOCKS) {
|
|
21
|
+
for (const path of parseForbidSocks(env.RT_TEST_FORBID_SOCKS))
|
|
22
|
+
forbidden.add(path);
|
|
23
|
+
}
|
|
24
|
+
if (env.RT_DAEMON_SOCK)
|
|
25
|
+
forbidden.add(env.RT_DAEMON_SOCK);
|
|
26
|
+
if (env.RT_APP_SOCKET)
|
|
27
|
+
forbidden.add(env.RT_APP_SOCKET);
|
|
28
|
+
forbidden.add(join(env.HOME ?? homedir(), ".mattstack", "rt", "rt.sock"));
|
|
29
|
+
delete env.RT_DAEMON_SOCK;
|
|
30
|
+
delete env.RT_APP_SOCKET;
|
|
31
|
+
env.RT_TEST_FORBID_SOCKS = JSON.stringify([...forbidden]);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// src/transport.ts
|
|
7
35
|
function defaultSock() {
|
|
8
|
-
return process.env.RT_DAEMON_SOCK ||
|
|
36
|
+
return process.env.RT_DAEMON_SOCK || join2(process.env.HOME ?? homedir2(), ".mattstack", "rt", "rt.sock");
|
|
9
37
|
}
|
|
10
38
|
var DEFAULT_SOCK = defaultSock();
|
|
39
|
+
function assertSockNotForbidden(cmd, sockPath) {
|
|
40
|
+
const raw = process.env.RT_TEST_FORBID_SOCKS;
|
|
41
|
+
if (!raw)
|
|
42
|
+
return;
|
|
43
|
+
const forbidden = parseForbidSocks(raw);
|
|
44
|
+
if (forbidden.includes(sockPath)) {
|
|
45
|
+
throw new Error(`rt-client: refusing "${cmd}" at forbidden socket ${sockPath}: RT_TEST_FORBID_SOCKS marks it as a live daemon socket, so this dispatch would have escaped test isolation`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
11
48
|
async function rtCommand(cmd, payload, opts = {}) {
|
|
12
49
|
const sockPath = opts.sockPath ?? defaultSock();
|
|
50
|
+
assertSockNotForbidden(cmd, sockPath);
|
|
13
51
|
try {
|
|
14
52
|
const res = await fetch(`http://localhost/${cmd}`, {
|
|
15
53
|
unix: sockPath,
|
|
@@ -691,10 +729,10 @@ async function daemonHealth(opts = {}) {
|
|
|
691
729
|
}
|
|
692
730
|
// src/repos.ts
|
|
693
731
|
import { existsSync, readFileSync } from "fs";
|
|
694
|
-
import { homedir as
|
|
695
|
-
import { dirname, join as
|
|
732
|
+
import { homedir as homedir3 } from "os";
|
|
733
|
+
import { dirname, join as join3 } from "path";
|
|
696
734
|
function defaultReposJsonPath() {
|
|
697
|
-
return
|
|
735
|
+
return join3(homedir3(), ".mattstack", "rt", "repos.json");
|
|
698
736
|
}
|
|
699
737
|
function loadBunSqliteDatabase() {
|
|
700
738
|
try {
|
|
@@ -746,7 +784,7 @@ function repoNameFromJson(repoPath, reposJsonPath) {
|
|
|
746
784
|
}
|
|
747
785
|
function repoNameForPath(repoPath, reposJsonPath) {
|
|
748
786
|
const jsonPath = reposJsonPath ?? defaultReposJsonPath();
|
|
749
|
-
const dbPath =
|
|
787
|
+
const dbPath = join3(dirname(jsonPath), "state.db");
|
|
750
788
|
const fromDb = repoNameFromStateDb(repoPath, dbPath);
|
|
751
789
|
if (fromDb !== null)
|
|
752
790
|
return fromDb;
|
|
@@ -826,33 +864,33 @@ function formatPaneRef(paneId, server) {
|
|
|
826
864
|
return BG_PREFIX + paneId;
|
|
827
865
|
}
|
|
828
866
|
// src/settings/resolve.ts
|
|
829
|
-
import { homedir as
|
|
830
|
-
import { join as
|
|
867
|
+
import { homedir as homedir5 } from "os";
|
|
868
|
+
import { join as join6 } from "path";
|
|
831
869
|
|
|
832
870
|
// src/settings/paths.ts
|
|
833
871
|
import { readFileSync as readFileSync2 } from "fs";
|
|
834
|
-
import { homedir as
|
|
835
|
-
import { join as
|
|
872
|
+
import { homedir as homedir4, hostname } from "os";
|
|
873
|
+
import { join as join4 } from "path";
|
|
836
874
|
function home() {
|
|
837
|
-
return process.env.HOME ??
|
|
875
|
+
return process.env.HOME ?? homedir4();
|
|
838
876
|
}
|
|
839
877
|
function userSettingsPath() {
|
|
840
|
-
return
|
|
878
|
+
return join4(home(), ".mattstack", "user", "settings.user.jsonc");
|
|
841
879
|
}
|
|
842
880
|
function teamSettingsPath(team) {
|
|
843
|
-
return
|
|
881
|
+
return join4(teamsDir(), team, "mattstack", "settings.team.jsonc");
|
|
844
882
|
}
|
|
845
883
|
function teamLocalPath(team) {
|
|
846
|
-
return
|
|
884
|
+
return join4(home(), ".mattstack", "rt", "teams", `${team}.json`);
|
|
847
885
|
}
|
|
848
886
|
function machineSettingsPath() {
|
|
849
|
-
return
|
|
887
|
+
return join4(home(), ".mattstack", "user", "local", machineKey(), "settings.local.jsonc");
|
|
850
888
|
}
|
|
851
889
|
function teamsDir() {
|
|
852
|
-
return
|
|
890
|
+
return join4(home(), ".mattstack", "teams");
|
|
853
891
|
}
|
|
854
892
|
function machineKey() {
|
|
855
|
-
const override =
|
|
893
|
+
const override = join4(home(), ".mattstack", "machine-key");
|
|
856
894
|
try {
|
|
857
895
|
const v = readFileSync2(override, "utf8").trim();
|
|
858
896
|
if (isSafeMachineKeySegment(v))
|
|
@@ -1591,7 +1629,7 @@ function findPathGuardViolation(value, guardFields) {
|
|
|
1591
1629
|
// src/settings/stores.ts
|
|
1592
1630
|
import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync3, statSync } from "fs";
|
|
1593
1631
|
import { parse } from "jsonc-parser";
|
|
1594
|
-
import { join as
|
|
1632
|
+
import { join as join5 } from "path";
|
|
1595
1633
|
var EMPTY_STORE = (file, exists) => ({
|
|
1596
1634
|
global: {},
|
|
1597
1635
|
repos: {},
|
|
@@ -1638,13 +1676,13 @@ function listTeams() {
|
|
|
1638
1676
|
const teams = [];
|
|
1639
1677
|
for (const entry of entries) {
|
|
1640
1678
|
try {
|
|
1641
|
-
const isDir = entry.isDirectory() || entry.isSymbolicLink() && statSync(
|
|
1679
|
+
const isDir = entry.isDirectory() || entry.isSymbolicLink() && statSync(join5(dir, entry.name)).isDirectory();
|
|
1642
1680
|
if (!isDir)
|
|
1643
1681
|
continue;
|
|
1644
1682
|
if (existsSync2(teamSettingsPath(entry.name)))
|
|
1645
1683
|
teams.push(entry.name);
|
|
1646
1684
|
} catch (err) {
|
|
1647
|
-
console.warn(`rt: skipping unreadable teams entry ${
|
|
1685
|
+
console.warn(`rt: skipping unreadable teams entry ${join5(dir, entry.name)}: ${err.message}`);
|
|
1648
1686
|
}
|
|
1649
1687
|
}
|
|
1650
1688
|
return teams;
|
|
@@ -1693,7 +1731,7 @@ function teamPath(teamsDir2, name) {
|
|
|
1693
1731
|
if (name.includes("/") || name.includes("\\") || name.includes("..")) {
|
|
1694
1732
|
throw new Error(`rt: cannot expand \${team:${name}} — a team name must be a single directory segment (no "/", "\\" or "..")`);
|
|
1695
1733
|
}
|
|
1696
|
-
return
|
|
1734
|
+
return join6(teamsDir2, name);
|
|
1697
1735
|
}
|
|
1698
1736
|
function required(value, name, needs) {
|
|
1699
1737
|
if (value === undefined || value === "") {
|
|
@@ -1868,7 +1906,7 @@ function expandCtxFrom(opts) {
|
|
|
1868
1906
|
return {
|
|
1869
1907
|
repoRoot: opts.expandCtx?.repoRoot,
|
|
1870
1908
|
worktree: opts.expandCtx?.worktree,
|
|
1871
|
-
home: process.env.HOME ??
|
|
1909
|
+
home: process.env.HOME ?? homedir5(),
|
|
1872
1910
|
teamsDir: teamsDir()
|
|
1873
1911
|
};
|
|
1874
1912
|
}
|
|
@@ -2409,6 +2447,7 @@ export {
|
|
|
2409
2447
|
herdAttend,
|
|
2410
2448
|
herdAsk,
|
|
2411
2449
|
herdAnswer,
|
|
2450
|
+
guardTestDaemonEnv,
|
|
2412
2451
|
getSetting,
|
|
2413
2452
|
getRun,
|
|
2414
2453
|
getDef,
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strict by design: a JSON string is iterable and has .includes(), so an
|
|
3
|
+
* unvalidated parse would let a malformed value corrupt the merge here or
|
|
4
|
+
* silently disarm the transport guard. Anything but a string array throws.
|
|
5
|
+
*/
|
|
6
|
+
export declare function parseForbidSocks(raw: string): string[];
|
|
7
|
+
export declare function guardTestDaemonEnv(env?: NodeJS.ProcessEnv): void;
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test-run isolation for suites that repoint HOME at a throwaway directory.
|
|
3
|
+
* An ambient RT_DAEMON_SOCK (herdr panes, agent shells) routes rtCommand at
|
|
4
|
+
* a live daemon over the top of the fake HOME, so a repointed HOME alone is
|
|
5
|
+
* not isolation. A test preload calls this BEFORE repointing HOME: it strips
|
|
6
|
+
* the live pointers from the env and arms RT_TEST_FORBID_SOCKS, the list of
|
|
7
|
+
* sockets rtCommand refuses to dispatch at (transport.ts).
|
|
8
|
+
*/
|
|
9
|
+
import { homedir } from "os";
|
|
10
|
+
import { join } from "path";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Strict by design: a JSON string is iterable and has .includes(), so an
|
|
14
|
+
* unvalidated parse would let a malformed value corrupt the merge here or
|
|
15
|
+
* silently disarm the transport guard. Anything but a string array throws.
|
|
16
|
+
*/
|
|
17
|
+
export function parseForbidSocks(raw: string): string[] {
|
|
18
|
+
const parsed: unknown = JSON.parse(raw);
|
|
19
|
+
if (!Array.isArray(parsed) || parsed.some((path) => typeof path !== "string")) {
|
|
20
|
+
throw new Error("rt-client: RT_TEST_FORBID_SOCKS must be a JSON string array of socket paths");
|
|
21
|
+
}
|
|
22
|
+
return parsed;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function guardTestDaemonEnv(env: NodeJS.ProcessEnv = process.env): void {
|
|
26
|
+
const forbidden = new Set<string>();
|
|
27
|
+
if (env.RT_TEST_FORBID_SOCKS) {
|
|
28
|
+
for (const path of parseForbidSocks(env.RT_TEST_FORBID_SOCKS)) forbidden.add(path);
|
|
29
|
+
}
|
|
30
|
+
if (env.RT_DAEMON_SOCK) forbidden.add(env.RT_DAEMON_SOCK);
|
|
31
|
+
if (env.RT_APP_SOCKET) forbidden.add(env.RT_APP_SOCKET);
|
|
32
|
+
forbidden.add(join(env.HOME ?? homedir(), ".mattstack", "rt", "rt.sock"));
|
|
33
|
+
delete env.RT_DAEMON_SOCK;
|
|
34
|
+
delete env.RT_APP_SOCKET;
|
|
35
|
+
env.RT_TEST_FORBID_SOCKS = JSON.stringify([...forbidden]);
|
|
36
|
+
}
|
package/src/transport.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { homedir } from "os";
|
|
11
11
|
import { join } from "path";
|
|
12
|
+
import { parseForbidSocks } from "./test-isolation.ts";
|
|
12
13
|
|
|
13
14
|
export interface RtResponse<T = unknown> {
|
|
14
15
|
ok: boolean;
|
|
@@ -56,12 +57,31 @@ function defaultSock(): string {
|
|
|
56
57
|
*/
|
|
57
58
|
export const DEFAULT_SOCK = defaultSock();
|
|
58
59
|
|
|
60
|
+
/**
|
|
61
|
+
* The one exception to the never-throws contract below: when a test preload
|
|
62
|
+
* has armed RT_TEST_FORBID_SOCKS (see test-isolation.ts), dispatching at a
|
|
63
|
+
* listed socket throws instead of degrading. A degrade envelope could be
|
|
64
|
+
* tolerated by the calling test; escaping isolation must fail the run loudly.
|
|
65
|
+
* A malformed list throws too rather than silently disarming the guard.
|
|
66
|
+
*/
|
|
67
|
+
function assertSockNotForbidden(cmd: string, sockPath: string): void {
|
|
68
|
+
const raw = process.env.RT_TEST_FORBID_SOCKS;
|
|
69
|
+
if (!raw) return;
|
|
70
|
+
const forbidden = parseForbidSocks(raw);
|
|
71
|
+
if (forbidden.includes(sockPath)) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
`rt-client: refusing "${cmd}" at forbidden socket ${sockPath}: RT_TEST_FORBID_SOCKS marks it as a live daemon socket, so this dispatch would have escaped test isolation`,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
59
78
|
export async function rtCommand<T = unknown>(
|
|
60
79
|
cmd: string,
|
|
61
80
|
payload: Record<string, unknown>,
|
|
62
81
|
opts: { sockPath?: string; timeoutMs?: number } = {},
|
|
63
82
|
): Promise<RtResponse<T>> {
|
|
64
83
|
const sockPath = opts.sockPath ?? defaultSock();
|
|
84
|
+
assertSockNotForbidden(cmd, sockPath);
|
|
65
85
|
try {
|
|
66
86
|
const res = await fetch(`http://localhost/${cmd}`, {
|
|
67
87
|
unix: sockPath,
|