space-architect 8.0.0 → 9.0.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 (102) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +54 -0
  3. data/README.md +53 -243
  4. data/lib/space_architect/bug_report.rb +2 -1
  5. data/lib/space_architect/cli/architect.rb +20 -7
  6. data/lib/space_architect/cli.rb +9 -14
  7. data/lib/space_architect/session_sync/bin_path.rb +1 -1
  8. data/lib/space_architect/session_sync/cursor.rb +1 -1
  9. data/lib/space_architect/session_sync/plist.rb +1 -1
  10. data/lib/{space_core → space_architect}/version.rb +2 -2
  11. data/lib/space_architect.rb +1 -1
  12. data/skill/architect/SKILL.md +3 -2
  13. data/skill/architect-vocabulary/SKILL.md +2 -2
  14. metadata +18 -177
  15. data/exe/space +0 -13
  16. data/exe/src +0 -13
  17. data/lib/space_architect/cli/space.rb +0 -29
  18. data/lib/space_architect/cli/src.rb +0 -38
  19. data/lib/space_core/atomic_write.rb +0 -21
  20. data/lib/space_core/cli/base_command.rb +0 -19
  21. data/lib/space_core/cli/build.rb +0 -27
  22. data/lib/space_core/cli/config.rb +0 -49
  23. data/lib/space_core/cli/current.rb +0 -16
  24. data/lib/space_core/cli/help.rb +0 -195
  25. data/lib/space_core/cli/helpers.rb +0 -115
  26. data/lib/space_core/cli/init.rb +0 -29
  27. data/lib/space_core/cli/list.rb +0 -24
  28. data/lib/space_core/cli/loop_status.rb +0 -47
  29. data/lib/space_core/cli/new.rb +0 -38
  30. data/lib/space_core/cli/pack.rb +0 -29
  31. data/lib/space_core/cli/path.rb +0 -16
  32. data/lib/space_core/cli/repeatable_options.rb +0 -84
  33. data/lib/space_core/cli/repo.rb +0 -76
  34. data/lib/space_core/cli/run.rb +0 -44
  35. data/lib/space_core/cli/shell.rb +0 -125
  36. data/lib/space_core/cli/show.rb +0 -21
  37. data/lib/space_core/cli/status.rb +0 -74
  38. data/lib/space_core/cli/use.rb +0 -17
  39. data/lib/space_core/cli.rb +0 -177
  40. data/lib/space_core/commands.rb +0 -51
  41. data/lib/space_core/config.rb +0 -152
  42. data/lib/space_core/errors.rb +0 -14
  43. data/lib/space_core/git_client.rb +0 -49
  44. data/lib/space_core/mise_client.rb +0 -37
  45. data/lib/space_core/oci_builder.rb +0 -56
  46. data/lib/space_core/oci_packer.rb +0 -103
  47. data/lib/space_core/oci_runner.rb +0 -89
  48. data/lib/space_core/paths.rb +0 -58
  49. data/lib/space_core/repo_reference.rb +0 -19
  50. data/lib/space_core/repo_resolver.rb +0 -167
  51. data/lib/space_core/shell_integration.rb +0 -438
  52. data/lib/space_core/slugger.rb +0 -16
  53. data/lib/space_core/space.rb +0 -174
  54. data/lib/space_core/space_store.rb +0 -319
  55. data/lib/space_core/state.rb +0 -86
  56. data/lib/space_core/templates/oci/dockerfile.erb +0 -73
  57. data/lib/space_core/templates/oci/dockerignore.erb +0 -17
  58. data/lib/space_core/templates/oci/entrypoint.sh.erb +0 -16
  59. data/lib/space_core/terminal.rb +0 -161
  60. data/lib/space_core/warnings.rb +0 -13
  61. data/lib/space_core/xdg.rb +0 -33
  62. data/lib/space_core.rb +0 -26
  63. data/lib/space_src/cli/clone.rb +0 -54
  64. data/lib/space_src/cli/config.rb +0 -65
  65. data/lib/space_src/cli/daemon.rb +0 -362
  66. data/lib/space_src/cli/options.rb +0 -21
  67. data/lib/space_src/cli/org.rb +0 -199
  68. data/lib/space_src/cli/repo.rb +0 -169
  69. data/lib/space_src/cli/shell.rb +0 -121
  70. data/lib/space_src/cli/status.rb +0 -75
  71. data/lib/space_src/cli/sync.rb +0 -161
  72. data/lib/space_src/cli.rb +0 -159
  73. data/lib/space_src/cloner.rb +0 -77
  74. data/lib/space_src/config/contract.rb +0 -54
  75. data/lib/space_src/config/duration.rb +0 -79
  76. data/lib/space_src/config/model.rb +0 -49
  77. data/lib/space_src/config/store.rb +0 -156
  78. data/lib/space_src/forge/client.rb +0 -31
  79. data/lib/space_src/forge/github.rb +0 -98
  80. data/lib/space_src/launchd/agent.rb +0 -195
  81. data/lib/space_src/launchd/plist.rb +0 -129
  82. data/lib/space_src/log_rotator.rb +0 -46
  83. data/lib/space_src/migration.rb +0 -43
  84. data/lib/space_src/nav.rb +0 -99
  85. data/lib/space_src/paths.rb +0 -72
  86. data/lib/space_src/scm/client.rb +0 -87
  87. data/lib/space_src/scm/git.rb +0 -232
  88. data/lib/space_src/scm/status.rb +0 -24
  89. data/lib/space_src/shell.rb +0 -90
  90. data/lib/space_src/shell_integration.rb +0 -321
  91. data/lib/space_src/state/lock.rb +0 -59
  92. data/lib/space_src/state/store.rb +0 -140
  93. data/lib/space_src/sync/engine.rb +0 -503
  94. data/lib/space_src/sync/repo_plan.rb +0 -215
  95. data/lib/space_src/sync/report.rb +0 -18
  96. data/lib/space_src/ui/interactive_reporter.rb +0 -280
  97. data/lib/space_src/ui/json_reporter.rb +0 -39
  98. data/lib/space_src/ui/mode.rb +0 -68
  99. data/lib/space_src/ui/plain_reporter.rb +0 -53
  100. data/lib/space_src/ui/reporter.rb +0 -48
  101. data/lib/space_src/version.rb +0 -7
  102. data/lib/space_src.rb +0 -38
@@ -1,321 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "fileutils"
4
- require "pathname"
5
-
6
- module Space::Src
7
- # Generates the standalone `src` fish shell integration and completions.
8
- # All paths resolved through Space::Src::Paths (no external core deps).
9
- module ShellIntegration
10
- FISH_TEMPLATE = <<~'FISH'
11
- # Generated by space-src. Do not edit by hand.
12
-
13
- function src --wraps src --description "Navigate source checkouts"
14
- if not set -q __src_compat_checked
15
- set -g __src_compat_checked 1
16
- set -l __src_installed_version __SRC_VERSION__
17
- set -l __src_binary_version (command src --version 2>/dev/null)
18
- if test "$__src_binary_version" != "$__src_installed_version"
19
- echo "space-src: shell integration version $__src_installed_version does not match binary version $__src_binary_version; re-run 'src shell fish install'" >&2
20
- end
21
- end
22
-
23
- switch "$argv[1]"
24
- case repo org sync status config daemon clone shell
25
- command src $argv
26
- return $status
27
- case '*'
28
- set -l __src_output (command src $argv)
29
- set -l __src_status $status
30
-
31
- if test (count $__src_output) -gt 0
32
- printf "%s\n" $__src_output
33
- end
34
-
35
- if test $__src_status -eq 0
36
- set -l __src_target $__src_output[-1]
37
- set __src_target (string replace -r "^~(?=/|$)" $HOME -- $__src_target)
38
- if test -d "$__src_target"
39
- cd "$__src_target"
40
- end
41
- end
42
-
43
- return $__src_status
44
- end
45
- end
46
- FISH
47
-
48
- FISH_COMPLETIONS = <<~'FISH'
49
- # Generated by space-src. Do not edit by hand.
50
-
51
- function __src_complete_command
52
- set -l tokens (commandline -opc)
53
- set -l index 2
54
-
55
- while test $index -le (count $tokens)
56
- set -l token $tokens[$index]
57
-
58
- switch "$token"
59
- case "-*"
60
- set index (math $index + 1)
61
- continue
62
- case "*"
63
- echo $token
64
- return 0
65
- end
66
- end
67
- end
68
-
69
- function __src_complete_needs_command
70
- test -z "(__src_complete_command)"
71
- end
72
-
73
- function __src_complete_using_command
74
- contains -- (__src_complete_command) $argv
75
- end
76
-
77
- function __src_complete_first_argument_after
78
- set -l commands $argv
79
- set -l tokens (commandline -opc)
80
- set -l index 2
81
- set -l matched_command 0
82
-
83
- while test $index -le (count $tokens)
84
- set -l token $tokens[$index]
85
-
86
- switch "$token"
87
- case "-*"
88
- set index (math $index + 1)
89
- continue
90
- end
91
-
92
- if test $matched_command -eq 0
93
- if contains -- "$token" $commands
94
- set matched_command 1
95
- else
96
- return 1
97
- end
98
- else
99
- echo $token
100
- return 0
101
- end
102
-
103
- set index (math $index + 1)
104
- end
105
-
106
- return 1
107
- end
108
-
109
- function __src_complete_has_first_argument_after
110
- set -q argv[1]; or return 1
111
- set -l first_argument (__src_complete_first_argument_after $argv)
112
- test -n "$first_argument"
113
- end
114
-
115
- function __src_complete_second_argument_after
116
- set -q argv[1]; or return 1
117
- set -l command $argv[1]
118
- set -l tokens (commandline -opc)
119
- set -l index 2
120
- set -l matched_command 0
121
- set -l matched_first_argument 0
122
-
123
- while test $index -le (count $tokens)
124
- set -l token $tokens[$index]
125
-
126
- switch "$token"
127
- case "-*"
128
- set index (math $index + 1)
129
- continue
130
- end
131
-
132
- if test $matched_command -eq 0
133
- test "$token" = "$command"; or return 1
134
- set matched_command 1
135
- else if test $matched_first_argument -eq 0
136
- set matched_first_argument 1
137
- else
138
- echo $token
139
- return 0
140
- end
141
-
142
- set index (math $index + 1)
143
- end
144
-
145
- return 1
146
- end
147
-
148
- function __src_complete_has_second_argument_after
149
- set -q argv[1]; or return 1
150
- set -l second_argument (__src_complete_second_argument_after $argv)
151
- test -n "$second_argument"
152
- end
153
-
154
- function __src_complete_first_argument_is
155
- set -q argv[1]; or return 1
156
- set -l expected $argv[1]
157
- set -e argv[1]
158
- test "$(__src_complete_first_argument_after $argv)" = "$expected"
159
- end
160
-
161
- function __src_complete_checkouts
162
- command src shell complete checkouts 2>/dev/null
163
- end
164
-
165
- function __src_complete_shells
166
- command src shell complete shells 2>/dev/null
167
- end
168
-
169
- complete -c src -f -n "__src_complete_needs_command" -a clone -d "Clone a repo into the source tree"
170
- complete -c src -f -n "__src_complete_needs_command" -a config -d "Show or update config"
171
- complete -c src -f -n "__src_complete_needs_command" -a daemon -d "Manage the background sync daemon"
172
- complete -c src -f -n "__src_complete_needs_command" -a org -d "Manage tracked GitHub orgs"
173
- complete -c src -f -n "__src_complete_needs_command" -a repo -d "Manage tracked repos"
174
- complete -c src -f -n "__src_complete_needs_command" -a shell -d "Manage shell integration"
175
- complete -c src -f -n "__src_complete_needs_command" -a status -d "Show sync status"
176
- complete -c src -f -n "__src_complete_needs_command" -a sync -d "Sync all tracked repos"
177
- complete -c src -f -n "__src_complete_needs_command" -a "(__src_complete_checkouts)" -d "Checkout"
178
-
179
- complete -c src -f -n "__src_complete_using_command config; and not __src_complete_has_first_argument_after config" -a path -d "Print config path"
180
- complete -c src -f -n "__src_complete_using_command config; and not __src_complete_has_first_argument_after config" -a show -d "Show config"
181
-
182
- complete -c src -f -n "__src_complete_using_command org; and not __src_complete_has_first_argument_after org" -a add -d "Add a GitHub org"
183
- complete -c src -f -n "__src_complete_using_command org; and not __src_complete_has_first_argument_after org" -a remove -d "Remove a GitHub org"
184
- complete -c src -f -n "__src_complete_using_command org; and not __src_complete_has_first_argument_after org" -a list -d "List tracked orgs"
185
-
186
- complete -c src -f -n "__src_complete_using_command repo; and not __src_complete_has_first_argument_after repo" -a add -d "Add a repo"
187
- complete -c src -f -n "__src_complete_using_command repo; and not __src_complete_has_first_argument_after repo" -a remove -d "Remove a repo"
188
- complete -c src -f -n "__src_complete_using_command repo; and not __src_complete_has_first_argument_after repo" -a list -d "List repos"
189
-
190
- complete -c src -f -n "__src_complete_using_command daemon; and not __src_complete_has_first_argument_after daemon" -a install -d "Install launchd agent"
191
- complete -c src -f -n "__src_complete_using_command daemon; and not __src_complete_has_first_argument_after daemon" -a uninstall -d "Uninstall launchd agent"
192
- complete -c src -f -n "__src_complete_using_command daemon; and not __src_complete_has_first_argument_after daemon" -a start -d "Start the agent"
193
- complete -c src -f -n "__src_complete_using_command daemon; and not __src_complete_has_first_argument_after daemon" -a stop -d "Stop the agent"
194
- complete -c src -f -n "__src_complete_using_command daemon; and not __src_complete_has_first_argument_after daemon" -a restart -d "Restart the agent"
195
- complete -c src -f -n "__src_complete_using_command daemon; and not __src_complete_has_first_argument_after daemon" -a status -d "Show agent status"
196
-
197
- complete -c src -f -n "__src_complete_using_command shell; and not __src_complete_has_first_argument_after shell" -a init -d "Print shell integration"
198
- complete -c src -f -n "__src_complete_using_command shell; and not __src_complete_has_first_argument_after shell" -a fish -d "Manage fish integration and completions"
199
- complete -c src -f -n "__src_complete_using_command shell; and not __src_complete_has_first_argument_after shell" -a complete -d "Print completion candidates"
200
-
201
- complete -c src -f -n "__src_complete_first_argument_is fish shell; and not __src_complete_has_second_argument_after shell" -a install -d "Install fish integration and completions"
202
- complete -c src -f -n "__src_complete_first_argument_is fish shell; and not __src_complete_has_second_argument_after shell" -a uninstall -d "Remove fish integration and completions"
203
- complete -c src -f -n "__src_complete_first_argument_is fish shell; and not __src_complete_has_second_argument_after shell" -a path -d "Print fish integration paths"
204
-
205
- complete -c src -f -n "__src_complete_first_argument_is init shell; and not __src_complete_has_second_argument_after shell" -a "(__src_complete_shells)" -d "Shell"
206
- complete -c src -f -n "__src_complete_first_argument_is complete shell; and not __src_complete_has_second_argument_after shell" -a "checkouts shells" -d "Completion kind"
207
- FISH
208
-
209
- def self.for(shell)
210
- case shell.to_s
211
- when "fish"
212
- FISH_TEMPLATE.gsub("__SRC_VERSION__", Space::Src::VERSION)
213
- else
214
- raise "Unsupported shell '#{shell}'. Expected: fish"
215
- end
216
- end
217
-
218
- def self.completions_for(shell)
219
- case shell.to_s
220
- when "fish"
221
- FISH_COMPLETIONS
222
- else
223
- raise "Unsupported shell '#{shell}'. Expected: fish"
224
- end
225
- end
226
-
227
- def self.path_for(shell, env: ENV)
228
- case shell.to_s
229
- when "fish"
230
- Pathname(Paths.new(environment: env).config_home).join("fish", "functions", "src.fish")
231
- else
232
- raise "Unsupported shell '#{shell}'. Expected: fish"
233
- end
234
- end
235
-
236
- def self.completions_path_for(shell, env: ENV)
237
- case shell.to_s
238
- when "fish"
239
- Pathname(Paths.new(environment: env).config_home).join("fish", "completions", "src.fish")
240
- else
241
- raise "Unsupported shell '#{shell}'. Expected: fish"
242
- end
243
- end
244
-
245
- def self.install(shell, env: ENV, force: false)
246
- fn_result = write_managed_file(
247
- path: path_for(shell, env:),
248
- content: self.for(shell),
249
- force: force,
250
- description: "fish function"
251
- )
252
- co_result = write_managed_file(
253
- path: completions_path_for(shell, env:),
254
- content: completions_for(shell),
255
- force: force,
256
- description: "fish completions"
257
- )
258
- fn_result.merge(
259
- completions_action: co_result.fetch(:action),
260
- completions_path: co_result.fetch(:path)
261
- )
262
- end
263
-
264
- def self.uninstall(shell, env: ENV, force: false)
265
- fn_result = remove_managed_file(
266
- path: path_for(shell, env:),
267
- content: self.for(shell),
268
- force: force,
269
- description: "fish function"
270
- )
271
- co_result = remove_managed_file(
272
- path: completions_path_for(shell, env:),
273
- content: completions_for(shell),
274
- force: force,
275
- description: "fish completions"
276
- )
277
- fn_result.merge(
278
- completions_action: co_result.fetch(:action),
279
- completions_path: co_result.fetch(:path)
280
- )
281
- end
282
-
283
- def self.managed_src?(content)
284
- content.include?("Generated by space-src")
285
- end
286
-
287
- private_class_method def self.write_managed_file(path:, content:, force:, description:)
288
- existing = File.read(path) if File.exist?(path)
289
-
290
- if existing && existing != content && !force && !managed_src?(existing)
291
- raise "Refusing to overwrite existing #{description} at #{path}. Re-run with --force."
292
- end
293
-
294
- if existing == content
295
- {action: :unchanged, path: path}
296
- else
297
- FileUtils.mkdir_p(File.dirname(path))
298
- tmp = "#{path}.tmp.#{Process.pid}"
299
- begin
300
- File.write(tmp, content)
301
- File.rename(tmp, path)
302
- ensure
303
- File.delete(tmp) if File.exist?(tmp)
304
- end
305
- {action: existing ? :updated : :installed, path: path}
306
- end
307
- end
308
-
309
- private_class_method def self.remove_managed_file(path:, content:, force:, description:)
310
- return {action: :missing, path: path} unless File.exist?(path)
311
-
312
- existing = File.read(path)
313
- if existing != content && !force && !managed_src?(existing)
314
- raise "Refusing to remove existing #{description} at #{path}. Re-run with --force."
315
- end
316
-
317
- FileUtils.rm_f(path)
318
- {action: :removed, path: path}
319
- end
320
- end
321
- end
@@ -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