chore: import upstream snapshot with attribution
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,587 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
|
||||
"github.com/zzet/gortex/internal/agents"
|
||||
"github.com/zzet/gortex/internal/progress"
|
||||
"github.com/zzet/gortex/internal/tui"
|
||||
)
|
||||
|
||||
// agentLabels maps adapter Name() → human-friendly label shown in the wizard
|
||||
// checklist. Anything not listed falls through to title-casing the slug, so a
|
||||
// new adapter that ships before this table is updated still renders sanely.
|
||||
var agentLabels = map[string]string{
|
||||
"claude-code": "Claude Code",
|
||||
"cursor": "Cursor",
|
||||
"aider": "Aider",
|
||||
"antigravity": "Antigravity",
|
||||
"cline": "Cline",
|
||||
"codex": "Codex CLI",
|
||||
"continue": "Continue.dev",
|
||||
"gemini": "Gemini CLI",
|
||||
"hermes": "Hermes",
|
||||
"kimi": "Kimi Code CLI",
|
||||
"kilocode": "Kilo Code",
|
||||
"kiro": "Kiro",
|
||||
"oh-my-pi": "Oh My Pi",
|
||||
"opencode": "OpenCode",
|
||||
"openclaw": "OpenClaw",
|
||||
"pi": "Pi",
|
||||
"vscode": "VS Code (Copilot)",
|
||||
"windsurf": "Windsurf",
|
||||
"zed": "Zed",
|
||||
}
|
||||
|
||||
// agentDetails is the one-line "what does Gortex write here" hint that
|
||||
// appears in dim text right of each agent's label.
|
||||
var agentDetails = map[string]string{
|
||||
"claude-code": ".mcp.json · .claude/ hooks · CLAUDE.md · skills",
|
||||
"cursor": ".cursor/rules + .cursor/mcp.json",
|
||||
"aider": ".aider.conf.yml",
|
||||
"antigravity": ".antigravity/mcp.json",
|
||||
"cline": ".clinerules + cline_mcp_settings.json",
|
||||
"codex": "AGENTS.md + config.toml hooks",
|
||||
"continue": ".continue/config.json",
|
||||
"gemini": ".gemini/settings.json",
|
||||
"hermes": "~/.hermes/config.yaml + profiles + skills",
|
||||
"kimi": ".kimi-code/mcp.json · user hook via install",
|
||||
"kilocode": ".kilocode/mcp.json",
|
||||
"kiro": ".kiro/settings/mcp.json",
|
||||
"oh-my-pi": ".omp/mcp.json",
|
||||
"opencode": "opencode.json",
|
||||
"openclaw": ".openclaw/mcp.json",
|
||||
"pi": ".pi/extensions/gortex",
|
||||
"vscode": ".vscode/mcp.json",
|
||||
"windsurf": ".windsurf/mcp.json",
|
||||
"zed": ".zed/settings.json",
|
||||
}
|
||||
|
||||
// agentLabel returns a friendly display name for the given adapter slug.
|
||||
func agentLabel(name string) string {
|
||||
if l, ok := agentLabels[name]; ok {
|
||||
return l
|
||||
}
|
||||
parts := strings.Split(name, "-")
|
||||
for i, p := range parts {
|
||||
if p == "" {
|
||||
continue
|
||||
}
|
||||
parts[i] = strings.ToUpper(p[:1]) + p[1:]
|
||||
}
|
||||
return strings.Join(parts, " ")
|
||||
}
|
||||
|
||||
// agentDetail returns the dim "writes …" hint for the given adapter slug.
|
||||
func agentDetail(name string) string {
|
||||
if d, ok := agentDetails[name]; ok {
|
||||
return d
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// wizardStep is the current screen the user is looking at.
|
||||
type wizardStep int
|
||||
|
||||
const (
|
||||
stepAgents wizardStep = iota
|
||||
stepOptions
|
||||
stepConfirm
|
||||
stepCancelled
|
||||
)
|
||||
|
||||
// initWizardModel is the bubbletea Model for `gortex init -i` (and reused by
|
||||
// `gortex install -i` with a different title). It drives the pre-flight
|
||||
// selection screens; once the user confirms, runInit / runInstall takes over
|
||||
// and the dashboard is rendered by a separate Dashboard model. Keeping the
|
||||
// two models separate avoids the wizard having to know about indexer state.
|
||||
type initWizardModel struct {
|
||||
step wizardStep
|
||||
checklist tui.Checklist
|
||||
options tui.OptionsPanel
|
||||
title string // banner title, e.g. "gortex init" or "gortex install"
|
||||
rootPath string // banner subtitle target ("this repo" / "your machine")
|
||||
|
||||
tick int
|
||||
width int
|
||||
confirmed bool
|
||||
cancelled bool
|
||||
|
||||
// Resolved choices — read by the caller after Run() returns.
|
||||
pickedAgents []string
|
||||
hooks bool
|
||||
hookMode string
|
||||
analyze bool
|
||||
skills bool
|
||||
telemetry bool
|
||||
showTelemetry bool
|
||||
}
|
||||
|
||||
// initialDetection runs adapter.Detect for every registered adapter so the
|
||||
// wizard can pre-check the ones already present on disk. Detect is contractually
|
||||
// side-effect-free, so this is cheap and safe to run during model setup.
|
||||
func detectAdapters(adapters []agents.Adapter, env agents.Env) map[string]bool {
|
||||
out := make(map[string]bool, len(adapters))
|
||||
for _, a := range adapters {
|
||||
ok, _ := a.Detect(env)
|
||||
out[a.Name()] = ok
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// newInitWizardModel builds the wizard, seeded with the registered agents.
|
||||
// Detected adapters start checked; Claude Code is always checked (it's the
|
||||
// load-bearing adapter for hooks + skills, and unchecking it almost always
|
||||
// reflects a mis-click, not intent).
|
||||
func newInitWizardModel(rootPath string, registered []agents.Adapter, detected map[string]bool, defaults initDefaults) *initWizardModel {
|
||||
items := make([]tui.CheckItem, 0, len(registered))
|
||||
for _, a := range registered {
|
||||
name := a.Name()
|
||||
picked := detected[name]
|
||||
// Claude Code always defaults to picked — it owns the hook surface
|
||||
// and the CLAUDE.md global guidance; opting it out makes most other
|
||||
// agents pointless. The user can still uncheck it in the wizard.
|
||||
if name == "claude-code" {
|
||||
picked = true
|
||||
}
|
||||
items = append(items, tui.CheckItem{
|
||||
Label: agentLabel(name),
|
||||
Detail: agentDetail(name),
|
||||
Picked: picked,
|
||||
})
|
||||
}
|
||||
cl := tui.NewChecklist(items)
|
||||
|
||||
opts := tui.OptionsPanel{
|
||||
Toggles: []tui.Toggle{
|
||||
{
|
||||
Label: "Agent hooks",
|
||||
Hint: "Claude Code + Codex",
|
||||
Detail: "inject graph orientation and redirect indexed source reads where supported",
|
||||
On: defaults.hooks,
|
||||
},
|
||||
{
|
||||
Label: "Codebase analysis",
|
||||
Hint: "richer CLAUDE.md preamble (--analyze)",
|
||||
Detail: "index the repo once to embed a codebase overview into CLAUDE.md",
|
||||
On: defaults.analyze,
|
||||
},
|
||||
{
|
||||
Label: "Community skills",
|
||||
Hint: "per-cluster routing under .claude/skills (--skills)",
|
||||
Detail: "auto-generate one SKILL.md per detected community",
|
||||
On: defaults.skills,
|
||||
},
|
||||
},
|
||||
Selects: []tui.Select{
|
||||
{
|
||||
Label: "Hook posture",
|
||||
Options: []tui.SelectOption{
|
||||
{Value: "deny", Label: "deny", Detail: "PreToolUse refuses Read/Grep/Glob against indexed source"},
|
||||
{Value: "enrich", Label: "enrich", Detail: "PostToolUse appends graph context after the tool runs"},
|
||||
{Value: "consult-unlock", Label: "consult-unlock", Detail: "deny fallback reads until Gortex is queried once this session"},
|
||||
{Value: "nudge", Label: "nudge", Detail: "soft-deny once per burst, then let the next call proceed"},
|
||||
},
|
||||
Picked: hookModeIndex(defaults.hookMode),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Install-only: append the anonymous-telemetry toggle as the last toggle
|
||||
// (kept last so the hooks/analyze/skills positional read-back is stable).
|
||||
if defaults.showTelemetry {
|
||||
opts.Toggles = append(opts.Toggles, tui.Toggle{
|
||||
Label: "Anonymous telemetry",
|
||||
Hint: "tool/command counts only — off by default",
|
||||
Detail: "no code, paths, or names; toggle later with `gortex telemetry on|off`",
|
||||
On: defaults.telemetry,
|
||||
})
|
||||
}
|
||||
|
||||
return &initWizardModel{
|
||||
step: stepAgents,
|
||||
checklist: cl,
|
||||
options: opts,
|
||||
title: "gortex init",
|
||||
rootPath: rootPath,
|
||||
hooks: defaults.hooks,
|
||||
hookMode: defaults.hookMode,
|
||||
analyze: defaults.analyze,
|
||||
skills: defaults.skills,
|
||||
telemetry: defaults.telemetry,
|
||||
showTelemetry: defaults.showTelemetry,
|
||||
}
|
||||
}
|
||||
|
||||
// initDefaults bundles the boolean defaults the wizard inherits from CLI
|
||||
// flags / global config. Resets are cheaper than threading a half-dozen
|
||||
// scalars through newInitWizardModel.
|
||||
type initDefaults struct {
|
||||
hooks bool
|
||||
hookMode string
|
||||
analyze bool
|
||||
skills bool
|
||||
// telemetry seeds the (install-only) anonymous-telemetry toggle;
|
||||
// showTelemetry adds that toggle to the options panel. `gortex init`
|
||||
// leaves both zero so the toggle never appears there.
|
||||
telemetry bool
|
||||
showTelemetry bool
|
||||
}
|
||||
|
||||
// hookModeIndex maps a hook-mode value to its Select index, defaulting to
|
||||
// "deny" (0) on an empty / unknown value.
|
||||
func hookModeIndex(mode string) int {
|
||||
switch mode {
|
||||
case "enrich":
|
||||
return 1
|
||||
case "consult-unlock":
|
||||
return 2
|
||||
case "nudge":
|
||||
return 3
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// wizardTickMsg paces the mesh animation at the same 80ms cadence as the
|
||||
// dashboard so the brand mark feels consistent across screens.
|
||||
type wizardTickMsg time.Time
|
||||
|
||||
func wizardTick() tea.Cmd {
|
||||
return tea.Tick(80*time.Millisecond, func(t time.Time) tea.Msg { return wizardTickMsg(t) })
|
||||
}
|
||||
|
||||
// Init starts the mesh animation ticker.
|
||||
func (m *initWizardModel) Init() tea.Cmd { return wizardTick() }
|
||||
|
||||
// Update handles keyboard input + ticks. Quit on q / esc / ctrl-c. Enter on
|
||||
// stepConfirm sets m.confirmed and quits — the caller checks that flag to
|
||||
// decide whether to proceed with runInit.
|
||||
func (m *initWizardModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case wizardTickMsg:
|
||||
_ = msg
|
||||
m.tick++
|
||||
return m, wizardTick()
|
||||
case tea.WindowSizeMsg:
|
||||
m.width = msg.Width
|
||||
return m, nil
|
||||
case tea.KeyMsg:
|
||||
return m.handleKey(msg)
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *initWizardModel) handleKey(k tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
// Global escape hatches first.
|
||||
switch k.String() {
|
||||
case "ctrl+c", "q", "esc":
|
||||
m.cancelled = true
|
||||
m.step = stepCancelled
|
||||
return m, tea.Quit
|
||||
}
|
||||
switch m.step {
|
||||
case stepAgents:
|
||||
return m.handleAgentsKey(k)
|
||||
case stepOptions:
|
||||
return m.handleOptionsKey(k)
|
||||
case stepConfirm:
|
||||
return m.handleConfirmKey(k)
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *initWizardModel) handleAgentsKey(k tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch k.String() {
|
||||
case "up", "k":
|
||||
m.checklist.Move(-1)
|
||||
case "down", "j":
|
||||
m.checklist.Move(1)
|
||||
case " ":
|
||||
m.checklist.ToggleCursor()
|
||||
case "a":
|
||||
m.checklist.PickAll()
|
||||
case "n":
|
||||
m.checklist.PickNone()
|
||||
case "i":
|
||||
m.checklist.PickInvert()
|
||||
case "enter":
|
||||
if m.checklist.PickedCount() == 0 {
|
||||
// Nothing picked — nudge by snapping to claude-code as the
|
||||
// load-bearing default rather than refusing to advance.
|
||||
for i := range m.checklist.Items {
|
||||
if m.checklist.Items[i].Label == agentLabel("claude-code") {
|
||||
m.checklist.Items[i].Picked = true
|
||||
}
|
||||
}
|
||||
}
|
||||
m.step = stepOptions
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *initWizardModel) handleOptionsKey(k tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch k.String() {
|
||||
case "up", "k":
|
||||
m.options.Move(-1)
|
||||
case "down", "j":
|
||||
m.options.Move(1)
|
||||
case " ", "enter":
|
||||
// Space toggles toggles, advances selects, and on the last row falls
|
||||
// through to "next step" — but only when the cursor is on a Toggle.
|
||||
// Selects use ←/→.
|
||||
if !m.options.ToggleCursor() {
|
||||
// Select row: ←/→ behaviour; enter still advances when nothing
|
||||
// is highlighted to flip.
|
||||
if k.String() == "enter" {
|
||||
m.step = stepConfirm
|
||||
}
|
||||
}
|
||||
case "left", "h":
|
||||
m.options.CycleCursor(-1)
|
||||
case "right", "l":
|
||||
m.options.CycleCursor(1)
|
||||
case "tab", "N":
|
||||
m.step = stepConfirm
|
||||
case "backspace":
|
||||
m.step = stepAgents
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *initWizardModel) handleConfirmKey(k tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch k.String() {
|
||||
case "enter", "y", "Y":
|
||||
m.collectChoices()
|
||||
m.confirmed = true
|
||||
return m, tea.Quit
|
||||
case "backspace", "b":
|
||||
m.step = stepOptions
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// collectChoices pulls the wizard's pick state out of the widgets and into
|
||||
// the flat fields runInit reads after the model exits. Called once on
|
||||
// confirmation so the wizard's internal state can change without leaking.
|
||||
func (m *initWizardModel) collectChoices() {
|
||||
m.pickedAgents = nil
|
||||
for _, it := range m.checklist.Items {
|
||||
if !it.Picked {
|
||||
continue
|
||||
}
|
||||
// Round-trip label → slug; preserve registration order.
|
||||
for slug, label := range agentLabels {
|
||||
if label == it.Label {
|
||||
m.pickedAgents = append(m.pickedAgents, slug)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
sort.Strings(m.pickedAgents)
|
||||
|
||||
if len(m.options.Toggles) >= 3 {
|
||||
m.hooks = m.options.Toggles[0].On
|
||||
m.analyze = m.options.Toggles[1].On
|
||||
m.skills = m.options.Toggles[2].On
|
||||
}
|
||||
if m.showTelemetry && len(m.options.Toggles) >= 4 {
|
||||
m.telemetry = m.options.Toggles[3].On
|
||||
}
|
||||
if len(m.options.Selects) >= 1 {
|
||||
m.hookMode = m.options.Selects[0].Value()
|
||||
}
|
||||
}
|
||||
|
||||
// View renders the current step. Layout: banner (always), step-line, body,
|
||||
// hint footer. Each step's body is rendered by its own helper so the View
|
||||
// stays a thin switch.
|
||||
func (m *initWizardModel) View() string {
|
||||
if m.step == stepCancelled {
|
||||
return progress.StyleHint.Render(" cancelled — no changes made.\n")
|
||||
}
|
||||
|
||||
title := m.title
|
||||
if title == "" {
|
||||
title = "gortex init"
|
||||
}
|
||||
banner := tui.Banner{
|
||||
Title: title,
|
||||
Subtitle: subtitleForStep(m.step, m.rootPath),
|
||||
Tick: m.tick,
|
||||
}.Render()
|
||||
|
||||
body := ""
|
||||
hint := ""
|
||||
step := 0
|
||||
heading := ""
|
||||
switch m.step {
|
||||
case stepAgents:
|
||||
step, heading = 1, "Pick coding assistants"
|
||||
body = m.checklist.Render(10, 18)
|
||||
hint = tui.KeyHint("↑/↓ move", "space toggle", "a all · n none · i invert", "enter next", "q quit")
|
||||
case stepOptions:
|
||||
step, heading = 2, "Configure options"
|
||||
body = m.options.Render()
|
||||
hint = tui.KeyHint("↑/↓ row", "space/enter toggle", "←/→ cycle select", "tab next", "backspace prev")
|
||||
case stepConfirm:
|
||||
step, heading = 3, "Confirm"
|
||||
body = m.renderConfirmSummary()
|
||||
hint = tui.KeyHint("enter run", "backspace prev", "q quit")
|
||||
}
|
||||
|
||||
stepLine := ""
|
||||
if heading != "" {
|
||||
stepLine = tui.StepLine(step, 3, heading)
|
||||
}
|
||||
|
||||
parts := []string{banner, "", stepLine, "", body, "", hint}
|
||||
return lipgloss.JoinVertical(lipgloss.Left, parts...) + "\n"
|
||||
}
|
||||
|
||||
func subtitleForStep(s wizardStep, root string) string {
|
||||
switch s {
|
||||
case stepAgents:
|
||||
// Use "this repo" when the path is "." or empty — the literal dot
|
||||
// reads awkwardly in the sentence ("wire ..").
|
||||
target := root
|
||||
if target == "" || target == "." {
|
||||
target = "this repo"
|
||||
}
|
||||
return "Welcome — let's wire " + target + "."
|
||||
case stepOptions:
|
||||
return "Tune the integration."
|
||||
case stepConfirm:
|
||||
return "Review and run."
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// renderConfirmSummary draws a card-style block listing every choice so the
|
||||
// user can verify before any disk write happens.
|
||||
func (m *initWizardModel) renderConfirmSummary() string {
|
||||
var sb strings.Builder
|
||||
|
||||
sb.WriteString(progress.Heading("agents", fmt.Sprintf("%d", m.checklist.PickedCount())))
|
||||
sb.WriteString("\n")
|
||||
picked := m.collectPickedLabels()
|
||||
if len(picked) == 0 {
|
||||
sb.WriteString(progress.StyleHint.Render(" (none — wizard will fall back to Claude Code)"))
|
||||
} else {
|
||||
sb.WriteString(" ")
|
||||
sb.WriteString(progress.Chips(picked, 64))
|
||||
}
|
||||
sb.WriteString("\n\n")
|
||||
|
||||
sb.WriteString(progress.Heading("options"))
|
||||
sb.WriteString("\n")
|
||||
sb.WriteString(progress.Row("Hooks", chipBool(m.options.Toggles[0].On), 18))
|
||||
sb.WriteString("\n")
|
||||
sb.WriteString(progress.Row("Hook posture", m.options.Selects[0].Value(), 18))
|
||||
sb.WriteString("\n")
|
||||
sb.WriteString(progress.Row("Analyze repo", chipBool(m.options.Toggles[1].On), 18))
|
||||
sb.WriteString("\n")
|
||||
sb.WriteString(progress.Row("Generate skills", chipBool(m.options.Toggles[2].On), 18))
|
||||
sb.WriteString("\n\n")
|
||||
|
||||
sb.WriteString(progress.StyleHint.Render(" press enter to apply these changes — files are written atomically and dry-run is available via --dry-run."))
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func (m *initWizardModel) collectPickedLabels() []string {
|
||||
out := make([]string, 0, len(m.checklist.Items))
|
||||
for _, it := range m.checklist.Items {
|
||||
if it.Picked {
|
||||
out = append(out, it.Label)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func chipBool(on bool) string {
|
||||
if on {
|
||||
return progress.StyleOK.Render("on")
|
||||
}
|
||||
return progress.StyleHint.Render("off")
|
||||
}
|
||||
|
||||
// cobraInitState carries the per-run inputs the wizard needs that aren't
|
||||
// already on the package-level init* globals. Keeping it as a struct rather
|
||||
// than passing scalars leaves room to grow without touching every callsite.
|
||||
type cobraInitState struct {
|
||||
rootPath string
|
||||
}
|
||||
|
||||
// runInitWizard launches the bubbletea wizard, blocks until the user either
|
||||
// confirms or cancels, and (on confirmation) mutates the init* globals to
|
||||
// reflect the wizard's picks. Returns cancelled=true when the user aborted
|
||||
// (Ctrl-C / q / Esc) so the caller can short-circuit before any disk write.
|
||||
//
|
||||
// The wizard writes to cmd's stderr — alt-screen mode keeps the wizard's
|
||||
// frame on its own surface, so when the user confirms the terminal scrollback
|
||||
// is clean before the dashboard takes over.
|
||||
func runInitWizard(cmd interface {
|
||||
ErrOrStderr() io.Writer
|
||||
}, st *cobraInitState) (bool, error) {
|
||||
registry := buildRegistry()
|
||||
registered := registry.All()
|
||||
|
||||
home, _ := osUserHomeDir()
|
||||
env := agents.Env{
|
||||
Root: st.rootPath,
|
||||
Home: home,
|
||||
Mode: agents.ModeProject,
|
||||
InstallHooks: initInstallHooks,
|
||||
HookMode: initHookMode,
|
||||
}
|
||||
detected := detectAdapters(registered, env)
|
||||
|
||||
defaults := initDefaults{
|
||||
hooks: initInstallHooks,
|
||||
hookMode: firstNonEmpty(initHookMode, "deny"),
|
||||
analyze: initAnalyze,
|
||||
skills: initSkills,
|
||||
}
|
||||
model := newInitWizardModel(st.rootPath, registered, detected, defaults)
|
||||
|
||||
prog := tea.NewProgram(model,
|
||||
tea.WithOutput(cmd.ErrOrStderr()),
|
||||
tea.WithAltScreen(),
|
||||
tea.WithoutSignalHandler(),
|
||||
)
|
||||
finalModel, err := prog.Run()
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("wizard: %w", err)
|
||||
}
|
||||
m, ok := finalModel.(*initWizardModel)
|
||||
if !ok || m.cancelled {
|
||||
return true, nil
|
||||
}
|
||||
if !m.confirmed {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Pour wizard picks into the init* globals so the orchestration code
|
||||
// downstream sees them as if they came from CLI flags.
|
||||
initInstallHooks = m.hooks
|
||||
initHookMode = m.hookMode
|
||||
initAnalyze = m.analyze
|
||||
initSkills = m.skills
|
||||
if len(m.pickedAgents) > 0 {
|
||||
initAgents = strings.Join(m.pickedAgents, ",")
|
||||
}
|
||||
wizardSelectedDashboard = true
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// osUserHomeDir and firstNonEmpty already live in daemon_status_tui.go so we
|
||||
// reuse them. Kept here as a comment marker because grepping for the seam
|
||||
// from this file is the more natural reading order.
|
||||
Reference in New Issue
Block a user