npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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.

Readme

@procedures/shell

npm

npm install @procedures/shell

License: MIT TypeScript Node.js

Generic shell command execution procedures - the foundation layer for all CLI wrapper packages in the Mark ecosystem.

Table of Contents


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 timeout and the signal kill the whole process tree. Windows uses taskkill /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 with keep: "tail".
  • A program without an input gets an empty stdin.
  • When the reader of streamCommand is 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#main

Architecture

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 --> Result

Command 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 --> Client

Quick 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.ts

API 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 --> Path

shell.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 --> Shell

With 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