From 1ff02f252d8cbb0de925951e675f24b3eddaf0e0 Mon Sep 17 00:00:00 2001 From: clawbot <35+clawbot@noreply.example.org> Date: Sun, 4 Oct 2026 01:03:24 +0000 Subject: [PATCH] make fmt and make fmt-check cover Markdown with prettier (closes #39) script/fmt now runs go fmt and then prettier --write on every Markdown file; script/fmt-check runs the gofmt check and then prettier --check. prettier is pinned in package.json and yarn.lock and configured in .prettierrc with four-space indents and prose wrapped at 80 columns, all copied from the sneak/prompts templates. After Go and the Go modules, script/bootstrap uses an installed node or else a pinned one through a hash-verified nvm archive, then an installed yarn or else a pinned one, then the yarn dependencies. README.md is reformatted by make fmt, and its Entrypoints section says what fmt, fmt-check and bootstrap now do. Model: opus-5-5 --- .prettierrc | 4 ++ README.md | 129 +++++++++++++++++++++++++---------------------- package.json | 5 ++ script/bootstrap | 70 +++++++++++++++++++++++-- script/fmt | 23 ++++++++- script/fmt-check | 20 ++++++++ yarn.lock | 8 +++ 7 files changed, 194 insertions(+), 65 deletions(-) create mode 100644 .prettierrc create mode 100644 package.json create mode 100644 yarn.lock diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..8af31cd --- /dev/null +++ b/.prettierrc @@ -0,0 +1,4 @@ +{ + "tabWidth": 4, + "proseWrap": "always" +} diff --git a/README.md b/README.md index 043ef54..fda0e0a 100644 --- a/README.md +++ b/README.md @@ -31,8 +31,8 @@ make build ``` `make build` produces `./keyfunc`. Every deriving command needs a mnemonic; see -[Giving it the mnemonic](#giving-it-the-mnemonic) for where it is read from, then -for example: +[Giving it the mnemonic](#giving-it-the-mnemonic) for where it is read from, +then for example: ``` ./keyfunc ssh pub -n 0 --mnemonic-command 'secret get foo' @@ -105,11 +105,12 @@ The mnemonic itself is never a command-line argument. It is looked for in this order; the first one found wins: 1. `--mnemonic-command `: a shell command, run with `sh -c`, whose - standard output is the mnemonic. Example: `--mnemonic-command 'secret get - foo'`. Whitespace around the output is dropped. If the command exits with a - non-zero status, the tool prints its standard error and exits with status 1. -2. Environment variable `KEYFUNC_MNEMONIC_COMMAND`: the same, as a shell - command held in the environment. + standard output is the mnemonic. Example: + `--mnemonic-command 'secret get foo'`. Whitespace around the output is + dropped. If the command exits with a non-zero status, the tool prints its + standard error and exits with status 1. +2. Environment variable `KEYFUNC_MNEMONIC_COMMAND`: the same, as a shell command + held in the environment. 3. Environment variable `KEYFUNC_MNEMONIC`: the mnemonic itself. 4. A prompt on the terminal with echo turned off. @@ -119,8 +120,7 @@ refused with a message saying so. `KEYFUNC_MNEMONIC` and `KEYFUNC_MNEMONIC_COMMAND` are removed from the environment before the system `ssh` (`keyfunc ssh to`) and `sftp` -(`keyfunc ssh install`) are started, so the mnemonic is never handed on to -them. +(`keyfunc ssh install`) are started, so the mnemonic is never handed on to them. Every command takes `--index` / `-n` and `--mnemonic-command`, and has `--help`. `keyfunc --version` prints the version. `make build` stamps it; a binary @@ -128,9 +128,8 @@ installed with `go install` reports the module version instead. ## SSH keys: `keyfunc ssh` -Only ed25519 keys are produced. The application number is `838372`, so the -path is `m/83696968'/838372'/'`. The 32 bytes from step 4 are the ed25519 -seed. +Only ed25519 keys are produced. The application number is `838372`, so the path +is `m/83696968'/838372'/'`. The 32 bytes from step 4 are the ed25519 seed. Test vector, mnemonic `abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about`: @@ -160,24 +159,24 @@ same as for `pub`. ### `keyfunc ssh install <[user@]host> [-- sftp options...]` Adds the `pub` line to `~/.ssh/authorized_keys` on the host. No command is run -on the host: the file is fetched, changed here, and written back with the -system `sftp` client in batch mode. +on the host: the file is fetched, changed here, and written back with the system +`sftp` client in batch mode. -The first connection lists `~/.ssh` and then fetches -`~/.ssh/authorized_keys` from it. The file reads as empty in two cases only: -`sftp` reported `~/.ssh` itself as not being there, or the listing came up and -the file was not in it. Any other outcome of that connection fails the run — a -`~/.ssh` that is there but cannot be entered, an `authorized_keys` that is there -but cannot be read, or a connection that did not come up — and the tool prints -what `sftp` said and exits with status 1 without writing anything, rather than -put a file back holding the new key alone. The listing is what tells a missing -directory from one shut to the user, which `sftp` reports on a fetch the same -way; the wording of a missing file elsewhere does not count either, since `ssh` -writes `No such file or directory` about an `-i` it cannot find on a session -that then authenticates through the agent. If an identical line is already in -the file, the tool prints `already present` and connects no further. Otherwise -the line is added (after a newline, if the file did not end with one) and a -second connection: +The first connection lists `~/.ssh` and then fetches `~/.ssh/authorized_keys` +from it. The file reads as empty in two cases only: `sftp` reported `~/.ssh` +itself as not being there, or the listing came up and the file was not in it. +Any other outcome of that connection fails the run — a `~/.ssh` that is there +but cannot be entered, an `authorized_keys` that is there but cannot be read, or +a connection that did not come up — and the tool prints what `sftp` said and +exits with status 1 without writing anything, rather than put a file back +holding the new key alone. The listing is what tells a missing directory from +one shut to the user, which `sftp` reports on a fetch the same way; the wording +of a missing file elsewhere does not count either, since `ssh` writes +`No such file or directory` about an `-i` it cannot find on a session that then +authenticates through the agent. If an identical line is already in the file, +the tool prints `already present` and connects no further. Otherwise the line is +added (after a newline, if the file did not end with one) and a second +connection: - makes `~/.ssh` and sets it to mode `0700`, but only when the first connection found none; a `~/.ssh` that was already there keeps the mode it had; @@ -188,15 +187,14 @@ second connection: The tool then prints `added`. So a run that adds a line connects twice. The rename is the step that either happens or does not: the file on the host is never half-written. `sftp` does it in one step against servers that offer -OpenSSH's POSIX rename extension, as OpenSSH's own server does; a server -without it may refuse to rename onto a file that is already there. +OpenSSH's POSIX rename extension, as OpenSSH's own server does; a server without +it may refuse to rename onto a file that is already there. If a step fails, the tool prints what `sftp` said, removes nothing, and exits -with status 1. It names the uploaded file only when the step that failed was -the upload or one after it, which is where a file of that name can be on the -host; a failure before the upload names none. Everything `sftp` -writes goes to standard error, so the tool's own standard output is only -`added` or `already present`. +with status 1. It names the uploaded file only when the step that failed was the +upload or one after it, which is where a file of that name can be on the host; a +failure before the upload names none. Everything `sftp` writes goes to standard +error, so the tool's own standard output is only `added` or `already present`. Anything after `--` is passed to `sftp` unchanged, which is where the port goes (`-P 2222`, not `-p`). How the connection authenticates is up to the user's @@ -216,10 +214,10 @@ and the tool then exits with status 1 unless `ssh` reported one of its own. ## age identities: `keyfunc age` The application number is `657169`, path `m/83696968'/657169'/'`. The 32 -bytes from step 4 are clamped as X25519 requires and become an age identity, -the same steps `sneak/secret` takes in its `agehd` package. `secret` derives at -a vendor-specific path today; for its keys to equal this tool's it moves to -this path, which is a change in `secret`, not here. +bytes from step 4 are clamped as X25519 requires and become an age identity, the +same steps `sneak/secret` takes in its `agehd` package. `secret` derives at a +vendor-specific path today; for its keys to equal this tool's it moves to this +path, which is a change in `secret`, not here. Test vectors, mnemonic `abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about`: @@ -256,11 +254,11 @@ says so and exits with status 1. ### `keyfunc mnemonic [-n N] [--words 12|18|24]` Prints a child mnemonic derived from the main one, using BIP-85's own mnemonic -application (number `39`, English, path -`m/83696968'/39'/0'/'/'`, entropy taken as the specification says, -not through step 4). Default 12 words. A child mnemonic is a full mnemonic in -its own right: it can seed another `keyfunc`, another wallet, or `secret`, and -it never has to be written down, since it can be derived again. +application (number `39`, English, path `m/83696968'/39'/0'/'/'`, +entropy taken as the specification says, not through step 4). Default 12 words. +A child mnemonic is a full mnemonic in its own right: it can seed another +`keyfunc`, another wallet, or `secret`, and it never has to be written down, +since it can be derived again. Test vector: the child-mnemonic step is checked against BIP-85's own published vectors, which derive from the specification's master key @@ -273,23 +271,33 @@ girl mad pet galaxy egg matter matrix prison refuse sense ordinary nose ## Errors -Errors go to standard error and the exit status is 1, except for `ssh to`, -which passes through `ssh`'s own exit status. +Errors go to standard error and the exit status is 1, except for `ssh to`, which +passes through `ssh`'s own exit status. ## Entrypoints The repo adheres to the [Scripts to Rule Them All](https://github.com/github/scripts-to-rule-them-all) -standard: most Makefile targets are thin shims over an executable in -`script/` (`build` and `clean` are the exceptions). +standard: most Makefile targets are thin shims over an executable in `script/` +(`build` and `clean` are the exceptions). -- `script/bootstrap` installs everything needed to build and develop, - idempotently: git and make from nix, apt, brew or apk, and Go, at the version - the `Dockerfile`'s Go image carries, from the official release archive - (checked against a sha256 in the script) into `~/.local/go`. `script/fmt`, - `script/fmt-check`, `script/precommit` and the `Makefile` put - `~/.local/go/bin` first on their `PATH`, so they use that Go. It does not - install the linter, which only runs inside Docker. +- `script/bootstrap` installs, idempotently, everything needed to build and + develop apart from Docker, which it only warns about when it is missing, and + the linter, which only runs inside Docker. In this order: + - git and make from nix, apt, brew or apk, and from there too curl and bash + when a later step needs them; + - Go at the version the `Dockerfile`'s Go image carries, from the official + release archive at go.dev (checked against a sha256 in the script) into + `~/.local/go`, unless the `go` first on the `PATH` already is that + version; then the Go modules. `script/bootstrap` itself, `script/fmt`, + `script/fmt-check`, `script/precommit` and the `Makefile` put + `~/.local/go/bin` first on their `PATH`, so they use that Go; + - node: an installed one is used as it is, otherwise a pinned one is + installed through nvm, which, when it is missing, comes from its release + archive, checked against a sha256; + - yarn: an installed one is used as it is, otherwise the pinned version + through corepack, or through npm where there is no corepack; + - the pinned prettier, through yarn. - `script/setup` prepares a fresh clone: it runs `bootstrap`, then installs the git pre-commit hook. - `script/projectname` prints the project name; other scripts call it so they @@ -300,9 +308,11 @@ standard: most Makefile targets are thin shims over an executable in - `script/lint` builds the `lint` phase of the `Dockerfile` alone, uncached: the linter, pinned by hash, runs inside the build, so a complaint fails it and leaves no container behind. -- `script/fmt` formats the Go source in place. -- `script/fmt-check` checks that formatting without writing, failing if anything - is unformatted. +- `script/fmt` formats in place: the Go source with `go fmt`, then every + Markdown file with prettier (four-space indents, prose wrapped at 80 columns). +- `script/fmt-check` checks the same files the same way without writing, failing + if anything is unformatted. Both need the node, yarn and prettier that + `bootstrap` installs. - `script/check` runs `test`, `lint` and `fmt-check` and changes no files. - `script/docker` builds the Docker image, uncached, tagged with the project name and stamped with the version `git describe` gives on the host. The image @@ -319,7 +329,6 @@ standard: most Makefile targets are thin shims over an executable in The open issues that stand between the tree and a 1.0 release: -- [#39 make fmt and make fmt-check cover Markdown with prettier](https://git.eeqj.de/sneak/keyfunc/issues/39) - [#42 go-bip39 no longer exists upstream: keep it, or copy it into the repo?](https://git.eeqj.de/sneak/keyfunc/issues/42) ## License diff --git a/package.json b/package.json new file mode 100644 index 0000000..dc05cde --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "devDependencies": { + "prettier": "3.8.1" + } +} diff --git a/script/bootstrap b/script/bootstrap index 33d46f2..d7d1668 100755 --- a/script/bootstrap +++ b/script/bootstrap @@ -4,10 +4,13 @@ # are already there are left alone. Base tooling comes from nix, apt, # brew, or apk, detected in that order, and nothing is assumed to be # present. Go is installed at the version the Dockerfile's Go image -# carries, from the official release archive, into ~/.local/go. The -# linter is not installed here: linting and testing run only as phases -# of the Dockerfile, so Docker is what is needed for them, and that is -# checked for rather than installed. +# carries, from the official release archive, into ~/.local/go. Node is +# used directly if installed; otherwise it is installed at a pinned +# version via nvm (installing nvm itself first, from a hash-verified +# release archive, never curl | sh). The linter is not installed here: +# linting and testing run only as phases of the Dockerfile, so Docker is +# what is needed for them, and that is checked for rather than +# installed. set -eu ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" @@ -22,6 +25,13 @@ GO_VERSION="1.26.8" GO_DIR="$HOME/.local/go" PATH="$GO_DIR/bin:$PATH" +# Pinned versions, 2026-07-06 +NODE_VERSION="22.17.0" +NVM_VERSION="0.40.3" +# sha256 of https://github.com/nvm-sh/nvm/archive/refs/tags/v0.40.3.tar.gz +NVM_SHA256="5f4d6aaa04a177dc93c985e31dbc411ab6b8c6e1e21d8015dbc1372625fcd1d0" +YARN_VERSION="1.22.22" + PKGMGR="" SUDO="" @@ -126,6 +136,54 @@ install_go() { rm -rf "$tmp" } +# nvm is a bash script; run a command in a bash with nvm loaded +nvm_sh() { + bash -c ". \"\$HOME/.nvm/nvm.sh\" && $*" +} + +ensure_nvm() { + [ -s "$HOME/.nvm/nvm.sh" ] && return 0 + # nvm prerequisites; nvm itself requires bash + if missing bash; then pkg_install bash bash bash bash; fi + if missing curl; then pkg_install curl curl curl curl; fi + if missing git; then pkg_install git git git git; fi + tmp="$(mktemp -d)" + curl -fsSL -o "$tmp/nvm.tar.gz" \ + "https://github.com/nvm-sh/nvm/archive/refs/tags/v${NVM_VERSION}.tar.gz" + verify_sha256 "$tmp/nvm.tar.gz" "$NVM_SHA256" + mkdir -p "$HOME/.nvm" + tar -xzf "$tmp/nvm.tar.gz" -C "$HOME/.nvm" --strip-components=1 + rm -rf "$tmp" +} + +ensure_node() { + if ! missing node; then return 0; fi + ensure_nvm + nvm_sh "nvm install $NODE_VERSION" +} + +ensure_yarn() { + if ! missing yarn; then return 0; fi + if ! missing corepack; then + corepack enable + corepack prepare "yarn@$YARN_VERSION" --activate + elif [ -s "$HOME/.nvm/nvm.sh" ]; then + nvm_sh "nvm use $NODE_VERSION >/dev/null && corepack enable && \ + corepack prepare yarn@$YARN_VERSION --activate" + else + npm install -g "yarn@$YARN_VERSION" + fi +} + +install_js_deps() { + if missing yarn && [ -s "$HOME/.nvm/nvm.sh" ]; then + nvm_sh "nvm use $NODE_VERSION >/dev/null && cd \"$ROOT\" && \ + yarn install --frozen-lockfile" + else + yarn install --frozen-lockfile + fi +} + main() { cd "$ROOT" @@ -144,6 +202,10 @@ main() { go mod download + ensure_node + ensure_yarn + install_js_deps + if missing docker; then echo "bootstrap: docker is not installed; make lint and make test need it" >&2 fi diff --git a/script/fmt b/script/fmt index 10a62ff..c8598b2 100755 --- a/script/fmt +++ b/script/fmt @@ -1,5 +1,6 @@ #!/bin/sh -# script/fmt: format all files (writes). +# script/fmt: format all files (writes): the Go source with go fmt, then +# the Markdown files with prettier. set -eu ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" @@ -7,9 +8,29 @@ ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" # Where script/bootstrap installs Go; it cannot put it on our PATH. PATH="$HOME/.local/go/bin:$PATH" +# Must match the pin in script/bootstrap. +NODE_VERSION="22.17.0" + +# script/bootstrap installs node and yarn under nvm and leaves neither +# on the PATH of the shell that called it, so resolve the pinned +# toolchain here the way bootstrap's own install step does. nvm is a +# bash script, hence the subshell. +run_yarn() { + if command -v yarn >/dev/null 2>&1; then + exec yarn "$@" + fi + if [ ! -s "$HOME/.nvm/nvm.sh" ]; then + echo "fmt: no yarn; run script/bootstrap first" >&2 + exit 1 + fi + exec bash -c '. "$HOME/.nvm/nvm.sh" && nvm use "$1" >/dev/null && + shift && exec yarn "$@"' bash "$NODE_VERSION" "$@" +} + main() { cd "$ROOT" go fmt ./... + run_yarn run prettier --write '**/*.md' --tab-width 4 --prose-wrap always } main "$@" diff --git a/script/fmt-check b/script/fmt-check index 8170456..b66223e 100755 --- a/script/fmt-check +++ b/script/fmt-check @@ -8,6 +8,25 @@ ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" # Where script/bootstrap installs Go; it cannot put it on our PATH. PATH="$HOME/.local/go/bin:$PATH" +# Must match the pin in script/bootstrap. +NODE_VERSION="22.17.0" + +# script/bootstrap installs node and yarn under nvm and leaves neither +# on the PATH of the shell that called it, so resolve the pinned +# toolchain here the way bootstrap's own install step does. nvm is a +# bash script, hence the subshell. +run_yarn() { + if command -v yarn >/dev/null 2>&1; then + exec yarn "$@" + fi + if [ ! -s "$HOME/.nvm/nvm.sh" ]; then + echo "fmt-check: no yarn; run script/bootstrap first" >&2 + exit 1 + fi + exec bash -c '. "$HOME/.nvm/nvm.sh" && nvm use "$1" >/dev/null && + shift && exec yarn "$@"' bash "$NODE_VERSION" "$@" +} + main() { cd "$ROOT" if [ -n "$(gofmt -l .)" ]; then @@ -15,6 +34,7 @@ main() { gofmt -l . exit 1 fi + run_yarn run prettier --check '**/*.md' --tab-width 4 --prose-wrap always } main "$@" diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..d846639 --- /dev/null +++ b/yarn.lock @@ -0,0 +1,8 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +prettier@3.8.1: + version "3.8.1" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.8.1.tgz#edf48977cf991558f4fcbd8a3ba6015ba2a3a173" + integrity sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==