1 Commits
Author SHA1 Message Date
clawbot a2cc08cae9 make fmt and make fmt-check cover Markdown with prettier (closes #39)
check / check (push) Failing after 5s
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, configured in
.prettierrc with four-space indents and prose wrapped at 80 columns,
all copied from the sneak/prompts templates. script/bootstrap installs
node through a hash-verified nvm archive when it is missing, yarn at a
pinned version, and the yarn dependencies. README.md is reformatted by
make fmt, and its Entrypoints section says what fmt, fmt-check and
bootstrap now cover.

Model: opus-5-5
2026-10-04 01:03:24 +00:00
7 changed files with 195 additions and 59 deletions
+4
View File
@@ -0,0 +1,4 @@
{
"tabWidth": 4,
"proseWrap": "always"
}
+56 -55
View File
@@ -31,8 +31,8 @@ make build
``` ```
`make build` produces `./keyfunc`. Every deriving command needs a mnemonic; see `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 [Giving it the mnemonic](#giving-it-the-mnemonic) for where it is read from,
for example: then for example:
``` ```
./keyfunc ssh pub -n 0 --mnemonic-command 'secret get foo' ./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: order; the first one found wins:
1. `--mnemonic-command <command>`: a shell command, run with `sh -c`, whose 1. `--mnemonic-command <command>`: a shell command, run with `sh -c`, whose
standard output is the mnemonic. Example: `--mnemonic-command 'secret get standard output is the mnemonic. Example:
foo'`. Whitespace around the output is dropped. If the command exits with a `--mnemonic-command 'secret get foo'`. Whitespace around the output is
non-zero status, the tool prints its standard error and exits with status 1. dropped. If the command exits with a non-zero status, the tool prints its
2. Environment variable `KEYFUNC_MNEMONIC_COMMAND`: the same, as a shell standard error and exits with status 1.
command held in the environment. 2. Environment variable `KEYFUNC_MNEMONIC_COMMAND`: the same, as a shell command
held in the environment.
3. Environment variable `KEYFUNC_MNEMONIC`: the mnemonic itself. 3. Environment variable `KEYFUNC_MNEMONIC`: the mnemonic itself.
4. A prompt on the terminal with echo turned off. 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 `KEYFUNC_MNEMONIC` and `KEYFUNC_MNEMONIC_COMMAND` are removed from the
environment before the system `ssh` (`keyfunc ssh to`) and `sftp` environment before the system `ssh` (`keyfunc ssh to`) and `sftp`
(`keyfunc ssh install`) are started, so the mnemonic is never handed on to (`keyfunc ssh install`) are started, so the mnemonic is never handed on to them.
them.
Every command takes `--index` / `-n` and `--mnemonic-command`, and has `--help`. Every command takes `--index` / `-n` and `--mnemonic-command`, and has `--help`.
`keyfunc --version` prints the version. `make build` stamps it; a binary `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` ## SSH keys: `keyfunc ssh`
Only ed25519 keys are produced. The application number is `838372`, so the Only ed25519 keys are produced. The application number is `838372`, so the path
path is `m/83696968'/838372'/<n>'`. The 32 bytes from step 4 are the ed25519 is `m/83696968'/838372'/<n>'`. The 32 bytes from step 4 are the ed25519 seed.
seed.
Test vector, mnemonic Test vector, mnemonic
`abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about`: `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...]` ### `keyfunc ssh install <[user@]host> [-- sftp options...]`
Adds the `pub` line to `~/.ssh/authorized_keys` on the host. No command is run 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 on the host: the file is fetched, changed here, and written back with the system
system `sftp` client in batch mode. `sftp` client in batch mode.
The first connection lists `~/.ssh` and then fetches The first connection lists `~/.ssh` and then fetches `~/.ssh/authorized_keys`
`~/.ssh/authorized_keys` from it. The file reads as empty in two cases only: from it. The file reads as empty in two cases only: `sftp` reported `~/.ssh`
`sftp` reported `~/.ssh` itself as not being there, or the listing came up and itself as not being there, or the listing came up and the file was not in it.
the file was not in it. Any other outcome of that connection fails the run — a Any other outcome of that connection fails the run — a `~/.ssh` that is there
`~/.ssh` that is there but cannot be entered, an `authorized_keys` that is there but cannot be entered, an `authorized_keys` that is there but cannot be read, or
but cannot be read, or a connection that did not come up — and the tool prints a connection that did not come up — and the tool prints what `sftp` said and
what `sftp` said and exits with status 1 without writing anything, rather than exits with status 1 without writing anything, rather than put a file back
put a file back holding the new key alone. The listing is what tells a missing holding the new key alone. The listing is what tells a missing directory from
directory from one shut to the user, which `sftp` reports on a fetch the same one shut to the user, which `sftp` reports on a fetch the same way; the wording
way; the wording of a missing file elsewhere does not count either, since `ssh` of a missing file elsewhere does not count either, since `ssh` writes
writes `No such file or directory` about an `-i` it cannot find on a session `No such file or directory` about an `-i` it cannot find on a session that then
that then authenticates through the agent. If an identical line is already in authenticates through the agent. If an identical line is already in the file,
the file, the tool prints `already present` and connects no further. Otherwise the tool prints `already present` and connects no further. Otherwise the line is
the line is added (after a newline, if the file did not end with one) and a added (after a newline, if the file did not end with one) and a second
second connection: connection:
- makes `~/.ssh` and sets it to mode `0700`, but only when the first 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; 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 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 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 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 OpenSSH's POSIX rename extension, as OpenSSH's own server does; a server without
without it may refuse to rename onto a file that is already there. 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 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 with status 1. It names the uploaded file only when the step that failed was the
the upload or one after it, which is where a file of that name can be on the upload or one after it, which is where a file of that name can be on the host; a
host; a failure before the upload names none. Everything `sftp` failure before the upload names none. Everything `sftp` writes goes to standard
writes goes to standard error, so the tool's own standard output is only error, so the tool's own standard output is only `added` or `already present`.
`added` or `already present`.
Anything after `--` is passed to `sftp` unchanged, which is where the port goes 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 (`-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` ## age identities: `keyfunc age`
The application number is `657169`, path `m/83696968'/657169'/<n>'`. The 32 The application number is `657169`, path `m/83696968'/657169'/<n>'`. The 32
bytes from step 4 are clamped as X25519 requires and become an age identity, bytes from step 4 are clamped as X25519 requires and become an age identity, the
the same steps `sneak/secret` takes in its `agehd` package. `secret` derives at same steps `sneak/secret` takes in its `agehd` package. `secret` derives at a
a vendor-specific path today; for its keys to equal this tool's it moves to vendor-specific path today; for its keys to equal this tool's it moves to this
this path, which is a change in `secret`, not here. path, which is a change in `secret`, not here.
Test vectors, mnemonic Test vectors, mnemonic
`abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about`: `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]` ### `keyfunc mnemonic [-n N] [--words 12|18|24]`
Prints a child mnemonic derived from the main one, using BIP-85's own mnemonic Prints a child mnemonic derived from the main one, using BIP-85's own mnemonic
application (number `39`, English, path application (number `39`, English, path `m/83696968'/39'/0'/<words>'/<n>'`,
`m/83696968'/39'/0'/<words>'/<n>'`, entropy taken as the specification says, entropy taken as the specification says, not through step 4). Default 12 words.
not through step 4). Default 12 words. A child mnemonic is a full mnemonic in A child mnemonic is a full mnemonic in its own right: it can seed another
its own right: it can seed another `keyfunc`, another wallet, or `secret`, and `keyfunc`, another wallet, or `secret`, and it never has to be written down,
it never has to be written down, since it can be derived again. since it can be derived again.
Test vector: the child-mnemonic step is checked against BIP-85's own published Test vector: the child-mnemonic step is checked against BIP-85's own published
vectors, which derive from the specification's master key vectors, which derive from the specification's master key
@@ -273,19 +271,21 @@ girl mad pet galaxy egg matter matrix prison refuse sense ordinary nose
## Errors ## Errors
Errors go to standard error and the exit status is 1, except for `ssh to`, Errors go to standard error and the exit status is 1, except for `ssh to`, which
which passes through `ssh`'s own exit status. passes through `ssh`'s own exit status.
## Entrypoints ## Entrypoints
The repo adheres to the The repo adheres to the
[Scripts to Rule Them All](https://github.com/github/scripts-to-rule-them-all) [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 standard: most Makefile targets are thin shims over an executable in `script/`
`script/` (`build` and `clean` are the exceptions). (`build` and `clean` are the exceptions).
- `script/bootstrap` installs everything needed to build and develop (git, make, - `script/bootstrap` installs everything needed to build and develop (git, make,
Go), idempotently, from nix, apt, brew or apk; it does not install the linter, Go, node and yarn), idempotently, from nix, apt, brew or apk, with node at a
which only runs inside Docker. pinned version through nvm when it is not already installed; it then installs
the pinned prettier with yarn. It does not install the linter, which only runs
inside Docker.
- `script/setup` prepares a fresh clone: it runs `bootstrap`, then installs the - `script/setup` prepares a fresh clone: it runs `bootstrap`, then installs the
git pre-commit hook. git pre-commit hook.
- `script/projectname` prints the project name; other scripts call it so they - `script/projectname` prints the project name; other scripts call it so they
@@ -296,9 +296,11 @@ standard: most Makefile targets are thin shims over an executable in
- `script/lint` builds the `lint` phase of the `Dockerfile` alone, uncached: the - `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 linter, pinned by hash, runs inside the build, so a complaint fails it and
leaves no container behind. leaves no container behind.
- `script/fmt` formats the Go source in place. - `script/fmt` formats in place: the Go source with `go fmt`, then every
- `script/fmt-check` checks that formatting without writing, failing if anything Markdown file with prettier (four-space indents, prose wrapped at 80 columns).
is unformatted. - `script/fmt-check` checks the same files the same way without writing, failing
if anything is unformatted. Both need the node and yarn that `bootstrap`
installs.
- `script/check` runs `test`, `lint` and `fmt-check` and changes no files. - `script/check` runs `test`, `lint` and `fmt-check` and changes no files.
- `script/docker` builds the Docker image, uncached, tagged with the project - `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 name and stamped with the version `git describe` gives on the host. The image
@@ -315,7 +317,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: 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) - [#42 go-bip39 no longer exists upstream: keep it, or copy it into the repo?](https://git.eeqj.de/sneak/keyfunc/issues/42)
## License ## License
+5
View File
@@ -0,0 +1,5 @@
{
"devDependencies": {
"prettier": "3.8.1"
}
}
+80 -3
View File
@@ -3,13 +3,23 @@
# repo. Idempotent: every install is guarded by a check, so tools that # repo. Idempotent: every install is guarded by a check, so tools that
# are already there are left alone. Base tooling comes from nix, apt, # 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 # brew, or apk, detected in that order, and nothing is assumed to be
# present. The linter is not installed here: linting and testing run # present. Node is used directly if installed; otherwise it is installed
# only as phases of the Dockerfile, so Docker is what is needed for # at a pinned version via nvm (installing nvm itself first, from a
# them, and that is checked for rather than installed. # 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 set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
# 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="" PKGMGR=""
SUDO="" SUDO=""
@@ -50,6 +60,69 @@ missing() {
! command -v "$1" >/dev/null 2>&1 ! command -v "$1" >/dev/null 2>&1
} }
# verify_sha256 <file> <expected-hash>
verify_sha256() {
if command -v sha256sum >/dev/null 2>&1; then
actual="$(sha256sum "$1" | cut -d' ' -f1)"
else
actual="$(shasum -a 256 "$1" | cut -d' ' -f1)"
fi
if [ "$actual" != "$2" ]; then
echo "bootstrap: sha256 mismatch for $1" >&2
echo " expected: $2" >&2
echo " actual: $actual" >&2
exit 1
fi
}
# 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() { main() {
cd "$ROOT" cd "$ROOT"
@@ -59,6 +132,10 @@ main() {
go mod download go mod download
ensure_node
ensure_yarn
install_js_deps
if missing docker; then if missing docker; then
echo "bootstrap: docker is not installed; make lint and make test need it" >&2 echo "bootstrap: docker is not installed; make lint and make test need it" >&2
fi fi
+22 -1
View File
@@ -1,12 +1,33 @@
#!/bin/sh #!/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 set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
# 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() { main() {
cd "$ROOT" cd "$ROOT"
go fmt ./... go fmt ./...
run_yarn run prettier --write '**/*.md' --tab-width 4 --prose-wrap always
} }
main "$@" main "$@"
+20
View File
@@ -5,6 +5,25 @@ set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
# 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() { main() {
cd "$ROOT" cd "$ROOT"
if [ -n "$(gofmt -l .)" ]; then if [ -n "$(gofmt -l .)" ]; then
@@ -12,6 +31,7 @@ main() {
gofmt -l . gofmt -l .
exit 1 exit 1
fi fi
run_yarn run prettier --check '**/*.md' --tab-width 4 --prose-wrap always
} }
main "$@" main "$@"
+8
View File
@@ -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==