@procedures/peer 0.0.0-stage → 0.1.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.
Files changed (135) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/LICENSE +21 -0
  3. package/README.md +725 -2
  4. package/dist/.tsbuildinfo +1 -0
  5. package/dist/connection/index.d.ts +45 -0
  6. package/dist/connection/index.d.ts.map +1 -0
  7. package/dist/connection/index.js +148 -0
  8. package/dist/connection/index.js.map +1 -0
  9. package/dist/discovery/index.d.ts +30 -0
  10. package/dist/discovery/index.d.ts.map +1 -0
  11. package/dist/discovery/index.js +101 -0
  12. package/dist/discovery/index.js.map +1 -0
  13. package/dist/expose.d.ts +14 -0
  14. package/dist/expose.d.ts.map +1 -0
  15. package/dist/expose.js +16 -0
  16. package/dist/expose.js.map +1 -0
  17. package/dist/index.d.ts +15 -0
  18. package/dist/index.d.ts.map +1 -0
  19. package/dist/index.js +20 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/input-schemas.d.ts +21 -0
  22. package/dist/input-schemas.d.ts.map +1 -0
  23. package/dist/input-schemas.js +80 -0
  24. package/dist/input-schemas.js.map +1 -0
  25. package/dist/lockfile.d.ts +48 -0
  26. package/dist/lockfile.d.ts.map +1 -0
  27. package/dist/lockfile.js +115 -0
  28. package/dist/lockfile.js.map +1 -0
  29. package/dist/manifest/index.d.ts +8 -0
  30. package/dist/manifest/index.d.ts.map +1 -0
  31. package/dist/manifest/index.js +8 -0
  32. package/dist/manifest/index.js.map +1 -0
  33. package/dist/manifest/json.d.ts +27 -0
  34. package/dist/manifest/json.d.ts.map +1 -0
  35. package/dist/manifest/json.js +24 -0
  36. package/dist/manifest/json.js.map +1 -0
  37. package/dist/manifest/typescript.d.ts +17 -0
  38. package/dist/manifest/typescript.d.ts.map +1 -0
  39. package/dist/manifest/typescript.js +123 -0
  40. package/dist/manifest/typescript.js.map +1 -0
  41. package/dist/module.d.ts +20 -0
  42. package/dist/module.d.ts.map +1 -0
  43. package/dist/module.js +42 -0
  44. package/dist/module.js.map +1 -0
  45. package/dist/output-schemas.d.ts +21 -0
  46. package/dist/output-schemas.d.ts.map +1 -0
  47. package/dist/output-schemas.js +85 -0
  48. package/dist/output-schemas.js.map +1 -0
  49. package/dist/peer/index.d.ts +47 -0
  50. package/dist/peer/index.d.ts.map +1 -0
  51. package/dist/peer/index.js +234 -0
  52. package/dist/peer/index.js.map +1 -0
  53. package/dist/peer/security.d.ts +25 -0
  54. package/dist/peer/security.d.ts.map +1 -0
  55. package/dist/peer/security.js +58 -0
  56. package/dist/peer/security.js.map +1 -0
  57. package/dist/procedures/index.d.ts +24 -0
  58. package/dist/procedures/index.d.ts.map +1 -0
  59. package/dist/procedures/index.js +15 -0
  60. package/dist/procedures/index.js.map +1 -0
  61. package/dist/procedures/manifest.generate.d.ts +11 -0
  62. package/dist/procedures/manifest.generate.d.ts.map +1 -0
  63. package/dist/procedures/manifest.generate.js +97 -0
  64. package/dist/procedures/manifest.generate.js.map +1 -0
  65. package/dist/procedures/server.call.d.ts +30 -0
  66. package/dist/procedures/server.call.d.ts.map +1 -0
  67. package/dist/procedures/server.call.js +60 -0
  68. package/dist/procedures/server.call.js.map +1 -0
  69. package/dist/procedures/server.connect.d.ts +11 -0
  70. package/dist/procedures/server.connect.d.ts.map +1 -0
  71. package/dist/procedures/server.connect.js +35 -0
  72. package/dist/procedures/server.connect.js.map +1 -0
  73. package/dist/procedures/server.connections.d.ts +22 -0
  74. package/dist/procedures/server.connections.d.ts.map +1 -0
  75. package/dist/procedures/server.connections.js +38 -0
  76. package/dist/procedures/server.connections.js.map +1 -0
  77. package/dist/procedures/server.create.d.ts +20 -0
  78. package/dist/procedures/server.create.d.ts.map +1 -0
  79. package/dist/procedures/server.create.js +64 -0
  80. package/dist/procedures/server.create.js.map +1 -0
  81. package/dist/procedures/server.disconnect.d.ts +11 -0
  82. package/dist/procedures/server.disconnect.d.ts.map +1 -0
  83. package/dist/procedures/server.disconnect.js +22 -0
  84. package/dist/procedures/server.disconnect.js.map +1 -0
  85. package/dist/procedures/server.start.d.ts +13 -0
  86. package/dist/procedures/server.start.d.ts.map +1 -0
  87. package/dist/procedures/server.start.js +179 -0
  88. package/dist/procedures/server.start.js.map +1 -0
  89. package/dist/procedures/server.status.d.ts +12 -0
  90. package/dist/procedures/server.status.d.ts.map +1 -0
  91. package/dist/procedures/server.status.js +116 -0
  92. package/dist/procedures/server.status.js.map +1 -0
  93. package/dist/procedures/server.stop.d.ts +11 -0
  94. package/dist/procedures/server.stop.d.ts.map +1 -0
  95. package/dist/procedures/server.stop.js +96 -0
  96. package/dist/procedures/server.stop.js.map +1 -0
  97. package/dist/register.d.ts +8 -0
  98. package/dist/register.d.ts.map +1 -0
  99. package/dist/register.js +14 -0
  100. package/dist/register.js.map +1 -0
  101. package/dist/schema.d.ts +14 -0
  102. package/dist/schema.d.ts.map +1 -0
  103. package/dist/schema.js +14 -0
  104. package/dist/schema.js.map +1 -0
  105. package/dist/types.d.ts +192 -0
  106. package/dist/types.d.ts.map +1 -0
  107. package/dist/types.js +5 -0
  108. package/dist/types.js.map +1 -0
  109. package/package.json +87 -3
  110. package/src/connection/index.ts +219 -0
  111. package/src/discovery/index.ts +137 -0
  112. package/src/expose.ts +17 -0
  113. package/src/index.ts +69 -0
  114. package/src/input-schemas.ts +121 -0
  115. package/src/lockfile.ts +129 -0
  116. package/src/manifest/index.ts +8 -0
  117. package/src/manifest/json.ts +39 -0
  118. package/src/manifest/typescript.ts +148 -0
  119. package/src/module.ts +46 -0
  120. package/src/output-schemas.ts +110 -0
  121. package/src/peer/index.ts +325 -0
  122. package/src/peer/security.ts +58 -0
  123. package/src/procedures/index.ts +32 -0
  124. package/src/procedures/manifest.generate.ts +113 -0
  125. package/src/procedures/server.call.ts +96 -0
  126. package/src/procedures/server.connect.ts +45 -0
  127. package/src/procedures/server.connections.ts +64 -0
  128. package/src/procedures/server.create.ts +84 -0
  129. package/src/procedures/server.disconnect.ts +31 -0
  130. package/src/procedures/server.start.ts +209 -0
  131. package/src/procedures/server.status.ts +139 -0
  132. package/src/procedures/server.stop.ts +121 -0
  133. package/src/register.ts +17 -0
  134. package/src/schema.ts +15 -0
  135. package/src/types.ts +257 -0
package/src/index.ts ADDED
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Client Server
3
+ *
4
+ * Transport-agnostic peer for bidirectional RPC.
5
+ * Exposes procedures and generates manifests.
6
+ */
7
+
8
+ // Types
9
+ export type {
10
+ TransportType,
11
+ TransportConfig,
12
+ HttpTransportConfig,
13
+ WebSocketTransportConfig,
14
+ LocalTransportConfig,
15
+ ServerCreateInput,
16
+ ServerCreateOutput,
17
+ ServerConnectInput,
18
+ ServerConnectOutput,
19
+ ServerDisconnectInput,
20
+ ServerDisconnectOutput,
21
+ ManifestGenerateInput,
22
+ ManifestGenerateOutput,
23
+ ProcedureInfo,
24
+ PeerConnection,
25
+ ProcedureAnnounceMessage,
26
+ ProcedureUpdateMessage,
27
+ } from "./types.js";
28
+
29
+ // Peer
30
+ export { createPeer, type Peer, type PeerOptions, type PeerEndpoint } from "./peer/index.js";
31
+ export { exposeRule } from "./expose.js";
32
+
33
+ // Procedures
34
+ export {
35
+ serverCreateProcedure,
36
+ getPeer,
37
+ getActivePeerIds,
38
+ serverConnectProcedure,
39
+ serverDisconnectProcedure,
40
+ manifestGenerateProcedure,
41
+ } from "./procedures/index.js";
42
+
43
+ // Manifest generation
44
+ export {
45
+ generateTypeScript,
46
+ generateJson,
47
+ generateJsonString,
48
+ type ProcedureManifest,
49
+ } from "./manifest/index.js";
50
+
51
+ // Connection
52
+ export {
53
+ connect,
54
+ disconnect,
55
+ getConnection,
56
+ getConnectionIds,
57
+ type Connection,
58
+ type ConnectOptions,
59
+ } from "./connection/index.js";
60
+
61
+ // Discovery
62
+ export {
63
+ discoveryAnnounceProcedure,
64
+ getProcedureInfos,
65
+ watchProcedures,
66
+ } from "./discovery/index.js";
67
+
68
+ // Registration
69
+ export { procedures, registerServerProcedures } from "./register.js";
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Input schemas of the `server.*` and `manifest.*` procedures.
3
+ *
4
+ * Before, these procedures used pass-through schemas: `mark server start --help` listed no
5
+ * options, and a port such as "abc" reached the handler. Each schema now validates its input,
6
+ * and the CLI help shows the fields.
7
+ */
8
+
9
+ import { z } from "zod";
10
+ import { zodAdapter, type ZodLikeSchema } from "@procedures/client";
11
+ import type {
12
+ ManifestGenerateInput,
13
+ ServerConnectInput,
14
+ ServerCreateInput,
15
+ ServerDisconnectInput,
16
+ ServerStartInput,
17
+ ServerStatusInput,
18
+ ServerStopInput,
19
+ } from "./types.js";
20
+ import type { ServerCallInput } from "./procedures/server.call.js";
21
+ import type { ServerConnectionsInput } from "./procedures/server.connections.js";
22
+
23
+ const port = z.number().int().min(1).max(65535);
24
+
25
+ const httpTransport = z.object({
26
+ type: z.literal("http"),
27
+ port: port.optional(),
28
+ host: z.string().min(1).optional(),
29
+ basePath: z.string().optional(),
30
+ cors: z.boolean().optional(),
31
+ corsOrigins: z.array(z.string()).optional(),
32
+ urlStrategy: z.enum(["rest", "rpc"]).optional(),
33
+ token: z.string().min(1).optional(),
34
+ });
35
+
36
+ const webSocketTransport = z.object({
37
+ type: z.literal("websocket"),
38
+ port: port.optional(),
39
+ host: z.string().min(1).optional(),
40
+ path: z.string().optional(),
41
+ origins: z.array(z.string()).optional(),
42
+ token: z.string().min(1).optional(),
43
+ });
44
+
45
+ const localTransport = z.object({ type: z.literal("local") });
46
+
47
+ export const serverCreateInputSchema: ZodLikeSchema<ServerCreateInput> = zodAdapter<ServerCreateInput>(
48
+ z.object({
49
+ transports: z
50
+ .array(z.discriminatedUnion("type", [httpTransport, webSocketTransport, localTransport]))
51
+ .optional()
52
+ .describe("The transports to start (default: HTTP on port 3000)"),
53
+ autoRegister: z.boolean().optional().describe("Serve every procedure of the registry (default: true)"),
54
+ token: z.string().min(1).optional().describe("A secret that every request must send"),
55
+ expose: z
56
+ .array(z.string().min(1))
57
+ .optional()
58
+ .describe("The procedures to serve: exact keys (git.status) or prefixes (git.*). Default: every procedure"),
59
+ })
60
+ );
61
+
62
+ export const serverStartInputSchema: ZodLikeSchema<ServerStartInput> = zodAdapter<ServerStartInput>(
63
+ z.object({
64
+ port: port.optional().describe("The port to listen on (default: 3000)"),
65
+ host: z.string().min(1).optional().describe("The host to bind to (default: 127.0.0.1)"),
66
+ transport: z.enum(["http", "websocket", "both"]).optional().describe("The transport (default: http)"),
67
+ })
68
+ );
69
+
70
+ export const serverStopInputSchema: ZodLikeSchema<ServerStopInput> = zodAdapter<ServerStopInput>(
71
+ z.object({
72
+ port: port.optional().describe("The port of the server to stop (default: all servers)"),
73
+ force: z.boolean().optional().describe("Stop with SIGKILL instead of SIGTERM"),
74
+ })
75
+ );
76
+
77
+ export const serverStatusInputSchema: ZodLikeSchema<ServerStatusInput> = zodAdapter<ServerStatusInput>(
78
+ z.object({
79
+ port: port.optional().describe("The port of the server (default: all servers)"),
80
+ })
81
+ );
82
+
83
+ export const serverConnectInputSchema: ZodLikeSchema<ServerConnectInput> = zodAdapter<ServerConnectInput>(
84
+ z.object({
85
+ address: z.string().min(1).describe("The address of the remote peer"),
86
+ transport: z.enum(["http", "websocket", "local"]).optional().describe("The transport to use"),
87
+ timeout: z.number().int().positive().optional().describe("The connection timeout in milliseconds"),
88
+ })
89
+ );
90
+
91
+ export const serverDisconnectInputSchema: ZodLikeSchema<ServerDisconnectInput> = zodAdapter<ServerDisconnectInput>(
92
+ z.object({
93
+ connectionId: z.string().min(1).describe("The connection id from server connect"),
94
+ })
95
+ );
96
+
97
+ export const serverCallInputSchema: ZodLikeSchema<ServerCallInput> = zodAdapter<ServerCallInput>(
98
+ z
99
+ .object({
100
+ connectionId: z.string().min(1).optional().describe("The connection id from server connect"),
101
+ address: z.string().min(1).optional().describe("The address of a peer, for a one-time connection"),
102
+ path: z
103
+ .union([z.string().min(1), z.array(z.string().min(1)).min(1)])
104
+ .describe("The procedure path on the peer: dot-separated text or a list"),
105
+ input: z.unknown().optional().describe("The input of the remote procedure"),
106
+ })
107
+ .refine((input) => input.connectionId !== undefined || input.address !== undefined, {
108
+ message: "Give connectionId or address",
109
+ })
110
+ );
111
+
112
+ export const serverConnectionsInputSchema: ZodLikeSchema<ServerConnectionsInput> =
113
+ zodAdapter<ServerConnectionsInput>(z.object({}));
114
+
115
+ export const manifestGenerateInputSchema: ZodLikeSchema<ManifestGenerateInput> = zodAdapter<ManifestGenerateInput>(
116
+ z.object({
117
+ formats: z.array(z.enum(["json", "typescript"])).optional().describe("The manifest formats (default: json)"),
118
+ namespace: z.array(z.string()).optional().describe("Only the procedures under this path"),
119
+ outputDir: z.string().optional().describe("The folder for the manifest files"),
120
+ })
121
+ );
@@ -0,0 +1,129 @@
1
+ /**
2
+ * The lockfiles of the CLI servers: one module for `mark` and the `server.*` procedures.
3
+ *
4
+ * `mark --server` writes `~/.mark/servers/<port>.lock` with its token, the folder it runs in,
5
+ * the build of `mark` and its peer id. Before, five copies of this logic had drifted, and a
6
+ * server counted as alive when its PID existed: Windows reuses PIDs, so a stale lockfile broke
7
+ * every `mark` command and `server stop` killed an unrelated process (deep dive CLI-4). Now a
8
+ * server is alive only when its health endpoint answers with the peer id of its lockfile.
9
+ */
10
+
11
+ import * as fs from "node:fs";
12
+ import * as os from "node:os";
13
+ import * as path from "node:path";
14
+
15
+ export interface LockfileData {
16
+ pid: number;
17
+ port: number;
18
+ transport: string;
19
+ endpoint: string;
20
+ startedAt: string;
21
+ /** The secret that every request must send (`Authorization: Bearer <token>`). An older lockfile has none. */
22
+ token?: string;
23
+ /** The folder the server runs in: relative paths and default `cwd` values resolve there */
24
+ cwd?: string;
25
+ /** The build of `mark` that runs the server */
26
+ build?: string;
27
+ /** The peer id that the server's health endpoint answers */
28
+ peerId?: string;
29
+ }
30
+
31
+ const MARK_DIR = path.join(os.homedir(), ".mark");
32
+ const SERVERS_DIR = path.join(MARK_DIR, "servers");
33
+ // The single lockfile of older versions: it is read only to remove it
34
+ const LEGACY_LOCKFILE_PATH = path.join(MARK_DIR, "server.lock");
35
+
36
+ export function getMarkDir(): string {
37
+ return MARK_DIR;
38
+ }
39
+
40
+ export function getServersDir(): string {
41
+ return SERVERS_DIR;
42
+ }
43
+
44
+ function lockfilePath(port: number): string {
45
+ return path.join(SERVERS_DIR, `${port}.lock`);
46
+ }
47
+
48
+ /** This function writes a lockfile that only its user can read (it holds the token). */
49
+ export function writeLockfile(data: LockfileData): void {
50
+ fs.mkdirSync(SERVERS_DIR, { recursive: true });
51
+ fs.writeFileSync(lockfilePath(data.port), JSON.stringify(data, null, 2), { mode: 0o600 });
52
+ }
53
+
54
+ export function readLockfileForPort(port: number): LockfileData | null {
55
+ try {
56
+ return JSON.parse(fs.readFileSync(lockfilePath(port), "utf-8")) as LockfileData;
57
+ } catch {
58
+ return null;
59
+ }
60
+ }
61
+
62
+ export function readAllLockfiles(): LockfileData[] {
63
+ const results: LockfileData[] = [];
64
+ let files: string[] = [];
65
+ try {
66
+ files = fs.readdirSync(SERVERS_DIR);
67
+ } catch {
68
+ return results; // The folder does not exist yet
69
+ }
70
+ for (const file of files) {
71
+ if (!file.endsWith(".lock")) continue;
72
+ try {
73
+ results.push(JSON.parse(fs.readFileSync(path.join(SERVERS_DIR, file), "utf-8")) as LockfileData);
74
+ } catch {
75
+ // Skip a corrupt lockfile
76
+ }
77
+ }
78
+ return results;
79
+ }
80
+
81
+ export function removeLockfileForPort(port: number): void {
82
+ try {
83
+ fs.unlinkSync(lockfilePath(port));
84
+ } catch {
85
+ // Already gone
86
+ }
87
+ try {
88
+ const legacy = JSON.parse(fs.readFileSync(LEGACY_LOCKFILE_PATH, "utf-8")) as LockfileData;
89
+ if (legacy.port === port) fs.unlinkSync(LEGACY_LOCKFILE_PATH);
90
+ } catch {
91
+ // No legacy lockfile
92
+ }
93
+ }
94
+
95
+ /**
96
+ * The state of a lockfile's server:
97
+ * - "alive": the health endpoint answers with the lockfile's peer id;
98
+ * - "dead": nothing answers (the lockfile is stale);
99
+ * - "other": something else answers on the port (another server, or a lockfile without a peer id).
100
+ */
101
+ export async function checkServer(lockfile: LockfileData, timeoutMs = 1000): Promise<"alive" | "dead" | "other"> {
102
+ let response: Response;
103
+ try {
104
+ response = await fetch(`${lockfile.endpoint}/health`, { signal: AbortSignal.timeout(timeoutMs) });
105
+ } catch {
106
+ return "dead";
107
+ }
108
+ try {
109
+ const health = (await response.json()) as { peerId?: unknown };
110
+ return lockfile.peerId !== undefined && health.peerId === lockfile.peerId ? "alive" : "other";
111
+ } catch {
112
+ return "other";
113
+ }
114
+ }
115
+
116
+ /**
117
+ * The lockfile of a running server that this caller can use: it has a token, it runs in `cwd`
118
+ * with the build `build`, and it answers as the peer of its lockfile. A dead server's lockfile
119
+ * is removed.
120
+ */
121
+ export async function findServer(match: { cwd: string; build: string }): Promise<LockfileData | null> {
122
+ for (const lockfile of readAllLockfiles()) {
123
+ if (!lockfile.token || lockfile.cwd !== match.cwd || lockfile.build !== match.build) continue;
124
+ const state = await checkServer(lockfile);
125
+ if (state === "alive") return lockfile;
126
+ if (state === "dead") removeLockfileForPort(lockfile.port);
127
+ }
128
+ return null;
129
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Manifest Generation Module
3
+ *
4
+ * Generates procedure manifests in various formats.
5
+ */
6
+
7
+ export { generateTypeScript } from "./typescript.js";
8
+ export { generateJson, generateJsonString, type ProcedureManifest } from "./json.js";
@@ -0,0 +1,39 @@
1
+ /**
2
+ * JSON Manifest Generator
3
+ *
4
+ * Generates JSON manifests from procedure definitions
5
+ * for runtime discovery and visualization.
6
+ */
7
+
8
+ import type { ProcedureInfo } from "../types.js";
9
+
10
+ /**
11
+ * JSON manifest structure
12
+ */
13
+ export interface ProcedureManifest {
14
+ /** Manifest version */
15
+ version: string;
16
+ /** Generation timestamp */
17
+ generatedAt: string;
18
+ /** All procedures */
19
+ procedures: ProcedureInfo[];
20
+ }
21
+
22
+ /**
23
+ * Generate JSON manifest
24
+ */
25
+ export function generateJson(procedures: ProcedureInfo[]): ProcedureManifest {
26
+ return {
27
+ version: "1.0.0",
28
+ generatedAt: new Date().toISOString(),
29
+ procedures,
30
+ };
31
+ }
32
+
33
+ /**
34
+ * Generate JSON string (formatted)
35
+ */
36
+ export function generateJsonString(procedures: ProcedureInfo[]): string {
37
+ const manifest = generateJson(procedures);
38
+ return JSON.stringify(manifest, null, 2);
39
+ }
@@ -0,0 +1,148 @@
1
+ /**
2
+ * TypeScript Declaration Generator
3
+ *
4
+ * Generates .d.ts files from procedure definitions
5
+ * for static type checking on clients.
6
+ */
7
+
8
+ import type { ProcedureInfo } from "../types.js";
9
+
10
+ /**
11
+ * Generate TypeScript declaration file content
12
+ */
13
+ export function generateTypeScript(procedures: ProcedureInfo[]): string {
14
+ const lines: string[] = [
15
+ "/**",
16
+ " * Auto-generated procedure type declarations",
17
+ ` * Generated at: ${new Date().toISOString()}`,
18
+ " */",
19
+ "",
20
+ ];
21
+
22
+ // Group procedures by namespace (first path segment)
23
+ const namespaces = new Map<string, ProcedureInfo[]>();
24
+
25
+ for (const proc of procedures) {
26
+ const namespace = proc.path[0] ?? "root";
27
+ const existing = namespaces.get(namespace) ?? [];
28
+ existing.push(proc);
29
+ namespaces.set(namespace, existing);
30
+ }
31
+
32
+ // Generate interface for each namespace
33
+ for (const [namespace, procs] of namespaces) {
34
+ lines.push(`export interface ${pascalCase(namespace)}Procedures {`);
35
+
36
+ for (const proc of procs) {
37
+ const path = proc.path.slice(1);
38
+ const methodName = path.join("_") || namespace;
39
+
40
+ if (proc.description) {
41
+ lines.push(` /** ${proc.description} */`);
42
+ }
43
+
44
+ // The types come from the JSON Schemas; a procedure without one has `unknown`
45
+ lines.push(` ${camelCase(methodName)}: {`);
46
+ lines.push(` path: ${JSON.stringify(proc.path)};`);
47
+ lines.push(` input: ${jsonSchemaToTs(proc.inputSchema, " ")};`);
48
+ lines.push(` output: ${jsonSchemaToTs(proc.outputSchema, " ")};`);
49
+ lines.push(` };`);
50
+ }
51
+
52
+ lines.push(`}`);
53
+ lines.push("");
54
+ }
55
+
56
+ // Generate combined type
57
+ lines.push("export interface RemoteProcedures {");
58
+ for (const namespace of namespaces.keys()) {
59
+ lines.push(` ${camelCase(namespace)}: ${pascalCase(namespace)}Procedures;`);
60
+ }
61
+ lines.push("}");
62
+ lines.push("");
63
+
64
+ return lines.join("\n");
65
+ }
66
+
67
+ /** A JSON Schema node, as the manifest stores it. */
68
+ interface JsonNode {
69
+ type?: string | string[];
70
+ enum?: unknown[];
71
+ const?: unknown;
72
+ items?: JsonNode | JsonNode[];
73
+ properties?: Record<string, JsonNode>;
74
+ required?: string[];
75
+ additionalProperties?: boolean | JsonNode;
76
+ anyOf?: JsonNode[];
77
+ oneOf?: JsonNode[];
78
+ allOf?: JsonNode[];
79
+ }
80
+
81
+ /** A property name as TypeScript writes it: quoted when it is not an identifier. */
82
+ function propertyName(name: string): string {
83
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) ? name : JSON.stringify(name);
84
+ }
85
+
86
+ /**
87
+ * The TypeScript type of a JSON Schema: objects, arrays, records, enums, constants and unions.
88
+ * A schema that it cannot read is `unknown`.
89
+ */
90
+ export function jsonSchemaToTs(schema: unknown, indent = ""): string {
91
+ if (!schema || typeof schema !== "object") return "unknown";
92
+ const node = schema as JsonNode;
93
+ const union = node.anyOf ?? node.oneOf;
94
+ if (union?.length) return union.map((member) => jsonSchemaToTs(member, indent)).join(" | ");
95
+ if (node.allOf?.length) return node.allOf.map((member) => jsonSchemaToTs(member, indent)).join(" & ");
96
+ if (Array.isArray(node.enum)) return node.enum.map((value) => JSON.stringify(value)).join(" | ") || "never";
97
+ if (node.const !== undefined) return JSON.stringify(node.const);
98
+ const types = Array.isArray(node.type) ? node.type : node.type ? [node.type] : [];
99
+ if (types.length > 1) return types.map((type) => jsonSchemaToTs({ ...node, type }, indent)).join(" | ");
100
+ switch (types[0]) {
101
+ case "string":
102
+ return "string";
103
+ case "number":
104
+ case "integer":
105
+ return "number";
106
+ case "boolean":
107
+ return "boolean";
108
+ case "null":
109
+ return "null";
110
+ case "array":
111
+ return Array.isArray(node.items)
112
+ ? `[${node.items.map((part) => jsonSchemaToTs(part, indent)).join(", ")}]`
113
+ : `Array<${jsonSchemaToTs(node.items, indent)}>`;
114
+ case "object": {
115
+ const properties = Object.entries(node.properties ?? {});
116
+ const extra = node.additionalProperties;
117
+ if (properties.length === 0) {
118
+ return extra && typeof extra === "object" ? `Record<string, ${jsonSchemaToTs(extra, indent)}>` : "Record<string, unknown>";
119
+ }
120
+ const required = new Set(node.required ?? []);
121
+ const inner = `${indent} `;
122
+ const members = properties.map(
123
+ ([name, value]) => `${inner}${propertyName(name)}${required.has(name) ? "" : "?"}: ${jsonSchemaToTs(value, inner)};`
124
+ );
125
+ return `{\n${members.join("\n")}\n${indent}}`;
126
+ }
127
+ default:
128
+ return "unknown";
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Convert string to PascalCase
134
+ */
135
+ function pascalCase(str: string): string {
136
+ return str
137
+ .split(/[-_\s]+/)
138
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
139
+ .join("");
140
+ }
141
+
142
+ /**
143
+ * Convert string to camelCase
144
+ */
145
+ function camelCase(str: string): string {
146
+ const pascal = pascalCase(str);
147
+ return pascal.charAt(0).toLowerCase() + pascal.slice(1);
148
+ }
package/src/module.ts ADDED
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Module (no side effects): the procedures of this package as data, for `createRegistry()`.
3
+ *
4
+ * Procedure Registration
5
+ *
6
+ * Registers server procedures with the client system.
7
+ * This file is referenced by package.json's client.procedures field.
8
+ */
9
+
10
+ import { type AnyProcedure, defineModule, type ProcedureModule } from "@procedures/client";
11
+
12
+ // Import all procedures
13
+ import { serverCreateProcedure } from "./procedures/server.create.js";
14
+ import { serverConnectProcedure } from "./procedures/server.connect.js";
15
+ import { serverDisconnectProcedure } from "./procedures/server.disconnect.js";
16
+ import { manifestGenerateProcedure } from "./procedures/manifest.generate.js";
17
+ import { discoveryAnnounceProcedure } from "./discovery/index.js";
18
+ import { serverStartProcedure } from "./procedures/server.start.js";
19
+ import { serverStopProcedure } from "./procedures/server.stop.js";
20
+ import { serverStatusProcedure } from "./procedures/server.status.js";
21
+ import { serverCallProcedure } from "./procedures/server.call.js";
22
+ import { serverConnectionsProcedure } from "./procedures/server.connections.js";
23
+
24
+ /**
25
+ * All server procedures
26
+ */
27
+ export const procedures: readonly AnyProcedure[] = [
28
+ serverCreateProcedure,
29
+ serverConnectProcedure,
30
+ serverDisconnectProcedure,
31
+ manifestGenerateProcedure,
32
+ discoveryAnnounceProcedure,
33
+ serverStartProcedure,
34
+ serverStopProcedure,
35
+ serverStatusProcedure,
36
+ serverCallProcedure,
37
+ serverConnectionsProcedure,
38
+ ];
39
+
40
+ /**
41
+ * The module of this package, for `createRegistry()`: its procedures as data. Importing this
42
+ * file registers nothing. (`register.ts` registers the same procedures in the global registry.)
43
+ */
44
+ export const serverModule: ProcedureModule = defineModule("@procedures/peer", [...procedures]);
45
+
46
+ export default serverModule;
@@ -0,0 +1,110 @@
1
+ /**
2
+ * The output schemas of the server.* and manifest.* procedures (roadmap 2.1).
3
+ *
4
+ * Loose objects: a field that a schema does not name stays in the result (a strict schema would
5
+ * remove it when the output is validated). Each schema is a `z.ZodType` of its output type, so
6
+ * the compiler checks it against the type.
7
+ */
8
+
9
+ import { z } from "zod";
10
+ import type {
11
+ ManifestGenerateOutput,
12
+ ServerConnectOutput,
13
+ ServerCreateOutput,
14
+ ServerDisconnectOutput,
15
+ ServerStartOutput,
16
+ ServerStatusOutput,
17
+ ServerStopOutput,
18
+ } from "./types.js";
19
+ import type { ServerCallOutput } from "./procedures/server.call.js";
20
+ import type { ServerConnectionsOutput } from "./procedures/server.connections.js";
21
+
22
+ const procedureInfo = z.looseObject({
23
+ path: z.array(z.string()).describe("The procedure path"),
24
+ description: z.string().optional().describe("The description of the procedure"),
25
+ inputSchema: z.record(z.string(), z.unknown()).optional().describe("The JSON Schema of the input"),
26
+ outputSchema: z.record(z.string(), z.unknown()).optional().describe("The JSON Schema of the output"),
27
+ });
28
+
29
+ export const ServerCreateOutputSchema: z.ZodType<ServerCreateOutput> = z.looseObject({
30
+ serverId: z.string().describe("The id of the server: server.stop takes it"),
31
+ endpoints: z
32
+ .array(
33
+ z.looseObject({
34
+ type: z.enum(["http", "websocket", "local"]).describe("The transport"),
35
+ address: z.string().describe("The address of the endpoint"),
36
+ })
37
+ )
38
+ .describe("The endpoints of the server"),
39
+ procedureCount: z.number().describe("The number of procedures that the server serves"),
40
+ });
41
+
42
+ export const ServerConnectOutputSchema: z.ZodType<ServerConnectOutput> = z.looseObject({
43
+ connectionId: z.string().describe("The id of the connection: server.call and server.disconnect take it"),
44
+ remotePeerId: z.string().describe("The id of the remote peer"),
45
+ remoteProcedures: z.array(procedureInfo).describe("The procedures of the remote peer"),
46
+ });
47
+
48
+ export const ServerDisconnectOutputSchema: z.ZodType<ServerDisconnectOutput> = z.looseObject({
49
+ success: z.boolean().describe("True when the connection closed"),
50
+ });
51
+
52
+ export const ManifestGenerateOutputSchema: z.ZodType<ManifestGenerateOutput> = z.looseObject({
53
+ json: z
54
+ .looseObject({
55
+ procedures: z.array(procedureInfo).describe("The procedures with their JSON Schemas"),
56
+ generatedAt: z.string().describe("The time of the manifest (ISO 8601)"),
57
+ version: z.string().describe("The version of the manifest format"),
58
+ })
59
+ .optional()
60
+ .describe("The JSON manifest, for the json format"),
61
+ typescript: z.string().optional().describe("The TypeScript declarations, for the typescript format"),
62
+ filesWritten: z.array(z.string()).optional().describe("The files that the call wrote, with outputDir"),
63
+ });
64
+
65
+ export const ServerStartOutputSchema: z.ZodType<ServerStartOutput> = z.looseObject({
66
+ success: z.boolean().describe("True when the server started"),
67
+ pid: z.number().optional().describe("The process id of the server"),
68
+ port: z.number().optional().describe("The port of the server"),
69
+ endpoint: z.string().optional().describe("The URL of the server"),
70
+ logFile: z.string().optional().describe("The log file of the server"),
71
+ message: z.string().describe("A message about the result"),
72
+ });
73
+
74
+ export const ServerStopOutputSchema: z.ZodType<ServerStopOutput> = z.looseObject({
75
+ success: z.boolean().describe("True when the server stopped"),
76
+ message: z.string().describe("A message about the result"),
77
+ });
78
+
79
+ export const ServerStatusOutputSchema: z.ZodType<ServerStatusOutput> = z.looseObject({
80
+ running: z.boolean().describe("True when a server runs"),
81
+ pid: z.number().optional().describe("The process id of the server"),
82
+ port: z.number().optional().describe("The port of the server"),
83
+ endpoint: z.string().optional().describe("The URL of the server"),
84
+ transport: z.string().optional().describe("The transport of the server"),
85
+ startedAt: z.string().optional().describe("The start time (ISO 8601)"),
86
+ uptime: z.string().optional().describe("The time since the start, as text"),
87
+ logFile: z.string().optional().describe("The log file of the server"),
88
+ message: z.string().describe("A message about the result"),
89
+ });
90
+
91
+ export const ServerCallOutputSchema: z.ZodType<ServerCallOutput> = z.looseObject({
92
+ result: z.unknown().describe("The result of the remote procedure"),
93
+ connectionId: z.string().describe("The connection that the call used"),
94
+ path: z.array(z.string()).describe("The path of the remote procedure"),
95
+ });
96
+
97
+ export const ServerConnectionsOutputSchema: z.ZodType<ServerConnectionsOutput> = z.looseObject({
98
+ connections: z
99
+ .array(
100
+ z.looseObject({
101
+ connectionId: z.string().describe("The id of the connection"),
102
+ remotePeerId: z.string().describe("The id of the remote peer"),
103
+ transport: z.string().describe("The transport of the connection"),
104
+ state: z.string().describe("The state: connecting, connected, disconnected or error"),
105
+ remoteProcedureCount: z.number().describe("The number of procedures of the remote peer"),
106
+ connectedAt: z.string().optional().describe("The time of the connection (ISO 8601)"),
107
+ })
108
+ )
109
+ .describe("The open connections"),
110
+ });