@mattstack/rt-client 0.15.1 → 0.19.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/client.d.ts +30 -0
- package/dist/commands.d.ts +295 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.js +192 -14
- package/dist/pane-ref.d.ts +8 -0
- package/dist/settings/paths.d.ts +8 -0
- package/dist/settings/team-local-read.d.ts +7 -0
- package/package.json +1 -1
- package/src/client.ts +155 -3
- package/src/commands.ts +86 -5
- package/src/index.ts +28 -1
- package/src/pane-ref.ts +28 -0
- package/src/settings/paths.ts +11 -0
- package/src/settings/registry-defs.ts +1 -1
- package/src/settings/team-local-read.ts +18 -0
- package/src/settings/write.ts +24 -3
package/src/commands.ts
CHANGED
|
@@ -95,11 +95,27 @@ export interface EventsBusEvent { id: number; topic: string; payload: unknown; e
|
|
|
95
95
|
export const GATE_BY_PANE = "pane";
|
|
96
96
|
|
|
97
97
|
export type GateStatus = "open" | "answered" | "parked" | "closed";
|
|
98
|
-
export
|
|
98
|
+
export type GateOption = string | { value: string; label: string };
|
|
99
|
+
export interface GateOrigin {
|
|
100
|
+
paneId?: string;
|
|
101
|
+
tabId?: string;
|
|
102
|
+
runId?: string;
|
|
103
|
+
worktree?: string;
|
|
104
|
+
presentation?: "form" | "wait";
|
|
105
|
+
}
|
|
106
|
+
export interface GateQuestion { id: string; label: string; multi: boolean; options: GateOption[] }
|
|
107
|
+
export function gateOptionValue(o: GateOption): string {
|
|
108
|
+
return typeof o === "string" ? o : o.value;
|
|
109
|
+
}
|
|
110
|
+
export function gateOptionLabel(o: GateOption): string {
|
|
111
|
+
return typeof o === "string" ? o : (o.label || o.value);
|
|
112
|
+
}
|
|
99
113
|
export interface GateAnswer { answers: Record<string, string | string[] | { value: string | string[]; note?: string }>; by: string; answeredAt: number }
|
|
100
114
|
export interface GateRow {
|
|
101
115
|
id: string; subject: string; kind: string;
|
|
102
116
|
questions: GateQuestion[]; meta: Record<string, unknown> | null;
|
|
117
|
+
context?: string | null;
|
|
118
|
+
origin?: GateOrigin | null;
|
|
103
119
|
status: GateStatus; answer: GateAnswer | null;
|
|
104
120
|
openedAt: number; parkedAt: number | null; closedAt: number | null;
|
|
105
121
|
closedReason: "abandoned" | "superseded" | "pruned" | null;
|
|
@@ -118,6 +134,21 @@ export interface GateSubscription {
|
|
|
118
134
|
dead: boolean;
|
|
119
135
|
}
|
|
120
136
|
|
|
137
|
+
export interface HerdInfo { id: string; repo: string; room: string; workspace: string; shepherdSession: string; shepherdHandle: string; herdrSocket: string | null; hidden: boolean; status: "active" | "wrapped"; createdAt: number; wrappedAt: number | null }
|
|
138
|
+
/** A herd row as `herd:list` reports it: the registry row plus how many jobs hang off it. */
|
|
139
|
+
export interface HerdListRow extends HerdInfo { jobs: number }
|
|
140
|
+
export interface HerdJobInfo { herd: string; name: string; worktree: string; branch: string | null; tree: string | null; pane: string | null; agentSession: string | null; agentId: string | null; handle: string; status: "spawning" | "active" | "at-gate" | "at-milestone" | "done" | "closed" | "crashed"; disposable: boolean; lastGate: string | null; lastReport: number | null; createdAt: number; updatedAt: number }
|
|
141
|
+
/** `lastGateStatus`/`lastGateDelivery` come from the job's `lastGate` row: an `answered` gate whose delivery is `dead-pane` is the "answered, worker not woken" case the shepherd must act on. */
|
|
142
|
+
export interface HerdStatusData {
|
|
143
|
+
herd: HerdInfo;
|
|
144
|
+
jobs: Array<HerdJobInfo & { openGate: string | null; paneStatus: string | null; lastGateStatus: GateStatus | null; lastGateDelivery: "delivered" | "dead-pane" | null }>;
|
|
145
|
+
unread: number;
|
|
146
|
+
lifecycleConnected: boolean;
|
|
147
|
+
hiddenUp: boolean | null;
|
|
148
|
+
/** The shepherd session's own `herd:<id>/` subscription row, or null when none is live. */
|
|
149
|
+
subscription: { id: string; dead: boolean; lastDelivery: GateSubscription["lastDelivery"] } | null;
|
|
150
|
+
}
|
|
151
|
+
|
|
121
152
|
/**
|
|
122
153
|
* Duplicated shape on purpose, same reasoning as EventsBusEvent above:
|
|
123
154
|
* these mirror lib/state/chat-store.ts's types, which rt-client cannot
|
|
@@ -213,7 +244,10 @@ export interface InviteResult { paneId: string; delivered: "accepted" | "queued"
|
|
|
213
244
|
/** Duplicated shape on purpose: mirrors lib/daemon/inject.ts's InjectResult. */
|
|
214
245
|
export type PaneDelivery = "accepted" | "queued" | "refused";
|
|
215
246
|
export interface PaneSendResult { paneId: string; delivered: PaneDelivery; reason?: string }
|
|
216
|
-
|
|
247
|
+
/** `attendTab` is set only for a `bg:` ref: focus for a background pane IS
|
|
248
|
+
the attend flow (a visible tab running a terminal attach), and this is
|
|
249
|
+
that tab's id. */
|
|
250
|
+
export interface PaneFocusResult { paneId: string; focused: boolean; attendTab?: string }
|
|
217
251
|
|
|
218
252
|
// SKILLS-53: one judgment, computed once in rt, so the console and the tray
|
|
219
253
|
// never derive two verdicts that can disagree.
|
|
@@ -524,7 +558,7 @@ export interface Commands {
|
|
|
524
558
|
"chat:dm-open": { payload: { from: string; to: string; sessionId?: string }; data: { room: string; created: boolean } };
|
|
525
559
|
|
|
526
560
|
// ─── Agent handoff (rt agent) ────────────────────────────────────────────
|
|
527
|
-
"agent:start": { payload: { repo: string; cwd: string; prompt?: string; surface?: AgentSurface; model?: string; effort?: string; account?: string; label?: string; caller?: string; workspace?: string; tab?: string; extraArgs?: string }; data: AgentRecord };
|
|
561
|
+
"agent:start": { payload: { repo: string; cwd: string; prompt?: string; surface?: AgentSurface; model?: string; effort?: string; account?: string; label?: string; caller?: string; workspace?: string; tab?: string; extraArgs?: string; env?: Record<string, string>; herdrSocket?: string; handle?: string; bg?: boolean }; data: AgentRecord };
|
|
528
562
|
"agent:resume": { payload: { id: string; prompt?: string; surface?: AgentSurface; workspace?: string; tab?: string }; data: AgentRecord };
|
|
529
563
|
"agent:get": { payload: { id: string }; data: AgentRecord };
|
|
530
564
|
"agent:list": { payload: { repo?: string }; data: { agents: AgentRecord[] } };
|
|
@@ -538,7 +572,9 @@ export interface Commands {
|
|
|
538
572
|
data: { pane: ChatPane; ready: boolean };
|
|
539
573
|
};
|
|
540
574
|
"pane:send": { payload: { paneId: string; text: string; callerPane?: string }; data: PaneSendResult };
|
|
541
|
-
|
|
575
|
+
/** `callerWorkspace` (HERDR_WORKSPACE_ID) is required only for a `bg:`
|
|
576
|
+
ref, whose focus opens an attend tab in the caller's own workspace. */
|
|
577
|
+
"pane:focus": { payload: { paneId: string; callerWorkspace?: string }; data: PaneFocusResult };
|
|
542
578
|
|
|
543
579
|
// ─── R013/R016 ────────────────────────────────────────────────
|
|
544
580
|
"cache:read": { payload: { branches?: string[]; maxAgeMs?: number; repoIdentity?: string }; data: Record<string, BranchEnrichment> };
|
|
@@ -574,7 +610,7 @@ export interface Commands {
|
|
|
574
610
|
"freshness:reconcile": { payload: Record<string, never>; data: unknown };
|
|
575
611
|
|
|
576
612
|
// ─── Gate facility (BOARD-20/21) ─────────────────────────────────────────
|
|
577
|
-
"gate:open": { payload: { subject: string; kind: string; questions: GateQuestion[]; meta?: Record<string, unknown>; agent?: string; pane?: string; nudge?: { session: string } }; data: { id: string; supersededId: string | null } };
|
|
613
|
+
"gate:open": { payload: { subject: string; kind: string; questions: GateQuestion[]; meta?: Record<string, unknown>; agent?: string; pane?: string; nudge?: { session: string }; context?: string; origin?: GateOrigin }; data: { id: string; supersededId: string | null } };
|
|
578
614
|
/**
|
|
579
615
|
* A CAS loss is a DEFINED OUTCOME, not an error: `ok:true` with
|
|
580
616
|
* `conflict:true` and the WINNING row, so every consumer gets the winner
|
|
@@ -596,6 +632,25 @@ export interface Commands {
|
|
|
596
632
|
* onto delivery outcomes (dead marks included). */
|
|
597
633
|
"gate:subscriptions": { payload: { session?: string; live?: boolean }; data: { subscriptions: GateSubscription[] } };
|
|
598
634
|
|
|
635
|
+
// ─── Herd (shepherd run registry) ────────────────────────────────────────
|
|
636
|
+
"herd:start": { payload: { name: string; repo: string; session: string; hidden?: boolean }; data: { herd: string; room: string; workspace: string; subscription: string; handle: string; hidden: boolean } };
|
|
637
|
+
"herd:resume": { payload: { herd: string; session: string }; data: { subscription: string; gates: GateRow[]; unread: number; status: HerdStatusData; handle: string } };
|
|
638
|
+
"herd:status": { payload: { herd: string }; data: HerdStatusData };
|
|
639
|
+
/** Active herds only unless `all`, so a shepherd's "which herd am I on" question has one answer. */
|
|
640
|
+
"herd:list": { payload: { all?: boolean }; data: { herds: HerdListRow[] } };
|
|
641
|
+
"herd:close": { payload: { herd: string; job: string }; data: { job: string; status: "closed" } };
|
|
642
|
+
/** `brief` is the brief TEXT, not a path: the CLI reads the file. It is stored at `<jobsRoot>/<herd>/<job>/job.md`, so a respawn with `dir` and no `brief` reads it back. */
|
|
643
|
+
"herd:spawn": { payload: { herd: string; job: string; brief?: string; dir?: string; model?: string; effort?: string; account?: string; disposable?: boolean }; data: { herd: string; job: string; pane: string; worktree: string; branch: string | null; tree: string | null; /** null = no provisioning ran (--dir); false = cold create, worth announcing. */ wasOnDeck: boolean | null; agentId: string; sessionId: string; handle: string } };
|
|
644
|
+
"herd:gates": { payload: { herd: string }; data: { gates: GateRow[] } };
|
|
645
|
+
"herd:ask": { payload: { herd: string; job: string; session: string; pane?: string; questions: GateQuestion[]; context?: string }; data: { gate: string } };
|
|
646
|
+
"herd:milestone": { payload: { herd: string; job: string; session: string; pane?: string; artifact: string; summary?: string }; data: { gate: string; message: number } };
|
|
647
|
+
"herd:answer": { payload: { gate: string }; data: { gate: string; status: GateStatus; answer: GateAnswer | null; closedReason: GateRow["closedReason"] } };
|
|
648
|
+
"herd:report": { payload: { herd: string; job: string; body: string }; data: { message: number } };
|
|
649
|
+
/** `callerWorkspace` is the attending session's own HERDR_WORKSPACE_ID: the attached tab opens there, not in the herd's workspace. */
|
|
650
|
+
"herd:attend": { payload: { herd: string; job: string; callerWorkspace: string }; data: { tab: string; pane: string } };
|
|
651
|
+
"herd:stop-hidden": { payload: Record<string, never>; data: { stopped: boolean } };
|
|
652
|
+
"herd:wrap-up": { payload: { herd: string; closePanes?: boolean; dispose?: string[]; deleteJobDirs?: boolean; archiveRoom?: boolean }; data: { closed: string[]; workspaceClosed: boolean; disposed: string[]; refused: Array<{ tree: string; reason: string }>; deletedJobDirs: boolean; archived: boolean } };
|
|
653
|
+
|
|
599
654
|
/** Wire reply on success is always `{ok:true, repaired}` (no `data`
|
|
600
655
|
* wrapper) — `data` here documents the extra field the same way PingData
|
|
601
656
|
* does for `ping`, not the literal wire nesting (R3). */
|
|
@@ -616,6 +671,13 @@ export interface Commands {
|
|
|
616
671
|
"worktree:restore": { payload: { repoName: string; tree: string }; data: WorktreeRestoreData };
|
|
617
672
|
"worktree:freshen": { payload: { repoName?: string; tree?: string }; data: WorktreeFreshenData };
|
|
618
673
|
"worktree:adopt": { payload: { repoName: string; claim?: boolean }; data: WorktreeAdoptData };
|
|
674
|
+
|
|
675
|
+
// ─── Background server (daemon-owned background herdr session) ──────────
|
|
676
|
+
"bg:ensure": { payload: { claim?: string }; data: { socket: string; started: boolean; parity: { ok: boolean; drift: string[] } | null } };
|
|
677
|
+
"bg:status": { payload: Record<string, never>; data: { up: boolean; socket: string; claims: Array<{ owner: string; pane: string | null; createdAt: number }> } };
|
|
678
|
+
/** Rejects (`ok:false`) naming every live claim owner while any claim is held. */
|
|
679
|
+
"bg:stop": { payload: Record<string, never>; data: { stopped: boolean } };
|
|
680
|
+
"bg:release": { payload: { claim: string }; data: { released: boolean } };
|
|
619
681
|
}
|
|
620
682
|
|
|
621
683
|
export type CommandName = keyof Commands;
|
|
@@ -699,6 +761,20 @@ export const COMMAND_NAMES: readonly CommandName[] = [
|
|
|
699
761
|
"gate:subscribe",
|
|
700
762
|
"gate:unsubscribe",
|
|
701
763
|
"gate:subscriptions",
|
|
764
|
+
"herd:start",
|
|
765
|
+
"herd:resume",
|
|
766
|
+
"herd:status",
|
|
767
|
+
"herd:list",
|
|
768
|
+
"herd:close",
|
|
769
|
+
"herd:spawn",
|
|
770
|
+
"herd:gates",
|
|
771
|
+
"herd:ask",
|
|
772
|
+
"herd:milestone",
|
|
773
|
+
"herd:answer",
|
|
774
|
+
"herd:report",
|
|
775
|
+
"herd:attend",
|
|
776
|
+
"herd:stop-hidden",
|
|
777
|
+
"herd:wrap-up",
|
|
702
778
|
"hooks:repair",
|
|
703
779
|
"hooks:watch",
|
|
704
780
|
"sdm:catalog",
|
|
@@ -713,4 +789,9 @@ export const COMMAND_NAMES: readonly CommandName[] = [
|
|
|
713
789
|
"worktree:restore",
|
|
714
790
|
"worktree:freshen",
|
|
715
791
|
"worktree:adopt",
|
|
792
|
+
|
|
793
|
+
"bg:ensure",
|
|
794
|
+
"bg:status",
|
|
795
|
+
"bg:stop",
|
|
796
|
+
"bg:release",
|
|
716
797
|
];
|
package/src/index.ts
CHANGED
|
@@ -54,9 +54,27 @@ export {
|
|
|
54
54
|
gateSubscribe,
|
|
55
55
|
gateUnsubscribe,
|
|
56
56
|
gateSubscriptions,
|
|
57
|
+
herdStart,
|
|
58
|
+
herdSpawn,
|
|
59
|
+
herdAsk,
|
|
60
|
+
herdMilestone,
|
|
61
|
+
herdAnswer,
|
|
62
|
+
herdReport,
|
|
63
|
+
herdGates,
|
|
64
|
+
herdStatus,
|
|
65
|
+
herdList,
|
|
66
|
+
herdResume,
|
|
67
|
+
herdClose,
|
|
68
|
+
herdAttend,
|
|
69
|
+
herdWrapUp,
|
|
70
|
+
herdStopHidden,
|
|
71
|
+
bgEnsure,
|
|
72
|
+
bgStatus,
|
|
73
|
+
bgStop,
|
|
74
|
+
bgRelease,
|
|
57
75
|
} from "./client.ts";
|
|
58
76
|
|
|
59
|
-
export { COMMAND_NAMES, GATE_BY_PANE } from "./commands.ts";
|
|
77
|
+
export { COMMAND_NAMES, GATE_BY_PANE, gateOptionValue, gateOptionLabel } from "./commands.ts";
|
|
60
78
|
export type {
|
|
61
79
|
Discussion,
|
|
62
80
|
DemandDecl,
|
|
@@ -94,10 +112,16 @@ export type {
|
|
|
94
112
|
PaneSendResult,
|
|
95
113
|
PaneFocusResult,
|
|
96
114
|
GateStatus,
|
|
115
|
+
GateOption,
|
|
116
|
+
GateOrigin,
|
|
97
117
|
GateQuestion,
|
|
98
118
|
GateAnswer,
|
|
99
119
|
GateRow,
|
|
100
120
|
GateSubscription,
|
|
121
|
+
HerdInfo,
|
|
122
|
+
HerdListRow,
|
|
123
|
+
HerdJobInfo,
|
|
124
|
+
HerdStatusData,
|
|
101
125
|
} from "./commands.ts";
|
|
102
126
|
|
|
103
127
|
export { subscribe, createRelay, DEFAULT_WS_URL } from "./relay.ts";
|
|
@@ -110,6 +134,9 @@ export { repoNameForPath } from "./repos.ts";
|
|
|
110
134
|
export { decidePlacement, openSmartPane } from "./smart-pane.ts";
|
|
111
135
|
export type { Placement, PlacementOpts, HerdrCall } from "./smart-pane.ts";
|
|
112
136
|
|
|
137
|
+
export { BG_PREFIX, parsePaneRef, formatPaneRef } from "./pane-ref.ts";
|
|
138
|
+
export type { PaneServer, PaneRef } from "./pane-ref.ts";
|
|
139
|
+
|
|
113
140
|
// ─── Settings (RT-50) ────────────────────────────────────────────────────────
|
|
114
141
|
|
|
115
142
|
export { getSetting, listSettings, explainSetting, expandVariables, SCOPE_ORDER, setSettingsWarnSink } from "./settings/resolve.ts";
|
package/src/pane-ref.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export const BG_PREFIX = "bg:";
|
|
2
|
+
|
|
3
|
+
export type PaneServer = "visible" | "bg";
|
|
4
|
+
|
|
5
|
+
export interface PaneRef {
|
|
6
|
+
server: PaneServer;
|
|
7
|
+
paneId: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function parsePaneRef(ref: string): PaneRef {
|
|
11
|
+
if (ref.startsWith(BG_PREFIX)) {
|
|
12
|
+
return {
|
|
13
|
+
server: "bg",
|
|
14
|
+
paneId: ref.slice(BG_PREFIX.length),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
return {
|
|
18
|
+
server: "visible",
|
|
19
|
+
paneId: ref,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function formatPaneRef(paneId: string, server: PaneServer): string {
|
|
24
|
+
if (server === "visible") {
|
|
25
|
+
return paneId;
|
|
26
|
+
}
|
|
27
|
+
return BG_PREFIX + paneId;
|
|
28
|
+
}
|
package/src/settings/paths.ts
CHANGED
|
@@ -27,6 +27,17 @@ export function teamSettingsPath(team: string): string {
|
|
|
27
27
|
return join(teamsDir(), team, "mattstack", "settings.team.jsonc");
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* ~/.mattstack/rt/teams/<team>.json: the machine-local team record. Mirrored
|
|
32
|
+
* from repo-tools/lib/team/team-local.ts's teamLocalPath, which is the
|
|
33
|
+
* authority: rt-client has no dependency on rt's lib/, so this literal is
|
|
34
|
+
* duplicated here rather than imported (same convention as `teamSettingsPath`
|
|
35
|
+
* and `userSettingsPath` above).
|
|
36
|
+
*/
|
|
37
|
+
export function teamLocalPath(team: string): string {
|
|
38
|
+
return join(home(), ".mattstack", "rt", "teams", `${team}.json`);
|
|
39
|
+
}
|
|
40
|
+
|
|
30
41
|
/**
|
|
31
42
|
* ~/.mattstack/user/local/<machineKey()>/settings.local.jsonc — the machine
|
|
32
43
|
* store, TRACKED and keyed per machine (path literals legal here only).
|
|
@@ -85,7 +85,7 @@ export const REGISTRY: readonly SettingDef[] = [
|
|
|
85
85
|
scopes: ["user"],
|
|
86
86
|
default: [],
|
|
87
87
|
merge: "replace",
|
|
88
|
-
description: "Event-bus glob rules that raise a desktop notification: [{pattern, category, title, message}]. pattern is matched against the events-bus topic (Bun.Glob semantics); title/message may interpolate `{field}` from the event payload. A fresh key, not an ownership-latch port, so a default is fine here.",
|
|
88
|
+
description: "Event-bus glob rules that raise a desktop notification: [{pattern, category, title, message, subjectPrefix?, url?}]. pattern is matched against the events-bus topic (Bun.Glob semantics); title/message may interpolate `{field}` from the event payload, plus the computed `{question}` field (the event payload's first question label, `payload.questions[0].label`, empty string when absent); optional subjectPrefix matches the event payload's subject as a prefix. The optional url is interpolated the same way as title/message and becomes the notification's Open target; a gate rule should set it. A fresh key, not an ownership-latch port, so a default is fine here.",
|
|
89
89
|
},
|
|
90
90
|
{
|
|
91
91
|
key: "rt.cron",
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One field of the machine-local team record, for the write guard. The record
|
|
3
|
+
* itself is owned by repo-tools/lib/team/team-local.ts; this reads only what
|
|
4
|
+
* the guard needs and never writes.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { readFileSync } from "fs";
|
|
8
|
+
import { teamLocalPath } from "./paths.ts";
|
|
9
|
+
|
|
10
|
+
/** Unreadable, absent or malformed all read as false, so nothing that predates the field is refused. */
|
|
11
|
+
export function isJoinedTeam(team: string): boolean {
|
|
12
|
+
try {
|
|
13
|
+
const parsed: unknown = JSON.parse(readFileSync(teamLocalPath(team), "utf8"));
|
|
14
|
+
return typeof parsed === "object" && parsed !== null && (parsed as { joinedByRt?: unknown }).joinedByRt === true;
|
|
15
|
+
} catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
package/src/settings/write.ts
CHANGED
|
@@ -100,6 +100,7 @@ import { dirname } from "path";
|
|
|
100
100
|
import { machineSettingsPath, teamSettingsPath, userSettingsPath } from "./paths.ts";
|
|
101
101
|
import { getDef, isMigrated, validateValue, type SettingDef, type SettingScope } from "./registry-machinery.ts";
|
|
102
102
|
import { listTeams } from "./stores.ts";
|
|
103
|
+
import { isJoinedTeam } from "./team-local-read.ts";
|
|
103
104
|
|
|
104
105
|
export interface SetSettingOpts {
|
|
105
106
|
/** Normalized repo identity — required to target a repoScoped key's `repos.<identity>` section. */
|
|
@@ -208,6 +209,19 @@ function migratedFalseMessage(key: string, def: SettingDef): string {
|
|
|
208
209
|
return `"${key}" is not writable through the settings resolver yet${legacyPart}`;
|
|
209
210
|
}
|
|
210
211
|
|
|
212
|
+
/**
|
|
213
|
+
* A clone that arrived by redeeming an invite is pull-only, so a write here
|
|
214
|
+
* would never reach the team AND would leave a tracked file dirty, which is
|
|
215
|
+
* enough on its own to make the daemon's fast-forward pull fail.
|
|
216
|
+
*/
|
|
217
|
+
function refuseIfJoined(team: string): void {
|
|
218
|
+
if (isJoinedTeam(team)) {
|
|
219
|
+
refuse(
|
|
220
|
+
`this machine joined "${team}" by invite, so its clone is pull-only and team settings cannot be written here. Ask the team's owner to make this change. Member-proposed changes are tracked in MAT-415.`,
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
211
225
|
/** Resolves which store file a write targets, applying the team-selection rule for `scope: "team"`. */
|
|
212
226
|
function resolveStorePath(scope: SettingScope, opts: SetSettingOpts): string {
|
|
213
227
|
if (scope === "user") return userSettingsPath();
|
|
@@ -218,6 +232,7 @@ function resolveStorePath(scope: SettingScope, opts: SetSettingOpts): string {
|
|
|
218
232
|
if (!existsSync(path)) {
|
|
219
233
|
refuse(`team store for "${opts.team}" does not exist (${path}) — clone/seed it before writing to it`);
|
|
220
234
|
}
|
|
235
|
+
refuseIfJoined(opts.team);
|
|
221
236
|
return path;
|
|
222
237
|
}
|
|
223
238
|
|
|
@@ -228,7 +243,9 @@ function resolveStorePath(scope: SettingScope, opts: SetSettingOpts): string {
|
|
|
228
243
|
if (teams.length > 1) {
|
|
229
244
|
refuse(`multiple local team stores found (${teams.join(", ")}) — pass opts.team to choose one`);
|
|
230
245
|
}
|
|
231
|
-
|
|
246
|
+
const team = teams[0] as string;
|
|
247
|
+
refuseIfJoined(team);
|
|
248
|
+
return teamSettingsPath(team);
|
|
232
249
|
}
|
|
233
250
|
|
|
234
251
|
/**
|
|
@@ -243,7 +260,9 @@ function resolveStorePathForUnset(scope: SettingScope, opts: SetSettingOpts): st
|
|
|
243
260
|
|
|
244
261
|
if (opts.team !== undefined) {
|
|
245
262
|
const path = teamSettingsPath(opts.team);
|
|
246
|
-
|
|
263
|
+
if (!existsSync(path)) return null;
|
|
264
|
+
refuseIfJoined(opts.team);
|
|
265
|
+
return path;
|
|
247
266
|
}
|
|
248
267
|
|
|
249
268
|
const teams = listTeams();
|
|
@@ -251,7 +270,9 @@ function resolveStorePathForUnset(scope: SettingScope, opts: SetSettingOpts): st
|
|
|
251
270
|
if (teams.length > 1) {
|
|
252
271
|
refuse(`multiple local team stores found (${teams.join(", ")}) — pass opts.team to choose one`);
|
|
253
272
|
}
|
|
254
|
-
|
|
273
|
+
const team = teams[0] as string;
|
|
274
|
+
refuseIfJoined(team);
|
|
275
|
+
return teamSettingsPath(team);
|
|
255
276
|
}
|
|
256
277
|
|
|
257
278
|
/** `// header comment\n{}\n` — see module doc for why the object must be seeded before the first `modify`. */
|