space-architect 1.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 +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +284 -0
- data/exe/architect +13 -0
- data/exe/space +13 -0
- data/lib/space_architect/architect_mission.rb +436 -0
- data/lib/space_architect/atomic_write.rb +21 -0
- data/lib/space_architect/cli/architect.rb +388 -0
- data/lib/space_architect/cli/config.rb +61 -0
- data/lib/space_architect/cli/current.rb +22 -0
- data/lib/space_architect/cli/helpers.rb +117 -0
- data/lib/space_architect/cli/init.rb +35 -0
- data/lib/space_architect/cli/list.rb +30 -0
- data/lib/space_architect/cli/new.rb +43 -0
- data/lib/space_architect/cli/options.rb +12 -0
- data/lib/space_architect/cli/path.rb +22 -0
- data/lib/space_architect/cli/repo.rb +88 -0
- data/lib/space_architect/cli/shell.rb +137 -0
- data/lib/space_architect/cli/show.rb +27 -0
- data/lib/space_architect/cli/space.rb +35 -0
- data/lib/space_architect/cli/src.rb +32 -0
- data/lib/space_architect/cli/status.rb +39 -0
- data/lib/space_architect/cli/use.rb +23 -0
- data/lib/space_architect/cli.rb +102 -0
- data/lib/space_architect/config.rb +152 -0
- data/lib/space_architect/dispatcher.rb +21 -0
- data/lib/space_architect/errors.rb +14 -0
- data/lib/space_architect/git_client.rb +49 -0
- data/lib/space_architect/harness.rb +168 -0
- data/lib/space_architect/mise_client.rb +37 -0
- data/lib/space_architect/repo_reference.rb +19 -0
- data/lib/space_architect/repo_resolver.rb +167 -0
- data/lib/space_architect/shell_integration.rb +438 -0
- data/lib/space_architect/slugger.rb +16 -0
- data/lib/space_architect/space.rb +110 -0
- data/lib/space_architect/space_store.rb +319 -0
- data/lib/space_architect/state.rb +86 -0
- data/lib/space_architect/templates/architect.md.erb +48 -0
- data/lib/space_architect/templates/iteration.md.erb +66 -0
- data/lib/space_architect/terminal.rb +163 -0
- data/lib/space_architect/version.rb +5 -0
- data/lib/space_architect/warnings.rb +13 -0
- data/lib/space_architect/xdg.rb +33 -0
- data/lib/space_architect.rb +26 -0
- data/vendor/repo-tender/lib/space_architect/pristine/cli/clone.rb +55 -0
- data/vendor/repo-tender/lib/space_architect/pristine/cli/config.rb +66 -0
- data/vendor/repo-tender/lib/space_architect/pristine/cli/daemon.rb +347 -0
- data/vendor/repo-tender/lib/space_architect/pristine/cli/options.rb +21 -0
- data/vendor/repo-tender/lib/space_architect/pristine/cli/org.rb +200 -0
- data/vendor/repo-tender/lib/space_architect/pristine/cli/repo.rb +170 -0
- data/vendor/repo-tender/lib/space_architect/pristine/cli/status.rb +76 -0
- data/vendor/repo-tender/lib/space_architect/pristine/cli/sync.rb +149 -0
- data/vendor/repo-tender/lib/space_architect/pristine/cli.rb +137 -0
- data/vendor/repo-tender/lib/space_architect/pristine/cloner.rb +75 -0
- data/vendor/repo-tender/lib/space_architect/pristine/config/contract.rb +54 -0
- data/vendor/repo-tender/lib/space_architect/pristine/config/duration.rb +79 -0
- data/vendor/repo-tender/lib/space_architect/pristine/config/model.rb +49 -0
- data/vendor/repo-tender/lib/space_architect/pristine/config/store.rb +156 -0
- data/vendor/repo-tender/lib/space_architect/pristine/forge/client.rb +31 -0
- data/vendor/repo-tender/lib/space_architect/pristine/forge/github.rb +98 -0
- data/vendor/repo-tender/lib/space_architect/pristine/launchd/agent.rb +195 -0
- data/vendor/repo-tender/lib/space_architect/pristine/launchd/plist.rb +129 -0
- data/vendor/repo-tender/lib/space_architect/pristine/log_rotator.rb +46 -0
- data/vendor/repo-tender/lib/space_architect/pristine/paths.rb +72 -0
- data/vendor/repo-tender/lib/space_architect/pristine/scm/client.rb +87 -0
- data/vendor/repo-tender/lib/space_architect/pristine/scm/git.rb +232 -0
- data/vendor/repo-tender/lib/space_architect/pristine/scm/status.rb +24 -0
- data/vendor/repo-tender/lib/space_architect/pristine/shell.rb +90 -0
- data/vendor/repo-tender/lib/space_architect/pristine/state/lock.rb +59 -0
- data/vendor/repo-tender/lib/space_architect/pristine/state/store.rb +140 -0
- data/vendor/repo-tender/lib/space_architect/pristine/sync/engine.rb +464 -0
- data/vendor/repo-tender/lib/space_architect/pristine/sync/repo_plan.rb +215 -0
- data/vendor/repo-tender/lib/space_architect/pristine/ui/interactive_reporter.rb +280 -0
- data/vendor/repo-tender/lib/space_architect/pristine/ui/json_reporter.rb +39 -0
- data/vendor/repo-tender/lib/space_architect/pristine/ui/mode.rb +68 -0
- data/vendor/repo-tender/lib/space_architect/pristine/ui/plain_reporter.rb +53 -0
- data/vendor/repo-tender/lib/space_architect/pristine/ui/reporter.rb +48 -0
- data/vendor/repo-tender/lib/space_architect/pristine/version.rb +7 -0
- data/vendor/repo-tender/lib/space_architect/pristine.rb +37 -0
- metadata +307 -0
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pastel"
|
|
4
|
+
require "tty-cursor"
|
|
5
|
+
|
|
6
|
+
module SpaceArchitect::Pristine
|
|
7
|
+
module UI
|
|
8
|
+
# Compact, single-line live progress renderer for `sync`.
|
|
9
|
+
# Driven by one render-loop fiber spawned as a child of the engine task
|
|
10
|
+
# via `attach(task)` — NO Ruby Thread.
|
|
11
|
+
#
|
|
12
|
+
# Two phases under one attach/detach (GS6):
|
|
13
|
+
#
|
|
14
|
+
# Phase 1 — Listing: fires between listing_started and listing_finished.
|
|
15
|
+
# Live status line shows "listing N orgs… ✓ K done". As each org
|
|
16
|
+
# completes, a persistent line is emitted (org name + count).
|
|
17
|
+
#
|
|
18
|
+
# Phase 2 — Sweep: fires after run_started through run_finished.
|
|
19
|
+
# Reverts to the compact repo counter (synced X/N + tallies).
|
|
20
|
+
#
|
|
21
|
+
# Output model:
|
|
22
|
+
# - One live status line, rewritten in place via \r + \e[K.
|
|
23
|
+
# - Persistent scrollback lines for listing phase (one per org) and
|
|
24
|
+
# for NON-CLEAN repos only in sweep phase.
|
|
25
|
+
# - Total output: O(orgs + non_clean + failed + constant).
|
|
26
|
+
#
|
|
27
|
+
# Invariants:
|
|
28
|
+
# - The render fiber is the sole writer to `out`; worker fibers only
|
|
29
|
+
# mutate tally/queue state via the reporter event methods.
|
|
30
|
+
# - `Kernel#sleep` inside the render fiber yields to the reactor
|
|
31
|
+
# (cooperative scheduling). Never Thread.new.
|
|
32
|
+
# - On `^C`, the scheduler cancels the child render fiber; its `ensure`
|
|
33
|
+
# block restores the cursor unconditionally.
|
|
34
|
+
class InteractiveReporter
|
|
35
|
+
FRAMES = %w[⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏].freeze
|
|
36
|
+
ADDED_LIST_THRESHOLD = 10
|
|
37
|
+
IN_FLIGHT_MAX_WIDTH = 40
|
|
38
|
+
|
|
39
|
+
def initialize(out, mode:, cadence: 0.1)
|
|
40
|
+
@out = out
|
|
41
|
+
@pastel = Pastel.new(enabled: mode.color)
|
|
42
|
+
@cadence = cadence
|
|
43
|
+
|
|
44
|
+
# Listing phase state
|
|
45
|
+
@org_total = 0
|
|
46
|
+
@org_done = 0
|
|
47
|
+
@pending_org_lines = []
|
|
48
|
+
|
|
49
|
+
# Sweep phase state
|
|
50
|
+
@total = 0
|
|
51
|
+
@finished = 0
|
|
52
|
+
@clean_count = 0
|
|
53
|
+
@nonclean_count = 0
|
|
54
|
+
@failed_count = 0
|
|
55
|
+
@pending_lines = []
|
|
56
|
+
|
|
57
|
+
# In-flight tracking (insertion-ordered: last entry = most-recently-started)
|
|
58
|
+
@in_flight = {}
|
|
59
|
+
|
|
60
|
+
# End-of-run breakdown state
|
|
61
|
+
@action_counts = Hash.new(0)
|
|
62
|
+
@total_commits = 0
|
|
63
|
+
@added_repos = []
|
|
64
|
+
|
|
65
|
+
@frame_idx = 0
|
|
66
|
+
@phase = :listing # :listing | :sweep
|
|
67
|
+
@done = false
|
|
68
|
+
@render_task = nil
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def attach(task)
|
|
72
|
+
@render_task = task.async { render_loop }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def detach
|
|
76
|
+
@done = true
|
|
77
|
+
@render_task&.wait
|
|
78
|
+
@render_task = nil
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# --- Listing phase events ---
|
|
82
|
+
|
|
83
|
+
def listing_started(total:)
|
|
84
|
+
@org_total = total
|
|
85
|
+
@phase = :listing
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def org_listed(ref, count:)
|
|
89
|
+
@org_done += 1
|
|
90
|
+
@pending_org_lines << if count
|
|
91
|
+
"#{@pastel.green("✓")} #{ref.name} #{count} repo(s)"
|
|
92
|
+
else
|
|
93
|
+
"#{@pastel.red("✗")} #{ref.name} FAILED"
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def listing_finished
|
|
98
|
+
# Phase transition handled by run_started
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# --- Sweep phase events ---
|
|
102
|
+
|
|
103
|
+
def run_started(total:)
|
|
104
|
+
@total = total
|
|
105
|
+
@phase = :sweep
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def repo_started(ref)
|
|
109
|
+
@in_flight[ref] = "checking"
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def repo_phase(ref, phase)
|
|
113
|
+
return unless @in_flight.key?(ref)
|
|
114
|
+
@in_flight[ref] = case phase
|
|
115
|
+
when :cloning then "cloning"
|
|
116
|
+
when :fast_forwarding then "fast-forwarding"
|
|
117
|
+
when :switching then "switching"
|
|
118
|
+
else @in_flight[ref]
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def repo_finished(ref, status, action:, commits: 0)
|
|
123
|
+
@in_flight.delete(ref)
|
|
124
|
+
@finished += 1
|
|
125
|
+
@action_counts[action] += 1
|
|
126
|
+
@total_commits += commits
|
|
127
|
+
@added_repos << ref if action == :cloned
|
|
128
|
+
if status.to_s == "clean"
|
|
129
|
+
@clean_count += 1
|
|
130
|
+
else
|
|
131
|
+
@nonclean_count += 1
|
|
132
|
+
@pending_lines << "#{@pastel.yellow("⚠")} #{ref} #{status}"
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def repo_failed(ref, error)
|
|
137
|
+
@in_flight.delete(ref)
|
|
138
|
+
@finished += 1
|
|
139
|
+
@failed_count += 1
|
|
140
|
+
@action_counts[:error] += 1
|
|
141
|
+
@pending_lines << "#{@pastel.red("✗")} #{ref} #{error}"
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def run_finished(summary) = nil
|
|
145
|
+
|
|
146
|
+
private
|
|
147
|
+
|
|
148
|
+
def render_loop
|
|
149
|
+
@out.write(TTY::Cursor.hide)
|
|
150
|
+
@out.flush
|
|
151
|
+
|
|
152
|
+
loop do
|
|
153
|
+
render_tick
|
|
154
|
+
@frame_idx += 1
|
|
155
|
+
break if @done
|
|
156
|
+
sleep @cadence
|
|
157
|
+
end
|
|
158
|
+
ensure
|
|
159
|
+
# Flush any remaining org lines (listing phase may have ended without
|
|
160
|
+
# a final tick draining them).
|
|
161
|
+
pending_org = @pending_org_lines.slice!(0, @pending_org_lines.length)
|
|
162
|
+
pending = @pending_lines.slice!(0, @pending_lines.length)
|
|
163
|
+
@out.write("\r\e[K")
|
|
164
|
+
pending_org.each { |line| @out.write("#{line}\n") }
|
|
165
|
+
pending.each { |line| @out.write("#{line}\n") }
|
|
166
|
+
@out.write("#{build_summary_line}\n")
|
|
167
|
+
breakdown = build_breakdown_line
|
|
168
|
+
@out.write("#{breakdown}\n") unless breakdown.empty?
|
|
169
|
+
added = build_added_repos_block
|
|
170
|
+
@out.write(added) unless added.empty?
|
|
171
|
+
@out.write(TTY::Cursor.show)
|
|
172
|
+
@out.flush
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def render_tick
|
|
176
|
+
if @phase == :listing
|
|
177
|
+
render_listing_tick
|
|
178
|
+
else
|
|
179
|
+
render_sweep_tick
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def render_listing_tick
|
|
184
|
+
pending = @pending_org_lines.slice!(0, @pending_org_lines.length)
|
|
185
|
+
if pending.any?
|
|
186
|
+
@out.write("\r\e[K")
|
|
187
|
+
pending.each { |line| @out.write("#{line}\n") }
|
|
188
|
+
end
|
|
189
|
+
frame = @pastel.cyan(FRAMES[@frame_idx % FRAMES.length])
|
|
190
|
+
@out.write("\r\e[K#{frame} listing #{@org_total} org(s)… #{@pastel.green("✓")} #{@org_done} done")
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def render_sweep_tick
|
|
194
|
+
org_pending = @pending_org_lines.slice!(0, @pending_org_lines.length)
|
|
195
|
+
if org_pending.any?
|
|
196
|
+
@out.write("\r\e[K")
|
|
197
|
+
org_pending.each { |line| @out.write("#{line}\n") }
|
|
198
|
+
end
|
|
199
|
+
pending = @pending_lines.slice!(0, @pending_lines.length)
|
|
200
|
+
if pending.any?
|
|
201
|
+
@out.write("\r\e[K")
|
|
202
|
+
pending.each { |line| @out.write("#{line}\n") }
|
|
203
|
+
end
|
|
204
|
+
@out.write("\r\e[K#{build_status_line}")
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def build_status_line
|
|
208
|
+
frame = @pastel.cyan(FRAMES[@frame_idx % FRAMES.length])
|
|
209
|
+
# Right-justify every counter to the digit-width of @total (the cap on
|
|
210
|
+
# all of them) so the in-flight suffix sits at a fixed column instead of
|
|
211
|
+
# drifting right as counts cross digit boundaries. Padding is invisible
|
|
212
|
+
# leading spaces.
|
|
213
|
+
w = @total.to_s.length
|
|
214
|
+
base = "#{frame} synced #{@finished.to_s.rjust(w)}/#{@total} #{@pastel.green("✓")} #{@clean_count.to_s.rjust(w)} #{@pastel.yellow("⚠")} #{@nonclean_count.to_s.rjust(w)} #{@pastel.red("✗")} #{@failed_count.to_s.rjust(w)}"
|
|
215
|
+
"#{base}#{build_in_flight_suffix}"
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def build_summary_line
|
|
219
|
+
"synced #{@finished}/#{@total} #{@pastel.green("✓")} #{@clean_count} clean #{@pastel.yellow("⚠")} #{@nonclean_count} non-clean #{@pastel.red("✗")} #{@failed_count} failed"
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def build_in_flight_suffix
|
|
223
|
+
return "" if @in_flight.empty?
|
|
224
|
+
ref, verb = @in_flight.to_a.last
|
|
225
|
+
short = ref.to_s.split("/", 2).last.to_s
|
|
226
|
+
short = short[0, IN_FLIGHT_MAX_WIDTH]
|
|
227
|
+
" · #{verb} #{short}"
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def build_breakdown_line
|
|
231
|
+
parts = []
|
|
232
|
+
if (n = @action_counts[:cloned]) > 0
|
|
233
|
+
parts << "cloned #{n}"
|
|
234
|
+
end
|
|
235
|
+
if (n = @action_counts[:fast_forwarded]) > 0
|
|
236
|
+
commit_str = (@total_commits > 0) ? " (#{@total_commits} commit#{"s" unless @total_commits == 1})" : ""
|
|
237
|
+
parts << "fast-forwarded #{n}#{commit_str}"
|
|
238
|
+
end
|
|
239
|
+
if (n = @action_counts[:up_to_date]) > 0
|
|
240
|
+
parts << "up-to-date #{n}"
|
|
241
|
+
end
|
|
242
|
+
if (n = @action_counts[:switched]) > 0
|
|
243
|
+
parts << "switched #{n}"
|
|
244
|
+
end
|
|
245
|
+
if (n = @action_counts[:dirty]) > 0
|
|
246
|
+
parts << "dirty #{n}"
|
|
247
|
+
end
|
|
248
|
+
if (n = @action_counts[:diverged]) > 0
|
|
249
|
+
parts << "diverged #{n}"
|
|
250
|
+
end
|
|
251
|
+
if (n = @action_counts[:wrong_branch]) > 0
|
|
252
|
+
parts << "wrong-branch #{n}"
|
|
253
|
+
end
|
|
254
|
+
if (n = @action_counts[:detached]) > 0
|
|
255
|
+
parts << "detached #{n}"
|
|
256
|
+
end
|
|
257
|
+
error_n = @action_counts[:error]
|
|
258
|
+
if error_n > 0
|
|
259
|
+
parts << "#{(error_n == 1) ? "error" : "errors"} #{error_n}"
|
|
260
|
+
end
|
|
261
|
+
parts.join(" ")
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def build_added_repos_block
|
|
265
|
+
return "" if @added_repos.empty?
|
|
266
|
+
count = @added_repos.size
|
|
267
|
+
if count > ADDED_LIST_THRESHOLD
|
|
268
|
+
"added #{count} repos\n"
|
|
269
|
+
else
|
|
270
|
+
lines = ["added #{count} #{(count == 1) ? "repo" : "repos"}:"]
|
|
271
|
+
@added_repos.each do |ref|
|
|
272
|
+
short = ref.to_s.split("/", 2).last.to_s
|
|
273
|
+
lines << " #{short}"
|
|
274
|
+
end
|
|
275
|
+
lines.join("\n") + "\n"
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "time"
|
|
5
|
+
|
|
6
|
+
module SpaceArchitect::Pristine
|
|
7
|
+
module UI
|
|
8
|
+
# Emits one JSON object per event line (12-factor style) to `out`.
|
|
9
|
+
# Every object carries at minimum: "event", "t" (ISO8601 timestamp),
|
|
10
|
+
# plus event-specific keys (ref, status, error, total, summary).
|
|
11
|
+
# @out.sync = true ensures output is immediate on non-TTY pipes (GS5).
|
|
12
|
+
class JsonReporter
|
|
13
|
+
def initialize(out)
|
|
14
|
+
@out = out
|
|
15
|
+
@out.sync = true
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def attach(task) = nil
|
|
19
|
+
def detach = nil
|
|
20
|
+
|
|
21
|
+
def listing_started(total:) = emit(event: "listing_started", total: total)
|
|
22
|
+
def org_listed(ref, count:) = emit(event: "org_listed", org: ref.name, count: count)
|
|
23
|
+
def listing_finished = emit(event: "listing_finished")
|
|
24
|
+
|
|
25
|
+
def run_started(total:) = emit(event: "run_started", total: total)
|
|
26
|
+
def repo_started(ref) = emit(event: "repo_started", ref: ref)
|
|
27
|
+
def repo_phase(ref, phase) = emit(event: "repo_phase", ref: ref, phase: phase)
|
|
28
|
+
def repo_finished(ref, status, action:, commits: 0) = emit(event: "repo_finished", ref: ref, status: status, action: action, commits: commits)
|
|
29
|
+
def repo_failed(ref, error) = emit(event: "repo_failed", ref: ref, error: error.to_s)
|
|
30
|
+
def run_finished(summary) = emit(event: "run_finished", summary: summary)
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def emit(payload)
|
|
35
|
+
@out.puts JSON.generate(payload.merge(t: Time.now.iso8601))
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "dry/struct"
|
|
4
|
+
require "dry/types"
|
|
5
|
+
|
|
6
|
+
module SpaceArchitect::Pristine
|
|
7
|
+
module UI
|
|
8
|
+
Types = Dry.Types()
|
|
9
|
+
|
|
10
|
+
# Resolved output mode — immutable dry-struct constructed once per command
|
|
11
|
+
# invocation from (flags, env, out.tty?) using precedence: flag > env > autodetect.
|
|
12
|
+
#
|
|
13
|
+
# Frozen contract (PRD §3.1):
|
|
14
|
+
#
|
|
15
|
+
# format: :pretty | :plain | :json
|
|
16
|
+
# color: true | false
|
|
17
|
+
# animate: true | false (always false in Slice A; Slice B gates on this)
|
|
18
|
+
# quiet: true | false
|
|
19
|
+
#
|
|
20
|
+
# Color precedence: --no-color (flag) > CLICOLOR_FORCE (env-force) >
|
|
21
|
+
# NO_COLOR/TERM=dumb/non-:pretty/non-TTY (env+autodetect).
|
|
22
|
+
class Mode < Dry::Struct
|
|
23
|
+
FORMATS = %i[pretty plain json].freeze
|
|
24
|
+
|
|
25
|
+
attribute :color, Types::Bool
|
|
26
|
+
attribute :animate, Types::Bool
|
|
27
|
+
attribute :quiet, Types::Bool
|
|
28
|
+
attribute :format, Types::Symbol.constrained(included_in: FORMATS)
|
|
29
|
+
|
|
30
|
+
# @param flags [Hash] CLI flags: :plain, :json, :no_color, :quiet (truthy = set)
|
|
31
|
+
# @param env [Hash] environment hash (use CLI.env for the injectable test seam)
|
|
32
|
+
# @param out [IO] the output stream; tested with out.tty?
|
|
33
|
+
def self.resolve(flags:, env:, out:)
|
|
34
|
+
json = !!flags[:json]
|
|
35
|
+
plain = !!flags[:plain]
|
|
36
|
+
no_color = !!flags[:no_color]
|
|
37
|
+
quiet = !!flags[:quiet]
|
|
38
|
+
|
|
39
|
+
format = if json
|
|
40
|
+
:json
|
|
41
|
+
elsif plain || !out.tty?
|
|
42
|
+
:plain
|
|
43
|
+
else
|
|
44
|
+
:pretty
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
no_color_env = env["NO_COLOR"] && !env["NO_COLOR"].empty?
|
|
48
|
+
clicolor_force = env["CLICOLOR_FORCE"] && !env["CLICOLOR_FORCE"].empty?
|
|
49
|
+
term_dumb = env["TERM"] == "dumb"
|
|
50
|
+
|
|
51
|
+
color = if no_color
|
|
52
|
+
false
|
|
53
|
+
elsif clicolor_force
|
|
54
|
+
true
|
|
55
|
+
elsif no_color_env || term_dumb || format != :pretty || !out.tty?
|
|
56
|
+
false
|
|
57
|
+
else
|
|
58
|
+
true
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
ci = env["CI"] && !env["CI"].empty?
|
|
62
|
+
animate = format == :pretty && out.tty? && !quiet && !ci
|
|
63
|
+
|
|
64
|
+
new(color: color, animate: animate, quiet: quiet, format: format)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SpaceArchitect::Pristine
|
|
4
|
+
module UI
|
|
5
|
+
# Emits one tab-separated line per terminal repo event, ANSI-free always.
|
|
6
|
+
# repo_finished → "ref\tstatus"; repo_failed → "ref\tFAILED\terror".
|
|
7
|
+
# org_listed → "listed: ref.name\tN repos" (or "listed: ref.name\tFAILED" on failure).
|
|
8
|
+
# Both write to the same `out` stream (G4 choice: no separate stderr stream
|
|
9
|
+
# in Slice A; FAILED marker distinguishes errors).
|
|
10
|
+
# @out.sync = true ensures output is immediate on non-TTY pipes (GS5).
|
|
11
|
+
class PlainReporter
|
|
12
|
+
def initialize(out, mode: nil)
|
|
13
|
+
@out = out
|
|
14
|
+
@out.sync = true
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def attach(task) = nil
|
|
18
|
+
def detach = nil
|
|
19
|
+
|
|
20
|
+
def listing_started(total:)
|
|
21
|
+
@out.puts "listing: #{total} org(s)"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def org_listed(ref, count:)
|
|
25
|
+
if count
|
|
26
|
+
@out.puts "listed: #{ref.name}\t#{count} repo(s)"
|
|
27
|
+
else
|
|
28
|
+
@out.puts "listed: #{ref.name}\tFAILED"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def listing_finished = nil
|
|
33
|
+
|
|
34
|
+
def run_started(total:)
|
|
35
|
+
@out.puts "starting: #{total} repo(s)"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def repo_started(ref) = nil
|
|
39
|
+
|
|
40
|
+
def repo_phase(ref, phase) = nil
|
|
41
|
+
|
|
42
|
+
def repo_finished(ref, status, action:, commits: 0)
|
|
43
|
+
@out.puts "#{ref}\t#{status}"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def repo_failed(ref, error)
|
|
47
|
+
@out.puts "#{ref}\tFAILED\t#{error}"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def run_finished(summary) = nil
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SpaceArchitect::Pristine
|
|
4
|
+
module UI
|
|
5
|
+
# The reporter event interface. Every implementation must respond to:
|
|
6
|
+
#
|
|
7
|
+
# attach(task) # spawn render-loop fiber as child of task
|
|
8
|
+
# # called BEFORE listing begins; no total yet
|
|
9
|
+
# listing_started(total:) # N orgs about to be listed
|
|
10
|
+
# org_listed(ref, count:) # one per org as it finishes listing;
|
|
11
|
+
# # ref is OrgRef; count: nil on failure
|
|
12
|
+
# listing_finished # all orgs listed (or skipped on auth failure)
|
|
13
|
+
# run_started(total:) # N repos about to be processed
|
|
14
|
+
# repo_started(ref) # work begins on ref ("host/owner/name" string)
|
|
15
|
+
# repo_phase(ref, phase) # :cloning | :fast_forwarding | :switching
|
|
16
|
+
# repo_finished(ref, status, action:, commits: 0)
|
|
17
|
+
# # final status string (matches state row);
|
|
18
|
+
# # action: realized-action Symbol; commits: Integer
|
|
19
|
+
# repo_failed(ref, error) # failure string (plan error or unhandled raise)
|
|
20
|
+
# run_finished(summary) # Hash<String,Integer> status→count
|
|
21
|
+
# detach # stop render fiber, restore terminal
|
|
22
|
+
#
|
|
23
|
+
# Engine event sequence:
|
|
24
|
+
# attach(task) → listing_started(total:) → {org_listed(ref, count:)} →
|
|
25
|
+
# listing_finished → run_started(total:) → {repo_started → repo_phase* →
|
|
26
|
+
# repo_finished|repo_failed} → run_finished(summary) → detach
|
|
27
|
+
#
|
|
28
|
+
# Implementations:
|
|
29
|
+
# NullReporter — all no-ops; the engine default
|
|
30
|
+
# PlainReporter — one ANSI-free line per terminal event
|
|
31
|
+
# JsonReporter — one JSON object per event line (12-factor)
|
|
32
|
+
# InteractiveReporter — color + animated progress (two-phase: listing + sweep)
|
|
33
|
+
|
|
34
|
+
class NullReporter
|
|
35
|
+
def attach(task) = nil
|
|
36
|
+
def listing_started(total:) = nil
|
|
37
|
+
def org_listed(ref, count:) = nil
|
|
38
|
+
def listing_finished = nil
|
|
39
|
+
def run_started(total:) = nil
|
|
40
|
+
def repo_started(ref) = nil
|
|
41
|
+
def repo_phase(ref, phase) = nil
|
|
42
|
+
def repo_finished(ref, status, action:, commits: 0) = nil
|
|
43
|
+
def repo_failed(ref, error) = nil
|
|
44
|
+
def run_finished(summary) = nil
|
|
45
|
+
def detach = nil
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "space_architect/pristine/version"
|
|
4
|
+
|
|
5
|
+
# repo-tender — keep local git clones evergreen.
|
|
6
|
+
# (clean · on the remote's default branch · fetched within refresh_interval)
|
|
7
|
+
#
|
|
8
|
+
# Slice 1 surface: Paths, Shell, Config::{Model,Contract,Store},
|
|
9
|
+
# State::Store, SCM::{Client,Git,Status}, Forge::{Client,GitHub}.
|
|
10
|
+
# Later slices build sync orchestration, CLI, and launchd on top.
|
|
11
|
+
|
|
12
|
+
module SpaceArchitect::Pristine
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
require "space_architect/pristine/paths"
|
|
16
|
+
require "space_architect/pristine/shell"
|
|
17
|
+
require "space_architect/pristine/config/model"
|
|
18
|
+
require "space_architect/pristine/config/contract"
|
|
19
|
+
require "space_architect/pristine/config/store"
|
|
20
|
+
require "space_architect/pristine/state/store"
|
|
21
|
+
require "space_architect/pristine/state/lock"
|
|
22
|
+
require "space_architect/pristine/scm/client"
|
|
23
|
+
require "space_architect/pristine/scm/status"
|
|
24
|
+
require "space_architect/pristine/scm/git"
|
|
25
|
+
require "space_architect/pristine/forge/client"
|
|
26
|
+
require "space_architect/pristine/forge/github"
|
|
27
|
+
require "space_architect/pristine/sync/repo_plan"
|
|
28
|
+
require "space_architect/pristine/sync/engine"
|
|
29
|
+
require "space_architect/pristine/config/duration"
|
|
30
|
+
require "space_architect/pristine/log_rotator"
|
|
31
|
+
require "space_architect/pristine/launchd/plist"
|
|
32
|
+
require "space_architect/pristine/launchd/agent"
|
|
33
|
+
require "space_architect/pristine/ui/reporter"
|
|
34
|
+
require "space_architect/pristine/ui/mode"
|
|
35
|
+
require "space_architect/pristine/ui/plain_reporter"
|
|
36
|
+
require "space_architect/pristine/ui/json_reporter"
|
|
37
|
+
require "space_architect/pristine/cli"
|