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

p4client-ts

v0.10.0

Published

Typed TypeScript helpers for the Perforce p4 CLI

Readme

p4-ts

Typed TypeScript helpers for the Perforce p4 CLI.

The published package name is p4client-ts.

  • Run p4 with a testable client abstraction
  • Parse classic tagged output and newline-delimited JSON
  • Query current Perforce environment with sensible fallbacks
  • Resolve local Perforce settings from p4 set, P4V, and the Windows registry
  • List and filter workspaces that are relevant to the local machine
  • Provide preview-first Perforce helpers with opt-in mutating sync support
  • Apply command timeouts across raw and higher-level APIs
  • Optional Effect-based service API

Runtime schemas and typed JSON

Typed JSON parsing requires an Effect schema. The result type is inferred from the decoder, including transformations such as CLI numeric strings to numbers:

import { Schema } from 'effect';
import { P4Client, P4DepotPathSchema, P4PositiveIntegerSchema, parseP4JsonLines } from 'p4client-ts';

const FileRow = Schema.Struct({
  depotFile: P4DepotPathSchema,
  rev: P4PositiveIntegerSchema
});

const files = parseP4JsonLines('{"depotFile":"//depot/file.txt","rev":"3"}', FileRow);
// files[0]?.rev is number; depotFile is a validated P4DepotPath.

const p4 = new P4Client();
const depotFiles = await p4.runTaggedJson(['files', '//depot/...'], { schema: FileRow });
const rawRows = await p4.runTaggedJson(['files', '//depot/...']);
// Raw fields remain unknown until narrowed or decoded.

Malformed command fields raise P4ParseError with the offending row and original schema error in cause. Missing optional fields remain supported. Built-in command schemas accept additional server tags, while a custom schema controls the shape returned by a custom parser call.

Migration: replace parseP4JsonLines<MyRow>(output) with parseP4JsonLines(output, MyRowSchema), and replace runTaggedJson<MyRow>(args) with runTaggedJson(args, { schema: MyRowSchema }). Generic-only typed calls are no longer supported.

P4DepotPathSchema, P4ClientPathSchema, P4LocalPathSchema, and P4FileActionSchema construct the existing branded types through validation. For example, Schema.decodeUnknownSync(P4DepotPathSchema)(input) replaces an unchecked input as P4DepotPath. The depot and client schemas validate syntax; they do not verify that the path exists on a server. Local paths can be relative. Opened, reconcile, and sync clientFile fields now use P4ClientPath | P4LocalPath | null, reflecting both forms the CLI emits.

Cancellation

Client operation options accept an AbortSignal. It reaches nested commands, local settings readers, and concurrent materialization or diff work:

const controller = new AbortController();
const pending = p4.getChangelistDiffSummary(12345, {
  includeLineCounts: true,
  signal: controller.signal
});

// For example, when the selected changelist changes:
controller.abort();
try {
  await pending;
} catch (error) {
  if (!controller.signal.aborted) throw error;
}

Each Effect service invocation owns its cancellation scope. Interrupting the Effect aborts its work and waits for cleanup; it does not abort the caller's signal or other invocations. Ending an Effect stream early, such as with Stream.take, also cancels and joins its operation.

For Promise-based watched operations, break from handle.events (or calling the iterator's return()) cancels unfinished work and waits for cleanup. handle.result rejects when that work is cancelled. To stop observing while letting the operation finish, keep draining events or await the result without starting iteration. Simply abandoning an iterator does not cancel it.

The built-in command adapter terminates the direct child and waits for its process and output streams to close before settling an aborted or timed-out result. Cancellation stops queued commands; it does not undo completed file writes or Perforce operations. Cancelled lookups do not cache fallback success.

Custom executor, streamExecutor, and settings readers must honor their supplied signal and settle after releasing resources. Streaming adapters must also finish pending event reads. Cleanup waits for these adapters; an adapter that ignores cancellation can therefore delay interruption indefinitely.

Scope

This package is intended for inspection, preview-oriented workflows, explicit sync() operations, and local P4CLIENT switching.

In scope:

  • Inspect current environment and workspace state
  • List relevant workspaces for the current machine
  • Inspect pending, submitted, and shelved changelists
  • Inspect opened files
  • Describe changelists and build lazy-load diff summaries, including shelved file rows via p4 describe -S -s
  • Diff workspace files against depot revisions (p4 diff)
  • Diff shelved files against their depot base without unshelving (p4 diff2 with @=<change>)
  • Print depot file content at a revision (p4 print)
  • List exact depot revisions at a submitted changelist and materialize a bounded set outside the workspace (p4 files plus p4 print -o)
  • Preview reconcile operations
  • Preview sync operations and apply sync when explicitly requested
  • Read file metadata and depot/local path mappings

Out of scope:

  • submit
  • shelve or unshelve mutations
  • edit, add, delete, or other checkout/open-for-edit commands
  • revert, lock, unlock, move, integrate, or resolve
  • Changelist creation or mutation
  • Client or stream spec mutation
  • Server administration or other server-mutating workflows

Install

npm install p4client-ts

Quick Start

import { P4Client } from "p4client-ts";

const p4 = new P4Client();

const environment = await p4.getEnvironment();
const localEnvironment = await p4.getEnvironment({ mode: "local" });
const workspaces = await p4.listWorkspaces();
const pending = await p4.listPendingChangelists();
const shelved = await p4.listShelvedChangelists({
  fileSpec: "//Project/main/..."
});
const opened = await p4.getOpenedFiles({ change: "default" });
const reconcilePreview = await p4.previewReconcile({
  fileSpec: "C:/work/project/..."
});
const syncPreview = await p4.previewSync({
  fileSpec: "//Project/main/..."
});

if (syncPreview.totalCount > 0) {
  const syncResult = await p4.sync({
    fileSpec: "//Project/main/..."
  });
}

const reconcileOperation = p4.watchPreviewReconcile({
  fileSpec: "C:/work/project/..."
});

for await (const event of reconcileOperation.events) {
  if (event.type === "progress") {
    console.log(event.rawLine);
  }
}

const reconcileWithProgress = await reconcileOperation.result;

Changelist Diff Inspection

Use describeChangelist() for changelist metadata and file rows, then load patches on demand with diffFile():

const description = await p4.describeChangelist(12345);
const file = description.files[0];

if (file && !isBinaryP4Type(file.type)) {
  const summary = await p4.getChangelistDiffSummary(12345);
  const fileSummary = summary.files.find((entry) => entry.depotFile === file.depotFile);

  const diff = await p4.diffFile({
    depotFile: file.depotFile,
    localFile: fileSummary?.localFile ?? undefined,
    action: file.action,
    revision: file.revision,
    changelistStatus: description.status,
    type: file.type,
    allowBinary: false
  });

  console.log(diff.source, diff.unifiedDiff);
}

const depotContent = await p4.printFile("//Project/main/foo.txt", {
  revision: "have"
});

diffFile() is the single entrypoint:

  • Pending changelists compare the workspace against depot #have via p4 diff.
  • Submitted changelists compare two depot revisions via p4 diff2, inferred from action/revision or supplied through fromRevision/toRevision.
  • Shelved changelists compare depot base revisions against shelf revisions via p4 diff2. Describe the shelf first, then pass changelistStatus: "shelved" and shelvedChange.
const shelvedDescription = await p4.describeChangelist(12345, { shelved: true });
const shelvedFile = shelvedDescription.files[0];

if (shelvedFile && !isBinaryP4Type(shelvedFile.type)) {
  const diff = await p4.diffFile({
    depotFile: shelvedFile.depotFile,
    action: shelvedFile.action,
    revision: shelvedFile.revision,
    changelistStatus: "shelved",
    shelvedChange: 12345,
    type: shelvedFile.type,
    allowBinary: false
  });

  console.log(diff.source, diff.unifiedDiff);
}

p4 diff and p4 diff2 exit with code 1 when differences exist. diffFile() treats exit codes 0 and 1 as success and only throws for exit code 2 or higher. Use a higher timeoutMs for diff operations on large files.

Historical Depot Materialization

Use listDepotFilesAtChange() to resolve the files that existed under a depot path at a submitted changelist. The required maxFiles bound also reports hasMore when the result is truncated:

const snapshot = await p4.listDepotFilesAtChange({
  depotPath: "//Project/main/Content/...",
  change: 12345,
  maxFiles: 100
});

Materialize a selected, bounded set into an existing temporary directory:

const materialized = await p4.materializeDepotFiles({
  files: snapshot.items.filter((file) => file.type.startsWith("binary")),
  directory: temporaryDirectory,
  maxFiles: 25,
  concurrency: 4
});

Files are written beneath <directory>/<depot>/<path> using their exact numeric revisions. materializeDepotFiles() uses p4 print -q -K -o, so binary payloads bypass the text-based command result and printFile() remains compatible. It does not run p4 sync or modify the active workspace.

Local Settings Resolution

resolveP4Settings() resolves P4PORT, P4USER, and P4CLIENT from local sources without contacting the server:

import {
  resolveP4Settings,
  resolveP4SettingsWithDetails
} from "p4client-ts";

const settings = await resolveP4Settings(
  { P4CLIENT: "Project_Main" },
  {
    sources: ["p4v-app-settings", "p4v-connection-map", "cli", "registry"]
  }
);

const detailed = await resolveP4SettingsWithDetails({}, {
  sources: ["cli", "registry"]
});

getEnvironment({ mode: "local" }) uses the same resolver and skips p4 info.

Timeouts

Set timeoutMs on P4Client to apply a process timeout to raw commands and higher-level helpers:

import { P4Client, P4TimeoutError } from "p4client-ts";

const p4 = new P4Client({ timeoutMs: 1500 });

try {
  await p4.previewSync({ fileSpec: "//Project/main/..." });
} catch (error) {
  if (error instanceof P4TimeoutError) {
    console.error(error.timeoutMs);
  }
}

Documentation

This repository includes a Starlight docs app in ../www with authored guides and generated API docs powered by TypeDoc.

Run the docs site locally from the repo root:

bun run docs:dev

Build the static docs site:

bun run docs:build

Reconcile Progress

previewReconcile() remains the simple buffered API. Use watchPreviewReconcile() when you need incremental progress while still awaiting the final structured reconcile result.

Progress output is best-effort:

  • Perforce progress lines are version-dependent and not treated as a stable schema.
  • The final structured reconcile preview remains the source of truth.
  • When -I progress is unsupported, the watcher retries once without -I and emits a progress-unavailable event.
  • When Perforce completes without any progress lines, the watcher emits progress-unavailable with reason not-emitted.

Effect Service API

For Effect-based codebases, createP4Service returns the same operations as P4Client wrapped in Effect:

import { Effect, Stream } from "effect";
import { createP4Service } from "p4client-ts";

const p4 = createP4Service();

const environment = await Effect.runPromise(p4.getP4Environment());
const workspaces = await Effect.runPromise(p4.listP4Workspaces());
const opened = await Effect.runPromise(p4.getOpenedFiles({ change: "default" }));
const reconcilePreview = await Effect.runPromise(p4.previewReconcile());
const reconcileEvents = await Effect.runPromise(
  p4.streamPreviewReconcile().pipe(Stream.runCollect)
);
const syncPreview = await Effect.runPromise(p4.previewSync({ fileSpec: "//Project/main/..." }));

if (syncPreview.totalCount > 0) {
  await Effect.runPromise(p4.sync({ fileSpec: "//Project/main/..." }));
}

Development

bun install
bun run typecheck
bun run test
bun run test:e2e
bun run build
bun run docs:build

End-to-End Tests

bun run test:e2e is the canonical E2E command. It downloads or reuses hash-verified Perforce p4/p4d binaries, creates a disposable localhost server, stream, client, and workspace from @p4-ts/test-stream, runs all scenarios, and cleans up the temporary fixture. The first run requires network access. Binaries are cached outside the repository.