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.
Files changed (107) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +54 -0
  3. data/README.md +39 -464
  4. data/lib/space_architect/architect_project.rb +90 -39
  5. data/lib/space_architect/bug_report.rb +2 -1
  6. data/lib/space_architect/cli/architect.rb +189 -11
  7. data/lib/space_architect/cli/research.rb +1 -1
  8. data/lib/space_architect/cli.rb +9 -14
  9. data/lib/space_architect/loop_cost.rb +592 -0
  10. data/lib/space_architect/research/supervisor.rb +9 -2
  11. data/lib/space_architect/routing.rb +149 -0
  12. data/lib/space_architect/session_sync/bin_path.rb +1 -1
  13. data/lib/space_architect/session_sync/cursor.rb +1 -1
  14. data/lib/space_architect/session_sync/plist.rb +1 -1
  15. data/lib/{space_core → space_architect}/version.rb +2 -2
  16. data/lib/space_architect.rb +2 -1
  17. data/skill/architect/SKILL.md +3 -2
  18. data/skill/architect-vocabulary/SKILL.md +2 -2
  19. metadata +20 -177
  20. data/exe/space +0 -13
  21. data/exe/src +0 -13
  22. data/lib/space_architect/cli/space.rb +0 -29
  23. data/lib/space_architect/cli/src.rb +0 -38
  24. data/lib/space_core/atomic_write.rb +0 -21
  25. data/lib/space_core/cli/base_command.rb +0 -19
  26. data/lib/space_core/cli/build.rb +0 -27
  27. data/lib/space_core/cli/config.rb +0 -49
  28. data/lib/space_core/cli/current.rb +0 -16
  29. data/lib/space_core/cli/help.rb +0 -195
  30. data/lib/space_core/cli/helpers.rb +0 -115
  31. data/lib/space_core/cli/init.rb +0 -29
  32. data/lib/space_core/cli/list.rb +0 -24
  33. data/lib/space_core/cli/loop_status.rb +0 -47
  34. data/lib/space_core/cli/new.rb +0 -38
  35. data/lib/space_core/cli/pack.rb +0 -29
  36. data/lib/space_core/cli/path.rb +0 -16
  37. data/lib/space_core/cli/repeatable_options.rb +0 -84
  38. data/lib/space_core/cli/repo.rb +0 -76
  39. data/lib/space_core/cli/run.rb +0 -44
  40. data/lib/space_core/cli/shell.rb +0 -125
  41. data/lib/space_core/cli/show.rb +0 -21
  42. data/lib/space_core/cli/status.rb +0 -74
  43. data/lib/space_core/cli/use.rb +0 -17
  44. data/lib/space_core/cli.rb +0 -177
  45. data/lib/space_core/commands.rb +0 -51
  46. data/lib/space_core/config.rb +0 -152
  47. data/lib/space_core/errors.rb +0 -14
  48. data/lib/space_core/git_client.rb +0 -49
  49. data/lib/space_core/mise_client.rb +0 -37
  50. data/lib/space_core/oci_builder.rb +0 -56
  51. data/lib/space_core/oci_packer.rb +0 -103
  52. data/lib/space_core/oci_runner.rb +0 -89
  53. data/lib/space_core/paths.rb +0 -58
  54. data/lib/space_core/repo_reference.rb +0 -19
  55. data/lib/space_core/repo_resolver.rb +0 -167
  56. data/lib/space_core/shell_integration.rb +0 -438
  57. data/lib/space_core/slugger.rb +0 -16
  58. data/lib/space_core/space.rb +0 -174
  59. data/lib/space_core/space_store.rb +0 -319
  60. data/lib/space_core/state.rb +0 -86
  61. data/lib/space_core/templates/oci/dockerfile.erb +0 -73
  62. data/lib/space_core/templates/oci/dockerignore.erb +0 -17
  63. data/lib/space_core/templates/oci/entrypoint.sh.erb +0 -16
  64. data/lib/space_core/terminal.rb +0 -161
  65. data/lib/space_core/warnings.rb +0 -13
  66. data/lib/space_core/xdg.rb +0 -33
  67. data/lib/space_core.rb +0 -26
  68. data/lib/space_src/cli/clone.rb +0 -54
  69. data/lib/space_src/cli/config.rb +0 -65
  70. data/lib/space_src/cli/daemon.rb +0 -362
  71. data/lib/space_src/cli/options.rb +0 -21
  72. data/lib/space_src/cli/org.rb +0 -199
  73. data/lib/space_src/cli/repo.rb +0 -169
  74. data/lib/space_src/cli/shell.rb +0 -121
  75. data/lib/space_src/cli/status.rb +0 -75
  76. data/lib/space_src/cli/sync.rb +0 -161
  77. data/lib/space_src/cli.rb +0 -159
  78. data/lib/space_src/cloner.rb +0 -77
  79. data/lib/space_src/config/contract.rb +0 -54
  80. data/lib/space_src/config/duration.rb +0 -79
  81. data/lib/space_src/config/model.rb +0 -49
  82. data/lib/space_src/config/store.rb +0 -156
  83. data/lib/space_src/forge/client.rb +0 -31
  84. data/lib/space_src/forge/github.rb +0 -98
  85. data/lib/space_src/launchd/agent.rb +0 -195
  86. data/lib/space_src/launchd/plist.rb +0 -129
  87. data/lib/space_src/log_rotator.rb +0 -46
  88. data/lib/space_src/migration.rb +0 -43
  89. data/lib/space_src/nav.rb +0 -99
  90. data/lib/space_src/paths.rb +0 -72
  91. data/lib/space_src/scm/client.rb +0 -87
  92. data/lib/space_src/scm/git.rb +0 -232
  93. data/lib/space_src/scm/status.rb +0 -24
  94. data/lib/space_src/shell.rb +0 -90
  95. data/lib/space_src/shell_integration.rb +0 -321
  96. data/lib/space_src/state/lock.rb +0 -59
  97. data/lib/space_src/state/store.rb +0 -140
  98. data/lib/space_src/sync/engine.rb +0 -503
  99. data/lib/space_src/sync/repo_plan.rb +0 -215
  100. data/lib/space_src/sync/report.rb +0 -18
  101. data/lib/space_src/ui/interactive_reporter.rb +0 -280
  102. data/lib/space_src/ui/json_reporter.rb +0 -39
  103. data/lib/space_src/ui/mode.rb +0 -68
  104. data/lib/space_src/ui/plain_reporter.rb +0 -53
  105. data/lib/space_src/ui/reporter.rb +0 -48
  106. data/lib/space_src/version.rb +0 -7
  107. data/lib/space_src.rb +0 -38
@@ -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
@@ -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
@@ -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