@theruntime/protocol 0.0.0 → 0.0.1

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.
Files changed (65) hide show
  1. package/LICENSE +21 -0
  2. package/package.json +14 -4
  3. package/src/error.ts +54 -0
  4. package/src/events/index.ts +51 -0
  5. package/src/guidance/index.ts +3 -0
  6. package/src/index.ts +46 -0
  7. package/src/logs/index.ts +46 -0
  8. package/src/notifications/event.ts +62 -0
  9. package/src/notifications.ts +5 -0
  10. package/src/operations/add-component.ts +121 -0
  11. package/src/operations/add-package.ts +58 -0
  12. package/src/operations/app-record.ts +29 -0
  13. package/src/operations/archive-route.ts +48 -0
  14. package/src/operations/capture-snapshot.ts +46 -0
  15. package/src/operations/declaration.ts +115 -0
  16. package/src/operations/declare-route.ts +66 -0
  17. package/src/operations/delete-app.ts +47 -0
  18. package/src/operations/delete-record.ts +46 -0
  19. package/src/operations/describe-route.ts +122 -0
  20. package/src/operations/emit-event.ts +66 -0
  21. package/src/operations/get-record.ts +44 -0
  22. package/src/operations/get-snapshot.ts +64 -0
  23. package/src/operations/guidance.ts +91 -0
  24. package/src/operations/heartbeat.ts +27 -0
  25. package/src/operations/identify.ts +50 -0
  26. package/src/operations/list-packages.ts +72 -0
  27. package/src/operations/list-presence.ts +64 -0
  28. package/src/operations/list-quick-actions.ts +60 -0
  29. package/src/operations/list-records.ts +55 -0
  30. package/src/operations/list-route-logs.ts +98 -0
  31. package/src/operations/list-routes.ts +101 -0
  32. package/src/operations/list-subscribers.ts +75 -0
  33. package/src/operations/list-subscriptions.ts +98 -0
  34. package/src/operations/list-templates.ts +71 -0
  35. package/src/operations/log-line.ts +222 -0
  36. package/src/operations/parse-helpers.ts +136 -0
  37. package/src/operations/query-selection.ts +44 -0
  38. package/src/operations/read-daemon-log.ts +29 -0
  39. package/src/operations/read-route-log.ts +39 -0
  40. package/src/operations/reboot-route.ts +45 -0
  41. package/src/operations/record-status.ts +18 -0
  42. package/src/operations/register-route.ts +135 -0
  43. package/src/operations/save-record.ts +55 -0
  44. package/src/operations/set-record-status.ts +63 -0
  45. package/src/operations/set-route-visibility.ts +48 -0
  46. package/src/operations/set-selection.ts +53 -0
  47. package/src/operations/subscribe.ts +47 -0
  48. package/src/operations/unregister-route.ts +36 -0
  49. package/src/operations/unsubscribe.ts +44 -0
  50. package/src/operations/visibility.ts +9 -0
  51. package/src/operations.ts +39 -0
  52. package/src/payloads/app-ready.ts +24 -0
  53. package/src/payloads/refusal.ts +44 -0
  54. package/src/payloads.ts +11 -0
  55. package/src/quick-actions/index.ts +12 -0
  56. package/src/readiness.ts +52 -0
  57. package/src/records/index.ts +46 -0
  58. package/src/routes/index.ts +104 -0
  59. package/src/selection/index.ts +16 -0
  60. package/src/sessions/index.ts +24 -0
  61. package/src/snapshots/index.ts +20 -0
  62. package/src/templates/index.ts +14 -0
  63. package/src/toolchain/index.ts +36 -0
  64. package/src/version.ts +3 -0
  65. package/README.md +0 -3
@@ -0,0 +1,115 @@
1
+ /** What an agent states about an app's setup. Three closed fields, each parsed against its own
2
+ * set, so a value outside one never reaches the daemon. */
3
+ import { asRecord } from "./parse-helpers";
4
+
5
+ export const HOST_DECLARATIONS = ["connected", "none"] as const;
6
+ export const REACT_DECLARATIONS = ["runtime", "host"] as const;
7
+ export const COMPONENT_DECLARATIONS = ["runtime", "host", "both"] as const;
8
+
9
+ export type HostDeclaration = (typeof HOST_DECLARATIONS)[number];
10
+ export type ReactDeclaration = (typeof REACT_DECLARATIONS)[number];
11
+ export type ComponentDeclaration = (typeof COMPONENT_DECLARATIONS)[number];
12
+
13
+ export interface Declaration {
14
+ readonly host?: HostDeclaration;
15
+ readonly react?: ReactDeclaration;
16
+ readonly components?: ComponentDeclaration;
17
+ }
18
+
19
+ /** The field names a declaration carries, in the order every message about one lists them. */
20
+ export const DECLARATION_FIELDS = ["host", "react", "components"] as const;
21
+
22
+ export type DeclarationField = (typeof DECLARATION_FIELDS)[number];
23
+
24
+ function isMember<Value extends string>(set: readonly Value[], value: unknown): value is Value {
25
+ if (typeof value !== "string") return false;
26
+ const members: readonly string[] = set;
27
+ return members.includes(value);
28
+ }
29
+
30
+ export function isHostDeclaration(value: unknown): value is HostDeclaration {
31
+ return isMember(HOST_DECLARATIONS, value);
32
+ }
33
+
34
+ export function isReactDeclaration(value: unknown): value is ReactDeclaration {
35
+ return isMember(REACT_DECLARATIONS, value);
36
+ }
37
+
38
+ export function isComponentDeclaration(value: unknown): value is ComponentDeclaration {
39
+ return isMember(COMPONENT_DECLARATIONS, value);
40
+ }
41
+
42
+ /**
43
+ * A sparse patch. Each field has three states, so a re-register never blanks one by omission:
44
+ * omitted keeps the stored value, "" clears it, a member of the set sets it.
45
+ */
46
+ export interface DeclarationPatch {
47
+ readonly host?: HostDeclaration | "";
48
+ readonly react?: ReactDeclaration | "";
49
+ readonly components?: ComponentDeclaration | "";
50
+ }
51
+
52
+ function parseField<Value extends string>(
53
+ record: Record<string, unknown>,
54
+ field: DeclarationField,
55
+ set: readonly Value[],
56
+ guard: (value: unknown) => value is Value,
57
+ noun: string,
58
+ ): Value | "" | undefined {
59
+ if (!(field in record) || record[field] === undefined) return undefined;
60
+ const value = record[field];
61
+ if (value === "") return "";
62
+ if (!guard(value)) {
63
+ throw new Error(`${noun}'s ${field} is one of ${set.join(", ")}, or "" to clear it`);
64
+ }
65
+ return value;
66
+ }
67
+
68
+ /** A patch read off a record's own keys. Every field is optional; an empty patch is a patch
69
+ * that writes nothing, and a caller that needs one refused says so itself. */
70
+ export function parseDeclarationPatch(
71
+ record: Record<string, unknown>,
72
+ noun: string,
73
+ ): DeclarationPatch {
74
+ const host = parseField(record, "host", HOST_DECLARATIONS, isHostDeclaration, noun);
75
+ const react = parseField(record, "react", REACT_DECLARATIONS, isReactDeclaration, noun);
76
+ const components = parseField(
77
+ record,
78
+ "components",
79
+ COMPONENT_DECLARATIONS,
80
+ isComponentDeclaration,
81
+ noun,
82
+ );
83
+ return {
84
+ ...(host !== undefined ? { host } : {}),
85
+ ...(react !== undefined ? { react } : {}),
86
+ ...(components !== undefined ? { components } : {}),
87
+ };
88
+ }
89
+
90
+ export function isEmptyDeclarationPatch(patch: DeclarationPatch): boolean {
91
+ return DECLARATION_FIELDS.every((field) => patch[field] === undefined);
92
+ }
93
+
94
+ /** A patch carried as one nested field, and undefined when the field is absent. */
95
+ export function optionalDeclarationPatch(
96
+ record: Record<string, unknown>,
97
+ field: string,
98
+ noun: string,
99
+ ): DeclarationPatch | undefined {
100
+ if (!(field in record) || record[field] === undefined) return undefined;
101
+ return parseDeclarationPatch(asRecord(record[field], `${noun}'s ${field}`), `${noun}'s ${field}`);
102
+ }
103
+
104
+ /** A stored declaration as it is read back. No clearing state here: a cleared field is absent. */
105
+ export function parseDeclaration(value: unknown, noun: string): Declaration {
106
+ const patch = parseDeclarationPatch(asRecord(value, noun), noun);
107
+ const host = patch.host === "" ? undefined : patch.host;
108
+ const react = patch.react === "" ? undefined : patch.react;
109
+ const components = patch.components === "" ? undefined : patch.components;
110
+ return {
111
+ ...(host !== undefined ? { host } : {}),
112
+ ...(react !== undefined ? { react } : {}),
113
+ ...(components !== undefined ? { components } : {}),
114
+ };
115
+ }
@@ -0,0 +1,66 @@
1
+ import { PROTOCOL_VERSION, type ProtocolVersion } from "../version";
2
+ import {
3
+ DECLARATION_FIELDS,
4
+ isEmptyDeclarationPatch,
5
+ parseDeclaration,
6
+ parseDeclarationPatch,
7
+ type Declaration,
8
+ type DeclarationPatch,
9
+ } from "./declaration";
10
+ import {
11
+ asRecord,
12
+ optionalStringArray,
13
+ requireOperation,
14
+ requireProtocol,
15
+ requireString,
16
+ } from "./parse-helpers";
17
+
18
+ const NOUN = "a declareRoute input";
19
+
20
+ export interface DeclareRouteInput extends DeclarationPatch {
21
+ readonly operation: "declareRoute";
22
+ readonly protocol: ProtocolVersion;
23
+ readonly name: string;
24
+ }
25
+
26
+ export function parseDeclareRouteInput(value: unknown): DeclareRouteInput {
27
+ const record = asRecord(value, NOUN);
28
+ requireOperation(record, "declareRoute", NOUN);
29
+ requireProtocol(record, NOUN);
30
+ const name = requireString(record, "name", NOUN);
31
+ const patch = parseDeclarationPatch(record, NOUN);
32
+ if (isEmptyDeclarationPatch(patch)) {
33
+ throw new Error(`${NOUN} changes at least one of ${DECLARATION_FIELDS.join(", ")}`);
34
+ }
35
+ return { operation: "declareRoute", protocol: PROTOCOL_VERSION, name, ...patch };
36
+ }
37
+
38
+ export function serializeDeclareRouteInput(input: DeclareRouteInput): string {
39
+ return JSON.stringify(input);
40
+ }
41
+
42
+ export interface DeclareRouteResult {
43
+ readonly name: string;
44
+ /** The fields this call wrote, which is every field the patch named. */
45
+ readonly wrote: readonly string[];
46
+ /** What the route declares after this call. A cleared field is absent. */
47
+ readonly declaration: Declaration;
48
+ }
49
+
50
+ export function parseDeclareRouteResult(value: unknown): DeclareRouteResult {
51
+ const record = asRecord(value, "a declareRoute result");
52
+ const name = requireString(record, "name", "a declareRoute result");
53
+ const wrote = optionalStringArray(
54
+ record,
55
+ "wrote",
56
+ "a declareRoute result",
57
+ DECLARATION_FIELDS.length,
58
+ );
59
+ if (wrote === undefined) throw new Error("a declareRoute result names the fields it wrote");
60
+ const declaration = parseDeclaration(record.declaration, "a declareRoute result's declaration");
61
+ return { name, wrote, declaration };
62
+ }
63
+
64
+ export function serializeDeclareRouteResult(result: DeclareRouteResult): string {
65
+ return JSON.stringify(result);
66
+ }
@@ -0,0 +1,47 @@
1
+ import { PROTOCOL_VERSION, type ProtocolVersion } from "../version";
2
+ import { asRecord, requireOperation, requireProtocol, requireString } from "./parse-helpers";
3
+
4
+ const NOUN = "a deleteApp input";
5
+
6
+ export interface DeleteAppInput {
7
+ readonly operation: "deleteApp";
8
+ readonly protocol: ProtocolVersion;
9
+ readonly name: string;
10
+ }
11
+
12
+ export function parseDeleteAppInput(value: unknown): DeleteAppInput {
13
+ const record = asRecord(value, NOUN);
14
+ requireOperation(record, "deleteApp", NOUN);
15
+ requireProtocol(record, NOUN);
16
+ const name = requireString(record, "name", NOUN);
17
+ return { operation: "deleteApp", protocol: PROTOCOL_VERSION, name };
18
+ }
19
+
20
+ export function serializeDeleteAppInput(input: DeleteAppInput): string {
21
+ return JSON.stringify(input);
22
+ }
23
+
24
+ export interface DeleteAppResult {
25
+ readonly name: string;
26
+ readonly filesTrashed: boolean;
27
+ readonly recordsDropped: number;
28
+ }
29
+
30
+ export function parseDeleteAppResult(value: unknown): DeleteAppResult {
31
+ const record = asRecord(value, "a deleteApp result");
32
+ const name = requireString(record, "name", "a deleteApp result");
33
+ if (typeof record.filesTrashed !== "boolean")
34
+ throw new Error("a deleteApp result states whether its files were trashed");
35
+ if (
36
+ typeof record.recordsDropped !== "number" ||
37
+ !Number.isInteger(record.recordsDropped) ||
38
+ record.recordsDropped < 0
39
+ ) {
40
+ throw new Error("a deleteApp result counts the records it dropped");
41
+ }
42
+ return { name, filesTrashed: record.filesTrashed, recordsDropped: record.recordsDropped };
43
+ }
44
+
45
+ export function serializeDeleteAppResult(result: DeleteAppResult): string {
46
+ return JSON.stringify(result);
47
+ }
@@ -0,0 +1,46 @@
1
+ import { PROTOCOL_VERSION, type ProtocolVersion } from "../version";
2
+ import {
3
+ asRecord,
4
+ requireOperation,
5
+ requirePositiveInteger,
6
+ requireProtocol,
7
+ requireString,
8
+ } from "./parse-helpers";
9
+
10
+ const NOUN = "a deleteRecord input";
11
+
12
+ export interface DeleteRecordInput {
13
+ readonly operation: "deleteRecord";
14
+ readonly protocol: ProtocolVersion;
15
+ readonly name: string;
16
+ readonly id: number;
17
+ }
18
+
19
+ export function parseDeleteRecordInput(value: unknown): DeleteRecordInput {
20
+ const record = asRecord(value, NOUN);
21
+ requireOperation(record, "deleteRecord", NOUN);
22
+ requireProtocol(record, NOUN);
23
+ const name = requireString(record, "name", NOUN);
24
+ const id = requirePositiveInteger(record, "id", NOUN);
25
+ return { operation: "deleteRecord", protocol: PROTOCOL_VERSION, name, id };
26
+ }
27
+
28
+ export function serializeDeleteRecordInput(input: DeleteRecordInput): string {
29
+ return JSON.stringify(input);
30
+ }
31
+
32
+ export interface DeleteRecordResult {
33
+ readonly name: string;
34
+ readonly id: number;
35
+ }
36
+
37
+ export function parseDeleteRecordResult(value: unknown): DeleteRecordResult {
38
+ const record = asRecord(value, "a deleteRecord result");
39
+ const name = requireString(record, "name", "a deleteRecord result");
40
+ const id = requirePositiveInteger(record, "id", "a deleteRecord result");
41
+ return { name, id };
42
+ }
43
+
44
+ export function serializeDeleteRecordResult(result: DeleteRecordResult): string {
45
+ return JSON.stringify(result);
46
+ }
@@ -0,0 +1,122 @@
1
+ import { PROTOCOL_VERSION, type ProtocolVersion } from "../version";
2
+ import {
3
+ asRecord,
4
+ optionalBoolean,
5
+ optionalString,
6
+ optionalStringArray,
7
+ requireOperation,
8
+ requireProtocol,
9
+ requireString,
10
+ } from "./parse-helpers";
11
+
12
+ const NOUN = "a describeRoute input";
13
+
14
+ // Field caps, as characterized against the previous engine's storage.ts (SUMMARY_MAX, ABOUT_MAX,
15
+ // DISPLAY_NAME_MAX, TAGS_MAX, and the icon column's 8-character trim).
16
+ const DISPLAY_NAME_MAX = 60;
17
+ const SUMMARY_MAX = 140;
18
+ const ABOUT_MAX = 8000;
19
+ const ICON_MAX = 8;
20
+ const TAGS_MAX = 12;
21
+
22
+ export interface DescribeRouteInput {
23
+ readonly operation: "describeRoute";
24
+ readonly protocol: ProtocolVersion;
25
+ readonly name: string;
26
+ readonly displayName?: string;
27
+ readonly summary?: string;
28
+ readonly about?: string;
29
+ readonly icon?: string;
30
+ readonly tags?: readonly string[];
31
+ readonly pinned?: boolean;
32
+ /** The per-route gate on agent-initiated deletion (deleteApp). Carried here rather than as
33
+ * its own operation because it rides the same sparse-patch wire shape; the dispatch seam is
34
+ * what refuses to honor it from an agent session, not this parse. */
35
+ readonly allowAgentDelete?: boolean;
36
+ }
37
+
38
+ export function parseDescribeRouteInput(value: unknown): DescribeRouteInput {
39
+ const record = asRecord(value, NOUN);
40
+ requireOperation(record, "describeRoute", NOUN);
41
+ requireProtocol(record, NOUN);
42
+ const name = requireString(record, "name", NOUN);
43
+ const displayName = optionalString(record, "displayName", NOUN, DISPLAY_NAME_MAX);
44
+ const summary = optionalString(record, "summary", NOUN, SUMMARY_MAX);
45
+ const about = optionalString(record, "about", NOUN, ABOUT_MAX);
46
+ const icon = optionalString(record, "icon", NOUN, ICON_MAX);
47
+ const tags = optionalStringArray(record, "tags", NOUN, TAGS_MAX);
48
+ const pinned = optionalBoolean(record, "pinned", NOUN);
49
+ const allowAgentDelete = optionalBoolean(record, "allowAgentDelete", NOUN);
50
+ if (
51
+ [displayName, summary, about, icon, tags, pinned, allowAgentDelete].every(
52
+ (field) => field === undefined,
53
+ )
54
+ ) {
55
+ throw new Error(
56
+ `${NOUN} changes at least one of displayName, summary, about, icon, tags, pinned, allowAgentDelete`,
57
+ );
58
+ }
59
+ return {
60
+ operation: "describeRoute",
61
+ protocol: PROTOCOL_VERSION,
62
+ name,
63
+ ...(displayName !== undefined ? { displayName } : {}),
64
+ ...(summary !== undefined ? { summary } : {}),
65
+ ...(about !== undefined ? { about } : {}),
66
+ ...(icon !== undefined ? { icon } : {}),
67
+ ...(tags !== undefined ? { tags } : {}),
68
+ ...(pinned !== undefined ? { pinned } : {}),
69
+ ...(allowAgentDelete !== undefined ? { allowAgentDelete } : {}),
70
+ };
71
+ }
72
+
73
+ export function serializeDescribeRouteInput(input: DescribeRouteInput): string {
74
+ return JSON.stringify(input);
75
+ }
76
+
77
+ export interface RouteCard {
78
+ readonly displayName: string;
79
+ readonly summary?: string;
80
+ readonly about?: string;
81
+ readonly icon?: string;
82
+ readonly tags: readonly string[];
83
+ readonly pinned: boolean;
84
+ }
85
+
86
+ function parseRouteCard(value: unknown): RouteCard {
87
+ const record = asRecord(value, "a route card");
88
+ const displayName = requireString(record, "displayName", "a route card");
89
+ const summary = optionalString(record, "summary", "a route card");
90
+ const about = optionalString(record, "about", "a route card");
91
+ const icon = optionalString(record, "icon", "a route card");
92
+ const tags = optionalStringArray(record, "tags", "a route card", TAGS_MAX) ?? [];
93
+ const pinned = optionalBoolean(record, "pinned", "a route card");
94
+ if (pinned === undefined) throw new Error("a route card states whether the route is pinned");
95
+ return {
96
+ displayName,
97
+ ...(summary !== undefined ? { summary } : {}),
98
+ ...(about !== undefined ? { about } : {}),
99
+ ...(icon !== undefined ? { icon } : {}),
100
+ tags,
101
+ pinned,
102
+ };
103
+ }
104
+
105
+ export interface DescribeRouteResult {
106
+ readonly name: string;
107
+ readonly wrote: readonly string[];
108
+ readonly card: RouteCard;
109
+ }
110
+
111
+ export function parseDescribeRouteResult(value: unknown): DescribeRouteResult {
112
+ const record = asRecord(value, "a describeRoute result");
113
+ const name = requireString(record, "name", "a describeRoute result");
114
+ const wrote = optionalStringArray(record, "wrote", "a describeRoute result", 7);
115
+ if (wrote === undefined) throw new Error("a describeRoute result names the fields it wrote");
116
+ const card = parseRouteCard(record.card);
117
+ return { name, wrote, card };
118
+ }
119
+
120
+ export function serializeDescribeRouteResult(result: DescribeRouteResult): string {
121
+ return JSON.stringify(result);
122
+ }
@@ -0,0 +1,66 @@
1
+ import { PROTOCOL_VERSION, type ProtocolVersion } from "../version";
2
+ import {
3
+ asRecord,
4
+ optionalPositiveInteger,
5
+ requireOperation,
6
+ requireProtocol,
7
+ requireString,
8
+ } from "./parse-helpers";
9
+
10
+ const NOUN = "an emitEvent input";
11
+
12
+ export interface EmitEventInput {
13
+ readonly operation: "emitEvent";
14
+ readonly protocol: ProtocolVersion;
15
+ readonly name: string;
16
+ readonly action: string;
17
+ // Payload is JSON, validated at the boundary.
18
+ readonly payload: unknown;
19
+ }
20
+
21
+ export function parseEmitEventInput(value: unknown): EmitEventInput {
22
+ const record = asRecord(value, NOUN);
23
+ requireOperation(record, "emitEvent", NOUN);
24
+ requireProtocol(record, NOUN);
25
+ const name = requireString(record, "name", NOUN);
26
+ const action = requireString(record, "action", NOUN);
27
+ if (!("payload" in record)) throw new Error(`${NOUN} carries a payload`);
28
+ return {
29
+ operation: "emitEvent",
30
+ protocol: PROTOCOL_VERSION,
31
+ name,
32
+ action,
33
+ payload: record.payload,
34
+ };
35
+ }
36
+
37
+ export function serializeEmitEventInput(input: EmitEventInput): string {
38
+ return JSON.stringify(input);
39
+ }
40
+
41
+ export interface EmitEventResult {
42
+ readonly name: string;
43
+ readonly action: string;
44
+ /** Absent for an action the daemon takes as a report, which it answers without logging. */
45
+ readonly id?: number;
46
+ readonly delivered: number;
47
+ }
48
+
49
+ export function parseEmitEventResult(value: unknown): EmitEventResult {
50
+ const record = asRecord(value, "an emitEvent result");
51
+ const name = requireString(record, "name", "an emitEvent result");
52
+ const action = requireString(record, "action", "an emitEvent result");
53
+ const id = optionalPositiveInteger(record, "id", "an emitEvent result");
54
+ if (
55
+ typeof record.delivered !== "number" ||
56
+ !Number.isInteger(record.delivered) ||
57
+ record.delivered < 0
58
+ ) {
59
+ throw new Error("an emitEvent result counts how many subscribers it delivered to");
60
+ }
61
+ return { name, action, ...(id !== undefined ? { id } : {}), delivered: record.delivered };
62
+ }
63
+
64
+ export function serializeEmitEventResult(result: EmitEventResult): string {
65
+ return JSON.stringify(result);
66
+ }
@@ -0,0 +1,44 @@
1
+ import { PROTOCOL_VERSION, type ProtocolVersion } from "../version";
2
+ import { parseAppRecord, type AppRecord } from "./app-record";
3
+ import {
4
+ asRecord,
5
+ requireOperation,
6
+ requirePositiveInteger,
7
+ requireProtocol,
8
+ requireString,
9
+ } from "./parse-helpers";
10
+
11
+ const NOUN = "a getRecord input";
12
+
13
+ export interface GetRecordInput {
14
+ readonly operation: "getRecord";
15
+ readonly protocol: ProtocolVersion;
16
+ readonly name: string;
17
+ readonly id: number;
18
+ }
19
+
20
+ export function parseGetRecordInput(value: unknown): GetRecordInput {
21
+ const record = asRecord(value, NOUN);
22
+ requireOperation(record, "getRecord", NOUN);
23
+ requireProtocol(record, NOUN);
24
+ const name = requireString(record, "name", NOUN);
25
+ const id = requirePositiveInteger(record, "id", NOUN);
26
+ return { operation: "getRecord", protocol: PROTOCOL_VERSION, name, id };
27
+ }
28
+
29
+ export function serializeGetRecordInput(input: GetRecordInput): string {
30
+ return JSON.stringify(input);
31
+ }
32
+
33
+ export interface GetRecordResult {
34
+ readonly record: AppRecord;
35
+ }
36
+
37
+ export function parseGetRecordResult(value: unknown): GetRecordResult {
38
+ const record = asRecord(value, "a getRecord result");
39
+ return { record: parseAppRecord(record.record) };
40
+ }
41
+
42
+ export function serializeGetRecordResult(result: GetRecordResult): string {
43
+ return JSON.stringify(result);
44
+ }
@@ -0,0 +1,64 @@
1
+ import { PROTOCOL_VERSION, type ProtocolVersion } from "../version";
2
+ import {
3
+ asRecord,
4
+ requireNonNegativeInteger,
5
+ requireOperation,
6
+ requireProtocol,
7
+ requireString,
8
+ } from "./parse-helpers";
9
+ import { isVisibility, type Visibility } from "./visibility";
10
+
11
+ const NOUN = "a getSnapshot input";
12
+
13
+ export interface GetSnapshotInput {
14
+ readonly operation: "getSnapshot";
15
+ readonly protocol: ProtocolVersion;
16
+ readonly name: string;
17
+ }
18
+
19
+ export function parseGetSnapshotInput(value: unknown): GetSnapshotInput {
20
+ const record = asRecord(value, NOUN);
21
+ requireOperation(record, "getSnapshot", NOUN);
22
+ requireProtocol(record, NOUN);
23
+ const name = requireString(record, "name", NOUN);
24
+ return { operation: "getSnapshot", protocol: PROTOCOL_VERSION, name };
25
+ }
26
+
27
+ export function serializeGetSnapshotInput(input: GetSnapshotInput): string {
28
+ return JSON.stringify(input);
29
+ }
30
+
31
+ export interface SnapshotState {
32
+ readonly generatedAt: string;
33
+ readonly files: number;
34
+ readonly bytes: number;
35
+ }
36
+
37
+ export interface GetSnapshotResult {
38
+ readonly name: string;
39
+ readonly visibility: Visibility;
40
+ readonly snapshot: SnapshotState | null;
41
+ }
42
+
43
+ function parseSnapshotState(value: unknown, noun: string): SnapshotState | null {
44
+ if (value === null) return null;
45
+ const record = asRecord(value, `${noun}'s snapshot`);
46
+ const generatedAt = requireString(record, "generatedAt", `${noun}'s snapshot`);
47
+ const files = requireNonNegativeInteger(record, "files", `${noun}'s snapshot`);
48
+ const bytes = requireNonNegativeInteger(record, "bytes", `${noun}'s snapshot`);
49
+ return { generatedAt, files, bytes };
50
+ }
51
+
52
+ export function parseGetSnapshotResult(value: unknown): GetSnapshotResult {
53
+ const record = asRecord(value, "a getSnapshot result");
54
+ const name = requireString(record, "name", "a getSnapshot result");
55
+ if (!isVisibility(record.visibility))
56
+ throw new Error("a getSnapshot result names a known visibility");
57
+ if (!("snapshot" in record)) throw new Error("a getSnapshot result states its snapshot");
58
+ const snapshot = parseSnapshotState(record.snapshot, "a getSnapshot result");
59
+ return { name, visibility: record.visibility, snapshot };
60
+ }
61
+
62
+ export function serializeGetSnapshotResult(result: GetSnapshotResult): string {
63
+ return JSON.stringify(result);
64
+ }
@@ -0,0 +1,91 @@
1
+ import {
2
+ asRecord,
3
+ optionalString,
4
+ optionalStringArray,
5
+ requireNonNegativeInteger,
6
+ requireString,
7
+ } from "./parse-helpers";
8
+
9
+ const NOUN = "a guidance block";
10
+ const REGISTER_NOUN = "a guidance register";
11
+ const LIBRARY_NOUN = "a guidance register library";
12
+ const SECTION_MAX = 500;
13
+
14
+ /** The design register the daemon found above an app: where the bundle is, and which of its
15
+ * artifacts exist. Absent fields are artifacts the bundle does not carry. */
16
+ export interface Register {
17
+ /** The bundle directory, or the directory holding a single-file pin. */
18
+ readonly dir: string;
19
+ /** The document that states the register. */
20
+ readonly anchor: string;
21
+ /** A stylesheet carrying the register's token values. Copied into an app as its tokens.css. */
22
+ readonly theme?: string;
23
+ /** A stylesheet carrying the register's faces. Copied into an app as its fonts.css. */
24
+ readonly fonts?: string;
25
+ /** The canonical reference image. */
26
+ readonly reference?: string;
27
+ /** The exemplar ids, each pairing a document, an image, and the source it names. */
28
+ readonly exemplars?: readonly string[];
29
+ /** The signed-off component library and how many components it holds. */
30
+ readonly library?: { readonly dir: string; readonly count: number };
31
+ }
32
+
33
+ /** What an operation result teaches the calling session: short text plus sections the daemon
34
+ * computes from what is installed at call time. */
35
+ export interface Guidance {
36
+ readonly text: string;
37
+ /** Every bare id an app may import, the same union listPackages reports. */
38
+ readonly packages?: readonly string[];
39
+ /** The subpath ids the components package's own exports map names. */
40
+ readonly components?: readonly string[];
41
+ /** The entry files this call wrote. Present iff the call scaffolded at least one. */
42
+ readonly scaffolded?: readonly string[];
43
+ /** The design register above the app. Present iff a walk up from the app found one. */
44
+ readonly register?: Register;
45
+ }
46
+
47
+ function parseLibrary(value: unknown): NonNullable<Register["library"]> {
48
+ const record = asRecord(value, LIBRARY_NOUN);
49
+ return {
50
+ dir: requireString(record, "dir", LIBRARY_NOUN),
51
+ count: requireNonNegativeInteger(record, "count", LIBRARY_NOUN),
52
+ };
53
+ }
54
+
55
+ export function parseRegister(value: unknown): Register {
56
+ const record = asRecord(value, REGISTER_NOUN);
57
+ const theme = optionalString(record, "theme", REGISTER_NOUN, SECTION_MAX);
58
+ const fonts = optionalString(record, "fonts", REGISTER_NOUN, SECTION_MAX);
59
+ const reference = optionalString(record, "reference", REGISTER_NOUN, SECTION_MAX);
60
+ const exemplars = optionalStringArray(record, "exemplars", REGISTER_NOUN, SECTION_MAX);
61
+ const library = record.library === undefined ? undefined : parseLibrary(record.library);
62
+ return {
63
+ dir: requireString(record, "dir", REGISTER_NOUN),
64
+ anchor: requireString(record, "anchor", REGISTER_NOUN),
65
+ ...(theme !== undefined ? { theme } : {}),
66
+ ...(fonts !== undefined ? { fonts } : {}),
67
+ ...(reference !== undefined ? { reference } : {}),
68
+ ...(exemplars !== undefined ? { exemplars } : {}),
69
+ ...(library !== undefined ? { library } : {}),
70
+ };
71
+ }
72
+
73
+ export function parseGuidance(value: unknown): Guidance {
74
+ const record = asRecord(value, NOUN);
75
+ const text = requireString(record, "text", NOUN);
76
+ const packages = optionalStringArray(record, "packages", NOUN, SECTION_MAX);
77
+ const components = optionalStringArray(record, "components", NOUN, SECTION_MAX);
78
+ const scaffolded = optionalStringArray(record, "scaffolded", NOUN, SECTION_MAX);
79
+ const register = record.register === undefined ? undefined : parseRegister(record.register);
80
+ return {
81
+ text,
82
+ ...(packages !== undefined ? { packages } : {}),
83
+ ...(components !== undefined ? { components } : {}),
84
+ ...(scaffolded !== undefined ? { scaffolded } : {}),
85
+ ...(register !== undefined ? { register } : {}),
86
+ };
87
+ }
88
+
89
+ export function serializeGuidance(guidance: Guidance): string {
90
+ return JSON.stringify(guidance);
91
+ }