cc-os/plugins/os-backlog/lib/backlog/project_index.rb

72 lines
2.3 KiB
Ruby
Raw Permalink Normal View History

require "json"
require "fileutils"
module Backlog
# The global project index at ~/.cc-os/projects.json: a derived map of
# canonical repo path -> {name, tracker, remote, updated_at}, maintained as
# a side effect of config-write (issue #27). Deliberately GLOBAL state
# (distinct from per-project .cc-os/ under ADR-0027) so any session can ask
# "which tracker does project X use" without visiting its repo.
#
# Pure over an injected path: callers hand in the already-canonical repo
# path (the CLI resolves it via `git rev-parse --show-toplevel`). Read
# paths never raise — a missing or corrupt file reads as an empty index; a
# corrupt file is only overwritten when a new upsert happens. Writes are
# atomic (temp file + rename in the same directory).
class ProjectIndex
VERSION = 1
# Default index path, honoring a CC_OS_HOME override (used by tests to
# keep the real index untouched) and falling back to $HOME/.cc-os.
def self.default_path(env: ENV)
base = env["CC_OS_HOME"] || File.join(env.fetch("HOME", Dir.home), ".cc-os")
File.join(base, "projects.json")
end
def initialize(path: self.class.default_path, today: -> { Time.now.strftime("%Y-%m-%d") })
@path = path
@today = today
end
# @return [Hash] the projects map (canonical path -> row); {} when the
# file is missing or unreadable/corrupt. Never raises.
def all
data = read
data["projects"].is_a?(Hash) ? data["projects"] : {}
end
# Insert or update the row for a canonical repo path. Atomic write;
# creates the parent directory if missing.
def upsert(path:, name:, tracker:, remote:)
projects = all
projects[path] = {
"name" => name,
"tracker" => tracker,
"remote" => remote,
"updated_at" => @today.call
}
write({ "version" => VERSION, "projects" => projects })
projects[path]
end
private
def read
return {} unless File.exist?(@path)
parsed = JSON.parse(File.read(@path))
parsed.is_a?(Hash) ? parsed : {}
rescue StandardError
{}
end
def write(data)
dir = File.dirname(@path)
FileUtils.mkdir_p(dir)
temp = File.join(dir, ".projects.json.tmp.#{Process.pid}")
File.write(temp, JSON.pretty_generate(data) + "\n")
File.rename(temp, @path)
end
end
end