cli: consolidate and improve cli

This commit is contained in:
Daylin Morgan 2024-07-15 16:24:46 -05:00
parent 6baa412e09
commit e7ec17916b
Signed by: daylin
GPG key ID: 950D13E9719334AD
16 changed files with 317 additions and 119 deletions

View file

@ -52,15 +52,14 @@ Usage:
oizys [command]
Available Commands:
boot nixos rebuild boot
build nix build
cache build and push to cachix
checks nix build checks
ci offload build to GHA
dry poor man's nix flake check
help Help about any command
os nixos-rebuild wrapper
output show nixosConfiguration attr
switch nixos rebuild switch
update update and run nixos rebuild
Flags:
@ -84,3 +83,4 @@ Oizys was birthed by the goddess Nyx/Nix and embodies suffering and misery. Whic
> I don't use home-manager to manager my shell/user configs. You can find those in my separate `chezmoi`-managed [`dotfiles`](https://git.dayl.in/daylin/dotfiles) repository.

View file

@ -1,19 +0,0 @@
package cmd
import (
"oizys/internal/oizys"
"github.com/spf13/cobra"
)
var bootCmd = &cobra.Command{
Use: "boot",
Short: "nixos rebuild boot",
Run: func(cmd *cobra.Command, args []string) {
oizys.NixosRebuild("boot", args...)
},
}
func init() {
rootCmd.AddCommand(bootCmd)
}

View file

@ -10,12 +10,11 @@ var buildCmd = &cobra.Command{
Use: "build",
Short: "nix build",
Run: func(cmd *cobra.Command, args []string) {
oizys.NixBuild(nom, minimal, args...)
oizys.NixBuild(minimal, args...)
},
}
func init() {
rootCmd.AddCommand(buildCmd)
buildCmd.Flags().BoolVar(&nom, "nom", false, "display result with nom")
buildCmd.Flags().BoolVar(&minimal, "minimal", false, "use system dry-run to make build args")
}

View file

@ -10,11 +10,10 @@ var checksCmd = &cobra.Command{
Use: "checks",
Short: "nix build checks",
Run: func(cmd *cobra.Command, args []string) {
oizys.Checks(nom, args...)
oizys.Checks(args...)
},
}
func init() {
rootCmd.AddCommand(checksCmd)
checksCmd.Flags().BoolVar(&nom, "nom", false, "display result with nom")
}

38
pkgs/oizys/cmd/os.go Normal file
View file

@ -0,0 +1,38 @@
package cmd
import (
"fmt"
"oizys/internal/oizys"
"slices"
"strings"
"github.com/spf13/cobra"
)
var validArgs = []string{
"switch", "boot", "test", "build", "dry-build", "dry-activate", "edit", "repl",
"build-vm", "build-vm-with-bootloader",
"list-generations",
}
var osCmd = &cobra.Command{
Use: "os [subcmd]",
Short: "nixos-rebuild wrapper",
Args: func(cmd *cobra.Command, args []string) error {
if err := cobra.MinimumNArgs(1)(cmd, args); err != nil {
return err
}
// Run the custom validation logic
if slices.Contains(validArgs, args[0]) {
return nil
}
return fmt.Errorf("unexpected arg: %s\nexpected one of:\n %s", args[0], strings.Join(validArgs, ", "))
},
Run: func(cmd *cobra.Command, args []string) {
subcmd := args[0]
oizys.NixosRebuild(subcmd, args[1:]...)
},
}
func init() {
rootCmd.AddCommand(osCmd)
}

View file

@ -33,7 +33,6 @@ var (
host string
debug bool
verbose bool
nom bool
systemPath bool
resetCache bool
minimal bool

View file

@ -1,19 +0,0 @@
package cmd
import (
"oizys/internal/oizys"
"github.com/spf13/cobra"
)
var switchCmd = &cobra.Command{
Use: "switch",
Short: "nixos rebuild switch",
Run: func(cmd *cobra.Command, args []string) {
oizys.NixosRebuild("switch", args...)
},
}
func init() {
rootCmd.AddCommand(switchCmd)
}

View file

@ -1,8 +1,13 @@
package cmd
import (
"fmt"
"oizys/internal/github"
"oizys/internal/oizys"
"oizys/internal/ui"
"os"
"github.com/charmbracelet/log"
"github.com/spf13/cobra"
)
@ -10,14 +15,25 @@ var updateCmd = &cobra.Command{
Use: "update",
Short: "update and run nixos rebuild",
Run: func(cmd *cobra.Command, args []string) {
oizys.GitPull()
oizys.NixosRebuild("switch", args...)
run := github.GetLastUpdateRun()
if preview {
md, err := github.GetUpateSummary(run.GetID())
if err != nil {
log.Fatal(err)
}
fmt.Println(md)
if !ui.Confirm("proceed with system update?") {
os.Exit(0)
}
}
oizys.UpdateRepo()
oizys.NixosRebuild("switch")
},
}
var boot bool
var preview bool
func init() {
rootCmd.AddCommand(updateCmd)
updateCmd.Flags().BoolVarP(&boot, "boot", "b", false, "run nixos-rebuild boot")
updateCmd.Flags().BoolVar(&preview, "preview", false, "confirm nix store diff")
}

View file

@ -3,19 +3,17 @@
installShellFiles,
buildGoModule,
makeWrapper,
gh,
nix-output-monitor,
...
}:
let
inherit (lib) cleanSource makeBinPath;
inherit (lib) cleanSource;
in
buildGoModule {
pname = "oizys";
version = "unstable";
src = cleanSource ./.;
vendorHash = "sha256-/JVXhXrU2np/ty7AGFy+LPZCo1NaLYl9NAyD9+FJYBI=";
vendorHash = "sha256-+4OtpcKHfomBAXRrJOvkhQdCSwU0W6+5OJuS4o12r5E=";
nativeBuildInputs = [
installShellFiles
@ -27,13 +25,4 @@ buildGoModule {
--zsh <(OIZYS_SKIP_CHECK=true $out/bin/oizys completion zsh)
'';
postFixup = ''
wrapProgram $out/bin/oizys \
--prefix PATH ':' ${
makeBinPath [
gh
nix-output-monitor
]
}
'';
}

View file

@ -6,6 +6,7 @@ require (
github.com/briandowns/spinner v1.23.0
github.com/charmbracelet/lipgloss v0.11.0
github.com/charmbracelet/log v0.4.0
github.com/google/go-github/v63 v63.0.0
github.com/ivanpirog/coloredcobra v1.0.1
github.com/spf13/cobra v1.8.0
golang.org/x/term v0.21.0
@ -16,6 +17,7 @@ require (
github.com/charmbracelet/x/ansi v0.1.2 // indirect
github.com/fatih/color v1.17.0 // indirect
github.com/go-logfmt/logfmt v0.6.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect

View file

@ -17,6 +17,13 @@ github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-github/v63 v63.0.0 h1:13xwK/wk9alSokujB9lJkuzdmQuVn2QCPeck76wR3nE=
github.com/google/go-github/v63 v63.0.0/go.mod h1:IqbcrgUmIcEaioWrGYei/09o+ge5vhffGOcxrO0AfmA=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
@ -60,6 +67,7 @@ golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA=
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

View file

@ -1,18 +1,24 @@
package oizys
package git
import (
"fmt"
"oizys/internal/ui"
"os"
"os/exec"
"github.com/charmbracelet/log"
"oizys/internal/ui"
)
type GitRepo struct {
path string
}
func NewRepo(path string) *GitRepo {
repo := new(GitRepo)
repo.path = path
return repo
}
func (g *GitRepo) git(rest ...string) *exec.Cmd {
args := []string{"-C", g.path}
args = append(args, rest...)
@ -21,8 +27,22 @@ func (g *GitRepo) git(rest ...string) *exec.Cmd {
return cmd
}
func GitPull(workDir string) {
g := GitRepo{workDir}
func (g *GitRepo) Fetch() {
err := g.git("fetch").Run()
if err != nil {
log.Fatal(err)
}
}
func (g *GitRepo) Rebase(ref string) {
g.Status()
err := g.git("rebase", ref).Run()
if err != nil {
log.Fatal(err)
}
}
func (g *GitRepo) Status() {
cmdOutput, err := g.git("status", "--porcelain").Output()
if err != nil {
log.Fatal(err)
@ -33,8 +53,11 @@ func GitPull(workDir string) {
ui.ShowFailedOutput(cmdOutput)
os.Exit(1)
}
}
cmdOutput, err = g.git("pull").CombinedOutput()
func (g *GitRepo) Pull() {
g.Status()
cmdOutput, err := g.git("pull").CombinedOutput()
if err != nil {
ui.ShowFailedOutput(cmdOutput)
log.Fatal(err)

View file

@ -0,0 +1,154 @@
package github
import (
"archive/zip"
"bytes"
"context"
"fmt"
"io"
"net/http"
"net/url"
"oizys/internal/oizys"
"github.com/charmbracelet/log"
"github.com/google/go-github/v63/github"
)
var client *github.Client
func init() {
client = github.NewClient(nil).WithAuthToken(oizys.GithubToken())
}
func ListWorkflows() {
workflowRuns, resp, err := client.Actions.ListWorkflowRunsByFileName(
context.Background(),
"daylinmorgan",
"oizys",
"update.yml",
nil,
)
if err != nil {
log.Fatal("Failed to get a list of workflows", "err", err, "resp", resp)
}
for _, w := range workflowRuns.WorkflowRuns {
fmt.Println(w.GetID())
fmt.Println(w.GetConclusion())
}
}
func ListUpdateRuns() (*github.WorkflowRuns, *github.Response) {
workflowRuns, resp, err := client.Actions.ListWorkflowRunsByFileName(
context.Background(),
"daylinmorgan",
"oizys",
"update.yml",
nil,
)
if err != nil {
log.Fatal("failed to get last update run", "resp", resp, "err", err)
}
return workflowRuns, resp
}
func GetArtifacts(runID int64) (*github.ArtifactList, *github.Response) {
artifactList, resp, err := client.Actions.ListWorkflowRunArtifacts(context.Background(), "daylinmorgan", "oizys", runID, nil)
if err != nil {
log.Fatal("failed to get artifacts for run", "id", runID, "err", err)
}
return artifactList, resp
}
func GetUpdateSummaryArtifact(runID int64) *github.Artifact {
artifactList, _ := GetArtifacts(runID)
for _, artifact := range artifactList.Artifacts {
if artifact.GetName() == "summary" {
return artifact
}
}
log.Fatal("failed to find summary for run", "id", runID)
return nil
}
func GetUpdateSummaryUrl(runID int64) *url.URL {
artifact := GetUpdateSummaryArtifact(runID)
url, resp, err := client.Actions.DownloadArtifact(context.Background(), "daylinmorgan", "oizys", artifact.GetID(), 4)
if err != nil {
log.Fatal("failed to get update summary URL", "artifact", artifact.GetID(), "resp", resp)
}
return url
}
func GetUpdateSummaryFromUrl(url *url.URL) []byte {
log.Debug(url.String())
res, err := http.Get(url.String())
if err != nil {
log.Fatal("failed to get update summary zip", "err", err)
}
body, err := io.ReadAll(res.Body)
res.Body.Close()
if res.StatusCode > 299 {
log.Fatalf("Response failed with status code: %d and\nbody: %s\n", res.StatusCode, body)
}
if err != nil {
log.Fatal(err)
}
return body
}
func GetLastUpdateRun() *github.WorkflowRun {
workflowRuns, _ := ListUpdateRuns()
run := workflowRuns.WorkflowRuns[0]
if run.GetConclusion() == "failure" {
log.Fatal("Most recent run was not successful", "runId", run.GetID(), "conclusion", run.GetConclusion())
}
if run.GetStatus() == "in_progress" {
log.Fatalf("Most recent run is not finished\nview workflow run at: %s", run.GetHTMLURL())
}
return run
}
func GetUpateSummary(runID int64) (string, error) {
url := GetUpdateSummaryUrl(runID)
bytes := GetUpdateSummaryFromUrl(url)
md, err := ReadMarkdownFromZip(bytes, "summary.md")
return md, err
}
func ReadMarkdownFromZip(zipData []byte, fileName string) (string, error) {
// Open the zip reader from the in-memory byte slice
reader, err := zip.NewReader(bytes.NewReader(zipData), int64(len(zipData)))
if err != nil {
return "", err
}
// Search for the target file
var markdownFile *zip.File
for _, f := range reader.File {
if f.Name == fileName {
markdownFile = f
break
}
}
if markdownFile == nil {
return "", fmt.Errorf("file %s not found in zip archive", fileName)
}
// Open the markdown file reader
fileReader, err := markdownFile.Open()
if err != nil {
return "", err
}
defer fileReader.Close()
// Read the markdown content
content, err := io.ReadAll(fileReader)
if err != nil {
return "", err
}
// Return the markdown content as string
return string(content), nil
}

View file

@ -5,6 +5,8 @@ import (
"errors"
"fmt"
"io/fs"
"oizys/internal/git"
"oizys/internal/ui"
"os"
"os/exec"
"strings"
@ -13,7 +15,6 @@ import (
"github.com/charmbracelet/log"
e "oizys/internal/exec"
"oizys/internal/ui"
)
var o *Oizys
@ -24,10 +25,13 @@ func init() {
// verbose vs debug?
type Oizys struct {
repo *git.GitRepo
flake string
host string
cache string
githubSummary string
githubToken string
local bool
inCI bool
verbose bool
systemPath bool
@ -52,21 +56,29 @@ func New() *Oizys {
}
o.githubSummary = os.Getenv("GITHUB_STEP_SUMMARY")
o.inCI = o.githubSummary != ""
o.githubToken = os.Getenv("GITHUB_TOKEN")
o.repo = git.NewRepo(o.flake)
return o
}
func GithubToken() string {
return o.githubToken
}
func SetFlake(path string) {
// Check path exists
if path != "" {
o.flake = path
}
// check local path exists
// check if path is local and exists
if !strings.HasPrefix(o.flake, "github") && !strings.HasPrefix(o.flake, "git+") {
if _, ok := os.LookupEnv("OIZYS_SKIP_CHECK"); !ok {
if _, err := os.Stat(o.flake); errors.Is(err, fs.ErrNotExist) {
log.Warnf("path to flake %s does not exist, using remote as fallback", o.flake)
o.flake = "github:daylinmorgan/oizys"
} else {
o.local = true
}
}
}
@ -127,33 +139,6 @@ func Output() {
}
}
func git(rest ...string) *exec.Cmd {
args := []string{"-C", o.flake}
args = append(args, rest...)
cmd := exec.Command("git", args...)
e.LogCmd(cmd)
return cmd
}
func GitPull() {
cmdOutput, err := git("status", "--porcelain").Output()
if err != nil {
log.Fatal(err)
}
if len(cmdOutput) > 0 {
fmt.Println("unstaged commits, cowardly exiting...")
ui.ShowFailedOutput(cmdOutput)
os.Exit(1)
}
cmdOutput, err = git("pull").CombinedOutput()
if err != nil {
ui.ShowFailedOutput(cmdOutput)
log.Fatal(err)
}
}
func parseDryRun(buf string) (*ui.Packages, *ui.Packages) {
lines := strings.Split(strings.TrimSpace(buf), "\n")
var parts [2][]string
@ -260,14 +245,8 @@ func NixosRebuild(subcmd string, rest ...string) {
e.ExitWithCommand(cmd)
}
func NixBuild(nom bool, minimal bool, rest ...string) {
var cmdName string
if nom {
cmdName = "nom"
} else {
cmdName = "nix"
}
cmd := exec.Command(cmdName, "build")
func NixBuild(minimal bool, rest ...string) {
cmd := exec.Command("nix", "build")
if o.resetCache {
cmd.Args = append(cmd.Args, "--narinfo-cache-negative-ttl", "0")
}
@ -318,10 +297,10 @@ func (o *Oizys) checkPath(name string) string {
return fmt.Sprintf("%s#checks.x86_64-linux.%s", o.flake, name)
}
func Checks(nom bool, rest ...string) {
func Checks(rest ...string) {
checks := o.getChecks()
for _, check := range checks {
NixBuild(nom, false, o.checkPath(check))
NixBuild(false, o.checkPath(check))
}
}
@ -338,8 +317,17 @@ func CacheBuild(rest ...string) {
}
func CI(rest ...string) {
args := []string{"workflow", "run", "build.yml", "-F", fmt.Sprintf("hosts=%s", o.host)}
args := []string{
"workflow", "run", "build.yml",
"-F", fmt.Sprintf("hosts=%s", o.host),
}
args = append(args, rest...)
cmd := exec.Command("gh", args...)
e.ExitWithCommand(cmd)
}
func UpdateRepo() {
log.Info("rebasing HEAD on origin/flake-lock")
o.repo.Fetch()
o.repo.Rebase("origin/flake-lock")
}

View file

@ -7,8 +7,9 @@ import (
"os/exec"
"strings"
"github.com/charmbracelet/log"
e "oizys/internal/exec"
"github.com/charmbracelet/log"
)
var ignoredMap = stringSliceToMap(
@ -16,16 +17,12 @@ var ignoredMap = stringSliceToMap(
// nix
"ld-library-path", "builder.pl", "profile", "system-path",
// nixos
"nixos-install",
"nixos-version",
"nixos-manual-html",
"nixos-configuration-reference-manpage",
"nixos-rebuild",
"nixos-help",
"nixos-generate-config",
"nixos-enter",
"nixos-container",
"nixos-build-vms",
"nixos-install", "nixos-version",
"nixos-manual-html", "nixos-rebuild",
"nixos-configuration-reference-manpage",
"nixos-generate-config", "nixos-enter",
"nixos-container", "nixos-build-vms",
"nixos-wsl-version", "nixos-wsl-welcome-message", "nixos-wsl-welcome",
// trivial packages
"restic-gdrive", "gitea", "lock", "code",

View file

@ -1,6 +1,7 @@
package ui
import (
"bufio"
"fmt"
"os"
"sort"
@ -101,3 +102,26 @@ func (p *Packages) summary() {
Render(fmt.Sprint(len(p.names))),
)
}
// Confirm asks the user for confirmation.
// valid inputs are: y/yes,n/no case insensitive.
func Confirm(s string) bool {
reader := bufio.NewReader(os.Stdin)
for {
fmt.Printf("%s [y/n]: ", s)
response, err := reader.ReadString('\n')
if err != nil {
log.Fatal(err)
}
response = strings.ToLower(strings.TrimSpace(response))
switch response {
case "y", "yes":
return true
case "n", "no":
return false
}
}
}