Class: Clacky::Server::ProjectManager

Inherits:
Object
  • Object
show all
Defined in:
lib/clacky/server/project_manager.rb

Overview

ProjectManager handles CRUD for "projects" — named groups that sessions can be assigned to, analogous to ChatGPT / Codex Projects.

Storage: ~/.clacky/projects.json Each project entry shape:

{
id:          String (8-char hex),
name:        String,
description: String | null,
color:       String | null (e.g. "#6366f1"),
icon:        String | null (e.g. "folder", "code"),
working_dir: String | null (absolute path; new sessions inherit this),
created_at:  ISO8601,
updated_at:  ISO8601
}

Thread-safety: a Mutex guards every read/write.

Constant Summary collapse

PROJECTS_FILE =
File.join(Dir.home, ".clacky", "projects.json")

Instance Method Summary collapse

Constructor Details

#initialize(projects_file: nil) ⇒ ProjectManager

Returns a new instance of ProjectManager.



30
31
32
33
34
# File 'lib/clacky/server/project_manager.rb', line 30

def initialize(projects_file: nil)
  @projects_file = projects_file || PROJECTS_FILE
  @mutex         = Mutex.new
  @cache         = nil
end

Instance Method Details

#allObject

Return all projects sorted by created_at ascending (oldest first).



37
38
39
# File 'lib/clacky/server/project_manager.rb', line 37

def all
  @mutex.synchronize { load_projects.dup }
end

#create(name:, description: nil, color: nil, icon: nil, working_dir: nil) ⇒ Object

Create a new project. Returns the created project hash. Required: name. Optional: description, color, icon, working_dir.

Raises:

  • (ArgumentError)


48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# File 'lib/clacky/server/project_manager.rb', line 48

def create(name:, description: nil, color: nil, icon: nil, working_dir: nil)
  raise ArgumentError, "name is required" if name.to_s.strip.empty?

  now     = Time.now.iso8601
  project = {
    id:          SecureRandom.hex(4),
    name:        name.to_s.strip,
    description: optional_str(description),
    color:       optional_str(color),
    icon:        optional_str(icon),
    working_dir: optional_str(working_dir),
    created_at:  now,
    updated_at:  now
  }

  @mutex.synchronize do
    projects = load_projects
    projects << project
    save_projects(projects)
  end

  project
end

#delete(id) ⇒ Object

Delete a project by id. Returns true if found and deleted, false otherwise. NOTE: caller is responsible for clearing project_id on orphaned sessions.



99
100
101
102
103
104
105
106
107
108
109
# File 'lib/clacky/server/project_manager.rb', line 99

def delete(id)
  @mutex.synchronize do
    projects = load_projects
    before   = projects.size
    projects.reject! { |p| p[:id] == id.to_s }
    return false if projects.size == before

    save_projects(projects)
    true
  end
end

#find(id) ⇒ Object

Find a single project by id. Returns nil if not found.



42
43
44
# File 'lib/clacky/server/project_manager.rb', line 42

def find(id)
  @mutex.synchronize { load_projects.find { |p| p[:id] == id.to_s } }
end

#update(id, name: :__unset, description: :__unset, color: :__unset, icon: :__unset, working_dir: :__unset) ⇒ Object

Update an existing project. Only explicitly passed (non-sentinel) keys are changed. Pass description: nil or color: nil or icon: nil or working_dir: nil to clear those fields. Returns updated project hash, or nil if not found.



75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# File 'lib/clacky/server/project_manager.rb', line 75

def update(id, name: :__unset, description: :__unset, color: :__unset, icon: :__unset, working_dir: :__unset)
  @mutex.synchronize do
    projects = load_projects
    project  = projects.find { |p| p[:id] == id.to_s }
    return nil unless project

    unless name == :__unset
      raise ArgumentError, "name cannot be empty" if name.to_s.strip.empty?

      project[:name] = name.to_s.strip
    end
    project[:description] = optional_str(description) unless description == :__unset
    project[:color]       = optional_str(color)       unless color == :__unset
    project[:icon]        = optional_str(icon)        unless icon == :__unset
    project[:working_dir] = optional_str(working_dir) unless working_dir == :__unset
    project[:updated_at]  = Time.now.iso8601

    save_projects(projects)
    project.dup
  end
end