Skip to content

Commit 741374a

Browse files
committed
add model-cli config command with INI file support
Introduce 'model-cli config' as a new top-level command with an interface and file format inspired by, but not referencing, 'git config'. - New cmd/cli/iniconfig package: parses and writes INI-style config files (section headers, subsections, boolean keys, inline comments, backslash escapes, quoted values, UTF-8 BOM). Writes are atomic via .lock + rename. - New 'config' command with subcommands: get, set, unset, list, edit. All subcommands accept --global (default, ~/.config/model-runner/config), --system (/etc/model-runner/config), and --file/-f flags. - Remove the 'config' alias from 'configure' to avoid a name collision; 'configure' remains hidden and undocumented for existing callers. - 'config' requires no running model-runner instance (pure local file I/O) and is registered outside the withStandaloneRunner group. - Regenerate CLI reference docs.
1 parent 317e689 commit 741374a

21 files changed

Lines changed: 1766 additions & 5 deletions

cmd/cli/commands/config.go

Lines changed: 345 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,345 @@
1+
package commands
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"os/exec"
7+
"path/filepath"
8+
"runtime"
9+
10+
"github.com/docker/model-runner/cmd/cli/iniconfig"
11+
"github.com/spf13/cobra"
12+
)
13+
14+
// defaultConfigPath returns the default (global/user-level) config file path.
15+
// It honours XDG_CONFIG_HOME when set:
16+
//
17+
// $XDG_CONFIG_HOME/model-runner/config
18+
// ~/.config/model-runner/config (fallback)
19+
func defaultConfigPath() string {
20+
if xdg := os.Getenv("XDG_CONFIG_HOME"); xdg != "" {
21+
return filepath.Join(xdg, "model-runner", "config")
22+
}
23+
home, err := os.UserHomeDir()
24+
if err != nil {
25+
return filepath.Join(".config", "model-runner", "config")
26+
}
27+
return filepath.Join(home, ".config", "model-runner", "config")
28+
}
29+
30+
// systemConfigPath returns the system-wide config file path.
31+
func systemConfigPath() string {
32+
if runtime.GOOS == "windows" {
33+
if pd := os.Getenv("ProgramData"); pd != "" {
34+
return filepath.Join(pd, "model-runner", "config")
35+
}
36+
return `C:\ProgramData\model-runner\config`
37+
}
38+
return "/etc/model-runner/config"
39+
}
40+
41+
// resolveConfigPath picks the config file to operate on, given the flags.
42+
// Exactly one of global, system, or file may be set.
43+
func resolveConfigPath(global, system bool, file string) (string, error) {
44+
count := 0
45+
if global {
46+
count++
47+
}
48+
if system {
49+
count++
50+
}
51+
if file != "" {
52+
count++
53+
}
54+
if count > 1 {
55+
return "", fmt.Errorf("only one of --global, --system, or --file may be specified")
56+
}
57+
switch {
58+
case system:
59+
return systemConfigPath(), nil
60+
case file != "":
61+
return file, nil
62+
default:
63+
// --global is the default
64+
return defaultConfigPath(), nil
65+
}
66+
}
67+
68+
// addLocationFlags adds the standard --global/--system/--file flags to a command.
69+
func addLocationFlags(cmd *cobra.Command, global, system *bool, file *string) {
70+
cmd.Flags().BoolVar(global, "global", false, "use the global (user-level) config file")
71+
cmd.Flags().BoolVar(system, "system", false, "use the system-wide config file")
72+
cmd.Flags().StringVarP(file, "file", "f", "", "use a specific config file")
73+
}
74+
75+
// newConfigCmd returns the top-level "config" command.
76+
func newConfigCmd() *cobra.Command {
77+
c := &cobra.Command{
78+
Use: "config",
79+
Short: "Read and write model-runner config file values",
80+
Long: `Read and write model-runner config file values.
81+
82+
The config file uses an INI format with sections and key=value pairs:
83+
84+
[section]
85+
key = value
86+
[section "subsection"]
87+
key = value
88+
89+
Keys are specified in dot notation: section.key or section.subsection.key.
90+
91+
The default file is ` + defaultConfigPath() + `.
92+
93+
Examples:
94+
model-cli config set user.name "Alice"
95+
model-cli config get user.name
96+
model-cli config list
97+
model-cli config unset user.name
98+
model-cli config edit`,
99+
// Do not run a PersistentPreRunE that requires a running model-runner;
100+
// config is pure local-file work.
101+
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
102+
return nil
103+
},
104+
}
105+
106+
c.AddCommand(
107+
newConfigGetCmd(),
108+
newConfigSetCmd(),
109+
newConfigUnsetCmd(),
110+
newConfigListCmd(),
111+
newConfigEditCmd(),
112+
)
113+
return c
114+
}
115+
116+
// newConfigGetCmd implements "model-cli config get <key>".
117+
func newConfigGetCmd() *cobra.Command {
118+
var (
119+
global bool
120+
system bool
121+
file string
122+
defaultVal string
123+
hasDefault bool
124+
showAll bool
125+
showOrigin bool
126+
)
127+
128+
c := &cobra.Command{
129+
Use: "get <key>",
130+
Short: "Get the value of a config key",
131+
Long: `Get the value of a config key.
132+
133+
Prints the value of the given key to stdout. If the key appears multiple times
134+
(multi-valued), the last value is printed. Use --all to print all values.
135+
136+
Exit status is 1 if the key is not found (unless --default is given).`,
137+
Args: cobra.ExactArgs(1),
138+
RunE: func(cmd *cobra.Command, args []string) error {
139+
path, err := resolveConfigPath(global, system, file)
140+
if err != nil {
141+
return err
142+
}
143+
f, err := iniconfig.Load(path)
144+
if err != nil {
145+
return err
146+
}
147+
148+
key := args[0]
149+
150+
if showAll {
151+
vals := f.GetAll(key)
152+
if len(vals) == 0 {
153+
if hasDefault {
154+
cmd.Println(defaultVal)
155+
return nil
156+
}
157+
return fmt.Errorf("key not found: %s", key)
158+
}
159+
for _, v := range vals {
160+
if showOrigin {
161+
cmd.Printf("file:%s\t%s\n", path, v)
162+
} else {
163+
cmd.Println(v)
164+
}
165+
}
166+
return nil
167+
}
168+
169+
v, ok := f.Get(key)
170+
if !ok {
171+
if hasDefault {
172+
cmd.Println(defaultVal)
173+
return nil
174+
}
175+
return fmt.Errorf("key not found: %s", key)
176+
}
177+
if showOrigin {
178+
cmd.Printf("file:%s\t%s\n", path, v)
179+
} else {
180+
cmd.Println(v)
181+
}
182+
return nil
183+
},
184+
}
185+
186+
addLocationFlags(c, &global, &system, &file)
187+
c.Flags().StringVar(&defaultVal, "default", "", "value to emit if the key is not set")
188+
c.Flags().BoolVar(&showAll, "all", false, "print all values for multi-valued keys")
189+
c.Flags().BoolVar(&showOrigin, "show-origin", false, "show the origin (file path) of each value")
190+
// Track whether --default was explicitly provided.
191+
c.PreRunE = func(cmd *cobra.Command, args []string) error {
192+
hasDefault = cmd.Flags().Changed("default")
193+
return nil
194+
}
195+
196+
return c
197+
}
198+
199+
// newConfigSetCmd implements "model-cli config set <key> <value>".
200+
func newConfigSetCmd() *cobra.Command {
201+
var global, system bool
202+
var file string
203+
204+
c := &cobra.Command{
205+
Use: "set <key> <value>",
206+
Short: "Set a config key to a value",
207+
Long: `Set a config key to a value.
208+
209+
If the key already exists its value is replaced. The file is written atomically.`,
210+
Args: cobra.ExactArgs(2),
211+
RunE: func(cmd *cobra.Command, args []string) error {
212+
path, err := resolveConfigPath(global, system, file)
213+
if err != nil {
214+
return err
215+
}
216+
f, err := iniconfig.Load(path)
217+
if err != nil {
218+
return err
219+
}
220+
return f.Set(args[0], args[1])
221+
},
222+
}
223+
224+
addLocationFlags(c, &global, &system, &file)
225+
return c
226+
}
227+
228+
// newConfigUnsetCmd implements "model-cli config unset <key>".
229+
func newConfigUnsetCmd() *cobra.Command {
230+
var global, system bool
231+
var file string
232+
233+
c := &cobra.Command{
234+
Use: "unset <key>",
235+
Short: "Remove a config key",
236+
Long: `Remove a config key (and all its values) from the file.`,
237+
Args: cobra.ExactArgs(1),
238+
RunE: func(cmd *cobra.Command, args []string) error {
239+
path, err := resolveConfigPath(global, system, file)
240+
if err != nil {
241+
return err
242+
}
243+
f, err := iniconfig.Load(path)
244+
if err != nil {
245+
return err
246+
}
247+
return f.Unset(args[0])
248+
},
249+
}
250+
251+
addLocationFlags(c, &global, &system, &file)
252+
return c
253+
}
254+
255+
// newConfigListCmd implements "model-cli config list".
256+
func newConfigListCmd() *cobra.Command {
257+
var global, system bool
258+
var file string
259+
var showOrigin bool
260+
261+
c := &cobra.Command{
262+
Use: "list",
263+
Aliases: []string{"ls"},
264+
Short: "List all config key/value pairs",
265+
Long: `List all key=value pairs from the config file, one per line.`,
266+
Args: cobra.NoArgs,
267+
RunE: func(cmd *cobra.Command, args []string) error {
268+
path, err := resolveConfigPath(global, system, file)
269+
if err != nil {
270+
return err
271+
}
272+
f, err := iniconfig.Load(path)
273+
if err != nil {
274+
return err
275+
}
276+
if showOrigin {
277+
for _, e := range f.Entries() {
278+
cmd.Printf("file:%s\t%s=%s\n", path, e.Key, e.Value)
279+
}
280+
return nil
281+
}
282+
return f.List(cmd.OutOrStdout())
283+
},
284+
}
285+
286+
addLocationFlags(c, &global, &system, &file)
287+
c.Flags().BoolVar(&showOrigin, "show-origin", false, "show the origin (file path) of each value")
288+
return c
289+
}
290+
291+
// newConfigEditCmd implements "model-cli config edit".
292+
func newConfigEditCmd() *cobra.Command {
293+
var global, system bool
294+
var file string
295+
296+
c := &cobra.Command{
297+
Use: "edit",
298+
Short: "Open the config file in your editor",
299+
Long: `Open the config file in the default editor.
300+
301+
The editor is determined by the VISUAL or EDITOR environment variables,
302+
falling back to vi on Unix and notepad on Windows.`,
303+
Args: cobra.NoArgs,
304+
RunE: func(cmd *cobra.Command, args []string) error {
305+
path, err := resolveConfigPath(global, system, file)
306+
if err != nil {
307+
return err
308+
}
309+
// Ensure the file (and its parent directory) exist so the editor
310+
// has something to open.
311+
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
312+
return err
313+
}
314+
if _, err := os.Stat(path); os.IsNotExist(err) {
315+
f, err2 := os.Create(path)
316+
if err2 != nil {
317+
return err2
318+
}
319+
_ = f.Close()
320+
}
321+
322+
editor := os.Getenv("VISUAL")
323+
if editor == "" {
324+
editor = os.Getenv("EDITOR")
325+
}
326+
if editor == "" {
327+
if runtime.GOOS == "windows" {
328+
editor = "notepad"
329+
} else {
330+
editor = "vi"
331+
}
332+
}
333+
334+
//nolint:gosec // editor and path are user-controlled inputs, which is intentional
335+
editorCmd := exec.CommandContext(cmd.Context(), editor, path)
336+
editorCmd.Stdin = os.Stdin
337+
editorCmd.Stdout = os.Stdout
338+
editorCmd.Stderr = os.Stderr
339+
return editorCmd.Run()
340+
},
341+
}
342+
343+
addLocationFlags(c, &global, &system, &file)
344+
return c
345+
}

cmd/cli/commands/configure.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,9 @@ func newConfigureCmd() *cobra.Command {
1111
var flags ConfigureFlags
1212

1313
c := &cobra.Command{
14-
Use: "configure [--context-size=<n>] [--speculative-draft-model=<model>] [--hf_overrides=<json>] [--gpu-memory-utilization=<float>] [--mode=<mode>] [--think] [--keep-alive=<duration>] MODEL [-- <runtime-flags...>]",
15-
Aliases: []string{"config"},
16-
Short: "Manage model runtime configurations",
17-
Hidden: true,
14+
Use: "configure [--context-size=<n>] [--speculative-draft-model=<model>] [--hf_overrides=<json>] [--gpu-memory-utilization=<float>] [--mode=<mode>] [--think] [--keep-alive=<duration>] MODEL [-- <runtime-flags...>]",
15+
Short: "Manage model runtime configurations",
16+
Hidden: true,
1817
Args: func(cmd *cobra.Command, args []string) error {
1918
argsBeforeDash := cmd.ArgsLenAtDash()
2019
if argsBeforeDash == -1 {

cmd/cli/commands/root.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ func NewRootCmd(cli *command.DockerCli) *cobra.Command {
105105
newReinstallRunner(),
106106
newSearchCmd(),
107107
newSkillsCmd(),
108+
newConfigCmd(),
108109
)
109110
rootCmd.AddCommand(newGatewayCmd())
110111

0 commit comments

Comments
 (0)