@procedures/pnpm
v0.1.0
Published
pnpm commands as typed procedures: install, add, remove, run and link, run with argv and option-injection guards.
Maintainers
Readme
@procedures/pnpm
PNPM package manager wrapper - exposes
pnpm.install,pnpm.add,pnpm.remove,pnpm.link,pnpm.run, andpnpm.store.pathas type-safe RPC procedures.
Overview
@procedures/pnpm wraps pnpm package manager commands as RPC procedures through the client system. It provides 6 procedures for package management operations including installation, adding/removing packages, linking local packages, running scripts, and accessing the pnpm store.
This package follows the CLI Wrapper Pattern using client-shell for command execution with full type safety via Zod schemas.
Architecture
graph TB
A[client-pnpm] -->|uses| B[client-shell]
A -->|depends on| C[@procedures/client]
A -->|validates with| D[zod]
E[Application] -->|calls| F[pnpm.* procedures]
F -->|builds command| G[pnpm CLI]
F -->|calls| H[shell.run]
H -->|executes| G
I[pnpm.install] -.-> F
J[pnpm.add/remove] -.-> F
K[pnpm.link] -.-> F
L[pnpm.run] -.-> F
M[pnpm.store.path] -.-> F
style A fill:#e1f5ff
style B fill:#ffe1e1
style F fill:#e1ffe1Procedure Categories
graph LR
A[client-pnpm<br/>6 Procedures] --> B[Package Management<br/>pnpm.install<br/>pnpm.add<br/>pnpm.remove]
A --> C[Development<br/>pnpm.link<br/>pnpm.run]
A --> D[Store Management<br/>pnpm.store.path]
style A fill:#e1f5ff
style B fill:#ffe1e1
style C fill:#fff4e1
style D fill:#e1ffe1Installation
# Using pnpm (recommended)
pnpm add @procedures/pnpm
# Using npm
npm install @procedures/pnpm
# Using yarn
yarn add @procedures/pnpmQuick Start
import { Client, LocalTransport, PROCEDURE_REGISTRY } from "@procedures/client";
import "@procedures/pnpm"; // registers its procedures
const client = new Client(new LocalTransport({ registry: PROCEDURE_REGISTRY }));
// Install all dependencies
await client.exec(["pnpm", "install"], {
cwd: "/path/to/project",
});
// Add a dev dependency
await client.exec(["pnpm", "add"], {
packages: ["typescript", "@types/node"],
dev: true,
});
// Run a script
await client.exec(["pnpm", "run"], {
script: "build",
});API Reference
Procedures
pnpm.install
Install dependencies from package.json or specific packages.
Path: ["pnpm", "install"]
interface PnpmInstallInput {
/** Packages to install. If not provided, installs from package.json */
packages?: string[];
/** Save as dev dependency */
dev?: boolean;
/** Use frozen lockfile (--frozen-lockfile) */
frozen?: boolean;
/** Working directory */
cwd?: string;
/** Timeout in milliseconds */
timeout?: number;
}
interface PnpmCommandOutput {
/** Exit code of the command */
exitCode: number;
/** Standard output */
stdout: string;
/** Standard error */
stderr: string;
/** Whether command succeeded (exit code 0) */
success: boolean;
/** Duration in milliseconds */
duration: number;
}Examples:
// Install all from lockfile (CI mode)
await client.exec(["pnpm", "install"], {
frozen: true,
cwd: "/my/project",
});
// Install specific packages
await client.exec(["pnpm", "install"], {
packages: ["lodash", "zod"],
dev: true,
});
// Install all dependencies
await client.exec(["pnpm", "install"], {});pnpm.add
Add packages to the project.
Path: ["pnpm", "add"]
interface PnpmAddInput {
/** Packages to add */
packages: string[];
/** Save as dev dependency */
dev?: boolean;
/** Install globally */
global?: boolean;
/** Working directory */
cwd?: string;
/** Timeout in milliseconds */
timeout?: number;
}Examples:
// Add production dependencies
await client.exec(["pnpm", "add"], {
packages: ["express", "cors"],
});
// Add dev dependencies
await client.exec(["pnpm", "add"], {
packages: ["vitest", "@vitest/coverage-v8", "typescript"],
dev: true,
});
// Add global tool
await client.exec(["pnpm", "add"], {
packages: ["typescript"],
global: true,
});
// Add with version
await client.exec(["pnpm", "add"], {
packages: ["react@18", "react-dom@18"],
});pnpm.remove
Remove packages from the project.
Path: ["pnpm", "remove"]
interface PnpmRemoveInput {
/** Packages to remove */
packages: string[];
/** Remove from global */
global?: boolean;
/** Working directory */
cwd?: string;
/** Timeout in milliseconds */
timeout?: number;
}Examples:
// Remove packages
await client.exec(["pnpm", "remove"], {
packages: ["old-package", "unused-lib"],
});
// Remove global package
await client.exec(["pnpm", "remove"], {
packages: ["typescript"],
global: true,
});pnpm.link
Link local packages for development.
Path: ["pnpm", "link"]
interface PnpmLinkInput {
/** Path to package to link. If not provided, links current directory globally */
path?: string;
/** Link globally */
global?: boolean;
/** Working directory */
cwd?: string;
/** Timeout in milliseconds */
timeout?: number;
}Examples:
// Link current directory globally
await client.exec(["pnpm", "link"], {
global: true,
cwd: "/path/to/my-package",
});
// Link a local package into current project
await client.exec(["pnpm", "link"], {
path: "../my-local-package",
});
// Link from global to current project
await client.exec(["pnpm", "link"], {
path: "my-global-package",
global: true,
});pnpm.run
Run package.json scripts.
Path: ["pnpm", "run"]
interface PnpmRunInput {
/** Script name to run */
script: string;
/** Additional arguments to pass to the script */
args?: string[];
/** Working directory */
cwd?: string;
/** Timeout in milliseconds */
timeout?: number;
}Examples:
// Run build script
await client.exec(["pnpm", "run"], {
script: "build",
});
// Run test with arguments
await client.exec(["pnpm", "run"], {
script: "test",
args: ["--coverage", "--watch"],
});
// Run custom script in specific directory
await client.exec(["pnpm", "run"], {
script: "deploy",
cwd: "/path/to/project",
timeout: 600000, // 10 minutes
});pnpm.store.path
Get the pnpm store path for snapshot/restore operations.
Path: ["pnpm", "store", "path"]
interface PnpmStorePathInput {
/** Working directory (affects store path on some configurations) */
cwd?: string;
}
interface PnpmStorePathOutput {
/** Absolute path to the pnpm store */
path: string;
/** Whether store exists */
exists: boolean;
/** Platform (win32, darwin, linux) */
platform: string;
}Example:
const store = await client.exec(["pnpm", "store", "path"], {});
console.log(`PNPM store: ${store.path}`);
console.log(`Exists: ${store.exists}`);
console.log(`Platform: ${store.platform}`);Complete Procedure List
| Procedure | Description |
|-----------|-------------|
| pnpm.install | Install dependencies from package.json/lockfile |
| pnpm.add | Add packages to project |
| pnpm.remove | Remove packages from project |
| pnpm.link | Link local packages for development |
| pnpm.run | Run package.json scripts |
| pnpm.store.path | Get pnpm store path |
Usage Examples
CI/CD Pipeline
// Frozen install for reproducible builds
const install = await client.exec(["pnpm", "install"], {
frozen: true,
cwd: "./project",
});
if (!install.success) {
throw new Error(`Install failed: ${install.stderr}`);
}
// Run tests
await client.exec(["pnpm", "run"], {
script: "test",
args: ["--coverage"],
});
// Build
await client.exec(["pnpm", "run"], {
script: "build",
});Development Workflow
// Add dependencies
await client.exec(["pnpm", "add"], {
packages: ["react", "react-dom"],
});
await client.exec(["pnpm", "add"], {
packages: ["@types/react", "@types/react-dom", "vite"],
dev: true,
});
// Link local packages
await client.exec(["pnpm", "link"], {
path: "../shared-components",
});
// Start dev server
await client.exec(["pnpm", "run"], {
script: "dev",
});Monorepo Package Management
// Install all workspace packages
await client.exec(["pnpm", "install"], {
cwd: "/monorepo/root",
});
// Add dependency to specific package
await client.exec(["pnpm", "add"], {
packages: ["lodash"],
cwd: "/monorepo/root/packages/utils",
});
// Build all packages in dependency order
await client.exec(["pnpm", "run"], {
script: "build",
args: ["--recursive"],
cwd: "/monorepo/root",
});Global Package Management
// Install global tool
await client.exec(["pnpm", "add"], {
packages: ["typescript", "ts-node"],
global: true,
});
// Link package globally for testing
await client.exec(["pnpm", "link"], {
global: true,
cwd: "/path/to/my-cli-tool",
});
// Remove global package
await client.exec(["pnpm", "remove"], {
packages: ["old-cli"],
global: true,
});Store Management
// Get store path for backup/restore
const store = await client.exec(["pnpm", "store", "path"], {});
console.log(`Backing up store: ${store.path}`);
// Could be used to:
// - Backup store for faster CI builds
// - Share store between CI jobs
// - Clean old cached packages
if (store.exists) {
// Perform backup/cleanup operations
}Types & Schemas
All input types have corresponding Zod schemas for validation:
import {
PnpmInstallInputSchema,
PnpmAddInputSchema,
PnpmRemoveInputSchema,
PnpmLinkInputSchema,
PnpmRunInputSchema,
PnpmStorePathInputSchema,
type PnpmInstallInput,
type PnpmAddInput,
type PnpmRemoveInput,
type PnpmLinkInput,
type PnpmRunInput,
type PnpmStorePathInput,
type PnpmCommandOutput,
type PnpmStorePathOutput,
} from "@procedures/pnpm";Functions
// Low-level procedure functions
export async function pnpmInstall(
input: PnpmInstallInput,
ctx: ProcedureContext
): Promise<PnpmCommandOutput>;
export async function pnpmAdd(
input: PnpmAddInput,
ctx: ProcedureContext
): Promise<PnpmCommandOutput>;
export async function pnpmRemove(
input: PnpmRemoveInput,
ctx: ProcedureContext
): Promise<PnpmCommandOutput>;
export async function pnpmLink(
input: PnpmLinkInput,
ctx: ProcedureContext
): Promise<PnpmCommandOutput>;
export async function pnpmRun(
input: PnpmRunInput,
ctx: ProcedureContext
): Promise<PnpmCommandOutput>;
export async function pnpmStorePath(
input: PnpmStorePathInput
): Promise<PnpmStorePathOutput>;
// Registration function
export function registerPnpmProcedures(): void;Configuration
Package.json Client Field
{
"client": {
"procedures": "./dist/register.js"
},
"scripts": {
"postinstall": "client-registry announce"
}
}Build Scripts
# Build the package
pnpm build
# Type check
pnpm typecheck
# Clean build artifacts
pnpm cleanDependencies
graph LR
A[@procedures/pnpm] --> B[@procedures/client]
A --> C[@procedures/shell]
A --> D[zod]
B -.->|provides| E[Client & Procedure System]
C -.->|provides| F[shell.run procedure]
D -.->|provides| G[Schema Validation]
style A fill:#e1f5ff
style B fill:#ffe1e1
style C fill:#ffe1e1
style D fill:#fff4e1@procedures/client- Core client library providing the procedure system@procedures/shell- Generic shell execution wrapperzod- Schema validation library
Integration with Ecosystem
Used By
// bundle-dev/src/register.ts
import "@procedures/pnpm"; // registers its procedures
// client-lib uses pnpm procedures for package management
import { pnpmInstall, pnpmRun } from "@procedures/pnpm";Usage in client-lib
The client-lib package uses pnpm procedures for ecosystem package management:
// Install dependencies in a package
await ctx.client.call(["pnpm", "install"], {
cwd: packagePath,
frozen: true,
});
// Build a package
await ctx.client.call(["pnpm", "run"], {
script: "build",
cwd: packagePath,
});CLI Wrapper Pattern
client-pnpm follows the ecosystem's CLI Wrapper Pattern:
- Multiple Focused Procedures - Each procedure wraps a specific pnpm command
- Shell Delegation - Uses
client-shellfor command execution - Consistent Output - Returns standardized
PnpmCommandOutput - Type Safety - Full TypeScript types and Zod validation
This pattern is also used by:
client-cli- Mark CLI wrapperclient-git- Git operations wrapper
Advanced Usage
Direct Function Imports
The handlers start pnpm through ctx.client.call(["shell", "run"], ...), so they need a procedure context. A Client is not a procedure context. Start the procedures through a client:
import { Client, LocalTransport, PROCEDURE_REGISTRY } from "@procedures/client";
import "@procedures/pnpm"; // registers pnpm.* and shell.*
const client = new Client(new LocalTransport({ registry: PROCEDURE_REGISTRY }));
await client.exec(["pnpm", "install"], { frozen: true });
await client.exec(["pnpm", "add"], { packages: ["lodash"] });
await client.exec(["pnpm", "run"], { script: "build" });Custom Registration
import { createProcedure, registerProcedures } from "@procedures/client";
import { pnpmInstall, PnpmInstallInputSchema } from "@procedures/pnpm";
// Register under custom namespace
const customInstallProcedure = createProcedure()
.path(["custom", "pnpm", "install"])
.input(PnpmInstallInputSchema)
.handler(pnpmInstall)
.build();
registerProcedures([customInstallProcedure]);Error Handling
try {
const result = await client.exec(["pnpm", "install"], {
frozen: true,
});
if (!result.success) {
console.error(`Install failed with exit code ${result.exitCode}`);
console.error(`stderr: ${result.stderr}`);
if (result.stderr.includes("ERR_PNPM_OUTDATED_LOCKFILE")) {
console.log("Lockfile is outdated. Run pnpm install without --frozen-lockfile.");
}
}
} catch (error) {
console.error("Procedure call failed:", error);
}Performance Considerations
- Process Spawning: Each call spawns a pnpm process
- Lockfile Parsing: Large lockfiles may slow down operations
- Frozen Installs: Use
frozen: truein CI for faster, reproducible installs - Workspace Filtering: Use pnpm's native workspace filtering for monorepos
- Store Location: Consider store location for performance (local vs network drive)
Troubleshooting
PNPM Not Found
Ensure pnpm is installed and in PATH:
pnpm --versionInstall pnpm:
npm install -g pnpmLockfile Issues
If you get lockfile errors:
// Update lockfile
await client.exec(["pnpm", "install"], {
// Don't use frozen: true
});
// Or delete lockfile and reinstall
await client.exec(["pnpm", "install"], {});Permission Errors
Check directory permissions:
ls -la node_modulesStore Issues
Check store path and permissions:
const store = await client.exec(["pnpm", "store", "path"], {});
console.log(`Store: ${store.path}, Exists: ${store.exists}`);PNPM Features
Workspace Support
PNPM workspaces are supported through the cwd parameter:
// Install all workspace packages
await client.exec(["pnpm", "install"], {
cwd: "/workspace/root",
});
// Add to specific workspace package
await client.exec(["pnpm", "add"], {
packages: ["lodash"],
cwd: "/workspace/root/packages/utils",
});Script Arguments
Pass arguments to pnpm scripts:
await client.exec(["pnpm", "run"], {
script: "test",
args: ["--", "--coverage", "--watch"],
});
// Runs: pnpm run test -- --coverage --watchVersion Specifiers
Use standard pnpm version syntax:
await client.exec(["pnpm", "add"], {
packages: [
"react@^18.0.0",
"lodash@latest",
"@types/node@*",
"my-package@workspace:*", // Workspace package
],
});License
MIT
Related Packages
- @procedures/client - Core client library
- @procedures/shell - Shell execution wrapper
- @procedures/lib - Library management (uses pnpm procedures)
- @procedures/cli-run - CLI wrapper
- @procedures/git - Git operations wrapper
- @procedures/bundle-dev - Development bundle
