diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8dca3a1..c4833a2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -71,6 +71,45 @@ jobs: - name: Build run: pnpm build + cli: + name: signet CLI (Go) + runs-on: ubuntu-latest + timeout-minutes: 10 + defaults: + run: + working-directory: cli + steps: + - uses: actions/checkout@v7 + + - uses: actions/setup-go@v5 + with: + go-version-file: cli/go.mod + + - name: Build + run: go build ./... + + - name: Vet + run: go vet ./... + + - name: Test + run: go test ./... + + # Three things have to agree here, and the original v6 + `version: latest` + # got two of them wrong: + # * cli/.golangci.yml is a v2 config (`version: "2"`, and the v2-only + # `formatters:` block), so the action must be v8 — v6 drives + # golangci-lint v1 and passes v1-only flags. + # * golangci-lint refuses to run when the Go version it was *built with* + # is older than the module's target, so this release has to be one + # built with >= go 1.25 to match cli/go.mod. v2.1.6 is built with + # go1.24 and fails with "can't load config: the Go language version + # (go1.24) ... is lower than the targeted Go version (1.25.0)". + # * `latest` would let both of those drift again without a commit here. + - uses: golangci/golangci-lint-action@v8 + with: + version: v2.13.2 + working-directory: cli + contracts: name: soroban contract tests runs-on: ubuntu-latest diff --git a/README.md b/README.md index 786ced5..8698089 100644 --- a/README.md +++ b/README.md @@ -169,6 +169,7 @@ set from the event stream. | `packages/db` | Prisma schema + generated client | | `packages/sdk` | External SDK for integrators | | `packages/types` | Shared TypeScript types | +| `cli` | `signet` CLI (Go) — links wallets, manages keys, talks to a Signet deployment | | `infra` | Local dev infra (Docker Postgres) | ## Scripts @@ -183,6 +184,7 @@ set from the event stream. | `pnpm db:migrate` | Run Prisma migrations | | `pnpm test` | All TypeScript tests via Turborepo | | `cargo test` (in `packages/contracts`) | Identity Registry unit tests | +| `go build ./...` / `go test ./...` (in `cli`) | Build / test the `signet` CLI | ## Tests diff --git a/cli/.gitignore b/cli/.gitignore new file mode 100644 index 0000000..ae3c172 --- /dev/null +++ b/cli/.gitignore @@ -0,0 +1 @@ +/bin/ diff --git a/cli/.golangci.yml b/cli/.golangci.yml new file mode 100644 index 0000000..5fbd60a --- /dev/null +++ b/cli/.golangci.yml @@ -0,0 +1,16 @@ +version: "2" + +linters: + default: standard + enable: + - bodyclose + - errcheck + - govet + - ineffassign + - staticcheck + - unused + +formatters: + enable: + - gofmt + - goimports diff --git a/cli/README.md b/cli/README.md new file mode 100644 index 0000000..acf5e70 --- /dev/null +++ b/cli/README.md @@ -0,0 +1,52 @@ +# signet CLI + +The `signet` command-line companion to the Signet developer identity registry +on Stellar/Soroban. It lives beside the pnpm workspace rather than inside it — +this is a standalone Go module, not a pnpm package — mirroring the split +between orchestration/UX (Go, here) and anything that must execute Soroban +semantics (Rust, `packages/contracts`). + +This module is currently a scaffold: the command tree exists (`signet +--help`, `signet --version`), but the actual subcommands (linking a wallet, +managing keys, talking to a deployment) land in follow-up issues. + +## Build + +```bash +cd cli +go build -o bin/signet ./cmd/signet +``` + +To bake a version string and commit hash into the binary: + +```bash +go build \ + -ldflags "-X main.version=$(git describe --tags --always) -X main.commit=$(git rev-parse --short HEAD)" \ + -o bin/signet ./cmd/signet +``` + +## Test / lint + +```bash +go vet ./... +go test ./... +golangci-lint run ./... +``` + +No cgo is used anywhere in this module, so it cross-compiles with the +standard `GOOS`/`GOARCH` combinations, e.g.: + +```bash +GOOS=darwin GOARCH=arm64 go build -o bin/signet-darwin-arm64 ./cmd/signet +GOOS=linux GOARCH=amd64 go build -o bin/signet-linux-amd64 ./cmd/signet +``` + +## Layout + +| Path | Purpose | +|------|---------| +| `cmd/signet` | `main.go` — the binary's entrypoint | +| `internal/cmd` | Cobra command tree | +| `internal/link` | Bind a wallet to a Signet handle (scaffolded, not yet implemented) | +| `internal/keys` | Local signing key management (scaffolded, not yet implemented) | +| `internal/spec` | Typed request/response models for a Signet deployment's HTTP API (scaffolded, not yet implemented) | diff --git a/cli/cmd/signet/main.go b/cli/cmd/signet/main.go new file mode 100644 index 0000000..796e01f --- /dev/null +++ b/cli/cmd/signet/main.go @@ -0,0 +1,28 @@ +// Command signet is the CLI companion to the Signet developer identity +// registry on Stellar/Soroban: it links wallets to on-chain handles, manages +// local signing keys, and talks to a Signet deployment over its HTTP API. +package main + +import ( + "fmt" + "os" + + "github.com/blockchain-maxis/signet/cli/internal/cmd" +) + +// version and commit are overridden at build time via: +// +// go build -ldflags "-X main.version=$(git describe --tags) -X main.commit=$(git rev-parse --short HEAD)" +// +// They default to "dev"/"none" for a plain `go build` or `go run`. +var ( + version = "dev" + commit = "none" +) + +func main() { + if err := cmd.Execute(version, commit); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} diff --git a/cli/go.mod b/cli/go.mod new file mode 100644 index 0000000..07575ab --- /dev/null +++ b/cli/go.mod @@ -0,0 +1,11 @@ +module github.com/blockchain-maxis/signet/cli + +go 1.25.0 + +require github.com/spf13/cobra v1.8.1 + +require ( + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + golang.org/x/net v0.58.0 // indirect +) diff --git a/cli/go.sum b/cli/go.sum new file mode 100644 index 0000000..79e887c --- /dev/null +++ b/cli/go.sum @@ -0,0 +1,12 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= +golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/cli/internal/cmd/root.go b/cli/internal/cmd/root.go new file mode 100644 index 0000000..20b12d6 --- /dev/null +++ b/cli/internal/cmd/root.go @@ -0,0 +1,46 @@ +// Package cmd wires up the signet CLI's command tree with Cobra. Subcommands +// that talk to a wallet, a keyring, or a Signet deployment live in their own +// internal packages (link, keys, spec) and are attached here as they land. +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +func newRootCmd(version, commit string) *cobra.Command { + root := &cobra.Command{ + Use: "signet", + Short: "Link wallets and manage identity on the Signet registry", + Long: `signet is the command-line companion to the Signet developer identity +registry on Stellar/Soroban. + +It links a local wallet to an on-chain handle, manages signing keys, and +talks to a Signet deployment (the default hosted one, or a self-hosted +instance) over its HTTP API.`, + SilenceUsage: true, + SilenceErrors: true, + } + + // Cobra only renders the "Usage:" section of --help (and of a bare + // invocation) when the command is Runnable() or has subcommands — neither + // is true yet for a fresh scaffold with no subcommands attached. Giving it + // a RunE that just prints help keeps `signet` and `signet --help` both + // showing real usage instead of only the Long description. + root.RunE = func(cmd *cobra.Command, _ []string) error { + return cmd.Help() + } + + // Cobra wires --version to this automatically once Version is non-empty. + root.Version = fmt.Sprintf("%s (commit %s)", version, commit) + root.SetVersionTemplate("signet version {{.Version}}\n") + + return root +} + +// Execute builds the command tree and runs it against the process's +// arguments. main() only needs to report the error and set an exit code. +func Execute(version, commit string) error { + return newRootCmd(version, commit).Execute() +} diff --git a/cli/internal/cmd/root_test.go b/cli/internal/cmd/root_test.go new file mode 100644 index 0000000..1fba9b2 --- /dev/null +++ b/cli/internal/cmd/root_test.go @@ -0,0 +1,63 @@ +package cmd + +import ( + "bytes" + "strings" + "testing" +) + +func TestRootCmdVersion(t *testing.T) { + root := newRootCmd("1.2.3", "abc1234") + out := &bytes.Buffer{} + root.SetOut(out) + root.SetErr(out) + root.SetArgs([]string{"--version"}) + + if err := root.Execute(); err != nil { + t.Fatalf("--version returned an error: %v", err) + } + + got := out.String() + want := "signet version 1.2.3 (commit abc1234)\n" + if got != want { + t.Fatalf("--version output = %q, want %q", got, want) + } +} + +func TestRootCmdHelp(t *testing.T) { + root := newRootCmd("dev", "none") + out := &bytes.Buffer{} + root.SetOut(out) + root.SetErr(out) + root.SetArgs([]string{"--help"}) + + if err := root.Execute(); err != nil { + t.Fatalf("--help returned an error: %v", err) + } + + got := out.String() + if !strings.Contains(got, "signet") { + t.Fatalf("--help output does not mention the command name: %q", got) + } + if !strings.Contains(got, "Usage:") { + t.Fatalf("--help output does not print usage: %q", got) + } +} + +func TestRootCmdRunsWithoutArgs(t *testing.T) { + // No subcommand is implemented yet — the bare root command should still + // print its own help rather than erroring, so `signet` alone is a + // friendly landing page rather than a crash. + root := newRootCmd("dev", "none") + out := &bytes.Buffer{} + root.SetOut(out) + root.SetErr(out) + root.SetArgs([]string{}) + + if err := root.Execute(); err != nil { + t.Fatalf("running with no args returned an error: %v", err) + } + if !strings.Contains(out.String(), "Usage:") { + t.Fatalf("running with no args did not print help: %q", out.String()) + } +} diff --git a/cli/internal/keys/doc.go b/cli/internal/keys/doc.go new file mode 100644 index 0000000..8655892 --- /dev/null +++ b/cli/internal/keys/doc.go @@ -0,0 +1,4 @@ +// Package keys will manage local Stellar signing keys used by the CLI — +// generating, storing, and loading them for the link and spec commands to +// sign with. Scaffolded here; implemented in a follow-up issue. +package keys diff --git a/cli/internal/link/doc.go b/cli/internal/link/doc.go new file mode 100644 index 0000000..7af7836 --- /dev/null +++ b/cli/internal/link/doc.go @@ -0,0 +1,4 @@ +// Package link will bind a local wallet key to a Signet handle through the +// Identity Registry's claim/release/transfer operations. Scaffolded here; +// implemented in a follow-up issue. +package link diff --git a/cli/internal/spec/doc.go b/cli/internal/spec/doc.go new file mode 100644 index 0000000..a5f5574 --- /dev/null +++ b/cli/internal/spec/doc.go @@ -0,0 +1,5 @@ +// Package spec will hold the request/response types for a Signet +// deployment's HTTP API, so the rest of the CLI talks to it through typed +// Go values instead of hand-built JSON. Scaffolded here; implemented in a +// follow-up issue. +package spec