2026-06-18 19:12:57 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"fmt"
|
|
|
|
|
"strings"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// version is stamped at build time via -ldflags "-X main.version=vX.Y.Z".
|
|
|
|
|
var version = "dev"
|
|
|
|
|
|
|
|
|
|
// buildRegistry returns every homelab verb. New verb-groups append here.
|
|
|
|
|
func buildRegistry() []Command {
|
|
|
|
|
var reg []Command
|
|
|
|
|
reg = append(reg, claimCommands()...)
|
homelab: add tf verbs + stack/git-crypt substrate
Adds the tf verb-group and the resolver substrate beneath it, continuing the
v0.1 infra-loop build.
- substrate: findInfraRoot (walk up to terragrunt.hcl + stacks/), stack→dir
resolver, and repo/remote/git-crypt detection (preferRemote forgejo>origin,
hasGitCryptAttr, gitCryptFlags) — the last is for `work` next.
- tf plan/validate/fmt/force-unlock/apply, resolving the stack from cwd and
delegating to scripts/tg (which owns state decrypt/encrypt, the Vault lock,
and the ingress auth-comment check) rather than calling terragrunt directly.
- tf apply is presence-coupled: claims stack:<name>, ALWAYS releases on exit
(normal, error, or SIGINT/SIGTERM via sync.Once + signal handler) — fixing
the documented ~200-claim leak — and prints an out-of-band reminder since CI
applies canonically on push.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 19:16:33 +00:00
|
|
|
reg = append(reg, tfCommands()...)
|
2026-06-18 19:24:08 +00:00
|
|
|
reg = append(reg, workCommands()...)
|
homelab: add k8s verb-group (v0.2) — the biggest remaining surface
Mining the post-v0.1 corpus showed kubectl is the dominant remaining domain by
far: 11,291 commands across 243 sessions (more than everything else combined).
This adds the full k8s verb-group built on an app→namespace→pod resolver (most
namespaces hold one app, so <app> defaults to the namespace and the target
defaults to deploy/<app>, letting kubectl resolve the pod; -n/--pod/-c/-l/--tty
override).
Read: status (pods + non-Normal events), get, logs, describe, debug (one-shot
triage), pf, rollout-status. Write/operational: db (the dbaas psql/mysql exec
pattern — PG via pg-cluster-rw -c postgres, MySQL via mysql-standalone-0 with the
env-password bash wrapper, never inline), exec, rm-pod (pods/jobs ONLY), restart.
Config-mutation verbs (apply/edit/patch/scale/create) are deliberately NOT
exposed — they stay raw per the Terraform-only policy.
Smoke-verified read verbs against the live cluster (get/logs/rollout-status);
write verbs are unit-tested (resolver, db-plan, shell-quoting) but not fired at
live state.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 22:29:51 +00:00
|
|
|
reg = append(reg, k8sCommands()...)
|
homelab: add memory verb-group (v0.3.0) — direct claude-memory HTTP client
Lets agents search/navigate memory via the CLI, as the first step toward
deprecating the memory MCP. claude-memory is a FastAPI service (the MCP is just
one frontend); homelab memory is a thin Bearer-auth HTTP client over the same
API, using the env the hooks already set (CLAUDE_MEMORY_API_URL/KEY). It works
even when the MCP frontend is down — the recurring disconnect that took the MCP
offline for this whole session.
Verbs: recall (server-side semantic search), list, categories, tags, stats,
secret (read); store, update, delete (write). Validated against the live API
including a store→recall→delete round-trip — full data-plane parity with the MCP.
The deprecation itself (rewiring the per-prompt auto-recall + auto-learn hooks to
the CLI, then uninstalling the MCP) is a deliberate follow-up, sequenced after
the CLI is proven in the hooks — see docs/adr/0008.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 05:56:25 +00:00
|
|
|
reg = append(reg, memoryCommands()...)
|
2026-06-19 10:59:14 +00:00
|
|
|
reg = append(reg, ciCommands()...)
|
|
|
|
|
reg = append(reg, deployCommands()...)
|
2026-06-19 11:27:31 +00:00
|
|
|
reg = append(reg, netCommands()...)
|
|
|
|
|
reg = append(reg, obsCommands()...)
|
homelab: v0.6.0 — usage telemetry (usage top), evidence-driven verb prioritization
Answers the question that drove the whole CLI — which verbs to add next — with
data instead of one maintainer's habits, and resolves the cross-user-usage ask
in-bounds (no reading anyone's home).
- emit on dispatch: every verb fire-and-forgets one Loki line {job,user,verb} +
"exit=N ver=X". ONLY the verb path + exit code — never args, paths, flags, or
secrets (the emit never sees arguments). Best-effort: 800ms timeout, errors
swallowed, never affects the command; opt-out HOMELAB_TELEMETRY=0. Discovery
verbs (manifest/version/help) and usage itself don't self-record.
- usage top [--since 30d] [--user U] [--json]: ranks verbs via
sum by (verb)(count_over_time({job="homelab-usage"}[…])) against the shared
Loki. Cross-user analytics WITHOUT touching ~/.claude — the privacy-preserving
answer to "what does the team use".
- Loki sink (zero new infra, dogfoods v0.5 logs path); push verified HTTP 204 no
auth. ADR docs/adr/0011.
Live-verified: ran 4 verbs, usage top ranked them correctly (metrics query=2).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 22:29:01 +00:00
|
|
|
reg = append(reg, usageCommands()...)
|
2026-06-18 19:12:57 +00:00
|
|
|
return reg
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// dispatchTop handles the homelab verb surface. handled=false means the args are
|
|
|
|
|
// not a homelab verb, so main() falls back to the legacy -use-case path.
|
|
|
|
|
func dispatchTop(args []string) (handled bool, err error) {
|
|
|
|
|
if len(args) == 0 {
|
|
|
|
|
fmt.Print(usage())
|
|
|
|
|
return true, nil
|
|
|
|
|
}
|
|
|
|
|
switch args[0] {
|
|
|
|
|
case "help", "-h", "--help":
|
|
|
|
|
fmt.Print(usage())
|
|
|
|
|
return true, nil
|
|
|
|
|
case "version", "--version":
|
|
|
|
|
fmt.Println("homelab " + version)
|
|
|
|
|
return true, nil
|
|
|
|
|
case "manifest":
|
|
|
|
|
reg := buildRegistry()
|
|
|
|
|
if containsArg(args[1:], "--json") {
|
|
|
|
|
out, err := manifestJSON(reg)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return true, err
|
|
|
|
|
}
|
|
|
|
|
fmt.Println(out)
|
|
|
|
|
return true, nil
|
|
|
|
|
}
|
|
|
|
|
fmt.Print(manifestText(reg))
|
|
|
|
|
return true, nil
|
|
|
|
|
}
|
|
|
|
|
if strings.HasPrefix(args[0], "-") {
|
|
|
|
|
return false, nil
|
|
|
|
|
}
|
|
|
|
|
reg := buildRegistry()
|
|
|
|
|
if !isCommandGroup(reg, args[0]) {
|
|
|
|
|
return false, nil
|
|
|
|
|
}
|
|
|
|
|
return true, dispatch(reg, args)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func isCommandGroup(reg []Command, group string) bool {
|
|
|
|
|
for _, c := range reg {
|
|
|
|
|
if len(c.Path) > 0 && c.Path[0] == group {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func containsArg(args []string, want string) bool {
|
|
|
|
|
for _, a := range args {
|
|
|
|
|
if a == want {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func usage() string {
|
|
|
|
|
var b strings.Builder
|
|
|
|
|
fmt.Fprintf(&b, "homelab %s — unified homelab operations CLI\n\n", version)
|
|
|
|
|
b.WriteString("Usage:\n homelab <command> [args]\n\nCommands:\n")
|
|
|
|
|
for _, line := range strings.Split(strings.TrimRight(manifestText(buildRegistry()), "\n"), "\n") {
|
|
|
|
|
if line != "" {
|
|
|
|
|
b.WriteString(" " + line + "\n")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
b.WriteString("\n manifest [--json] list all commands (machine-readable with --json)\n")
|
|
|
|
|
b.WriteString(" version print version\n")
|
|
|
|
|
b.WriteString("\nLegacy webhook use-cases remain available via -use-case=<name>.\n")
|
|
|
|
|
return b.String()
|
|
|
|
|
}
|