space-architect 8.0.0 → 9.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 +4 -4
- data/CHANGELOG.md +54 -0
- data/README.md +39 -464
- data/lib/space_architect/architect_project.rb +90 -39
- data/lib/space_architect/bug_report.rb +2 -1
- data/lib/space_architect/cli/architect.rb +189 -11
- data/lib/space_architect/cli/research.rb +1 -1
- data/lib/space_architect/cli.rb +9 -14
- data/lib/space_architect/loop_cost.rb +592 -0
- data/lib/space_architect/research/supervisor.rb +9 -2
- data/lib/space_architect/routing.rb +149 -0
- 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 +2 -1
- data/skill/architect/SKILL.md +3 -2
- data/skill/architect-vocabulary/SKILL.md +2 -2
- metadata +20 -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
data/lib/space_src/scm/git.rb
DELETED
|
@@ -1,232 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "fileutils"
|
|
4
|
-
require "time"
|
|
5
|
-
require "space_src/scm/client"
|
|
6
|
-
require "space_src/scm/status"
|
|
7
|
-
require "space_src/shell"
|
|
8
|
-
|
|
9
|
-
module Space::Src
|
|
10
|
-
module SCM
|
|
11
|
-
# Git CLI implementation of SCM::Client. All subprocess work is
|
|
12
|
-
# delegated to Shell.run (which requires an ambient Async::Task).
|
|
13
|
-
class Git < Client
|
|
14
|
-
# Resolve the bare remote's HEAD. First try the local
|
|
15
|
-
# `origin/HEAD` symbolic ref; if missing/stale, do a one-shot
|
|
16
|
-
# `git remote set-head origin -a` (network) to refresh it, then
|
|
17
|
-
# re-read. This is the gotcha path from AGENTS.md: a plain
|
|
18
|
-
# `git fetch` does NOT update `origin/HEAD`.
|
|
19
|
-
def default_branch(path)
|
|
20
|
-
symbolic = read_origin_head(path)
|
|
21
|
-
return Dry::Monads::Success(symbolic) if symbolic
|
|
22
|
-
|
|
23
|
-
refresh = Shell.run("git", "remote", "set-head", "origin", "-a", chdir: path)
|
|
24
|
-
return refresh if refresh.failure?
|
|
25
|
-
|
|
26
|
-
resolved = read_origin_head(path)
|
|
27
|
-
if resolved
|
|
28
|
-
Dry::Monads::Success(resolved)
|
|
29
|
-
else
|
|
30
|
-
Dry::Monads::Failure({path: path, reason: "could not resolve origin/HEAD after set-head -a"})
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def current_branch(path)
|
|
35
|
-
# `git symbolic-ref --short HEAD` exits non-zero on detached HEAD.
|
|
36
|
-
result = Shell.run("git", "symbolic-ref", "--short", "HEAD", chdir: path)
|
|
37
|
-
if result.success?
|
|
38
|
-
Dry::Monads::Success(result.success.strip)
|
|
39
|
-
else
|
|
40
|
-
# Detached HEAD is not a hard failure — report nil.
|
|
41
|
-
head = Shell.run("git", "rev-parse", "--verify", "HEAD", chdir: path)
|
|
42
|
-
if head.success?
|
|
43
|
-
Dry::Monads::Success(nil)
|
|
44
|
-
else
|
|
45
|
-
Dry::Monads::Failure({path: path, reason: "no HEAD", stderr: result.failure[:stderr]})
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
def last_fetch_at(path)
|
|
51
|
-
fetch_head = File.join(path, ".git", "FETCH_HEAD")
|
|
52
|
-
return Dry::Monads::Success(nil) unless File.exist?(fetch_head)
|
|
53
|
-
Dry::Monads::Success(Time.at(File.mtime(fetch_head).to_i))
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
def fetch(path)
|
|
57
|
-
Shell.run("git", "fetch", "--prune", "--no-tags", "origin", chdir: path)
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
# `merge --ff-only` refuses on divergence. We additionally check
|
|
61
|
-
# the rev-list left/right count first so we can surface a clean
|
|
62
|
-
# "diverged" failure with diagnostic info, not just a git error
|
|
63
|
-
# string.
|
|
64
|
-
def fast_forward(path, default_branch)
|
|
65
|
-
upstream = "origin/#{default_branch}"
|
|
66
|
-
counts = Shell.run("git", "rev-list", "--left-right", "--count", "HEAD...#{upstream}", chdir: path)
|
|
67
|
-
return counts if counts.failure?
|
|
68
|
-
|
|
69
|
-
left, right = counts.success.strip.split("\t").map(&:to_i)
|
|
70
|
-
if left > 0
|
|
71
|
-
return Dry::Monads::Failure({
|
|
72
|
-
path: path,
|
|
73
|
-
reason: "diverged: local is #{left} commit(s) ahead of #{upstream}; not auto-resolving",
|
|
74
|
-
local_ahead: left,
|
|
75
|
-
remote_ahead: right
|
|
76
|
-
})
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
if right == 0
|
|
80
|
-
return Dry::Monads::Success(0)
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
# Do the fetch (cheap; FETCH_HEAD mtime hint logic can skip this
|
|
84
|
-
# later, but Slice 1 always fetches when asked to fast-forward).
|
|
85
|
-
fetch_result = fetch(path)
|
|
86
|
-
return fetch_result if fetch_result.failure?
|
|
87
|
-
|
|
88
|
-
merge = Shell.run("git", "merge", "--ff-only", upstream, chdir: path)
|
|
89
|
-
if merge.success?
|
|
90
|
-
Dry::Monads::Success(right)
|
|
91
|
-
else
|
|
92
|
-
# On --ff-only failure, git leaves the working tree and local
|
|
93
|
-
# commits intact — the test asserts this.
|
|
94
|
-
Dry::Monads::Failure({
|
|
95
|
-
path: path,
|
|
96
|
-
reason: "fast-forward failed (likely raced divergence)",
|
|
97
|
-
stderr: merge.failure[:stderr]
|
|
98
|
-
})
|
|
99
|
-
end
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
def status(path)
|
|
103
|
-
result = Shell.run("git", "status", "--porcelain=v2", "--branch", "--untracked-files=normal", chdir: path)
|
|
104
|
-
return result if result.failure?
|
|
105
|
-
|
|
106
|
-
parsed = parse_porcelain_v2(result.success)
|
|
107
|
-
Dry::Monads::Success(parsed)
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
def clone(url, dest)
|
|
111
|
-
parent = File.dirname(dest)
|
|
112
|
-
FileUtils.mkdir_p(parent)
|
|
113
|
-
result = Shell.run("git", "clone", url, dest)
|
|
114
|
-
if result.success?
|
|
115
|
-
Dry::Monads::Success(dest)
|
|
116
|
-
else
|
|
117
|
-
Dry::Monads::Failure({url: url, dest: dest, stderr: result.failure[:stderr]})
|
|
118
|
-
end
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
# `git switch <branch>`. `git switch` aborts on a dirty tree by
|
|
122
|
-
# default (man git-switch: "The operation is aborted however if
|
|
123
|
-
# the operation leads to loss of local changes"), so a nonzero
|
|
124
|
-
# exit here most likely means the caller violated the engine's
|
|
125
|
-
# dirty-tree guard. We surface that as a Failure with the
|
|
126
|
-
# captured stderr so the engine / log can diagnose it.
|
|
127
|
-
def switch(path, branch)
|
|
128
|
-
result = Shell.run("git", "switch", branch, chdir: path)
|
|
129
|
-
if result.success?
|
|
130
|
-
Dry::Monads::Success(branch)
|
|
131
|
-
else
|
|
132
|
-
Dry::Monads::Failure({path: path, branch: branch, reason: "git switch refused", stderr: result.failure[:stderr]})
|
|
133
|
-
end
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
# Handle an unborn (empty) local clone. If the remote has no
|
|
137
|
-
# branches, the repo is already a valid empty clone — return
|
|
138
|
-
# Success(:empty) with no mutation. If the remote has gained
|
|
139
|
-
# commits, fetch and fast-forward the unborn branch into them.
|
|
140
|
-
#
|
|
141
|
-
# `git ls-remote --heads origin` is the authoritative
|
|
142
|
-
# empty-vs-error discriminator: exit 0 + empty stdout means the
|
|
143
|
-
# remote truly has no branches; exit 0 + output means it has
|
|
144
|
-
# commits; non-zero exit means a real network/probe error.
|
|
145
|
-
def sync_empty(path)
|
|
146
|
-
ls = Shell.run("git", "ls-remote", "--heads", "origin", chdir: path)
|
|
147
|
-
return ls if ls.failure?
|
|
148
|
-
|
|
149
|
-
return Dry::Monads::Success(:empty) if ls.success.strip.empty?
|
|
150
|
-
|
|
151
|
-
fetch_result = fetch(path)
|
|
152
|
-
return fetch_result if fetch_result.failure?
|
|
153
|
-
|
|
154
|
-
branch_result = default_branch(path)
|
|
155
|
-
return branch_result if branch_result.failure?
|
|
156
|
-
|
|
157
|
-
upstream = "origin/#{branch_result.success}"
|
|
158
|
-
merge = Shell.run("git", "merge", "--ff-only", upstream, chdir: path)
|
|
159
|
-
if merge.success?
|
|
160
|
-
Dry::Monads::Success(:fast_forwarded)
|
|
161
|
-
else
|
|
162
|
-
Dry::Monads::Failure({path: path, reason: "ff merge into unborn branch failed", stderr: merge.failure[:stderr]})
|
|
163
|
-
end
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
private
|
|
167
|
-
|
|
168
|
-
# `git symbolic-ref --short refs/remotes/origin/HEAD` returns
|
|
169
|
-
# `origin/<branch>` (the short form of `refs/remotes/origin/<branch>`);
|
|
170
|
-
# callers want the bare branch name. Returns nil when origin/HEAD
|
|
171
|
-
# is unset.
|
|
172
|
-
def read_origin_head(path)
|
|
173
|
-
result = Shell.run("git", "symbolic-ref", "--short", "refs/remotes/origin/HEAD", chdir: path)
|
|
174
|
-
return nil unless result.success?
|
|
175
|
-
line = result.success.strip
|
|
176
|
-
return nil if line.empty?
|
|
177
|
-
line.sub(%r{\Aorigin/}, "")
|
|
178
|
-
end
|
|
179
|
-
|
|
180
|
-
# Parse `git status --porcelain=v2 --branch --untracked-files=normal`.
|
|
181
|
-
# v2 grammar (from the git-status man page):
|
|
182
|
-
# # branch.oid <commit-ish> | (initial)
|
|
183
|
-
# # branch.head <name> | (detached)
|
|
184
|
-
# # branch.upstream <upstream-branch>
|
|
185
|
-
# # branch.ab +<ahead> -<behind>
|
|
186
|
-
# 1 <XY> <sub> <mH> <mI> <mW> <hH> <hI> <path>
|
|
187
|
-
# 2 <XY> <sub> <mH> <mI> <mW> <hH> <hI> <X><score> <path><TAB><origPath>
|
|
188
|
-
# u <XY> <sub> <m1> <m2> <m3> <mW> <h1> <h2> <h3> <path>
|
|
189
|
-
# ? <path>
|
|
190
|
-
# ! <path>
|
|
191
|
-
def parse_porcelain_v2(output)
|
|
192
|
-
branch = nil
|
|
193
|
-
upstream = nil
|
|
194
|
-
ahead = 0
|
|
195
|
-
behind = 0
|
|
196
|
-
detached = false
|
|
197
|
-
unborn = false
|
|
198
|
-
entries = []
|
|
199
|
-
|
|
200
|
-
output.each_line do |raw|
|
|
201
|
-
line = raw.chomp
|
|
202
|
-
next if line.empty?
|
|
203
|
-
case line
|
|
204
|
-
when /\A# branch\.oid (.+)/
|
|
205
|
-
unborn = (Regexp.last_match(1) == "(initial)")
|
|
206
|
-
when /\A# branch\.head (.+)/
|
|
207
|
-
branch = Regexp.last_match(1)
|
|
208
|
-
detached = (branch == "(detached)")
|
|
209
|
-
when /\A# branch\.upstream (.+)/
|
|
210
|
-
upstream = Regexp.last_match(1)
|
|
211
|
-
when /\A# branch\.ab \+(\d+) -(\d+)/
|
|
212
|
-
ahead = Regexp.last_match(1).to_i
|
|
213
|
-
behind = Regexp.last_match(2).to_i
|
|
214
|
-
when /\A[12u?!]/
|
|
215
|
-
entries << line
|
|
216
|
-
end
|
|
217
|
-
end
|
|
218
|
-
|
|
219
|
-
Status.new(
|
|
220
|
-
clean: entries.empty?,
|
|
221
|
-
branch: branch,
|
|
222
|
-
upstream: upstream,
|
|
223
|
-
ahead: ahead,
|
|
224
|
-
behind: behind,
|
|
225
|
-
detached: detached,
|
|
226
|
-
entries: entries,
|
|
227
|
-
unborn: unborn
|
|
228
|
-
)
|
|
229
|
-
end
|
|
230
|
-
end
|
|
231
|
-
end
|
|
232
|
-
end
|
data/lib/space_src/scm/status.rb
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Space::Src
|
|
4
|
-
module SCM
|
|
5
|
-
# Value object produced by parsing `git status --porcelain=v2
|
|
6
|
-
# --branch --untracked-files=normal`. v2 is mandatory (per
|
|
7
|
-
# AGENTS.md gotcha) so submodule state and rename detection are
|
|
8
|
-
# stable.
|
|
9
|
-
#
|
|
10
|
-
# A working tree is "clean" iff the only porcelain-v2 lines are the
|
|
11
|
-
# `# branch.*` header lines. Any `1`/`2`/`u`/`?`/`!` line is dirty.
|
|
12
|
-
Status = Data.define(:clean, :branch, :upstream, :ahead, :behind, :detached, :entries, :unborn) do
|
|
13
|
-
def initialize(clean:, branch: nil, upstream: nil, ahead: 0, behind: 0, detached: false, entries: [], unborn: false)
|
|
14
|
-
super
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def clean? = clean
|
|
18
|
-
|
|
19
|
-
def detached? = detached
|
|
20
|
-
|
|
21
|
-
def unborn? = unborn
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
end
|
data/lib/space_src/shell.rb
DELETED
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "open3"
|
|
4
|
-
require "async"
|
|
5
|
-
require "dry/monads"
|
|
6
|
-
|
|
7
|
-
module Space::Src
|
|
8
|
-
# Thin Open3.capture3 wrapper that:
|
|
9
|
-
# * requires an ambient Async::Task (so subprocess I/O flows through
|
|
10
|
-
# Ruby's Fiber scheduler → kqueue on macOS and is non-blocking);
|
|
11
|
-
# * returns a Dry::Monads::Result — Success(stdout) on zero exit,
|
|
12
|
-
# Failure({argv:, stderr:, status:}) otherwise.
|
|
13
|
-
#
|
|
14
|
-
# Per AGENTS.md: no `async-process`. Per PRD §2: boundaries return
|
|
15
|
-
# Result, exceptions are for programmer error only.
|
|
16
|
-
class Shell
|
|
17
|
-
extend Dry::Monads[:result]
|
|
18
|
-
|
|
19
|
-
@run_count = 0
|
|
20
|
-
@saved_roe = nil
|
|
21
|
-
|
|
22
|
-
def self.run(*argv, chdir: nil, env: nil)
|
|
23
|
-
raise ArgumentError, "Shell.run requires at least argv" if argv.empty?
|
|
24
|
-
raise "Shell.run must be called inside an ambient Async::Task" unless Async::Task.current?
|
|
25
|
-
|
|
26
|
-
full_env = env ? ENV.to_h.merge(env.transform_keys(&:to_s)) : nil
|
|
27
|
-
opts = {}
|
|
28
|
-
opts[:chdir] = chdir if chdir
|
|
29
|
-
# Open3.capture3: env is a leading hash positional arg, not a kwarg.
|
|
30
|
-
#
|
|
31
|
-
# Open3.capture3 spawns the child with two internal reader
|
|
32
|
-
# threads (one for stdout, one for stderr; see
|
|
33
|
-
# `rubylibdir/open3.rb` ~L644: `out_reader = Thread.new { o.read }`
|
|
34
|
-
# / `err_reader = Thread.new { e.read }`). When the `popen3`
|
|
35
|
-
# block exits via exception (e.g. the user ^C'd mid-Shell.run
|
|
36
|
-
# via SIGINT), `popen_run`'s ensure closes the read pipes from
|
|
37
|
-
# the main thread while those reader threads are still inside
|
|
38
|
-
# `o.read` / `e.read`. The mid-read close races with the reader
|
|
39
|
-
# and raises `IOError: stream closed in another thread` in the
|
|
40
|
-
# reader thread. With the default `Thread.report_on_exception
|
|
41
|
-
# = true` (since Ruby 2.5), Ruby prints a multi-line backtrace
|
|
42
|
-
# to stderr for that orphaned thread — exactly the noise
|
|
43
|
-
# Slice 6 G3 silences.
|
|
44
|
-
#
|
|
45
|
-
# We bracket the `Open3.capture3` call with a save/restore of
|
|
46
|
-
# `Thread.report_on_exception = false`. This is targeted
|
|
47
|
-
# because, at this code site, the ONLY threads in flight are:
|
|
48
|
-
# * the main thread (this method's caller);
|
|
49
|
-
# * Async's internal `io_select` thread
|
|
50
|
-
# (`async/lib/async/scheduler.rb` L425) — which silences
|
|
51
|
-
# its own report (`Thread.current.report_on_exception =
|
|
52
|
-
# false` on that thread, not globally);
|
|
53
|
-
# * the Open3 reader threads (the source of the noise).
|
|
54
|
-
# `lib/` has zero `Thread.new` calls; `dry-cli`, `dry-monads`,
|
|
55
|
-
# `dry-validation`, `dry-struct`, `dry-types`, `dry-schema`,
|
|
56
|
-
# `xdg` have none either (verified Slice 6 PHASE 0). So we are
|
|
57
|
-
# NOT hiding any app-owned worker-thread crashes — the only
|
|
58
|
-
# thread that can raise here is the Open3 reader thread, and
|
|
59
|
-
# the only thing it can raise is the IOError we explicitly
|
|
60
|
-
# want to silence. The original value is restored in `ensure`
|
|
61
|
-
# so we never leak the suppression past this call.
|
|
62
|
-
# Refcount the active Shell.run calls so the global flag is suppressed
|
|
63
|
-
# for the entire overlapping window, not just per-fiber. On 0→1: capture
|
|
64
|
-
# original and set false. On 1→0 (in ensure): restore the original.
|
|
65
|
-
# Safe without a Mutex: the reactor is single-threaded; fibers only yield
|
|
66
|
-
# at Open3.capture3's thread-join, never between these plain assignments.
|
|
67
|
-
if @run_count == 0
|
|
68
|
-
@saved_roe = Thread.report_on_exception
|
|
69
|
-
Thread.report_on_exception = false
|
|
70
|
-
end
|
|
71
|
-
@run_count += 1
|
|
72
|
-
begin
|
|
73
|
-
stdout, stderr, status = if full_env
|
|
74
|
-
Open3.capture3(full_env, *argv, **opts)
|
|
75
|
-
else
|
|
76
|
-
Open3.capture3(*argv, **opts)
|
|
77
|
-
end
|
|
78
|
-
ensure
|
|
79
|
-
@run_count -= 1
|
|
80
|
-
Thread.report_on_exception = @saved_roe if @run_count == 0
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
if status.success?
|
|
84
|
-
Success(stdout)
|
|
85
|
-
else
|
|
86
|
-
Failure({argv: argv, stderr: stderr, status: status.exitstatus})
|
|
87
|
-
end
|
|
88
|
-
end
|
|
89
|
-
end
|
|
90
|
-
end
|
|
@@ -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
|