|
7 | 7 | "fmt" |
8 | 8 | "os" |
9 | 9 | "os/exec" |
| 10 | + "runtime" |
10 | 11 | "strings" |
11 | 12 |
|
12 | 13 | "github.com/morluto/gitcontribute/internal/cli" |
@@ -61,17 +62,18 @@ func (s *Service) setup(ctx context.Context, opts cli.SetupOptions, observer cli |
61 | 62 | } |
62 | 63 |
|
63 | 64 | type setupRun struct { |
64 | | - service *Service |
65 | | - ctx context.Context |
66 | | - opts cli.SetupOptions |
67 | | - observer cli.SetupObserver |
68 | | - operation clientsetup.Operation |
69 | | - report *cli.SetupReport |
70 | | - clientOptions clientsetup.Options |
71 | | - clientReport clientsetup.Report |
72 | | - managedRuntime string |
73 | | - mcpCommandPending bool |
74 | | - configurationOK bool |
| 65 | + service *Service |
| 66 | + ctx context.Context |
| 67 | + opts cli.SetupOptions |
| 68 | + observer cli.SetupObserver |
| 69 | + operation clientsetup.Operation |
| 70 | + report *cli.SetupReport |
| 71 | + clientOptions clientsetup.Options |
| 72 | + clientReport clientsetup.Report |
| 73 | + managedRuntime string |
| 74 | + installedExecutable string |
| 75 | + mcpCommandPending bool |
| 76 | + configurationOK bool |
75 | 77 | } |
76 | 78 |
|
77 | 79 | func (s *Service) newSetupRun(ctx context.Context, opts cli.SetupOptions, observer cli.SetupObserver) (*setupRun, error) { |
@@ -173,6 +175,7 @@ func (r *setupRun) setupRuntime() error { |
173 | 175 | step, executable := installCLI(r.ctx, r.opts.Version, r.opts.DryRun) |
174 | 176 | r.report.Steps = append(r.report.Steps, step) |
175 | 177 | setupCompleted(r.observer, step) |
| 178 | + r.installedExecutable = executable |
176 | 179 | if executable == "" { |
177 | 180 | if !r.opts.DryRun { |
178 | 181 | r.mcpCommandPending = false |
@@ -203,6 +206,7 @@ func (r *setupRun) installManagedRuntime() error { |
203 | 206 | return nil |
204 | 207 | } |
205 | 208 | setupStarted(r.observer, cli.SetupPhaseMCPRuntime) |
| 209 | + r.installedExecutable = r.managedRuntime |
206 | 210 | source := r.opts.Executable |
207 | 211 | if source == "" { |
208 | 212 | var err error |
@@ -333,6 +337,9 @@ func (r *setupRun) appendClientResults() { |
333 | 337 | for _, result := range r.clientReport.Results { |
334 | 338 | step := cli.SetupStep{Name: string(result.Client), Path: result.Path, Status: result.Status, Message: result.Error} |
335 | 339 | r.report.Steps = append(r.report.Steps, step) |
| 340 | + if !r.opts.DryRun && r.operation == clientsetup.Configure && (result.Status == "configured" || result.Status == "updated") { |
| 341 | + r.report.RestartClients = append(r.report.RestartClients, string(result.Client)) |
| 342 | + } |
336 | 343 | setupCompleted(r.observer, step) |
337 | 344 | } |
338 | 345 | } |
@@ -362,34 +369,77 @@ func (r *setupRun) verify() { |
362 | 369 | return |
363 | 370 | } |
364 | 371 | setupStarted(r.observer, cli.SetupPhaseVerification) |
365 | | - diagnostics, err := r.service.doctor(r.ctx, false) |
366 | 372 | step := cli.SetupStep{Name: "verification", Status: "verified"} |
367 | | - if err != nil || diagnostics == nil || !diagnostics.Healthy { |
| 373 | + if err := r.verifyAppliedSetup(); err != nil { |
368 | 374 | step.Status = "failed" |
369 | | - if err != nil { |
370 | | - step.Message = err.Error() |
371 | | - } else { |
372 | | - step.Message = setupVerificationFailure(diagnostics) |
373 | | - } |
| 375 | + step.Message = err.Error() |
374 | 376 | } |
375 | 377 | r.report.Steps = append(r.report.Steps, step) |
376 | 378 | setupCompleted(r.observer, step) |
377 | 379 | } |
378 | 380 |
|
379 | | -func setupVerificationFailure(diagnostics *cli.DoctorResult) string { |
380 | | - if diagnostics == nil { |
381 | | - return "required installation checks failed" |
382 | | - } |
383 | | - failures := make([]string, 0, len(diagnostics.Checks)) |
384 | | - for _, check := range diagnostics.Checks { |
385 | | - if check.Required && check.Status == "error" { |
386 | | - failures = append(failures, check.Name+": "+check.Message) |
| 381 | +func (r *setupRun) verifyAppliedSetup() error { |
| 382 | + failures := make([]string, 0, 5) |
| 383 | + if executableErr := verifySetupExecutable(r.installedExecutable); executableErr != nil { |
| 384 | + failures = append(failures, "executable: "+executableErr.Error()) |
| 385 | + } |
| 386 | + c, err := r.service.openCorpus(r.ctx) |
| 387 | + if err != nil { |
| 388 | + failures = append(failures, "database: "+err.Error()) |
| 389 | + } else { |
| 390 | + current, target, schemaErr := c.SchemaVersions(r.ctx) |
| 391 | + if schemaErr != nil { |
| 392 | + failures = append(failures, "schema: "+schemaErr.Error()) |
| 393 | + } else if current != target { |
| 394 | + failures = append(failures, fmt.Sprintf("schema: database version %d does not match expected version %d", current, target)) |
| 395 | + } |
| 396 | + integrityCtx, cancel := context.WithTimeout(r.ctx, databaseIntegrityTimeout) |
| 397 | + integrityErr := c.CheckIntegrity(integrityCtx) |
| 398 | + cancel() |
| 399 | + if integrityErr != nil { |
| 400 | + failures = append(failures, "database_integrity: "+integrityErr.Error()) |
| 401 | + } |
| 402 | + } |
| 403 | + if gitErr := commandAvailable(r.ctx, "git", "--version"); gitErr != nil { |
| 404 | + failures = append(failures, "git: "+redactDiagnostic(gitErr.Error())) |
| 405 | + } |
| 406 | + if r.configuresClients() { |
| 407 | + opts := r.clientOptions |
| 408 | + opts.DryRun = true |
| 409 | + report, clientErr := clientsetup.Run(opts) |
| 410 | + if clientErr != nil { |
| 411 | + failures = append(failures, "mcp registration: "+clientErr.Error()) |
| 412 | + } else { |
| 413 | + for _, result := range report.Results { |
| 414 | + if result.Error != "" { |
| 415 | + failures = append(failures, string(result.Client)+": "+result.Error) |
| 416 | + } else if result.Status != "already configured" { |
| 417 | + failures = append(failures, fmt.Sprintf("%s: registration does not match the configured MCP command", result.Client)) |
| 418 | + } |
| 419 | + } |
387 | 420 | } |
388 | 421 | } |
389 | 422 | if len(failures) == 0 { |
390 | | - return "required installation checks failed" |
| 423 | + return nil |
| 424 | + } |
| 425 | + return errors.New(strings.Join(failures, "; ")) |
| 426 | +} |
| 427 | + |
| 428 | +func verifySetupExecutable(path string) error { |
| 429 | + if strings.TrimSpace(path) == "" { |
| 430 | + return errors.New("installed command path is unavailable") |
391 | 431 | } |
392 | | - return strings.Join(failures, "; ") |
| 432 | + info, err := os.Stat(path) |
| 433 | + if err != nil { |
| 434 | + return fmt.Errorf("inspect installed command: %w", err) |
| 435 | + } |
| 436 | + if !info.Mode().IsRegular() { |
| 437 | + return fmt.Errorf("installed command is not a regular file: %s", path) |
| 438 | + } |
| 439 | + if runtime.GOOS != "windows" && info.Mode().Perm()&0o111 == 0 { |
| 440 | + return fmt.Errorf("installed command is not executable: %s", path) |
| 441 | + } |
| 442 | + return nil |
393 | 443 | } |
394 | 444 |
|
395 | 445 | func setupStarted(observer cli.SetupObserver, phase cli.SetupPhase) { |
|
0 commit comments