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
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,10 @@ A Nix flake is also provided (`nix build .#mise`).
`mise` reads a config file from `$XDG_CONFIG_HOME/mise/config.toml` - see
[the example config](./docs/EXAMPLE_CONFIG.toml) for an example. You can also
configure mise using env vars e.g. `MISE_TANDOOR_TOKEN` or with a global flag
e.g. `--tandoor.token`.
e.g. `--tandoor-url`.

Environment variables use `MISE_`, then the section and key in uppercase;
for example, `tandoor.token` becomes `MISE_TANDOOR_TOKEN`.

### Providers

Expand Down
5 changes: 3 additions & 2 deletions cmd/configure.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ import (
)

var configureCmd = &cli.Command{
Name: "configure",
Usage: "Provide configuration values for mise",
Name: "configure",
Usage: "Provide configuration values for mise",
Metadata: cliutil.GlobalFlagMetadata(cliutil.ProviderOptions, cliutil.TandoorOptions),
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "print",
Expand Down
7 changes: 4 additions & 3 deletions cmd/genkit.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@ import (
// Initializes the AI client and idles, for use with genkit's dev tooling
// Run with `genkit start -- mise genkit`
var genkitDevCmd = &cli.Command{
Name: "genkit",
Usage: "Initialize the AI client and idle, for use with genkit's dev tooling",
Hidden: true,
Name: "genkit",
Usage: "Initialize the AI client and idle, for use with genkit's dev tooling",
Hidden: true,
Metadata: cliutil.GlobalFlagMetadata(cliutil.ProviderOptions, cliutil.TandoorOptions),
Action: func(ctx context.Context, cmd *cli.Command) error {
cfg := config.FromContext(ctx)

Expand Down
76 changes: 61 additions & 15 deletions cmd/help.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package cmd

import (
"context"
"sort"

"github.com/derethil/mise/internal/cliutil"
"github.com/urfave/cli/v3"
)

Expand All @@ -15,42 +17,47 @@ const commandHelpTemplate = `NAME:
{{template "helpNameTemplate" .}}

USAGE:
{{template "usageTemplate" .}}{{if .Category}}
{{if .UsageText}}{{wrap .UsageText 3}}{{else}}{{.FullName}}{{if .VisibleFlags}} [options]{{end}}{{if .VisibleCommands}} [command [command options]]{{end}}{{if .ArgsUsage}} {{trim .ArgsUsage}}{{else}}{{range .Arguments}} {{.Usage}}{{end}}{{end}}{{end}}{{if .Category}}

CATEGORY:
{{.Category}}{{end}}{{if .Description}}

DESCRIPTION:
{{template "descriptionTemplate" .}}{{end}}{{if .VisibleFlagCategories}}
{{template "descriptionTemplate" .}}{{end}}{{if .Metadata.hasLocalOptions}}

OPTIONS:{{template "visibleFlagCategoryTemplate" .}}{{else if .VisibleFlags}}

OPTIONS:{{template "visibleFlagTemplate" .}}{{end}}{{if .VisiblePersistentFlags}}

GLOBAL OPTIONS:{{template "visibleFlagCategoryTemplate" .Root.Metadata.globalFlagCategories}}{{end}}
OPTIONS:{{if .VisibleFlagCategories}}{{template "visibleFlagCategoryTemplate" .}}{{else}}{{template "visibleFlagTemplate" .}}
{{end}}{{else}}
{{end}}{{if .VisiblePersistentFlags}}
GLOBAL OPTIONS:{{template "visibleFlagCategoryTemplate" .Metadata.visibleGlobalFlagCategories}}
See 'mise --help' for all global configuration overrides.{{end}}
`

const subcommandHelpTemplate = `NAME:
{{template "helpNameTemplate" .}}

USAGE:
{{if .UsageText}}{{wrap .UsageText 3}}{{else}}{{.FullName}}{{if .VisibleCommands}} [command [command options]]{{end}}{{if .ArgsUsage}} {{.ArgsUsage}}{{else}}{{if .Arguments}} [arguments...]{{end}}{{end}}{{end}}{{if .Category}}
{{if .UsageText}}{{wrap .UsageText 3}}{{else}}{{.FullName}}{{if .VisibleFlags}} [options]{{end}}{{if .VisibleCommands}} [command [command options]]{{end}}{{if .ArgsUsage}} {{trim .ArgsUsage}}{{else}}{{range .Arguments}} {{.Usage}}{{end}}{{end}}{{end}}{{if .Category}}

CATEGORY:
{{.Category}}{{end}}{{if .Description}}

DESCRIPTION:
{{template "descriptionTemplate" .}}{{end}}{{if .VisibleCommands}}

COMMANDS:{{template "visibleCommandTemplate" .}}{{end}}{{if .VisibleFlagCategories}}

OPTIONS:{{template "visibleFlagCategoryTemplate" .}}{{else if .VisibleFlags}}
COMMANDS:{{template "visibleCommandTemplate" .}}{{end}}{{if .Metadata.hasLocalOptions}}

OPTIONS:{{template "visibleFlagTemplate" .}}{{end}}{{if .VisiblePersistentFlags}}

GLOBAL OPTIONS:{{template "visibleFlagCategoryTemplate" .Root.Metadata.globalFlagCategories}}{{end}}
OPTIONS:{{if .VisibleFlagCategories}}{{template "visibleFlagCategoryTemplate" .}}{{else}}{{template "visibleFlagTemplate" .}}
{{end}}{{else}}
{{end}}{{if .VisiblePersistentFlags}}
GLOBAL OPTIONS:{{template "visibleFlagCategoryTemplate" .Metadata.visibleGlobalFlagCategories}}
See 'mise --help' for all global configuration overrides.{{end}}
`

const (
hasLocalOptionsMetadataKey = "hasLocalOptions"
visibleGlobalFlagCategoriesMetadataKey = "visibleGlobalFlagCategories"
)

type globalFlagCategories struct {
categories []cli.VisibleFlagCategory
}
Expand All @@ -72,10 +79,18 @@ func (g globalFlagCategory) Flags() []cli.Flag {
return g.flags
}

func newGlobalFlagCategories(flags []cli.Flag) globalFlagCategories {
func newGlobalFlagCategories(flags []cli.Flag, allowedNames ...string) globalFlagCategories {
allowed := make(map[string]bool, len(allowedNames))
for _, name := range allowedNames {
allowed[name] = true
}

byCategory := make(map[string][]cli.Flag)
for _, flag := range flags {
category := flag.(cli.CategorizableFlag).GetCategory()
if len(allowed) > 0 && !allowed[category] {
continue
}
byCategory[category] = append(byCategory[category], flag)
}

Expand All @@ -96,3 +111,34 @@ func newGlobalFlagCategories(flags []cli.Flag) globalFlagCategories {

return globalFlagCategories{categories: categories}
}

func configureGlobalHelp(cmd *cli.Command, flags []cli.Flag) {
categories := cmd.Metadata[cliutil.GlobalFlagCategoriesMetadataKey].([]string)
cmd.Metadata[hasLocalOptionsMetadataKey] = len(cmd.Flags) > 0
cmd.Metadata[visibleGlobalFlagCategoriesMetadataKey] = newGlobalFlagCategories(flags, categories...)
if cmd.Name != "mise" {
cmd.ShellComplete = completeWithGlobalFlags
}

for _, child := range cmd.Commands {
configureGlobalHelp(child, flags)
}
}

func completeWithGlobalFlags(ctx context.Context, cmd *cli.Command) {
localFlags := cmd.Flags
cmd.Flags = append(append([]cli.Flag{}, localFlags...), relevantGlobalFlags(cmd)...)
defer func() { cmd.Flags = localFlags }()

cli.DefaultCompleteWithFlags(ctx, cmd)
}

func relevantGlobalFlags(cmd *cli.Command) []cli.Flag {
categories := cmd.Metadata[visibleGlobalFlagCategoriesMetadataKey].(globalFlagCategories)
var flags []cli.Flag
for _, category := range categories.VisibleFlagCategories() {
flags = append(flags, category.Flags()...)
}

return flags
}
123 changes: 123 additions & 0 deletions cmd/help_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package cmd

import (
"bytes"
"context"
"testing"

"github.com/derethil/mise/internal/cliutil"
"github.com/stretchr/testify/assert"
"github.com/urfave/cli/v3"
)

func TestGlobalHelpCategoriesAreRelevantToCommand(t *testing.T) {
tests := []struct {
name string
command *cli.Command
categories []string
}{
{name: "logs", command: rootCmd.Command("logs"), categories: []string{"GENERAL OPTIONS"}},
{name: "recipe backup", command: rootCmd.Command("recipe").Command("backup"), categories: []string{"GENERAL OPTIONS", "TANDOOR OPTIONS"}},
{name: "recipe keyword", command: rootCmd.Command("recipe").Command("keyword"), categories: []string{"GENERAL OPTIONS", "PROVIDER OPTIONS", "TANDOOR OPTIONS"}},
{name: "models clear", command: rootCmd.Command("models").Command("clear"), categories: []string{"GENERAL OPTIONS", "PROVIDER OPTIONS"}},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
metadata := test.command.Metadata[visibleGlobalFlagCategoriesMetadataKey].(globalFlagCategories)
var names []string
for _, category := range metadata.VisibleFlagCategories() {
names = append(names, category.Name())
}
assert.Equal(t, test.categories, names)
})
}
}

func TestEveryCommandDeclaresGlobalFlagCategories(t *testing.T) {
var check func(*cli.Command)
check = func(command *cli.Command) {
if command.Name == "help" {
return
}

categories, ok := command.Metadata[cliutil.GlobalFlagCategoriesMetadataKey].([]string)
assert.True(t, ok, "%s must declare its global flag categories", command.Name)
assert.Contains(t, categories, cliutil.GeneralOptions)

for _, child := range command.Commands {
check(child)
}
}

check(rootCmd)
}

func TestCompletionIncludesRelevantGlobalFlags(t *testing.T) {
assert.ElementsMatch(t,
[]string{"config", "verbose", "model", "ollama-url", "start-ollama", "tandoor-url"},
flagNames(relevantGlobalFlags(rootCmd.Command("import"))),
)
assert.ElementsMatch(t,
[]string{"config", "verbose", "tandoor-url"},
flagNames(relevantGlobalFlags(rootCmd.Command("recipe").Command("backup"))),
)
assert.ElementsMatch(t,
[]string{"config", "verbose"},
flagNames(relevantGlobalFlags(rootCmd.Command("logs"))),
)
}

func TestOptionsSectionRequiresLocalOptions(t *testing.T) {
withoutOptions := renderCommandHelp(t, nil)
assert.NotContains(t, withoutOptions, "\nOPTIONS:")
assert.Contains(t, withoutOptions, "\nGLOBAL OPTIONS:")
assert.Contains(t, withoutOptions, "mise child [options]\n\nGLOBAL OPTIONS:")

withOptions := renderCommandHelp(t, []cli.Flag{&cli.BoolFlag{Name: "local"}})
assert.Contains(t, withOptions, "\nOPTIONS:")
assert.Contains(t, withOptions, "--local")
assert.Contains(t, withOptions, "--help, -h show help\n\nGLOBAL OPTIONS:")

withArgument := renderCommandHelpWithArguments(t, nil, []cli.Argument{&cli.StringArg{Name: "value"}})
assert.Contains(t, withArgument, "mise child [options] [value]\n")
assert.NotContains(t, withArgument, "[value] \n")
}

func renderCommandHelp(t *testing.T, localFlags []cli.Flag) string {
return renderCommandHelpWithArguments(t, localFlags, nil)
}

func renderCommandHelpWithArguments(t *testing.T, localFlags []cli.Flag, arguments []cli.Argument) string {
t.Helper()

var output bytes.Buffer
command := &cli.Command{
Name: "child",
Flags: localFlags,
Arguments: arguments,
Metadata: cliutil.GlobalFlagMetadata(),
}
root := &cli.Command{
Name: "mise",
Writer: &output,
Flags: []cli.Flag{&cli.BoolFlag{Name: "verbose", Category: cliutil.GeneralOptions}},
Metadata: cliutil.GlobalFlagMetadata(),
Commands: []*cli.Command{command},
}
configureGlobalHelp(root, root.Flags)

err := root.Run(context.Background(), []string{"mise", "child", "--help"})
assert.NoError(t, err)

return output.String()
}

func flagNames(flags []cli.Flag) []string {
names := make([]string, 0, len(flags))
for _, flag := range flags {
names = append(names, flag.Names()[0])
}

return names
}
20 changes: 12 additions & 8 deletions cmd/import/import.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,24 +13,28 @@ import (
)

const (
flagCookiesFile = "video.cookies-file"
flagCookiesFromBrowser = "video.cookies-from-browser"
flagCookiesFile = "cookies-file"
flagCookiesFromBrowser = "cookies-from-browser"
flagDryRun = "dry-run"
)

var Command = &cli.Command{
Name: "import",
Usage: "Import a recipe from various social media video sources",
Name: "import",
Usage: "Import a recipe from various social media video sources",
Metadata: cliutil.GlobalFlagMetadata(cliutil.ProviderOptions, cliutil.TandoorOptions),
Arguments: []cli.Argument{
&cli.StringArg{
Name: "url",
Required: true,
},
},
Flags: append(config.FlagsForCommand("import"), &cli.BoolFlag{
Name: flagDryRun,
Usage: "Fetch and print video metadata without downloading",
}),
Flags: append([]cli.Flag{
&cli.BoolFlag{
Name: flagDryRun,
Usage: "Fetch and print video metadata without downloading",
Category: "ACTION OPTIONS",
},
}, config.FlagsForCommand("import")...),
ArgValidator: func(ctx context.Context, cmd *cli.Command) error {
validators := []func(*cli.Command) error{
validateUrl,
Expand Down
6 changes: 4 additions & 2 deletions cmd/logs.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ import (
"context"
"fmt"

"github.com/derethil/mise/internal/cliutil"
"github.com/derethil/mise/internal/logging"
"github.com/urfave/cli/v3"
)

var logsCmd = &cli.Command{
Name: "logs",
Usage: "Print path to the mise logs file",
Name: "logs",
Usage: "Print path to the mise logs file",
Metadata: cliutil.GlobalFlagMetadata(),
Action: func(ctx context.Context, cmd *cli.Command) error {
fmt.Println(logging.LogPath)
return nil
Expand Down
5 changes: 3 additions & 2 deletions cmd/model/clear.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ import (
)

var clearCmd = &cli.Command{
Name: "clear",
Usage: "Delete Ollama models that are not configured for use by mise",
Name: "clear",
Usage: "Delete Ollama models that are not configured for use by mise",
Metadata: cliutil.GlobalFlagMetadata(cliutil.ProviderOptions),
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "yes",
Expand Down
6 changes: 4 additions & 2 deletions cmd/model/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,16 @@ import (
"strings"

"github.com/derethil/mise/internal/ai/providers"
"github.com/derethil/mise/internal/cliutil"
"github.com/derethil/mise/internal/config"
"github.com/ollama/ollama/format"
"github.com/urfave/cli/v3"
)

var listCmd = &cli.Command{
Name: "list",
Usage: "Print Mise's configured Ollama models and their availability",
Name: "list",
Usage: "Print Mise's configured Ollama models and their availability",
Metadata: cliutil.GlobalFlagMetadata(cliutil.ProviderOptions),
Action: func(ctx context.Context, cmd *cli.Command) error {
cfg := config.FromContext(ctx)

Expand Down
10 changes: 7 additions & 3 deletions cmd/model/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@
// pulling, and clearing Ollama models.
package model

import "github.com/urfave/cli/v3"
import (
"github.com/derethil/mise/internal/cliutil"
"github.com/urfave/cli/v3"
)

var Command = &cli.Command{
Name: "models",
Usage: "Manage your configured models and their availability",
Name: "models",
Usage: "Manage your configured models and their availability",
Metadata: cliutil.GlobalFlagMetadata(cliutil.ProviderOptions),
Commands: []*cli.Command{
listCmd,
pullCmd,
Expand Down
Loading
Loading