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,199 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "pastel"
4
- require "dry/monads"
5
- require "space_src/ui/mode"
6
- require "space_src/cli/options"
7
-
8
- module Space::Src
9
- module CLI
10
- # `org` command group: add / remove / list tracked orgs.
11
- # Same shape as `repo` but against `Config::OrgRef` (host + name
12
- # + two bool flags). The host defaults to "github.com" per
13
- # PRD §3.1 when omitted (so `org add socketry` works as well as
14
- # `org add github.com/socketry`).
15
- module Org
16
- module Helpers
17
- module_function
18
-
19
- # Parse an org ref string. Accepts "github.com/socketry" or
20
- # bare "socketry" (defaults host to github.com). Anything
21
- # with 3+ parts is rejected (org has only host + name).
22
- def parse_ref(name, include_archived: false, include_forks: false, ignored_repos: [])
23
- parts = name.to_s.split("/")
24
- case parts.length
25
- when 1
26
- org = parts[0]
27
- return Dry::Monads::Failure("invalid org reference: empty name in #{name.inspect}") if org.empty?
28
- Dry::Monads::Success(Config::OrgRef.new(
29
- host: Config::DEFAULT_HOST,
30
- name: org,
31
- include_archived: include_archived,
32
- include_forks: include_forks,
33
- ignored_repos: ignored_repos
34
- ))
35
- when 2
36
- host, n = parts
37
- return Dry::Monads::Failure("invalid org reference: empty host in #{name.inspect}") if host.empty?
38
- return Dry::Monads::Failure("invalid org reference: empty name in #{name.inspect}") if n.empty?
39
- Dry::Monads::Success(Config::OrgRef.new(
40
- host: host,
41
- name: n,
42
- include_archived: include_archived,
43
- include_forks: include_forks,
44
- ignored_repos: ignored_repos
45
- ))
46
- else
47
- Dry::Monads::Failure("invalid org reference: #{name.inspect} (expected \"<name>\" or \"<host>/<name>\")")
48
- end
49
- end
50
-
51
- def same_org?(a, b) = a.host == b.host && a.name == b.name
52
- def format_ref(o) = "#{o.host}/#{o.name}"
53
- def format_failure(f) = f.is_a?(Hash) ? f.inspect : f.to_s
54
-
55
- def format_ignored(o)
56
- return "" if o.ignored_repos.empty?
57
- " ignored_repos=#{o.ignored_repos.inspect}"
58
- end
59
-
60
- def fail_with(cmd, msg)
61
- cmd.send(:err).puts msg
62
- Space::Src::CLI.record_outcome(Outcome.new(exit_code: 1, message: msg))
63
- end
64
- end
65
-
66
- class Add < Dry::CLI::Command
67
- include Helpers
68
- include GlobalOptions
69
-
70
- desc "Add a tracked org (idempotent on host/name)"
71
- argument :name, required: true,
72
- desc: "Org identity as <name> or <host>/<name> (host defaults to github.com)"
73
- option :include_archived, type: :boolean, default: false,
74
- desc: "Include archived repos when expanding the org"
75
- option :include_forks, type: :boolean, default: false,
76
- desc: "Include forks when expanding the org"
77
- option :ignored_repos, type: :array, default: [],
78
- desc: "Repos to exclude from expansion (bare name or owner/name)"
79
-
80
- def call(name:, include_archived: false, include_forks: false, ignored_repos: [], plain: nil, json: nil, no_color: nil, quiet: nil, **)
81
- mode = UI::Mode.resolve(
82
- flags: {plain: plain, json: json, no_color: no_color, quiet: quiet},
83
- env: CLI.env,
84
- out: out
85
- )
86
- pastel = Pastel.new(enabled: mode.color)
87
-
88
- parsed = parse_ref(name,
89
- include_archived: include_archived,
90
- include_forks: include_forks,
91
- ignored_repos: ignored_repos)
92
- return fail_with(self, parsed.failure) if parsed.failure?
93
-
94
- new_ref = parsed.success
95
- paths = CLI.make_paths
96
- config = Config::Store.load(paths.config_file).success
97
-
98
- if config.orgs.any? { |o| same_org?(o, new_ref) }
99
- out.puts pastel.yellow("already tracked: #{format_ref(new_ref)}" \
100
- " (include_archived=#{new_ref.include_archived}, include_forks=#{new_ref.include_forks})" \
101
- "#{format_ignored(new_ref)}")
102
- return CLI.record_outcome(Outcome.new(exit_code: 0))
103
- end
104
-
105
- result = Config::Store.update(paths.config_file) do |c|
106
- Config::Store.with(c, orgs: c.orgs + [new_ref])
107
- end
108
- if result.failure?
109
- return fail_with(self, "failed to update config: #{format_failure(result.failure)}")
110
- end
111
-
112
- out.puts pastel.green("added: #{format_ref(new_ref)}" \
113
- " (include_archived=#{new_ref.include_archived}, include_forks=#{new_ref.include_forks})" \
114
- "#{format_ignored(new_ref)}")
115
- CLI.record_outcome(Outcome.new(exit_code: 0))
116
- end
117
- end
118
-
119
- class Remove < Dry::CLI::Command
120
- include Helpers
121
- include GlobalOptions
122
-
123
- desc "Remove a tracked org (host/name)"
124
- argument :name, required: true,
125
- desc: "Org identity as <name> or <host>/<name> (host defaults to github.com)"
126
-
127
- def call(name:, plain: nil, json: nil, no_color: nil, quiet: nil, **)
128
- # The flags don't affect the identity match for remove; we
129
- # match on (host, name) only. This matches the user's
130
- # expectation that "remove" targets the org, not the flag
131
- # combination they added it with.
132
- mode = UI::Mode.resolve(
133
- flags: {plain: plain, json: json, no_color: no_color, quiet: quiet},
134
- env: CLI.env,
135
- out: out
136
- )
137
- pastel = Pastel.new(enabled: mode.color)
138
-
139
- parsed = parse_ref(name)
140
- return fail_with(self, parsed.failure) if parsed.failure?
141
-
142
- target = parsed.success
143
- paths = CLI.make_paths
144
- config = Config::Store.load(paths.config_file).success
145
-
146
- kept = config.orgs.reject { |o| same_org?(o, target) }
147
- if kept.size == config.orgs.size
148
- return fail_with(self, "not tracked: #{format_ref(target)}")
149
- end
150
-
151
- result = Config::Store.update(paths.config_file) do |c|
152
- Config::Store.with(c, orgs: kept)
153
- end
154
- if result.failure?
155
- return fail_with(self, "failed to update config: #{format_failure(result.failure)}")
156
- end
157
-
158
- out.puts pastel.green("removed: #{format_ref(target)}")
159
- CLI.record_outcome(Outcome.new(exit_code: 0))
160
- end
161
- end
162
-
163
- class List < Dry::CLI::Command
164
- include Helpers
165
- include GlobalOptions
166
-
167
- desc "List tracked orgs"
168
-
169
- def call(plain: nil, json: nil, no_color: nil, quiet: nil, **)
170
- mode = UI::Mode.resolve(
171
- flags: {plain: plain, json: json, no_color: no_color, quiet: quiet},
172
- env: CLI.env,
173
- out: out
174
- )
175
- pastel = Pastel.new(enabled: mode.color)
176
-
177
- paths = CLI.make_paths
178
- config = Config::Store.load(paths.config_file).success
179
- if config.orgs.empty?
180
- out.puts pastel.dim("(no tracked orgs)")
181
- else
182
- config.orgs.each do |o|
183
- out.puts pastel.cyan("#{o.host}/#{o.name}" \
184
- " (include_archived=#{o.include_archived}, include_forks=#{o.include_forks})" \
185
- "#{format_ignored(o)}")
186
- end
187
- end
188
- CLI.record_outcome(Outcome.new(exit_code: 0))
189
- end
190
- end
191
- end
192
- end
193
- end
194
-
195
- Space::Src::CLI::Registry.register "org" do |prefix|
196
- prefix.register "add", Space::Src::CLI::Org::Add
197
- prefix.register "remove", Space::Src::CLI::Org::Remove
198
- prefix.register "list", Space::Src::CLI::Org::List
199
- end
@@ -1,169 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "pastel"
4
- require "dry/monads"
5
- require "space_src/ui/mode"
6
- require "space_src/cli/options"
7
-
8
- module Space::Src
9
- module CLI
10
- # `repo` command group: add / remove / list tracked repos.
11
- # Backed by Config::Store (the on-disk config.yaml is the
12
- # source of truth — the CLI is just the CRUD interface).
13
- module Repo
14
- # Shared ref parser + formatters used by Add and Remove. Kept
15
- # in a module-level Helpers so the parsing rules live in one
16
- # place (the "invalid repo reference" message must be identical
17
- # across add/remove for G3 consistency).
18
- module Helpers
19
- module_function
20
-
21
- def parse_ref(ref)
22
- parts = ref.to_s.split("/")
23
- return Dry::Monads::Failure("invalid repo reference: #{ref.inspect} (expected host/owner/name)") if parts.length != 3
24
- host, owner, name = parts
25
- return Dry::Monads::Failure("invalid repo reference: empty host in #{ref.inspect}") if host.empty?
26
- return Dry::Monads::Failure("invalid repo reference: empty owner in #{ref.inspect}") if owner.empty?
27
- return Dry::Monads::Failure("invalid repo reference: empty name in #{ref.inspect}") if name.empty?
28
- Dry::Monads::Success(Config::RepoRef.new(host: host, owner: owner, name: name))
29
- end
30
-
31
- def same_repo?(a, b)
32
- a.host == b.host && a.owner == b.owner && a.name == b.name
33
- end
34
-
35
- def format_ref(r) = "#{r.host}/#{r.owner}/#{r.name}"
36
-
37
- def format_failure(f) = f.is_a?(Hash) ? f.inspect : f.to_s
38
-
39
- def fail_with(cmd, msg)
40
- cmd.send(:err).puts msg
41
- Space::Src::CLI.record_outcome(Outcome.new(exit_code: 1, message: msg))
42
- end
43
- end
44
-
45
- # Add a tracked repo. Idempotent on the (host, owner, name)
46
- # triple: adding the same ref twice prints "already tracked"
47
- # and exits 0 (does NOT write a duplicate).
48
- class Add < Dry::CLI::Command
49
- include Helpers
50
- include GlobalOptions
51
-
52
- desc "Add a tracked repo (idempotent on host/owner/name)"
53
- argument :ref, required: true,
54
- desc: "Repo identity as host/owner/name (e.g. github.com/ruby/ruby)"
55
-
56
- def call(ref:, plain: nil, json: nil, no_color: nil, quiet: nil, **)
57
- mode = UI::Mode.resolve(
58
- flags: {plain: plain, json: json, no_color: no_color, quiet: quiet},
59
- env: CLI.env,
60
- out: out
61
- )
62
- pastel = Pastel.new(enabled: mode.color)
63
-
64
- parsed = parse_ref(ref)
65
- return fail_with(self, parsed.failure) if parsed.failure?
66
-
67
- new_ref = parsed.success
68
- paths = CLI.make_paths
69
- config = Config::Store.load(paths.config_file).success
70
-
71
- if config.repos.any? { |r| same_repo?(r, new_ref) }
72
- out.puts pastel.yellow("already tracked: #{format_ref(new_ref)}")
73
- return CLI.record_outcome(Outcome.new(exit_code: 0))
74
- end
75
-
76
- result = Config::Store.update(paths.config_file) do |c|
77
- Config::Store.with(c, repos: c.repos + [new_ref])
78
- end
79
-
80
- if result.failure?
81
- return fail_with(self, "failed to update config: #{format_failure(result.failure)}")
82
- end
83
-
84
- out.puts pastel.green("added: #{format_ref(new_ref)}")
85
- CLI.record_outcome(Outcome.new(exit_code: 0))
86
- end
87
- end
88
-
89
- # Remove a tracked repo. Exits 0 with a clear message if the
90
- # ref was present; exits 1 with "not tracked" if it wasn't
91
- # (and the config is untouched in that case).
92
- class Remove < Dry::CLI::Command
93
- include Helpers
94
- include GlobalOptions
95
-
96
- desc "Remove a tracked repo (host/owner/name)"
97
- argument :ref, required: true,
98
- desc: "Repo identity as host/owner/name (e.g. github.com/ruby/ruby)"
99
-
100
- def call(ref:, plain: nil, json: nil, no_color: nil, quiet: nil, **)
101
- mode = UI::Mode.resolve(
102
- flags: {plain: plain, json: json, no_color: no_color, quiet: quiet},
103
- env: CLI.env,
104
- out: out
105
- )
106
- pastel = Pastel.new(enabled: mode.color)
107
-
108
- parsed = parse_ref(ref)
109
- return fail_with(self, parsed.failure) if parsed.failure?
110
-
111
- target = parsed.success
112
- paths = CLI.make_paths
113
- config = Config::Store.load(paths.config_file).success
114
-
115
- kept = config.repos.reject { |r| same_repo?(r, target) }
116
- if kept.size == config.repos.size
117
- return fail_with(self, "not tracked: #{format_ref(target)}")
118
- end
119
-
120
- result = Config::Store.update(paths.config_file) do |c|
121
- Config::Store.with(c, repos: kept)
122
- end
123
- if result.failure?
124
- return fail_with(self, "failed to update config: #{format_failure(result.failure)}")
125
- end
126
-
127
- out.puts pastel.green("removed: #{format_ref(target)}")
128
- CLI.record_outcome(Outcome.new(exit_code: 0))
129
- end
130
- end
131
-
132
- # List tracked repos. One per line: "host/owner/name".
133
- class List < Dry::CLI::Command
134
- include GlobalOptions
135
-
136
- desc "List tracked repos"
137
-
138
- def call(plain: nil, json: nil, no_color: nil, quiet: nil, **)
139
- mode = UI::Mode.resolve(
140
- flags: {plain: plain, json: json, no_color: no_color, quiet: quiet},
141
- env: CLI.env,
142
- out: out
143
- )
144
- pastel = Pastel.new(enabled: mode.color)
145
-
146
- paths = CLI.make_paths
147
- config = Config::Store.load(paths.config_file).success
148
- if config.repos.empty?
149
- out.puts pastel.dim("(no tracked repos)")
150
- else
151
- config.repos.each do |r|
152
- out.puts pastel.cyan("#{r.host}/#{r.owner}/#{r.name}")
153
- end
154
- end
155
- CLI.record_outcome(Outcome.new(exit_code: 0))
156
- end
157
- end
158
- end
159
- end
160
- end
161
-
162
- # Register the `repo` group + its subcommands. The block's `prefix`
163
- # is a Dry::CLI::Registry::Prefix proxy that namespaces the
164
- # subcommand names under "repo".
165
- Space::Src::CLI::Registry.register "repo" do |prefix|
166
- prefix.register "add", Space::Src::CLI::Repo::Add
167
- prefix.register "remove", Space::Src::CLI::Repo::Remove
168
- prefix.register "list", Space::Src::CLI::Repo::List
169
- end
@@ -1,121 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "space_src/shell_integration"
4
-
5
- module Space::Src
6
- module CLI
7
- module Shell
8
- class Init < Dry::CLI::Command
9
- desc "Print shell integration script"
10
- argument :shell_name, required: true, desc: "Shell name (e.g. fish)"
11
-
12
- def call(shell_name:, **)
13
- out.puts ShellIntegration.for(shell_name)
14
- CLI.record_outcome(Outcome.new(exit_code: 0))
15
- rescue => e
16
- err.puts "src shell init: #{e.message}"
17
- CLI.record_outcome(Outcome.new(exit_code: 1))
18
- end
19
- end
20
-
21
- class Fish < Dry::CLI::Command
22
- desc "Manage fish shell integration: install, uninstall, path"
23
- argument :subcommand, required: false, desc: "install, uninstall, or path (default: install)"
24
- option :force, type: :boolean, default: false, desc: "Overwrite or remove existing shell files"
25
-
26
- def call(subcommand: "install", force: false, **)
27
- case subcommand
28
- when "install"
29
- result = ShellIntegration.install("fish", env: CLI.env, force: force)
30
- out.puts fish_install_message(result.fetch(:action), result.fetch(:path))
31
- out.puts fish_completions_install_message(result.fetch(:completions_action), result.fetch(:completions_path))
32
- out.puts "Restart fish to load the integration in this terminal: exec fish"
33
- CLI.record_outcome(Outcome.new(exit_code: 0))
34
- when "uninstall"
35
- result = ShellIntegration.uninstall("fish", env: CLI.env, force: force)
36
- out.puts fish_uninstall_message(result.fetch(:action), result.fetch(:path))
37
- out.puts fish_completions_uninstall_message(result.fetch(:completions_action), result.fetch(:completions_path))
38
- CLI.record_outcome(Outcome.new(exit_code: 0))
39
- when "path"
40
- out.puts "Function: #{ShellIntegration.path_for("fish", env: CLI.env)}"
41
- out.puts "Completions: #{ShellIntegration.completions_path_for("fish", env: CLI.env)}"
42
- CLI.record_outcome(Outcome.new(exit_code: 0))
43
- else
44
- err.puts "Usage: src shell fish [install|uninstall|path]"
45
- CLI.record_outcome(Outcome.new(exit_code: 1))
46
- end
47
- rescue => e
48
- err.puts "src shell fish: #{e.message}"
49
- CLI.record_outcome(Outcome.new(exit_code: 1))
50
- end
51
-
52
- private
53
-
54
- def fish_install_message(action, path)
55
- case action
56
- when :unchanged then "Fish integration already installed: #{path}"
57
- when :updated then "Updated fish integration: #{path}"
58
- else "Installed fish integration: #{path}"
59
- end
60
- end
61
-
62
- def fish_uninstall_message(action, path)
63
- case action
64
- when :missing then "Fish integration was not installed: #{path}"
65
- else "Removed fish integration: #{path}"
66
- end
67
- end
68
-
69
- def fish_completions_install_message(action, path)
70
- case action
71
- when :unchanged then "Fish completions already installed: #{path}"
72
- when :updated then "Updated fish completions: #{path}"
73
- else "Installed fish completions: #{path}"
74
- end
75
- end
76
-
77
- def fish_completions_uninstall_message(action, path)
78
- case action
79
- when :missing then "Fish completions were not installed: #{path}"
80
- else "Removed fish completions: #{path}"
81
- end
82
- end
83
- end
84
-
85
- class Complete < Dry::CLI::Command
86
- desc "Print completion candidates"
87
- argument :kind, required: true, desc: "Completion kind"
88
- argument :extra, type: :array, required: false, desc: "Extra args for completion"
89
-
90
- def call(kind:, extra: [], **)
91
- case kind
92
- when "checkouts"
93
- begin
94
- paths = CLI.make_paths
95
- config = Config::Store.load(paths.config_file).success
96
- Nav.scan(config.base_dir).each { |e| out.puts e[:target] }
97
- rescue
98
- # Never raise on a completion call — missing/unparseable config is fine.
99
- end
100
- CLI.record_outcome(Outcome.new(exit_code: 0))
101
- when "shells"
102
- out.puts "fish"
103
- CLI.record_outcome(Outcome.new(exit_code: 0))
104
- else
105
- err.puts "Usage: src shell complete checkouts|shells"
106
- CLI.record_outcome(Outcome.new(exit_code: 1))
107
- end
108
- rescue => e
109
- err.puts "src shell complete: #{e.message}"
110
- CLI.record_outcome(Outcome.new(exit_code: 1))
111
- end
112
- end
113
- end
114
- end
115
- end
116
-
117
- Space::Src::CLI::Registry.register "shell" do |prefix|
118
- prefix.register "init", Space::Src::CLI::Shell::Init
119
- prefix.register "fish", Space::Src::CLI::Shell::Fish
120
- prefix.register "complete", Space::Src::CLI::Shell::Complete
121
- end
@@ -1,75 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "pastel"
4
- require "space_src/ui/mode"
5
- require "space_src/cli/options"
6
-
7
- module Space::Src
8
- module CLI
9
- # `status` command: render a per-repo evergreen table from
10
- # State::Store. Per G5, the rows must include each repo key
11
- # with status, last_synced_at, and default_branch.
12
- module Status
13
- class Show < Dry::CLI::Command
14
- include GlobalOptions
15
-
16
- STATUS_COLORS = {
17
- "clean" => :green,
18
- "dirty" => :yellow,
19
- "diverged" => :yellow,
20
- "wrong_branch" => :yellow,
21
- "detached" => :yellow,
22
- "error" => :red
23
- }.freeze
24
-
25
- desc "Show the per-repo evergreen status table (from $XDG_STATE_HOME/space-src/state.yaml)"
26
-
27
- def call(plain: nil, json: nil, no_color: nil, quiet: nil, **)
28
- mode = UI::Mode.resolve(
29
- flags: {plain: plain, json: json, no_color: no_color, quiet: quiet},
30
- env: CLI.env,
31
- out: out
32
- )
33
- pastel = Pastel.new(enabled: mode.color)
34
-
35
- paths = CLI.make_paths
36
- state = State::Store.load(paths.state_file).success
37
-
38
- if state.repos.empty?
39
- out.puts "(no repos in state — run `src sync` to populate)"
40
- return CLI.record_outcome(Outcome.new(exit_code: 0))
41
- end
42
-
43
- # Tab-separated columns. The G5 assertion is on captured
44
- # stdout containing each repo key and its status string —
45
- # tab-separated is the most assertion-friendly (no width
46
- # padding complexity, no ANSI codes).
47
- out.puts ["REPO", "STATUS", "DEFAULT_BRANCH", "LAST_SYNCED_AT", "LAST_FETCH_AT"].join("\t")
48
- state.repos.keys.sort.each do |key|
49
- r = state.repos[key]
50
- status_str = r.status.to_s
51
- color = STATUS_COLORS[status_str]
52
- styled_status = color ? pastel.decorate(status_str, color) : status_str
53
- out.puts [
54
- key,
55
- styled_status,
56
- r.default_branch.to_s,
57
- format_time(r.last_synced_at),
58
- format_time(r.last_fetch_at)
59
- ].join("\t")
60
- end
61
- CLI.record_outcome(Outcome.new(exit_code: 0))
62
- end
63
-
64
- private
65
-
66
- def format_time(t)
67
- return "" if t.nil?
68
- t.respond_to?(:iso8601) ? t.iso8601 : t.to_s
69
- end
70
- end
71
- end
72
- end
73
- end
74
-
75
- Space::Src::CLI::Registry.register "status", Space::Src::CLI::Status::Show