@procedures/peer 0.0.0-stage → 0.2.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 +25 -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 +31 -0
  10. package/dist/discovery/index.d.ts.map +1 -0
  11. package/dist/discovery/index.js +102 -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 +26 -0
  42. package/dist/module.d.ts.map +1 -0
  43. package/dist/module.js +50 -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 +12 -0
  62. package/dist/procedures/manifest.generate.d.ts.map +1 -0
  63. package/dist/procedures/manifest.generate.js +101 -0
  64. package/dist/procedures/manifest.generate.js.map +1 -0
  65. package/dist/procedures/server.call.d.ts +31 -0
  66. package/dist/procedures/server.call.d.ts.map +1 -0
  67. package/dist/procedures/server.call.js +62 -0
  68. package/dist/procedures/server.call.js.map +1 -0
  69. package/dist/procedures/server.connect.d.ts +12 -0
  70. package/dist/procedures/server.connect.d.ts.map +1 -0
  71. package/dist/procedures/server.connect.js +40 -0
  72. package/dist/procedures/server.connect.js.map +1 -0
  73. package/dist/procedures/server.connections.d.ts +23 -0
  74. package/dist/procedures/server.connections.d.ts.map +1 -0
  75. package/dist/procedures/server.connections.js +40 -0
  76. package/dist/procedures/server.connections.js.map +1 -0
  77. package/dist/procedures/server.create.d.ts +21 -0
  78. package/dist/procedures/server.create.d.ts.map +1 -0
  79. package/dist/procedures/server.create.js +68 -0
  80. package/dist/procedures/server.create.js.map +1 -0
  81. package/dist/procedures/server.disconnect.d.ts +12 -0
  82. package/dist/procedures/server.disconnect.d.ts.map +1 -0
  83. package/dist/procedures/server.disconnect.js +26 -0
  84. package/dist/procedures/server.disconnect.js.map +1 -0
  85. package/dist/procedures/server.start.d.ts +14 -0
  86. package/dist/procedures/server.start.d.ts.map +1 -0
  87. package/dist/procedures/server.start.js +182 -0
  88. package/dist/procedures/server.start.js.map +1 -0
  89. package/dist/procedures/server.status.d.ts +13 -0
  90. package/dist/procedures/server.status.d.ts.map +1 -0
  91. package/dist/procedures/server.status.js +118 -0
  92. package/dist/procedures/server.status.js.map +1 -0
  93. package/dist/procedures/server.stop.d.ts +12 -0
  94. package/dist/procedures/server.stop.d.ts.map +1 -0
  95. package/dist/procedures/server.stop.js +98 -0
  96. package/dist/procedures/server.stop.js.map +1 -0
  97. package/dist/register.d.ts +7 -0
  98. package/dist/register.d.ts.map +1 -0
  99. package/dist/register.js +9 -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 +90 -3
  110. package/src/connection/index.ts +219 -0
  111. package/src/discovery/index.ts +140 -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 +55 -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 +117 -0
  125. package/src/procedures/server.call.ts +98 -0
  126. package/src/procedures/server.connect.ts +50 -0
  127. package/src/procedures/server.connections.ts +66 -0
  128. package/src/procedures/server.create.ts +88 -0
  129. package/src/procedures/server.disconnect.ts +35 -0
  130. package/src/procedures/server.start.ts +212 -0
  131. package/src/procedures/server.status.ts +141 -0
  132. package/src/procedures/server.stop.ts +123 -0
  133. package/src/register.ts +11 -0
  134. package/src/schema.ts +15 -0
  135. package/src/types.ts +257 -0
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Procedure: server.connections
3
+ * List all outbound connections to remote peers
4
+ */
5
+
6
+ import { createProcedure, type Effects, type Procedure } from "@procedures/client";
7
+ import { wrapSchema } from "../schema.js";
8
+ import { ServerConnectionsOutputSchema } from "../output-schemas.js";
9
+ import { serverConnectionsInputSchema } from "../input-schemas.js";
10
+ import { getConnectionIds, getConnection } from "../connection/index.js";
11
+
12
+ // =============================================================================
13
+ // Types
14
+ // =============================================================================
15
+
16
+ export interface ServerConnectionsInput {
17
+ // No input required
18
+ }
19
+
20
+ export interface ServerConnectionsOutput {
21
+ connections: Array<{
22
+ connectionId: string;
23
+ remotePeerId: string;
24
+ transport: string;
25
+ state: string;
26
+ remoteProcedureCount: number;
27
+ connectedAt?: string | undefined;
28
+ }>;
29
+ }
30
+
31
+ // =============================================================================
32
+ // Procedure
33
+ // =============================================================================
34
+
35
+ const serverConnectionsOutputSchema = wrapSchema<ServerConnectionsOutput>(ServerConnectionsOutputSchema);
36
+
37
+ export const serverConnectionsProcedure: Procedure<
38
+ ServerConnectionsInput,
39
+ ServerConnectionsOutput,
40
+ { description: string; effects: Effects; output: string }
41
+ > = createProcedure()
42
+ .path(["server", "connections"])
43
+ .input(serverConnectionsInputSchema)
44
+ .output(serverConnectionsOutputSchema)
45
+ .meta({
46
+ description: "List outbound connections to remote peers",
47
+ // It reads the connections that server.connect opened, and sends nothing
48
+ effects: { reads: ["network"], writes: [] },
49
+ output: "json",
50
+ })
51
+ .handler(async (): Promise<ServerConnectionsOutput> => {
52
+ const ids = getConnectionIds();
53
+ const connections = ids.map((id) => {
54
+ const conn = getConnection(id)!;
55
+ return {
56
+ connectionId: conn.id,
57
+ remotePeerId: conn.remote.remotePeerId,
58
+ transport: conn.remote.transport,
59
+ state: conn.remote.state,
60
+ remoteProcedureCount: conn.remote.remoteProcedures.length,
61
+ connectedAt: conn.remote.connectedAt?.toISOString(),
62
+ };
63
+ });
64
+ return { connections };
65
+ })
66
+ .build();
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Procedure: server.create
3
+ * Creates a transport-agnostic peer that exposes procedures
4
+ */
5
+
6
+ import { createProcedure, type Effects, type Procedure, PROCEDURE_REGISTRY } from "@procedures/client";
7
+ import { wrapSchema } from "../schema.js";
8
+ import { ServerCreateOutputSchema } from "../output-schemas.js";
9
+ import { serverCreateInputSchema } from "../input-schemas.js";
10
+ import type { ServerCreateInput, ServerCreateOutput } from "../types.js";
11
+ import { createPeer, type Peer } from "../peer/index.js";
12
+ import { exposeRule } from "../expose.js";
13
+
14
+ // Use pass-through schemas for simplicity (validation happens at Zod level if needed)
15
+ const serverCreateOutputSchema = wrapSchema<ServerCreateOutput>(ServerCreateOutputSchema);
16
+
17
+ // Active peers (module-level state for now, could be in context)
18
+ const activePeers = new Map<string, Peer>();
19
+
20
+ export const serverCreateProcedure: Procedure<
21
+ ServerCreateInput,
22
+ ServerCreateOutput,
23
+ { description: string; effects: Effects }
24
+ > = createProcedure()
25
+ .path(["server", "create"])
26
+ .input(serverCreateInputSchema)
27
+ .output(serverCreateOutputSchema)
28
+ .meta({
29
+ description: "Create a transport-agnostic peer that exposes procedures",
30
+ // Each call starts a new peer that listens on its transports. It replaces nothing.
31
+ effects: { reads: ["registry"], writes: ["network", "server"], destructive: false },
32
+ })
33
+ .handler(async (input: ServerCreateInput) => {
34
+ // Generate unique server ID
35
+ const serverId = `peer-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
36
+
37
+ // Default transports if not specified
38
+ // A server-wide token goes to each transport that has none of its own
39
+ const transports = (input.transports ?? [{ type: "http" as const, port: 3000 }]).map((transport) =>
40
+ transport.type === "local" || input.token === undefined || transport.token !== undefined
41
+ ? transport
42
+ : { ...transport, token: input.token }
43
+ );
44
+ const autoRegister = input.autoRegister ?? true;
45
+
46
+ // Create peer with configured transports
47
+ const peer = await createPeer({
48
+ id: serverId,
49
+ transports,
50
+ autoRegister,
51
+ registry: PROCEDURE_REGISTRY,
52
+ ...(input.expose ? { expose: exposeRule(input.expose) } : {}),
53
+ });
54
+
55
+ // Start the peer
56
+ await peer.start();
57
+
58
+ // Store for later reference
59
+ activePeers.set(serverId, peer);
60
+
61
+ // Get procedure count
62
+ const procedureCount = input.autoRegister
63
+ ? PROCEDURE_REGISTRY.getAll().filter((p) => p.handler).length
64
+ : 0;
65
+
66
+ return {
67
+ serverId,
68
+ endpoints: peer.getEndpoints(),
69
+ procedureCount,
70
+ };
71
+ })
72
+ .build();
73
+
74
+ /**
75
+ * Get an active peer by ID
76
+ */
77
+ export function getPeer(serverId: string): Peer | undefined {
78
+ return activePeers.get(serverId);
79
+ }
80
+
81
+ /**
82
+ * Get all active peer IDs
83
+ */
84
+ export function getActivePeerIds(): string[] {
85
+ return Array.from(activePeers.keys());
86
+ }
87
+
88
+ export type { ServerCreateInput, ServerCreateOutput };
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Procedure: server.disconnect
3
+ * Disconnects from a remote peer
4
+ */
5
+
6
+ import { createProcedure, type Effects, type Procedure } from "@procedures/client";
7
+ import { wrapSchema } from "../schema.js";
8
+ import { ServerDisconnectOutputSchema } from "../output-schemas.js";
9
+ import { serverDisconnectInputSchema } from "../input-schemas.js";
10
+ import type { ServerDisconnectInput, ServerDisconnectOutput } from "../types.js";
11
+ import { disconnect } from "../connection/index.js";
12
+
13
+ // Use pass-through schemas
14
+ const serverDisconnectOutputSchema = wrapSchema<ServerDisconnectOutput>(ServerDisconnectOutputSchema);
15
+
16
+ export const serverDisconnectProcedure: Procedure<
17
+ ServerDisconnectInput,
18
+ ServerDisconnectOutput,
19
+ { description: string; effects: Effects }
20
+ > = createProcedure()
21
+ .path(["server", "disconnect"])
22
+ .input(serverDisconnectInputSchema)
23
+ .output(serverDisconnectOutputSchema)
24
+ .meta({
25
+ description: "Disconnect from a remote peer",
26
+ effects: { reads: ["network"], writes: ["network"], idempotent: true },
27
+ args: ["connectionId"],
28
+ })
29
+ .handler(async (input: ServerDisconnectInput) => {
30
+ const success = await disconnect(input.connectionId);
31
+ return { success };
32
+ })
33
+ .build();
34
+
35
+ export type { ServerDisconnectInput, ServerDisconnectOutput };
@@ -0,0 +1,212 @@
1
+ /**
2
+ * Procedure: server.start
3
+ * Starts the CLI server as a background daemon
4
+ */
5
+
6
+ import { spawn } from "node:child_process";
7
+ import * as fs from "node:fs";
8
+ import * as path from "node:path";
9
+ import * as os from "node:os";
10
+ import { createProcedure, type Effects, type Procedure } from "@procedures/client";
11
+ import { wrapSchema } from "../schema.js";
12
+ import { ServerStartOutputSchema } from "../output-schemas.js";
13
+ import { serverStartInputSchema } from "../input-schemas.js";
14
+ import { checkServer, readLockfileForPort } from "../lockfile.js";
15
+ import type { ServerStartInput, ServerStartOutput } from "../types.js";
16
+
17
+ const MARK_DIR = path.join(os.homedir(), ".mark");
18
+ const SERVERS_DIR = path.join(MARK_DIR, "servers");
19
+ const LOG_PATH = path.join(MARK_DIR, "server.log");
20
+ const LOG_PREV_PATH = path.join(MARK_DIR, "server.log.1");
21
+
22
+ const serverStartOutputSchema = wrapSchema<ServerStartOutput>(ServerStartOutputSchema);
23
+
24
+
25
+ /**
26
+ * Check if process is alive
27
+ */
28
+ function isProcessAlive(pid: number): boolean {
29
+ try {
30
+ process.kill(pid, 0);
31
+ return true;
32
+ } catch {
33
+ return false;
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Rotate log file
39
+ */
40
+ function rotateLog(): void {
41
+ try {
42
+ fs.mkdirSync(MARK_DIR, { recursive: true });
43
+ if (fs.existsSync(LOG_PATH)) {
44
+ fs.renameSync(LOG_PATH, LOG_PREV_PATH);
45
+ }
46
+ } catch {
47
+ // Ignore rotation errors
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Find the CLI entry point
53
+ */
54
+ function findCliPath(): string {
55
+ // First, try to use the currently running script if it's the CLI
56
+ const currentScript = process.argv[1];
57
+ if (currentScript) {
58
+ // Normalize path for cross-platform check
59
+ const normalized = currentScript.replace(/\\/g, "/");
60
+ // mark runs as dist/bin.js (its bin) or dist/cli.js
61
+ if (normalized.endsWith("/dist/bin.js") || normalized.endsWith("cli.js") || normalized.includes("cli/dist")) {
62
+ // Return absolute path
63
+ return path.resolve(currentScript);
64
+ }
65
+ }
66
+
67
+ // Try to find the mark CLI in node_modules or as a direct path
68
+ const candidates = [
69
+ path.join(process.cwd(), "node_modules", "@procedures", "mark", "dist", "cli.js"),
70
+ path.join(process.cwd(), "node_modules", ".bin", "mark"),
71
+ // Check parent directories for monorepo setups
72
+ path.join(process.cwd(), "..", "cli", "dist", "cli.js"),
73
+ // Direct check in current directory
74
+ path.join(process.cwd(), "dist", "cli.js"),
75
+ ];
76
+
77
+ // Check which candidate exists
78
+ for (const candidate of candidates) {
79
+ if (fs.existsSync(candidate)) {
80
+ return candidate;
81
+ }
82
+ }
83
+
84
+ // Fallback: try mark in PATH (will fail if not installed globally)
85
+ return "mark";
86
+ }
87
+
88
+ export const serverStartProcedure: Procedure<
89
+ ServerStartInput,
90
+ ServerStartOutput,
91
+ { description: string; effects: Effects; args: string[]; shorts: Record<string, string> }
92
+ > = createProcedure()
93
+ .path(["server", "start"])
94
+ .input(serverStartInputSchema)
95
+ .output(serverStartOutputSchema)
96
+ .meta({
97
+ description: "Start CLI server as background daemon",
98
+ // The daemon loads the procedure packages of the workspace. The procedure moves server.log
99
+ // over server.log.1.
100
+ effects: { reads: ["fs", "server"], writes: ["fs", "network", "process", "server"] },
101
+ args: [],
102
+ shorts: { port: "p", host: "h" },
103
+ })
104
+ .handler(async (input: ServerStartInput): Promise<ServerStartOutput> => {
105
+ const port = input.port ?? 3000;
106
+ // Loopback by default; pass host: "0.0.0.0" to expose on the LAN deliberately.
107
+ // See documentation/BUGS-2026-07.md (H19).
108
+ const host = input.host ?? "127.0.0.1";
109
+ const transport = input.transport ?? "http";
110
+
111
+ // Check if server is already running on this port
112
+ const existing = readLockfileForPort(port);
113
+ if (existing && (await checkServer(existing)) === "alive") {
114
+ return {
115
+ success: false,
116
+ message: `Server already running on port ${port} (PID ${existing.pid})`,
117
+ };
118
+ }
119
+
120
+ // Rotate log file
121
+ rotateLog();
122
+
123
+ // Ensure directories exist
124
+ fs.mkdirSync(SERVERS_DIR, { recursive: true });
125
+
126
+ // Open log file for writing
127
+ const logFd = fs.openSync(LOG_PATH, "a");
128
+
129
+ // Find CLI path
130
+ const cliPath = findCliPath();
131
+
132
+ // Build arguments
133
+ const args = ["--server", "--port", String(port), "--host", host];
134
+ if (transport !== "http") {
135
+ args.push("--transport", transport);
136
+ }
137
+
138
+ // Determine how to spawn
139
+ let spawnCmd: string;
140
+ let spawnArgs: string[];
141
+
142
+ if (cliPath === "mark" || cliPath.endsWith(".bin/mark") || cliPath.endsWith(".bin\\mark")) {
143
+ // Use mark command directly
144
+ spawnCmd = cliPath;
145
+ spawnArgs = args;
146
+ } else {
147
+ // Use node to run the JS file
148
+ spawnCmd = process.execPath;
149
+ spawnArgs = [cliPath, ...args];
150
+ }
151
+
152
+ // Spawn daemon process
153
+ const child = spawn(spawnCmd, spawnArgs, {
154
+ detached: true,
155
+ stdio: ["ignore", logFd, logFd],
156
+ env: { ...process.env },
157
+ cwd: process.cwd(),
158
+ // No console window on Windows (deep dive CLI-16)
159
+ windowsHide: true,
160
+ });
161
+
162
+ // Unref so parent can exit
163
+ child.unref();
164
+ fs.closeSync(logFd);
165
+
166
+ // Poll for lockfile (server takes ~4-8s to load ecosystem)
167
+ const maxWait = 15000;
168
+ const pollInterval = 500;
169
+ const startTime = Date.now();
170
+
171
+ while (Date.now() - startTime < maxWait) {
172
+ await new Promise((resolve) => setTimeout(resolve, pollInterval));
173
+
174
+ // Check if child died
175
+ if (child.pid && !isProcessAlive(child.pid)) {
176
+ break;
177
+ }
178
+
179
+ // Check for lockfile
180
+ const lockfile = readLockfileForPort(port);
181
+ if (lockfile && (await checkServer(lockfile)) === "alive") {
182
+ return {
183
+ success: true,
184
+ pid: lockfile.pid,
185
+ port: lockfile.port,
186
+ endpoint: lockfile.endpoint,
187
+ logFile: LOG_PATH,
188
+ message: `Server started (PID ${lockfile.pid})`,
189
+ };
190
+ }
191
+ }
192
+
193
+ // Timed out waiting for lockfile, but child may still be alive
194
+ if (child.pid && isProcessAlive(child.pid)) {
195
+ return {
196
+ success: true,
197
+ pid: child.pid,
198
+ port,
199
+ endpoint: `http://${host === "0.0.0.0" ? "localhost" : host}:${port}/api`,
200
+ logFile: LOG_PATH,
201
+ message: `Server starting (PID ${child.pid}, still loading...)`,
202
+ };
203
+ }
204
+
205
+ return {
206
+ success: false,
207
+ message: `Failed to start server. Check ${LOG_PATH} for details.`,
208
+ };
209
+ })
210
+ .build();
211
+
212
+ export type { ServerStartInput, ServerStartOutput };
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Procedure: server.status
3
+ * Gets the current CLI server status
4
+ */
5
+
6
+ import * as path from "node:path";
7
+ import * as os from "node:os";
8
+ import { createProcedure, type Effects, type Procedure } from "@procedures/client";
9
+ import { wrapSchema } from "../schema.js";
10
+ import { ServerStatusOutputSchema } from "../output-schemas.js";
11
+ import { serverStatusInputSchema } from "../input-schemas.js";
12
+ import { checkServer, readAllLockfiles, readLockfileForPort, removeLockfileForPort, type LockfileData } from "../lockfile.js";
13
+ import type { ServerStatusInput, ServerStatusOutput } from "../types.js";
14
+
15
+ const MARK_DIR = path.join(os.homedir(), ".mark");
16
+ const LOG_PATH = path.join(MARK_DIR, "server.log");
17
+
18
+ const serverStatusOutputSchema = wrapSchema<ServerStatusOutput>(ServerStatusOutputSchema);
19
+
20
+
21
+
22
+
23
+
24
+ /**
25
+ * Format uptime as human-readable string
26
+ */
27
+ function formatUptime(startedAt: string): string {
28
+ const start = new Date(startedAt).getTime();
29
+ const now = Date.now();
30
+ const diff = Math.floor((now - start) / 1000);
31
+
32
+ if (diff < 60) {
33
+ return `${diff}s`;
34
+ }
35
+ if (diff < 3600) {
36
+ const mins = Math.floor(diff / 60);
37
+ const secs = diff % 60;
38
+ return `${mins}m ${secs}s`;
39
+ }
40
+ if (diff < 86400) {
41
+ const hours = Math.floor(diff / 3600);
42
+ const mins = Math.floor((diff % 3600) / 60);
43
+ return `${hours}h ${mins}m`;
44
+ }
45
+ const days = Math.floor(diff / 86400);
46
+ const hours = Math.floor((diff % 86400) / 3600);
47
+ return `${days}d ${hours}h`;
48
+ }
49
+
50
+ export const serverStatusProcedure: Procedure<
51
+ ServerStatusInput,
52
+ ServerStatusOutput,
53
+ { description: string; effects: Effects; output: string }
54
+ > = createProcedure()
55
+ .path(["server", "status"])
56
+ .input(serverStatusInputSchema)
57
+ .output(serverStatusOutputSchema)
58
+ .meta({
59
+ description: "Get CLI server status",
60
+ // It deletes the lockfile of a server that does not answer
61
+ effects: { reads: ["fs", "server"], writes: ["fs"], idempotent: true },
62
+ args: [],
63
+ shorts: { port: "p" },
64
+ output: "text",
65
+ })
66
+ .handler(async (input: ServerStatusInput): Promise<ServerStatusOutput> => {
67
+ // Get target servers
68
+ let targets: LockfileData[];
69
+ if (input.port !== undefined) {
70
+ const lockfile = readLockfileForPort(input.port);
71
+ targets = lockfile ? [lockfile] : [];
72
+ } else {
73
+ targets = readAllLockfiles();
74
+ }
75
+
76
+ if (targets.length === 0) {
77
+ return {
78
+ running: false,
79
+ message: input.port
80
+ ? `No server found on port ${input.port}`
81
+ : "No servers running",
82
+ };
83
+ }
84
+
85
+ // For single server, return detailed info
86
+ if (targets.length === 1) {
87
+ const lockfile = targets[0]!;
88
+ const { pid, port, endpoint, startedAt, transport } = lockfile;
89
+
90
+ if ((await checkServer(lockfile)) !== "alive") {
91
+ removeLockfileForPort(port);
92
+ return {
93
+ running: false,
94
+ message: "Server is not running (cleaned up stale lockfile)",
95
+ };
96
+ }
97
+
98
+ const uptime = formatUptime(startedAt);
99
+ return {
100
+ running: true,
101
+ pid,
102
+ port,
103
+ endpoint,
104
+ transport,
105
+ startedAt,
106
+ uptime,
107
+ logFile: LOG_PATH,
108
+ message: `Server running (PID ${pid}, port ${port}, uptime ${uptime})`,
109
+ };
110
+ }
111
+
112
+ // Multiple servers - summarize
113
+ const lines: string[] = [];
114
+ let anyRunning = false;
115
+
116
+ for (const lockfile of targets) {
117
+ const { pid, port, endpoint, startedAt } = lockfile;
118
+ if ((await checkServer(lockfile)) !== "alive") {
119
+ removeLockfileForPort(port);
120
+ continue;
121
+ }
122
+ anyRunning = true;
123
+ const uptime = formatUptime(startedAt);
124
+ lines.push(` Port ${port}: PID ${pid}, uptime ${uptime}, ${endpoint}`);
125
+ }
126
+
127
+ if (!anyRunning) {
128
+ return {
129
+ running: false,
130
+ message: "No servers running (cleaned up stale lockfiles)",
131
+ };
132
+ }
133
+
134
+ return {
135
+ running: true,
136
+ message: `${lines.length} server(s) running:\n${lines.join("\n")}`,
137
+ };
138
+ })
139
+ .build();
140
+
141
+ export type { ServerStatusInput, ServerStatusOutput };
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Procedure: server.stop
3
+ * Stops the running CLI server daemon
4
+ */
5
+
6
+ import { createProcedure, type Effects, type Procedure } from "@procedures/client";
7
+ import { wrapSchema } from "../schema.js";
8
+ import { ServerStopOutputSchema } from "../output-schemas.js";
9
+ import { serverStopInputSchema } from "../input-schemas.js";
10
+ import { checkServer, readAllLockfiles, readLockfileForPort, removeLockfileForPort, type LockfileData } from "../lockfile.js";
11
+ import type { ServerStopInput, ServerStopOutput } from "../types.js";
12
+
13
+
14
+ const serverStopOutputSchema = wrapSchema<ServerStopOutput>(ServerStopOutputSchema);
15
+
16
+
17
+
18
+
19
+
20
+ /**
21
+ * Kill process by PID
22
+ */
23
+ function killProcess(pid: number, signal: NodeJS.Signals = "SIGTERM"): boolean {
24
+ try {
25
+ process.kill(pid, signal);
26
+ return true;
27
+ } catch {
28
+ return false;
29
+ }
30
+ }
31
+
32
+ export const serverStopProcedure: Procedure<
33
+ ServerStopInput,
34
+ ServerStopOutput,
35
+ { description: string; effects: Effects }
36
+ > = createProcedure()
37
+ .path(["server", "stop"])
38
+ .input(serverStopInputSchema)
39
+ .output(serverStopOutputSchema)
40
+ .meta({
41
+ description: "Stop running CLI server",
42
+ // It stops the servers of the lockfiles and deletes their lockfiles
43
+ effects: { reads: ["fs", "server"], writes: ["fs", "server"], idempotent: true },
44
+ args: [],
45
+ shorts: { port: "p" },
46
+ })
47
+ .handler(async (input: ServerStopInput): Promise<ServerStopOutput> => {
48
+ const force = input.force ?? false;
49
+ const signal: NodeJS.Signals = force ? "SIGKILL" : "SIGTERM";
50
+
51
+ // Get target servers
52
+ let targets: LockfileData[];
53
+ if (input.port !== undefined) {
54
+ const lockfile = readLockfileForPort(input.port);
55
+ targets = lockfile ? [lockfile] : [];
56
+ } else {
57
+ targets = readAllLockfiles();
58
+ }
59
+
60
+ if (targets.length === 0) {
61
+ return {
62
+ success: false,
63
+ message: input.port
64
+ ? `No server found on port ${input.port}`
65
+ : "No servers running",
66
+ };
67
+ }
68
+
69
+ const results: string[] = [];
70
+ let allSuccess = true;
71
+
72
+ for (const lockfile of targets) {
73
+ const { pid, port } = lockfile;
74
+
75
+ // Signal the PID only when the server answers as the peer of this lockfile: Windows reuses
76
+ // PIDs, and before, a stale lockfile made this kill an unrelated process (deep dive CLI-4)
77
+ const state = await checkServer(lockfile);
78
+ if (state !== "alive") {
79
+ removeLockfileForPort(port);
80
+ results.push(
81
+ state === "dead"
82
+ ? `Port ${port}: cleaned up stale lockfile`
83
+ : `Port ${port}: another process answers on this port; nothing stopped (stale lockfile removed)`
84
+ );
85
+ continue;
86
+ }
87
+
88
+ const sent = killProcess(pid, signal);
89
+ if (!sent) {
90
+ allSuccess = false;
91
+ results.push(`Port ${port}: failed to send ${signal} to PID ${pid}`);
92
+ continue;
93
+ }
94
+
95
+ // Wait for process to die
96
+ const maxWait = force ? 500 : 5000;
97
+ const startTime = Date.now();
98
+ let stopped = false;
99
+
100
+ while (Date.now() - startTime < maxWait) {
101
+ if ((await checkServer(lockfile, 300)) === "dead") {
102
+ removeLockfileForPort(port);
103
+ results.push(`Port ${port}: stopped (PID ${pid})`);
104
+ stopped = true;
105
+ break;
106
+ }
107
+ await new Promise((resolve) => setTimeout(resolve, 100));
108
+ }
109
+
110
+ if (!stopped) {
111
+ allSuccess = false;
112
+ results.push(`Port ${port}: PID ${pid} did not stop${force ? "" : " (try --force)"}`);
113
+ }
114
+ }
115
+
116
+ return {
117
+ success: allSuccess,
118
+ message: results.join("\n"),
119
+ };
120
+ })
121
+ .build();
122
+
123
+ export type { ServerStopInput, ServerStopOutput };
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Importing this file registers the procedures of @procedures/peer, and of the modules that it
3
+ * requires, in the global registry: `import "@procedures/peer/register"`. The package root and the
4
+ * `./module` entry register nothing (ARCHITECTURE-PROPOSALS-2026-10, P9).
5
+ */
6
+
7
+ import { registerServerProcedures } from "./module.js";
8
+
9
+ export * from "./module.js";
10
+
11
+ registerServerProcedures();
package/src/schema.ts ADDED
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Schema Helpers
3
+ *
4
+ * Wraps Zod schemas for the client procedure system.
5
+ */
6
+
7
+ /**
8
+ * Wrap a Zod schema for use with client procedures
9
+ */
10
+ export { zodAdapter as wrapSchema } from "@procedures/client";
11
+
12
+ /**
13
+ * Create a pass-through schema for type T (no validation)
14
+ */
15
+ export { outputSchema as schema } from "@procedures/client";