Compare commits
21
Commits
5a5577c7a5
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7feb212d04 | ||
|
|
538eca9f5d | ||
|
|
09ec70009d | ||
|
|
e1b385f847 | ||
|
|
897dc9aced | ||
|
|
ad1d51a9f9 | ||
|
|
fe6dbba428 | ||
|
|
ea6faaecff | ||
|
|
5932cc12ee | ||
|
|
9def7ba586 | ||
|
|
4d45aa4ca3 | ||
|
|
f53b6aad42 | ||
|
|
c9ce5ff442 | ||
|
|
b7028fd7e3 | ||
|
|
02386a6ff1 | ||
|
|
4d27e30ea8 | ||
|
|
157e5ef124 | ||
|
|
7be0fa7060 | ||
|
|
386155a480 | ||
|
|
3510f8a144 | ||
|
|
6f9dcc8f93 |
No files matched your search
@@ -0,0 +1,80 @@
|
|||||||
|
# .dockerignore does NOT use .gitignore semantics. Docker matches with
|
||||||
|
# moby/patternmatcher: filepath.Match plus `**`, so `*` does not cross
|
||||||
|
# `/` and an unprefixed pattern is anchored at the context root. Every
|
||||||
|
# depth-independent pattern therefore needs `**/`, or `config/.env` and
|
||||||
|
# `certs/server.key` still ship while this file reads as solved. Only
|
||||||
|
# genuinely root-anchored entries go unprefixed. Never transplant these
|
||||||
|
# into .gitignore, where `**/` is wrong.
|
||||||
|
#
|
||||||
|
# Matching is case-sensitive, so secrets use character ranges rather
|
||||||
|
# than an ALL-CAPS twin, which would still miss `Server.Key`.
|
||||||
|
#
|
||||||
|
# Extend with this repo's own host-built artifacts, written anchored:
|
||||||
|
# `/myapp`, never `**/myapp`, which also matches `cmd/myapp/` and
|
||||||
|
# deletes the package directory from the context.
|
||||||
|
|
||||||
|
# .git is sent without its config. Without a VERSION build argument the
|
||||||
|
# stage that compiles runs `git describe --tags --always` on .git, which
|
||||||
|
# does not need .git/config; that file can hold a credential, such as a
|
||||||
|
# password in a remote URL or the token the CI checkout step stores there.
|
||||||
|
# Each submodule keeps a config with the same exposure in its git directory
|
||||||
|
# under .git/modules/, nested again for a submodule's own submodules, or in
|
||||||
|
# its own .git directory when it keeps one.
|
||||||
|
# KNOWN GAP: a submodule whose name has a `config` segment (`config`,
|
||||||
|
# `deploy/config`, `config/lib`) loses its whole git directory, because
|
||||||
|
# `**/.git/modules/**/config` also matches that segment's directory
|
||||||
|
# under .git/modules/. Go's version stamping then fails the build;
|
||||||
|
# nothing leaks. Name such a submodule without that segment:
|
||||||
|
# `git submodule add --name`.
|
||||||
|
**/.git/config
|
||||||
|
**/.git/modules/**/config
|
||||||
|
|
||||||
|
# Agent scratch: one full checkout of the repo per in-flight agent.
|
||||||
|
# Anchored because it occurs once where agents run at the repo root.
|
||||||
|
# KNOWN GAP: a repo running agents in subdirectories still ships
|
||||||
|
# `services/api/.claude/` and must add its own anchored entry.
|
||||||
|
.claude
|
||||||
|
|
||||||
|
# Environment files. `*.env` covers bare `.env` and the `prod.env`
|
||||||
|
# convention. Re-include a committed template with a negation if the
|
||||||
|
# build needs one: `!docs/example.env`.
|
||||||
|
**/*.[eE][nN][vV]
|
||||||
|
**/.[eE][nN][vV].*
|
||||||
|
**/.[eE][nN][vV][rR][cC]
|
||||||
|
|
||||||
|
# Private keys and the bundles carrying them. Public certificates
|
||||||
|
# (*.crt, *.cer) are deliberately absent: they are legitimate inputs.
|
||||||
|
**/*.[pP][eE][mM]
|
||||||
|
**/*.[kK][eE][yY]
|
||||||
|
**/*.[pP]12
|
||||||
|
**/*.[pP][fF][xX]
|
||||||
|
**/[iI][dD]_[rR][sS][aA]
|
||||||
|
**/[iI][dD]_[dD][sS][aA]
|
||||||
|
**/[iI][dD]_[eE][cC][dD][sS][aA]
|
||||||
|
**/[iI][dD]_[eE][cC][dD][sS][aA]_[sS][kK]
|
||||||
|
**/[iI][dD]_[eE][dD]25519
|
||||||
|
**/[iI][dD]_[eE][dD]25519_[sS][kK]
|
||||||
|
|
||||||
|
# Dependencies: restored inside the image, never copied in.
|
||||||
|
**/node_modules
|
||||||
|
|
||||||
|
# OS metadata.
|
||||||
|
**/.DS_Store
|
||||||
|
**/Thumbs.db
|
||||||
|
|
||||||
|
# Editor state: never a build input, and it churns COPY.
|
||||||
|
**/*.swp
|
||||||
|
**/*.swo
|
||||||
|
**/*~
|
||||||
|
**/*.bak
|
||||||
|
**/.idea
|
||||||
|
**/.vscode
|
||||||
|
**/*.sublime-*
|
||||||
|
|
||||||
|
# This repository's own host-built artifacts, anchored.
|
||||||
|
/sutil
|
||||||
|
**/*.test
|
||||||
|
**/*.out
|
||||||
|
**/*.log
|
||||||
|
**/*.prof
|
||||||
|
**/coverage.*
|
||||||
-10
@@ -1,10 +0,0 @@
|
|||||||
kind: pipeline
|
|
||||||
name: test-go
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: test-go
|
|
||||||
image: golang
|
|
||||||
commands:
|
|
||||||
- go test ./...
|
|
||||||
#- go get github.com/golangci/golangci-lint/cmd/golangci-lint@v1.31.0
|
|
||||||
#- golangci-lint run
|
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
end_of_line = lf
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
|
|
||||||
|
[Makefile]
|
||||||
|
indent_style = tab
|
||||||
|
|
||||||
|
[*.go]
|
||||||
|
indent_style = tab
|
||||||
|
|
||||||
|
# This repository's own sections, such as one for another language it
|
||||||
|
# uses, go below this comment, and a re-vendor keeps them.
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
name: check
|
||||||
|
on: [push]
|
||||||
|
# Free the shared runner: a new push cancels only the same branch's older run.
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
jobs:
|
||||||
|
check:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
# Free the shared runner from a hung build.
|
||||||
|
timeout-minutes: 20
|
||||||
|
steps:
|
||||||
|
# actions/checkout v4.2.2, 2026-02-22
|
||||||
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||||
|
# script/cibuild needs no token, so none is left in .git/config.
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
# All history and tags, so git describe finds the version tag.
|
||||||
|
fetch-depth: 0
|
||||||
|
- run: script/cibuild
|
||||||
+61
-1
@@ -1 +1,61 @@
|
|||||||
/x
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Editors
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
*.bak
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.sublime-*
|
||||||
|
|
||||||
|
# Agent scratch (worktrees of this repo, created and destroyed by
|
||||||
|
# in-flight tooling). Unanchored: .gitignore patterns already match at
|
||||||
|
# every depth, so no prefix is wanted here. This is not a .dockerignore
|
||||||
|
# entry and must not be given a `**/` prefix on the way into one.
|
||||||
|
.claude/
|
||||||
|
|
||||||
|
# Node
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Secrets. Unanchored like every entry above, so each matches at every
|
||||||
|
# depth. Matching is case-sensitive on Linux, so names use character
|
||||||
|
# ranges rather than a lowercase form that misses `Server.Key`.
|
||||||
|
|
||||||
|
# Environment files. `*.env` covers bare `.env` and the `prod.env`
|
||||||
|
# convention. Only the templates `example.env` and `sample.env` are
|
||||||
|
# re-included below. A repository that commits any other template adds
|
||||||
|
# its own negation at the end of this file, for example `!.env.example`.
|
||||||
|
*.[eE][nN][vV]
|
||||||
|
.[eE][nN][vV].*
|
||||||
|
.[eE][nN][vV][rR][cC]
|
||||||
|
!example.env
|
||||||
|
!sample.env
|
||||||
|
|
||||||
|
# Private keys and the bundles carrying them.
|
||||||
|
*.[pP][eE][mM]
|
||||||
|
*.[kK][eE][yY]
|
||||||
|
*.[pP]12
|
||||||
|
*.[pP][fF][xX]
|
||||||
|
[iI][dD]_[rR][sS][aA]
|
||||||
|
[iI][dD]_[dD][sS][aA]
|
||||||
|
[iI][dD]_[eE][cC][dD][sS][aA]
|
||||||
|
[iI][dD]_[eE][cC][dD][sS][aA]_[sS][kK]
|
||||||
|
[iI][dD]_[eE][dD]25519
|
||||||
|
[iI][dD]_[eE][dD]25519_[sS][kK]
|
||||||
|
|
||||||
|
# This repository's own entries, such as its build outputs, go below
|
||||||
|
# this comment, and a re-vendor keeps them. Anchor a binary built at the
|
||||||
|
# root: `/myapp`, never `myapp`, which also ignores `cmd/myapp/`.
|
||||||
|
|
||||||
|
# The sutil binary that `make build` writes at the root.
|
||||||
|
/sutil
|
||||||
|
|
||||||
|
# Go test and profiling outputs.
|
||||||
|
*.test
|
||||||
|
*.out
|
||||||
|
*.log
|
||||||
|
*.prof
|
||||||
|
coverage.*
|
||||||
+122
@@ -0,0 +1,122 @@
|
|||||||
|
version: "2"
|
||||||
|
|
||||||
|
# Config schema uses the golangci-lint v2 layout (settings live under
|
||||||
|
# linters.settings, not top-level linters-settings) so that the
|
||||||
|
# thresholds below are actually applied by golangci-lint >= v2.
|
||||||
|
#
|
||||||
|
# This file has no formatters section, so golangci-lint run does not run
|
||||||
|
# gofmt. A repository's script/fmt-check runs it, on the host.
|
||||||
|
|
||||||
|
run:
|
||||||
|
timeout: 5m
|
||||||
|
modules-download-mode: readonly
|
||||||
|
|
||||||
|
linters:
|
||||||
|
default: all
|
||||||
|
enable:
|
||||||
|
# Successor to the deprecated gomodguard. Named explicitly, rather than
|
||||||
|
# left to `default: all`, because it carries the module policy below.
|
||||||
|
- gomodguard_v2
|
||||||
|
disable:
|
||||||
|
# Genuinely incompatible with project patterns
|
||||||
|
- exhaustruct_v5 # Requires all struct fields
|
||||||
|
- godot # Requires comments to end with periods
|
||||||
|
- wrapcheck # Too verbose for internal packages
|
||||||
|
- varnamelen # Short names like db, id are idiomatic Go
|
||||||
|
# Deprecated: the warning is attached to the old name, so it is
|
||||||
|
# silenced by disabling that name, not by enabling the successor.
|
||||||
|
- exhaustruct # Deprecated, replaced by exhaustruct_v5
|
||||||
|
- wsl # Deprecated, replaced by wsl_v5
|
||||||
|
- gomodguard # Deprecated, replaced by gomodguard_v2
|
||||||
|
settings:
|
||||||
|
lll:
|
||||||
|
line-length: 88
|
||||||
|
funlen:
|
||||||
|
lines: 80
|
||||||
|
statements: 50
|
||||||
|
cyclop:
|
||||||
|
max-complexity: 15
|
||||||
|
dupl:
|
||||||
|
threshold: 100
|
||||||
|
depguard:
|
||||||
|
# Test-support code must not be compiled into the shipped binary. A
|
||||||
|
# test-support package exists to hand a test privileges the program
|
||||||
|
# itself must never have, so a file that is not a test must not import
|
||||||
|
# one. Test files, and the files inside a package whose directory name
|
||||||
|
# ends in `test`, are where that code belongs, and are exempt. A package
|
||||||
|
# in a subdirectory of that directory is not exempt. `${base-path}` is
|
||||||
|
# the directory holding this file, so the name of the directory a
|
||||||
|
# repository is cloned into does not count.
|
||||||
|
#
|
||||||
|
# The deny list below is the one part of this file a repository is
|
||||||
|
# expected to extend, and the only part it may. depguard matches an
|
||||||
|
# import path against a list of prefixes, so it cannot be told "any path
|
||||||
|
# whose last segment ends in test"; a repository's own test-support
|
||||||
|
# packages have to be named here one at a time, by full import path,
|
||||||
|
# under a module path that differs from repository to repository. Add
|
||||||
|
# them; change nothing else.
|
||||||
|
rules:
|
||||||
|
test-support:
|
||||||
|
list-mode: lax
|
||||||
|
files:
|
||||||
|
- "$all"
|
||||||
|
- "!$test"
|
||||||
|
- "!${base-path}**/*test/*"
|
||||||
|
deny:
|
||||||
|
- pkg: net/http/httptest
|
||||||
|
desc: >-
|
||||||
|
Test-support code belongs in test files and in packages whose
|
||||||
|
directory name ends in test, not in the shipped binary.
|
||||||
|
# Each entry names a module not to use and what to use instead. Every
|
||||||
|
# entry matches the module path exactly.
|
||||||
|
gomodguard_v2:
|
||||||
|
blocked:
|
||||||
|
- module: github.com/rs/zerolog
|
||||||
|
recommendations:
|
||||||
|
- log/slog
|
||||||
|
reason: "Structured logging is stdlib log/slog."
|
||||||
|
# These entries cover only the github.com/go-redis/redis module paths,
|
||||||
|
# one entry per path, because the later releases are separate paths.
|
||||||
|
# A prefix match would be shorter but would also reach
|
||||||
|
# github.com/go-redis/redismock/v9, the test double for the successor
|
||||||
|
# these entries recommend.
|
||||||
|
- module: github.com/go-redis/redis
|
||||||
|
recommendations:
|
||||||
|
- github.com/redis/go-redis/v9
|
||||||
|
reason: "Pre-fork module; use the maintained go-redis v9."
|
||||||
|
- module: github.com/go-redis/redis/v7
|
||||||
|
recommendations:
|
||||||
|
- github.com/redis/go-redis/v9
|
||||||
|
reason: "Pre-fork module; use the maintained go-redis v9."
|
||||||
|
- module: github.com/go-redis/redis/v8
|
||||||
|
recommendations:
|
||||||
|
- github.com/redis/go-redis/v9
|
||||||
|
reason: "Pre-fork module; use the maintained go-redis v9."
|
||||||
|
- module: github.com/go-redis/redis/v9
|
||||||
|
recommendations:
|
||||||
|
- github.com/redis/go-redis/v9
|
||||||
|
reason: "Pre-fork module; use the maintained go-redis v9."
|
||||||
|
- module: github.com/sergi/go-diff
|
||||||
|
recommendations:
|
||||||
|
- github.com/aymanbagabas/go-udiff
|
||||||
|
reason: "No unified diff output; use go-udiff."
|
||||||
|
- module: github.com/hexops/gotextdiff
|
||||||
|
recommendations:
|
||||||
|
- github.com/aymanbagabas/go-udiff
|
||||||
|
reason: "Unmaintained fork; use go-udiff."
|
||||||
|
exclusions:
|
||||||
|
rules:
|
||||||
|
# homoicon keeps a local copy of sobek, its JavaScript engine, as sneak
|
||||||
|
# decided in
|
||||||
|
# https://git.eeqj.de/sneak/homoicon/issues/1759#issuecomment-153261.
|
||||||
|
# This is an exclusion because gomoddirectives does not check its
|
||||||
|
# replace-allow-list for a replace that points at a local path.
|
||||||
|
- path: ^go\.mod$
|
||||||
|
linters:
|
||||||
|
- gomoddirectives
|
||||||
|
text: '^local replacement are not allowed: github\.com/grafana/sobek$'
|
||||||
|
|
||||||
|
issues:
|
||||||
|
max-issues-per-linter: 0
|
||||||
|
max-same-issues: 0
|
||||||
|
uniq-by-line: false
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
node_modules/
|
||||||
|
yarn.lock
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"tabWidth": 4,
|
||||||
|
"proseWrap": "always"
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# AGENTS.md
|
||||||
|
|
||||||
|
Guidance for coding agents working in this repository. The repository policies
|
||||||
|
are in `REPO_POLICIES.md`, vendored from the `prompts` repository; read that
|
||||||
|
first. Everything below is specific to this repository, and the README's Design
|
||||||
|
section explains each point.
|
||||||
|
|
||||||
|
- `pkg/` holds only packages designed to be imported by other programs. Anything
|
||||||
|
that exists to support `sutil` lives under `internal/`.
|
||||||
|
- `sutil` is not a command-line front end for the packages in `pkg/`. It holds
|
||||||
|
only the utilities the author wants available everywhere; a command is added
|
||||||
|
because a tool is wanted, not because a package exists.
|
||||||
|
- `pkg/cbk` derives crypto_box keypairs from BIP39 mnemonics with BIP85 and does
|
||||||
|
nothing else. BIP39 passphrases are deliberately unsupported.
|
||||||
|
- cgo stays off: builds are static, and the test phase runs without `-race`.
|
||||||
|
- Version tags are created by the author alone, when the author decides. Do not
|
||||||
|
propose, schedule, or create tags.
|
||||||
|
- `.golangci.yml` is byte-identical to the canonical copy and is not edited
|
||||||
|
here; changes go to the `prompts` repository.
|
||||||
+95
@@ -0,0 +1,95 @@
|
|||||||
|
# In every stage ARG CACHEBUST comes right after the steps that install
|
||||||
|
# system packages and download Go's modules, or right after FROM in a
|
||||||
|
# stage with neither, and every step that lints, tests or compiles
|
||||||
|
# comes after it. script/ passes a new value on every build, which
|
||||||
|
# reruns every RUN step after it. Docker keeps the layers above it
|
||||||
|
# until the base image, their own lines or go.mod and go.sum change.
|
||||||
|
# The steps that compile mount Go's build cache, and golangci-lint run
|
||||||
|
# also mounts golangci-lint's; both survive from build to build.
|
||||||
|
#
|
||||||
|
# This repository keeps cgo off, so its test phase departs from the
|
||||||
|
# canonical Go Dockerfile in REPO_POLICIES.md in one way: it runs on the
|
||||||
|
# alpine Go image without -race, which needs a C toolchain. Everything
|
||||||
|
# else follows the canonical file.
|
||||||
|
|
||||||
|
# Lint phase
|
||||||
|
# golangci/golangci-lint:v2.15.0, 2026-10-16
|
||||||
|
FROM golangci/golangci-lint@sha256:fbfcfd12a6f8d7f1b97b3362ca80fc2a244d46863148c73cb88e7793af96c481 AS lint
|
||||||
|
WORKDIR /src
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
ARG CACHEBUST
|
||||||
|
COPY . .
|
||||||
|
RUN --mount=type=cache,target=/root/.cache/go-build \
|
||||||
|
--mount=type=cache,target=/root/.cache/golangci-lint \
|
||||||
|
golangci-lint run --config .golangci.yml ./...
|
||||||
|
|
||||||
|
# Test phase. -count=1 because the mounted cache also holds test
|
||||||
|
# results, which go test would otherwise replay. A first run that timed
|
||||||
|
# out gets no rerun, because a rerun would only wait for the same hang.
|
||||||
|
# golang:1.26-alpine, 2026-10-27
|
||||||
|
FROM golang@sha256:a2012b4a1d1f9f7e9f44e0d39b805a901e856b1dd80b4e6ee4338db547050308 AS test
|
||||||
|
ENV CGO_ENABLED=0
|
||||||
|
WORKDIR /src
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
ARG CACHEBUST
|
||||||
|
COPY . .
|
||||||
|
RUN --mount=type=cache,target=/root/.cache/go-build \
|
||||||
|
status=0; \
|
||||||
|
output=$(go test -count=1 -timeout 90s -cover ./... 2>&1) || status=$?; \
|
||||||
|
printf '%s\n' "$output"; \
|
||||||
|
test "$status" -eq 0 && exit 0; \
|
||||||
|
case "$output" in \
|
||||||
|
*"panic: test timed out"*|*"*** Test killed"*) exit 1 ;; \
|
||||||
|
esac; \
|
||||||
|
echo "--- Rerunning with -v for details ---"; \
|
||||||
|
go test -count=1 -timeout 90s -v ./...; \
|
||||||
|
exit 1
|
||||||
|
|
||||||
|
# Build stage. Nothing is wanted from either phase above; the copies
|
||||||
|
# are what make BuildKit build them first, so nothing after them can
|
||||||
|
# run unless lint and test passed.
|
||||||
|
# golang:1.26-alpine, 2026-10-27
|
||||||
|
FROM golang@sha256:a2012b4a1d1f9f7e9f44e0d39b805a901e856b1dd80b4e6ee4338db547050308 AS builder
|
||||||
|
# git comes from the base image's own distribution package index, which
|
||||||
|
# is trusted, so it needs no version and no hash.
|
||||||
|
RUN apk add --no-cache git
|
||||||
|
WORKDIR /src
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
ARG CACHEBUST
|
||||||
|
COPY --from=lint /src/go.sum /dev/null
|
||||||
|
COPY --from=test /src/go.sum /dev/null
|
||||||
|
# A tar-stream context keeps the sender's file owners, which git refuses.
|
||||||
|
RUN git config --system --add safe.directory /src
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# The VERSION build arg when one is given, otherwise
|
||||||
|
# `git describe --tags --always` on the .git in the build context. With
|
||||||
|
# .git present, a version that is still empty, dev or unknown fails the
|
||||||
|
# build: git is missing or could not read the checkout.
|
||||||
|
ARG VERSION
|
||||||
|
RUN --mount=type=cache,target=/root/.cache/go-build \
|
||||||
|
VERSION="${VERSION:-$(git describe --tags --always)}"; \
|
||||||
|
if [ -e .git ]; then \
|
||||||
|
case "$VERSION" in ""|dev|unknown) \
|
||||||
|
echo "version is '$VERSION' although .git is present" >&2; \
|
||||||
|
exit 1 ;; \
|
||||||
|
esac; \
|
||||||
|
fi; \
|
||||||
|
CGO_ENABLED=0 go build -trimpath \
|
||||||
|
-ldflags="-s -w -X main.Version=${VERSION}" \
|
||||||
|
-o /sutil ./cmd/sutil
|
||||||
|
# The runtime stage ships the source; the repository history is not
|
||||||
|
# part of it.
|
||||||
|
RUN rm -rf /src/.git
|
||||||
|
|
||||||
|
# Runtime stage, and the last one: the binary plus the source it was
|
||||||
|
# built from.
|
||||||
|
# alpine:3.24, 2026-10-23
|
||||||
|
FROM alpine@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a
|
||||||
|
ARG CACHEBUST
|
||||||
|
COPY --from=builder /sutil /usr/local/bin/sutil
|
||||||
|
COPY --from=builder /src /usr/local/src/x
|
||||||
|
ENTRYPOINT ["sutil"]
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
||||||
|
Version 2, December 2004
|
||||||
|
|
||||||
|
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
|
||||||
|
Copyright (C) 2020-2026 sneak <sneak@sneak.berlin>
|
||||||
|
|
||||||
|
Everyone is permitted to copy and distribute verbatim or modified
|
||||||
|
copies of this license document, and changing it is allowed as long
|
||||||
|
as the name is changed.
|
||||||
|
|
||||||
|
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
||||||
|
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||||
|
|
||||||
|
0. You just DO WHAT THE FUCK YOU WANT TO.
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
.PHONY: bootstrap setup test lint fmt fmt-check check docker hooks \
|
||||||
|
build install tidy clean
|
||||||
|
|
||||||
|
# Makefile targets are thin shims; the implementations live in script/
|
||||||
|
# per the scripts-to-rule-them-all pattern (see the Entrypoints section
|
||||||
|
# of README.md). The targets after the shims are this repo's own.
|
||||||
|
|
||||||
|
bootstrap:
|
||||||
|
@script/bootstrap
|
||||||
|
|
||||||
|
setup:
|
||||||
|
@script/setup
|
||||||
|
|
||||||
|
test:
|
||||||
|
@script/test
|
||||||
|
|
||||||
|
lint:
|
||||||
|
@script/lint
|
||||||
|
|
||||||
|
fmt:
|
||||||
|
@script/fmt
|
||||||
|
|
||||||
|
fmt-check:
|
||||||
|
@script/fmt-check
|
||||||
|
|
||||||
|
check:
|
||||||
|
@script/check
|
||||||
|
|
||||||
|
docker:
|
||||||
|
@script/docker
|
||||||
|
|
||||||
|
hooks:
|
||||||
|
@script/install-precommit
|
||||||
|
|
||||||
|
# This repository's own targets.
|
||||||
|
|
||||||
|
BINARY := sutil
|
||||||
|
VERSION := $(shell git describe --tags --always --dirty 2>/dev/null || echo unknown)
|
||||||
|
GOLDFLAGS := -X main.Version=$(VERSION)
|
||||||
|
|
||||||
|
# cgo stays off everywhere: builds are static, and the test phase of the
|
||||||
|
# Dockerfile runs without -race, which would need a C toolchain.
|
||||||
|
export CGO_ENABLED := 0
|
||||||
|
|
||||||
|
# Build the sutil binary in the repo root with the git version embedded.
|
||||||
|
build:
|
||||||
|
go build -trimpath -ldflags "$(GOLDFLAGS)" -o $(BINARY) ./cmd/$(BINARY)
|
||||||
|
|
||||||
|
# Install sutil into $(go env GOBIN) (or $GOPATH/bin).
|
||||||
|
install:
|
||||||
|
go install -trimpath -ldflags "$(GOLDFLAGS)" ./cmd/$(BINARY)
|
||||||
|
|
||||||
|
# Tidy go.mod and go.sum; script/precommit refuses a commit that needs it.
|
||||||
|
tidy:
|
||||||
|
go mod tidy
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -f $(BINARY)
|
||||||
@@ -1,16 +1,301 @@
|
|||||||
# x
|
# x
|
||||||
|
|
||||||
Experimental code.
|
## Description
|
||||||
|
|
||||||
# Build Status
|
`sneak.berlin/go/x` is a WTFPL-licensed Go module and CLI tool by
|
||||||
|
[@sneak](https://sneak.berlin) that serves as an umbrella publishing space for
|
||||||
|
small reusable packages, plus `sutil`, a command-line utility holding a few
|
||||||
|
small tools worth having on every machine.
|
||||||
|
|
||||||
[](https://drone.datavi.be/sneak/x)
|
Importable packages live in `pkg/`. The CLI is `cmd/sutil/`, and the code that
|
||||||
|
exists only to support it lives in `internal/`.
|
||||||
|
|
||||||
# License
|
## Getting Started
|
||||||
|
|
||||||
WTFPL
|
### Install the CLI
|
||||||
|
|
||||||
# Author
|
```sh
|
||||||
|
go install sneak.berlin/go/x/cmd/sutil@latest
|
||||||
|
```
|
||||||
|
|
||||||
sneak@sneak.berlin
|
Or, from a checkout:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make build # builds ./sutil with the git version embedded
|
||||||
|
make install # installs sutil into $(go env GOBIN)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Use the CLI
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# An endless stream of fast pseudorandom bytes; stop it with Ctrl-C.
|
||||||
|
sutil generate random | pv > /dev/null
|
||||||
|
|
||||||
|
# The same, but deterministic: the same seed always produces the same bytes.
|
||||||
|
sutil generate random --count 1073741824 "my seed" > testdata.bin
|
||||||
|
|
||||||
|
# A memorable password: three dictionary words, each followed by a digit,
|
||||||
|
# joined by a randomly chosen delimiter. Requires a system word list.
|
||||||
|
sutil generate password
|
||||||
|
# marble7-sprout2-velvet9
|
||||||
|
|
||||||
|
# Build metadata, including which config file (if any) was loaded.
|
||||||
|
sutil version
|
||||||
|
sutil info
|
||||||
|
|
||||||
|
# Every command first loads ~/.sneak.yaml (or .json, .toml, ...) if it
|
||||||
|
# exists, or the file named with --config.
|
||||||
|
sutil --config ./sneak.yaml info
|
||||||
|
```
|
||||||
|
|
||||||
|
### Use the packages
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"sneak.berlin/go/x/pkg/bip39wordlist"
|
||||||
|
"sneak.berlin/go/x/pkg/cbk"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
mnemonic, err := bip39wordlist.GenerateMnemonic(bip39wordlist.MaxWordCount)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The same mnemonic always yields the same keypair, on any machine.
|
||||||
|
// The keys are plain crypto_box keys for use with
|
||||||
|
// golang.org/x/crypto/nacl/box.
|
||||||
|
keypair, err := cbk.New(cbk.Params{Mnemonic: mnemonic})
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(keypair.PublicKeyHex())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rationale
|
||||||
|
|
||||||
|
- **Reusable odds and ends need a home.** Several small packages are worth
|
||||||
|
sharing between projects but are too small to each deserve a repository, CI
|
||||||
|
pipeline, and release cadence of their own. This module collects them under
|
||||||
|
one import path.
|
||||||
|
- **`/dev/urandom` is slow.** Operating system random devices are tuned for
|
||||||
|
small requests of key material, not for filling a disk or producing gigabytes
|
||||||
|
of test data. `sutil generate random` streams pseudorandom bytes at close to
|
||||||
|
memory bandwidth, and can do so deterministically from a seed so test data is
|
||||||
|
reproducible.
|
||||||
|
- **Keys you can carry in your head.** A crypto_box keypair derived from a BIP39
|
||||||
|
mnemonic can be regenerated anywhere from a phrase a human can write down or
|
||||||
|
memorize, with no key file to lose. `pkg/cbk` makes that derivation explicit
|
||||||
|
and reproducible.
|
||||||
|
- **Passwords you can type.** Three dictionary words with digits and a delimiter
|
||||||
|
are far easier to transcribe than line noise, and with the macOS word list
|
||||||
|
they still carry about 62 bits of entropy.
|
||||||
|
|
||||||
|
## Design
|
||||||
|
|
||||||
|
### Layout
|
||||||
|
|
||||||
|
| Path | Purpose |
|
||||||
|
| ---------------------- | --------------------------------------------------- |
|
||||||
|
| `cmd/sutil/` | `package main`; the whole entrypoint is a few lines |
|
||||||
|
| `internal/cli/` | The cobra command tree, built by `cli.New` |
|
||||||
|
| `internal/randstream/` | Fast seeded pseudorandom byte stream |
|
||||||
|
| `internal/pwgen/` | Password generator over the system dictionary |
|
||||||
|
| `pkg/bip39wordlist/` | BIP39 English wordlist and mnemonic helpers |
|
||||||
|
| `pkg/cbk/` | crypto_box keypairs derived from BIP39 mnemonics |
|
||||||
|
|
||||||
|
The split between `pkg/` and `internal/` is deliberate and should be kept:
|
||||||
|
`pkg/` holds only packages designed to be imported by other programs, with a
|
||||||
|
stable, documented API. Anything that exists to support `sutil` goes under
|
||||||
|
`internal/`, even if it would be harmless to export, so that the public surface
|
||||||
|
of the module stays small and intentional.
|
||||||
|
|
||||||
|
The command tree holds no global state: `cli.New` builds every command from a
|
||||||
|
`Params` struct, so tests construct it repeatedly with captured input and
|
||||||
|
output, and nothing happens in `init` functions. Before any command runs, the
|
||||||
|
root command's persistent pre-run loads the optional configuration file (the one
|
||||||
|
named by `--config`, or else `.sneak.*` in the home directory found with
|
||||||
|
`go-homedir`) into a viper instance that also exposes environment variables. No
|
||||||
|
command consumes settings from it yet; `sutil info` reports which file was
|
||||||
|
loaded. Tests point `Params.ConfigDir` at a temporary directory so the real home
|
||||||
|
directory is never read.
|
||||||
|
|
||||||
|
### `pkg/cbk`
|
||||||
|
|
||||||
|
Derives NaCl crypto_box keypairs (Curve25519) deterministically from BIP39
|
||||||
|
mnemonics, and nothing else. The entropy comes from
|
||||||
|
[BIP85](https://github.com/bitcoin/bips/blob/master/bip-0085.mediawiki) through
|
||||||
|
`sneak.berlin/go/secret/pkg/bip85`, so any BIP85-capable tool given the same
|
||||||
|
mnemonic reproduces the same key, and one mnemonic can back any number of
|
||||||
|
independent keypairs:
|
||||||
|
|
||||||
|
1. The mnemonic is validated and canonicalized with
|
||||||
|
`bip39wordlist.NormalizeWordlist`.
|
||||||
|
2. The standard 64-byte BIP39 seed is computed with PBKDF2-HMAC-SHA512 (2048
|
||||||
|
rounds, salt `"mnemonic"`), exactly as a BIP39 wallet would with an empty
|
||||||
|
passphrase. BIP39 passphrases are deliberately not supported.
|
||||||
|
3. The seed becomes a BIP32 master key.
|
||||||
|
4. BIP85's HEX application derives 32 bytes of entropy at
|
||||||
|
`m/83696968'/128169'/32'/{index}'`; those bytes are the private key.
|
||||||
|
5. The public key is the Curve25519 base-point multiple of the private key.
|
||||||
|
|
||||||
|
The resulting keys are plain 32-byte crypto_box keys for use with
|
||||||
|
`golang.org/x/crypto/nacl/box`. The API:
|
||||||
|
|
||||||
|
- `cbk.New(cbk.Params{Mnemonic, Index})` and `cbk.FromMnemonic(m)` (index 0)
|
||||||
|
return a `CryptoBoxKeypair`.
|
||||||
|
- `CryptoBoxKey.Hex()`, `CryptoBoxKey.Bytes()`, and the `PublicKeyHex`,
|
||||||
|
`PrivateKeyHex`, `PublicKeyBytes`, `PrivateKeyBytes` accessors on the keypair.
|
||||||
|
|
||||||
|
The test suite checks the mnemonic-to-master-key step and the BIP85 HEX
|
||||||
|
application against the BIP85 specification's own test vectors, verifies that
|
||||||
|
the private key is exactly the BIP85 output, and pins the keypair for the
|
||||||
|
specification's mnemonic at index 0. If the pinned test ever fails, the
|
||||||
|
derivation has changed and existing keys are no longer reproducible.
|
||||||
|
|
||||||
|
### `pkg/bip39wordlist`
|
||||||
|
|
||||||
|
The BIP39 English wordlist, embedded from `english.txt` and checked against the
|
||||||
|
reference implementation in the tests, plus mnemonic helpers:
|
||||||
|
|
||||||
|
- `ValidateWordlist(mnemonic)` checks word count, every word, and the checksum,
|
||||||
|
ignoring letter case and whitespace. It reports what is wrong with sentinel
|
||||||
|
errors (`ErrInvalidWordCount`, `ErrUnknownWord` naming the word and its
|
||||||
|
position, `ErrInvalidChecksum`).
|
||||||
|
- `NormalizeWordlist(mnemonic)` returns the canonical lowercase,
|
||||||
|
single-space-separated form, and refuses anything invalid.
|
||||||
|
- `GenerateMnemonic(wordCount)` makes a new mnemonic of 12, 15, 18, 21, or 24
|
||||||
|
words from `crypto/rand`.
|
||||||
|
- `Contains(word)`, `ValidWordCount(n)`, and the `WordlistEnglish` and
|
||||||
|
`Wordlist` tables.
|
||||||
|
|
||||||
|
### `internal/randstream`
|
||||||
|
|
||||||
|
An `io.Reader` that expands a 32-byte key with the ChaCha8 generator from
|
||||||
|
`math/rand/v2`. With a seed string the key is its SHA-256 and the stream is
|
||||||
|
deterministic; without one the key comes from `crypto/rand`, which is the only
|
||||||
|
time the operating system's random source is touched. The output is for bulk
|
||||||
|
data, not for cryptographic keys.
|
||||||
|
|
||||||
|
### `internal/pwgen`
|
||||||
|
|
||||||
|
Produces passwords of the form
|
||||||
|
|
||||||
|
```
|
||||||
|
<word><digit><delim><word><digit><delim><word><digit>
|
||||||
|
```
|
||||||
|
|
||||||
|
where the delimiter is drawn once per password from `.-_|~+=:,!@#%^&*` and used
|
||||||
|
in both positions. Words come from the system word list
|
||||||
|
(`/usr/share/dict/words`) through `github.com/kavfixnel/words`, filtered to
|
||||||
|
lowercase ASCII words of four to eight letters, which drops proper nouns and
|
||||||
|
abbreviations. All choices use `crypto/rand`. On a system without a word list,
|
||||||
|
`sutil generate password` panics rather than falling back to something weaker;
|
||||||
|
the upstream library is only known to work on macOS.
|
||||||
|
|
||||||
|
### CLI
|
||||||
|
|
||||||
|
```
|
||||||
|
sutil
|
||||||
|
├── version print the build version
|
||||||
|
├── info print version, Go version, module, VCS metadata
|
||||||
|
├── generate (gen)
|
||||||
|
│ ├── random [seedstr] stream pseudorandom bytes (--count N to stop)
|
||||||
|
│ └── password (pw) print a memorable password
|
||||||
|
```
|
||||||
|
|
||||||
|
`sutil` is not a command-line front end for the packages in `pkg/`. It holds
|
||||||
|
only the handful of utilities the author wants available everywhere. `pkg/cbk`
|
||||||
|
and `pkg/bip39wordlist` are libraries with no CLI surface, and a command is
|
||||||
|
added because a tool is wanted, not because a package exists.
|
||||||
|
|
||||||
|
### Build, test, and CI
|
||||||
|
|
||||||
|
The gates live in the `Dockerfile`, in the canonical Go layout that
|
||||||
|
`REPO_POLICIES.md` prescribes: a `lint` phase on the hash-pinned golangci-lint
|
||||||
|
image and a `test` phase on the Go image. `script/lint` and `script/test` each
|
||||||
|
build one phase alone, with a fresh `CACHEBUST` value so the check step runs on
|
||||||
|
every build instead of being served from Docker's cache, and write no image. The
|
||||||
|
build stage depends on both phases, so the image cannot be built unless they
|
||||||
|
pass. Go's build cache and golangci-lint's cache are mounted into the phases and
|
||||||
|
survive between builds. Formatting is the one check that runs on the host:
|
||||||
|
`script/fmt-check` runs gofmt and the prettier pinned in `yarn.lock`.
|
||||||
|
`script/check` runs all three. `script/cibuild`, which the Gitea workflow calls,
|
||||||
|
bootstraps, runs `script/check`, and then builds the tagged image with the
|
||||||
|
version from `git describe`.
|
||||||
|
|
||||||
|
Every external reference, from base images to the GitHub Action in
|
||||||
|
`.gitea/workflows/check.yml`, is pinned by content hash with a version and date
|
||||||
|
comment. `.golangci.yml` is byte-identical to the canonical copy in the
|
||||||
|
`prompts` repository and is never edited here; the lint phase pins the
|
||||||
|
golangci-lint release that the policy names. cgo stays off: the Makefile exports
|
||||||
|
`CGO_ENABLED=0`, binaries are static, and the test phase runs on the alpine Go
|
||||||
|
image without `-race`, which is this repository's one departure from the
|
||||||
|
canonical `Dockerfile`. The runtime image carries the binary and the source it
|
||||||
|
was built from.
|
||||||
|
|
||||||
|
Version tags are created by the author alone, when the author decides. Tooling,
|
||||||
|
contributors, and agents working in this repository do not propose, schedule, or
|
||||||
|
create tags.
|
||||||
|
|
||||||
|
## Entrypoints
|
||||||
|
|
||||||
|
This repository adheres to the
|
||||||
|
[Scripts to Rule Them All](https://github.com/github/scripts-to-rule-them-all)
|
||||||
|
standard: normalized scripts in `script/` are the entrypoints for the
|
||||||
|
development workflow, and the Makefile targets are thin shims that call them. We
|
||||||
|
provide:
|
||||||
|
|
||||||
|
- `script/bootstrap`: install everything the repository needs, idempotently:
|
||||||
|
make, git, go, and, because `yarn.lock` is present, node, yarn 1 and the
|
||||||
|
pinned prettier.
|
||||||
|
- `script/setup`: make a fresh clone ready for development by running
|
||||||
|
`script/bootstrap` and then `script/install-precommit`.
|
||||||
|
- `script/projectname`: print the project name, `x`, which `script/docker` and
|
||||||
|
`script/cibuild` use as the image tag.
|
||||||
|
- `script/test`: build the `test` phase of the `Dockerfile` with a new
|
||||||
|
`CACHEBUST` value, writing no image.
|
||||||
|
- `script/lint`: build the `lint` phase the same way.
|
||||||
|
- `script/fmt`: gofmt and prettier, writing files.
|
||||||
|
- `script/fmt-check`: the read-only twin of `script/fmt`.
|
||||||
|
- `script/check`: `script/test`, `script/lint`, then `script/fmt-check`. It
|
||||||
|
modifies nothing.
|
||||||
|
- `script/docker`: build the image tagged `x`, passing the version from
|
||||||
|
`git describe`.
|
||||||
|
- `script/cibuild`: what CI runs: `script/bootstrap`, `script/check`, then the
|
||||||
|
tagged image build.
|
||||||
|
- `script/precommit`: run by the pre-commit hook. It refuses a commit whose
|
||||||
|
`go.mod` or `go.sum` is not tidy, then runs `script/check`.
|
||||||
|
- `script/install-precommit`: install that hook; `make hooks` shims to it.
|
||||||
|
|
||||||
|
The Makefile adds four targets of its own: `make build`, `make install`,
|
||||||
|
`make tidy`, and `make clean`.
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make setup # once: bootstrap and install the pre-commit hook
|
||||||
|
make check # test, lint, fmt-check: what the hook and CI run
|
||||||
|
make fmt # gofmt and prettier, writes files
|
||||||
|
make build # build ./sutil on the host
|
||||||
|
make docker # build the image tagged x
|
||||||
|
```
|
||||||
|
|
||||||
|
## TODO
|
||||||
|
|
||||||
|
Nothing outstanding.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
WTFPL. See `LICENSE`.
|
||||||
|
|
||||||
|
## Author
|
||||||
|
|
||||||
|
[@sneak](https://sneak.berlin)
|
||||||
|
<[sneak@sneak.berlin](mailto:sneak@sneak.berlin)>
|
||||||
+1107
File diff suppressed because it is too large.
Load diff
@@ -1,14 +0,0 @@
|
|||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
rootCmd.AddCommand(generateCommand)
|
|
||||||
}
|
|
||||||
|
|
||||||
var generateCommand = &cobra.Command{
|
|
||||||
Use: "generate",
|
|
||||||
Short: "Generate things",
|
|
||||||
}
|
|
||||||
-21
@@ -1,21 +0,0 @@
|
|||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"runtime/debug"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
rootCmd.AddCommand(infoCmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
var infoCmd = &cobra.Command{
|
|
||||||
Use: "info",
|
|
||||||
Short: "Print tool info",
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
info, _ := debug.ReadBuildInfo()
|
|
||||||
fmt.Printf("%#v\n", info)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/rand"
|
|
||||||
"fmt"
|
|
||||||
"math/big"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"sneak.berlin/x/pkg/bip39wordlist"
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
generateCommand.AddCommand(passwordCmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
var passwordCmd = &cobra.Command{
|
|
||||||
Use: "password",
|
|
||||||
Short: "Print a randomly generated password",
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
fmt.Printf("%s\n", randomPassword())
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func randomPassword() string {
|
|
||||||
wl := bip39wordlist.WordListEnglish
|
|
||||||
count := len(wl)
|
|
||||||
r, err := rand.Int(rand.Reader, big.Int(count))
|
|
||||||
return wl[r]
|
|
||||||
}
|
|
||||||
-52
@@ -1,52 +0,0 @@
|
|||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/mitchellh/go-homedir"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
// Used for flags.
|
|
||||||
cfgFile string
|
|
||||||
userLicense string
|
|
||||||
|
|
||||||
rootCmd = &cobra.Command{
|
|
||||||
Use: "sn",
|
|
||||||
Short: "A shell utility program",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// Execute executes the root command.
|
|
||||||
func Execute() error {
|
|
||||||
return rootCmd.Execute()
|
|
||||||
}
|
|
||||||
|
|
||||||
func er(msg interface{}) {
|
|
||||||
fmt.Println("Error:", msg)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
cobra.OnInitialize(initConfig)
|
|
||||||
}
|
|
||||||
|
|
||||||
func initConfig() {
|
|
||||||
home, err := homedir.Dir()
|
|
||||||
if err != nil {
|
|
||||||
er(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Search config in home directory with name ".cobra" (without extension).
|
|
||||||
viper.AddConfigPath(home)
|
|
||||||
viper.SetConfigName(".sneak")
|
|
||||||
|
|
||||||
viper.AutomaticEnv()
|
|
||||||
|
|
||||||
if err := viper.ReadInConfig(); err == nil {
|
|
||||||
fmt.Println("Using config file:", viper.ConfigFileUsed())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
// Command sutil is sneak's shell utility: a few small tools worth having on
|
||||||
|
// every machine. Run `sutil --help` for the command list.
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"sneak.berlin/go/x/internal/cli"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Version is the build version. The Makefile and Dockerfile set it at link
|
||||||
|
// time with -ldflags "-X main.Version=...".
|
||||||
|
//
|
||||||
|
//nolint:gochecknoglobals // set by the linker; there is no other way
|
||||||
|
var Version = "dev"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
err := cli.New(cli.Params{Version: Version, ConfigDir: ""}).Execute()
|
||||||
|
if err != nil {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
rootCmd.AddCommand(versionCmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
var versionCmd = &cobra.Command{
|
|
||||||
Use: "version",
|
|
||||||
Short: "Print the version number of Hugo",
|
|
||||||
Long: `All software has versions. This is Hugo's`,
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
fmt.Println("sn version")
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,35 @@
|
|||||||
module sneak.berlin/x
|
module sneak.berlin/go/x
|
||||||
|
|
||||||
go 1.15
|
go 1.26.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/btcsuite/btcd v0.24.2
|
||||||
|
github.com/btcsuite/btcd/btcutil v1.1.6
|
||||||
|
github.com/kavfixnel/words v1.0.0
|
||||||
github.com/mitchellh/go-homedir v1.1.0
|
github.com/mitchellh/go-homedir v1.1.0
|
||||||
github.com/spf13/cobra v1.0.0
|
github.com/spf13/cobra v1.10.2
|
||||||
github.com/spf13/viper v1.7.1
|
github.com/spf13/viper v1.21.0
|
||||||
github.com/stretchr/testify v1.3.0
|
github.com/stretchr/testify v1.12.1
|
||||||
|
github.com/tyler-smith/go-bip39 v1.1.0
|
||||||
|
golang.org/x/crypto v0.58.0
|
||||||
|
sneak.berlin/go/secret v1.0.0-rc4
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/btcsuite/btcd/btcec/v2 v2.1.3 // indirect
|
||||||
|
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect
|
||||||
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect
|
||||||
|
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||||
|
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
|
github.com/sagikazarmark/locafero v0.11.0 // indirect
|
||||||
|
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
|
||||||
|
github.com/spf13/afero v1.15.0 // indirect
|
||||||
|
github.com/spf13/cast v1.10.0 // indirect
|
||||||
|
github.com/spf13/pflag v1.0.10 // indirect
|
||||||
|
github.com/subosito/gotenv v1.6.0 // indirect
|
||||||
|
go.yaml.in/yaml/v3 v3.0.5 // indirect
|
||||||
|
golang.org/x/sys v0.49.0 // indirect
|
||||||
|
golang.org/x/text v0.43.0 // indirect
|
||||||
)
|
)
|
||||||
@@ -1,312 +1,166 @@
|
|||||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
|
||||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
|
||||||
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M=
|
||||||
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
|
github.com/btcsuite/btcd v0.23.5-0.20231215221805-96c9fd8078fd/go.mod h1:nm3Bko6zh6bWP60UxwoT5LzdGJsQJaPo6HjduXq9p6A=
|
||||||
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
|
github.com/btcsuite/btcd v0.24.2 h1:aLmxPguqxza+4ag8R1I2nnJjSu2iFn/kqtHTIImswcY=
|
||||||
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
|
github.com/btcsuite/btcd v0.24.2/go.mod h1:5C8ChTkl5ejr3WHj8tkQSCmydiMEPB0ZhQhehpq7Dgg=
|
||||||
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
|
github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA=
|
||||||
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
github.com/btcsuite/btcd/btcec/v2 v2.1.3 h1:xM/n3yIhHAhHy04z4i43C8p4ehixJZMsnrVJkgl+MTE=
|
||||||
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE=
|
||||||
cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
|
github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A=
|
||||||
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE=
|
||||||
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
github.com/btcsuite/btcd/btcutil v1.1.5/go.mod h1:PSZZ4UitpLBWzxGd5VGOrLnmOjtPP/a6HaFo12zMs00=
|
||||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
github.com/btcsuite/btcd/btcutil v1.1.6 h1:zFL2+c3Lb9gEgqKNzowKUPQNb8jV7v5Oaodi/AYFd6c=
|
||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
github.com/btcsuite/btcd/btcutil v1.1.6/go.mod h1:9dFymx8HpuLqBnsPELrImQeTQfKBQqzqGbbV3jK55aE=
|
||||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
|
||||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
|
||||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 h1:59Kx4K6lzOW5w6nFlA0v5+lk/6sjybR934QNHSJZPTQ=
|
||||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
|
||||||
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
|
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA=
|
||||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
|
||||||
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
|
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg=
|
||||||
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY=
|
||||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
github.com/btcsuite/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I=
|
||||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
|
||||||
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
|
||||||
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
|
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
|
||||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
|
||||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||||
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
|
github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
|
||||||
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
|
||||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
|
||||||
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
|
||||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
|
||||||
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
|
||||||
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc=
|
||||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs=
|
||||||
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218=
|
||||||
|
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||||
|
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
|
||||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
|
||||||
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
|
||||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
|
||||||
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
|
||||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
|
||||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
|
||||||
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
|
|
||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||||
|
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||||
|
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||||
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ=
|
||||||
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
github.com/kavfixnel/words v1.0.0 h1:k7/KXUnLIVNCAlBLXhdq1GunnxXmQqglZooHA+NRzuQ=
|
||||||
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
|
github.com/kavfixnel/words v1.0.0/go.mod h1:i2LITxcnsJnKfT+ruhPGVwzdcRDnsgQTL9TIooX9XAU=
|
||||||
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
|
github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
|
||||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
|
|
||||||
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
|
|
||||||
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
|
|
||||||
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
|
|
||||||
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
|
||||||
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
|
||||||
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
|
|
||||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
|
||||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
|
||||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
|
||||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
|
||||||
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
|
|
||||||
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
|
|
||||||
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
|
|
||||||
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
|
|
||||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
|
||||||
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
|
||||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
|
||||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
|
||||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
|
||||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
|
||||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
|
||||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
|
||||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
|
||||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
|
||||||
github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
|
|
||||||
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
|
||||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
|
||||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
|
||||||
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
|
||||||
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
|
|
||||||
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
|
||||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||||
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
|
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||||
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
|
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
|
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||||
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
|
github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
|
||||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||||
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
|
|
||||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
|
||||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
|
||||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
|
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||||
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
|
||||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
|
||||||
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
|
||||||
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
|
||||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||||
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
||||||
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
|
||||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
|
||||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||||
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
|
||||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
|
||||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
|
||||||
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
|
|
||||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
|
||||||
github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
|
|
||||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
|
||||||
github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
|
|
||||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
|
||||||
github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8=
|
|
||||||
github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
|
|
||||||
github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
|
|
||||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
|
||||||
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
|
|
||||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
|
||||||
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
|
|
||||||
github.com/spf13/viper v1.7.1 h1:pM5oEahlgWv/WnHXpgbKz7iLIxRf65tye2Ci+XFK5sk=
|
|
||||||
github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
|
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
|
||||||
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
|
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
|
||||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||||
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc=
|
||||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
github.com/tyler-smith/go-bip39 v1.1.0 h1:5eUemwrMargf3BSLRRCalXT93Ns6pQJIjYQN2nyfOP8=
|
||||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
github.com/tyler-smith/go-bip39 v1.1.0/go.mod h1:gUYDtqQw1JS3ZJ8UWVcGTGqqr6YIN3CWg+kkNaLt55U=
|
||||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
|
||||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
|
||||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.58.0 h1:COkYLr4k7nDI4r6QtMML2AfzeIswWHAJthLS6K/M/54=
|
||||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/crypto v0.58.0/go.mod h1:Xh+kl5A+M0gMiWDytH2Hhr5Z7WbNCpv/eB/EFgfSk+w=
|
||||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
|
|
||||||
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
|
|
||||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
|
||||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
|
||||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
|
||||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
|
||||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
|
||||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
|
||||||
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
|
||||||
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
|
||||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
|
||||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
|
||||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
|
||||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
|
||||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
|
||||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||||
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
|
||||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
|
||||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
|
||||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0 h1:HyfiK1WMnHj5FXFXatD+Qs1A/xC2Run6RzeW1SyHxpc=
|
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA=
|
||||||
|
golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
|
||||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
|
||||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/text v0.43.0 h1:1QivrlhwAsnMOcqgOdl9my7s9OyZl8O5/UGUTJVXoWc=
|
||||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/text v0.43.0/go.mod h1:hbSIYA/amXcRXTFSZqh8tWHEoqRvvbKPJ2uz/KU+a+s=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||||
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||||
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||||
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
|
||||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
|
||||||
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
|
||||||
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
|
||||||
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
|
||||||
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
|
||||||
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
|
||||||
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
|
||||||
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
|
||||||
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
|
||||||
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
|
||||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
|
||||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
|
||||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
|
||||||
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
|
||||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
|
||||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
|
||||||
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
|
||||||
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
|
||||||
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
|
||||||
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
|
||||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
|
||||||
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
|
|
||||||
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
|
||||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
|
||||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
|
||||||
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
|
||||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
|
||||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||||
gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno=
|
|
||||||
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
|
||||||
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
|
|
||||||
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
|
||||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
||||||
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
|
|
||||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
sneak.berlin/go/secret v1.0.0-rc4 h1:BFpPs34qhcYS2ZtK+nvWmBT7CGFgjYWY9KA9Amske3w=
|
||||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
sneak.berlin/go/secret v1.0.0-rc4/go.mod h1:pvhNbatHqGCO9l7hXYjlKEHkpGh7u8q/uE7mzP9EZXo=
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
// Package cli implements the sutil command tree.
|
||||||
|
//
|
||||||
|
// The tree is built by New rather than assembled in init functions, so
|
||||||
|
// the package holds no global state and tests can construct it repeatedly
|
||||||
|
// with captured input and output.
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Params configures New.
|
||||||
|
type Params struct {
|
||||||
|
// Version is the version string embedded at build time. It is shown
|
||||||
|
// by `sutil version`, `sutil --version`, and `sutil info`.
|
||||||
|
Version string
|
||||||
|
|
||||||
|
// ConfigDir is the directory searched for the optional .sneak
|
||||||
|
// configuration file. Empty means the user's home directory; tests set
|
||||||
|
// it to a temporary directory so the real home is never consulted.
|
||||||
|
ConfigDir string
|
||||||
|
}
|
||||||
|
|
||||||
|
// app carries the state shared by every command: the build parameters,
|
||||||
|
// the --config flag, and the configuration loaded before any command runs.
|
||||||
|
type app struct {
|
||||||
|
params Params
|
||||||
|
configFile string
|
||||||
|
config *viper.Viper
|
||||||
|
}
|
||||||
|
|
||||||
|
// New builds the root sutil command with every subcommand attached.
|
||||||
|
func New(p Params) *cobra.Command {
|
||||||
|
a := &app{params: p, configFile: "", config: nil}
|
||||||
|
|
||||||
|
root := &cobra.Command{
|
||||||
|
Use: "sutil",
|
||||||
|
Short: "sneak's shell utility",
|
||||||
|
Long: `sutil is a small collection of shell utilities worth having on every
|
||||||
|
machine: a fast pseudorandom byte stream and a memorable password generator.
|
||||||
|
|
||||||
|
Before any command runs, sutil loads the optional configuration file named
|
||||||
|
by --config, or else ~/.sneak.yaml (or .json, .toml, ...) if it exists, and
|
||||||
|
makes environment variables available alongside it.`,
|
||||||
|
Version: p.Version,
|
||||||
|
SilenceUsage: true,
|
||||||
|
PersistentPreRunE: a.loadConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
root.PersistentFlags().StringVar(
|
||||||
|
&a.configFile, "config", "",
|
||||||
|
"config file (default is "+configName+".* in the home directory)",
|
||||||
|
)
|
||||||
|
|
||||||
|
root.AddCommand(
|
||||||
|
newVersionCommand(p),
|
||||||
|
newInfoCommand(a),
|
||||||
|
newGenerateCommand(),
|
||||||
|
)
|
||||||
|
|
||||||
|
return root
|
||||||
|
}
|
||||||
@@ -0,0 +1,186 @@
|
|||||||
|
package cli_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"sneak.berlin/go/x/internal/cli"
|
||||||
|
"sneak.berlin/go/x/internal/pwgen"
|
||||||
|
)
|
||||||
|
|
||||||
|
const testVersion = "v1.2.3-test"
|
||||||
|
|
||||||
|
func TestVersionCommand(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
output, err := execute(t, "version")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, testVersion+"\n", output)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVersionFlag(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
output, err := execute(t, "--version")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Contains(t, output, testVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInfoCommand(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
output, err := execute(t, "info")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Contains(t, output, "version: "+testVersion+"\n")
|
||||||
|
assert.Contains(t, output, "go: go")
|
||||||
|
assert.Contains(t, output, "module: sneak.berlin/go/x\n")
|
||||||
|
assert.Contains(t, output, "config: (none)\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigFileDiscoveredInConfigDir(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
configDir := t.TempDir()
|
||||||
|
path := filepath.Join(configDir, ".sneak.yaml")
|
||||||
|
require.NoError(t, os.WriteFile(path, []byte("example: 1\n"), 0o600))
|
||||||
|
|
||||||
|
params := cli.Params{Version: testVersion, ConfigDir: configDir}
|
||||||
|
|
||||||
|
output, err := executeWithParams(t, params, "info")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Contains(t, output, "Using config file: "+path+"\n")
|
||||||
|
assert.Contains(t, output, "config: "+path+"\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigFlagLoadsNamedFile(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
path := filepath.Join(t.TempDir(), "elsewhere.yaml")
|
||||||
|
require.NoError(t, os.WriteFile(path, []byte("example: 1\n"), 0o600))
|
||||||
|
|
||||||
|
output, err := execute(t, "--config", path, "info")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Contains(t, output, "Using config file: "+path+"\n")
|
||||||
|
assert.Contains(t, output, "config: "+path+"\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigFlagRejectsMissingFile(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
path := filepath.Join(t.TempDir(), "missing.yaml")
|
||||||
|
|
||||||
|
_, err := execute(t, "--config", path, "version")
|
||||||
|
require.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigRejectsMalformedFile(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
configDir := t.TempDir()
|
||||||
|
path := filepath.Join(configDir, ".sneak.yaml")
|
||||||
|
require.NoError(t, os.WriteFile(path, []byte("not: [valid\n"), 0o600))
|
||||||
|
|
||||||
|
params := cli.Params{Version: testVersion, ConfigDir: configDir}
|
||||||
|
|
||||||
|
_, err := executeWithParams(t, params, "version")
|
||||||
|
require.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenerateRandomSeededIsDeterministic(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
first, err := execute(t, "generate", "random", "--count", "4096", "seed")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Len(t, first, 4096)
|
||||||
|
|
||||||
|
second, err := execute(t, "generate", "random", "--count", "4096", "seed")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, first, second)
|
||||||
|
|
||||||
|
other, err := execute(t, "gen", "random", "-c", "4096", "other seed")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotEqual(t, first, other)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenerateRandomUnseeded(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
first, err := execute(t, "generate", "random", "--count", "1024")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Len(t, first, 1024)
|
||||||
|
|
||||||
|
second, err := execute(t, "generate", "random", "--count", "1024")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotEqual(t, first, second)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenerateRandomRejectsExtraArguments(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
_, err := execute(t, "generate", "random", "one", "two")
|
||||||
|
require.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGeneratePassword(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
_, err := pwgen.New(pwgen.Params{Words: nil, MinWordLength: 0, MaxWordLength: 0})
|
||||||
|
if errors.Is(err, pwgen.ErrNoUsableWords) {
|
||||||
|
t.Skip("this system has no word list")
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
output, err := execute(t, "generate", "password")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
password := strings.TrimSuffix(output, "\n")
|
||||||
|
assert.NotContains(t, password, "\n")
|
||||||
|
assert.GreaterOrEqual(t, len(password), 3*(pwgen.DefaultMinWordLength+1)+2)
|
||||||
|
assert.True(t, strings.ContainsAny(password, "0123456789"), password)
|
||||||
|
assert.True(t, strings.ContainsAny(password, pwgen.Delimiters), password)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnknownCommandFails(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
_, err := execute(t, "cbk")
|
||||||
|
require.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// execute runs sutil with the given arguments, looking for config in an
|
||||||
|
// empty temporary directory, and returns everything it wrote to stdout and
|
||||||
|
// stderr.
|
||||||
|
func execute(t *testing.T, args ...string) (string, error) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
params := cli.Params{Version: testVersion, ConfigDir: t.TempDir()}
|
||||||
|
|
||||||
|
return executeWithParams(t, params, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// executeWithParams is execute with explicit Params.
|
||||||
|
func executeWithParams(
|
||||||
|
t *testing.T, params cli.Params, args ...string,
|
||||||
|
) (string, error) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
root := cli.New(params)
|
||||||
|
|
||||||
|
var output bytes.Buffer
|
||||||
|
|
||||||
|
root.SetOut(&output)
|
||||||
|
root.SetErr(&output)
|
||||||
|
root.SetArgs(args)
|
||||||
|
|
||||||
|
err := root.Execute()
|
||||||
|
|
||||||
|
return output.String(), err
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/mitchellh/go-homedir"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
// configName is the base name of the optional configuration file looked
|
||||||
|
// up in the config directory; viper tries every extension it supports, so
|
||||||
|
// ~/.sneak.yaml, ~/.sneak.json, ~/.sneak.toml, and so on all work.
|
||||||
|
const configName = ".sneak"
|
||||||
|
|
||||||
|
// loadConfig runs before every command. It reads the optional
|
||||||
|
// configuration file into a.config and makes environment variables
|
||||||
|
// available through it. The file is the one named by --config, or else
|
||||||
|
// configName in Params.ConfigDir, which defaults to the user's home
|
||||||
|
// directory. A missing default file is not an error; a file that is named
|
||||||
|
// explicitly but missing, or one that cannot be parsed, is.
|
||||||
|
func (a *app) loadConfig(cmd *cobra.Command, _ []string) error {
|
||||||
|
configDir := a.params.ConfigDir
|
||||||
|
if configDir == "" {
|
||||||
|
home, err := homedir.Dir()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("finding home directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
configDir = home
|
||||||
|
}
|
||||||
|
|
||||||
|
config := viper.New()
|
||||||
|
config.AddConfigPath(configDir)
|
||||||
|
config.SetConfigName(configName)
|
||||||
|
config.AutomaticEnv()
|
||||||
|
|
||||||
|
if a.configFile != "" {
|
||||||
|
config.SetConfigFile(a.configFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := config.ReadInConfig()
|
||||||
|
|
||||||
|
_, isNotFound := errors.AsType[viper.ConfigFileNotFoundError](err)
|
||||||
|
if isNotFound {
|
||||||
|
a.config = config
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("reading config file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
a.config = config
|
||||||
|
|
||||||
|
_, err = fmt.Fprintln(
|
||||||
|
cmd.ErrOrStderr(), "Using config file:", config.ConfigFileUsed(),
|
||||||
|
)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// configFileUsed returns the path of the configuration file that
|
||||||
|
// loadConfig read, or an empty string if there was none.
|
||||||
|
func (a *app) configFileUsed() string {
|
||||||
|
if a.config == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.config.ConfigFileUsed()
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"sneak.berlin/go/x/internal/pwgen"
|
||||||
|
"sneak.berlin/go/x/internal/randstream"
|
||||||
|
)
|
||||||
|
|
||||||
|
// streamChunkSize is the write size used by `generate random`. Large
|
||||||
|
// writes keep the per-syscall overhead negligible.
|
||||||
|
const streamChunkSize = 1 << 20
|
||||||
|
|
||||||
|
// newGenerateCommand returns `sutil generate`, the parent of everything
|
||||||
|
// that produces random output.
|
||||||
|
func newGenerateCommand() *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "generate",
|
||||||
|
Aliases: []string{"gen"},
|
||||||
|
Short: "Generate random bytes and passwords",
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.AddCommand(
|
||||||
|
newGenerateRandomCommand(),
|
||||||
|
newGeneratePasswordCommand(),
|
||||||
|
)
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// newGenerateRandomCommand returns `sutil generate random [seedstr]`.
|
||||||
|
func newGenerateRandomCommand() *cobra.Command {
|
||||||
|
var byteCount int64
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "random [seedstr]",
|
||||||
|
Short: "Write a fast stream of pseudorandom bytes to stdout",
|
||||||
|
Long: `Writes pseudorandom bytes to stdout until stopped, or until --count
|
||||||
|
bytes have been written.
|
||||||
|
|
||||||
|
With seedstr the stream is deterministic: the same seed always produces
|
||||||
|
the same bytes. Without it the stream is keyed from the operating
|
||||||
|
system's secure random source and is different every run.
|
||||||
|
|
||||||
|
This exists because /dev/urandom is slow.`,
|
||||||
|
Args: cobra.MaximumNArgs(1),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
seed := ""
|
||||||
|
if len(args) == 1 {
|
||||||
|
seed = args[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
stream, err := randstream.New(randstream.Params{Seed: seed})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return streamBytes(cmd.OutOrStdout(), stream, byteCount)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Flags().Int64VarP(
|
||||||
|
&byteCount, "count", "c", 0, "stop after this many bytes (0 = never)",
|
||||||
|
)
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// newGeneratePasswordCommand returns `sutil generate password`.
|
||||||
|
func newGeneratePasswordCommand() *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "password",
|
||||||
|
Aliases: []string{"pw"},
|
||||||
|
Short: "Print a memorable password built from system dictionary words",
|
||||||
|
Long: `Prints a password of the form
|
||||||
|
|
||||||
|
<word><digit><delim><word><digit><delim><word><digit>
|
||||||
|
|
||||||
|
for example marble7-sprout2-velvet9. The words come from the system word
|
||||||
|
list (/usr/share/dict/words), the delimiter is chosen at random from
|
||||||
|
|
||||||
|
` + pwgen.Delimiters + `
|
||||||
|
|
||||||
|
and is the same in both positions. The command panics if the system has
|
||||||
|
no usable word list.`,
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||||
|
generator, err := pwgen.New(pwgen.Params{})
|
||||||
|
if err != nil {
|
||||||
|
panic("sutil: cannot load the system word list: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
password, err := generator.Generate()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = fmt.Fprintln(cmd.OutOrStdout(), password)
|
||||||
|
|
||||||
|
return err
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// streamBytes copies limit bytes (or everything, when limit is zero) from
|
||||||
|
// src to dst in streamChunkSize chunks.
|
||||||
|
func streamBytes(dst io.Writer, src io.Reader, limit int64) error {
|
||||||
|
if limit > 0 {
|
||||||
|
src = io.LimitReader(src, limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrapping dst hides any ReadFrom method it has, so io.CopyBuffer
|
||||||
|
// cannot bypass the large buffer.
|
||||||
|
_, err := io.CopyBuffer(writerOnly{dst}, src, make([]byte, streamChunkSize))
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// writerOnly exposes only the Write method of the wrapped writer.
|
||||||
|
type writerOnly struct {
|
||||||
|
io.Writer
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"runtime/debug"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrNoBuildInfo is returned by `sutil info` when the binary carries no
|
||||||
|
// Go build information, which only happens for binaries not built with
|
||||||
|
// module support.
|
||||||
|
var ErrNoBuildInfo = errors.New("sutil: no build information in this binary")
|
||||||
|
|
||||||
|
// noConfigFile is what `sutil info` prints when no config file was loaded.
|
||||||
|
const noConfigFile = "(none)"
|
||||||
|
|
||||||
|
// newInfoCommand returns `sutil info`, which prints the build version, the
|
||||||
|
// Go toolchain version, the module path, the configuration file in use,
|
||||||
|
// and the VCS metadata recorded by the Go toolchain at build time.
|
||||||
|
func newInfoCommand(a *app) *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "info",
|
||||||
|
Short: "Print build information",
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||||
|
return writeBuildInfo(
|
||||||
|
cmd.OutOrStdout(), a.params.Version, a.configFileUsed(),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeBuildInfo writes one "key: value" line per fact to w.
|
||||||
|
func writeBuildInfo(w io.Writer, version, configFile string) error {
|
||||||
|
info, ok := debug.ReadBuildInfo()
|
||||||
|
if !ok {
|
||||||
|
return ErrNoBuildInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
if configFile == "" {
|
||||||
|
configFile = noConfigFile
|
||||||
|
}
|
||||||
|
|
||||||
|
lines := []string{
|
||||||
|
"version: " + version,
|
||||||
|
"go: " + info.GoVersion,
|
||||||
|
"module: " + info.Main.Path,
|
||||||
|
"config: " + configFile,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, setting := range info.Settings {
|
||||||
|
if strings.HasPrefix(setting.Key, "vcs.") {
|
||||||
|
lines = append(lines, setting.Key+": "+setting.Value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := fmt.Fprintln(w, strings.Join(lines, "\n"))
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
// newVersionCommand returns `sutil version`, which prints the build
|
||||||
|
// version on its own line.
|
||||||
|
func newVersionCommand(p Params) *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "version",
|
||||||
|
Short: "Print the sutil version",
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||||
|
_, err := fmt.Fprintln(cmd.OutOrStdout(), p.Version)
|
||||||
|
|
||||||
|
return err
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,195 @@
|
|||||||
|
// Package pwgen generates memorable passwords from the system dictionary.
|
||||||
|
// It backs `sutil generate password`.
|
||||||
|
//
|
||||||
|
// A password is three dictionary words, each followed by one decimal
|
||||||
|
// digit, joined by a delimiter that is chosen at random from Delimiters
|
||||||
|
// and used in both positions:
|
||||||
|
//
|
||||||
|
// <word><digit><delim><word><digit><delim><word><digit>
|
||||||
|
//
|
||||||
|
// for example "marble7-sprout2-velvet9".
|
||||||
|
//
|
||||||
|
// Words come from the operating system's word list (/usr/share/dict/words
|
||||||
|
// on macOS and most Unix systems) through github.com/kavfixnel/words,
|
||||||
|
// filtered to words made only of lowercase ASCII letters whose length is
|
||||||
|
// within the configured bounds. That drops proper nouns, abbreviations,
|
||||||
|
// and anything awkward to type. All random choices use crypto/rand.
|
||||||
|
package pwgen
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"math/big"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/kavfixnel/words"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// WordCount is the number of words in every generated password.
|
||||||
|
WordCount = 3
|
||||||
|
|
||||||
|
// Delimiters is the set of characters from which the delimiter that
|
||||||
|
// separates the words of a password is chosen.
|
||||||
|
Delimiters = ".-_|~+=:,!@#%^&*"
|
||||||
|
|
||||||
|
// DefaultMinWordLength is the shortest word used when
|
||||||
|
// Params.MinWordLength is zero.
|
||||||
|
DefaultMinWordLength = 4
|
||||||
|
|
||||||
|
// DefaultMaxWordLength is the longest word used when
|
||||||
|
// Params.MaxWordLength is zero.
|
||||||
|
DefaultMaxWordLength = 8
|
||||||
|
|
||||||
|
// digitCount is the number of decimal digits (0 through 9) one of
|
||||||
|
// which follows every word.
|
||||||
|
digitCount = 10
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrNoUsableWords is returned by New when the word source yields no
|
||||||
|
// words within the length bounds. On a system without a word list,
|
||||||
|
// github.com/kavfixnel/words returns an empty list rather than an
|
||||||
|
// error, so this is also the error for "no system dictionary".
|
||||||
|
ErrNoUsableWords = errors.New("pwgen: no usable words in word list")
|
||||||
|
|
||||||
|
// ErrInvalidWordLength is returned by New when the word length bounds
|
||||||
|
// are negative or the minimum exceeds the maximum.
|
||||||
|
ErrInvalidWordLength = errors.New("pwgen: invalid word length bounds")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Params configures New.
|
||||||
|
type Params struct {
|
||||||
|
// Words, when non-nil, is used instead of the system dictionary. It
|
||||||
|
// is filtered by the same length and character rules.
|
||||||
|
Words []string
|
||||||
|
|
||||||
|
// MinWordLength is the shortest word to use; zero means
|
||||||
|
// DefaultMinWordLength.
|
||||||
|
MinWordLength int
|
||||||
|
|
||||||
|
// MaxWordLength is the longest word to use; zero means
|
||||||
|
// DefaultMaxWordLength.
|
||||||
|
MaxWordLength int
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generator produces passwords from a fixed set of usable words. It is
|
||||||
|
// safe for concurrent use.
|
||||||
|
type Generator struct {
|
||||||
|
words []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// New loads and filters the word source described by p. It returns
|
||||||
|
// ErrNoUsableWords if no words survive filtering, which on a system
|
||||||
|
// without a word list is the normal outcome.
|
||||||
|
func New(p Params) (*Generator, error) {
|
||||||
|
minLength, maxLength := p.MinWordLength, p.MaxWordLength
|
||||||
|
|
||||||
|
if minLength == 0 {
|
||||||
|
minLength = DefaultMinWordLength
|
||||||
|
}
|
||||||
|
|
||||||
|
if maxLength == 0 {
|
||||||
|
maxLength = DefaultMaxWordLength
|
||||||
|
}
|
||||||
|
|
||||||
|
if minLength < 0 || maxLength < minLength {
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"%w: min %d, max %d", ErrInvalidWordLength, minLength, maxLength,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
source := p.Words
|
||||||
|
if source == nil {
|
||||||
|
systemWords, err := words.NewWordList(nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("pwgen: reading system word list: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
source = systemWords
|
||||||
|
}
|
||||||
|
|
||||||
|
usable := filterWords(source, minLength, maxLength)
|
||||||
|
if len(usable) == 0 {
|
||||||
|
return nil, ErrNoUsableWords
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Generator{words: usable}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UsableWords returns the number of distinct words the generator chooses
|
||||||
|
// from, which determines the strength of its passwords.
|
||||||
|
func (g *Generator) UsableWords() int {
|
||||||
|
return len(g.words)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate returns a new random password.
|
||||||
|
func (g *Generator) Generate() (string, error) {
|
||||||
|
delimiterIndex, err := randomIndex(len(Delimiters))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
delimiter := Delimiters[delimiterIndex]
|
||||||
|
|
||||||
|
var password strings.Builder
|
||||||
|
|
||||||
|
for i := range WordCount {
|
||||||
|
if i > 0 {
|
||||||
|
password.WriteByte(delimiter)
|
||||||
|
}
|
||||||
|
|
||||||
|
wordIndex, err := randomIndex(len(g.words))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
digit, err := randomIndex(digitCount)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
password.WriteString(g.words[wordIndex])
|
||||||
|
password.WriteString(strconv.Itoa(digit))
|
||||||
|
}
|
||||||
|
|
||||||
|
return password.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// filterWords returns the entries of source that isUsableWord accepts.
|
||||||
|
func filterWords(source []string, minLength, maxLength int) []string {
|
||||||
|
usable := make([]string, 0, len(source))
|
||||||
|
|
||||||
|
for _, word := range source {
|
||||||
|
if isUsableWord(word, minLength, maxLength) {
|
||||||
|
usable = append(usable, word)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return usable
|
||||||
|
}
|
||||||
|
|
||||||
|
// isUsableWord reports whether word is made only of lowercase ASCII
|
||||||
|
// letters and has a length within the bounds.
|
||||||
|
func isUsableWord(word string, minLength, maxLength int) bool {
|
||||||
|
if len(word) < minLength || len(word) > maxLength {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return !strings.ContainsFunc(word, func(r rune) bool {
|
||||||
|
return r < 'a' || r > 'z'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// randomIndex returns a uniformly random integer in [0, n) from
|
||||||
|
// crypto/rand.
|
||||||
|
func randomIndex(n int) (int, error) {
|
||||||
|
value, err := rand.Int(rand.Reader, big.NewInt(int64(n)))
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("pwgen: reading from crypto/rand: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return int(value.Int64()), nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
package pwgen_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"sneak.berlin/go/x/internal/pwgen"
|
||||||
|
)
|
||||||
|
|
||||||
|
const generateRounds = 300
|
||||||
|
|
||||||
|
// testWords mixes usable words with entries the filter must drop:
|
||||||
|
// capitalized, too short, too long, punctuation, digits, and non-ASCII.
|
||||||
|
func testWords() []string {
|
||||||
|
return []string{
|
||||||
|
"apple", "banana", "cherry", "grape", "lemon", "mango", "melon", "peach",
|
||||||
|
"Capital", "ab", "abc", "toolongwordhere", "has-dash", "num3er", "éclair",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func usableTestWords() map[string]bool {
|
||||||
|
return map[string]bool{
|
||||||
|
"apple": true, "banana": true, "cherry": true, "grape": true,
|
||||||
|
"lemon": true, "mango": true, "melon": true, "peach": true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewFiltersWords(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
generator, err := pwgen.New(pwgen.Params{Words: testWords()})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, len(usableTestWords()), generator.UsableWords())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewHonorsLengthBounds(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
generator, err := pwgen.New(pwgen.Params{
|
||||||
|
Words: testWords(),
|
||||||
|
MinWordLength: 2,
|
||||||
|
MaxWordLength: 3,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, 2, generator.UsableWords(), `only "ab" and "abc" fit`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewRejectsNoUsableWords(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
_, err := pwgen.New(pwgen.Params{Words: []string{"Capital", "ab"}})
|
||||||
|
require.ErrorIs(t, err, pwgen.ErrNoUsableWords)
|
||||||
|
|
||||||
|
_, err = pwgen.New(pwgen.Params{Words: []string{}})
|
||||||
|
require.ErrorIs(t, err, pwgen.ErrNoUsableWords)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewRejectsInvalidBounds(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
_, err := pwgen.New(pwgen.Params{
|
||||||
|
Words: testWords(),
|
||||||
|
MinWordLength: 9,
|
||||||
|
MaxWordLength: 4,
|
||||||
|
})
|
||||||
|
require.ErrorIs(t, err, pwgen.ErrInvalidWordLength)
|
||||||
|
|
||||||
|
_, err = pwgen.New(pwgen.Params{Words: testWords(), MinWordLength: -1})
|
||||||
|
require.ErrorIs(t, err, pwgen.ErrInvalidWordLength)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenerateFormat(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
generator, err := pwgen.New(pwgen.Params{Words: testWords()})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
usable := usableTestWords()
|
||||||
|
|
||||||
|
for range generateRounds {
|
||||||
|
password, err := generator.Generate()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
words, digits, delimiter := parsePassword(t, password)
|
||||||
|
|
||||||
|
assert.Contains(t, pwgen.Delimiters, string(delimiter))
|
||||||
|
assert.Len(t, words, pwgen.WordCount)
|
||||||
|
|
||||||
|
for _, word := range words {
|
||||||
|
assert.True(t, usable[word], "unexpected word %q in %q", word, password)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, digit := range digits {
|
||||||
|
assert.True(t, digit >= '0' && digit <= '9', "digit in %q", password)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenerateVariesDelimitersAndDigits(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
generator, err := pwgen.New(pwgen.Params{Words: testWords()})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
delimitersSeen := map[byte]bool{}
|
||||||
|
digitsSeen := map[byte]bool{}
|
||||||
|
passwordsSeen := map[string]bool{}
|
||||||
|
|
||||||
|
for range generateRounds {
|
||||||
|
password, err := generator.Generate()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, digits, delimiter := parsePassword(t, password)
|
||||||
|
delimitersSeen[delimiter] = true
|
||||||
|
passwordsSeen[password] = true
|
||||||
|
|
||||||
|
for _, digit := range digits {
|
||||||
|
digitsSeen[digit] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Greater(t, len(delimitersSeen), 1, "delimiter should vary")
|
||||||
|
assert.Greater(t, len(digitsSeen), 1, "digits should vary")
|
||||||
|
assert.Greater(t, len(passwordsSeen), generateRounds/2, "passwords repeat")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewWithSystemDictionary(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
generator, err := pwgen.New(pwgen.Params{})
|
||||||
|
if errors.Is(err, pwgen.ErrNoUsableWords) {
|
||||||
|
t.Skip("this system has no word list")
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Greater(t, generator.UsableWords(), 1000)
|
||||||
|
|
||||||
|
password, err := generator.Generate()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
parsePassword(t, password)
|
||||||
|
}
|
||||||
|
|
||||||
|
// parsePassword splits a password of the form
|
||||||
|
// <word><digit><delim><word><digit><delim><word><digit> into its words,
|
||||||
|
// digits, and the delimiter, failing the test if it is not of that shape.
|
||||||
|
func parsePassword(t *testing.T, password string) ([]string, []byte, byte) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
delimiterIndex := strings.IndexFunc(password, func(r rune) bool {
|
||||||
|
return (r < 'a' || r > 'z') && (r < '0' || r > '9')
|
||||||
|
})
|
||||||
|
require.NotEqual(t, -1, delimiterIndex, "no delimiter in %q", password)
|
||||||
|
|
||||||
|
delimiter := password[delimiterIndex]
|
||||||
|
parts := strings.Split(password, string(delimiter))
|
||||||
|
require.Len(t, parts, pwgen.WordCount, "wrong shape: %q", password)
|
||||||
|
|
||||||
|
words := make([]string, 0, len(parts))
|
||||||
|
digits := make([]byte, 0, len(parts))
|
||||||
|
|
||||||
|
for _, part := range parts {
|
||||||
|
require.GreaterOrEqual(t, len(part), 2, "short part in %q", password)
|
||||||
|
|
||||||
|
words = append(words, part[:len(part)-1])
|
||||||
|
digits = append(digits, part[len(part)-1])
|
||||||
|
}
|
||||||
|
|
||||||
|
return words, digits, delimiter
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
// Package randstream provides a fast, unbounded stream of pseudorandom
|
||||||
|
// bytes as an io.Reader. It backs `sutil generate random`.
|
||||||
|
//
|
||||||
|
// It exists because reading /dev/urandom is slow: operating system random
|
||||||
|
// devices are built for small requests of key material, not for filling
|
||||||
|
// disks or producing gigabytes of test data. A Reader instead expands a
|
||||||
|
// 32-byte key with the ChaCha8 generator from math/rand/v2, which runs at
|
||||||
|
// close to memory bandwidth and never blocks.
|
||||||
|
//
|
||||||
|
// When Params.Seed is set, the key is SHA-256 of the seed string and the
|
||||||
|
// stream is fully deterministic: the same seed always produces the same
|
||||||
|
// bytes, which makes test data reproducible. When Seed is empty, the key
|
||||||
|
// is read from crypto/rand (the operating system's secure random source,
|
||||||
|
// /dev/urandom or its equivalent) and the stream is unpredictable.
|
||||||
|
//
|
||||||
|
// The output is not meant to be used as cryptographic key material; use
|
||||||
|
// crypto/rand for that.
|
||||||
|
package randstream
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/sha256"
|
||||||
|
"fmt"
|
||||||
|
mathrand "math/rand/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// KeySize is the size in bytes of the key that seeds the stream.
|
||||||
|
const KeySize = 32
|
||||||
|
|
||||||
|
// Params configures New.
|
||||||
|
type Params struct {
|
||||||
|
// Seed, when non-empty, makes the stream deterministic: the same seed
|
||||||
|
// always yields the same byte sequence. When empty, the stream is
|
||||||
|
// keyed from crypto/rand and differs on every call to New.
|
||||||
|
Seed string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reader is an unbounded stream of pseudorandom bytes. Read never returns
|
||||||
|
// an error and never returns fewer bytes than requested. A Reader is not
|
||||||
|
// safe for concurrent use.
|
||||||
|
type Reader struct {
|
||||||
|
generator *mathrand.ChaCha8
|
||||||
|
}
|
||||||
|
|
||||||
|
// New returns a Reader configured by p.
|
||||||
|
func New(p Params) (*Reader, error) {
|
||||||
|
key, err := keyFromParams(p)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Reader{generator: mathrand.NewChaCha8(key)}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read fills b with pseudorandom bytes. It always returns len(b) and a nil
|
||||||
|
// error.
|
||||||
|
func (r *Reader) Read(b []byte) (int, error) {
|
||||||
|
return r.generator.Read(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// keyFromParams derives the ChaCha8 key: SHA-256 of the seed string, or
|
||||||
|
// fresh bytes from crypto/rand when no seed is given.
|
||||||
|
func keyFromParams(p Params) ([KeySize]byte, error) {
|
||||||
|
if p.Seed != "" {
|
||||||
|
return sha256.Sum256([]byte(p.Seed)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var key [KeySize]byte
|
||||||
|
|
||||||
|
_, err := rand.Read(key[:])
|
||||||
|
if err != nil {
|
||||||
|
return key, fmt.Errorf("randstream: reading key from crypto/rand: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return key, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
package randstream_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"sneak.berlin/go/x/internal/randstream"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
sampleSize = 1 << 20
|
||||||
|
helloSeed = "hello"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSeededStreamIsDeterministic(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
first := readSample(t, randstream.Params{Seed: helloSeed}, sampleSize)
|
||||||
|
second := readSample(t, randstream.Params{Seed: helloSeed}, sampleSize)
|
||||||
|
|
||||||
|
assert.Equal(t, first, second)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDifferentSeedsDiffer(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
hello := readSample(t, randstream.Params{Seed: helloSeed}, sampleSize)
|
||||||
|
world := readSample(t, randstream.Params{Seed: "world"}, sampleSize)
|
||||||
|
|
||||||
|
assert.NotEqual(t, hello, world)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnseededStreamsDiffer(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
first := readSample(t, randstream.Params{Seed: ""}, sampleSize)
|
||||||
|
second := readSample(t, randstream.Params{Seed: ""}, sampleSize)
|
||||||
|
|
||||||
|
assert.NotEqual(t, first, second)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadAlwaysFillsBuffer(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
stream, err := randstream.New(randstream.Params{Seed: "fill"})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
for _, size := range []int{0, 1, 7, 64, 65, 4096, sampleSize} {
|
||||||
|
buffer := make([]byte, size)
|
||||||
|
|
||||||
|
n, err := stream.Read(buffer)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, size, n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStreamIsContinuousAcrossReads(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
const headSize, tailSize = 300, 700
|
||||||
|
|
||||||
|
whole := readSample(t, randstream.Params{Seed: "continuous"}, headSize+tailSize)
|
||||||
|
|
||||||
|
stream, err := randstream.New(randstream.Params{Seed: "continuous"})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
head := make([]byte, headSize)
|
||||||
|
tail := make([]byte, tailSize)
|
||||||
|
|
||||||
|
_, err = io.ReadFull(stream, head)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = io.ReadFull(stream, tail)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, whole[:headSize], head)
|
||||||
|
assert.Equal(t, whole[headSize:], tail)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOutputIsNotTriviallyPatterned(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
sample := readSample(t, randstream.Params{Seed: "entropy"}, sampleSize)
|
||||||
|
|
||||||
|
var seen [256]bool
|
||||||
|
for _, b := range sample {
|
||||||
|
seen[b] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
for value, wasSeen := range seen {
|
||||||
|
assert.True(t, wasSeen, "byte value %d never appeared", value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// readSample reads size bytes from a new Reader configured by p.
|
||||||
|
func readSample(t *testing.T, p randstream.Params, size int) []byte {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
stream, err := randstream.New(p)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
sample := make([]byte, size)
|
||||||
|
|
||||||
|
_, err = io.ReadFull(stream, sample)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
return sample
|
||||||
|
}
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"sneak.berlin/x/cmd"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
cmd.Execute()
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"name": "x",
|
||||||
|
"private": true,
|
||||||
|
"description": "prettier is used by `make fmt` and `make fmt-check` to format markdown",
|
||||||
|
"devDependencies": {
|
||||||
|
"prettier": "3.9.10"
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -1,17 +0,0 @@
|
|||||||
package bip39wordlist_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"sneak.berlin/x/pkg/bip39wordlist"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestLength(t *testing.T) {
|
|
||||||
assert := assert.New(t)
|
|
||||||
|
|
||||||
wl := bip39wordlist.Wordlist
|
|
||||||
assert.Equal(2048, len(wl), "should have correct number of words")
|
|
||||||
assert.Equal(wl[0], "abandon", "first word should be correct")
|
|
||||||
assert.Equal(wl[len(wl)-1], "zoo", "last word should be correct")
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,118 @@
|
|||||||
|
package bip39wordlist
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/tyler-smith/go-bip39"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// MinWordCount is the number of words in the shortest valid mnemonic,
|
||||||
|
// which encodes 128 bits of entropy.
|
||||||
|
MinWordCount = 12
|
||||||
|
|
||||||
|
// MaxWordCount is the number of words in the longest valid mnemonic,
|
||||||
|
// which encodes 256 bits of entropy.
|
||||||
|
MaxWordCount = 24
|
||||||
|
|
||||||
|
// WordCountStep is the granularity of valid word counts. Every valid
|
||||||
|
// mnemonic has a multiple of three words, because each group of three
|
||||||
|
// words carries 32 bits of entropy plus one checksum bit.
|
||||||
|
WordCountStep = 3
|
||||||
|
|
||||||
|
// entropyBitsPerWordGroup is the entropy carried by each group of
|
||||||
|
// WordCountStep words.
|
||||||
|
entropyBitsPerWordGroup = 32
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrInvalidWordCount is returned when a mnemonic does not have 12,
|
||||||
|
// 15, 18, 21, or 24 words.
|
||||||
|
ErrInvalidWordCount = errors.New(
|
||||||
|
"bip39wordlist: mnemonic must have 12, 15, 18, 21, or 24 words",
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrUnknownWord is returned when a mnemonic contains a word that is
|
||||||
|
// not in the wordlist. The returned error names the word and its
|
||||||
|
// one-based position.
|
||||||
|
ErrUnknownWord = errors.New("bip39wordlist: word is not in the wordlist")
|
||||||
|
|
||||||
|
// ErrInvalidChecksum is returned when every word is valid but the
|
||||||
|
// checksum bits encoded in the final word do not match the entropy.
|
||||||
|
ErrInvalidChecksum = errors.New("bip39wordlist: mnemonic checksum mismatch")
|
||||||
|
)
|
||||||
|
|
||||||
|
// ValidWordCount reports whether n is a valid number of words for a
|
||||||
|
// mnemonic: a multiple of three from 12 to 24 inclusive.
|
||||||
|
func ValidWordCount(n int) bool {
|
||||||
|
return n >= MinWordCount && n <= MaxWordCount && n%WordCountStep == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateWordlist checks that mnemonic is a valid BIP39 English mnemonic:
|
||||||
|
// a valid number of words, every word in the wordlist, and a correct
|
||||||
|
// checksum. Letter case and the amount of whitespace between words are
|
||||||
|
// ignored, so any phrase ValidateWordlist accepts can be canonicalized with
|
||||||
|
// NormalizeWordlist. It returns nil for a valid mnemonic and otherwise an
|
||||||
|
// error wrapping ErrInvalidWordCount, ErrUnknownWord, or
|
||||||
|
// ErrInvalidChecksum.
|
||||||
|
func ValidateWordlist(mnemonic string) error {
|
||||||
|
words := splitWords(mnemonic)
|
||||||
|
|
||||||
|
if !ValidWordCount(len(words)) {
|
||||||
|
return fmt.Errorf("%w: got %d", ErrInvalidWordCount, len(words))
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, word := range words {
|
||||||
|
if !Contains(word) {
|
||||||
|
return fmt.Errorf("%w: %q at position %d", ErrUnknownWord, word, i+1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bip39.IsMnemonicValid(strings.Join(words, " ")) {
|
||||||
|
return ErrInvalidChecksum
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NormalizeWordlist returns mnemonic in canonical form: lowercase, with
|
||||||
|
// the words separated by exactly one space and no leading or trailing
|
||||||
|
// whitespace. It refuses invalid input: if mnemonic fails
|
||||||
|
// ValidateWordlist, that error is returned and the string is empty.
|
||||||
|
func NormalizeWordlist(mnemonic string) (string, error) {
|
||||||
|
err := ValidateWordlist(mnemonic)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(splitWords(mnemonic), " "), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateMnemonic returns a new random mnemonic of wordCount words, using
|
||||||
|
// entropy from crypto/rand. wordCount must satisfy ValidWordCount.
|
||||||
|
func GenerateMnemonic(wordCount int) (string, error) {
|
||||||
|
if !ValidWordCount(wordCount) {
|
||||||
|
return "", fmt.Errorf("%w: got %d", ErrInvalidWordCount, wordCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
entropyBits := wordCount / WordCountStep * entropyBitsPerWordGroup
|
||||||
|
|
||||||
|
entropy, err := bip39.NewEntropy(entropyBits)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("bip39wordlist: generating entropy: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
mnemonic, err := bip39.NewMnemonic(entropy)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("bip39wordlist: encoding mnemonic: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return mnemonic, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// splitWords lowercases mnemonic and splits it on any run of whitespace.
|
||||||
|
func splitWords(mnemonic string) []string {
|
||||||
|
return strings.Fields(strings.ToLower(mnemonic))
|
||||||
|
}
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
package bip39wordlist_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"sneak.berlin/go/x/pkg/bip39wordlist"
|
||||||
|
)
|
||||||
|
|
||||||
|
// vectorRepeats is how many times "abandon" appears in the standard BIP39
|
||||||
|
// test vector for all-zero 128-bit entropy, which ends with "about".
|
||||||
|
const vectorRepeats = bip39wordlist.MinWordCount - 1
|
||||||
|
|
||||||
|
// vectorMnemonic returns the standard BIP39 test vector for all-zero
|
||||||
|
// 128-bit entropy.
|
||||||
|
func vectorMnemonic() string {
|
||||||
|
return strings.Repeat("abandon ", vectorRepeats) + "about"
|
||||||
|
}
|
||||||
|
|
||||||
|
// messyVectorMnemonic returns vectorMnemonic with mixed case and irregular
|
||||||
|
// whitespace; it must normalize to vectorMnemonic.
|
||||||
|
func messyVectorMnemonic() string {
|
||||||
|
return " Abandon ABANDON\t" + strings.Repeat("abandon ", 4) + "\n" +
|
||||||
|
strings.Repeat("abandon ", 5) + "About \n"
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidWordCount(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
for _, n := range []int{12, 15, 18, 21, 24} {
|
||||||
|
assert.True(t, bip39wordlist.ValidWordCount(n), "%d words", n)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, n := range []int{0, 1, 9, 11, 13, 14, 16, 23, 25, 27, 36} {
|
||||||
|
assert.False(t, bip39wordlist.ValidWordCount(n), "%d words", n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateWordlistAcceptsValidMnemonics(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
require.NoError(t, bip39wordlist.ValidateWordlist(vectorMnemonic()))
|
||||||
|
require.NoError(t, bip39wordlist.ValidateWordlist(messyVectorMnemonic()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateWordlistRejectsWrongWordCount(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
err := bip39wordlist.ValidateWordlist("")
|
||||||
|
require.ErrorIs(t, err, bip39wordlist.ErrInvalidWordCount)
|
||||||
|
|
||||||
|
err = bip39wordlist.ValidateWordlist(vectorMnemonic() + " abandon")
|
||||||
|
require.ErrorIs(t, err, bip39wordlist.ErrInvalidWordCount)
|
||||||
|
assert.Contains(t, err.Error(), "got 13")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateWordlistRejectsUnknownWord(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
words := strings.Fields(vectorMnemonic())
|
||||||
|
words[4] = "bogus"
|
||||||
|
|
||||||
|
err := bip39wordlist.ValidateWordlist(strings.Join(words, " "))
|
||||||
|
require.ErrorIs(t, err, bip39wordlist.ErrUnknownWord)
|
||||||
|
assert.Contains(t, err.Error(), `"bogus" at position 5`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateWordlistRejectsBadChecksum(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
// Twelve times "abandon" has valid words and count but the checksum
|
||||||
|
// bits in the last word are wrong.
|
||||||
|
allAbandon := strings.Repeat("abandon ", bip39wordlist.MinWordCount)
|
||||||
|
|
||||||
|
err := bip39wordlist.ValidateWordlist(allAbandon)
|
||||||
|
require.ErrorIs(t, err, bip39wordlist.ErrInvalidChecksum)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizeWordlistCanonicalizes(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
normalized, err := bip39wordlist.NormalizeWordlist(messyVectorMnemonic())
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, vectorMnemonic(), normalized)
|
||||||
|
|
||||||
|
normalized, err = bip39wordlist.NormalizeWordlist(vectorMnemonic())
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, vectorMnemonic(), normalized)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizeWordlistRefusesInvalid(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
normalized, err := bip39wordlist.NormalizeWordlist("Abandon Bogus about")
|
||||||
|
require.ErrorIs(t, err, bip39wordlist.ErrInvalidWordCount)
|
||||||
|
assert.Empty(t, normalized)
|
||||||
|
|
||||||
|
normalized, err = bip39wordlist.NormalizeWordlist(
|
||||||
|
strings.Repeat("abandon ", bip39wordlist.MinWordCount),
|
||||||
|
)
|
||||||
|
require.ErrorIs(t, err, bip39wordlist.ErrInvalidChecksum)
|
||||||
|
assert.Empty(t, normalized)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenerateMnemonicProducesValidMnemonics(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
for _, wordCount := range []int{12, 15, 18, 21, 24} {
|
||||||
|
mnemonic, err := bip39wordlist.GenerateMnemonic(wordCount)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, bip39wordlist.ValidateWordlist(mnemonic))
|
||||||
|
assert.Len(t, strings.Fields(mnemonic), wordCount)
|
||||||
|
|
||||||
|
normalized, err := bip39wordlist.NormalizeWordlist(mnemonic)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, mnemonic, normalized, "generated mnemonic is canonical")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenerateMnemonicIsRandom(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
first, err := bip39wordlist.GenerateMnemonic(bip39wordlist.MaxWordCount)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
second, err := bip39wordlist.GenerateMnemonic(bip39wordlist.MaxWordCount)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.NotEqual(t, first, second)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenerateMnemonicRejectsInvalidWordCount(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
for _, wordCount := range []int{0, 11, 13, 25} {
|
||||||
|
mnemonic, err := bip39wordlist.GenerateMnemonic(wordCount)
|
||||||
|
require.ErrorIs(t, err, bip39wordlist.ErrInvalidWordCount)
|
||||||
|
assert.Empty(t, mnemonic)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
// Package bip39wordlist provides the BIP39 English wordlist and helpers for
|
||||||
|
// validating, normalizing, and generating BIP39 mnemonic phrases.
|
||||||
|
//
|
||||||
|
// The wordlist is embedded in the binary from english.txt and parsed once
|
||||||
|
// at package initialization. The mnemonic helpers accept phrases in any
|
||||||
|
// letter case and with any whitespace between words; NormalizeWordlist
|
||||||
|
// returns the canonical lowercase, single-space-separated form that BIP39
|
||||||
|
// seed derivation requires, and refuses anything that is not a valid
|
||||||
|
// mnemonic.
|
||||||
|
package bip39wordlist
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "embed"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// WordlistSize is the number of words in a BIP39 wordlist.
|
||||||
|
const WordlistSize = 2048
|
||||||
|
|
||||||
|
//go:embed english.txt
|
||||||
|
var englishWordlistData string
|
||||||
|
|
||||||
|
// WordlistEnglish is the BIP39 English wordlist, one entry per word, in
|
||||||
|
// the canonical (sorted) BIP39 order. Callers must not modify it.
|
||||||
|
//
|
||||||
|
//nolint:gochecknoglobals // static lookup table parsed once from embedded data
|
||||||
|
var WordlistEnglish = strings.Fields(englishWordlistData)
|
||||||
|
|
||||||
|
// Wordlist is the default wordlist, which is WordlistEnglish. Callers must
|
||||||
|
// not modify it.
|
||||||
|
//
|
||||||
|
//nolint:gochecknoglobals // static lookup table, alias of WordlistEnglish
|
||||||
|
var Wordlist = WordlistEnglish
|
||||||
|
|
||||||
|
// Contains reports whether word is in the English wordlist. The comparison
|
||||||
|
// is exact, so word must already be lowercase.
|
||||||
|
func Contains(word string) bool {
|
||||||
|
_, found := slices.BinarySearch(WordlistEnglish, word)
|
||||||
|
|
||||||
|
return found
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package bip39wordlist_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"slices"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/tyler-smith/go-bip39/wordlists"
|
||||||
|
|
||||||
|
"sneak.berlin/go/x/pkg/bip39wordlist"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestWordlistEnglishShape(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
wordlist := bip39wordlist.WordlistEnglish
|
||||||
|
|
||||||
|
assert.Len(t, wordlist, bip39wordlist.WordlistSize)
|
||||||
|
assert.Equal(t, "abandon", wordlist[0])
|
||||||
|
assert.Equal(t, "zoo", wordlist[len(wordlist)-1])
|
||||||
|
assert.True(t, slices.IsSorted(wordlist), "wordlist must be sorted")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWordlistEnglishMatchesReferenceImplementation(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
assert.Equal(t, wordlists.English, bip39wordlist.WordlistEnglish)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWordlistIsEnglish(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
assert.Equal(t, bip39wordlist.WordlistEnglish, bip39wordlist.Wordlist)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContains(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
assert.True(t, bip39wordlist.Contains("abandon"))
|
||||||
|
assert.True(t, bip39wordlist.Contains("zoo"))
|
||||||
|
assert.True(t, bip39wordlist.Contains("legal"))
|
||||||
|
assert.False(t, bip39wordlist.Contains("Abandon"), "lookup is case sensitive")
|
||||||
|
assert.False(t, bip39wordlist.Contains("notaword"))
|
||||||
|
assert.False(t, bip39wordlist.Contains(""))
|
||||||
|
}
|
||||||
+156
@@ -0,0 +1,156 @@
|
|||||||
|
// Package cbk derives NaCl crypto_box keypairs (Curve25519) deterministically
|
||||||
|
// from BIP39 mnemonic phrases using BIP85. That is all it does; the
|
||||||
|
// resulting keys are plain 32-byte crypto_box keys for use with
|
||||||
|
// golang.org/x/crypto/nacl/box.
|
||||||
|
//
|
||||||
|
// The same mnemonic and index always produce the same keypair:
|
||||||
|
//
|
||||||
|
// 1. The mnemonic is validated and canonicalized with
|
||||||
|
// bip39wordlist.NormalizeWordlist (lowercase, single spaces).
|
||||||
|
// 2. The standard 64-byte BIP39 seed is computed from the canonical
|
||||||
|
// mnemonic with PBKDF2-HMAC-SHA512 (2048 rounds, salt "mnemonic"),
|
||||||
|
// exactly as a wallet would with an empty passphrase. BIP39
|
||||||
|
// passphrases are deliberately not supported.
|
||||||
|
// 3. The seed becomes a BIP32 master key.
|
||||||
|
// 4. BIP85 (sneak.berlin/go/secret/pkg/bip85) derives 32 bytes of
|
||||||
|
// application entropy from the master key with the HEX application,
|
||||||
|
// at path m/83696968'/128169'/32'/{index}'. Those bytes are the
|
||||||
|
// crypto_box private key.
|
||||||
|
// 5. The public key is the Curve25519 scalar multiplication of the
|
||||||
|
// private key with the base point, via golang.org/x/crypto/nacl/box.
|
||||||
|
//
|
||||||
|
// Because the entropy comes from BIP85, any BIP85-capable tool given the
|
||||||
|
// same mnemonic reproduces the private key, and the index lets one
|
||||||
|
// mnemonic back any number of independent keypairs.
|
||||||
|
package cbk
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/btcutil/hdkeychain"
|
||||||
|
"github.com/btcsuite/btcd/chaincfg"
|
||||||
|
"github.com/tyler-smith/go-bip39"
|
||||||
|
"golang.org/x/crypto/nacl/box"
|
||||||
|
|
||||||
|
"sneak.berlin/go/secret/pkg/bip85"
|
||||||
|
"sneak.berlin/go/x/pkg/bip39wordlist"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// KeySize is the length in bytes of a crypto_box public or private key.
|
||||||
|
// It is also the byte count requested from the BIP85 HEX application.
|
||||||
|
KeySize = 32
|
||||||
|
|
||||||
|
// MaxIndex is the largest valid Params.Index. BIP85 path components
|
||||||
|
// are hardened BIP32 child indexes, which leaves 31 bits.
|
||||||
|
MaxIndex = hdkeychain.HardenedKeyStart - 1
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrIndexTooLarge is returned by New when Params.Index exceeds MaxIndex.
|
||||||
|
var ErrIndexTooLarge = errors.New("cbk: index exceeds the BIP32 hardened range")
|
||||||
|
|
||||||
|
// CryptoBoxKey is a 32-byte crypto_box key, either public or private.
|
||||||
|
type CryptoBoxKey [KeySize]byte
|
||||||
|
|
||||||
|
// Bytes returns a copy of the key as a byte slice.
|
||||||
|
func (k CryptoBoxKey) Bytes() []byte {
|
||||||
|
return k[:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hex returns the key as 64 lowercase hexadecimal characters.
|
||||||
|
func (k CryptoBoxKey) Hex() string {
|
||||||
|
return hex.EncodeToString(k[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Params configures New.
|
||||||
|
type Params struct {
|
||||||
|
// Mnemonic is the BIP39 mnemonic phrase. Any letter case and any
|
||||||
|
// whitespace between words is accepted; it is normalized before use.
|
||||||
|
Mnemonic string
|
||||||
|
|
||||||
|
// Index selects which of the keypairs derivable from the mnemonic to
|
||||||
|
// return. It is the final component of the BIP85 derivation path, so
|
||||||
|
// each index yields an independent keypair. Zero is the first.
|
||||||
|
Index uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
// CryptoBoxKeypair is a crypto_box public and private key pair.
|
||||||
|
type CryptoBoxKeypair struct {
|
||||||
|
PublicKey CryptoBoxKey
|
||||||
|
PrivateKey CryptoBoxKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// New derives the keypair for the mnemonic and index in p. If the mnemonic
|
||||||
|
// is not valid it returns an error wrapping one of the bip39wordlist
|
||||||
|
// sentinel errors.
|
||||||
|
func New(p Params) (CryptoBoxKeypair, error) {
|
||||||
|
if p.Index > MaxIndex {
|
||||||
|
return CryptoBoxKeypair{}, fmt.Errorf("%w: %d", ErrIndexTooLarge, p.Index)
|
||||||
|
}
|
||||||
|
|
||||||
|
mnemonic, err := bip39wordlist.NormalizeWordlist(p.Mnemonic)
|
||||||
|
if err != nil {
|
||||||
|
return CryptoBoxKeypair{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// The seed is always computed with an empty BIP39 passphrase. The
|
||||||
|
// network parameters only choose the version bytes used when a key is
|
||||||
|
// serialized; they do not affect derivation.
|
||||||
|
masterKey, err := hdkeychain.NewMaster(
|
||||||
|
bip39.NewSeed(mnemonic, ""), &chaincfg.MainNetParams,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return CryptoBoxKeypair{}, fmt.Errorf("cbk: deriving BIP32 master key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
privateKeyHex, err := bip85.DeriveHex(masterKey, KeySize, p.Index)
|
||||||
|
if err != nil {
|
||||||
|
return CryptoBoxKeypair{}, fmt.Errorf("cbk: deriving BIP85 entropy: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
privateKeyBytes, err := hex.DecodeString(privateKeyHex)
|
||||||
|
if err != nil {
|
||||||
|
return CryptoBoxKeypair{}, fmt.Errorf("cbk: decoding BIP85 entropy: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
publicKey, privateKey, err := box.GenerateKey(bytes.NewReader(privateKeyBytes))
|
||||||
|
if err != nil {
|
||||||
|
return CryptoBoxKeypair{}, fmt.Errorf("cbk: computing public key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return CryptoBoxKeypair{
|
||||||
|
PublicKey: CryptoBoxKey(*publicKey),
|
||||||
|
PrivateKey: CryptoBoxKey(*privateKey),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FromMnemonic derives the keypair for mnemonic at index zero. It is
|
||||||
|
// shorthand for New(Params{Mnemonic: mnemonic}).
|
||||||
|
func FromMnemonic(mnemonic string) (CryptoBoxKeypair, error) {
|
||||||
|
return New(Params{Mnemonic: mnemonic, Index: 0})
|
||||||
|
}
|
||||||
|
|
||||||
|
// PublicKeyBytes returns a copy of the public key as a byte slice.
|
||||||
|
func (kp CryptoBoxKeypair) PublicKeyBytes() []byte {
|
||||||
|
return kp.PublicKey.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrivateKeyBytes returns a copy of the private key as a byte slice.
|
||||||
|
func (kp CryptoBoxKeypair) PrivateKeyBytes() []byte {
|
||||||
|
return kp.PrivateKey.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
// PublicKeyHex returns the public key as 64 lowercase hexadecimal
|
||||||
|
// characters.
|
||||||
|
func (kp CryptoBoxKeypair) PublicKeyHex() string {
|
||||||
|
return kp.PublicKey.Hex()
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrivateKeyHex returns the private key as 64 lowercase hexadecimal
|
||||||
|
// characters.
|
||||||
|
func (kp CryptoBoxKeypair) PrivateKeyHex() string {
|
||||||
|
return kp.PrivateKey.Hex()
|
||||||
|
}
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
package cbk_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/btcutil/hdkeychain"
|
||||||
|
"github.com/btcsuite/btcd/chaincfg"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/tyler-smith/go-bip39"
|
||||||
|
|
||||||
|
"sneak.berlin/go/secret/pkg/bip85"
|
||||||
|
"sneak.berlin/go/x/pkg/bip39wordlist"
|
||||||
|
"sneak.berlin/go/x/pkg/cbk"
|
||||||
|
)
|
||||||
|
|
||||||
|
// The BIP85 specification publishes its test vectors against one master
|
||||||
|
// key. specMnemonic is the BIP39 phrase that produces that master key, so
|
||||||
|
// the whole mnemonic-to-entropy chain this package relies on can be
|
||||||
|
// checked against the specification.
|
||||||
|
const (
|
||||||
|
specMnemonic = "install scatter logic circle pencil average fall shoe " +
|
||||||
|
"quantum disease suspect usage"
|
||||||
|
|
||||||
|
specMasterKey = "xprv9s21ZrQH143K2LBWUUQRFXhucrQqBpKdRRxNVq2zBqsx8HVqFk2uYo8" +
|
||||||
|
"kmbaLLHRdqtQpUm98uKfu3vca1LqdGhUtyoFnCNkfmXRyPXLjbKb"
|
||||||
|
|
||||||
|
// specHex64 is the specification's HEX application vector for
|
||||||
|
// m/83696968'/128169'/64'/0'.
|
||||||
|
specHex64 = "492db4698cf3b73a5a24998aa3e9d7fa96275d85724a91e7" +
|
||||||
|
"1aa2d645442f878555d078fd1f1f67e368976f04137b1f7a" +
|
||||||
|
"0d19232136ca50c44614af72b5582a5c"
|
||||||
|
)
|
||||||
|
|
||||||
|
// The keys below are the keypair for specMnemonic at index 0, pinned when
|
||||||
|
// the BIP85 derivation was implemented. If this test starts failing, the
|
||||||
|
// derivation has changed and existing keys are no longer reproducible.
|
||||||
|
const (
|
||||||
|
vectorPublicKeyHex = "dbef711b3f340d336eb99cd8d0dbd491" +
|
||||||
|
"e1edce8eaad71cd66be228032a9a743e"
|
||||||
|
|
||||||
|
vectorPrivateKeyHex = "ea3ceb0b02ee8e587779c63f4b7b3a21" +
|
||||||
|
"e950a213f1ec53cab608d13e8796e6dc"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSpecMnemonicProducesSpecMasterKey(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
masterKey := specMasterKeyFromMnemonic(t)
|
||||||
|
assert.Equal(t, specMasterKey, masterKey.String())
|
||||||
|
|
||||||
|
hex64, err := bip85.DeriveHex(masterKey, 64, 0)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, specHex64, hex64)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewMatchesPinnedVector(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
keypair, err := cbk.FromMnemonic(specMnemonic)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, vectorPublicKeyHex, keypair.PublicKeyHex())
|
||||||
|
assert.Equal(t, vectorPrivateKeyHex, keypair.PrivateKeyHex())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPrivateKeyIsBIP85HexApplicationOutput(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
masterKey := specMasterKeyFromMnemonic(t)
|
||||||
|
|
||||||
|
for _, index := range []uint32{0, 1, 7} {
|
||||||
|
expected, err := bip85.DeriveHex(masterKey, cbk.KeySize, index)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
keypair, err := cbk.New(cbk.Params{Mnemonic: specMnemonic, Index: index})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, expected, keypair.PrivateKeyHex(), "index %d", index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewIndexSelectsIndependentKeypairs(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
first, err := cbk.New(cbk.Params{Mnemonic: specMnemonic, Index: 0})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
second, err := cbk.New(cbk.Params{Mnemonic: specMnemonic, Index: 1})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.NotEqual(t, first.PublicKey, second.PublicKey)
|
||||||
|
assert.NotEqual(t, first.PrivateKey, second.PrivateKey)
|
||||||
|
|
||||||
|
fromMnemonic, err := cbk.FromMnemonic(specMnemonic)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, first, fromMnemonic, "FromMnemonic uses index 0")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewRejectsIndexBeyondHardenedRange(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
_, err := cbk.New(cbk.Params{Mnemonic: specMnemonic, Index: cbk.MaxIndex + 1})
|
||||||
|
require.ErrorIs(t, err, cbk.ErrIndexTooLarge)
|
||||||
|
|
||||||
|
_, err = cbk.New(cbk.Params{Mnemonic: specMnemonic, Index: cbk.MaxIndex})
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewNormalizesMnemonic(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
canonical, err := cbk.FromMnemonic(specMnemonic)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
words := strings.Fields(specMnemonic)
|
||||||
|
messy := " " + strings.ToUpper(words[0]) + "\t" +
|
||||||
|
strings.Join(words[1:], " ") + "\n"
|
||||||
|
|
||||||
|
normalized, err := cbk.New(cbk.Params{Mnemonic: messy, Index: 0})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, canonical, normalized)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewDifferentMnemonicsGiveDifferentKeys(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
other, err := bip39wordlist.GenerateMnemonic(bip39wordlist.MaxWordCount)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
specKeypair, err := cbk.FromMnemonic(specMnemonic)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
otherKeypair, err := cbk.FromMnemonic(other)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.NotEqual(t, specKeypair.PublicKey, otherKeypair.PublicKey)
|
||||||
|
assert.NotEqual(t, specKeypair.PrivateKey, otherKeypair.PrivateKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewRejectsInvalidMnemonic(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
_, err := cbk.FromMnemonic("abandon bogus about")
|
||||||
|
require.ErrorIs(t, err, bip39wordlist.ErrInvalidWordCount)
|
||||||
|
|
||||||
|
words := strings.Fields(specMnemonic)
|
||||||
|
words[len(words)-1] = "bogus"
|
||||||
|
|
||||||
|
_, err = cbk.FromMnemonic(strings.Join(words, " "))
|
||||||
|
require.ErrorIs(t, err, bip39wordlist.ErrUnknownWord)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestKeyAccessors(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
keypair, err := cbk.FromMnemonic(specMnemonic)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, keypair.PublicKey[:], keypair.PublicKeyBytes())
|
||||||
|
assert.Equal(t, keypair.PrivateKey[:], keypair.PrivateKeyBytes())
|
||||||
|
assert.Len(t, keypair.PublicKeyBytes(), cbk.KeySize)
|
||||||
|
assert.Len(t, keypair.PrivateKeyBytes(), cbk.KeySize)
|
||||||
|
assert.Equal(t, keypair.PublicKey.Hex(), keypair.PublicKeyHex())
|
||||||
|
assert.Equal(t, keypair.PrivateKey.Hex(), keypair.PrivateKeyHex())
|
||||||
|
assert.Len(t, keypair.PublicKeyHex(), 2*cbk.KeySize)
|
||||||
|
}
|
||||||
|
|
||||||
|
// specMasterKeyFromMnemonic builds the BIP32 master key from specMnemonic
|
||||||
|
// the same way the package does, independently of it.
|
||||||
|
func specMasterKeyFromMnemonic(t *testing.T) *hdkeychain.ExtendedKey {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
seed := bip39.NewSeed(specMnemonic, "")
|
||||||
|
|
||||||
|
masterKey, err := hdkeychain.NewMaster(seed, &chaincfg.MainNetParams)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
return masterKey
|
||||||
|
}
|
||||||
Executable
+104
@@ -0,0 +1,104 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# script/bootstrap: install all dependencies needed to build and develop
|
||||||
|
# this repo. Idempotent: every install is guarded by a check so already
|
||||||
|
# installed tools are skipped. Base tools, go, node and yarn come from
|
||||||
|
# nix, apt, brew, or apk (detected in that order), with no version and
|
||||||
|
# no hash, because the distribution is trusted. It installs node, yarn
|
||||||
|
# and the node packages only when yarn.lock is in the tree. Assumes
|
||||||
|
# nothing is present.
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||||
|
|
||||||
|
PKGMGR=""
|
||||||
|
SUDO=""
|
||||||
|
APT_UPDATED=""
|
||||||
|
|
||||||
|
detect_pkgmgr() {
|
||||||
|
[ -n "$PKGMGR" ] && return 0
|
||||||
|
if command -v nix-env >/dev/null 2>&1; then
|
||||||
|
PKGMGR="nix"
|
||||||
|
elif command -v apt-get >/dev/null 2>&1; then
|
||||||
|
PKGMGR="apt"
|
||||||
|
elif command -v brew >/dev/null 2>&1; then
|
||||||
|
PKGMGR="brew"
|
||||||
|
elif command -v apk >/dev/null 2>&1; then
|
||||||
|
PKGMGR="apk"
|
||||||
|
else
|
||||||
|
echo "bootstrap: no supported package manager (nix, apt, brew, apk)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [ "$PKGMGR" = "apt" ]; then
|
||||||
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
|
if [ "$(id -u)" != "0" ]; then
|
||||||
|
SUDO="sudo"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# pkg_install <nix-attr> <apt-pkg> <brew-formula> <apk-pkg>
|
||||||
|
pkg_install() {
|
||||||
|
detect_pkgmgr
|
||||||
|
case "$PKGMGR" in
|
||||||
|
nix) nix-env -iA "nixpkgs.$1" ;;
|
||||||
|
apt)
|
||||||
|
# Package lists may be empty (fresh images); refresh once per run.
|
||||||
|
if [ -z "$APT_UPDATED" ]; then
|
||||||
|
$SUDO env DEBIAN_FRONTEND=noninteractive apt-get update
|
||||||
|
APT_UPDATED=1
|
||||||
|
fi
|
||||||
|
$SUDO env DEBIAN_FRONTEND=noninteractive apt-get install -y "$2"
|
||||||
|
;;
|
||||||
|
brew) brew install "$3" ;;
|
||||||
|
apk) apk add --no-cache "$4" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
missing() {
|
||||||
|
! command -v "$1" >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Debian and Ubuntu name the yarn command yarnpkg.
|
||||||
|
run_yarn() {
|
||||||
|
if command -v yarn >/dev/null 2>&1; then
|
||||||
|
yarn "$@"
|
||||||
|
elif command -v yarnpkg >/dev/null 2>&1; then
|
||||||
|
yarnpkg "$@"
|
||||||
|
else
|
||||||
|
echo "no yarn; run script/bootstrap first" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
main() {
|
||||||
|
cd "$ROOT"
|
||||||
|
|
||||||
|
if missing make; then pkg_install gnumake make make make; fi
|
||||||
|
if missing git; then pkg_install git git git git; fi
|
||||||
|
# Go on the host is for script/fmt and script/fmt-check (gofmt) and
|
||||||
|
# for make build; the gates compile inside the Dockerfile's phases.
|
||||||
|
if missing go; then pkg_install go golang-go go go; fi
|
||||||
|
|
||||||
|
# A Dockerfile stage that needs no yarn copies no yarn.lock.
|
||||||
|
if [ -f yarn.lock ]; then
|
||||||
|
if missing node; then pkg_install nodejs nodejs node nodejs; fi
|
||||||
|
if missing yarn && missing yarnpkg; then
|
||||||
|
pkg_install yarn yarnpkg yarn yarn
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Debian 13 and Ubuntu 26.04 package yarn 4, which refuses a yarn 1
|
||||||
|
# yarn.lock, or rewrites it in its own format.
|
||||||
|
yarn_version="$(run_yarn --version)"
|
||||||
|
if [ "${yarn_version%%.*}" != "1" ]; then
|
||||||
|
echo "bootstrap: yarn $yarn_version refuses this yarn 1 yarn.lock;" \
|
||||||
|
"use a distribution that packages yarn 1" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
run_yarn install --frozen-lockfile
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "bootstrap complete"
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
Executable
+16
@@ -0,0 +1,16 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# script/check: run all checks (test, lint, fmt-check). Our own
|
||||||
|
# extension to scripts-to-rule-them-all. test and lint are Docker
|
||||||
|
# phases; fmt-check is native, because a formatter writes the working
|
||||||
|
# tree. Must not modify any files.
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
||||||
|
|
||||||
|
main() {
|
||||||
|
"$SCRIPT_DIR/test"
|
||||||
|
"$SCRIPT_DIR/lint"
|
||||||
|
"$SCRIPT_DIR/fmt-check"
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
Executable
+33
@@ -0,0 +1,33 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# script/cibuild: run the CI build. It bootstraps first: a CI runner
|
||||||
|
# checks out and runs this and nothing else, and script/fmt-check runs
|
||||||
|
# the formatter on the host, which a pristine checkout cannot do.
|
||||||
|
# A new CACHEBUST value reruns every RUN step after the ARG CACHEBUST
|
||||||
|
# line, since a cached gate phase is a check that did not run; unlike
|
||||||
|
# --no-cache, it keeps the layers above that line and the cache mounts.
|
||||||
|
# This relies on every stage of the Dockerfile declaring ARG CACHEBUST
|
||||||
|
# before every step that lints, tests or compiles; a stage without that
|
||||||
|
# line is served from cache on an unchanged tree, with no warning.
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
||||||
|
ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
|
||||||
|
|
||||||
|
main() {
|
||||||
|
cd "$ROOT"
|
||||||
|
"$SCRIPT_DIR/bootstrap"
|
||||||
|
"$SCRIPT_DIR/check"
|
||||||
|
# The version and the tag each get their own line: a failing
|
||||||
|
# command substitution inside an argument does not trip `set -e`,
|
||||||
|
# so the inline form degrades silently to an empty constant. The
|
||||||
|
# VERSION build argument takes precedence over the version a build
|
||||||
|
# stage derives from the .git in the context.
|
||||||
|
version="$(git describe --tags --always --dirty 2>/dev/null || true)"
|
||||||
|
[ -n "$version" ] || version="unknown"
|
||||||
|
tag="$("$SCRIPT_DIR/projectname")"
|
||||||
|
docker build --build-arg CACHEBUST="$(date +%s)-$$" \
|
||||||
|
--build-arg VERSION="$version" \
|
||||||
|
-t "$tag" .
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
Executable
+30
@@ -0,0 +1,30 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# script/docker: build the Docker image tagged with the project name.
|
||||||
|
# Identical in all repos; the tag comes from script/projectname.
|
||||||
|
# A new CACHEBUST value reruns every RUN step after the ARG CACHEBUST
|
||||||
|
# line, since a cached gate phase is a check that did not run; unlike
|
||||||
|
# --no-cache, it keeps the layers above that line and the cache mounts.
|
||||||
|
# This relies on every stage of the Dockerfile declaring ARG CACHEBUST
|
||||||
|
# before every step that lints, tests or compiles; a stage without that
|
||||||
|
# line is served from cache on an unchanged tree, with no warning.
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
||||||
|
ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
|
||||||
|
|
||||||
|
main() {
|
||||||
|
cd "$ROOT"
|
||||||
|
# The version and the tag each get their own line: a failing
|
||||||
|
# command substitution inside an argument does not trip `set -e`,
|
||||||
|
# so the inline form degrades silently to an empty constant. The
|
||||||
|
# VERSION build argument takes precedence over the version a build
|
||||||
|
# stage derives from the .git in the context.
|
||||||
|
version="$(git describe --tags --always --dirty 2>/dev/null || true)"
|
||||||
|
[ -n "$version" ] || version="unknown"
|
||||||
|
tag="$("$SCRIPT_DIR/projectname")"
|
||||||
|
docker build --build-arg CACHEBUST="$(date +%s)-$$" \
|
||||||
|
--build-arg VERSION="$version" \
|
||||||
|
-t "$tag" .
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
Executable
+29
@@ -0,0 +1,29 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# script/fmt: format all files (writes): gofmt for Go, prettier for
|
||||||
|
# markdown.
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||||
|
|
||||||
|
GO_DIRS="cmd internal pkg"
|
||||||
|
|
||||||
|
# Debian and Ubuntu name the yarn command yarnpkg.
|
||||||
|
run_yarn() {
|
||||||
|
if command -v yarn >/dev/null 2>&1; then
|
||||||
|
yarn "$@"
|
||||||
|
elif command -v yarnpkg >/dev/null 2>&1; then
|
||||||
|
yarnpkg "$@"
|
||||||
|
else
|
||||||
|
echo "no yarn; run script/bootstrap first" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
main() {
|
||||||
|
cd "$ROOT"
|
||||||
|
# shellcheck disable=SC2086 # GO_DIRS is a word list on purpose
|
||||||
|
gofmt -w $GO_DIRS
|
||||||
|
run_yarn run prettier --write '**/*.md' --tab-width 4 --prose-wrap always
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
Executable
+34
@@ -0,0 +1,34 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# script/fmt-check: check formatting (read-only): gofmt for Go, prettier
|
||||||
|
# for markdown.
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||||
|
|
||||||
|
GO_DIRS="cmd internal pkg"
|
||||||
|
|
||||||
|
# Debian and Ubuntu name the yarn command yarnpkg.
|
||||||
|
run_yarn() {
|
||||||
|
if command -v yarn >/dev/null 2>&1; then
|
||||||
|
yarn "$@"
|
||||||
|
elif command -v yarnpkg >/dev/null 2>&1; then
|
||||||
|
yarnpkg "$@"
|
||||||
|
else
|
||||||
|
echo "no yarn; run script/bootstrap first" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
main() {
|
||||||
|
cd "$ROOT"
|
||||||
|
# shellcheck disable=SC2086 # GO_DIRS is a word list on purpose
|
||||||
|
unformatted="$(gofmt -l $GO_DIRS)"
|
||||||
|
if [ -n "$unformatted" ]; then
|
||||||
|
echo "gofmt: the following files need formatting:" >&2
|
||||||
|
echo "$unformatted" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
run_yarn run prettier --check '**/*.md' --tab-width 4 --prose-wrap always
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
Executable
+16
@@ -0,0 +1,16 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# script/install-precommit: install the git pre-commit hook that runs
|
||||||
|
# script/precommit. Our own extension to scripts-to-rule-them-all.
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||||
|
|
||||||
|
main() {
|
||||||
|
cd "$ROOT"
|
||||||
|
hook=".git/hooks/pre-commit"
|
||||||
|
printf '#!/bin/sh\nset -e\nscript/precommit\n' > .git/hooks/pre-commit
|
||||||
|
chmod +x .git/hooks/pre-commit
|
||||||
|
echo "pre-commit hook installed: runs script/precommit"
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
Executable
+28
@@ -0,0 +1,28 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# script/lint: run the linter. Linting is a phase of the Dockerfile and
|
||||||
|
# this builds that phase alone; the linter is never installed or run on
|
||||||
|
# a developer host, where a shared result cache and a host-global lock
|
||||||
|
# make its answer untrustworthy.
|
||||||
|
#
|
||||||
|
# The phase is not the last stage in the file, so it is built only when
|
||||||
|
# --target names it. A new CACHEBUST value reruns every RUN step after
|
||||||
|
# the ARG CACHEBUST line, since a cached lint layer is a lint that did
|
||||||
|
# not run; unlike --no-cache, it keeps the layers above that line and
|
||||||
|
# the cache mounts. This relies on every stage of the Dockerfile
|
||||||
|
# declaring ARG CACHEBUST before every step that lints, tests or
|
||||||
|
# compiles; a stage without that line is served from cache on an
|
||||||
|
# unchanged tree, with no warning.
|
||||||
|
# --output type=cacheonly writes no image, since nothing uses one.
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
||||||
|
ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
|
||||||
|
|
||||||
|
main() {
|
||||||
|
cd "$ROOT"
|
||||||
|
docker build --build-arg CACHEBUST="$(date +%s)-$$" \
|
||||||
|
--target lint \
|
||||||
|
--output type=cacheonly .
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
Executable
+22
@@ -0,0 +1,22 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# script/precommit: run by the git pre-commit hook; fails the commit if
|
||||||
|
# checks fail. Our own extension to scripts-to-rule-them-all. The Go
|
||||||
|
# extra: go.mod and go.sum must already be tidy, so a commit never
|
||||||
|
# carries a module file that go mod tidy would rewrite.
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
||||||
|
ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
|
||||||
|
|
||||||
|
main() {
|
||||||
|
cd "$ROOT"
|
||||||
|
go mod tidy
|
||||||
|
if ! git diff --quiet --exit-code -- go.mod go.sum; then
|
||||||
|
echo "precommit: go mod tidy changed go.mod or go.sum;" \
|
||||||
|
"stage them and retry" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
"$SCRIPT_DIR/check"
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
Executable
+12
@@ -0,0 +1,12 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# script/projectname: output the name of this project. Our own
|
||||||
|
# extension to scripts-to-rule-them-all. Other scripts that need the
|
||||||
|
# name (e.g. script/docker) call this, so they can stay identical
|
||||||
|
# across all repos.
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
main() {
|
||||||
|
echo "x"
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
Executable
+13
@@ -0,0 +1,13 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# script/setup: set up the repo for development after a fresh clone:
|
||||||
|
# installs dependencies and the git pre-commit hook.
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
||||||
|
|
||||||
|
main() {
|
||||||
|
"$SCRIPT_DIR/bootstrap"
|
||||||
|
"$SCRIPT_DIR/install-precommit"
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
Executable
+24
@@ -0,0 +1,24 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# script/test: run the test suite. Testing is a phase of the Dockerfile
|
||||||
|
# and this builds that phase alone, on the same terms as script/lint:
|
||||||
|
# --target because a phase that is not the last stage is built only when
|
||||||
|
# named, and a new CACHEBUST value because a cached test layer is a test
|
||||||
|
# that did not run; unlike --no-cache, it keeps the layers above the ARG
|
||||||
|
# CACHEBUST line and the cache mounts. This relies on every stage of the
|
||||||
|
# Dockerfile declaring ARG CACHEBUST before every step that lints, tests
|
||||||
|
# or compiles; a stage without that line is served from cache on an
|
||||||
|
# unchanged tree, with no warning. --output type=cacheonly writes no
|
||||||
|
# image, since nothing uses one.
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
||||||
|
ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
|
||||||
|
|
||||||
|
main() {
|
||||||
|
cd "$ROOT"
|
||||||
|
docker build --build-arg CACHEBUST="$(date +%s)-$$" \
|
||||||
|
--target test \
|
||||||
|
--output type=cacheonly .
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||||
|
# yarn lockfile v1
|
||||||
|
|
||||||
|
|
||||||
|
prettier@3.9.10:
|
||||||
|
version "3.9.10"
|
||||||
|
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.9.10.tgz#ac378e432f1a7381518f2e080816bbaefa38d585"
|
||||||
|
integrity sha512-skGHTH1Yb9QvaTC4hWnRTzEYtCpApF0UMBPz+t/0ZPzP8ihefo/btKd+QA7pUE0fEwA1SbS/NmhyEcstdXOIdA==
|
||||||
Reference in new issue
Block a user