space-architect 8.0.0 → 9.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +54 -0
- data/README.md +39 -464
- data/lib/space_architect/architect_project.rb +90 -39
- data/lib/space_architect/bug_report.rb +2 -1
- data/lib/space_architect/cli/architect.rb +189 -11
- data/lib/space_architect/cli/research.rb +1 -1
- data/lib/space_architect/cli.rb +9 -14
- data/lib/space_architect/loop_cost.rb +592 -0
- data/lib/space_architect/research/supervisor.rb +9 -2
- data/lib/space_architect/routing.rb +149 -0
- data/lib/space_architect/session_sync/bin_path.rb +1 -1
- data/lib/space_architect/session_sync/cursor.rb +1 -1
- data/lib/space_architect/session_sync/plist.rb +1 -1
- data/lib/{space_core → space_architect}/version.rb +2 -2
- data/lib/space_architect.rb +2 -1
- data/skill/architect/SKILL.md +3 -2
- data/skill/architect-vocabulary/SKILL.md +2 -2
- metadata +20 -177
- data/exe/space +0 -13
- data/exe/src +0 -13
- data/lib/space_architect/cli/space.rb +0 -29
- data/lib/space_architect/cli/src.rb +0 -38
- data/lib/space_core/atomic_write.rb +0 -21
- data/lib/space_core/cli/base_command.rb +0 -19
- data/lib/space_core/cli/build.rb +0 -27
- data/lib/space_core/cli/config.rb +0 -49
- data/lib/space_core/cli/current.rb +0 -16
- data/lib/space_core/cli/help.rb +0 -195
- data/lib/space_core/cli/helpers.rb +0 -115
- data/lib/space_core/cli/init.rb +0 -29
- data/lib/space_core/cli/list.rb +0 -24
- data/lib/space_core/cli/loop_status.rb +0 -47
- data/lib/space_core/cli/new.rb +0 -38
- data/lib/space_core/cli/pack.rb +0 -29
- data/lib/space_core/cli/path.rb +0 -16
- data/lib/space_core/cli/repeatable_options.rb +0 -84
- data/lib/space_core/cli/repo.rb +0 -76
- data/lib/space_core/cli/run.rb +0 -44
- data/lib/space_core/cli/shell.rb +0 -125
- data/lib/space_core/cli/show.rb +0 -21
- data/lib/space_core/cli/status.rb +0 -74
- data/lib/space_core/cli/use.rb +0 -17
- data/lib/space_core/cli.rb +0 -177
- data/lib/space_core/commands.rb +0 -51
- data/lib/space_core/config.rb +0 -152
- data/lib/space_core/errors.rb +0 -14
- data/lib/space_core/git_client.rb +0 -49
- data/lib/space_core/mise_client.rb +0 -37
- data/lib/space_core/oci_builder.rb +0 -56
- data/lib/space_core/oci_packer.rb +0 -103
- data/lib/space_core/oci_runner.rb +0 -89
- data/lib/space_core/paths.rb +0 -58
- data/lib/space_core/repo_reference.rb +0 -19
- data/lib/space_core/repo_resolver.rb +0 -167
- data/lib/space_core/shell_integration.rb +0 -438
- data/lib/space_core/slugger.rb +0 -16
- data/lib/space_core/space.rb +0 -174
- data/lib/space_core/space_store.rb +0 -319
- data/lib/space_core/state.rb +0 -86
- data/lib/space_core/templates/oci/dockerfile.erb +0 -73
- data/lib/space_core/templates/oci/dockerignore.erb +0 -17
- data/lib/space_core/templates/oci/entrypoint.sh.erb +0 -16
- data/lib/space_core/terminal.rb +0 -161
- data/lib/space_core/warnings.rb +0 -13
- data/lib/space_core/xdg.rb +0 -33
- data/lib/space_core.rb +0 -26
- data/lib/space_src/cli/clone.rb +0 -54
- data/lib/space_src/cli/config.rb +0 -65
- data/lib/space_src/cli/daemon.rb +0 -362
- data/lib/space_src/cli/options.rb +0 -21
- data/lib/space_src/cli/org.rb +0 -199
- data/lib/space_src/cli/repo.rb +0 -169
- data/lib/space_src/cli/shell.rb +0 -121
- data/lib/space_src/cli/status.rb +0 -75
- data/lib/space_src/cli/sync.rb +0 -161
- data/lib/space_src/cli.rb +0 -159
- data/lib/space_src/cloner.rb +0 -77
- data/lib/space_src/config/contract.rb +0 -54
- data/lib/space_src/config/duration.rb +0 -79
- data/lib/space_src/config/model.rb +0 -49
- data/lib/space_src/config/store.rb +0 -156
- data/lib/space_src/forge/client.rb +0 -31
- data/lib/space_src/forge/github.rb +0 -98
- data/lib/space_src/launchd/agent.rb +0 -195
- data/lib/space_src/launchd/plist.rb +0 -129
- data/lib/space_src/log_rotator.rb +0 -46
- data/lib/space_src/migration.rb +0 -43
- data/lib/space_src/nav.rb +0 -99
- data/lib/space_src/paths.rb +0 -72
- data/lib/space_src/scm/client.rb +0 -87
- data/lib/space_src/scm/git.rb +0 -232
- data/lib/space_src/scm/status.rb +0 -24
- data/lib/space_src/shell.rb +0 -90
- data/lib/space_src/shell_integration.rb +0 -321
- data/lib/space_src/state/lock.rb +0 -59
- data/lib/space_src/state/store.rb +0 -140
- data/lib/space_src/sync/engine.rb +0 -503
- data/lib/space_src/sync/repo_plan.rb +0 -215
- data/lib/space_src/sync/report.rb +0 -18
- data/lib/space_src/ui/interactive_reporter.rb +0 -280
- data/lib/space_src/ui/json_reporter.rb +0 -39
- data/lib/space_src/ui/mode.rb +0 -68
- data/lib/space_src/ui/plain_reporter.rb +0 -53
- data/lib/space_src/ui/reporter.rb +0 -48
- data/lib/space_src/version.rb +0 -7
- data/lib/space_src.rb +0 -38
data/lib/space_src/state/lock.rb
DELETED
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "fileutils"
|
|
4
|
-
|
|
5
|
-
module Space::Src
|
|
6
|
-
module State
|
|
7
|
-
# Advisory process-level lock on a sidecar file derived from the
|
|
8
|
-
# state file path. Serializes overlapping `sync` runs so the
|
|
9
|
-
# later run bails cleanly rather than clobbering the in-flight
|
|
10
|
-
# run's state with a last-writer-wins atomic rename (CF10).
|
|
11
|
-
#
|
|
12
|
-
# The lockfile is a persistent zero-byte sentinel — never unlinked.
|
|
13
|
-
# Deleting a flock'd file while another fd holds it creates a race
|
|
14
|
-
# where both processes think they hold the lock on different inodes.
|
|
15
|
-
#
|
|
16
|
-
# `LOCK_NB` is for daemon safety, not reactor yielding: a blocking
|
|
17
|
-
# `LOCK_EX` would wedge a launchd tick forever behind a hung run.
|
|
18
|
-
# `flock` and the file I/O it guards are ordinary blocking syscalls
|
|
19
|
-
# with no Async scheduler hook — fine here, they're sub-millisecond
|
|
20
|
-
# on a local state dir, same as the rest of `State::Store`.
|
|
21
|
-
class Lock
|
|
22
|
-
NOT_ACQUIRED = :not_acquired
|
|
23
|
-
|
|
24
|
-
# Returns the sidecar lockfile path for a given state_file path.
|
|
25
|
-
def self.path_for(state_file)
|
|
26
|
-
"#{state_file}.lock"
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
# Acquires an exclusive non-blocking advisory lock on the sidecar
|
|
30
|
-
# lockfile. Creates the file (and its parent directory) if missing.
|
|
31
|
-
#
|
|
32
|
-
# Yields to the block and returns its value when the lock is
|
|
33
|
-
# acquired; releases the lock in an `ensure` so it is freed on
|
|
34
|
-
# normal return, explicit `return`, AND any escaping exception
|
|
35
|
-
# (including Interrupt / SignalException).
|
|
36
|
-
#
|
|
37
|
-
# Returns `NOT_ACQUIRED` *without* yielding when another process
|
|
38
|
-
# already holds the lock. The caller decides what to do (e.g.
|
|
39
|
-
# bail cleanly with a Success).
|
|
40
|
-
def self.acquire(state_file)
|
|
41
|
-
lock_path = path_for(state_file)
|
|
42
|
-
FileUtils.mkdir_p(File.dirname(lock_path))
|
|
43
|
-
f = File.open(lock_path, File::RDWR | File::CREAT)
|
|
44
|
-
# Everything that can raise — `flock` itself can (EINTR on a
|
|
45
|
-
# signal, ENOLCK on some filesystems) — runs inside the `begin`,
|
|
46
|
-
# so the `ensure` always closes the fd (no leak). Closing the fd
|
|
47
|
-
# releases the lock (it lives on the open file description), so
|
|
48
|
-
# no explicit LOCK_UN is needed — and `close` can't be skipped
|
|
49
|
-
# by a raising unlock.
|
|
50
|
-
begin
|
|
51
|
-
return NOT_ACQUIRED unless f.flock(File::LOCK_EX | File::LOCK_NB)
|
|
52
|
-
yield
|
|
53
|
-
ensure
|
|
54
|
-
f.close
|
|
55
|
-
end
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
|
-
end
|
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "yaml"
|
|
4
|
-
require "fileutils"
|
|
5
|
-
require "time"
|
|
6
|
-
require "dry/monads"
|
|
7
|
-
|
|
8
|
-
module Space::Src
|
|
9
|
-
module State
|
|
10
|
-
# Machine-managed state at $XDG_STATE_HOME/space-src/state.yaml.
|
|
11
|
-
# Never hand-edited (per PRD §3.2). Per-repo + per-org records with
|
|
12
|
-
# a fixed status enum; the store validates the enum and timestamp
|
|
13
|
-
# format on write.
|
|
14
|
-
class Store
|
|
15
|
-
extend Dry::Monads[:result]
|
|
16
|
-
|
|
17
|
-
STATUSES = %w[clean dirty diverged detached wrong_branch missing error].freeze
|
|
18
|
-
|
|
19
|
-
Repo = Data.define(:default_branch, :last_fetch_at, :last_synced_at, :status, :last_error) do
|
|
20
|
-
def initialize(default_branch: nil, last_fetch_at: nil, last_synced_at: nil, status: nil, last_error: nil)
|
|
21
|
-
super
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def to_h_compact
|
|
25
|
-
{
|
|
26
|
-
"default_branch" => default_branch,
|
|
27
|
-
"last_fetch_at" => format_time(last_fetch_at),
|
|
28
|
-
"last_synced_at" => format_time(last_synced_at),
|
|
29
|
-
"status" => status,
|
|
30
|
-
"last_error" => last_error
|
|
31
|
-
}.compact
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
private
|
|
35
|
-
|
|
36
|
-
def format_time(t)
|
|
37
|
-
t.respond_to?(:iso8601) ? t.iso8601 : t
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
Org = Data.define(:last_listed_at, :repo_count, :last_error) do
|
|
42
|
-
def initialize(last_listed_at: nil, repo_count: 0, last_error: nil)
|
|
43
|
-
super
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
def to_h_compact
|
|
47
|
-
{
|
|
48
|
-
"last_listed_at" => format_time(last_listed_at),
|
|
49
|
-
"repo_count" => repo_count,
|
|
50
|
-
"last_error" => last_error
|
|
51
|
-
}.compact
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
private
|
|
55
|
-
|
|
56
|
-
# `last_listed_at` may arrive as a Time (fresh from the
|
|
57
|
-
# engine) or as a String (round-tripped from YAML, since
|
|
58
|
-
# `to_h_compact` always emits the iso8601 form on
|
|
59
|
-
# write). Both forms are written as the same string on
|
|
60
|
-
# the next emit; the helper accepts either.
|
|
61
|
-
def format_time(t)
|
|
62
|
-
return nil if t.nil?
|
|
63
|
-
t.respond_to?(:iso8601) ? t.iso8601 : t
|
|
64
|
-
end
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
def self.load(path)
|
|
68
|
-
raw = read_yaml(path)
|
|
69
|
-
Success(build_state(raw))
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
def self.write(path, state)
|
|
73
|
-
validation = validate(state)
|
|
74
|
-
return validation if validation.failure?
|
|
75
|
-
|
|
76
|
-
FileUtils.mkdir_p(File.dirname(path))
|
|
77
|
-
tmp = "#{path}.tmp.#{Process.pid}"
|
|
78
|
-
begin
|
|
79
|
-
File.write(tmp, emit(state))
|
|
80
|
-
File.rename(tmp, path)
|
|
81
|
-
ensure
|
|
82
|
-
File.delete(tmp) if File.exist?(tmp)
|
|
83
|
-
end
|
|
84
|
-
Success(state)
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
def self.validate(state)
|
|
88
|
-
state.repos.each do |key, repo|
|
|
89
|
-
unless STATUSES.include?(repo.status)
|
|
90
|
-
return Failure({repos: {key => {status: ["must be one of: #{STATUSES.join(", ")}"]}}})
|
|
91
|
-
end
|
|
92
|
-
end
|
|
93
|
-
Success(state)
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
def self.read_yaml(path)
|
|
97
|
-
return {} unless File.exist?(path)
|
|
98
|
-
# Time class permitted because state.yaml stores ISO8601
|
|
99
|
-
# timestamps; Psych will deserialize them as Time when the
|
|
100
|
-
# scalar is tagged. No other arbitrary classes allowed.
|
|
101
|
-
YAML.safe_load_file(path, permitted_classes: [Symbol, Time], aliases: false) || {}
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
def self.build_state(raw)
|
|
105
|
-
repos = (raw["repos"] || {}).each_with_object({}) do |(key, attrs), acc|
|
|
106
|
-
acc[key] = Repo.new(
|
|
107
|
-
default_branch: attrs["default_branch"],
|
|
108
|
-
last_fetch_at: attrs["last_fetch_at"],
|
|
109
|
-
last_synced_at: attrs["last_synced_at"],
|
|
110
|
-
status: attrs["status"],
|
|
111
|
-
last_error: attrs["last_error"]
|
|
112
|
-
)
|
|
113
|
-
end
|
|
114
|
-
orgs = (raw["orgs"] || {}).each_with_object({}) do |(key, attrs), acc|
|
|
115
|
-
acc[key] = Org.new(
|
|
116
|
-
last_listed_at: attrs["last_listed_at"],
|
|
117
|
-
repo_count: attrs["repo_count"] || 0,
|
|
118
|
-
last_error: attrs["last_error"]
|
|
119
|
-
)
|
|
120
|
-
end
|
|
121
|
-
State.new(repos: repos, orgs: orgs)
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
# State value object — top-level container.
|
|
125
|
-
State = Data.define(:repos, :orgs) do
|
|
126
|
-
def initialize(repos: {}, orgs: {})
|
|
127
|
-
super
|
|
128
|
-
end
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
def self.emit(state)
|
|
132
|
-
payload = {
|
|
133
|
-
"repos" => state.repos.each_with_object({}) { |(k, v), acc| acc[k] = v.to_h_compact },
|
|
134
|
-
"orgs" => state.orgs.each_with_object({}) { |(k, v), acc| acc[k] = v.to_h_compact }
|
|
135
|
-
}
|
|
136
|
-
YAML.dump(payload, line_width: -1)
|
|
137
|
-
end
|
|
138
|
-
end
|
|
139
|
-
end
|
|
140
|
-
end
|