1import { type DeviceToolVersions, WS_METHODS } from "@t3tools/contracts";
2import { Atom } from "effect/reactivity";
3
4import type { EnvironmentRegistry } from "../connection/registry.ts";
5import {
6 createAtomCommandScheduler,
7 createEnvironmentRpcCommand,
8 createEnvironmentRpcSubscriptionAtomFamily,
9} from "./runtime.ts";
10
11export function createDeviceEnvironmentAtoms<R, E>(
12 runtime: Atom.AtomRuntime<EnvironmentRegistry | R, E>,
13) {
14 const scheduler = createAtomCommandScheduler();
15 const concurrency = {
16 mode: "serial" as const,
17 key: ({ environmentId }: { environmentId: string }) => environmentId,
18 };
19 return {
20 /** Server-pushed device hosts, devices, and open sessions for one environment. */
21 state: createEnvironmentRpcSubscriptionAtomFamily(runtime, {
22 label: "environment-data:device:state",
23 tag: WS_METHODS.subscribeDeviceState,
24 }),
25 configure: createEnvironmentRpcCommand(runtime, {
26 label: "environment-data:device:configure",
27 tag: WS_METHODS.deviceConfigure,
28 scheduler,
29 concurrency,
30 }),
31 testHost: createEnvironmentRpcCommand(runtime, {
32 label: "environment-data:device:test-host",
33 tag: WS_METHODS.deviceTestHost,
34 }),
35 list: createEnvironmentRpcCommand(runtime, {
36 label: "environment-data:device:list",
37 tag: WS_METHODS.deviceList,
38 scheduler,
39 concurrency,
40 }),
41 open: createEnvironmentRpcCommand(runtime, {
42 label: "environment-data:device:open",
43 tag: WS_METHODS.deviceOpen,
44 scheduler,
45 concurrency,
46 }),
47 close: createEnvironmentRpcCommand(runtime, {
48 label: "environment-data:device:close",
49 tag: WS_METHODS.deviceClose,
50 scheduler,
51 concurrency,
52 }),
53 shutdown: createEnvironmentRpcCommand(runtime, {
54 label: "environment-data:device:shutdown",
55 tag: WS_METHODS.deviceShutdown,
56 scheduler,
57 concurrency,
58 }),
59 detail: createEnvironmentRpcCommand(runtime, {
60 label: "environment-data:device:detail",
61 tag: WS_METHODS.deviceDetail,
62 scheduler,
63 concurrency,
64 }),
65 action: createEnvironmentRpcCommand(runtime, {
66 label: "environment-data:device:action",
67 tag: WS_METHODS.deviceAction,
68 scheduler,
69 concurrency,
70 }),
71 };
72}
73
74/** Unknown inventory is distinct from a completed check that found no install. */
75export function deviceToolVersionLabels(tools: DeviceToolVersions | undefined) {
76 if (!tools) return ["Device tool versions have not been checked."];
77 return (
78 [
79 ["Device hub", tools.hub],
80 ["Agent tools", tools.agent],
81 ] as const
82 ).map(([name, tool]) => {
83 const installed = tool.installedVersions.length ? tool.installedVersions.join(", ") : "none";
84 return `${name}: installed ${installed}; required ${tool.requiredVersion}${tool.runningVersion ? `; running ${tool.runningVersion}` : ""}.`;
85 });
86}
87
88export function deviceToolUpdatePolicy(tools: DeviceToolVersions | undefined) {
89 if (!tools) return "Versions have not been checked. Reconnect the host and check versions.";
90 const outdated = [tools.hub, tools.agent].filter(
91 (tool) =>
92 tool.installedVersions.length > 0 && !tool.installedVersions.includes(tool.requiredVersion),
93 );
94 return outdated.length > 0
95 ? "Update pending. Required tools will install automatically when next used. The host needs network access; an older install is not used as a fallback."
96 : "Required tools are installed automatically when needed. Checking versions does not install or start anything.";
97}
98export const deviceToolUpdateOwnership =
99 "This environment's T3 server chooses device tool versions for itself and its SSH hosts. Update that server to receive newer tool versions; updating only your browser or mobile app does not update a remote server.";