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

@gripsack/core

v0.46.0

Published

gripsack typescript frontend — typed module DSL, emits IR

Readme

@gripsack/core

TypeScript frontend for gripsack — a typed module DSL whose evaluation the grip core runs inside a sandboxed, provisioned Deno (no environment variables, no network, no subprocesses, read-only within your repo). Every host observation (facts, tags, probes) is injected by the core; the frontend returns a value; effects are explicit probe requests the core binds and feeds back (plan/0013).

The supported grip hosts are Linux and WSL2. macOS and native Windows are outside the current support scope. Historical target metadata remains readable; it does not grant a supported execution platform.

A host is a function

// hosts/laptop.ts
import { defineEnv } from "@gripsack/core";
import { helix } from "../modules/helix.js";

export default defineEnv((ctx) => ({
  tags: ["gui", "work"],
  modules: [
    helix,
    ctx.facts.os === "linux" && steam,          // falsy entries drop out
    ctx.probe.executable("nvidia-smi") && cuda, // symbolic: bound by the core
  ],
}));

ctx is core-injected: { facts, tags, probe, settings }. Nothing is registered by import side effect — the function returns the environment, so Inputs → Environment is testable and cacheable.

A workspace is a function (IR v7)

Use matching 0.46 core and SDK releases for the new baseline and host-runtime declarations. Older strict cores do not admit v7. The new core retains v6 execution with its original closed field set, v5 native file profiles, v4 read-only workspaces and historical module readers; older envelopes never acquire v7 authority.

Linux and WSL2 are the supported host scope. Installing the npm package does not upgrade the core binary. Workspace locks capture frontend identity, so an intentional SDK change can require explicit grip update and reviewed exact-digest source approval.

A root gripsack.ts — preferred over hosts/<name>.ts when present — default-exports defineWorkspace and returns a workspace({ outputs }) value. No hostname selection and no fake host file: the core injects the same facts/probes context, and the emitter produces the v7 workspace envelope ({ir_version: 7, host, workspace}):

// gripsack.ts
import {
  conda, defineWorkspace, environment, pkg, provider, targetPlatform, workspace,
} from "@gripsack/core";

const target = targetPlatform({ os: "linux", arch: "x86_64", abi: "gnu" });
const ripgrep = pkg("ripgrep", {
  producer: provider(conda.environment({
    channels: ["conda-forge"],
    packages: { ripgrep: "*" },
    systemRequirements: {
      libc: { family: "glibc", version: "2.28" },
      linux: "4.18",
    },
  })),
  commands: { rg: "bin/rg" },
  target,
  layout: { kind: "prefix_materialized" },
});

export default defineWorkspace(() => workspace({
  outputs: [ripgrep, environment("search", { packages: ["ripgrep"], target })],
}));

Run grip update to record the complete selection, then grip build ripgrep or grip run --env search -- rg --version. Frozen consumers use those records without re-solving. Provider packages do not need a synthetic recipe or worker. Lock updates change captured inputs: review and approve the new source/policy snapshot when prompted before the next build. This is not an approval bypass.

conda.environment({ channels, packages }) describes one coherent Conda environment, not independently solved packages. pixi.fromLock({ manifest, lock, environment }) explicitly imports a named Pixi environment; manifest and lock name captured workspace inputs. Both namespaces are exported from @gripsack/core and its deliberate-pin entrypoint. The old pixi(package) constructor is removed; retained legacy IR remains versioned data, not a second workspace producer API. Conda/Pixi native and image qualification is tracked separately in plan 0052; this authoring surface alone is not evidence that every consumer/platform is qualified.

packages values are version MatchSpecs: use "==25.07.1" for exact equality, "=25.07" for a version-prefix match, ">=25.07.1,<26" for a range, or "*". A bare "25.07.1" is not a valid constraint. The resulting lock always fixes exact archive identities.

systemRequirements explicitly selects the corresponding Linux solve virtuals; omitted libc/kernel capabilities still use updater measurements. The declared policy is recorded and checked on frozen use. A solve's measured __glibc=2.39 is not by itself every selected archive's runtime floor. Native archive constraints, actual CPU/libc/kernel facts and sealed-loader controls remain separate checks. An explicit baseline is not universal portability proof.

Outputs are the nine typed kinds — recipe, pkg, environment, task, schedule, check, image, profile, hook — each a pure, frozen value with a mandatory source span. Duplicate catalog names show both declaration sites; invalid references show the referring declaration (and the conflicting target where applicable); dependency cycles show the causal path. runBash bodies are literal text (${…} interpolation is rejected; dynamic values enter through typed env/argv refs) and require a declared packageCommand interpreter, never ambient Bash. The reference identifies a package command; its bytes are pinned when the core resolves and publishes the package. Strict Bash options are explicit on the v6 wire; an authored digest claim is checked against the publication receipt, never treated as executable authority.

Object and immutable fluent command forms lower to the same command IR, aside from their declaration spans:

import { bash, bashBody, exec, lit, packageCommand } from "@gripsack/core";

const args = exec(packageCommand("tools", "jq"))
  .arg(lit("--sort-keys")).arg(lit("two words"))
  .env("MODE", lit("strict")).build();
// Same command as exec({ argv: [packageCommand("tools", "jq"),
//   lit("--sort-keys"), lit("two words")], env: { MODE: lit("strict") } }).

const script = bash(packageCommand("shell", "bash"))
  .body(bashBody`
    echo "$INPUT"
  `)
  .env("INPUT", lit("two words")).build();

bashBody captures the template's source line before dedenting, so a generated script error can map back to the original line. A plain string is supported only for single-line runBash({ body }); multiline strings without an original body location are rejected rather than given a false line_map. JavaScript evaluates template expressions before calling a tag: do not put ${…} expressions in bashBody. The frontend rejects them when invoked, but this is not a static pre-evaluation check. Recipe commands execute only through explicit production; the native file-profile path below does not execute command declarations.

Recipe execution is explicit. In 0.44, production recipes declare kind: "isolated_linux", worker: "buildkit", a Linux platform, and toolchain: { reference: "<registry/image>@sha256:<digest>" }. They do not run during grip check. A { kind: "host", access: "unconfined" } declaration is admitted as contract data but host recipe execution remains gated. Native downloads are provider-backed packages, not a recipe "native" execution mode. Supported Linux targets may declare abi: "gnu" or "musl" and minimum_os: { major, minor, patch? }. Fixed-prefix packages use { kind: "fixed_prefix", prefix: "/opt/tool" }; selecting one into an environment requires that environment's matching prefix.

A Linux GNU package may explicitly declare hostRuntime: { libraryDirectories: ["/opt/bb/lib64"] } for reviewed host runtime dependencies and absolute ELF search paths. This is a host-coupled package policy, not portable retained content, an evaluator read grant or a BuildKit host mount. Undeclared directories remain refused. Native invocation re-admits the dependency graph and preserves sealed-loader search equivalence; host-dependent persistent wrappers re-enter the absolute installed core path. Keep that core path and private state available. Host libraries remain trusted, mutable host inputs, not sealed package bytes, and such packages cannot be silently placed into an OCI image.

grip check evaluates and admits the workspace without a host file or env.toml and lists named outputs. Native file-only profiles now execute: repository files are captured once, literal/template content is prepared before deployment, and the same content can be linked, copied or merged as a managed block. check and plan prepare without deploying; apply uses the existing generations, ownership and journal; rollback restores retained bytes without rerendering current inputs. File-only update validates these inputs without writing a host lock.

grip update --check surveys external workspace resolutions without publishing sources or writing gripsack.lock; grip update publishes the per-platform source and captured frontend/import pins. Repository file sources use the current approved snapshot rather than an old checkout stored in the lock. Pin changes are captured inputs and therefore participate in trust approval.

grip build <recipe-package-or-image> --json returns retained native output paths. Provider-only and already-retained outputs need no worker. New compatible Linux production uses one checked solve through the pinned, hash-verified bridge helper, provisioned lazily into $GRIPSACK_HOME/tools on first use (--bridge <helper> selects a deliberate operator override instead; GRIPSACK_BRIDGE_MIRROR redirects only the download origin). Linux helper hashes are measured from independent pinned-toolchain builds and compiled into the core. Unreleased source builds need a matching artifact mirror or override until the matching core release publishes those assets. The owned worker is qualified for Linux x86_64 with Docker; this does not claim other-architecture worker qualification. macOS is outside the support scope.

sourcePath(selector) and outputPath(selector) bind immutable input and writable staging paths without embedding host paths. A check's source is its immutable subject. These values cannot grant staging authority to tasks/hooks. Required checks gate publication; exported commands and artifacts survive grip builder cache-clean because the native store owns retention.

cargoPackage(name, spec) returns a producer/package pair to spread into workspace.outputs. Its source must include Cargo.lock and the vendored dependency tree with .cargo/config.toml. Select a digest-pinned official Rust image with native linker tools, an explicit Linux gnu or musl target, and a map of public command names to Cargo binary targets. The helper runs release tests and builds with --frozen --offline, installs only the named binaries, and removes intermediate Cargo output before export. Cargo's vendor checksums remain enforced; the lockfile alone does not provide dependency bytes.

const service = cargoPackage("service", {
  source: fileFetch("service"),
  toolchain: "docker.io/library/rust@sha256:a10e64dd139b7387337c7fbe8aca31b959b57b2fd4c8ae20a02cf1d6ea424dce",
  target: { os: "linux", arch: "x86_64", abi: "musl" },
  binaries: { service: "service" },
});
// Include ...service in workspace.outputs.

image(name, { packages, target, base?, destinations?, config? }) uses the BuildKit OCI exporter, not a native image assembler. An omitted base means scratch; an explicit base must be digest-pinned and contributes no inherited runtime configuration. Package placements default to /opt/gripsack/<name>. destinations can set an explicit { path, owner: { uid, gid } }; overlapping placements and leftover base files inside a package prefix are rejected. config declares typed literal/package-command entrypoint, string args, env, absolute cwd, and numeric user: { uid, gid }.

The returned image path is an OCI archive. Native admission checks descriptor sizes/SHA-256, gzip DiffIDs, platform/configuration, timestamps, package bytes/links/modes/ownership and unselected payloads before publication, including cache hits. Linux x86_64 static, GNU dynamic and coherent Conda consumers have runtime evidence, including independent read-only/no-network OCI execution and clean exports. Other host/image platforms need their own qualification.

Profiles may select the same compatible environment used by grip run and deploy artifactFile or artifactTree sources. A cold apply realizes required packages before host mutation, then transfers its held lifecycle authority directly into the existing generation transaction. It does not require a manual pre-build. Exported command aliases and literal environment values persist with the generation; rollback reuses retained package bytes without a worker.

artifactTree expands selected paths/subtrees into individual owned file entries, with exclusions winning. It never replaces the destination directory: foreign children remain, removals use existing drift/prune policy, and expanded collisions fail before destination writes. Symlinks and special source entries are refused rather than followed. Per-file symlinks do not preserve the real sibling layout of a release tree: use trackedCopyTo for binaries that locate runtime files relative to their resolved executable path. This does not add a directory-level symlink or fixed-prefix relocation mechanism. Profile hooks and task postconditions execute; schedules, staged profile-file checks and host recipe execution remain explicitly gated. Historical v4 workspaces remain read-only pending A5 migration. check never bootstraps BuildKit or a scheduler. adopt does not edit workspace TypeScript automatically: declare the file policy and use explicit apply --take-over when reversible adoption of a foreign tracked-copy destination is intended.

Four graduated workspaces use the real SDK without a hostname shim or a synthetic package: dotfiles only, an offline-staged native tool and profile, a source recipe with alternate producer and typed command consumer, and manual plus scheduled task declarations. The ts-test gate strictly type-checks their source, and e2e admits each through the real grip check. The dotfile and native-tool profiles use the native lifecycle; the source-built tool uses the pinned BuildKit policy. Scheduler registration remains gated: a declared schedule activates nothing.

The legacy hosts/<name>.ts path emits a v7 modules-compatibility envelope. Strict retained readers preserve their versioned meanings; current fields are not silently added to an older declaration.

grip check --json emits one versioned document on stdout: {version: 1, ok, diagnostics, host?, outputs?, modules?, layouts?}. Diagnostics use the same codes, labels, spans and help as terminal output; failures exit nonzero. Readable snippets come only from files under the evaluated repo's pinned directory capability, at most 1 MiB per file. Out-of-repo labels remain visible without reading their contents; JSON carries diagnostic facts, not source-file bytes. Operational failures such as a denied trust gate still report stderr and may have no structured diagnostic in the JSON document.

Probes are requests, not effects

The sandbox cannot run probes, so ctx.probe.executable(name) / ctx.probe.file_exists(path) return the bound answer from the inputs (absent → false) and record unbound calls into the eval envelope as probe_requests. The core evaluates them (PATH lookup / absolute-path stat) and re-runs eval with the answers bound — a fixpoint, capped at 4 rounds. Probe results re-evaluate every run: plug in a GPU and the next plan changes with zero repo changes.

Probe a stable reference, never the tool's own installed presence — !ctx.probe.executable("node") && node oscillates, because installing the tool makes the next eval drop the module. Gate on the specific system path you must not overwrite: !ctx.probe.file_exists("/opt/vendor/bin/node") && node.

Evaluation

The core spawns the embedded driver under Deno with deny-by-default permissions:

deno run --no-remote --cached-only --no-lock --no-config \
    --node-modules-dir=manual --import-map=<captured frontend>/deno.json \
    --allow-read=<captured repo>,<captured frontend>,<optional captured pin>,<input file> \
    <captured frontend>/src/cli.ts <captured repo> --inputs <input file>

and reads one JSON line off stdout: {"ir": …, "diagnostics": [], "probe_requests": […]}. The IR (JSON) is the only contract — the Rust core never executes your code. A repo's own node_modules/@gripsack/core install still wins when it shadows the embedded copy (the deliberate-pin rule); stale pins fail with instructions.

Approval binds canonical repository identity, the copied read set and the runtime/grant policy. Ignored and untracked imports count as source; edits, pin changes and expanded native policy require renewed approval. Every probe round uses the same captured bundle. grip trust inspect --json exposes the inventory and fingerprints; non-interactive grip trust add requires both --bundle and --policy. Old path-only entries and GRIPSACK_TRUST_ALL=1 do not authorize evaluation.

API overview

| area | exports | |---|---| | hosts | defineEnv, Env, EnvContext, EnvFn | | workspace | defineWorkspace, workspace, WorkspaceValue, WorkspaceContext | | outputs | recipe, pkg, environment, task, schedule, check, image, profile, hook, provider, targetPlatform | | commands/files | exec (object or fluent), bash, bashBody, runBash, file, lit, artifact, hostPath, packageCommand, repoFile, artifactFile, identity, literalText, templateText, symlinkTo, trackedCopyTo, managedBlock, daily, weekly | | legacy modules | module, ModuleSpec, ModuleValue (A5 migration inventory) | | probes | ctx.probe (executable, file_exists), ProbeBuilder, ProbeKind | | facts | HostFacts (core-injected), when, hasTag, Condition | | captured inputs / pure locks | inputFile, inputDirectory, input, lock, ensureArtifact | | fetchers | githubRelease, tarball, git, fileFetch, pluginFetch, brew | | workspace environment sources | conda.environment, pixi.fromLock | | destinations | symlink, trackedCopy, merge, template | | dependencies | dep(module, { for? }) | | activation | service, fonts, desktopEntry, customHook | | steps | step, fetchStep, buildStep, installStep, configStep, runStep, shellStep | | verify | verifyBinary, verifyFile, verifyShell, verifyDeployed | | legacy resources | resource (A5 migration inventory; new workspaces use pure lock values) |

Compiler integrations explicitly import @gripsack/core/advanced for emitIr, emitWorkspaceIr, IR_VERSION, mergeTags, parseInputs, createProbeBuilder, Inputs, ProbeRequest and compiler node DTOs. These are absent from the ordinary installed-package root and its embedded import-map equivalent. Registry-reset utilities are internal, not advanced SDK. The driver resolves both entry points from the same deliberate pin; historical pins retain their original compiler ABI rather than mixing package instances.

inputFile/inputDirectory declare captured inputs; task context.mutable_paths describes live host paths instead. lock(scope, key) returns an immutable value: only locks reachable through returned task declarations enter the workspace. Equivalent scope/key pairs intern without import registration or reset calls. They grant neither worker ownership nor execution/network authority.

Build dependencies and IR v3

dep("compiler", { for: "build" }) supplies store artifacts to the consumer's build/custom/run steps through a prepended PATH and GRIP_DEP_COMPILER. A dependency referenced only by build edges has no destinations, activation hooks, or shell-profile exports; a runtime incoming edge wins. Both modules still belong in the host environment. Build closures follow build edges transitively, with graph-ordered PATH and deduplicated members. Runtime deps of a build tool still deploy normally. This is not a hermetic environment.

Core/TypeScript 0.36.0 uses IR v3. Remove module/step retries: the field was accepted but did not execute retries; it is now rejected. Update a pinned @gripsack/core to ^0.36.0, or remove it to use the embedded frontend. v1/v2 input is rejected before decoding. Dependency purposes still use for, not edge, with dep(name, { for: "build" }). Old lockfiles and generations remain readable; rollback restores files without rebuilding. GC retains closure paths for every retained generation.

Execution contracts

Explicit steps and declarative fields share source staging, config lint and module-level verification. Do not mix steps with fetch/build/install/config/activate fields.

Cross-module needs: ["producer:step"] waits for the producer module's pre-activation work, including verification. This is module-granular ordering, not a global step scheduler. It adds no deployment role or PATH export; use dep for those. Activation targets, self-qualified refs and cycles are rejected. Use sibling ids within a module.

Build, shell and run steps are cached artifact recipes. Declared outputs must exist after shell and run actions; omitting outputs does not mean "always run". Put repeatable activation effects in customHook instead.

Development

deno task test     # frontend contract + sandbox driver tests
npm run build      # tsc — typecheck + emit the npm dist

API is pre-alpha and will change with the IR schema (plan).