Phase 1: local kernel catalog scaffolding
Introduces a read/write kernel catalog on disk without any network dependency, so later phases (image register --kernel-ref, import, pull) can build on a working foundation. Layout: adds KernelsDir to paths.Layout, ensured under ~/.local/state/banger/kernels/. Each cataloged kernel lives at <KernelsDir>/<name>/ with a manifest.json alongside vmlinux and optional initrd.img / modules/. New internal/kernelcat package owns the disk format: - Entry (Name, Distro, Arch, KernelVersion, SHA256, Source, ImportedAt) - ValidateName (alphanumeric + dots/hyphens/underscores, no traversal) - ReadLocal / ListLocal / WriteLocal / DeleteLocal - SumFile helper The daemon exposes three RPC methods dispatched in daemon.go: kernel.list, kernel.show, kernel.delete. Implementations live in a new internal/daemon/kernels.go and are thin wrappers over kernelcat using d.layout.KernelsDir. CLI: new top-level `banger kernel` with list / show / rm subcommands mirroring the image-command pattern (ensureDaemon, RPC call, table or JSON output). No sudo required — kernel ops are user-space only. Users can now manually populate ~/.local/state/banger/kernels/<name>/ and see it via `banger kernel list`. Phase 2 wires --kernel-ref into image register; Phase 3 adds `banger kernel import`; Phase 4 adds remote pulls. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
ca4865447c
commit
83cc3aee15
9 changed files with 691 additions and 3 deletions
|
|
@ -163,7 +163,7 @@ func NewBangerCommand() *cobra.Command {
|
|||
RunE: helpNoArgs,
|
||||
}
|
||||
root.CompletionOptions.DisableDefaultCmd = true
|
||||
root.AddCommand(newDaemonCommand(), newDoctorCommand(), newImageCommand(), newInternalCommand(), newVersionCommand(), newPSCommand(), newVMCommand())
|
||||
root.AddCommand(newDaemonCommand(), newDoctorCommand(), newImageCommand(), newInternalCommand(), newKernelCommand(), newVersionCommand(), newPSCommand(), newVMCommand())
|
||||
return root
|
||||
}
|
||||
|
||||
|
|
@ -1585,6 +1585,106 @@ func newImageDeleteCommand() *cobra.Command {
|
|||
}
|
||||
}
|
||||
|
||||
func newKernelCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "kernel",
|
||||
Short: "Manage the local kernel catalog",
|
||||
RunE: helpNoArgs,
|
||||
}
|
||||
cmd.AddCommand(
|
||||
newKernelListCommand(),
|
||||
newKernelShowCommand(),
|
||||
newKernelRmCommand(),
|
||||
)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func newKernelListCommand() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List locally available kernels",
|
||||
Args: noArgsUsage("usage: banger kernel list"),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
layout, _, err := ensureDaemon(cmd.Context())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
result, err := rpc.Call[api.KernelListResult](cmd.Context(), layout.SocketPath, "kernel.list", api.Empty{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return printKernelListTable(cmd.OutOrStdout(), result.Entries)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func newKernelShowCommand() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "show <name>",
|
||||
Short: "Show kernel catalog entry details",
|
||||
Args: exactArgsUsage(1, "usage: banger kernel show <name>"),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
layout, _, err := ensureDaemon(cmd.Context())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
result, err := rpc.Call[api.KernelShowResult](cmd.Context(), layout.SocketPath, "kernel.show", api.KernelRefParams{Name: args[0]})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return printJSON(cmd.OutOrStdout(), result.Entry)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func newKernelRmCommand() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "rm <name>",
|
||||
Aliases: []string{"remove", "delete"},
|
||||
Short: "Remove a kernel catalog entry",
|
||||
Args: exactArgsUsage(1, "usage: banger kernel rm <name>"),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
layout, _, err := ensureDaemon(cmd.Context())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := rpc.Call[api.Empty](cmd.Context(), layout.SocketPath, "kernel.delete", api.KernelRefParams{Name: args[0]}); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = fmt.Fprintf(cmd.OutOrStdout(), "removed %s\n", args[0])
|
||||
return err
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func printKernelListTable(out anyWriter, entries []api.KernelEntry) error {
|
||||
w := tabwriter.NewWriter(out, 0, 8, 2, ' ', 0)
|
||||
if _, err := fmt.Fprintln(w, "NAME\tDISTRO\tARCH\tKERNEL\tIMPORTED"); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, entry := range entries {
|
||||
if _, err := fmt.Fprintf(
|
||||
w,
|
||||
"%s\t%s\t%s\t%s\t%s\n",
|
||||
entry.Name,
|
||||
dashIfEmpty(entry.Distro),
|
||||
dashIfEmpty(entry.Arch),
|
||||
dashIfEmpty(entry.KernelVersion),
|
||||
dashIfEmpty(entry.ImportedAt),
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return w.Flush()
|
||||
}
|
||||
|
||||
func dashIfEmpty(s string) string {
|
||||
if strings.TrimSpace(s) == "" {
|
||||
return "-"
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func helpNoArgs(cmd *cobra.Command, args []string) error {
|
||||
if len(args) != 0 {
|
||||
return fmt.Errorf("unknown arguments: %s", strings.Join(args, " "))
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ import (
|
|||
"banger/internal/model"
|
||||
"banger/internal/system"
|
||||
"banger/internal/toolingplan"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func TestNewBangerCommandHasExpectedSubcommands(t *testing.T) {
|
||||
|
|
@ -27,7 +29,7 @@ func TestNewBangerCommandHasExpectedSubcommands(t *testing.T) {
|
|||
for _, sub := range cmd.Commands() {
|
||||
names = append(names, sub.Name())
|
||||
}
|
||||
want := []string{"daemon", "doctor", "image", "internal", "ps", "version", "vm"}
|
||||
want := []string{"daemon", "doctor", "image", "internal", "kernel", "ps", "version", "vm"}
|
||||
if !reflect.DeepEqual(names, want) {
|
||||
t.Fatalf("subcommands = %v, want %v", names, want)
|
||||
}
|
||||
|
|
@ -57,6 +59,28 @@ func TestVersionCommandPrintsBuildInfo(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestKernelCommandExposesSubcommands(t *testing.T) {
|
||||
cmd := NewBangerCommand()
|
||||
var kernel *cobra.Command
|
||||
for _, sub := range cmd.Commands() {
|
||||
if sub.Name() == "kernel" {
|
||||
kernel = sub
|
||||
break
|
||||
}
|
||||
}
|
||||
if kernel == nil {
|
||||
t.Fatalf("kernel command missing from root")
|
||||
}
|
||||
names := []string{}
|
||||
for _, sub := range kernel.Commands() {
|
||||
names = append(names, sub.Name())
|
||||
}
|
||||
want := []string{"list", "rm", "show"}
|
||||
if !reflect.DeepEqual(names, want) {
|
||||
t.Fatalf("kernel subcommands = %v, want %v", names, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLegacyRemovedCommandIsRejected(t *testing.T) {
|
||||
cmd := NewBangerCommand()
|
||||
cmd.SetArgs([]string{"tui"})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue