@procedures/shell
v0.1.0
Published
Safe command execution for TypeScript: shell.run with argv and no shell, shell.stream line streaming with backpressure, process-tree kill, timeouts and output caps.
Maintainers
Readme
@procedures/shell
npm install @procedures/shellGeneric shell command execution procedures - the foundation layer for all CLI wrapper packages in the Mark ecosystem.
Table of Contents
- Overview
- Installation
- Architecture
- Quick Start
- API Reference
- Use Cases
- Cross-Platform Notes
- Integration
- Requirements
- License
Overview
client-shell provides low-level shell command execution as RPC procedures:
- shell.run - Spawn processes with arguments (no shell interpretation, safer)
- shell.exec - Execute command strings via shell (supports pipes, redirects)
- shell.which - Find command paths cross-platform
- shell.stream - Run a command with arguments, and give each output line while it runs
This package is the foundation for all CLI wrapper packages.
Command helpers
The wrapper packages start their programs with the helpers of @procedures/shell/command. This entry point registers no procedures.
| Helper | What it does |
|---|---|
| runCommand(command, options) | It runs a program to its end and gives exitCode, stdout, stderr and duration. It does not throw. |
| streamCommand(command, options) | It gives each output line while the program runs, then an exit item. |
| processes | It records a long-running program: start, list, waitFor, output, stop. |
| killTree(child) | It kills a child process and all the processes that it started. |
The rules of the helpers:
- A helper passes the arguments as a list. No shell reads them.
- The
timeoutand thesignalkill the whole process tree. Windows usestaskkill /T /F. The other systems use a process group. - Each output stream has a limit (
maxOutputBytes, 64 MiB by default). The helper keeps the first bytes, or the last bytes withkeep: "tail". - A program without an
inputgets an empty stdin. - When the reader of
streamCommandis slow, the helper stops reading the pipes, so the program waits. - When the host ends, the helpers kill each child that still runs.
import { runCommand } from "@procedures/shell/command";
const result = await runCommand("git", { args: ["status", "--porcelain"], cwd, signal: ctx.signal });Installation
npm install github:mark1russell7/client-shell#mainArchitecture
System Overview
graph TB
subgraph "Application Layer"
App[Your Application]
end
subgraph "client-shell"
Run[shell.run<br/>Spawn with args]
Exec[shell.exec<br/>Shell string]
Which[shell.which<br/>Find command]
end
subgraph "Node.js"
Spawn[child_process.spawn]
ExecFn[child_process.exec]
WhichFn[which / where]
end
subgraph "Output"
Result[exitCode, stdout, stderr,<br/>success, duration]
end
App --> Run
App --> Exec
App --> Which
Run --> Spawn
Exec --> ExecFn
Which --> WhichFn
Spawn --> Result
ExecFn --> ResultCommand Execution Flow
sequenceDiagram
participant App as Application
participant Shell as client-shell
participant Node as Node.js
participant OS as Operating System
App->>Shell: shell.run({ command: "git", args: ["status"] })
Shell->>Shell: Validate input
Shell->>Node: spawn("git", ["status"], options)
Node->>OS: Execute process
OS-->>Node: stdout, stderr, exitCode
Node-->>Shell: Process result
Shell->>Shell: Build output
Shell-->>App: { success, stdout, stderr, exitCode, duration }Dependency Tree
graph TB
subgraph "CLI Wrapper Packages"
CLI[client-cli]
Pnpm[client-pnpm]
Git[client-git]
Docker[client-docker]
Vitest[client-vitest]
Cue[client-cue]
end
subgraph "Foundation"
Shell[client-shell]
end
subgraph "Core"
Client[client]
end
CLI --> Shell
Pnpm --> Shell
Git --> Shell
Docker --> Shell
Vitest --> Shell
Cue --> Shell
Shell --> ClientQuick Start
import { Client, LocalTransport, PROCEDURE_REGISTRY } from "@procedures/client";
import "@procedures/shell"; // registers its procedures // Auto-registers procedures
const client = new Client(new LocalTransport({ registry: PROCEDURE_REGISTRY }));
// Run a command with arguments
const result = await client.exec(["shell", "run"], {
command: "git",
args: ["status", "--short"],
cwd: "/path/to/repo",
});
console.log(result.stdout);
// M src/index.ts
// ?? new-file.tsAPI Reference
Procedures Summary
| Path | Description |
|------|-------------|
| shell.run | Spawn process with arguments (no shell) |
| shell.exec | Execute command string through shell |
| shell.which | Find full path to a command |
shell.run
Spawn a process directly without shell interpretation. Safer for untrusted input.
interface ShellRunInput {
command: string; // Command to run
args?: string[]; // Arguments (default: [])
cwd?: string; // Working directory
env?: Record<string, string>; // Environment variables
timeout?: number; // Timeout in ms
encoding?: string; // Output encoding (default: "utf8")
}
interface ShellRunOutput {
exitCode: number;
stdout: string;
stderr: string;
success: boolean; // true if exitCode === 0
duration: number; // ms
}Example:
await client.exec(["shell", "run"], {
command: "npm",
args: ["install", "--save-dev", "typescript"],
cwd: "/my/project",
timeout: 60000,
});shell.exec
Execute a command string through the system shell. Supports pipes, redirects, etc.
interface ShellExecInput {
command: string; // Full command string
cwd?: string; // Working directory
env?: Record<string, string>; // Environment variables
timeout?: number; // Timeout in ms
shell?: boolean | string; // Shell to use (default: true)
maxBuffer?: number; // Max stdout/stderr buffer
stdin?: string; // Input to pipe to stdin
}
interface ShellExecOutput {
exitCode: number;
stdout: string;
stderr: string;
success: boolean;
duration: number;
signal?: string; // Signal that terminated process
}Example:
await client.exec(["shell", "exec"], {
command: "git log --oneline | head -5",
cwd: "/my/repo",
});shell.which
Find the full path to a command. Uses where on Windows, which on Unix.
interface ShellWhichInput {
command: string; // Command to find
}
interface ShellWhichOutput {
path: string | null; // Full path or null if not found
found: boolean;
}Example:
const { path, found } = await client.exec(["shell", "which"], {
command: "node",
});
// { path: "/usr/local/bin/node", found: true }Use Cases
Building CLI Wrappers
The shell.run procedure is the foundation for CLI wrapper packages:
// In client-git/src/procedures/git/status.ts
export async function gitStatus(input: GitStatusInput, ctx: ProcedureContext) {
const args = ["status"];
if (input.short) args.push("--short");
const result = await ctx.client.call(["shell", "run"], {
command: "git",
args,
cwd: input.cwd,
});
return parseGitStatus(result.stdout);
}Checking Command Availability
async function ensureGitAvailable() {
const { found } = await client.exec(["shell", "which"], { command: "git" });
if (!found) {
throw new Error("Git is not installed or not in PATH");
}
}Piping Commands
// Using shell.exec for pipes
const result = await client.exec(["shell", "exec"], {
command: "cat package.json | jq '.dependencies'",
cwd: "/my/project",
});Cross-Platform Notes
shell.which Behavior
graph LR
subgraph "Unix/macOS"
UnixWhich["which command"]
end
subgraph "Windows"
WinWhere["where command"]
end
subgraph "Result"
Path["/usr/bin/git<br/>or C:\\Program Files\\Git\\bin\\git.exe"]
end
UnixWhich --> Path
WinWhere --> Pathshell.run vs shell.exec
| Feature | shell.run | shell.exec | |---------|-----------|------------| | Shell interpretation | No | Yes | | Pipes & redirects | No | Yes | | Argument safety | Safer | Less safe | | Use case | Known commands | Shell scripts | | Platform quoting | Handled | Manual |
Integration
With Other Packages
graph TB
subgraph "High-Level"
Lib[client-lib<br/>lib.refresh]
end
subgraph "Mid-Level"
Git[client-git<br/>git.commit]
Pnpm[client-pnpm<br/>pnpm.install]
end
subgraph "Foundation"
Shell[client-shell<br/>shell.run]
end
Lib --> Git
Lib --> Pnpm
Git --> Shell
Pnpm --> ShellWith MCP (Claude)
When using the MCP server with bundle-dev, Claude has access to shell.* procedures:
User: "Run npm install in /my/project"
Claude: [Uses shell.run with command: "npm", args: ["install"]]Requirements
- Node.js >= 22
- Dependencies:
@procedures/client(peer dependency)zod^3.24.0
License
MIT
