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.
Files changed (80) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +284 -0
  4. data/exe/architect +13 -0
  5. data/exe/space +13 -0
  6. data/lib/space_architect/architect_mission.rb +436 -0
  7. data/lib/space_architect/atomic_write.rb +21 -0
  8. data/lib/space_architect/cli/architect.rb +388 -0
  9. data/lib/space_architect/cli/config.rb +61 -0
  10. data/lib/space_architect/cli/current.rb +22 -0
  11. data/lib/space_architect/cli/helpers.rb +117 -0
  12. data/lib/space_architect/cli/init.rb +35 -0
  13. data/lib/space_architect/cli/list.rb +30 -0
  14. data/lib/space_architect/cli/new.rb +43 -0
  15. data/lib/space_architect/cli/options.rb +12 -0
  16. data/lib/space_architect/cli/path.rb +22 -0
  17. data/lib/space_architect/cli/repo.rb +88 -0
  18. data/lib/space_architect/cli/shell.rb +137 -0
  19. data/lib/space_architect/cli/show.rb +27 -0
  20. data/lib/space_architect/cli/space.rb +35 -0
  21. data/lib/space_architect/cli/src.rb +32 -0
  22. data/lib/space_architect/cli/status.rb +39 -0
  23. data/lib/space_architect/cli/use.rb +23 -0
  24. data/lib/space_architect/cli.rb +102 -0
  25. data/lib/space_architect/config.rb +152 -0
  26. data/lib/space_architect/dispatcher.rb +21 -0
  27. data/lib/space_architect/errors.rb +14 -0
  28. data/lib/space_architect/git_client.rb +49 -0
  29. data/lib/space_architect/harness.rb +168 -0
  30. data/lib/space_architect/mise_client.rb +37 -0
  31. data/lib/space_architect/repo_reference.rb +19 -0
  32. data/lib/space_architect/repo_resolver.rb +167 -0
  33. data/lib/space_architect/shell_integration.rb +438 -0
  34. data/lib/space_architect/slugger.rb +16 -0
  35. data/lib/space_architect/space.rb +110 -0
  36. data/lib/space_architect/space_store.rb +319 -0
  37. data/lib/space_architect/state.rb +86 -0
  38. data/lib/space_architect/templates/architect.md.erb +48 -0
  39. data/lib/space_architect/templates/iteration.md.erb +66 -0
  40. data/lib/space_architect/terminal.rb +163 -0
  41. data/lib/space_architect/version.rb +5 -0
  42. data/lib/space_architect/warnings.rb +13 -0
  43. data/lib/space_architect/xdg.rb +33 -0
  44. data/lib/space_architect.rb +26 -0
  45. data/vendor/repo-tender/lib/space_architect/pristine/cli/clone.rb +55 -0
  46. data/vendor/repo-tender/lib/space_architect/pristine/cli/config.rb +66 -0
  47. data/vendor/repo-tender/lib/space_architect/pristine/cli/daemon.rb +347 -0
  48. data/vendor/repo-tender/lib/space_architect/pristine/cli/options.rb +21 -0
  49. data/vendor/repo-tender/lib/space_architect/pristine/cli/org.rb +200 -0
  50. data/vendor/repo-tender/lib/space_architect/pristine/cli/repo.rb +170 -0
  51. data/vendor/repo-tender/lib/space_architect/pristine/cli/status.rb +76 -0
  52. data/vendor/repo-tender/lib/space_architect/pristine/cli/sync.rb +149 -0
  53. data/vendor/repo-tender/lib/space_architect/pristine/cli.rb +137 -0
  54. data/vendor/repo-tender/lib/space_architect/pristine/cloner.rb +75 -0
  55. data/vendor/repo-tender/lib/space_architect/pristine/config/contract.rb +54 -0
  56. data/vendor/repo-tender/lib/space_architect/pristine/config/duration.rb +79 -0
  57. data/vendor/repo-tender/lib/space_architect/pristine/config/model.rb +49 -0
  58. data/vendor/repo-tender/lib/space_architect/pristine/config/store.rb +156 -0
  59. data/vendor/repo-tender/lib/space_architect/pristine/forge/client.rb +31 -0
  60. data/vendor/repo-tender/lib/space_architect/pristine/forge/github.rb +98 -0
  61. data/vendor/repo-tender/lib/space_architect/pristine/launchd/agent.rb +195 -0
  62. data/vendor/repo-tender/lib/space_architect/pristine/launchd/plist.rb +129 -0
  63. data/vendor/repo-tender/lib/space_architect/pristine/log_rotator.rb +46 -0
  64. data/vendor/repo-tender/lib/space_architect/pristine/paths.rb +72 -0
  65. data/vendor/repo-tender/lib/space_architect/pristine/scm/client.rb +87 -0
  66. data/vendor/repo-tender/lib/space_architect/pristine/scm/git.rb +232 -0
  67. data/vendor/repo-tender/lib/space_architect/pristine/scm/status.rb +24 -0
  68. data/vendor/repo-tender/lib/space_architect/pristine/shell.rb +90 -0
  69. data/vendor/repo-tender/lib/space_architect/pristine/state/lock.rb +59 -0
  70. data/vendor/repo-tender/lib/space_architect/pristine/state/store.rb +140 -0
  71. data/vendor/repo-tender/lib/space_architect/pristine/sync/engine.rb +464 -0
  72. data/vendor/repo-tender/lib/space_architect/pristine/sync/repo_plan.rb +215 -0
  73. data/vendor/repo-tender/lib/space_architect/pristine/ui/interactive_reporter.rb +280 -0
  74. data/vendor/repo-tender/lib/space_architect/pristine/ui/json_reporter.rb +39 -0
  75. data/vendor/repo-tender/lib/space_architect/pristine/ui/mode.rb +68 -0
  76. data/vendor/repo-tender/lib/space_architect/pristine/ui/plain_reporter.rb +53 -0
  77. data/vendor/repo-tender/lib/space_architect/pristine/ui/reporter.rb +48 -0
  78. data/vendor/repo-tender/lib/space_architect/pristine/version.rb +7 -0
  79. data/vendor/repo-tender/lib/space_architect/pristine.rb +37 -0
  80. 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,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SpaceArchitect
4
+ module Pristine
5
+ VERSION = "0.1.0"
6
+ end
7
+ 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"