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,388 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SpaceArchitect
|
|
4
|
+
module CLI
|
|
5
|
+
module Architect
|
|
6
|
+
class Init < Dry::CLI::Command
|
|
7
|
+
include GlobalOptions
|
|
8
|
+
include Helpers
|
|
9
|
+
|
|
10
|
+
desc "Scaffold architect mission memory in the current space"
|
|
11
|
+
argument :space, required: false, desc: "Space identifier (default: $PWD)"
|
|
12
|
+
|
|
13
|
+
def call(space: nil, **opts)
|
|
14
|
+
setup_terminal(**opts.slice(:color, :colors))
|
|
15
|
+
handle_errors do
|
|
16
|
+
render(store.find(space)) do |sp|
|
|
17
|
+
mission = ArchitectMission.new(space: sp)
|
|
18
|
+
path = mission.init!
|
|
19
|
+
terminal.say "Mission ready: #{terminal.path(path)}"
|
|
20
|
+
CLI.record_outcome(Outcome.new(exit_code: 0))
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
class New < Dry::CLI::Command
|
|
27
|
+
include GlobalOptions
|
|
28
|
+
include Helpers
|
|
29
|
+
|
|
30
|
+
desc "Scaffold the next iteration file (architecture/I<NN>-<iteration>.md)"
|
|
31
|
+
argument :iteration, required: true, desc: "Iteration name (kebab-case)"
|
|
32
|
+
argument :space, required: false, desc: "Space identifier (default: $PWD)"
|
|
33
|
+
|
|
34
|
+
def call(iteration:, space: nil, **opts)
|
|
35
|
+
setup_terminal(**opts.slice(:color, :colors))
|
|
36
|
+
handle_errors do
|
|
37
|
+
render(store.find(space)) do |sp|
|
|
38
|
+
mission = ArchitectMission.new(space: sp)
|
|
39
|
+
path = mission.new_iteration!(iteration)
|
|
40
|
+
terminal.say "Iteration scaffolded: #{terminal.path(path)}"
|
|
41
|
+
CLI.record_outcome(Outcome.new(exit_code: 0))
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
class Status < Dry::CLI::Command
|
|
48
|
+
include GlobalOptions
|
|
49
|
+
include Helpers
|
|
50
|
+
|
|
51
|
+
desc "Show architect mission state (read-only)"
|
|
52
|
+
argument :space, required: false, desc: "Space identifier (default: $PWD)"
|
|
53
|
+
|
|
54
|
+
def call(space: nil, **opts)
|
|
55
|
+
setup_terminal(**opts.slice(:color, :colors))
|
|
56
|
+
handle_errors do
|
|
57
|
+
render(store.find(space)) do |sp|
|
|
58
|
+
mission = ArchitectMission.new(space: sp)
|
|
59
|
+
info = mission.status
|
|
60
|
+
block = info[:block]
|
|
61
|
+
|
|
62
|
+
terminal.say "Mission status: #{block['status'] || '(none)'}"
|
|
63
|
+
terminal.say "Current iteration: #{block['current_iteration'] || '(none)'}"
|
|
64
|
+
|
|
65
|
+
iterations = block["iterations"] || []
|
|
66
|
+
if iterations.empty?
|
|
67
|
+
terminal.say "Iterations: (none)"
|
|
68
|
+
else
|
|
69
|
+
rows = iterations.map do |s|
|
|
70
|
+
nn = s["ordinal"] ? format("%02d", s["ordinal"]) : "-"
|
|
71
|
+
lane_list = s["lanes"] || []
|
|
72
|
+
lanes_str = lane_list.map do |l|
|
|
73
|
+
h = l["harness"] || "claude-code"
|
|
74
|
+
m = l["model"] || Harness::CLAUDE_DEFAULT_MODEL
|
|
75
|
+
eff = l["effort"] ? "·#{l['effort']}" : ""
|
|
76
|
+
"#{l['name']}(#{l['repo']}·#{h}·#{m}#{eff})"
|
|
77
|
+
end.join(", ")
|
|
78
|
+
lanes = lane_list.any? { |l| l["variant"] } ? "variant: #{lanes_str}" : lanes_str
|
|
79
|
+
lanes = "#{lanes} → winner: #{s['winner']}" if s["winner"]
|
|
80
|
+
[nn, s["name"], s["freeze_sha"]&.[](0, 8) || "-", lanes, s["verdict"] || "-"]
|
|
81
|
+
end
|
|
82
|
+
terminal.say terminal.table(%w[II Iteration FreezeSHA Lanes Verdict], rows)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
unless info[:iteration_files].empty?
|
|
86
|
+
terminal.say "Iteration files: #{info[:iteration_files].join(', ')}"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
CLI.record_outcome(Outcome.new(exit_code: 0))
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
class Freeze < Dry::CLI::Command
|
|
96
|
+
include GlobalOptions
|
|
97
|
+
include Helpers
|
|
98
|
+
|
|
99
|
+
desc "Freeze gates for an iteration"
|
|
100
|
+
argument :iteration, required: true, desc: "Iteration name"
|
|
101
|
+
argument :space, required: false, desc: "Space identifier (default: $PWD)"
|
|
102
|
+
|
|
103
|
+
def call(iteration:, space: nil, **opts)
|
|
104
|
+
setup_terminal(**opts.slice(:color, :colors))
|
|
105
|
+
handle_errors do
|
|
106
|
+
render(store.find(space)) do |sp|
|
|
107
|
+
mission = ArchitectMission.new(space: sp)
|
|
108
|
+
sha = mission.freeze!(iteration)
|
|
109
|
+
terminal.say "Frozen #{iteration} at #{sha}"
|
|
110
|
+
CLI.record_outcome(Outcome.new(exit_code: 0))
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
class Verify < Dry::CLI::Command
|
|
117
|
+
include GlobalOptions
|
|
118
|
+
include Helpers
|
|
119
|
+
|
|
120
|
+
desc "Post-flight mechanical checks for an iteration (reports only, no judgment)"
|
|
121
|
+
argument :iteration, required: true, desc: "Iteration name"
|
|
122
|
+
argument :space, required: false, desc: "Space identifier (default: $PWD)"
|
|
123
|
+
|
|
124
|
+
def call(iteration:, space: nil, **opts)
|
|
125
|
+
setup_terminal(**opts.slice(:color, :colors))
|
|
126
|
+
handle_errors do
|
|
127
|
+
render(store.find(space)) do |sp|
|
|
128
|
+
mission = ArchitectMission.new(space: sp)
|
|
129
|
+
results = mission.verify(iteration)
|
|
130
|
+
|
|
131
|
+
if results.empty?
|
|
132
|
+
terminal.say "No lanes recorded for iteration '#{iteration}'"
|
|
133
|
+
CLI.record_outcome(Outcome.new(exit_code: 0))
|
|
134
|
+
next
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
rows = results.flat_map do |r|
|
|
138
|
+
lane = r[:lane]
|
|
139
|
+
c = r[:checks]
|
|
140
|
+
[
|
|
141
|
+
[lane, "(a) frozen sections untouched", pass_fail(c[:frozen_untouched])],
|
|
142
|
+
[lane, "(b) no builder commits", pass_fail(c[:no_builder_commits])],
|
|
143
|
+
[lane, "(c) scratch report exists", pass_fail(c[:report_exists])],
|
|
144
|
+
[lane, "(d) in-bounds", pass_fail(c[:in_bounds])]
|
|
145
|
+
]
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
terminal.say terminal.table(%w[Lane Check Result], rows)
|
|
149
|
+
CLI.record_outcome(Outcome.new(exit_code: 0))
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
private
|
|
155
|
+
|
|
156
|
+
def pass_fail(val)
|
|
157
|
+
case val
|
|
158
|
+
when true then "PASS"
|
|
159
|
+
when false then "FAIL"
|
|
160
|
+
else "N/A"
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
class Dispatch < Dry::CLI::Command
|
|
166
|
+
include GlobalOptions
|
|
167
|
+
include Helpers
|
|
168
|
+
|
|
169
|
+
desc "Dispatch a builder for a lane (streams to build/<id>-<lane>/run.jsonl)"
|
|
170
|
+
argument :iteration, required: true, desc: "Iteration name"
|
|
171
|
+
argument :lane, required: true, desc: "Lane name"
|
|
172
|
+
argument :space, required: false, desc: "Space identifier (default: $PWD)"
|
|
173
|
+
option :model, default: nil, desc: "Model to use (default: lane entry or claude-sonnet-4-6)"
|
|
174
|
+
option :max_turns, default: "200", desc: "Max turns for the builder"
|
|
175
|
+
option :harness, default: nil, desc: "Harness override (claude-code, opencode)"
|
|
176
|
+
option :effort, default: nil, desc: "Reasoning effort override (opencode only; sets reasoningEffort in the model config)"
|
|
177
|
+
|
|
178
|
+
def call(iteration:, lane:, space: nil, model: nil,
|
|
179
|
+
max_turns: "200", harness: nil, effort: nil, **opts)
|
|
180
|
+
setup_terminal(**opts.slice(:color, :colors))
|
|
181
|
+
handle_errors do
|
|
182
|
+
render(store.find(space)) do |sp|
|
|
183
|
+
mission = ArchitectMission.new(space: sp)
|
|
184
|
+
kwargs = { max_turns: max_turns.to_i }
|
|
185
|
+
kwargs[:model] = model if model
|
|
186
|
+
kwargs[:harness] = harness if harness
|
|
187
|
+
kwargs[:effort] = effort if effort
|
|
188
|
+
res = mission.dispatch(iteration, lane, **kwargs)
|
|
189
|
+
terminal.say "Run log: #{terminal.path(res[:run_log])}"
|
|
190
|
+
terminal.say "Report: #{terminal.path(res[:report])}"
|
|
191
|
+
terminal.say "Builder exited with status #{res[:exit_code]}"
|
|
192
|
+
CLI.record_outcome(Outcome.new(exit_code: res[:exit_code]))
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
module Worktree
|
|
199
|
+
class Add < Dry::CLI::Command
|
|
200
|
+
include GlobalOptions
|
|
201
|
+
include Helpers
|
|
202
|
+
|
|
203
|
+
desc "Create a worktree for a lane"
|
|
204
|
+
argument :repo, required: true, desc: "Repo name (under repos/)"
|
|
205
|
+
argument :iteration, required: true, desc: "Iteration name"
|
|
206
|
+
argument :lane, required: true, desc: "Lane name"
|
|
207
|
+
option :base, default: nil, desc: "Base ref (default: HEAD of repo)"
|
|
208
|
+
option :harness, default: "claude-code", desc: "Harness (claude-code, opencode)"
|
|
209
|
+
option :model, default: nil, desc: "Model (required for opencode)"
|
|
210
|
+
option :effort, default: nil, desc: "Reasoning effort (opencode only; sets reasoningEffort in the model config)"
|
|
211
|
+
|
|
212
|
+
def call(repo:, iteration:, lane:, base: nil, harness: "claude-code", model: nil, effort: nil, **opts)
|
|
213
|
+
setup_terminal(**opts.slice(:color, :colors))
|
|
214
|
+
handle_errors do
|
|
215
|
+
render(store.find) do |sp|
|
|
216
|
+
mission = ArchitectMission.new(space: sp)
|
|
217
|
+
result = mission.worktree_add(repo, iteration, lane, base: base,
|
|
218
|
+
harness: harness, model: model, effort: effort)
|
|
219
|
+
terminal.say "Worktree: #{terminal.path(result[:worktree])}"
|
|
220
|
+
terminal.say "Base SHA: #{result[:base_sha]}"
|
|
221
|
+
CLI.record_outcome(Outcome.new(exit_code: 0))
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
class Remove < Dry::CLI::Command
|
|
228
|
+
include GlobalOptions
|
|
229
|
+
include Helpers
|
|
230
|
+
|
|
231
|
+
desc "Remove a lane worktree"
|
|
232
|
+
argument :iteration, required: true, desc: "Iteration name"
|
|
233
|
+
argument :lane, required: true, desc: "Lane name"
|
|
234
|
+
|
|
235
|
+
def call(iteration:, lane:, **opts)
|
|
236
|
+
setup_terminal(**opts.slice(:color, :colors))
|
|
237
|
+
handle_errors do
|
|
238
|
+
render(store.find) do |sp|
|
|
239
|
+
mission = ArchitectMission.new(space: sp)
|
|
240
|
+
mission.worktree_remove(iteration, lane)
|
|
241
|
+
terminal.say "Removed worktree for #{iteration}/#{lane}"
|
|
242
|
+
CLI.record_outcome(Outcome.new(exit_code: 0))
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
class List < Dry::CLI::Command
|
|
249
|
+
include GlobalOptions
|
|
250
|
+
include Helpers
|
|
251
|
+
|
|
252
|
+
desc "List active architect worktrees"
|
|
253
|
+
|
|
254
|
+
def call(**opts)
|
|
255
|
+
setup_terminal(**opts.slice(:color, :colors))
|
|
256
|
+
handle_errors do
|
|
257
|
+
render(store.find) do |sp|
|
|
258
|
+
mission = ArchitectMission.new(space: sp)
|
|
259
|
+
worktrees = mission.worktree_list
|
|
260
|
+
if worktrees.empty?
|
|
261
|
+
terminal.say "No active architect worktrees"
|
|
262
|
+
else
|
|
263
|
+
worktrees.each { |wt| terminal.say wt }
|
|
264
|
+
end
|
|
265
|
+
CLI.record_outcome(Outcome.new(exit_code: 0))
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
module Variant
|
|
273
|
+
class Add < Dry::CLI::Command
|
|
274
|
+
include GlobalOptions
|
|
275
|
+
include Helpers
|
|
276
|
+
|
|
277
|
+
desc "Create a variant set (competing lanes over one frozen spec)"
|
|
278
|
+
argument :repo, required: true, desc: "Repo name (under repos/)"
|
|
279
|
+
argument :iteration, required: true, desc: "Iteration name"
|
|
280
|
+
argument :space, required: false, desc: "Space identifier (default: $PWD)"
|
|
281
|
+
option :pairs, required: true, desc: "Comma-separated harness[:model] pairs (e.g. claude-code,opencode:fireworks-ai/accounts/fireworks/models/glm-5p2)"
|
|
282
|
+
option :base, default: nil, desc: "Base ref (default: HEAD of repo)"
|
|
283
|
+
option :prompt, default: nil, desc: "Prompt file to fan-out byte-identical to each variant"
|
|
284
|
+
|
|
285
|
+
def call(repo:, iteration:, space: nil, pairs:, base: nil, prompt: nil, **opts)
|
|
286
|
+
setup_terminal(**opts.slice(:color, :colors))
|
|
287
|
+
handle_errors do
|
|
288
|
+
render(store.find(space)) do |sp|
|
|
289
|
+
parsed_pairs = pairs.to_s.split(",").map do |spec|
|
|
290
|
+
harness, model = spec.split(":", 2)
|
|
291
|
+
model = nil if model.nil? || model.empty?
|
|
292
|
+
[harness, model]
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
mission = ArchitectMission.new(space: sp)
|
|
296
|
+
variants = mission.variant_add(repo, iteration, parsed_pairs, base: base, prompt: prompt)
|
|
297
|
+
variants.each do |v|
|
|
298
|
+
terminal.say "#{v[:name]} · #{v[:harness]} · #{v[:model] || "(default)"} · #{terminal.path(v[:worktree])}"
|
|
299
|
+
end
|
|
300
|
+
CLI.record_outcome(Outcome.new(exit_code: 0))
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
class Promote < Dry::CLI::Command
|
|
307
|
+
include GlobalOptions
|
|
308
|
+
include Helpers
|
|
309
|
+
|
|
310
|
+
desc "Promote one variant of a variant set as the winner"
|
|
311
|
+
argument :iteration, required: true, desc: "Iteration name"
|
|
312
|
+
argument :winner, required: true, desc: "Variant lane name to promote (e.g. v02)"
|
|
313
|
+
argument :space, required: false, desc: "Space identifier (default: $PWD)"
|
|
314
|
+
|
|
315
|
+
def call(iteration:, winner:, space: nil, **opts)
|
|
316
|
+
setup_terminal(**opts.slice(:color, :colors))
|
|
317
|
+
handle_errors do
|
|
318
|
+
render(store.find(space)) do |sp|
|
|
319
|
+
mission = ArchitectMission.new(space: sp)
|
|
320
|
+
result = mission.variant_promote(iteration, winner)
|
|
321
|
+
if result[:discarded].any?
|
|
322
|
+
terminal.say "Promoted #{result[:winner]} (discarded: #{result[:discarded].join(', ')})"
|
|
323
|
+
else
|
|
324
|
+
terminal.say "Promoted #{result[:winner]}"
|
|
325
|
+
end
|
|
326
|
+
CLI.record_outcome(Outcome.new(exit_code: 0))
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
class Compare < Dry::CLI::Command
|
|
333
|
+
include GlobalOptions
|
|
334
|
+
include Helpers
|
|
335
|
+
|
|
336
|
+
desc "Compare variants of an iteration's variant set (read-only)"
|
|
337
|
+
argument :iteration, required: true, desc: "Iteration name"
|
|
338
|
+
argument :space, required: false, desc: "Space identifier (default: $PWD)"
|
|
339
|
+
|
|
340
|
+
def call(iteration:, space: nil, **opts)
|
|
341
|
+
setup_terminal(**opts.slice(:color, :colors))
|
|
342
|
+
handle_errors do
|
|
343
|
+
render(store.find(space)) do |sp|
|
|
344
|
+
mission = ArchitectMission.new(space: sp)
|
|
345
|
+
info = mission.variant_compare(iteration)
|
|
346
|
+
|
|
347
|
+
terminal.say "Variant comparison: #{iteration} (freeze #{info[:freeze_sha]&.[](0, 8) || "-"})"
|
|
348
|
+
terminal.say "Winner: #{info[:winner] || '(none)'}"
|
|
349
|
+
terminal.say ""
|
|
350
|
+
rows = info[:variants].map do |v|
|
|
351
|
+
[
|
|
352
|
+
v[:name],
|
|
353
|
+
v[:harness],
|
|
354
|
+
v[:model] || "(default)",
|
|
355
|
+
v[:effort] || "-",
|
|
356
|
+
v[:status] == "winner" ? "WINNER" : v[:status],
|
|
357
|
+
v[:integration_branch] || "-",
|
|
358
|
+
v[:base_sha]&.[](0, 8) || "-"
|
|
359
|
+
]
|
|
360
|
+
end
|
|
361
|
+
terminal.say terminal.table(%w[Variant Harness Model Effort Status Integration Base], rows)
|
|
362
|
+
|
|
363
|
+
CLI.record_outcome(Outcome.new(exit_code: 0))
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
end
|
|
367
|
+
end
|
|
368
|
+
end
|
|
369
|
+
end
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
SpaceArchitect::CLI::Registry.register "init", SpaceArchitect::CLI::Architect::Init
|
|
374
|
+
SpaceArchitect::CLI::Registry.register "new", SpaceArchitect::CLI::Architect::New
|
|
375
|
+
SpaceArchitect::CLI::Registry.register "status", SpaceArchitect::CLI::Architect::Status
|
|
376
|
+
SpaceArchitect::CLI::Registry.register "freeze", SpaceArchitect::CLI::Architect::Freeze
|
|
377
|
+
SpaceArchitect::CLI::Registry.register "verify", SpaceArchitect::CLI::Architect::Verify
|
|
378
|
+
SpaceArchitect::CLI::Registry.register "dispatch", SpaceArchitect::CLI::Architect::Dispatch
|
|
379
|
+
SpaceArchitect::CLI::Registry.register "worktree" do |wt|
|
|
380
|
+
wt.register "add", SpaceArchitect::CLI::Architect::Worktree::Add
|
|
381
|
+
wt.register "remove", SpaceArchitect::CLI::Architect::Worktree::Remove
|
|
382
|
+
wt.register "list", SpaceArchitect::CLI::Architect::Worktree::List
|
|
383
|
+
end
|
|
384
|
+
SpaceArchitect::CLI::Registry.register "variant" do |v|
|
|
385
|
+
v.register "add", SpaceArchitect::CLI::Architect::Variant::Add
|
|
386
|
+
v.register "promote", SpaceArchitect::CLI::Architect::Variant::Promote
|
|
387
|
+
v.register "compare", SpaceArchitect::CLI::Architect::Variant::Compare
|
|
388
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SpaceArchitect
|
|
4
|
+
module CLI
|
|
5
|
+
module Config
|
|
6
|
+
class Show < Dry::CLI::Command
|
|
7
|
+
include GlobalOptions
|
|
8
|
+
include Helpers
|
|
9
|
+
|
|
10
|
+
desc "Show current config"
|
|
11
|
+
|
|
12
|
+
def call(**opts)
|
|
13
|
+
setup_terminal(**opts.slice(:color, :colors))
|
|
14
|
+
handle_errors do
|
|
15
|
+
rows = SpaceArchitect::Config::EDITABLE_KEYS.map do |key|
|
|
16
|
+
value = project_config.data[key]
|
|
17
|
+
[key, value.nil? ? "" : value.to_s]
|
|
18
|
+
end
|
|
19
|
+
terminal.say terminal.table(%w[Key Value], rows)
|
|
20
|
+
CLI.record_outcome(Outcome.new(exit_code: 0))
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
class ConfigPath < Dry::CLI::Command
|
|
26
|
+
include GlobalOptions
|
|
27
|
+
include Helpers
|
|
28
|
+
|
|
29
|
+
desc "Print the config file path"
|
|
30
|
+
|
|
31
|
+
def call(**opts)
|
|
32
|
+
setup_terminal(**opts.slice(:color, :colors))
|
|
33
|
+
handle_errors do
|
|
34
|
+
terminal.say terminal.path(project_config.path)
|
|
35
|
+
CLI.record_outcome(Outcome.new(exit_code: 0))
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
class Set < Dry::CLI::Command
|
|
41
|
+
include GlobalOptions
|
|
42
|
+
include Helpers
|
|
43
|
+
|
|
44
|
+
desc "Set a config key"
|
|
45
|
+
argument :key, required: true, desc: "Config key"
|
|
46
|
+
argument :value, required: true, desc: "Config value"
|
|
47
|
+
|
|
48
|
+
def call(key:, value:, **opts)
|
|
49
|
+
setup_terminal(**opts.slice(:color, :colors))
|
|
50
|
+
handle_errors do
|
|
51
|
+
project_config.set(key, value)
|
|
52
|
+
stored = project_config.data[key]
|
|
53
|
+
terminal.success "Set #{key}=#{stored.nil? ? '' : stored.to_s}"
|
|
54
|
+
CLI.record_outcome(Outcome.new(exit_code: 0))
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SpaceArchitect
|
|
4
|
+
module CLI
|
|
5
|
+
class Current < Dry::CLI::Command
|
|
6
|
+
include GlobalOptions
|
|
7
|
+
include Helpers
|
|
8
|
+
|
|
9
|
+
desc "Show the current space"
|
|
10
|
+
|
|
11
|
+
def call(**opts)
|
|
12
|
+
setup_terminal(**opts.slice(:color, :colors))
|
|
13
|
+
render(store.find) do |space|
|
|
14
|
+
terminal.say space.id.to_s
|
|
15
|
+
terminal.say terminal.path(space.path)
|
|
16
|
+
CLI.record_outcome(Outcome.new(exit_code: 0))
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "dry/monads"
|
|
4
|
+
|
|
5
|
+
module SpaceArchitect
|
|
6
|
+
module CLI
|
|
7
|
+
module Helpers
|
|
8
|
+
include Dry::Monads[:result]
|
|
9
|
+
def project_config
|
|
10
|
+
@project_config ||= SpaceArchitect::Config.load
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def state
|
|
14
|
+
@state ||= SpaceArchitect::State.load
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def store
|
|
18
|
+
@store ||= SpaceArchitect::SpaceStore.new(config: project_config, state: state)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def terminal
|
|
22
|
+
@terminal
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def setup_terminal(color: "auto", colors: nil)
|
|
26
|
+
@terminal = Terminal.new(
|
|
27
|
+
config: project_config,
|
|
28
|
+
stdout: out,
|
|
29
|
+
stderr: err,
|
|
30
|
+
color_mode: colors || color || "auto"
|
|
31
|
+
)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def display_date(space)
|
|
35
|
+
id_date = space.id.match(/\A(\d{4})(\d{2})(\d{2})/)
|
|
36
|
+
return "#{id_date[1]}-#{id_date[2]}-#{id_date[3]}" if id_date
|
|
37
|
+
|
|
38
|
+
space.data["created_at"].to_s[0, 10]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def handle_errors
|
|
42
|
+
yield
|
|
43
|
+
rescue SpaceArchitect::Error => e
|
|
44
|
+
if terminal
|
|
45
|
+
terminal.error(e.message)
|
|
46
|
+
else
|
|
47
|
+
err.puts e.message
|
|
48
|
+
end
|
|
49
|
+
CLI.record_outcome(Outcome.new(exit_code: 1, message: e.message))
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def render(result)
|
|
53
|
+
case result
|
|
54
|
+
when Dry::Monads::Result::Success
|
|
55
|
+
yield result.value! if block_given?
|
|
56
|
+
when Dry::Monads::Result::Failure
|
|
57
|
+
error = result.failure
|
|
58
|
+
message = error.respond_to?(:message) ? error.message : error.to_s
|
|
59
|
+
terminal ? terminal.error(message) : err.puts(message)
|
|
60
|
+
CLI.record_outcome(Outcome.new(exit_code: 1, message: message))
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
class RepoProgress
|
|
66
|
+
def initialize(total)
|
|
67
|
+
@total = total
|
|
68
|
+
@statuses = {}
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def start(addition)
|
|
72
|
+
source = addition[:src_source]
|
|
73
|
+
@statuses[addition.fetch(:reference).full_name] = source&.directory? ? :copying : :cloning
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def trust(addition)
|
|
77
|
+
@statuses[addition.fetch(:reference).full_name] = :trusting
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def finish(addition)
|
|
81
|
+
@statuses[addition.fetch(:reference).full_name] = :done
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def fail(addition)
|
|
85
|
+
@statuses[addition.fetch(:reference).full_name] = :failed
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def message
|
|
89
|
+
done = @statuses.count { |_repo, status| status == :done }
|
|
90
|
+
failed = @statuses.count { |_repo, status| status == :failed }
|
|
91
|
+
copying = @statuses.select { |_repo, status| status == :copying }.keys
|
|
92
|
+
cloning = @statuses.select { |_repo, status| status == :cloning }.keys
|
|
93
|
+
trusting = @statuses.select { |_repo, status| status == :trusting }.keys
|
|
94
|
+
|
|
95
|
+
if @total == 1
|
|
96
|
+
copying_repo = copying.first
|
|
97
|
+
cloning_repo = cloning.first
|
|
98
|
+
trusting_repo = trusting.first
|
|
99
|
+
return "Copying #{copying_repo}" if copying_repo
|
|
100
|
+
return "Cloning #{cloning_repo}" if cloning_repo
|
|
101
|
+
return "Trusting #{trusting_repo}" if trusting_repo
|
|
102
|
+
return "Fetch failed" if failed.positive?
|
|
103
|
+
|
|
104
|
+
"Preparing repos"
|
|
105
|
+
else
|
|
106
|
+
active = []
|
|
107
|
+
active << "copying #{copying.join(', ')}" unless copying.empty?
|
|
108
|
+
active << "cloning #{cloning.join(', ')}" unless cloning.empty?
|
|
109
|
+
active << "trusting #{trusting.join(', ')}" unless trusting.empty?
|
|
110
|
+
suffix = active.empty? ? nil : ": #{active.join('; ')}"
|
|
111
|
+
failed_text = failed.positive? ? ", #{failed} failed" : ""
|
|
112
|
+
"Fetching repos #{done}/#{@total}#{failed_text}#{suffix}"
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SpaceArchitect
|
|
4
|
+
module CLI
|
|
5
|
+
class Init < Dry::CLI::Command
|
|
6
|
+
include GlobalOptions
|
|
7
|
+
include Helpers
|
|
8
|
+
|
|
9
|
+
desc "Create default XDG config and state files"
|
|
10
|
+
option :force, type: :boolean, default: false, desc: "Overwrite existing config and state files"
|
|
11
|
+
|
|
12
|
+
def call(force: false, **opts)
|
|
13
|
+
setup_terminal(**opts.slice(:color, :colors))
|
|
14
|
+
handle_errors do
|
|
15
|
+
if force
|
|
16
|
+
@project_config = SpaceArchitect::Config.new
|
|
17
|
+
@state = SpaceArchitect::State.new
|
|
18
|
+
project_config.save
|
|
19
|
+
state.save
|
|
20
|
+
else
|
|
21
|
+
project_config.ensure_exists!
|
|
22
|
+
state.ensure_exists!
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
FileUtils.mkdir_p(project_config.spaces_dir)
|
|
26
|
+
terminal.success "Config: #{terminal.path(project_config.path)}"
|
|
27
|
+
terminal.success "State: #{terminal.path(state.path)}"
|
|
28
|
+
terminal.success "Spaces: #{terminal.path(project_config.spaces_dir)}"
|
|
29
|
+
CLI.record_outcome(Outcome.new(exit_code: 0))
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SpaceArchitect
|
|
4
|
+
module CLI
|
|
5
|
+
class List < Dry::CLI::Command
|
|
6
|
+
include GlobalOptions
|
|
7
|
+
include Helpers
|
|
8
|
+
|
|
9
|
+
desc "List spaces"
|
|
10
|
+
|
|
11
|
+
def call(**opts)
|
|
12
|
+
setup_terminal(**opts.slice(:color, :colors))
|
|
13
|
+
handle_errors do
|
|
14
|
+
spaces = store.list
|
|
15
|
+
if spaces.empty?
|
|
16
|
+
terminal.say "No spaces found in #{terminal.path(project_config.spaces_dir)}"
|
|
17
|
+
next
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
rows = spaces.map do |space|
|
|
21
|
+
[space.status, display_date(space), space.title, terminal.path(space.path)]
|
|
22
|
+
end
|
|
23
|
+
terminal.say terminal.table(%w[Status Date Title Path], rows)
|
|
24
|
+
CLI.record_outcome(Outcome.new(exit_code: 0))
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|