Docs Packages

@procedures/node

packages/client-node/README.md on GitHub · Package map · Procedures

Node.js process management procedures for the client ecosystem. Provides procedures for running scripts, spawning long-running processes, and managing process lifecycles.

Architecture Overview#

┌─────────────────────────────────────────────────────────────────────────────┐
│                              Client Application                              │
│                                                                              │
│   await client.exec(["node", "run"], { script: "build.js" })                │
│   await client.exec(["node", "spawn"], { script: "server.js" })             │
│   await client.exec(["node", "kill"], { processId: "abc123" })              │
│   await client.exec(["node", "status"], {})                                 │
│                                                                              │
└─────────────────────────────────┬───────────────────────────────────────────┘
                                  │
                                  ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│                           Procedure Registry                                 │
│                                                                              │
│   ┌───────────────┐  ┌───────────────┐  ┌───────────────┐  ┌─────────────┐ │
│   │   node.run    │  │  node.spawn   │  │   node.kill   │  │ node.status │ │
│   │               │  │               │  │               │  │             │ │
│   │ Run to        │  │ Start         │  │ Kill by       │  │ List all    │ │
│   │ completion    │  │ detached      │  │ process ID    │  │ processes   │ │
│   └───────┬───────┘  └───────┬───────┘  └───────┬───────┘  └──────┬──────┘ │
│           │                  │                  │                 │        │
└───────────┼──────────────────┼──────────────────┼─────────────────┼────────┘
            │                  │                  │                 │
            ▼                  ▼                  ▼                 ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│                          ProcessManager (Singleton)                          │
│                                                                              │
│   ┌─────────────────────────────────────────────────────────────────────┐   │
│   │                        Active Processes Map                          │   │
│   │                                                                      │   │
│   │   processId: "abc123"  ──►  { script, pid, startedAt, status }      │   │
│   │   processId: "def456"  ──►  { script, pid, startedAt, status }      │   │
│   │   processId: "ghi789"  ──►  { script, pid, startedAt, status }      │   │
│   │                                                                      │   │
│   └─────────────────────────────────────────────────────────────────────┘   │
│                                                                              │
└─────────────────────────────────────────────────────────────────────────────┘
                                  │
                                  ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│                          Node.js child_process                               │
│                                                                              │
│   spawn("node", ["script.js"], { detached: true, stdio: "pipe" })           │
│                                                                              │
└─────────────────────────────────────────────────────────────────────────────┘

Installation#

pnpm add @procedures/node

Procedures#

node.run#

Run a Node.js script to completion and return the output.

import { Client } from "@procedures/client";

const result = await client.exec<{
  stdout: string;
  stderr: string;
  exitCode: number;
}>(["node", "run"], {
  script: "./scripts/build.js",
  args: ["--production"],
  cwd: "/path/to/project",
  env: { NODE_ENV: "production" },
  timeout: 60000 // 1 minute timeout
});

console.log("Exit code:", result.exitCode);
console.log("Output:", result.stdout);

Input Schema:

FieldTypeRequiredDescription
scriptstringYesPath to script file
argsstring[]NoCommand line arguments
cwdstringNoWorking directory
envRecord<string, string>NoEnvironment variables
timeoutnumberNoTimeout in milliseconds. At the timeout, the script and its child processes are killed, and the call throws.
maxOutputBytesnumberNoThe most bytes kept of each output stream (default: 64 MiB)

The script runs with the Node.js of the host (process.execPath). The signal of the call also kills the script and its child processes.

Output Schema:

FieldTypeDescription
stdoutstringStandard output
stderrstringStandard error
exitCodenumberProcess exit code
truncatedbooleanPresent when an output stream was longer than maxOutputBytes

node.spawn#

Start a long-running Node.js process. The process is a record of the process registry of client-shell: node.status shows it, node.kill ends it, and it ends with the host.

const result = await client.exec<{
  processId: string;
  pid: number;
}>(["node", "spawn"], {
  script: "./server.js",
  args: ["--port", "3000"],
  cwd: "/path/to/project",
  env: { NODE_ENV: "production" },
  ready: { pattern: "Server listening on", timeout: 30000 } // Wait for this output
});

console.log("Process ID:", result.processId);
console.log("PID:", result.pid);

Input Schema:

FieldTypeRequiredDescription
scriptstringYesPath to script file
argsstring[]NoCommand line arguments
cwdstringNoWorking directory
envRecord<string, string>NoEnvironment variables
ready{ pattern: string; timeout?: number }NoWait until the output matches pattern (a regular expression). When the process does not get ready, the procedure stops it and throws.

Output Schema:

FieldTypeDescription
processIdstringInternal process identifier
pidnumberSystem process ID

node.kill#

Kill a spawned process by its process ID, with the processes that it started, and wait for its end.

const result = await client.exec<{
  success: boolean;
}>(["node", "kill"], {
  processId: "abc123"
});

if (result.success) {
  console.log("Process killed successfully");
}

Input Schema:

FieldTypeRequiredDescription
processIdstringYesProcess ID from spawn

Output Schema:

FieldTypeDescription
successbooleanWhether kill succeeded

node.status#

Get status of all running processes or a specific one.

// Get all processes
const result = await client.exec<{
  processes: ProcessInfo[];
}>(["node", "status"], {});

// Get specific process
const result = await client.exec<{
  processes: ProcessInfo[];
}>(["node", "status"], {
  processId: "abc123"
});

for (const proc of result.processes) {
  console.log(`${proc.processId}: ${proc.status} (PID: ${proc.pid})`);
}

Input Schema:

FieldTypeRequiredDescription
processIdstringNoSpecific process ID to query
outputbooleanNoAlso give the end of the output of each process (the last 64 KiB)

Output Schema:

FieldTypeDescription
processesProcessInfo[]Array of process information

ProcessInfo:

FieldTypeDescription
processIdstringInternal process identifier
scriptstringScript path
pidnumberSystem process ID
status"running" | "stopped"Process status
startedAtstringISO timestamp when started

Process Lifecycle#

┌─────────────┐     spawn()      ┌─────────────┐
│             │ ───────────────► │             │
│   (none)    │                  │   running   │
│             │                  │             │
└─────────────┘                  └──────┬──────┘
                                        │
                    ┌───────────────────┼───────────────────┐
                    │                   │                   │
                    ▼                   ▼                   ▼
             kill() called       process exits       crash/error
                    │                   │                   │
                    ▼                   ▼                   ▼
             ┌─────────────┐     ┌─────────────┐     ┌─────────────┐
             │   stopped   │     │   stopped   │     │   stopped   │
             │  (killed)   │     │  (exited)   │     │  (crashed)  │
             └─────────────┘     └─────────────┘     └─────────────┘

Ready Pattern Detection#

To wait until a server is ready, give ready.pattern (and optionally ready.timeout in milliseconds):

// The spawn resolves when "Server listening on port" appears in stdout
const result = await client.exec(["node", "spawn"], {
  script: "./server.js",
  ready: { pattern: "Server listening on port", timeout: 30000 },
});
Timeline:

spawn() called
     │
     ▼
┌─────────────────────────────────────────────┐
│  Process Starting                           │
│                                             │
│  stdout: "Loading configuration..."         │
│  stdout: "Connecting to database..."        │
│  stdout: "Server listening on port 3000" ◄──┼── Pattern matched!
│                                             │
└─────────────────────────────────────────────┘
     │
     ▼
Promise resolves with { processId, pid }

Integration Example#

import { Client, LocalTransport, PROCEDURE_REGISTRY } from "@procedures/client";
import "@procedures/node"; // registers its procedures

const client = new Client(new LocalTransport({ registry: PROCEDURE_REGISTRY }));

// Start a dev server
const server = await client.exec(["node", "spawn"], {
  script: "./server.js",
  args: ["--port", "3000"],
  ready: { pattern: "listening on" },
  env: { NODE_ENV: "development" }
});

console.log(`Server started with PID ${server.pid}`);

// Check status
const status = await client.exec(["node", "status"], {
  processId: server.processId
});

console.log(`Status: ${status.processes[0].status}`);

// Cleanup
await client.exec(["node", "kill"], {
  processId: server.processId
});

Error Handling#

try {
  const result = await client.exec(["node", "run"], {
    script: "./nonexistent.js"
  });
} catch (error) {
  if (error.code === "ENOENT") {
    console.error("Script not found");
  } else if (error.code === "TIMEOUT") {
    console.error("Script timed out");
  }
}

Auto-Registration#

Import the register module to auto-register all procedures:

import "@procedures/node"; // registers its procedures

Or register manually:

import { registerNodeProcedures } from "@procedures/node";

registerNodeProcedures();
  • @procedures/client - Core client framework
  • @procedures/shell - Shell command execution
  • @procedures/vite - Vite dev server management

License#

MIT