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