BINARY := nself MODULE := github.com/nself-org/cli DIST_DIR := dist VERSION := $(shell cat .github/VERSION 2>/dev/null || git describe --tags 2>/dev/null || echo "dev") COMMIT := $(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown") BUILD_DATE := $(shell date -u +%Y-%m-%dT%H:%M:%SZ) LDFLAGS := -s -w \ -X $(MODULE)/internal/version.Version=$(VERSION) \ -X $(MODULE)/internal/version.Commit=$(COMMIT) \ -X $(MODULE)/internal/version.BuildDate=$(BUILD_DATE) BUILDFLAGS := -trimpath .PHONY: FORCE build clean test vet install cross dist verify-prod sport-f21 sport-f02 cmd-inventory registry canon core-services wiki-commands wiki-check flag-drift-audit parity schemas schemas-check contract-check sbom man fmt fmt-check verify-prod: @bash scripts/prod-verify/p87-verification.sh ## Generator tools (tools/) are built once into $(TOOLS_BIN)/ and then run. ## `go run` would store a fresh 35-50 MB linked executable in GOCACHE for every ## changed tool (Go 1.24+, no size cap); `go build -o` keeps it in the ignored bin/. ## FORCE rebuilds on every make invocation so an edited tool is never stale; the ## compile itself stays cached. TOOLS_BIN := bin $(TOOLS_BIN)/%: FORCE @CGO_ENABLED=0 go build -mod=vendor -o $@ ./tools/$* FORCE: sport-f21: @bash scripts/sport/generate-f21.sh ## cmd-inventory — CLI-R06. Regenerate the command inventory from the cobra tree. ## Writes .github/command-inventory.json, .github/command-registry.json (the ## command registry, contract cli.command-registry v1) and the generated block ## of .github/wiki/Commands.md. ## Set NSELF_SPORT_DIR to also refresh SPORT F02 (it lives in the PPI, not here). ## internal/repoqa asserts the committed copies match, so run this after adding, ## renaming, or removing any command. cmd-inventory: @bash scripts/sport/generate-f02.sh sport-f02: cmd-inventory ## registry — P7-REG-07. Alias of cmd-inventory: regenerates the command registry ## .github/command-registry.json together with the inventory and wiki block. registry: cmd-inventory ## canon — P7-CANON-21. Regenerate cmd/commands/canon_moves_gen.go (the canon engine's ## move table) from internal/canon/canon.yaml and domains/*.yaml. Run after editing ## a canon fragment's rows; TestCanonTableCurrent fails when the file is stale. canon: $(TOOLS_BIN)/canongen @$(TOOLS_BIN)/canongen ## core-services — CLI-R07. Regenerate .github/wiki/Core-Services.md from the ## compose service catalog. Set NSELF_SPORT_DIR to also refresh SPORT F08. core-services: @bash scripts/sport/generate-core-services.sh ## wiki-commands — CLI-R08. Regenerate one wiki page per top-level command, plus ## the sidebar index and llms.txt. Human prose inside PROSE blocks is preserved; ## only the cobra-derived parts are rewritten. Use -report to list pages that ## still carry placeholder prose. wiki-commands: $(TOOLS_BIN)/wikigen @$(TOOLS_BIN)/wikigen -report ## wiki-check — verify the wiki is current and every [[link]] resolves. wiki-check: $(TOOLS_BIN)/wikigen @$(TOOLS_BIN)/wikigen -check @bash scripts/ci/wiki-link-audit.sh ## flag-drift-audit — checks every `nself --flag` invocation in ## .github/wiki/ and scripts/ against the live cobra tree, so a documented or ## scripted flag that was never registered on the command it names fails the ## build instead of shipping silently (see scripts/ci/flag-drift-audit.sh). flag-drift-audit: @bash scripts/ci/flag-drift-audit.sh ## parity — CLI-R17. Regenerate the four-surface parity matrix (wiki page, MCP ## tools, env var docs, HTTP routes) for every top-level command. Writes ## .github/surface-parity.md and .github/surface-parity.json. internal/repoqa ## asserts the committed copies match; run this after adding, renaming, or ## removing a command, an MCP tool, or an env var. parity: $(TOOLS_BIN)/parity @$(TOOLS_BIN)/parity ## schemas — P7-REG-08. Regenerate schemas/ (JSON Schemas for the envelope, error ## object, command registry and every envelope command's data) from the Go types ## with tools/schemagen. Commit the result; internal/repoqa and `schemas-check` ## fail when the committed files drift. schemas: $(TOOLS_BIN)/schemagen @$(TOOLS_BIN)/schemagen ## schemas-check — fail (exit 1) when any schemas/*.json differs from what ## tools/schemagen generates. Non-JSON files under schemas/ are ignored. schemas-check: $(TOOLS_BIN)/schemagen @$(TOOLS_BIN)/schemagen -check ## regen — Run every generator in order to regenerate all generated surfaces ## before verifying a unit. Invoked by .github/land-regen. regen: go run -mod=vendor ./tools/regen ## contract-check — P7-REG-10. The one gate for every machine contract the CLI ## publishes: the committed schemas/, the registry and inventory, the compat ## marker table (cheap, so a drift fails in seconds), then the P7-REG Epic's go ## test lines run twice, with NSELF_V15 unset and =1 (ADR 0021). Nothing is skipped: a missing input, a hand-edited ## generated file or a Go type that drifted from its schema exits non-zero with ## a message naming the check and the fix. The registry check regenerates with ## `make cmd-inventory` into the tree, compares every file it writes (registry, ## inventory, Commands.md, Command-Canon.md, facts.json, the README facts blocks) against what was committed (or ## hand-edited) before, then restores them, so a failing run leaves the tree as ## it found it. The wiki check runs tools/wikigen -check (the generated tables in ## error-codes.md and Exit-Codes.md) and scripts/contract-check-wiki.sh (the ## hand tables in JSON-Output.md and Exit-Codes.md against schemas/index.json, ## the registry and error-codes.md; D-0254). The Linux smoke ## (scripts/ci/contract-smoke.sh) is separate: it drives the built binary. CONTRACT_GOTEST := CGO_ENABLED=0 go test -mod=vendor -count=1 CONTRACT_PKGS := ./internal/errs/... ./internal/output/... ./internal/canon/... ./internal/cmdregistry/... \ ./internal/compat/... ./internal/portable/... ./internal/plugin/manifestv2/... ./tools/schemagen/... CONTRACT_REGEN := .github/command-registry.json .github/command-inventory.json .github/wiki/Commands.md \ .github/wiki/Command-Canon.md .github/facts.json README.md contract-check: $(TOOLS_BIN)/schemagen $(TOOLS_BIN)/wikigen @set -e; \ fail() { echo "contract-check: FAILED: $$1" >&2; echo "contract-check: fix: $$2" >&2; exit 1; }; \ for f in $(CONTRACT_REGEN) schemas/index.json scripts/ci/compat-markers.sh; do \ [ -f "$$f" ] || fail "missing input $$f" "restore it; a gate input that is absent must fail, never pass"; \ done; \ echo "contract-check: schemas"; \ $(TOOLS_BIN)/schemagen -check \ || fail "schemas/ differs from tools/schemagen output" "run make schemas and commit; never hand-edit schemas/"; \ echo "contract-check: registry and inventory"; \ snap=$$(mktemp -d "$${TMPDIR:-/tmp}/contract-check.XXXXXX"); \ restore() { for f in $(CONTRACT_REGEN); do cp "$$snap/$$(basename $$f)" "$$f"; done; rm -rf "$$snap"; }; \ for f in $(CONTRACT_REGEN); do cp "$$f" "$$snap/$$(basename $$f)"; done; \ trap restore EXIT; \ $(MAKE) --no-print-directory cmd-inventory >/dev/null \ || fail "make cmd-inventory failed (the registry did not build)" "read the error above; a command missing from internal/canon/canon.yaml is the usual cause"; \ for f in $(CONTRACT_REGEN); do \ cmp -s "$$snap/$$(basename $$f)" "$$f" || { \ diff -u "$$snap/$$(basename $$f)" "$$f" | head -40 >&2; \ fail "$$f is not what the generator produces (hand edit or stale)" "run make cmd-inventory and commit; never hand-edit generated files"; }; \ done; \ restore; trap - EXIT; \ echo "contract-check: wiki contract pages"; \ $(TOOLS_BIN)/wikigen -check \ || fail "tools/wikigen -check found stale generated wiki pages" "run make wiki-commands and commit; never hand-edit a GENERATED block"; \ bash scripts/contract-check-wiki.sh \ || fail "a hand table in JSON-Output.md or Exit-Codes.md disagrees with its source" "edit the row the message names (scripts/contract-check-wiki.sh lists each check)"; \ echo "contract-check: compat markers"; \ bash scripts/ci/compat-markers.sh --check \ || fail "compat-markers.sh --check failed" "run bash scripts/ci/compat-markers.sh --write-wiki and commit"; \ for mode in unset 1; do \ if [ "$$mode" = 1 ]; then NSELF_V15=1; export NSELF_V15; else unset NSELF_V15; fi; \ echo "contract-check: go tests (NSELF_V15=$$mode)"; \ $(CONTRACT_GOTEST) $(CONTRACT_PKGS) \ || fail "contract packages failed (NSELF_V15=$$mode)" "fix the code or regenerate the contract it names"; \ $(CONTRACT_GOTEST) ./internal/cmdregistry/... -run Golden \ || fail "registry golden failed (NSELF_V15=$$mode)" "run make cmd-inventory and review the diff"; \ $(CONTRACT_GOTEST) ./internal/repoqa/... \ || fail "repoqa drift tests failed (NSELF_V15=$$mode)" "regenerate the file the test names"; \ $(CONTRACT_GOTEST) ./cmd/nself/... \ || fail "cmd/nself exit-code tests failed (NSELF_V15=$$mode)" "fix errs.ExitCodeFor wiring"; \ $(CONTRACT_GOTEST) ./cmd/commands/ -run 'Registry|Canon|JSONContract|Invocation|Help|Pilot|Surface' \ || fail "command contract tests failed (NSELF_V15=$$mode)" "fix the command or regenerate the contract it names"; \ done; \ unset NSELF_V15; \ echo "contract-check: OK" ## Q04 — SBOM generation (local dev target) ## Requires: syft (https://github.com/anchore/syft) ## Generates sbom.spdx.json (SPDX) and sbom.cdx.json (CycloneDX 1.5) from the source tree. ## Run `make sbom` before a release to verify SBOM generation works locally. sbom: @echo "Checking for syft..." @if ! command -v syft >/dev/null 2>&1; then \ echo "syft not found. Install via:"; \ echo " curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin"; \ exit 1; \ fi @echo "Generating SPDX SBOM..." @syft packages . --output spdx-json=sbom.spdx.json @echo "Generating CycloneDX 1.5 SBOM (Q04)..." @syft packages . --output cyclonedx-json=sbom.cdx.json @echo "" @echo "SBOMs written:" @echo " sbom.spdx.json (SPDX 2.3) $$(wc -c < sbom.spdx.json) bytes" @echo " sbom.cdx.json (CycloneDX 1.5) $$(wc -c < sbom.cdx.json) bytes" @echo "" @echo "Verify a release SBOM signature:" @echo " bash tools/sbom/verify.sh v$(VERSION)" @echo "" @echo "Query SBOM for a package:" @echo " bash tools/sbom/query.sh --local sbom.cdx.json --pkg cobra" ## man — generate man pages for all nself commands into ./man/ man: build @mkdir -p man @./$(BINARY) man --output man @echo "Man pages written to man/" build: CGO_ENABLED=0 go build $(BUILDFLAGS) -mod=vendor -ldflags="$(LDFLAGS)" -o $(BINARY) ./cmd/nself/ install: build cp $(BINARY) /usr/local/bin/$(BINARY) clean: rm -f $(BINARY) rm -rf $(DIST_DIR) test: CGO_ENABLED=0 go test -mod=vendor ./... vet: CGO_ENABLED=0 go vet -mod=vendor ./... ## fmt — format all first-party Go source (cmd/ internal/ tools/). fmt: gofmt -w cmd internal tools ## fmt-check — CLI-R01 gate. Same script CI runs; fails on any unformatted file. fmt-check: @bash scripts/ci/gofmt-check.sh cross: cross-linux cross-darwin cross-linux: CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build $(BUILDFLAGS) -mod=vendor -ldflags="$(LDFLAGS)" -o $(BINARY)-linux-amd64 ./cmd/nself/ CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build $(BUILDFLAGS) -mod=vendor -ldflags="$(LDFLAGS)" -o $(BINARY)-linux-arm64 ./cmd/nself/ cross-darwin: CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build $(BUILDFLAGS) -mod=vendor -ldflags="$(LDFLAGS)" -o $(BINARY)-darwin-amd64 ./cmd/nself/ CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build $(BUILDFLAGS) -mod=vendor -ldflags="$(LDFLAGS)" -o $(BINARY)-darwin-arm64 ./cmd/nself/ dist: @mkdir -p $(DIST_DIR) @for platform in linux/amd64 linux/arm64 darwin/amd64 darwin/arm64; do \ os=$$(echo $$platform | cut -d/ -f1); \ arch=$$(echo $$platform | cut -d/ -f2); \ name=$(BINARY)-$(VERSION)-$$os-$$arch; \ echo "Building $$os/$$arch..."; \ mkdir -p $(DIST_DIR)/$$name; \ CGO_ENABLED=0 GOOS=$$os GOARCH=$$arch go build $(BUILDFLAGS) -mod=vendor -ldflags="$(LDFLAGS)" -o $(DIST_DIR)/$$name/$(BINARY) ./cmd/nself/; \ cp README.md LICENSE $(DIST_DIR)/$$name/; \ tar -czf $(DIST_DIR)/$$name.tar.gz -C $(DIST_DIR) $$name; \ rm -rf $(DIST_DIR)/$$name; \ done @for arch in amd64 arm64; do \ name=$(BINARY)-$(VERSION)-windows-$$arch; \ echo "Building windows/$$arch..."; \ mkdir -p $(DIST_DIR)/$$name; \ CGO_ENABLED=0 GOOS=windows GOARCH=$$arch go build $(BUILDFLAGS) -mod=vendor -ldflags="$(LDFLAGS)" -o $(DIST_DIR)/$$name/$(BINARY).exe ./cmd/nself/; \ cp README.md LICENSE $(DIST_DIR)/$$name/; \ cd $(DIST_DIR) && zip -qr $$name.zip $$name && cd ..; \ rm -rf $(DIST_DIR)/$$name; \ done @cd $(DIST_DIR) && if command -v sha256sum >/dev/null 2>&1; then \ sha256sum *.tar.gz *.zip > checksums.txt; \ else \ shasum -a 256 *.tar.gz *.zip > checksums.txt; \ fi @echo "dist/ ready."