@tsplc/debug 0.0.0-stage → 0.2.17

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/paths.js ADDED
@@ -0,0 +1,138 @@
1
+ // packages/debug/src/paths.ts
2
+ var segmentPattern = /[^.[\]]+|\[(?:-?\d+|\*)\]/g;
3
+ var pathSegments = (path) => path.match(segmentPattern) ?? [];
4
+ var joinPath = (prefix, segment) => segment.startsWith("[") || !prefix ? `${prefix}${segment}` : `${prefix}.${segment}`;
5
+ var typeOf = (manifest, id) => manifest.types[id] ?? { kind: "scalar", iec: id };
6
+ var createPaths = (manifest) => {
7
+ const typeOfId = (id) => typeOf(manifest, id);
8
+ const bases = new Map([
9
+ ...manifest.roots.map(({ path, ...root }) => [path, { kind: "target", path, ...root }]),
10
+ ...manifest.io.map(({ path, target, type }) => [path, { kind: "target", path, target, type, readable: true }])
11
+ ]);
12
+ const constants = new Map(manifest.constants.map(({ path, ...constant }) => [
13
+ path,
14
+ { kind: "constant", path, ...constant }
15
+ ]));
16
+ const groups = new Map;
17
+ for (const path of [...bases.keys(), ...constants.keys()]) {
18
+ let prefix = "";
19
+ for (const segment of pathSegments(path)) {
20
+ let children = groups.get(prefix);
21
+ if (!children)
22
+ groups.set(prefix, children = new Set);
23
+ children.add(segment);
24
+ prefix = joinPath(prefix, segment);
25
+ }
26
+ }
27
+ const resolve = (path) => {
28
+ const constant = constants.get(path);
29
+ if (constant)
30
+ return constant;
31
+ const segments = pathSegments(path);
32
+ for (let length = segments.length;length > 0; length--) {
33
+ const base = bases.get(segments.slice(0, length).reduce(joinPath, ""));
34
+ if (!base)
35
+ continue;
36
+ let resolved = base;
37
+ for (const segment of segments.slice(length)) {
38
+ const type = typeOfId(resolved.type);
39
+ if (type.kind === "record") {
40
+ const field = type.fields.find(({ name }) => name === segment);
41
+ if (!field)
42
+ return;
43
+ resolved = {
44
+ kind: "target",
45
+ path: joinPath(resolved.path, segment),
46
+ target: `${resolved.target}.${field.target}`,
47
+ type: field.type,
48
+ readable: resolved.readable,
49
+ ...field.generated && { generated: true },
50
+ ...field.declaration && { declaration: field.declaration }
51
+ };
52
+ } else if (type.kind === "array") {
53
+ const index = /^\[(-?\d+)\]$/.exec(segment)?.[1];
54
+ if (index === undefined || Number(index) < type.lower || Number(index) >= type.lower + type.length)
55
+ return;
56
+ resolved = {
57
+ kind: "target",
58
+ path: joinPath(resolved.path, segment),
59
+ target: `${resolved.target}[${index}]`,
60
+ type: type.element,
61
+ readable: resolved.readable
62
+ };
63
+ } else
64
+ return;
65
+ }
66
+ return resolved;
67
+ }
68
+ };
69
+ const typedChildren = (resolved) => {
70
+ const type = resolved?.kind === "target" ? typeOfId(resolved.type) : undefined;
71
+ if (type?.kind === "record")
72
+ return type.fields.map(({ name }) => name);
73
+ if (type?.kind === "array")
74
+ return Array.from({ length: type.length }, (_, index) => `[${type.lower + index}]`);
75
+ return [];
76
+ };
77
+ const children = (path) => {
78
+ const names = [...typedChildren(resolve(path)), ...groups.get(path) ?? []];
79
+ return [...new Set(names)].map((name) => {
80
+ const child = joinPath(path, name);
81
+ return { name, ...resolve(child) ?? { kind: "group", path: child } };
82
+ });
83
+ };
84
+ const isLeaf = (node) => node.kind === "constant" || node.kind === "target" && ["scalar", "enum"].includes(typeOfId(node.type).kind);
85
+ const expand = (patterns, limit = 2000) => {
86
+ const leaves = new Set;
87
+ const add = (path) => {
88
+ leaves.add(path);
89
+ if (leaves.size > limit)
90
+ throw new RangeError(`More than ${limit} values match`);
91
+ };
92
+ const below = (node) => {
93
+ if (isLeaf(node))
94
+ return add(node.path);
95
+ for (const child of children(node.path))
96
+ if (!(child.kind === "target" && child.generated))
97
+ below(child);
98
+ };
99
+ const match = (prefix, rest, wildcard) => {
100
+ const [head, ...tail] = rest;
101
+ if (head === undefined) {
102
+ const node = resolve(prefix) ?? (groups.has(prefix) ? { kind: "group", path: prefix } : undefined);
103
+ if (node)
104
+ below(node);
105
+ else if (!wildcard)
106
+ add(prefix);
107
+ return;
108
+ }
109
+ if (head === "*" || head === "[*]") {
110
+ for (const child of children(prefix))
111
+ if (!(child.kind === "target" && child.generated) && (head === "*" || child.name.startsWith("[")))
112
+ match(child.path, tail, true);
113
+ } else
114
+ match(joinPath(prefix, head), tail, wildcard);
115
+ };
116
+ for (const pattern of patterns)
117
+ match("", pathSegments(pattern), false);
118
+ return [...leaves];
119
+ };
120
+ const decode = (typeId, raw) => {
121
+ const type = typeOfId(typeId);
122
+ if (type.kind === "enum")
123
+ return Object.entries(type.members).find(([, value]) => value === raw)?.[0] ?? raw;
124
+ if (type.kind === "scalar" && type.iec === "BOOL" && typeof raw === "number")
125
+ return raw !== 0;
126
+ return raw;
127
+ };
128
+ return { typeOf: typeOfId, resolve, children, isLeaf, expand, decode };
129
+ };
130
+ export {
131
+ createPaths,
132
+ joinPath,
133
+ pathSegments,
134
+ typeOf
135
+ };
136
+
137
+ //# debugId=9B65CE7419829CA364756E2164756E21
138
+ //# sourceMappingURL=paths.js.map
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/paths.ts"],
4
+ "sourcesContent": [
5
+ "import type { DebugManifest, DebugType, SourceRange } from \"./manifest.ts\";\n\n/**\n * TypeScript paths over a manifest: `program.diverters.binTrackers[10].bin` names a root\n * (`program.diverters`), then record fields and array indexes. Pure, so the browser UI can use it.\n */\n\n/** A path that reads a target, or a constant the compiler folded. */\nexport type ResolvedPath =\n | {\n kind: \"target\";\n path: string;\n target: string;\n type: string;\n readable: boolean;\n generated?: true;\n declaration?: SourceRange;\n }\n | { kind: \"constant\"; path: string; type: string; value: unknown; declaration?: SourceRange };\n\n/** Roots or constants that share a prefix, such as `program` above `program.diverters`. */\ninterface GroupPath {\n kind: \"group\";\n path: string;\n}\n\n/** A child in the instance tree. */\nexport type PathNode = { name: string } & (ResolvedPath | GroupPath);\n\nconst segmentPattern = /[^.[\\]]+|\\[(?:-?\\d+|\\*)\\]/g;\n\nexport const pathSegments = (path: string) => path.match(segmentPattern) ?? [];\n\nexport const joinPath = (prefix: string, segment: string) =>\n segment.startsWith(\"[\") || !prefix ? `${prefix}${segment}` : `${prefix}.${segment}`;\n\n/** Scalar type ids are their IEC names, and may have no entry in `types`. */\nexport const typeOf = (manifest: DebugManifest, id: string): DebugType =>\n manifest.types[id] ?? { kind: \"scalar\", iec: id };\n\nexport const createPaths = (manifest: DebugManifest) => {\n const typeOfId = (id: string) => typeOf(manifest, id);\n // I/O bindings are readable symbols with TypeScript paths of their own.\n const bases = new Map<string, Extract<ResolvedPath, { kind: \"target\" }>>([\n ...manifest.roots.map(\n ({ path, ...root }) => [path, { kind: \"target\" as const, path, ...root }] as const,\n ),\n ...manifest.io.map(\n ({ path, target, type }) =>\n [path, { kind: \"target\" as const, path, target, type, readable: true }] as const,\n ),\n ]);\n const constants = new Map(\n manifest.constants.map(({ path, ...constant }) => [\n path,\n { kind: \"constant\" as const, path, ...constant },\n ]),\n );\n /** Children of the prefixes of every root and constant path, which are groups unless they resolve. */\n const groups = new Map<string, Set<string>>();\n for (const path of [...bases.keys(), ...constants.keys()]) {\n let prefix = \"\";\n for (const segment of pathSegments(path)) {\n let children = groups.get(prefix);\n if (!children) groups.set(prefix, (children = new Set()));\n children.add(segment);\n prefix = joinPath(prefix, segment);\n }\n }\n\n const resolve = (path: string): ResolvedPath | undefined => {\n const constant = constants.get(path);\n if (constant) return constant;\n const segments = pathSegments(path);\n for (let length = segments.length; length > 0; length--) {\n const base = bases.get(segments.slice(0, length).reduce(joinPath, \"\"));\n if (!base) continue;\n let resolved: ResolvedPath = base;\n for (const segment of segments.slice(length)) {\n const type = typeOfId(resolved.type);\n if (type.kind === \"record\") {\n const field = type.fields.find(({ name }) => name === segment);\n if (!field) return;\n resolved = {\n kind: \"target\",\n path: joinPath(resolved.path, segment),\n target: `${resolved.target}.${field.target}`,\n type: field.type,\n readable: resolved.readable,\n ...(field.generated && { generated: true }),\n ...(field.declaration && { declaration: field.declaration }),\n };\n } else if (type.kind === \"array\") {\n const index = /^\\[(-?\\d+)\\]$/.exec(segment)?.[1];\n if (\n index === undefined ||\n Number(index) < type.lower ||\n Number(index) >= type.lower + type.length\n )\n return;\n resolved = {\n kind: \"target\",\n path: joinPath(resolved.path, segment),\n target: `${resolved.target}[${index}]`,\n type: type.element,\n readable: resolved.readable,\n };\n } else return;\n }\n return resolved;\n }\n };\n\n const typedChildren = (resolved: ResolvedPath | undefined) => {\n const type = resolved?.kind === \"target\" ? typeOfId(resolved.type) : undefined;\n if (type?.kind === \"record\") return type.fields.map(({ name }) => name);\n if (type?.kind === \"array\")\n return Array.from({ length: type.length }, (_, index) => `[${type.lower + index}]`);\n return [];\n };\n\n /** A path's fields or indexes, and the roots and constants below it. */\n const children = (path: string): PathNode[] => {\n const names = [...typedChildren(resolve(path)), ...(groups.get(path) ?? [])];\n return [...new Set(names)].map((name) => {\n const child = joinPath(path, name);\n return { name, ...(resolve(child) ?? { kind: \"group\", path: child }) };\n });\n };\n\n const isLeaf = (node: ResolvedPath | GroupPath) =>\n node.kind === \"constant\" ||\n (node.kind === \"target\" && [\"scalar\", \"enum\"].includes(typeOfId(node.type).kind));\n\n /**\n * Leaf paths matching `patterns`, where `*` stands for one segment. A pattern naming a record,\n * array or group stands for all its leaves; wildcards and expansion skip compiler-made fields.\n * Paths that don't resolve are kept, so callers can report them.\n */\n const expand = (patterns: readonly string[], limit = 2000) => {\n const leaves = new Set<string>();\n const add = (path: string) => {\n leaves.add(path);\n if (leaves.size > limit) throw new RangeError(`More than ${limit} values match`);\n };\n const below = (node: ResolvedPath | GroupPath) => {\n if (isLeaf(node)) return add(node.path);\n for (const child of children(node.path))\n if (!(child.kind === \"target\" && child.generated)) below(child);\n };\n const match = (prefix: string, rest: readonly string[], wildcard: boolean) => {\n const [head, ...tail] = rest;\n if (head === undefined) {\n const node: ResolvedPath | GroupPath | undefined =\n resolve(prefix) ?? (groups.has(prefix) ? { kind: \"group\", path: prefix } : undefined);\n if (node) below(node);\n else if (!wildcard) add(prefix);\n return;\n }\n if (head === \"*\" || head === \"[*]\") {\n for (const child of children(prefix))\n if (\n !(child.kind === \"target\" && child.generated) &&\n (head === \"*\" || child.name.startsWith(\"[\"))\n )\n match(child.path, tail, true);\n } else match(joinPath(prefix, head), tail, wildcard);\n };\n for (const pattern of patterns) match(\"\", pathSegments(pattern), false);\n return [...leaves];\n };\n\n /** A value as TypeScript shows it: enum members by name, BOOL as a boolean. */\n const decode = (typeId: string, raw: unknown) => {\n const type = typeOfId(typeId);\n if (type.kind === \"enum\")\n return Object.entries(type.members).find(([, value]) => value === raw)?.[0] ?? raw;\n if (type.kind === \"scalar\" && type.iec === \"BOOL\" && typeof raw === \"number\") return raw !== 0;\n return raw;\n };\n\n return { typeOf: typeOfId, resolve, children, isLeaf, expand, decode };\n};\n\nexport type Paths = ReturnType<typeof createPaths>;\n"
6
+ ],
7
+ "mappings": "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",
8
+ "debugId": "9B65CE7419829CA364756E2164756E21",
9
+ "names": []
10
+ }
package/package.json CHANGED
@@ -1,6 +1,48 @@
1
1
  {
2
2
  "name": "@tsplc/debug",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.17",
4
+ "type": "module",
5
+ "license": "MIT",
6
+ "description": "Debugger for tsPLC programs: live TypeScript variables, hardware, async jobs, backend requests and the program's recent telemetry.",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/livingforjesus/tsplc.git",
10
+ "directory": "packages/debug"
11
+ },
12
+ "exports": {
13
+ ".": {
14
+ "types": "./src/index.ts",
15
+ "bun": "./src/index.ts",
16
+ "default": "./dist/index.js"
17
+ },
18
+ "./manifest": {
19
+ "types": "./src/manifest.ts",
20
+ "bun": "./src/manifest.ts",
21
+ "default": "./dist/manifest.js"
22
+ },
23
+ "./format": {
24
+ "types": "./src/ui/format.ts",
25
+ "bun": "./src/ui/format.ts",
26
+ "default": "./dist/format.js"
27
+ },
28
+ "./paths": {
29
+ "types": "./src/paths.ts",
30
+ "bun": "./src/paths.ts",
31
+ "default": "./dist/paths.js"
32
+ }
33
+ },
34
+ "files": [
35
+ "dist",
36
+ "src",
37
+ "!**/*.test.ts"
38
+ ],
39
+ "publishConfig": {
40
+ "access": "public"
41
+ },
42
+ "dependencies": {
43
+ "@tsplc/config": "0.2.17",
44
+ "@tsplc/opcua": "0.2.17",
45
+ "@tsplc/server": "0.2.17",
46
+ "zod": "4.3.6"
47
+ }
48
+ }
package/src/adapter.ts ADDED
@@ -0,0 +1,59 @@
1
+ import type { ConnectionState, Quality } from "./api.ts";
2
+ import type { DebugType } from "./manifest.ts";
3
+
4
+ /** One target's value, before the debugger names it by TypeScript path and decodes it. */
5
+ export interface TargetSample {
6
+ value: unknown;
7
+ quality: Quality;
8
+ status: string;
9
+ receivedAt: string;
10
+ }
11
+
12
+ /** A target to read, with its type in the manifest, which an adapter may need to parse it. */
13
+ export interface TargetRead {
14
+ target: string;
15
+ type: DebugType;
16
+ }
17
+
18
+ const qualityRank: Record<Quality, number> = { good: 0, uncertain: 1, bad: 2, unavailable: 3 };
19
+
20
+ /** The worst of these qualities; nothing read is unavailable. */
21
+ export const worstQuality = (samples: readonly (TargetSample | undefined)[]): Quality =>
22
+ samples.reduce<Quality>((worst, sample) => {
23
+ const quality = sample?.quality ?? "unavailable";
24
+ return qualityRank[quality] > qualityRank[worst] ? quality : worst;
25
+ }, "good");
26
+
27
+ export interface Connection {
28
+ state: ConnectionState;
29
+ since: string;
30
+ error?: string;
31
+ }
32
+
33
+ /**
34
+ * Reads one vendor's controller. Only the adapter interprets `target` strings, which come from
35
+ * the manifest (for CODESYS, IEC symbol paths). It never writes.
36
+ */
37
+ export interface TargetAdapter {
38
+ /** How it reads, for display, such as "engineering login". */
39
+ readonly method: string;
40
+ /** Where it reads, for display. */
41
+ readonly endpoint: string;
42
+ /** How often subscribed targets are read; a value is at most about this old while connected. */
43
+ readonly samplingMs: number;
44
+ connection(): Connection;
45
+ onConnection(listener: (connection: Connection) => void): () => void;
46
+ /** Reads targets now; fails while disconnected. Targets that can't be read come back bad or unavailable. */
47
+ read(targets: readonly TargetRead[]): Promise<Map<string, TargetSample>>;
48
+ /**
49
+ * Calls `listener` with each target's value, about every `intervalMs` (`samplingMs` by
50
+ * default) or on every change, across reconnects, until the returned function is called. While
51
+ * disconnected each target is reported unavailable or bad.
52
+ */
53
+ subscribe(
54
+ targets: readonly TargetRead[],
55
+ listener: (target: string, sample: TargetSample) => void,
56
+ intervalMs?: number,
57
+ ): () => void;
58
+ close(): Promise<void>;
59
+ }
package/src/api.ts ADDED
@@ -0,0 +1,182 @@
1
+ import type { AppRecord } from "@tsplc/server/telemetry";
2
+ import type { DebugManifest, SourceRange } from "./manifest.ts";
3
+
4
+ /**
5
+ * The debugger API's response shapes, shared by the host, the browser UI and the VS Code extension.
6
+ * Routes are listed in docs/design/debugging.md; streams are Server-Sent Events whose event names are
7
+ * the keys of `StreamEvents`.
8
+ */
9
+
10
+ export type Quality = "good" | "uncertain" | "bad" | "unavailable";
11
+
12
+ /** One value as read. Values of different paths may come from different scans. */
13
+ export interface Sample {
14
+ path: string;
15
+ value: unknown;
16
+ quality: Quality;
17
+ status: string;
18
+ /** When the debugger host received it; the controller reports no time of its own. */
19
+ receivedAt: string;
20
+ }
21
+
22
+ /** `unavailable`: nothing to read through, such as no engineering login open. */
23
+ export type ConnectionState = "connecting" | "connected" | "lost" | "failed" | "unavailable";
24
+
25
+ export interface Session {
26
+ build: { manifest: string | null; controller: string | null; matches: boolean };
27
+ connection: { state: ConnectionState; since: string; endpoint: string; error?: string };
28
+ /** How live values are read, such as "engineering login", and how often. */
29
+ reading: { method: string; intervalMs: number };
30
+ /** From the tasks' telemetry batches; `dropped` counts records overwritten before they were sent. */
31
+ tasks: { name: string; scan: number | null; scansPerSecond: number | null; dropped: number }[];
32
+ /** PLC clock minus the app server's clock, estimated from telemetry batches. */
33
+ clock: { offsetMs: number; uncertaintyMs: number } | null;
34
+ release?: { id: string; deployedAt: string; operation: string };
35
+ /** The project directory on the host, when served by `tsplc debug`. */
36
+ projectRoot?: string;
37
+ }
38
+
39
+ /** A trace record, decoded through the manifest's event sites. */
40
+ export interface TraceEvent {
41
+ sequence: number;
42
+ task: string;
43
+ scan: number;
44
+ plcTime: number;
45
+ site: number;
46
+ value: unknown;
47
+ /** The site's guard operands, in the site's order. */
48
+ operands: { text: string; path?: string; stable: boolean; value: unknown }[];
49
+ }
50
+
51
+ export type { AppRecord };
52
+
53
+ export interface RequestTrace {
54
+ /** The server's own sequence number. */
55
+ seq: number;
56
+ service: string;
57
+ /** From `x-tsplc-request: <job id>:<sequence>`. */
58
+ job?: number;
59
+ sequence?: number;
60
+ startedAt: string;
61
+ durationMs?: number;
62
+ status: "pending" | "ok" | "error" | "timeout";
63
+ httpStatus?: number;
64
+ error?: string;
65
+ input?: unknown;
66
+ output?: unknown;
67
+ }
68
+
69
+ /**
70
+ * A job as last seen. `busy` is null until it is known: from the trace's start and finish
71
+ * records, or read through the login while a page shows the job. The rest is read only while it
72
+ * is busy and shown.
73
+ */
74
+ export interface JobState {
75
+ id: number;
76
+ /** Of the `busy` reading; a traced job's is good. */
77
+ quality: Quality;
78
+ busy: boolean | null;
79
+ cancelling?: boolean;
80
+ step?: number;
81
+ current?: { kind: string; range: SourceRange; text: string };
82
+ startedAt?: string;
83
+ elapsedMs?: number;
84
+ request?: { service: string; sequence: number; trace?: RequestTrace };
85
+ /** Busy, but over the number of jobs whose details are read on each poll. */
86
+ unread?: true;
87
+ /** The last failure nothing caught, from the program's `exception` records. */
88
+ failure?: { at: string; code: number; name?: string; source: string };
89
+ }
90
+
91
+ export interface DeviceState {
92
+ id: string;
93
+ /** "unknown" unless the driver's own state was read. */
94
+ health: {
95
+ state: "unknown" | "ok" | "fault" | "stale";
96
+ value?: number;
97
+ name?: string;
98
+ diagnostic?: boolean;
99
+ message?: string;
100
+ quality: Quality;
101
+ };
102
+ assemblies: {
103
+ direction: "input" | "output";
104
+ address?: string;
105
+ bytes: number[] | null;
106
+ quality: Quality;
107
+ }[];
108
+ /** Channels bound to TypeScript I/O paths, by channel id. */
109
+ channels: { id: string; path: string; value: unknown; quality: Quality }[];
110
+ /** The device's hand-configured status symbols, by label. */
111
+ status: Record<string, { value: unknown; quality: Quality }>;
112
+ }
113
+
114
+ /** An entry of the "Recent" view: the server's recent window, merged with the agent's logs. */
115
+ export type TimelineEntry = {
116
+ at: string;
117
+ /** Placed through another clock (the PLC's, or the app server's), so its order is approximate. */
118
+ approximate: boolean;
119
+ source: "plc" | "server" | "agent" | "debugger";
120
+ } & (
121
+ | { kind: "app"; task: string; sequence: number; record: AppRecord }
122
+ | { kind: "trace"; trace: TraceEvent }
123
+ | { kind: "request"; request: RequestTrace }
124
+ | {
125
+ kind: "deploy";
126
+ release: string;
127
+ operation: string;
128
+ build?: string;
129
+ user?: string;
130
+ host?: string;
131
+ }
132
+ | {
133
+ kind: "audit";
134
+ /** As typed after `tsplc`, such as `force` or `reset cold`. */
135
+ command: string;
136
+ /** Expressions written or forced, with their values; unforced expressions map to "". */
137
+ values: Record<string, string>;
138
+ /** The values read just before a write or force. */
139
+ previous?: Record<string, string>;
140
+ user?: string;
141
+ host?: string;
142
+ release?: string;
143
+ error?: string;
144
+ }
145
+ | { kind: "connection"; state: ConnectionState; error?: string }
146
+ );
147
+
148
+ export interface WatchGroups {
149
+ groups: Record<string, string[]>;
150
+ }
151
+
152
+ /**
153
+ * What a page shows, so the host reads only that: `watch` paths, `jobs` by id, the health of
154
+ * `health` devices, and everything of `devices`. Stream query parameters, comma-separated.
155
+ */
156
+ export interface Interest {
157
+ watch: string[];
158
+ jobs: number[];
159
+ health: string[];
160
+ devices: string[];
161
+ }
162
+
163
+ export interface StreamEvents {
164
+ session: Session;
165
+ sample: Sample;
166
+ job: JobState;
167
+ request: RequestTrace;
168
+ device: DeviceState;
169
+ timeline: TimelineEntry;
170
+ }
171
+
172
+ /** Response bodies by route. Errors are `{ error: string }` with a 4xx or 5xx status. */
173
+ export interface ApiResponses {
174
+ "GET /api/session": Session;
175
+ "GET /api/manifest": DebugManifest;
176
+ "GET /api/values": Sample[];
177
+ "GET /api/jobs": JobState[];
178
+ "GET /api/hardware": DeviceState[];
179
+ "GET /api/timeline": TimelineEntry[];
180
+ "GET /api/watch": WatchGroups;
181
+ "PUT /api/watch": WatchGroups;
182
+ }