@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 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 || join(process.env.HOME ?? homedir(), ".mattstack", "rt", "rt.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 homedir2 } from "os";
695
- import { dirname, join as join2 } from "path";
732
+ import { homedir as homedir3 } from "os";
733
+ import { dirname, join as join3 } from "path";
696
734
  function defaultReposJsonPath() {
697
- return join2(homedir2(), ".mattstack", "rt", "repos.json");
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 = join2(dirname(jsonPath), "state.db");
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 homedir4 } from "os";
830
- import { join as join5 } from "path";
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 homedir3, hostname } from "os";
835
- import { join as join3 } from "path";
872
+ import { homedir as homedir4, hostname } from "os";
873
+ import { join as join4 } from "path";
836
874
  function home() {
837
- return process.env.HOME ?? homedir3();
875
+ return process.env.HOME ?? homedir4();
838
876
  }
839
877
  function userSettingsPath() {
840
- return join3(home(), ".mattstack", "user", "settings.user.jsonc");
878
+ return join4(home(), ".mattstack", "user", "settings.user.jsonc");
841
879
  }
842
880
  function teamSettingsPath(team) {
843
- return join3(teamsDir(), team, "mattstack", "settings.team.jsonc");
881
+ return join4(teamsDir(), team, "mattstack", "settings.team.jsonc");
844
882
  }
845
883
  function teamLocalPath(team) {
846
- return join3(home(), ".mattstack", "rt", "teams", `${team}.json`);
884
+ return join4(home(), ".mattstack", "rt", "teams", `${team}.json`);
847
885
  }
848
886
  function machineSettingsPath() {
849
- return join3(home(), ".mattstack", "user", "local", machineKey(), "settings.local.jsonc");
887
+ return join4(home(), ".mattstack", "user", "local", machineKey(), "settings.local.jsonc");
850
888
  }
851
889
  function teamsDir() {
852
- return join3(home(), ".mattstack", "teams");
890
+ return join4(home(), ".mattstack", "teams");
853
891
  }
854
892
  function machineKey() {
855
- const override = join3(home(), ".mattstack", "machine-key");
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 join4 } from "path";
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(join4(dir, entry.name)).isDirectory();
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 ${join4(dir, entry.name)}: ${err.message}`);
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 join5(teamsDir2, name);
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 ?? homedir4(),
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mattstack/rt-client",
3
- "version": "0.24.0",
3
+ "version": "0.25.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
package/src/index.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
 
4
5
  export {
5
6
  readProjectMRs,
@@ -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,