@procedures/playground
v0.1.0
Published
A typed fluent API to write procedure programs in TypeScript (proc, ref, run) with autocomplete, then run them with the client.
Maintainers
Readme
@procedures/playground
Type-safe aggregation building for procedure compositions
Overview
client-playground provides a fluent TypeScript API for composing procedure references with full type safety and autocomplete. Write aggregations as executable TypeScript files (.play.ts) that can be run directly with tsx or ts-node. Perfect for testing procedure compositions, building complex workflows, and exploring procedure APIs interactively.
Key Features
- Type-Safe Builder API: Fluent interface with full TypeScript autocomplete
- Executable Scripts: Write
.play.tsfiles that run directly - Output References: Reference previous stage outputs with
$refsyntax - Named Stages: Name stages for later reference in the execution DAG
- Execution Control: Control when procedures execute (
$immediate,$never,$parent) - Local Transport: Uses in-process transport for fast execution
- Zero Configuration: Auto-discovers registered procedures
Architecture
graph TB
subgraph "Playground File (.play.ts)"
BUILDER[ProcBuilder API]
REF[Output References]
RUN[run() Executor]
end
subgraph "Runtime"
LOCAL[Local Transport]
REGISTRY[Procedure Registry]
EXEC[Execution Engine]
end
subgraph "Procedures"
PROC1[git.status]
PROC2[git.add]
PROC3[pnpm.install]
end
BUILDER --> REF
REF --> RUN
RUN --> LOCAL
LOCAL --> REGISTRY
REGISTRY --> EXEC
EXEC --> PROC1
EXEC --> PROC2
EXEC --> PROC3
style BUILDER fill:#7ed321
style RUN fill:#f5a623
style LOCAL fill:#50e3c2Execution Flow
sequenceDiagram
participant Script as playground.play.ts
participant Builder as proc()
participant Executor as run()
participant Registry as PROCEDURE_REGISTRY
participant Proc as Procedure Handler
Script->>Builder: proc(["git", "status"]).input({ cwd: "." })
Builder-->>Script: ProcBuilder with .ref
Script->>Executor: run(procRef)
Executor->>Registry: Discover all procedures
Registry-->>Executor: Registered procedures
Executor->>Executor: Create local transport
Executor->>Executor: Sync registry to transport
Executor->>Proc: Execute procedure
Proc-->>Executor: Result
Executor->>Executor: Print result (if print: true)
Executor-->>Script: Typed resultInstallation
npm install @procedures/playgroundOr with pnpm:
pnpm add @procedures/playgroundDependencies
This package depends on:
@procedures/client- Core client/procedure system (dynamically imported)
Runtime Requirements
tsxorts-node- For executing.play.tsfiles- Node.js >= 22.0.0
Usage
Quick Start
Create a playground file:
// my-workflow.play.ts
import { proc, ref, run } from "@procedures/playground";
// Execute a simple procedure
run(
proc(["git", "status"]).input({ cwd: "." }).ref
);Run it:
npx tsx my-workflow.play.tsOutput:
{
"branch": "main",
"ahead": 0,
"behind": 0,
"files": [],
"clean": true
}Builder API
The proc() builder provides a fluent interface:
import { proc } from "@procedures/playground";
// Simple procedure reference
const gitStatus = proc(["git", "status"])
.input({ cwd: "/path/to/repo" })
.ref;
// Named stage for reference
const hasChanges = proc(["git", "hasChanges"])
.input({ cwd: "." })
.name("changes") // Can be referenced as $ref: "changes"
.ref;
// With execution control
const conditionalAdd = proc(["git", "add"])
.input({ all: true })
.when("$parent") // Execute in parent context
.ref;Output References
Reference outputs from named stages:
import { proc, ref, run } from "@procedures/playground";
run(
proc(["dag", "traverse"]).input({
visit: [
// Stage 1: Check for changes
proc(["git", "hasChanges"])
.name("changes")
.ref,
// Stage 2: Conditional on stage 1
proc(["client", "conditional"]).input({
condition: ref("changes.value"), // Reference "changes" output
then: proc(["git", "add"]).input({ all: true }).ref,
}).ref,
],
}).ref
);Reference Syntax
ref("changes.value") // Reference the .value field of "changes" output
ref("$last") // Reference the previous step's output
ref("$last.success") // Reference the .success field of previous output
ref("install.packages") // Deep property accessComplex Workflows
import { proc, ref, run } from "@procedures/playground";
// Multi-step build and deploy workflow
run(
proc(["dag", "traverse"]).input({
visit: [
// 1. Install dependencies
proc(["pnpm", "install"])
.name("install")
.ref,
// 2. Run tests
proc(["shell", "exec"])
.input({ command: "npm test" })
.name("test")
.ref,
// 3. Build only if tests pass
proc(["client", "conditional"]).input({
condition: ref("test.exitCode"), // 0 = success
operator: "equals",
value: 0,
then: proc(["shell", "exec"])
.input({ command: "npm run build" })
.name("build")
.ref,
}).ref,
// 4. Deploy if build succeeded
proc(["client", "conditional"]).input({
condition: ref("build.success"),
then: proc(["docker", "build"]).input({
context: ".",
tag: "my-app:latest",
}).ref,
}).ref,
],
}).ref
);Running Without Printing
import { run } from "@procedures/playground";
// Get result without printing
const result = await run(
proc(["git", "status"]).ref,
{ print: false }
);
console.log(`Current branch: ${result.branch}`);
// Custom formatting
const data = await run(
proc(["git", "log"]).input({ count: 5 }).ref,
{ print: true, format: "text" }
);API Reference
proc(path: ProcedurePath): ProcBuilder
Create a procedure reference builder.
Parameters:
path: string[]- Procedure path (e.g.,["git", "status"])
Returns: ProcBuilder with methods:
.input<T>(input: T): ProcBuilder<T>
Set the input for this procedure.
proc(["git", "add"])
.input({ files: ["file.ts"], all: false })
.ref.name(name: string): ProcBuilder
Name this stage for later reference with $ref.
proc(["git", "hasChanges"])
.name("changes")
.ref
// Later:
ref("changes.value").when(when: string): ProcBuilder
Set when this reference should be executed.
Values:
"$immediate"- Execute immediately"$never"- Never execute (for testing)"$parent"- Execute in parent context- Custom context name
proc(["git", "add"])
.when("$parent")
.ref.ref: ProcedureRefJson
Get the procedure reference object.
const procRef = proc(["git", "status"])
.input({ cwd: "." })
.ref;
// procRef is:
// {
// $proc: ["git", "status"],
// input: { cwd: "." }
// }ref(path: string): OutputRef
Create an output reference.
Parameters:
path: string- Reference path (e.g.,"stageName.value"or"$last.value")
Returns: OutputRef object
const outputRef = ref("changes.value");
// { $ref: "changes.value" }run<T>(procedureRef: ProcedureRefJson, options?: RunOptions): Promise<T>
Execute a procedure reference.
Parameters:
procedureRef: ProcedureRefJson- Procedure reference to executeoptions?: RunOptions- Execution options
Options:
interface RunOptions {
print?: boolean; // Print result (default: true)
format?: "json" | "text"; // Output format (default: "json")
}Returns: Promise<T> - Typed result
// With printing (default)
await run(proc(["git", "status"]).ref);
// Without printing
const result = await run(
proc(["git", "status"]).ref,
{ print: false }
);
// Text format
await run(
proc(["shell", "exec"]).input({ command: "ls" }).ref,
{ format: "text" }
);Types
ProcBuilder
interface ProcBuilder<TInput = unknown> {
input<T>(input: T): ProcBuilder<T>;
name(name: string): ProcBuilder<TInput>;
when(when: string): ProcBuilder<TInput>;
readonly ref: ProcedureRefJson<TInput>;
}ProcedureRefJson
interface ProcedureRefJson<TInput = unknown> {
$proc: ProcedurePath;
input: TInput;
$name?: string;
$when?: string;
}OutputRef
interface OutputRef {
readonly $ref: string;
}Examples
Git Workflow
// git-workflow.play.ts
import { proc, ref, run } from "@procedures/playground";
run(
proc(["dag", "traverse"]).input({
visit: [
// Check for changes
proc(["git", "hasChanges"])
.name("changes")
.ref,
// Stage all if changes exist
proc(["client", "conditional"]).input({
condition: ref("changes.value"),
then: proc(["git", "add"]).input({ all: true }).ref,
}).ref,
// Commit
proc(["git", "commit"]).input({
message: "Automated commit from playground",
}).ref,
],
}).ref
);Build Pipeline
// build.play.ts
import { proc, run } from "@procedures/playground";
run(
proc(["dag", "traverse"]).input({
visit: [
proc(["pnpm", "install"]).ref,
proc(["shell", "exec"]).input({ command: "npm run lint" }).ref,
proc(["shell", "exec"]).input({ command: "npm test" }).ref,
proc(["shell", "exec"]).input({ command: "npm run build" }).ref,
],
}).ref
);Docker Deployment
// deploy.play.ts
import { proc, ref, run } from "@procedures/playground";
run(
proc(["dag", "traverse"]).input({
visit: [
// Build image
proc(["docker", "build"]).input({
context: ".",
tag: "my-app:latest",
}).name("build").ref,
// Run container
proc(["docker", "run"]).input({
image: "my-app:latest",
name: "my-app-container",
ports: ["3000:3000"],
detach: true,
}).ref,
// Health check
proc(["shell", "exec"]).input({
command: "curl http://localhost:3000/health",
}).ref,
],
}).ref
);Testing Procedures
// test-procedures.play.ts
import { proc, run } from "@procedures/playground";
// Test git status
console.log("=== Testing git.status ===");
await run(proc(["git", "status"]).input({ cwd: "." }).ref);
// Test pnpm store path
console.log("\n=== Testing pnpm.store.path ===");
await run(proc(["pnpm", "store", "path"]).ref);
// Test shell execution
console.log("\n=== Testing shell.exec ===");
await run(proc(["shell", "exec"]).input({ command: "echo 'Hello from playground!'" }).ref);Conditional Logic
// conditional.play.ts
import { proc, ref, run } from "@procedures/playground";
run(
proc(["dag", "traverse"]).input({
visit: [
// Get environment
proc(["shell", "exec"])
.input({ command: "echo $NODE_ENV" })
.name("env")
.ref,
// Install based on environment
proc(["client", "conditional"]).input({
condition: ref("env.stdout"),
operator: "contains",
value: "production",
then: proc(["pnpm", "install"]).input({ production: true }).ref,
else: proc(["pnpm", "install"]).ref,
}).ref,
],
}).ref
);Use Cases
1. Interactive Procedure Testing
Quickly test procedure calls without setting up a full client:
// test-docker.play.ts
import { proc, run } from "@procedures/playground";
await run(proc(["docker", "ps"]).input({ all: true }).ref);npx tsx test-docker.play.ts2. Complex Workflow Prototyping
Prototype multi-step workflows before integrating into apps:
// prototype-workflow.play.ts
import { proc, ref, run } from "@procedures/playground";
// Test the entire workflow
run(proc(["dag", "traverse"]).input({ visit: [...] }).ref);3. CI/CD Scripts
Write deployment scripts as executable TypeScript:
// deploy.play.ts
run(proc(["dag", "traverse"]).input({
visit: [
proc(["git", "pull"]).ref,
proc(["pnpm", "install"]).ref,
proc(["shell", "exec"]).input({ command: "npm run build" }).ref,
proc(["docker", "build"]).input({ context: ".", tag: "app:latest" }).ref,
proc(["docker", "compose", "up"]).input({ detach: true }).ref,
],
}).ref);4. Development Automation
Automate common development tasks:
// setup.play.ts
// Run: npx tsx setup.play.ts
import { proc, run } from "@procedures/playground";
run(proc(["dag", "traverse"]).input({
visit: [
proc(["git", "checkout"]).input({ branch: "main" }).ref,
proc(["git", "pull"]).ref,
proc(["pnpm", "install"]).ref,
proc(["shell", "exec"]).input({ command: "npm run setup:env" }).ref,
],
}).ref);Advanced Usage
Custom Transport
import { run } from "@procedures/playground";
import { LocalTransport, PROCEDURE_REGISTRY } from "@procedures/client";
// run() uses LocalTransport internally
// The registry is synced automaticallyError Handling
import { proc, run } from "@procedures/playground";
try {
const result = await run(
proc(["nonexistent", "procedure"]).ref,
{ print: false }
);
} catch (error) {
console.error("Execution failed:", error.message);
}TypeScript Integration
import { proc, run } from "@procedures/playground";
import type { GitStatusOutput } from "@procedures/git";
// Typed result
const status = await run<GitStatusOutput>(
proc(["git", "status"]).input({ cwd: "." }).ref,
{ print: false }
);
console.log(`Branch: ${status.branch}`);
console.log(`Clean: ${status.clean}`);Performance Considerations
- Local Transport: Uses in-process transport for minimal overhead
- Registry Sync: Procedures synced once per
run()call - No Network: All execution is local - no HTTP/WebSocket overhead
- Fast Iteration: Changes to
.play.tsfiles are immediate (no build step)
Comparison with Alternatives
vs. Direct Client Usage
// Direct client - more boilerplate
import { Client, LocalTransport, PROCEDURE_REGISTRY } from "@procedures/client";
const client = new Client(new LocalTransport({ registry: PROCEDURE_REGISTRY }));
const result = await client.exec(["git", "status"], { cwd: "." });
// Playground - concise
import { proc, run } from "@procedures/playground";
const result = await run(proc(["git", "status"]).input({ cwd: "." }).ref);vs. Shell Scripts
# Shell script - no type safety
git status
pnpm install
npm test// Playground - type-safe, composable
run(proc(["dag", "traverse"]).input({
visit: [
proc(["git", "status"]).ref,
proc(["pnpm", "install"]).ref,
proc(["shell", "exec"]).input({ command: "npm test" }).ref,
],
}).ref);License
MIT
Related Packages
@procedures/client- Core client/procedure system@procedures/bundle-dev- Development bundle with procedures- All
client-*packages - Provide procedures to use in playground
