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),
pinned_at:   String | null (ISO8601; set when pinned, nil when not),
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.



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

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).



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

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)


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

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.



102
103
104
105
106
107
108
109
110
111
112
# File 'lib/clacky/server/project_manager.rb', line 102

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.



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

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, pinned: :__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. Pass pinned: true to pin (sets pinned_at to now), pinned: false to unpin. Returns updated project hash, or nil if not found.



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

def update(id, name: :__unset, description: :__unset, color: :__unset, icon: :__unset, working_dir: :__unset, pinned: :__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[:pinned_at]   = pinned == true ? Time.now.iso8601 : nil unless pinned == :__unset
    project[:updated_at]  = Time.now.iso8601

    save_projects(projects)
    project.dup
  end
end