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