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

@profullstack/sh1pt-automation-browser

v0.3.4

Published

Reusable Playwright recipes for the console chores that have no API: OAuth consent screens, app review settings, redirect URIs.

Readme

@profullstack/sh1pt-automation-browser

Reusable Playwright recipes for the provider chores that have no CLI and no API.

sh1pt prefers, in order: an official CLI, an official API, an unofficial API, an MCP server, and only then a browser. This package is that last rung. It exists because some settings genuinely have no other door — Google's OAuth consent screen being the standard example. gcloud covers the rest of Google Cloud, but test users, publishing status and a client's redirect URIs are console pages, and an app left in Testing with no test users answers every sign-in with Error 403: access_denied.

sh1pt browser list
sh1pt browser google-cloud-oauth status --project 1234567890
sh1pt browser google-cloud-oauth add-test-users --project 1234567890 --email [email protected]
sh1pt browser google-cloud-oauth publish --project 1234567890

playwright is an optional peer dependency; install it where you run the recipes.

Trusted publishers

PyPI and RubyGems let a GitHub workflow publish with no long-lived token, and neither exposes that setting to an API. PyPI's upload API publishes packages rather than account settings; RubyGems keeps publishers under the profile and gem has no command for them. Both are therefore browser work.

The form worth automating is the pending publisher. It names a package that does not exist yet and brings it into being on the first publish, so a brand-new project ships without a token ever being minted. Registering four of them by hand across two registries is exactly the chore this package is for.

sh1pt browser pypi-trusted-publisher add-pending \
  --package profullstack-x402-gateway --owner profullstack \
  --repo x402-ports --workflow release-python.yml

sh1pt browser rubygems-trusted-publisher add-pending \
  --package x402-gateway --owner profullstack \
  --repo x402-ports --workflow release-ruby.yml

sh1pt browser pypi-trusted-publisher list

Both actions are idempotent: add-pending reads the existing list first and returns { "added": false, "alreadyPresent": true } rather than creating a duplicate, so a fleet script can call it unconditionally.

Leave --environment off unless the workflow declares a GitHub environment. Both registries match it exactly, so a value set against a workflow that has none rejects every publish. The recipes always write the field, empty included, so a stale value cannot survive an edit.

Credentials come from the environment, never from flags, so nothing lands in shell history:

| Variable | Used for | | --- | --- | | PYPI_USERNAME, PYPI_PASSWORD | PyPI sign-in | | PYPI_TOTP_SECRET | optional; the base32 seed, for an unattended run | | RUBYGEMS_USERNAME, RUBYGEMS_PASSWORD | RubyGems sign-in | | RUBYGEMS_TOTP_SECRET | optional; same |

Both registries require two-factor on any account that can publish. With a seed the run is unattended. Without one it parks on session.ask() and waits for you to write the code into the answer file it names, which is worth having because a seed is not always available. A security key instead of an authenticator cannot be driven headlessly at all, and the PyPI recipe says so rather than hanging.

The profile is the point

The clicking is easy. The sign-in is not: providers challenge a fresh browser with 2FA, device confirmation and "this browser may not be secure". So a session is a persistent Chrome profile under ~/.config/sh1pt/browser/profiles/<name>. Sign in once, answer whatever is asked, and every later run of every recipe reuses it unattended.

When something can only be answered by a person, a recipe calls session.ask(). That writes a screenshot and a question into the run's artifacts directory and waits for an answer file, so an unattended box parks instead of failing.

Three things that decide whether a sign-in works

All in session.ts, all learned the hard way against Google:

  1. A real Chrome in new headless mode. Playwright's headless: true asks for the old headless binary, which sign-in pages recognise and refuse. The session finds a system Chrome and passes --headless=new itself.
  2. A desktop user agent. New headless still reports HeadlessChrome, and Google answers that with a stripped-down flow that will not take a password at all. desktopUserAgent() spells the binary's real version the way a desktop Chrome would.
  3. A virtual authenticator. With no authenticator present, a passkey prompt never settles: the page sits on "Verifying it's you…" and even its own "Try another way" button does nothing, because the flow is still waiting. Registering Chrome's DevTools virtual authenticator makes the request fail the way an empty security key would, and the site offers its password fallback.

Writing a recipe

A recipe is a plain function over a Session, so it composes and tests like any other code.

import { openSession, clickFirst } from '@profullstack/sh1pt-automation-browser';

const session = await openSession({ profile: 'acme' });
await session.page.goto('https://console.example.com/settings');
await clickFirst(session.page, ['button:has-text("Save")', 'button:has-text("SAVE")']);
await session.close();

clickFirst takes every wording you have seen for the same button and tries them in order, then falls back from a normal click to a forced one to the element's own handler. Consoles rename and re-nest their buttons constantly; pinning one selector rots.

Add the recipe to RECIPES in index.ts so sh1pt browser list shows it, and say there why a browser is needed rather than an API.