@procedures/server 0.2.3 → 0.4.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 (46) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/README.md +18 -1
  3. package/dist/.tsbuildinfo +1 -1
  4. package/dist/index.d.ts +6 -1
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +7 -0
  7. package/dist/index.js.map +1 -1
  8. package/dist/module.d.ts +24 -0
  9. package/dist/module.d.ts.map +1 -0
  10. package/dist/module.js +29 -0
  11. package/dist/module.js.map +1 -0
  12. package/dist/procedures/server.start.d.ts +17 -0
  13. package/dist/procedures/server.start.d.ts.map +1 -0
  14. package/dist/procedures/server.start.js +183 -0
  15. package/dist/procedures/server.start.js.map +1 -0
  16. package/dist/procedures/server.status.d.ts +15 -0
  17. package/dist/procedures/server.status.d.ts.map +1 -0
  18. package/dist/procedures/server.status.js +118 -0
  19. package/dist/procedures/server.status.js.map +1 -0
  20. package/dist/procedures/server.stop.d.ts +14 -0
  21. package/dist/procedures/server.stop.d.ts.map +1 -0
  22. package/dist/procedures/server.stop.js +98 -0
  23. package/dist/procedures/server.stop.js.map +1 -0
  24. package/dist/register.d.ts +7 -0
  25. package/dist/register.d.ts.map +1 -0
  26. package/dist/register.js +9 -0
  27. package/dist/register.js.map +1 -0
  28. package/dist/run.d.ts.map +1 -1
  29. package/dist/run.js +18 -3
  30. package/dist/run.js.map +1 -1
  31. package/dist/schemas.d.ts +19 -0
  32. package/dist/schemas.d.ts.map +1 -0
  33. package/dist/schemas.js +48 -0
  34. package/dist/schemas.js.map +1 -0
  35. package/dist/types.d.ts +58 -0
  36. package/dist/types.d.ts.map +1 -1
  37. package/package.json +19 -5
  38. package/src/index.ts +26 -1
  39. package/src/module.ts +33 -0
  40. package/src/procedures/server.start.ts +213 -0
  41. package/src/procedures/server.status.ts +141 -0
  42. package/src/procedures/server.stop.ts +123 -0
  43. package/src/register.ts +11 -0
  44. package/src/run.ts +20 -3
  45. package/src/schemas.ts +69 -0
  46. package/src/types.ts +76 -0
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Procedure: server.status
3
+ * Gets the current CLI server status
4
+ *
5
+ * The procedure came from `@procedures/peer`: it belongs to the host.
6
+ */
7
+
8
+ import * as path from "node:path";
9
+ import * as os from "node:os";
10
+ import { createProcedure, zodAdapter, type Effects, type Procedure } from "@procedures/client";
11
+ import { checkServer, readAllLockfiles, readLockfileForPort, removeLockfileForPort, type LockfileData } from "@procedures/peer/lockfile";
12
+ import { ServerStatusOutputSchema, serverStatusInputSchema } from "../schemas.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 = zodAdapter<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
+ * The procedure came from `@procedures/peer`: it belongs to the host.
6
+ */
7
+
8
+ import { createProcedure, zodAdapter, type Effects, type Procedure } from "@procedures/client";
9
+ import { checkServer, readAllLockfiles, readLockfileForPort, removeLockfileForPort, type LockfileData } from "@procedures/peer/lockfile";
10
+ import { ServerStopOutputSchema, serverStopInputSchema } from "../schemas.js";
11
+ import type { ServerStopInput, ServerStopOutput } from "../types.js";
12
+
13
+
14
+ const serverStopOutputSchema = zodAdapter<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/server in the global registry:
3
+ * `import "@procedures/server/register"`. The package root and the `./module` entry register
4
+ * 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/run.ts CHANGED
@@ -23,9 +23,11 @@ import {
23
23
  type ProcedureRegistry,
24
24
  } from "@procedures/client";
25
25
  import { createPeer, type Peer, type PeerEndpoint } from "@procedures/peer";
26
- import serverModule from "@procedures/peer/module";
26
+ import peerModule from "@procedures/peer/module";
27
27
  import { parseConfig } from "./config.js";
28
+ import { serverModule } from "./module.js";
28
29
  import type { ServerConfig } from "./types.js";
30
+ import { seconds } from "@procedures/client";
29
31
 
30
32
  /** A package name and the subpath after it: "@a/b/register" gives ["@a/b", "./register"]. */
31
33
  function splitSpec(spec: string): { name: string; subpath: string } {
@@ -133,7 +135,9 @@ export async function startServer(
133
135
  log: (line: string) => void = console.log,
134
136
  options: ServerRunOptions = {}
135
137
  ): Promise<RunningServer> {
136
- const modules: ProcedureModule[] = [serverModule];
138
+ // The server.* procedures: those of the peer package (server.create, server.call and the
139
+ // others), and server.start, server.stop and server.status of this package
140
+ const modules: ProcedureModule[] = [peerModule, serverModule];
137
141
  for (const spec of config.procedures) {
138
142
  if (config.verbose) log(` Loading: ${spec}`);
139
143
  try {
@@ -194,9 +198,22 @@ export async function runServer(argv: string[], options: ServerRunOptions = {}):
194
198
  }
195
199
  console.log("\nPress Ctrl+C to stop the server.");
196
200
 
201
+ // The peer stops before the process exits, with a limit of 10 seconds. A failed stop exits
202
+ // with 1, and a second signal exits at once (roadmap R5).
203
+ let stopping = false;
197
204
  const stop = (): void => {
205
+ if (stopping) process.exit(1);
206
+ stopping = true;
198
207
  console.log("\nShutting down...");
199
- running.peer.stop().finally(() => process.exit(0));
208
+ setTimeout(() => process.exit(1), 10_000).unref();
209
+ // The calls in flight have 5 seconds to end (roadmap R14)
210
+ running.peer.stop({ grace: seconds(5) }).then(
211
+ () => process.exit(0),
212
+ (error: unknown) => {
213
+ console.error(`The server did not stop cleanly: ${error instanceof Error ? error.message : String(error)}`);
214
+ process.exit(1);
215
+ },
216
+ );
200
217
  };
201
218
  process.on("SIGINT", stop);
202
219
  process.on("SIGTERM", stop);
package/src/schemas.ts ADDED
@@ -0,0 +1,69 @@
1
+ /**
2
+ * The input and output schemas of `server.start`, `server.stop` and `server.status`.
3
+ *
4
+ * The three procedures came from `@procedures/peer`: they manage the background server of the
5
+ * `mark` CLI, and this package holds the host. The input schemas validate each field, so the
6
+ * help of `mark server start` lists the options, and the port "abc" fails before the
7
+ * handler. The output schemas are loose objects: a field that a schema does not name stays in
8
+ * the result.
9
+ */
10
+
11
+ import { z } from "zod";
12
+ import { zodAdapter, type ZodLikeSchema } from "@procedures/client";
13
+ import type {
14
+ ServerStartInput,
15
+ ServerStartOutput,
16
+ ServerStatusInput,
17
+ ServerStatusOutput,
18
+ ServerStopInput,
19
+ ServerStopOutput,
20
+ } from "./types.js";
21
+
22
+ const port = z.number().int().min(1).max(65535);
23
+
24
+ export const serverStartInputSchema: ZodLikeSchema<ServerStartInput> = zodAdapter<ServerStartInput>(
25
+ z.object({
26
+ port: port.optional().describe("The port to listen on (default: 3000)"),
27
+ host: z.string().min(1).optional().describe("The host to bind to (default: 127.0.0.1)"),
28
+ transport: z.enum(["http", "websocket", "both"]).optional().describe("The transport (default: http)"),
29
+ })
30
+ );
31
+
32
+ export const serverStopInputSchema: ZodLikeSchema<ServerStopInput> = zodAdapter<ServerStopInput>(
33
+ z.object({
34
+ port: port.optional().describe("The port of the server to stop (default: all servers)"),
35
+ force: z.boolean().optional().describe("Stop with SIGKILL instead of SIGTERM"),
36
+ })
37
+ );
38
+
39
+ export const serverStatusInputSchema: ZodLikeSchema<ServerStatusInput> = zodAdapter<ServerStatusInput>(
40
+ z.object({
41
+ port: port.optional().describe("The port of the server (default: all servers)"),
42
+ })
43
+ );
44
+
45
+ export const ServerStartOutputSchema: z.ZodType<ServerStartOutput> = z.looseObject({
46
+ success: z.boolean().describe("True when the server started"),
47
+ pid: z.number().optional().describe("The process id of the server"),
48
+ port: z.number().optional().describe("The port of the server"),
49
+ endpoint: z.string().optional().describe("The URL of the server"),
50
+ logFile: z.string().optional().describe("The log file of the server"),
51
+ message: z.string().describe("A message about the result"),
52
+ });
53
+
54
+ export const ServerStopOutputSchema: z.ZodType<ServerStopOutput> = z.looseObject({
55
+ success: z.boolean().describe("True when the server stopped"),
56
+ message: z.string().describe("A message about the result"),
57
+ });
58
+
59
+ export const ServerStatusOutputSchema: z.ZodType<ServerStatusOutput> = z.looseObject({
60
+ running: z.boolean().describe("True when a server runs"),
61
+ pid: z.number().optional().describe("The process id of the server"),
62
+ port: z.number().optional().describe("The port of the server"),
63
+ endpoint: z.string().optional().describe("The URL of the server"),
64
+ transport: z.string().optional().describe("The transport of the server"),
65
+ startedAt: z.string().optional().describe("The start time (ISO 8601)"),
66
+ uptime: z.string().optional().describe("The time since the start, as text"),
67
+ logFile: z.string().optional().describe("The log file of the server"),
68
+ message: z.string().describe("A message about the result"),
69
+ });
package/src/types.ts CHANGED
@@ -30,3 +30,79 @@ export interface ServerCreateResult {
30
30
  endpoints: Array<{ type: string; address: string }>;
31
31
  procedureCount: number;
32
32
  }
33
+
34
+ // =============================================================================
35
+ // server.start Types (from @procedures/peer)
36
+ // =============================================================================
37
+
38
+ export interface ServerStartInput {
39
+ /** Port to listen on */
40
+ port?: number | undefined;
41
+ /** Host to bind to */
42
+ host?: string | undefined;
43
+ /** Transport type */
44
+ transport?: "http" | "websocket" | "both" | undefined;
45
+ }
46
+
47
+ export interface ServerStartOutput {
48
+ /** Whether start succeeded */
49
+ success: boolean;
50
+ /** Process ID of server */
51
+ pid?: number | undefined;
52
+ /** Port server is running on */
53
+ port?: number | undefined;
54
+ /** Endpoint URL */
55
+ endpoint?: string | undefined;
56
+ /** Log file path */
57
+ logFile?: string | undefined;
58
+ /** Status message */
59
+ message: string;
60
+ }
61
+
62
+ // =============================================================================
63
+ // server.stop Types (from @procedures/peer)
64
+ // =============================================================================
65
+
66
+ export interface ServerStopInput {
67
+ /** Port of server to stop (stops all if omitted) */
68
+ port?: number | undefined;
69
+ /** Force kill with SIGKILL instead of SIGTERM */
70
+ force?: boolean | undefined;
71
+ }
72
+
73
+ export interface ServerStopOutput {
74
+ /** Whether stop succeeded */
75
+ success: boolean;
76
+ /** Status message */
77
+ message: string;
78
+ }
79
+
80
+ // =============================================================================
81
+ // server.status Types (from @procedures/peer)
82
+ // =============================================================================
83
+
84
+ export interface ServerStatusInput {
85
+ /** Port to check (shows all if omitted) */
86
+ port?: number | undefined;
87
+ }
88
+
89
+ export interface ServerStatusOutput {
90
+ /** Whether server is running */
91
+ running: boolean;
92
+ /** Process ID */
93
+ pid?: number | undefined;
94
+ /** Port */
95
+ port?: number | undefined;
96
+ /** Endpoint URL */
97
+ endpoint?: string | undefined;
98
+ /** Transport type */
99
+ transport?: string | undefined;
100
+ /** When server started (ISO string) */
101
+ startedAt?: string | undefined;
102
+ /** Human-readable uptime */
103
+ uptime?: string | undefined;
104
+ /** Log file path */
105
+ logFile?: string | undefined;
106
+ /** Status message */
107
+ message: string;
108
+ }