a06f331eb8
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
532 lines
18 KiB
Go
532 lines
18 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"github.com/spf13/cobra"
|
|
"go.uber.org/zap"
|
|
|
|
"github.com/zzet/gortex/internal/agents"
|
|
"github.com/zzet/gortex/internal/agents/aider"
|
|
"github.com/zzet/gortex/internal/agents/antigravity"
|
|
"github.com/zzet/gortex/internal/agents/claudecode"
|
|
"github.com/zzet/gortex/internal/agents/cline"
|
|
"github.com/zzet/gortex/internal/agents/codex"
|
|
"github.com/zzet/gortex/internal/agents/continuedev"
|
|
"github.com/zzet/gortex/internal/agents/cursor"
|
|
"github.com/zzet/gortex/internal/agents/gemini"
|
|
"github.com/zzet/gortex/internal/agents/hermes"
|
|
"github.com/zzet/gortex/internal/agents/kilocode"
|
|
"github.com/zzet/gortex/internal/agents/kimi"
|
|
"github.com/zzet/gortex/internal/agents/kiro"
|
|
"github.com/zzet/gortex/internal/agents/ohmypi"
|
|
"github.com/zzet/gortex/internal/agents/openclaw"
|
|
"github.com/zzet/gortex/internal/agents/opencode"
|
|
"github.com/zzet/gortex/internal/agents/pi"
|
|
"github.com/zzet/gortex/internal/agents/vscode"
|
|
"github.com/zzet/gortex/internal/agents/windsurf"
|
|
"github.com/zzet/gortex/internal/agents/zed"
|
|
"github.com/zzet/gortex/internal/claudemd"
|
|
"github.com/zzet/gortex/internal/config"
|
|
"github.com/zzet/gortex/internal/graph"
|
|
"github.com/zzet/gortex/internal/graph/store_sqlite"
|
|
"github.com/zzet/gortex/internal/indexer"
|
|
"github.com/zzet/gortex/internal/parser"
|
|
"github.com/zzet/gortex/internal/parser/languages"
|
|
"github.com/zzet/gortex/internal/platform"
|
|
"github.com/zzet/gortex/internal/progress"
|
|
"github.com/zzet/gortex/internal/query"
|
|
genskills "github.com/zzet/gortex/internal/skills"
|
|
"github.com/zzet/gortex/internal/telemetry"
|
|
"github.com/zzet/gortex/internal/workspace"
|
|
)
|
|
|
|
// Per-flag globals. Behaviour flags stay separate from UX flags so
|
|
// the wizard and the orchestrator read them without cross-pollution.
|
|
var (
|
|
// Core behaviour
|
|
initAnalyze bool
|
|
initInstallHooks = true
|
|
initNoHooks bool
|
|
initHooksOnly bool
|
|
initHookMode string
|
|
|
|
// Community skills generation (replaces the old `gortex skills`).
|
|
initSkills = true
|
|
initNoSkills bool
|
|
initSkillsMinSize int
|
|
initSkillsMaxSkills int
|
|
|
|
// Non-interactive / reporting knobs
|
|
initYes bool
|
|
initInteractive bool
|
|
initAgents string
|
|
initAgentsSkip string
|
|
initJSON bool
|
|
initDryRun bool
|
|
initDryRunIntake bool
|
|
initForce bool
|
|
)
|
|
|
|
var initCmd = &cobra.Command{
|
|
Use: "init [path]",
|
|
Short: "Wire Gortex into the current repository for every detected AI coding assistant",
|
|
Long: `Configure Gortex for this repository: per-repo MCP and instruction files for each
|
|
detected assistant, optional agent hooks, community-derived routing, and (with --analyze)
|
|
a richer CLAUDE.md overview.
|
|
|
|
For one-time machine-wide setup (user MCP config, user skills /
|
|
Knowledge Items, user hooks), run ` + "`gortex install`" + ` once.`,
|
|
Args: cobra.MaximumNArgs(1),
|
|
RunE: runInit,
|
|
}
|
|
|
|
func init() {
|
|
initCmd.Flags().BoolVar(&initAnalyze, "analyze", false, "index the repo to generate a richer CLAUDE.md with codebase overview")
|
|
initCmd.Flags().BoolVar(&initInstallHooks, "hooks", true, "install supported agent hooks; use --no-hooks to skip")
|
|
initCmd.Flags().BoolVar(&initNoHooks, "no-hooks", false, "skip installing supported agent hooks (inverse of --hooks)")
|
|
initCmd.Flags().BoolVar(&initHooksOnly, "hooks-only", false, "only install/update supported agent hooks, skip everything else")
|
|
initCmd.Flags().StringVar(&initHookMode, "hook-mode", "deny",
|
|
"hook posture: 'deny' (PreToolUse redirects Grep/Glob/Read of indexed source) or 'enrich' "+
|
|
"(PreToolUse never denies; PostToolUse appends graph context after the tool runs)")
|
|
|
|
initCmd.Flags().BoolVar(&initSkills, "skills", true, "generate per-community routing + SKILL.md files; use --no-skills to skip")
|
|
initCmd.Flags().BoolVar(&initNoSkills, "no-skills", false, "skip community-skill generation (inverse of --skills)")
|
|
initCmd.Flags().IntVar(&initSkillsMinSize, "skills-min-size", 3, "minimum community size to generate a skill")
|
|
initCmd.Flags().IntVar(&initSkillsMaxSkills, "skills-max", 20, "maximum number of skills to generate")
|
|
|
|
initCmd.Flags().BoolVarP(&initYes, "yes", "y", false, "skip the interactive wizard (implied when stdin is not a TTY)")
|
|
initCmd.Flags().BoolVarP(&initInteractive, "interactive", "i", false, "force the full-screen wizard (banner + agent checklist + options panel + live dashboard) even when stdin would normally fall through to defaults")
|
|
initCmd.Flags().StringVar(&initAgents, "agents", "", "comma-separated list of agents to configure ('auto' means every registered adapter)")
|
|
initCmd.Flags().StringVar(&initAgentsSkip, "agents-skip", "", "comma-separated list of agents to skip (composable with --agents)")
|
|
initCmd.Flags().BoolVar(&initJSON, "json", false, "emit a structured JSON report on stdout")
|
|
initCmd.Flags().BoolVar(&initDryRun, "dry-run", false, "plan writes without modifying disk")
|
|
initCmd.Flags().BoolVar(&initDryRunIntake, "dry-run-intake", false, "emit a privacy-safe corpus intake manifest and exit before parsing or writing")
|
|
initCmd.Flags().BoolVar(&initForce, "force", false, "overwrite keys we would otherwise preserve during a merge")
|
|
|
|
rootCmd.AddCommand(initCmd)
|
|
}
|
|
|
|
// buildRegistry wires up every registered adapter. Registration
|
|
// order is also execution order — Claude Code first (hooks-heavy),
|
|
// then Cursor (common MCP + project rules), then alphabetical for
|
|
// stable --json.
|
|
func buildRegistry() *agents.Registry {
|
|
r := agents.NewRegistry()
|
|
r.Register(claudecode.New())
|
|
r.Register(cursor.New())
|
|
r.Register(aider.New())
|
|
r.Register(antigravity.New())
|
|
r.Register(cline.New())
|
|
r.Register(codex.New())
|
|
r.Register(continuedev.New())
|
|
r.Register(gemini.New())
|
|
r.Register(hermes.New())
|
|
r.Register(kimi.New())
|
|
r.Register(kilocode.New())
|
|
r.Register(kiro.New())
|
|
r.Register(ohmypi.New())
|
|
r.Register(opencode.New())
|
|
r.Register(openclaw.New())
|
|
r.Register(pi.New())
|
|
r.Register(vscode.New())
|
|
r.Register(windsurf.New())
|
|
r.Register(zed.New())
|
|
return r
|
|
}
|
|
|
|
func runInit(cmd *cobra.Command, args []string) (err error) {
|
|
root := "."
|
|
if len(args) > 0 {
|
|
root = args[0]
|
|
}
|
|
|
|
if initNoHooks {
|
|
initInstallHooks = false
|
|
}
|
|
if initNoSkills {
|
|
initSkills = false
|
|
}
|
|
|
|
// Interactive wizard. Two flavours:
|
|
// * --interactive (-i) or a TTY without --yes: launch the full
|
|
// bubbletea wizard (banner + agent checklist + options panel +
|
|
// confirm). The wizard owns every decision; if the user cancels
|
|
// we return without touching disk.
|
|
// * Non-TTY or --yes: fall through to the legacy bufio prompt for
|
|
// the single hooks question so CI scripts that piped a yes/no
|
|
// line into `gortex init` keep working.
|
|
wantWizard := initInteractive || (!initDryRunIntake && !initYes && !initHooksOnly && progress.IsTTY(cmd.ErrOrStderr()) && isInteractive())
|
|
if wantWizard {
|
|
cancelled, err := runInitWizard(cmd, &cobraInitState{
|
|
rootPath: root,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if cancelled {
|
|
fmt.Fprintln(cmd.ErrOrStderr(), " cancelled — no changes made.")
|
|
return nil
|
|
}
|
|
} else if !initYes && !initHooksOnly && isInteractive() {
|
|
hooksPreset := cmd.Flags().Changed("hooks") || cmd.Flags().Changed("no-hooks")
|
|
if choice, ran := runInteractiveInit(os.Stdin, cmd.ErrOrStderr(), hooksPreset); ran {
|
|
if !hooksPreset {
|
|
initInstallHooks = choice.Hooks
|
|
}
|
|
}
|
|
}
|
|
|
|
absRoot, err := filepath.Abs(root)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if initDryRunIntake {
|
|
return emitInitDryRunIntake(cmd, absRoot)
|
|
}
|
|
|
|
// Bind this directory as a single-project entry point so the MCP
|
|
// server can resolve it without --hooks-only setups, daemon-less
|
|
// clients, or future runs needing manual setup. The marker is the
|
|
// `.gortex/` directory itself (see internal/workspace.IndexDir);
|
|
// nothing else needs to live inside it.
|
|
if !initDryRun && !initHooksOnly {
|
|
if err := ensureProjectMarker(absRoot, cmd.ErrOrStderr()); err != nil {
|
|
fmt.Fprintf(cmd.ErrOrStderr(), "[gortex init] warning: could not create %s: %v\n", workspace.IndexDir, err)
|
|
}
|
|
}
|
|
|
|
// --hooks-only short-circuit: install/heal supported agent hooks
|
|
// and exit. Everything else is a no-op.
|
|
if initHooksOnly {
|
|
return runInitHooksOnly(cmd, absRoot)
|
|
}
|
|
|
|
realStderr := cmd.ErrOrStderr()
|
|
prog := selectInitProgress(realStderr)
|
|
prog.Start("Initializing gortex")
|
|
|
|
// Buffer chatty adapter logs while the animation is running. On success
|
|
// drop them — the summary already conveys outcome. On failure replay
|
|
// them so the user can debug. env.Stderr is captured AFTER the swap so
|
|
// adapters write into the buffer too.
|
|
var (
|
|
chatter bytes.Buffer
|
|
results []*agents.Result
|
|
opts agents.ApplyOpts
|
|
)
|
|
captured := prog.Enabled()
|
|
if captured {
|
|
cmd.SetErr(&chatter)
|
|
}
|
|
|
|
home, _ := os.UserHomeDir()
|
|
env := agents.Env{
|
|
Root: absRoot,
|
|
Home: home,
|
|
HookCommand: claudecode.ResolveHookCommand(cmd.ErrOrStderr()),
|
|
Mode: agents.ModeProject,
|
|
InstallHooks: initInstallHooks,
|
|
HookMode: initHookMode,
|
|
AnalyzeRepo: initAnalyze,
|
|
Stderr: cmd.ErrOrStderr(),
|
|
}
|
|
defer func() {
|
|
if err != nil {
|
|
prog.Fail(err)
|
|
} else {
|
|
prog.Done()
|
|
}
|
|
if captured {
|
|
cmd.SetErr(realStderr)
|
|
if err != nil {
|
|
_, _ = io.Copy(realStderr, &chatter)
|
|
}
|
|
}
|
|
if err == nil {
|
|
emitHumanSummary(realStderr, results, opts)
|
|
}
|
|
}()
|
|
|
|
// Indexing powers both --analyze (codebase overview in
|
|
// CLAUDE.md) and --skills (community routing in every per-repo
|
|
// instructions surface). Index once, feed both.
|
|
needIndex := initAnalyze || initSkills
|
|
if needIndex {
|
|
prog.Stage(stageIndex, absRoot)
|
|
ctx := progress.WithReporter(context.Background(), prog.Reporter())
|
|
// Silence zap info logs from the indexer when the surface is live;
|
|
// the dashboard / spinner already shows the same stage transitions.
|
|
var idxLogger *zap.Logger
|
|
if prog.Enabled() {
|
|
idxLogger = zap.NewNop()
|
|
}
|
|
g, cleanup, idxErr := indexRepoForInit(ctx, absRoot, idxLogger)
|
|
if idxErr != nil {
|
|
fmt.Fprintf(cmd.ErrOrStderr(), "[gortex init] indexing failed: %v — proceeding without analysis/skills\n", idxErr)
|
|
} else {
|
|
defer cleanup()
|
|
prog.StageDone(stageIndex, "")
|
|
if initAnalyze {
|
|
prog.Stage(stageAnalyze, "")
|
|
eng := query.NewEngine(g)
|
|
env.AnalyzedOverview = claudemd.Generate(eng, 180)
|
|
prog.StageDone(stageAnalyze, "")
|
|
}
|
|
if initSkills {
|
|
prog.Stage(stageSkills, "")
|
|
generated, routing := genskills.Build(g, genskills.BuildOpts{
|
|
MinSize: initSkillsMinSize,
|
|
MaxSkills: initSkillsMaxSkills,
|
|
})
|
|
if len(generated) > 0 {
|
|
env.GeneratedSkills = toEnvSkills(generated)
|
|
env.SkillsRouting = routing
|
|
prog.StageDone(stageSkills, fmt.Sprintf("%d community skill(s)", len(generated)))
|
|
} else {
|
|
prog.StageDone(stageSkills, fmt.Sprintf("no communities large enough (min-size: %d)", initSkillsMinSize))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
prog.Stage(stageAdapters, "")
|
|
registry := buildRegistry()
|
|
selected, err := registry.Filter(initAgents, initAgentsSkip)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
opts = agents.ApplyOpts{DryRun: initDryRun, Force: initForce}
|
|
results = make([]*agents.Result, 0, len(selected))
|
|
for _, a := range selected {
|
|
prog.Sub(a.Name())
|
|
r, applyErr := a.Apply(env, opts)
|
|
if applyErr != nil {
|
|
if a.Name() == claudecode.Name {
|
|
return fmt.Errorf("%s: %w", a.Name(), applyErr)
|
|
}
|
|
fmt.Fprintf(cmd.ErrOrStderr(), "[gortex init] warning: %s setup failed: %v\n", a.Name(), applyErr)
|
|
}
|
|
if r != nil {
|
|
results = append(results, r)
|
|
}
|
|
}
|
|
prog.StageDone(stageAdapters, fmt.Sprintf("%d adapter(s) configured", len(results)))
|
|
|
|
// Always update Gortex's own global config so the daemon picks
|
|
// up this repo next time it starts (harmless when no daemon).
|
|
if !initDryRun {
|
|
if err := ensureGlobalConfig(absRoot); err != nil {
|
|
fmt.Fprintf(cmd.ErrOrStderr(), "[gortex init] warning: could not update global config: %v\n", err)
|
|
}
|
|
}
|
|
|
|
// One-time opt-in telemetry notice. Telemetry is off by default; this
|
|
// only informs and records the default choice so it shows at most once.
|
|
// Goes to stderr, so it never pollutes --json output. Skipped on dry run.
|
|
if !initDryRun {
|
|
telemetry.MaybeFirstRunNotice(platform.TelemetryDir(), cmd.ErrOrStderr())
|
|
}
|
|
|
|
if initJSON {
|
|
if err := emitJSONReport(cmd.OutOrStdout(), results, opts); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func runInitHooksOnly(cmd *cobra.Command, absRoot string) error {
|
|
opts := agents.ApplyOpts{DryRun: initDryRun, Force: initForce}
|
|
|
|
registry := buildRegistry()
|
|
selected, err := registry.Filter(initAgents, initAgentsSkip)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
selectedAgents := make(map[string]bool, len(selected))
|
|
for _, a := range selected {
|
|
selectedAgents[a.Name()] = true
|
|
}
|
|
|
|
if selectedAgents[claudecode.Name] {
|
|
settingsPath := filepath.Join(absRoot, ".claude", "settings.local.json")
|
|
claudeAction, err := claudecode.InstallHookWithMode(cmd.ErrOrStderr(), settingsPath, initHookMode, opts)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fmt.Fprintf(cmd.ErrOrStderr(), "[gortex init --hooks-only] %s %s\n", claudeAction.Action, claudeAction.Path)
|
|
}
|
|
|
|
home, _ := os.UserHomeDir()
|
|
if home == "" || !selectedAgents[codex.Name] {
|
|
return nil
|
|
}
|
|
env := agents.Env{
|
|
Root: absRoot,
|
|
Home: home,
|
|
HookCommand: claudecode.ResolveHookCommand(cmd.ErrOrStderr()),
|
|
Mode: agents.ModeProject,
|
|
InstallHooks: true,
|
|
HookMode: initHookMode,
|
|
Stderr: cmd.ErrOrStderr(),
|
|
}
|
|
detected, _ := codex.New().Detect(env)
|
|
if !detected {
|
|
return nil
|
|
}
|
|
|
|
codexPath := filepath.Join(home, ".codex", "config.toml")
|
|
codexAction, err := codex.InstallHooksOnly(cmd.ErrOrStderr(), codexPath, env, opts)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fmt.Fprintf(cmd.ErrOrStderr(), "[gortex init --hooks-only] %s %s\n", codexAction.Action, codexAction.Path)
|
|
return nil
|
|
}
|
|
|
|
// toEnvSkills converts the skills generator's output into the
|
|
// agents.GeneratedSkill payload carried on Env. The two shapes are
|
|
// identical today; the mirror keeps the agents package free of the
|
|
// internal/skills dependency.
|
|
func toEnvSkills(src []genskills.GeneratedSkill) []agents.GeneratedSkill {
|
|
out := make([]agents.GeneratedSkill, len(src))
|
|
for i, s := range src {
|
|
out[i] = agents.GeneratedSkill{
|
|
CommunityID: s.CommunityID,
|
|
Label: s.Label,
|
|
DirName: s.DirName,
|
|
Content: s.Content,
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// indexRepoForInit runs a one-shot index of the repo. Kept inside
|
|
// cmd/gortex (not an adapter) because the indexer pulls in many
|
|
// gortex-internal packages we'd rather not leak into internal/agents.
|
|
// The ctx carries a progress.Reporter so the caller's spinner picks up
|
|
// stage transitions ("walking files", "parsing", …) as sub-status.
|
|
// Pass a Nop logger when running under an animated spinner so structured
|
|
// info logs don't duplicate the mesh frame.
|
|
//
|
|
// It indexes into a temporary on-disk sqlite store rather than an
|
|
// all-in-memory graph: nodes persist per file and the content sink leans
|
|
// document / section text to disk, so a content-heavy repo (a RAG corpus
|
|
// of decks, spreadsheets, and dataset shards) can't pin the whole
|
|
// post-parse graph in RAM and OOM `gortex init` (#120). The store inherits
|
|
// the indexer's shadow / byte-budget guards. The returned cleanup closes
|
|
// the store and removes the temp dir; callers MUST call it once they are
|
|
// done reading the returned graph.
|
|
func indexRepoForInit(ctx context.Context, root string, logger *zap.Logger) (graph.Store, func(), error) {
|
|
if logger == nil {
|
|
logger = newLogger()
|
|
}
|
|
defer func() { _ = logger.Sync() }()
|
|
|
|
cfg, err := config.Load("")
|
|
if err != nil {
|
|
cfg = &config.Config{}
|
|
}
|
|
|
|
tmpDir, err := os.MkdirTemp("", "gortex-init-store-*")
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
st, err := store_sqlite.Open(filepath.Join(tmpDir, "init.sqlite"))
|
|
if err != nil {
|
|
_ = os.RemoveAll(tmpDir)
|
|
return nil, nil, err
|
|
}
|
|
cleanup := func() {
|
|
_ = st.Close()
|
|
_ = os.RemoveAll(tmpDir)
|
|
}
|
|
|
|
reg := parser.NewRegistry()
|
|
languages.RegisterAll(reg)
|
|
|
|
idx := indexer.New(st, reg, cfg.Index, logger)
|
|
if _, ierr := idx.IndexCtx(ctx, root); ierr != nil {
|
|
cleanup()
|
|
return nil, nil, ierr
|
|
}
|
|
return st, cleanup, nil
|
|
}
|
|
|
|
// emitJSONReport writes a single JSON object to w. Shape kept
|
|
// compatible with earlier releases (agents array, dry_run, force)
|
|
// plus a mode discriminator so the install/init outputs are
|
|
// distinguishable.
|
|
func emitJSONReport(w io.Writer, results []*agents.Result, opts agents.ApplyOpts) error {
|
|
payload := map[string]any{
|
|
"dry_run": opts.DryRun,
|
|
"force": opts.Force,
|
|
"mode": "project",
|
|
"agents": results,
|
|
}
|
|
enc := json.NewEncoder(w)
|
|
enc.SetIndent("", " ")
|
|
return enc.Encode(payload)
|
|
}
|
|
|
|
// emitHumanSummary prints the per-agent file counts to stderr.
|
|
func emitHumanSummary(w io.Writer, results []*agents.Result, opts agents.ApplyOpts) {
|
|
emitAgentSummary(w, results, opts, []string{
|
|
"if your editor uses MCP, enable the gortex server there (and reload the window) when tools do not appear after the first init",
|
|
"commit the generated files your team relies on (.mcp.json, .claude/, .cursor/, CLAUDE.md, and other adapter outputs)",
|
|
"run `gortex install` once per machine to wire user-level integration",
|
|
})
|
|
}
|
|
|
|
// ensureProjectMarker creates `.gortex/` at the repo root to hold the
|
|
// project's Gortex config (`.gortex.yaml` and friends). Idempotent: a
|
|
// no-op if the directory already exists. Reports first-time creation to
|
|
// stderr.
|
|
func ensureProjectMarker(root string, w io.Writer) error {
|
|
dir := filepath.Join(root, workspace.IndexDir)
|
|
existed := true
|
|
if _, err := os.Stat(dir); err != nil {
|
|
if !os.IsNotExist(err) {
|
|
return err
|
|
}
|
|
existed = false
|
|
}
|
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
|
return err
|
|
}
|
|
// Keep the project's local index state out of git — write (or heal) a
|
|
// self-scoping .gitignore inside the marker dir so a user never commits it.
|
|
if _, err := workspace.EnsureIndexDirGitignore(dir); err != nil {
|
|
return err
|
|
}
|
|
if !existed {
|
|
fmt.Fprintf(w, "[gortex init] created %s/ to hold this project's Gortex config\n", workspace.IndexDir)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ensureGlobalConfig adds this repo to ~/.gortex/config.yaml
|
|
// so the daemon picks it up on its next restart. Skipped in --dry-run.
|
|
func ensureGlobalConfig(root string) error {
|
|
gc, err := config.LoadGlobal()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := gc.AddRepo(config.RepoEntry{Path: root}); err != nil {
|
|
return err
|
|
}
|
|
if err := gc.Save(); err != nil {
|
|
return err
|
|
}
|
|
fmt.Fprintf(os.Stderr, "[gortex init] updated global config at %s\n", gc.ConfigPath())
|
|
return nil
|
|
}
|