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

@sudo-ku/cli

v0.1.0

Published

Play sudo-ku.com in your terminal. Twelve terminal colour schemes, rendering where they were born.

Readme

sudo-ku

Sudoku in your terminal.

npx @sudo-ku/cli

The game is called sudo-ku because of sudo, and the web version already wears terminal chrome. The twelve themes are terminal colour schemes — Solarized, Gruvbox, Nord, Dracula, Tokyo Night, Catppuccin — so this is where they were always going to look right.

sudo-ku                       a random medium puzzle
sudo-ku evil                  a random evil one
sudo-ku --difficulty hard     the same, spelled out
sudo-ku PY6R-3B54-6V2V        that exact puzzle, any spelling
sudo-ku --theme gruvbox-dark  pin a theme, and remember it
sudo-ku --list-themes         all twelve, with swatches
sudo-ku --help                the manual

What it looks like

A real session, ? pressed, captured from a terminal:

$ sudo-ku --difficulty expert
                       0EWB-AXAQ-W91R
┃   │   │   ┃   │   │   ┃   │   │   ┃ │ $ keyboard
┃ 4 │ 7 │ 3 ┃ 5 │ 8 │ 1 ┃ 6 │ 2 │ 9 ┃ │
┃   │   │   ┃   │   │   ┃   │   │   ┃ │ ↑↓←→ hjkl  Move. Wraps at edges.
┃   │   │   ┃   │   │   ┃   │   │   ┃ │ 1 – 9      Enter a digit.
┃ 6 │ 1 │ 8 ┃ 2 │ 3 │ 9 ┃ 4 │ 7 │ 5 ┃ │ ! @ # $ …  The other mode.
┃   │   │   ┃   │   │   ┃   │   │   ┃ │ 0  bksp    Clear the cell.
┃   │   │   ┃   │   │   ┃   │   │   ┃ │ space      Swap entry mode.
┃ 9 │ 5 │ 2 [ 7 ] 4 │ 6 ┃ 8 │ 1 │ 3 ┃ │ u  ^Z      Undo.
┃   │   │   ┃   │   │   ┃   │   │   ┃ │ r  ^Y      Redo.
┣━━━┿━━━┿━━━╋━━━┿━━━┿━━━╋━━━┿━━━┿━━━┫ │ f          Pencil legal marks.
┃   │   │   ┃   │   │   ┃   │   │   ┃ │ a          Show computed marks.
┃ 5 │ 6 │ 4 ┃ 3 │ 7 │ 8 ┃ 1 │ 9 │ 2 ┃ │ c          Auto-clear marks.
┃   │   │   ┃   │   │   ┃   │   │   ┃ │ s          Show or hide the timer.
┃   │   │   ┃   │ 23│   ┃   │1  │   ┃ │ p          Pause. Hides the grid.
┃ 3 │ 8 │ 9 ┃ 1 │   │ 56┃   │  6│45 ┃ │ t          Themes.
┃   │   │   ┃   │   │  9┃78 │7  │   ┃ │ esc        Drop it, or close this.
┃12 │   │   ┃   │   │   ┃   │   │   ┃ │ q  ^C      Quit.
┃ 5 │   │ 7 ┃ 9 │ 6 │   ┃   │ 5 │   ┃ │
┃   │   │   ┃   │   │   ┃   │   │   ┃ │ esc or ? to close
┣━━━┿━━━┿━━━╋━━━┿━━━┿━━━╋━━━┿━━━┿━━━┫
┃   │   │   ┃   │   │   ┃   │   │   ┃
┃ 2 │   │   ┃ 8 │   │   ┃ 5 │   │   ┃
┃   │   │   ┃   │   │   ┃   │   │   ┃
┃   │   │   ┃   │   │   ┃   │   │   ┃
┃   │   │   ┃   │ 5 │   ┃ 9 │   │ 6 ┃
┃   │   │   ┃   │   │   ┃   │   │   ┃
┃   │   │   ┃   │   │   ┃   │   │   ┃
┃   │ 9 │ 5 ┃ 6 │   │   ┃   │ 4 │ 9 ┃
┃   │   │   ┃   │   │   ┃   │   │   ┃

00:08  value   55/81
  1   2   3   4   5   6   7   8   9
  4   4   4   4   1   2   4   3   ·
? keys   t themes   u undo   q quit

In colour, which is the point: docs/screenshots/cli-01-board-tokyo-night.png in the repository.

Each cell is three columns by three rows, which is what buys the candidate sub-grid: marks sit in a fixed 3×3 positioned by value, never packed, so a cell holding {5, 8} shows the 5 dead centre and the 8 below it and can be read at a glance. The keypad under the board counts what is left of each digit.

Keys

| Keys | Does | | ----------------------------------- | -------------------------------------------------------------------- | | , h j k l | Move. Wraps at the edges. | | 19 | Enter a digit. | | ! @ # $ % ^ & * ( | Shift+digit: the other mode, both ways. | | 0, Backspace, Delete | Clear the cell. Again clears its marks. | | Space | Swap between value and pencil entry. | | u, Ctrl+Z | Undo. | | r, Ctrl+Y | Redo. | | f | Pencil every legal candidate. | | a | Show computed candidates, without touching your own marks. | | c | Auto-clear marks from peers when a digit is placed. Persisted. | | s | Show or hide the timer. | | p | Pause. Hides the grid, because a paused game is not a readable one. | | t | Themes. / previews live, Enter keeps it, Esc puts it back. | | ? | The crib sheet, beside the board. | | Esc | Drop the selection, or close what is open. | | q, Ctrl+C | Quit. | | Ctrl+L | Redraw, if something else scribbled on the screen. |

Shift+digit inverts the mode in both directions: in value mode it pencils a mark, and in pencil mode it writes a value. That is the rule from the brief, and it costs nothing here because a terminal reports Shift+4 as $ and the engine already knows what $ means.

Two bindings differ from the web on purpose. A terminal transmits Ctrl+Z and Ctrl+Shift+Z as the same byte — the shift never reaches the wire — so redo is r or Ctrl+Y, and Ctrl+Shift+F becomes f.

When you finish

A full grid with no conflict is the answer, because the puzzle has exactly one, so it is sent for verification without asking. A full grid with a conflict is the one state the client cannot speak for: it knows two cells are arguing, never which of them is wrong. So it asks the server how far off you are, and then asks you:

You still have 2 errors.   e  highlight them    esc  keep playing

Esc costs nothing. e marks the cells and is recorded as assistance, which keeps that solve out of the shared statistics — the prompt says so before it is pressed. The marks clear on your next edit, so they cannot be used as a live oracle.

When the window is small

The board is 37 columns by 29 rows and that is not negotiable, so the layout is a ladder of complete states rather than a fluid grid:

| Window | What you get | | ------------- | -------------------------------------------------------------- | | 76×35 or more | Full board, keypad, and the crib sheet beside it | | 37×29 | Full board; chrome drops off before the grid does | | 37×17 | Compact board — one row per cell, so no candidate marks | | 37×11 | Compact board alone | | smaller | A sentence naming both numbers, and it redraws when you resize |

Resizing mid-game is fine: it redraws, and the game is untouched.

It needs a connection

Puzzles come from the server. There is no offline mode.

Generation and grading live in an Elixir service, and they are frozen: a puzzle id deterministically rebuilds one exact grid, forever, so a second generator in TypeScript would be a second thing that could drift from the first. Every id anyone has ever shared would quietly start naming a different puzzle. That is not a trade worth making for an aeroplane.

Solving is entirely local. Once a puzzle has arrived there is no round trip until you finish it: conflicts, candidates, undo and the timer are all @sudo-ku/core, running here.

Point it somewhere else with SUDO_KU_API=http://localhost:4000 sudo-ku, or --api.

Colour

Twelve themes, and the terminal gets whichever of them you pick — 24-bit where COLORTERM says so, quantised to the xterm 256 cube where it does not.

NO_COLOR is honoured, and the board is still completely playable without it: box boundaries are a heavier glyph rather than a darker one, clues are bold, the active cell is bracketed, and a conflict is reversed. Only the peer tints go, and they are ambient hints by design.

A pipe is honoured too: sudo-ku --list-themes | grep nord returns text, not escape sequences. --color=always or FORCE_COLOR overrides that; --no-color overrides everything.

Box-drawing characters need a UTF-8 locale. Without one the grid falls back to |, - and +, which is plainer but correct.

What the terminal does differently

Four deliberate divergences from the web app, all of them visible:

The default theme is sudo-dark, not sudo-light. An unstyled web page is white; a terminal that has said nothing about itself is far more likely to be dark, and guessing wrong means dark ink on a dark background on the very first run. COLORFGBG is read as a stand-in for prefers-color-scheme when the terminal sets it.

The active cell is brackets plus the hardware cursor. The brief makes it a ring rather than a fill so that marking it costs nothing from the contrast budget the digits need. A terminal has no ring, but it has two channels that are equally free, and neither eats a pixel of the fill or the ink. A reverse-video fill — the obvious terminal answer — is exactly the trade that decision rejects.

The four highlight switches are on and cannot be turned off. Row and column, box, identical numbers and conflicts are four independent settings on the web. The CLI's config file is a closed allowlist and the engine's settings object carries only two switches, so there is nowhere to keep four more booleans without changing a package shared with the web app. It ships with the documented defaults, which is all four on.

A game is not saved. Quitting with work in progress asks first, and that is the whole safety net: there is no resume. The puzzle id is a permalink, so the same grid comes back with sudo-ku <id>, but the digits you had entered do not.

What it keeps

One file, and you can see where with sudo-ku --config-path:

| platform | location | | -------- | ---------------------------------------------- | | macOS | ~/Library/Application Support/sudo-ku | | Linux | $XDG_CONFIG_HOME/sudo-ku, else ~/.config/… | | Windows | %APPDATA%\sudo-ku |

It holds a theme id, two gameplay switches, the last difficulty played, and an anonymous player token. Nothing in it identifies you, because there is no field for it anywhere in this game — not in the file, not in the API, not in the database. sudo-ku --sign-out deletes the token; --no-save never writes one.

SUDO_KU_CONFIG_DIR moves the file. SUDO_KU_TOKEN replaces the token without writing anything.

Accounts

Playing needs no account. An anonymous identity is minted on the first puzzle and carries your streaks.

Accounts are passkey-only, and passkeys cannot work in a terminal — there is no browser and no authenticator ceremony. The fallback the product already has is a generated 8-word recovery passphrase, which is a very good terminal credential: it was drawn from the EFF long wordlist precisely so it could be written down, read aloud and typed in.

sudo-ku --sign-in takes it, never echoes it, and never accepts it on the command line, where the shell's history file and ps would both see it. Piping works: pass show sudo-ku | sudo-ku --sign-in.

Read what it prints. Redeeming a phrase proves the account, but the session the server issues for it lasts ten minutes and its only permitted next step is enrolling a passkey — which needs a browser. So today --sign-in confirms the account rather than moving your history onto this machine. To carry a history across right now, sign in on the web and put the player token in SUDO_KU_TOKEN.

When it goes wrong

Every failure is a sentence and an exit code, never a stack trace. The codes follow sysexits(3), so a shell script wrapping this gets what it expects:

| code | meaning | | ---- | -------------------------------------------------------------- | | 0 | fine | | 1 | the game said no — a puzzle id that names nothing, say | | 2 | the command line was wrong | | 69 | offline, timed out, or the service is down | | 70 | our bug; it prints the stack, after putting your terminal back | | 78 | the environment is wrong — an unsupported Node, or no terminal | | 130 | interrupted |

The terminal is restored on every one of those paths, including a crash and including SIGTERM: the alternate screen is left, the cursor comes back, and raw mode is handed back so your shell still echoes what you type.

Start-up

npx @sudo-ku/cli should feel instant, so:

  • published as ESM, built ahead of time; npx compiles nothing
  • one bundled file plus a 30-line version guard, no dependency tree to resolve
  • zero runtime dependencies
  • --version, --help and --list-themes touch neither disk nor network

Measured on an M-series laptop, 20 runs: --version has a median of 45 ms against an empty node --version floor of 25 ms, so the package itself costs about 20 ms. A playable board over the real network arrives in a median of 244 ms, which is the round trip and not the program.

pnpm --filter ./apps/cli bench measures the first number against that floor, so a regression shows up as a delta over Node rather than a number that drifts with the machine.

Node

Node 22 or newer, which is the oldest release still getting security updates. An older one gets a sentence telling it so, and exit 78 — not a stack trace about optional chaining.

Building it

pnpm --filter ./apps/cli build       # esbuild, two outputs, into dist/
pnpm --filter ./apps/cli test
pnpm --filter ./apps/cli typecheck
pnpm --filter ./apps/cli bench

prepublishOnly runs the type check, the tests and the build, so a broken tree cannot be published.

Both @sudo-ku/core and @sudo-ku/theme are private workspace packages that export raw TypeScript, so they are bundled in rather than depended on — that is why there is a bundler here at all, and why they sit in devDependencies.

Licence

Part of sudo-ku.com. See the repository root.