Install links the instruction identified by id into the coding-agent path for (app, level, projectDir), using a symlink with a copy fallback.
(ctx context.Context, id int64, app, level, projectDir string)
| 42 | // Install links the instruction identified by id into the coding-agent path |
| 43 | // for (app, level, projectDir), using a symlink with a copy fallback. |
| 44 | func (s *Store) Install(ctx context.Context, id int64, app, level, projectDir string) (Install, error) { |
| 45 | if err := s.Init(ctx); err != nil { |
| 46 | return Install{}, err |
| 47 | } |
| 48 | current, err := s.Get(ctx, id) |
| 49 | if err != nil { |
| 50 | return Install{}, err |
| 51 | } |
| 52 | |
| 53 | lvl := entities.InstallLevel(level) |
| 54 | if lvl == entities.InstallLevelProject && strings.TrimSpace(projectDir) == "" { |
| 55 | return Install{}, ErrProjectDirRequired |
| 56 | } |
| 57 | if !levelSupported(app, lvl) { |
| 58 | return Install{}, fmt.Errorf("%w: %s does not support %s-level installs", ErrUnsupportedTarget, app, level) |
| 59 | } |
| 60 | |
| 61 | target, err := entities.InstructionPath(app, lvl, projectDir) |
| 62 | if err != nil { |
| 63 | // Distinguish "unsupported" (programmer/input error) from filesystem |
| 64 | // problems like a missing project directory. |
| 65 | return Install{}, err |
| 66 | } |
| 67 | |
| 68 | src := managedFilePath(current.Name) |
| 69 | |
| 70 | if err := s.checkConflict(ctx, target, id); err != nil { |
| 71 | return Install{}, err |
| 72 | } |
| 73 | |
| 74 | if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil { |
| 75 | return Install{}, fmt.Errorf("instructions: mkdir target dir: %w", err) |
| 76 | } |
| 77 | |
| 78 | linkKind := "symlink" |
| 79 | if err := symlinkFunc(src, target); err != nil { |
| 80 | if isPrivilegeError(err) { |
| 81 | if cerr := copyFile(src, target); cerr != nil { |
| 82 | return Install{}, fmt.Errorf("instructions: copy fallback: %w", cerr) |
| 83 | } |
| 84 | linkKind = "copy" |
| 85 | } else { |
| 86 | return Install{}, fmt.Errorf("instructions: symlink: %w", err) |
| 87 | } |
| 88 | } |
| 89 | |
| 90 | db, err := s.open() |
| 91 | if err != nil { |
| 92 | return Install{}, err |
| 93 | } |
| 94 | defer db.Close() |
| 95 | // Remove any prior install row for this (app, level, project_dir) so a |
| 96 | // re-install replaces the old one cleanly. The actual file was already |
| 97 | // backed up by checkConflict above. |
| 98 | _, _ = db.ExecContext(ctx, `DELETE FROM instruction_installs WHERE app = ? AND level = ? AND project_dir = ?`, app, level, projectDir) |
| 99 | |
| 100 | now := time.Now().UTC().Format(rfc) |
| 101 | res, err := db.ExecContext(ctx, `INSERT INTO instruction_installs(instruction_id, app, level, project_dir, target_path, link_kind, created_at) VALUES(?, ?, ?, ?, ?, ?, ?)`, |