@sudo-ku/cli
v0.1.0
Published
Play sudo-ku.com in your terminal. Twelve terminal colour schemes, rendering where they were born.
Maintainers
Readme
sudo-ku
Sudoku in your terminal.
npx @sudo-ku/cliThe 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 manualWhat 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 quitIn 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. |
| 1–9 | 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 playingEsc 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;
npxcompiles nothing - one bundled file plus a 30-line version guard, no dependency tree to resolve
- zero runtime dependencies
--version,--helpand--list-themestouch 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 benchprepublishOnly 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.
