Docs Packages

@procedures/playground

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

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.ts files that run directly
  • Output References: Reference previous stage outputs with $ref syntax
  • 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#

Diagram source (Mermaid). GitHub draws it on the page of the file.
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:#50e3c2

Execution Flow#

Diagram source (Mermaid). GitHub draws it on the page of the file.
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 result

Installation#

npm install @procedures/playground

Or with pnpm:

pnpm add @procedures/playground

Dependencies#

This package depends on:

  • @procedures/client - Core client/procedure system (dynamically imported)

Runtime Requirements#

  • tsx or ts-node - For executing .play.ts files
  • 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.ts

Output:

{
  "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 access

Complex 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 execute
  • options?: 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.ts

2. 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 automatically

Error 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.ts files 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

  • @procedures/client - Core client/procedure system
  • @procedures/bundle-dev - Development bundle with procedures
  • All client-* packages - Provide procedures to use in playground