Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,24 @@ notes and errors go to stderr.
vabc --json inventory check 010807 --near 22182 | jq '.store.quantity, .nearbyStores[].quantity'
```

`vabc` conforms to the [Agent CLI Guidelines](https://aclig.dev/) **v0.4.0** at the **Full** level.
The conformance block is machine-verifiable from the binary itself — `vabc schema --json` emits a
top-level `conformance` key — so an agent can confirm the contract version without trusting this badge:

```json
{
"conformance": {
"spec": "agent-cli-guidelines",
"version": "0.4.0",
"level": "Full"
}
}
```

```bash
vabc schema --json | jq '.conformance'
```

## How it works

Everything is a live read against Virginia ABC's public, unauthenticated endpoints — product
Expand Down
33 changes: 29 additions & 4 deletions internal/cli/misc.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,11 @@ func (c *SchemaCmd) Run(rt *Runtime) error {
return errs.New(errs.ExitGeneric, "SCHEMA_ERROR", err.Error(), "")
}
out := map[string]any{
"tool": "vabc",
"version": version.String(),
"tool": "vabc",
"version": version.String(),
"conformance": map[string]any{
"spec": "agent-cli-guidelines", "version": version.Spec, "level": "Full",
},
"commands": nodeToMap(k.Model.Node),
"exit_codes": errs.Table(),
"safety": map[string]any{
Expand Down Expand Up @@ -159,8 +162,30 @@ func (c *AgentCmd) Run(rt *Runtime) error {

// --- version ----------------------------------------------------------------

type VersionCmd struct{}
// VersionCmd prints the version, or with --check asks (pull-based, fail-silent) whether a
// newer release exists. Update awareness, never self-mutation (contract §11). The tool never
// auto-updates; it only reports the upgrade command for the human/package manager.

type VersionCmd struct {
Check bool `help:"Check for a newer release (network, short timeout, fail-silent)."`
}

func (c *VersionCmd) Run(rt *Runtime) error {
return rt.Out.Emit(map[string]any{"version": version.String()})
cur := version.String()
if !c.Check {
return rt.Out.Emit(map[string]any{"version": cur})
}
out := map[string]any{
"current": cur,
"latest": nil,
"updateAvailable": false,
"upgrade": version.UpgradeHint(),
}
if latest, err := version.Latest(rt.Ctx); err == nil && latest != "" {
out["latest"] = latest
out["updateAvailable"] = version.UpdateAvailable(latest, cur)
} else {
out["note"] = "could not check for updates"
}
return rt.Out.Emit(out)
}
11 changes: 11 additions & 0 deletions internal/cli/testdata/schema.golden.json
Original file line number Diff line number Diff line change
Expand Up @@ -215,11 +215,22 @@
"name": "agent"
},
{
"flags": [
{
"help": "Check for a newer release (network, short timeout, fail-silent).",
"name": "check"
}
],
"help": "Print the version.",
"name": "version"
}
]
},
"conformance": {
"level": "Full",
"spec": "agent-cli-guidelines",
"version": "0.4.0"
},
"exit_codes": {
"auth_required": 4,
"cancelled": 130,
Expand Down
87 changes: 86 additions & 1 deletion internal/version/version.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
package version

import "runtime/debug"
import (
"context"
"encoding/json"
"fmt"
"net/http"
"os"
"runtime/debug"
"strings"
"time"
)

// Spec is the Agent CLI Guidelines version this tool conforms to (declared in `schema`).
const Spec = "0.4.0"

// version is a plain literal so -ldflags "-X .../version.version=vX" can override it.
// It MUST NOT be initialized from a function call (golang/go#64246).
Expand All @@ -24,3 +36,76 @@ func String() string {
}
return version
}

// repoSlug parses "owner/repo" from the module path (github.com/owner/repo), or "" if
// it can't be determined.
func repoSlug() string {
if bi, ok := debug.ReadBuildInfo(); ok {
if rest, found := strings.CutPrefix(bi.Main.Path, "github.com/"); found {
parts := strings.Split(rest, "/")
if len(parts) >= 2 {
return parts[0] + "/" + parts[1]
}
}
}
return ""
}

// UpgradeHint returns the recommended upgrade command for this tool. The main package lives at
// ./cmd/<tool> (the scaffold layout), so the install path includes /cmd/<tool> — a bare
// module@latest would fail for tools with no root main.
func UpgradeHint() string {
slug := repoSlug()
if slug == "" {
return ""
}
repo := slug[strings.LastIndex(slug, "/")+1:]
return "go install github.com/" + slug + "/cmd/" + repo + "@latest"
}

// Latest returns the latest released version tag (from GitHub Releases by default).
// Network, short timeout, **fail-silent**: returns ("", err) on any problem so a
// `version --check` never errors or blocks an agent loop. The release source can be
// overridden with VABC_RELEASES_URL (used in tests).
func Latest(ctx context.Context) (string, error) {
url := os.Getenv("VABC_RELEASES_URL")
if url == "" {
slug := repoSlug()
if slug == "" {
return "", fmt.Errorf("unknown repository")
}
url = "https://api.github.com/repos/" + slug + "/releases/latest"
}
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return "", err
}
req.Header.Set("Accept", "application/vnd.github+json")
req.Header.Set("User-Agent", "vabc-version-check") // GitHub's REST API rejects requests with no UA
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("release source: %s", resp.Status)
}
var body struct {
TagName string `json:"tag_name"`
}
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
return "", err
}
return body.TagName, nil
}

// UpdateAvailable reports whether latest is a different release than current.
// Dev/source builds (current == "dev") never report an update — don't nag them.
func UpdateAvailable(latest, current string) bool {
if latest == "" || current == "" || current == "dev" {
return false
}
return strings.TrimPrefix(latest, "v") != strings.TrimPrefix(current, "v")
}
2 changes: 2 additions & 0 deletions site/astro.config.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// @ts-check
import { defineConfig } from "astro/config";
import starlight from "@astrojs/starlight";
import starlightLlmsTxt from "starlight-llms-txt";

// Canonical site URL. Currently the live Vercel alias; cut over to https://vabc.sh
// once the domain is bought (update here + redeploy + re-alias + regenerate OG cards).
Expand All @@ -15,6 +16,7 @@ export default defineConfig({
description:
"Virginia ABC product search and store inventory from your terminal — agent-friendly, read-only.",
tagline: "Find the bottle. Skip the website.",
plugins: [starlightLlmsTxt()],
logo: { src: "./src/assets/mark.svg", replacesTitle: false },
customCss: ["./src/styles/tokens.css", "./src/styles/docs.css"],
social: { github: "https://github.com/rnwolfe/vabc" },
Expand Down
3 changes: 2 additions & 1 deletion site/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
"og": "node scripts/gen-og.mjs"
},
"dependencies": {
"@astrojs/starlight": "^0.30.0",
"@astrojs/starlight": "^0.31.0",
"starlight-llms-txt": "^0.4.0",
"@fontsource/bricolage-grotesque": "^5.2.10",
"@fontsource/ibm-plex-mono": "^5.2.7",
"@fontsource/ibm-plex-sans": "^5.2.8",
Expand Down
Loading
Loading