snapshot of c000b74a5b09c433f96a69fa102c5c8f6583dff0 Annotations about the code that implements koment.

internal/cli/bootstrap.go

1 package cli
2
3 import (
4 "bufio"
5 "errors"
6 "fmt"
7 "io"
8 "io/fs"
9 "os"
10 "strings"
11
12 "github.com/koment-dev/koment/internal/agentpolicy"
13 "github.com/koment-dev/koment/internal/policy"
14 )
15
16 func runBootstrap(args []string, env Environment) int {
17 flags := flagSet("bootstrap", env)
18 agentsList := flags.String("agents", "", "comma-separated adapter names to install (claude, copilot, cursor, codex, opencode, agents)")
19 all := flags.Bool("all", false, "install every supported adapter")
20 policyOnly := flags.Bool("policy-only", false, "install the policy only; do not refresh any adapter")
21 nonInteractive := flags.Bool("non-interactive", false, "do not prompt; require explicit --agents, --all or --policy-only")
22 if code, ok := parse(flags, args); !ok {
23 return code
24 }
25 if flags.NArg() != 0 {
26 return misuse(env, "bootstrap takes no arguments")
27 }
28
29 annotations, err := openStore()
30 if err != nil {
31 return fail(env, err)
32 }
33 existing, err := policy.Load(annotations.Root())
34 if err != nil && !isMissingPolicy(err) {
35 return fail(env, err)
36 }
37 havePolicy := err == nil
38
39 selected, err := resolveBootstrapAdapters(*agentsList, *all, *policyOnly, *nonInteractive, existing, env)
40 if err != nil {
41 return misuse(env, "%v", err)
42 }
43
44 configured := existing
45 if !havePolicy {
46 configured = policy.Default()
47 }
48 if len(selected) > 0 {
49 configured.Spec.Agents.Adapters = selected
50 }
51
52 if !havePolicy {
53 if err := policy.Save(annotations.Root(), configured); err != nil {
54 return fail(env, err)
55 }
56 fmt.Fprintf(env.Stdout, "installed %s\n", policy.FileName)
57 }
58
59 var changes []agentpolicy.Change
60 if *policyOnly {
61 changes, err = agentpolicy.InstallContributing(annotations.Root(), configured)
62 } else {
63 changes, err = agentpolicy.Install(annotations.Root(), configured)
64 }
65 if err != nil {
66 return fail(env, err)
67 }
68 for _, change := range changes {
69 fmt.Fprintf(env.Stdout, "%s %s\n", change.Action, change.Path)
70 }
71 if len(changes) == 0 {
72 fmt.Fprintln(env.Stdout, "agent policy: already current")
73 }
74 return ExitOK
75 }
76
77 func resolveBootstrapAdapters(agentsList string, all, policyOnly, nonInteractive bool, existing policy.Policy, env Environment) ([]policy.Adapter, error) {
78 switch {
79 case agentsList != "":
80 return parseAdapterList(agentsList)
81 case all:
82 return append([]policy.Adapter(nil), policy.Default().Spec.Agents.Adapters...), nil
83 case policyOnly:
84 return nil, nil
85 case existing.Spec.Agents.Adapters != nil:
86 return append([]policy.Adapter(nil), existing.Spec.Agents.Adapters...), nil
87 }
88 if nonInteractive || !stdinIsTerminal() {
89 return nil, fmt.Errorf("no adapter selection resolved; pass --agents <list>, --all, or --policy-only (or run interactively in a terminal)")
90 }
91 return promptAdapters(existing.Spec.Agents.Adapters, env)
92 }
93
94 func parseAdapterList(raw string) ([]policy.Adapter, error) {
95 parts := strings.Split(raw, ",")
96 selected := make([]policy.Adapter, 0, len(parts))
97 seen := map[policy.Adapter]bool{}
98 for _, part := range parts {
99 name := strings.TrimSpace(part)
100 if name == "" {
101 continue
102 }
103 adapter := policy.Adapter(name)
104 if !validAdapter(adapter) {
105 return nil, fmt.Errorf("unknown adapter %q", name)
106 }
107 if seen[adapter] {
108 continue
109 }
110 seen[adapter] = true
111 selected = append(selected, adapter)
112 }
113 return selected, nil
114 }
115
116 func validAdapter(adapter policy.Adapter) bool {
117 for _, candidate := range policy.Default().Spec.Agents.Adapters {
118 if candidate == adapter {
119 return true
120 }
121 }
122 return false
123 }
124
125 func promptAdapters(current []policy.Adapter, env Environment) ([]policy.Adapter, error) {
126 all := policy.Default().Spec.Agents.Adapters
127 selected := make([]policy.Adapter, 0, len(all))
128 for _, adapter := range all {
129 defaultYes := containsAdapter(current, adapter)
130 answer, err := askYesNo(adapterName(adapter), defaultYes, env)
131 if err != nil {
132 return nil, err
133 }
134 if answer {
135 selected = append(selected, adapter)
136 }
137 }
138 return selected, nil
139 }
140
141 func askYesNo(prompt string, defaultYes bool, env Environment) (bool, error) {
142 hint := "[Y/n]"
143 if !defaultYes {
144 hint = "[y/N]"
145 }
146 fmt.Fprintf(env.Stdout, "%s %s ", prompt, hint)
147 reader := bufio.NewReader(env.Stdin)
148 line, err := reader.ReadString('\n')
149 if err != nil && err != io.EOF {
150 return false, fmt.Errorf("reading answer: %w", err)
151 }
152 answer := strings.TrimSpace(line)
153 switch strings.ToLower(answer) {
154 case "":
155 return defaultYes, nil
156 case "y", "yes":
157 return true, nil
158 case "n", "no":
159 return false, nil
160 }
161 return defaultYes, nil
162 }
163
164 func containsAdapter(list []policy.Adapter, adapter policy.Adapter) bool {
165 for _, candidate := range list {
166 if candidate == adapter {
167 return true
168 }
169 }
170 return false
171 }
172
173 func adapterName(adapter policy.Adapter) string {
174 switch adapter {
175 case policy.AdapterAgents:
176 return "AGENTS.md (managed contract for every client)"
177 case policy.AdapterClaude:
178 return "Claude Code (.mcp.json + CLAUDE.md import)"
179 case policy.AdapterCopilot:
180 return "GitHub Copilot (.github/copilot-instructions.md + .vscode/mcp.json)"
181 case policy.AdapterCursor:
182 return "Cursor (.cursor/rules/koment.mdc + .cursor/mcp.json)"
183 case policy.AdapterCodex:
184 return "Codex CLI (.codex/hooks.json + .codex/config.toml)"
185 case policy.AdapterOpencode:
186 return "opencode (.opencode/plugins/koment.js + opencode.json)"
187 }
188 return string(adapter)
189 }
190
191 func stdinIsTerminal() bool {
192 info, err := os.Stdin.Stat()
193 if err != nil {
194 return false
195 }
196 return info.Mode()&os.ModeCharDevice != 0
197 }
198
199 func isMissingPolicy(err error) bool {
200 return errors.Is(err, fs.ErrNotExist) || errors.Is(err, os.ErrNotExist)
201 }

Find an annotation

Search file paths, rationale, kinds, and authors.

moveEnter openEsc close