space-architect 1.3.0 → 2.0.0.rc1

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 (113) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +103 -0
  3. data/README.md +248 -155
  4. data/exe/architect +1 -1
  5. data/exe/space +2 -2
  6. data/exe/src +13 -0
  7. data/lib/space_architect/architect_mission.rb +84 -53
  8. data/lib/space_architect/cli/architect.rb +92 -132
  9. data/lib/space_architect/cli/research.rb +94 -0
  10. data/lib/space_architect/cli/space.rb +25 -31
  11. data/lib/space_architect/cli/src.rb +20 -14
  12. data/lib/space_architect/cli.rb +22 -22
  13. data/lib/space_architect/dispatcher.rb +5 -1
  14. data/lib/space_architect/harness.rb +123 -16
  15. data/lib/space_architect/research/mux.rb +127 -0
  16. data/lib/space_architect/research/registry.rb +70 -0
  17. data/lib/space_architect/research/renderer.rb +101 -0
  18. data/lib/space_architect/research/run.rb +7 -0
  19. data/lib/space_architect/research/supervisor.rb +108 -0
  20. data/lib/space_architect/research.rb +13 -0
  21. data/lib/space_architect/run_creator.rb +53 -0
  22. data/lib/space_architect/skill_installer.rb +81 -79
  23. data/lib/space_architect.rb +5 -20
  24. data/lib/{space_architect → space_core}/atomic_write.rb +1 -1
  25. data/lib/space_core/cli/base_command.rb +19 -0
  26. data/lib/space_core/cli/config.rb +49 -0
  27. data/lib/space_core/cli/current.rb +16 -0
  28. data/lib/space_core/cli/help.rb +110 -0
  29. data/lib/space_core/cli/helpers.rb +115 -0
  30. data/lib/space_core/cli/init.rb +29 -0
  31. data/lib/space_core/cli/list.rb +24 -0
  32. data/lib/space_core/cli/new.rb +38 -0
  33. data/lib/space_core/cli/path.rb +16 -0
  34. data/lib/space_core/cli/repeatable_options.rb +75 -0
  35. data/lib/space_core/cli/repo.rb +76 -0
  36. data/lib/space_core/cli/shell.rb +125 -0
  37. data/lib/space_core/cli/show.rb +21 -0
  38. data/lib/space_core/cli/status.rb +33 -0
  39. data/lib/space_core/cli/use.rb +17 -0
  40. data/lib/space_core/cli.rb +171 -0
  41. data/lib/{space_architect → space_core}/config.rb +1 -1
  42. data/lib/{space_architect → space_core}/errors.rb +1 -1
  43. data/lib/{space_architect → space_core}/git_client.rb +1 -1
  44. data/lib/{space_architect → space_core}/mise_client.rb +1 -1
  45. data/lib/{space_architect → space_core}/repo_reference.rb +1 -1
  46. data/lib/{space_architect → space_core}/repo_resolver.rb +1 -1
  47. data/lib/{space_architect → space_core}/shell_integration.rb +1 -1
  48. data/lib/{space_architect → space_core}/slugger.rb +1 -1
  49. data/lib/{space_architect → space_core}/space.rb +1 -1
  50. data/lib/{space_architect → space_core}/space_store.rb +12 -12
  51. data/lib/{space_architect → space_core}/state.rb +1 -1
  52. data/lib/{space_architect → space_core}/terminal.rb +1 -1
  53. data/lib/space_core/version.rb +7 -0
  54. data/lib/{space_architect → space_core}/warnings.rb +1 -1
  55. data/lib/{space_architect → space_core}/xdg.rb +1 -1
  56. data/lib/space_core.rb +24 -0
  57. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/cli/clone.rb +5 -5
  58. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/cli/config.rb +7 -7
  59. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/cli/daemon.rb +46 -30
  60. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/cli/options.rb +1 -1
  61. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/cli/org.rb +9 -9
  62. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/cli/repo.rb +9 -9
  63. data/lib/space_src/cli/shell.rb +122 -0
  64. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/cli/status.rb +7 -7
  65. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/cli/sync.rb +17 -17
  66. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/cli.rb +42 -11
  67. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/cloner.rb +3 -3
  68. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/config/contract.rb +1 -1
  69. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/config/duration.rb +1 -1
  70. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/config/model.rb +1 -1
  71. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/config/store.rb +5 -5
  72. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/forge/client.rb +2 -2
  73. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/forge/github.rb +4 -4
  74. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/launchd/agent.rb +5 -5
  75. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/launchd/plist.rb +3 -3
  76. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/log_rotator.rb +1 -1
  77. data/lib/space_src/migration.rb +43 -0
  78. data/lib/space_src/nav.rb +98 -0
  79. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/paths.rb +2 -2
  80. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/scm/client.rb +1 -1
  81. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/scm/git.rb +4 -4
  82. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/scm/status.rb +1 -1
  83. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/shell.rb +1 -1
  84. data/lib/space_src/shell_integration.rb +321 -0
  85. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/state/lock.rb +1 -1
  86. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/state/store.rb +2 -2
  87. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/sync/engine.rb +12 -12
  88. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/sync/repo_plan.rb +3 -3
  89. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/ui/interactive_reporter.rb +1 -1
  90. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/ui/json_reporter.rb +1 -1
  91. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/ui/mode.rb +1 -1
  92. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/ui/plain_reporter.rb +1 -1
  93. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/ui/reporter.rb +1 -1
  94. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/version.rb +2 -2
  95. data/lib/space_src.rb +37 -0
  96. data/skill/architect/SKILL.md +2 -2
  97. data/skill/architect/research.md +46 -37
  98. metadata +115 -67
  99. data/lib/space_architect/cli/config.rb +0 -61
  100. data/lib/space_architect/cli/current.rb +0 -22
  101. data/lib/space_architect/cli/helpers.rb +0 -117
  102. data/lib/space_architect/cli/init.rb +0 -35
  103. data/lib/space_architect/cli/list.rb +0 -30
  104. data/lib/space_architect/cli/new.rb +0 -43
  105. data/lib/space_architect/cli/options.rb +0 -12
  106. data/lib/space_architect/cli/path.rb +0 -22
  107. data/lib/space_architect/cli/repo.rb +0 -88
  108. data/lib/space_architect/cli/shell.rb +0 -137
  109. data/lib/space_architect/cli/show.rb +0 -27
  110. data/lib/space_architect/cli/status.rb +0 -39
  111. data/lib/space_architect/cli/use.rb +0 -23
  112. data/lib/space_architect/version.rb +0 -5
  113. data/vendor/repo-tender/lib/space_architect/pristine.rb +0 -44
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Space::Core::CLI
4
+ class New < BaseCommand
5
+ desc "Create a new project space"
6
+ argument :title, required: true, desc: "Space title"
7
+ option :repo, type: :array, aliases: ["-r"], desc: "Repo ref to clone (repeatable: pass -r once per repo)"
8
+ option :git, type: :boolean, default: true, desc: "Initialize the space as a Git repository (use --no-git to skip)"
9
+ example "\"My Space\" -r org/repo -r example-tools/alpha # clone two repos into the space"
10
+
11
+ def call(title:, repo: [], git: true, **opts)
12
+ setup_terminal(**opts.slice(:color, :colors))
13
+ result = store.create(title, git: git).bind do |space|
14
+ terminal.success "Created #{space.id}"
15
+
16
+ repo_specs = Array(repo).compact
17
+ repo_specs.each { |spec| terminal.say "Queued #{spec}" }
18
+
19
+ next Success(space) if repo_specs.empty?
20
+
21
+ progress = RepoProgress.new(repo_specs.length)
22
+ terminal.with_spinner(-> { progress.message }) do
23
+ store.add_repos_to(space, repo_specs, reporter: progress)
24
+ end.fmap do |results|
25
+ results.each do |r|
26
+ terminal.success "Added #{r.fetch(:repo).fetch('full_name')}"
27
+ terminal.say terminal.path(r.fetch(:path))
28
+ end
29
+ space
30
+ end
31
+ end
32
+ render(result) do |space|
33
+ terminal.say terminal.path(space.path)
34
+ CLI.record_outcome(Outcome.new(exit_code: 0))
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Space::Core::CLI
4
+ class Path < BaseCommand
5
+ desc "Print the path for a space or the current space"
6
+ argument :identifier, required: false, desc: "Space ID or title slug"
7
+
8
+ def call(identifier: nil, **opts)
9
+ setup_terminal(**opts.slice(:color, :colors))
10
+ render(store.path_for(identifier)) do |path|
11
+ terminal.say terminal.path(path)
12
+ CLI.record_outcome(Outcome.new(exit_code: 0))
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/cli"
4
+
5
+ # dry-cli (1.4.x) treats `type: :array` options as comma-separated and OVERWRITES
6
+ # on each occurrence, so `-r a -r b` yields ["b"]. We want repeated flags to
7
+ # accumulate (`-r a -r b -r c` => ["a", "b", "c"]) the way git/docker-style CLIs
8
+ # do, while still accepting the comma form. dry-cli exposes no hook for this, so
9
+ # we reopen two private seams, each mirroring dry-cli 1.4.1 with a single change:
10
+ #
11
+ # * Parser.call — concat instead of assign for array options.
12
+ # * Banner.extended_command_options — drop the "=VALUE1,VALUE2,.." hint that
13
+ # advertised the comma form as the only way; show the plain repeatable flag,
14
+ # matching how you actually type it (-r VALUE).
15
+ #
16
+ # These mirror the released 1.4.1 source EXACTLY (not the dry-rb main branch,
17
+ # which already differs). Pinned via `~> 1.4`; if a future dry-cli reworks these
18
+ # methods, repeatable_options_test goes red and we re-sync. Rationale:
19
+ # notes/ruby-cli-gems-report.md.
20
+ module Dry
21
+ class CLI
22
+ module Parser
23
+ def self.call(command, arguments, prog_name)
24
+ original_arguments = arguments.dup
25
+ parsed_options = {}
26
+
27
+ OptionParser.new do |opts|
28
+ command.options.each do |option|
29
+ opts.on(*option.parser_options) do |value|
30
+ if option.array?
31
+ (parsed_options[option.name.to_sym] ||= []).concat(value)
32
+ else
33
+ parsed_options[option.name.to_sym] = value
34
+ end
35
+ end
36
+ end
37
+
38
+ opts.on_tail("-h", "--help") do
39
+ return Result.help
40
+ end
41
+ end.parse!(arguments)
42
+
43
+ parsed_options = command.default_params.merge(parsed_options)
44
+ parse_required_params(command, arguments, prog_name, parsed_options)
45
+ rescue ::OptionParser::ParseError, ValueError
46
+ Result.failure("ERROR: \"#{prog_name}\" was called with arguments \"#{original_arguments.join(" ")}\"")
47
+ end
48
+ end
49
+
50
+ module Banner
51
+ def self.extended_command_options(command)
52
+ result = command.options.map do |option|
53
+ name = Inflector.dasherize(option.name)
54
+ name = if option.boolean?
55
+ "[no-]#{name}"
56
+ elsif option.flag?
57
+ name
58
+ else
59
+ # array options included: repeated flags accumulate, so show
60
+ # the single repeatable form rather than "=VALUE1,VALUE2,..".
61
+ "#{name}=VALUE"
62
+ end
63
+ name = "#{name}, #{option.alias_names.join(", ")}" if option.aliases.any?
64
+ name = " --#{name.ljust(30)}"
65
+ name = "#{name} # #{option.desc}"
66
+ name = "#{name}, default: #{option.default.inspect}" unless option.default.nil?
67
+ name
68
+ end
69
+
70
+ result << " --#{"help, -h".ljust(30)} # Print this help"
71
+ result.join("\n")
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Space::Core::CLI
4
+ module Repo
5
+ class Add < BaseCommand
6
+ desc "Clone repos into the current space"
7
+ argument :repos, type: :array, required: false, desc: "REPO [REPO...]"
8
+
9
+ def call(repos: [], **opts)
10
+ setup_terminal(**opts.slice(:color, :colors))
11
+ handle_errors do
12
+ specs = Array(repos).compact
13
+ if specs.empty?
14
+ terminal.error("Usage: space repo add REPO [REPO...]")
15
+ CLI.record_outcome(Outcome.new(exit_code: 1))
16
+ next
17
+ end
18
+
19
+ progress = RepoProgress.new(specs.length)
20
+ add_result = terminal.with_spinner(-> { progress.message }) do
21
+ store.add_repos(specs, reporter: progress)
22
+ end
23
+ render(add_result) do |results|
24
+ results.each do |result|
25
+ terminal.success "Added #{result.fetch(:repo).fetch('full_name')}"
26
+ terminal.say terminal.path(result.fetch(:path))
27
+ end
28
+ CLI.record_outcome(Outcome.new(exit_code: 0))
29
+ end
30
+ end
31
+ end
32
+ end
33
+
34
+ class RepoList < BaseCommand
35
+ desc "List repos in the current space"
36
+
37
+ def call(**opts)
38
+ setup_terminal(**opts.slice(:color, :colors))
39
+ handle_errors do
40
+ render(store.repos) do |repos|
41
+ if repos.empty?
42
+ id = store.find.fmap(&:id).value_or("(unknown space)")
43
+ terminal.say "No repos found in #{id}"
44
+ next
45
+ end
46
+
47
+ rows = repos.map { |repo| [repo.fetch("full_name", repo["name"]), repo.fetch("path", "")] }
48
+ terminal.say terminal.table(["Repo", "Path"], rows)
49
+ CLI.record_outcome(Outcome.new(exit_code: 0))
50
+ end
51
+ end
52
+ end
53
+ end
54
+
55
+ class Resolve < BaseCommand
56
+ desc "Resolve repo refs without cloning"
57
+ argument :repos, type: :array, required: false, desc: "REPO [REPO...]"
58
+
59
+ def call(repos: [], **opts)
60
+ setup_terminal(**opts.slice(:color, :colors))
61
+ handle_errors do
62
+ specs = Array(repos).compact
63
+ if specs.empty?
64
+ terminal.error("Usage: space repo resolve REPO [REPO...]")
65
+ CLI.record_outcome(Outcome.new(exit_code: 1))
66
+ next
67
+ end
68
+
69
+ references = specs.map { |spec| Space::Core::RepoResolver.new(project_config).resolve(spec) }
70
+ terminal.say terminal.table(["Repo", "Clone URL"], references.map { |ref| [ref.full_name, ref.clone_url] })
71
+ CLI.record_outcome(Outcome.new(exit_code: 0))
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Space::Core::CLI
4
+ module Shell
5
+ class ShellInit < BaseCommand
6
+ desc "Print shell integration script"
7
+ argument :shell_name, required: true, desc: "Shell name (e.g. fish)"
8
+
9
+ def call(shell_name:, **opts)
10
+ setup_terminal(**opts.slice(:color, :colors))
11
+ handle_errors do
12
+ terminal.say Space::Core::ShellIntegration.for(shell_name)
13
+ CLI.record_outcome(Outcome.new(exit_code: 0))
14
+ end
15
+ end
16
+ end
17
+
18
+ class Fish < BaseCommand
19
+ desc "Manage fish shell integration: install, uninstall, path"
20
+ argument :subcommand, required: false, desc: "install, uninstall, or path (default: install)"
21
+ option :force, type: :boolean, default: false, desc: "Overwrite or remove existing shell files"
22
+
23
+ def call(subcommand: "install", force: false, **opts)
24
+ setup_terminal(**opts.slice(:color, :colors))
25
+ handle_errors do
26
+ case subcommand
27
+ when "install"
28
+ result = Space::Core::ShellIntegration.install("fish", env: project_config.env, force: force)
29
+ terminal.success fish_install_message(result.fetch(:action), result.fetch(:path))
30
+ terminal.success fish_completions_install_message(result.fetch(:completions_action), result.fetch(:completions_path))
31
+ terminal.say "Restart fish to load the integration in this terminal: exec fish"
32
+ when "uninstall"
33
+ result = Space::Core::ShellIntegration.uninstall("fish", env: project_config.env, force: force)
34
+ terminal.success fish_uninstall_message(result.fetch(:action), result.fetch(:path))
35
+ terminal.success fish_completions_uninstall_message(result.fetch(:completions_action), result.fetch(:completions_path))
36
+ when "path"
37
+ terminal.say "Function: #{terminal.path(Space::Core::ShellIntegration.path_for('fish', env: project_config.env))}"
38
+ terminal.say "Completions: #{terminal.path(Space::Core::ShellIntegration.completions_path_for('fish', env: project_config.env))}"
39
+ else
40
+ err.puts "Usage: space shell fish [install|uninstall|path]"
41
+ CLI.record_outcome(Outcome.new(exit_code: 1))
42
+ next
43
+ end
44
+ CLI.record_outcome(Outcome.new(exit_code: 0))
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ def fish_install_message(action, path)
51
+ case action
52
+ when :unchanged then "Fish integration already installed: #{terminal.path(path)}"
53
+ when :updated then "Updated fish integration: #{terminal.path(path)}"
54
+ else "Installed fish integration: #{terminal.path(path)}"
55
+ end
56
+ end
57
+
58
+ def fish_uninstall_message(action, path)
59
+ case action
60
+ when :missing then "Fish integration was not installed: #{terminal.path(path)}"
61
+ else "Removed fish integration: #{terminal.path(path)}"
62
+ end
63
+ end
64
+
65
+ def fish_completions_install_message(action, path)
66
+ case action
67
+ when :unchanged then "Fish completions already installed: #{terminal.path(path)}"
68
+ when :updated then "Updated fish completions: #{terminal.path(path)}"
69
+ else "Installed fish completions: #{terminal.path(path)}"
70
+ end
71
+ end
72
+
73
+ def fish_completions_uninstall_message(action, path)
74
+ case action
75
+ when :missing then "Fish completions were not installed: #{terminal.path(path)}"
76
+ else "Removed fish completions: #{terminal.path(path)}"
77
+ end
78
+ end
79
+ end
80
+
81
+ class Complete < BaseCommand
82
+ desc "Print completion candidates"
83
+ argument :kind, required: true, desc: "Completion kind"
84
+ argument :extra, type: :array, required: false, desc: "Extra args for completion"
85
+
86
+ def call(kind:, extra: [], **opts)
87
+ setup_terminal(**opts.slice(:color, :colors))
88
+ handle_errors do
89
+ completion_candidates(kind, Array(extra)).each { |c| terminal.say c }
90
+ CLI.record_outcome(Outcome.new(exit_code: 0))
91
+ end
92
+ end
93
+
94
+ private
95
+
96
+ def completion_candidates(kind, args)
97
+ case kind
98
+ when "spaces" then store.list.map { |space| "#{space.id}\t#{space.title}" }
99
+ when "statuses" then Space::Core::Space::VALID_STATUSES
100
+ when "config-keys" then Space::Core::Config::EDITABLE_KEYS
101
+ when "config-values" then completion_values_for_config_key(args.first)
102
+ when "shells" then ["fish"]
103
+ when "color-modes" then %w[auto always never]
104
+ when "repo-subcommands" then %w[add list ls resolve]
105
+ when "config-subcommands" then %w[show path set]
106
+ when "fish-subcommands" then %w[install uninstall path]
107
+ else
108
+ raise Space::Core::Error, "Usage: space shell complete #{completion_kinds.join('|')}"
109
+ end
110
+ end
111
+
112
+ def completion_values_for_config_key(key)
113
+ case key
114
+ when "git_clone_protocol" then Space::Core::Config::VALID_GIT_CLONE_PROTOCOLS
115
+ when "default_provider" then %w[github.com gitlab.com]
116
+ else []
117
+ end
118
+ end
119
+
120
+ def completion_kinds
121
+ %w[spaces statuses config-keys config-values shells color-modes repo-subcommands config-subcommands fish-subcommands]
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Space::Core::CLI
4
+ class Show < BaseCommand
5
+ desc "Show metadata for a space or the current space"
6
+ argument :identifier, required: false, desc: "Space ID or title slug"
7
+
8
+ def call(identifier: nil, **opts)
9
+ setup_terminal(**opts.slice(:color, :colors))
10
+ render(store.find(identifier)) do |space|
11
+ terminal.say "ID: #{space.id}"
12
+ terminal.say "Title: #{space.title}"
13
+ terminal.say "Status: #{space.status}"
14
+ terminal.say "Path: #{terminal.path(space.path)}"
15
+ terminal.say "Created: #{space.data['created_at']}"
16
+ terminal.say "Updated: #{space.data['updated_at']}"
17
+ CLI.record_outcome(Outcome.new(exit_code: 0))
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Space::Core::CLI
4
+ class Status < BaseCommand
5
+ desc "Set a space status: active, paused, done, archived"
6
+ argument :rest, type: :array, required: false, desc: "[SPACE] STATUS"
7
+
8
+ def call(rest: [], **opts)
9
+ setup_terminal(**opts.slice(:color, :colors))
10
+ handle_errors do
11
+ identifier, status_value = parse_status_args(Array(rest))
12
+ render(store.find(identifier)) do |space|
13
+ space.update_status(status_value)
14
+ terminal.success "#{space.id} is #{space.status}"
15
+ CLI.record_outcome(Outcome.new(exit_code: 0))
16
+ end
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def parse_status_args(args)
23
+ case args.length
24
+ when 1
25
+ [nil, args.first]
26
+ when 2
27
+ args
28
+ else
29
+ raise Space::Core::Error, "Usage: space status [SPACE] STATUS"
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Space::Core::CLI
4
+ class Use < BaseCommand
5
+ desc "Remember a space in recent state and print its path"
6
+ argument :identifier, required: true, desc: "Space ID or title slug"
7
+
8
+ def call(identifier:, **opts)
9
+ setup_terminal(**opts.slice(:color, :colors))
10
+ render(store.use(identifier)) do |space|
11
+ terminal.success "Recent space: #{space.id}"
12
+ terminal.say terminal.path(space.path)
13
+ CLI.record_outcome(Outcome.new(exit_code: 0))
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,171 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/cli"
4
+ require "pastel"
5
+ require_relative "cli/repeatable_options"
6
+ require_relative "cli/help"
7
+
8
+ module Space::Core
9
+ module CLI
10
+ CLI = self
11
+
12
+ Outcome = Data.define(:exit_code, :message) do
13
+ def initialize(exit_code:, message: nil) = super
14
+ end
15
+
16
+ def self.record_outcome(o) = (Thread.current[:space_core_cli_outcome] = o)
17
+ def self.last_outcome = Thread.current[:space_core_cli_outcome]
18
+
19
+ # Pastel used by the colourful help listing (Help / the Usage reopen). Set
20
+ # per-invocation in .call from the output stream and --color; defaults to a
21
+ # disabled instance so non-CLI callers and tests render plain text.
22
+ def self.help_pastel = @help_pastel ||= Pastel.new(enabled: false)
23
+
24
+ def self.help_pastel=(pastel)
25
+ @help_pastel = pastel
26
+ end
27
+
28
+ # Whether the help listing should be colourised: honour an explicit
29
+ # --color/--colors (always/never), otherwise auto-detect from the streams the
30
+ # listing can land on (stdout for top-level help, stderr for bare namespaces).
31
+ def self.help_colors?(argv, out, err)
32
+ case color_mode(argv)
33
+ when "always" then true
34
+ when "never" then false
35
+ else tty?(out) || tty?(err)
36
+ end
37
+ end
38
+
39
+ def self.tty?(io) = io.respond_to?(:tty?) && io.tty?
40
+
41
+ def self.color_mode(argv)
42
+ argv.each_with_index do |arg, i|
43
+ return arg.split("=", 2)[1].to_s.downcase if arg.start_with?("--color=", "--colors=")
44
+ return argv[i + 1].to_s.downcase if %w[--color --colors].include?(arg)
45
+ end
46
+ "auto"
47
+ end
48
+
49
+ module Registry
50
+ extend Dry::CLI::Registry
51
+ end
52
+
53
+ TOP_LEVEL_HELP = [[], ["--help"], ["-h"], ["help"]].freeze
54
+ VERSION_REQUEST = [["version"], ["--version"]].freeze
55
+
56
+ def self.call(argv, out = $stdout, err = $stderr)
57
+ Thread.current[:space_core_cli_outcome] = nil
58
+ self.help_pastel = Pastel.new(enabled: help_colors?(argv, out, err))
59
+
60
+ if TOP_LEVEL_HELP.include?(argv)
61
+ print_usage(out)
62
+ return 0
63
+ end
64
+
65
+ if VERSION_REQUEST.include?(argv)
66
+ print_version(out)
67
+ return 0
68
+ end
69
+
70
+ Dry::CLI.new(Registry).call(arguments: normalize_args(argv), out: out, err: err)
71
+ last_outcome&.exit_code || 0
72
+ end
73
+
74
+ # Move --color/--colors options to the end of the argument list so dry-cli's
75
+ # command routing is not confused by options before the subcommand name.
76
+ #
77
+ # Two passes:
78
+ # 1. Leading: extract two-token form (--color VALUE) and =-form from the
79
+ # front while args still look like options.
80
+ # 2. Non-leading: extract =-form (--color=VALUE / --colors=VALUE) from any
81
+ # position before the -- separator. The bare two-token form is ambiguous
82
+ # with a subcommand name in non-leading position and is left in place.
83
+ def self.normalize_args(argv)
84
+ args = argv.dup
85
+ extracted = []
86
+
87
+ # Pass 1: leading two-token and =-form (existing behavior, unchanged)
88
+ while (arg = args.first) && arg != "--" && arg.start_with?("-")
89
+ if %w[--color --colors].include?(arg)
90
+ extracted << args.shift
91
+ extracted << args.shift if args.first && !args.first.start_with?("-")
92
+ elsif arg.start_with?("--color=", "--colors=")
93
+ extracted << args.shift
94
+ else
95
+ break
96
+ end
97
+ end
98
+
99
+ # Pass 2: =-form from any non-leading position, stop at --
100
+ sep = args.index("--")
101
+ head = sep ? args[0, sep] : args
102
+ tail = sep ? args[sep..] : []
103
+ mid_color, head = head.partition { |a| a.start_with?("--color=", "--colors=") }
104
+ extracted += mid_color
105
+ args = head + tail
106
+
107
+ extracted.empty? ? args : args + extracted
108
+ end
109
+
110
+ def self.print_usage(out)
111
+ out.puts Dry::CLI::Usage.call(Registry.get([]))
112
+ end
113
+
114
+ def self.print_version(out)
115
+ out.puts ::Space::Core::VERSION
116
+ end
117
+
118
+ def self.run(argv, out = $stdout, err = $stderr)
119
+ Kernel.exit(call(argv, out, err))
120
+ rescue Interrupt
121
+ err.puts "interrupted"
122
+ Kernel.exit(130)
123
+ end
124
+ end
125
+ end
126
+
127
+ require_relative "cli/helpers"
128
+ require_relative "cli/base_command"
129
+ require_relative "cli/init"
130
+ require_relative "cli/new"
131
+ require_relative "cli/list"
132
+ require_relative "cli/show"
133
+ require_relative "cli/path"
134
+ require_relative "cli/use"
135
+ require_relative "cli/current"
136
+ require_relative "cli/status"
137
+ require_relative "cli/config"
138
+ require_relative "cli/repo"
139
+ require_relative "cli/shell"
140
+
141
+ Space::Core::CLI::Registry.register "init", Space::Core::CLI::Init
142
+ Space::Core::CLI::Registry.register "new", Space::Core::CLI::New
143
+ Space::Core::CLI::Registry.register "list", Space::Core::CLI::List
144
+ Space::Core::CLI::Registry.register "ls", Space::Core::CLI::List
145
+ Space::Core::CLI::Registry.register "show", Space::Core::CLI::Show
146
+ Space::Core::CLI::Registry.register "path", Space::Core::CLI::Path
147
+ Space::Core::CLI::Registry.register "use", Space::Core::CLI::Use
148
+ Space::Core::CLI::Registry.register "current", Space::Core::CLI::Current
149
+ Space::Core::CLI::Registry.register "status", Space::Core::CLI::Status
150
+ Space::Core::CLI::Registry.register "config" do |c|
151
+ c.register "show", Space::Core::CLI::Config::Show
152
+ c.register "path", Space::Core::CLI::Config::ConfigPath
153
+ c.register "set", Space::Core::CLI::Config::Set
154
+ end
155
+ Space::Core::CLI::Registry.register "repo" do |r|
156
+ r.register "add", Space::Core::CLI::Repo::Add
157
+ r.register "list", Space::Core::CLI::Repo::RepoList
158
+ r.register "ls", Space::Core::CLI::Repo::RepoList
159
+ r.register "resolve", Space::Core::CLI::Repo::Resolve
160
+ end
161
+ Space::Core::CLI::Registry.register "repos" do |r|
162
+ r.register "add", Space::Core::CLI::Repo::Add
163
+ r.register "list", Space::Core::CLI::Repo::RepoList
164
+ r.register "ls", Space::Core::CLI::Repo::RepoList
165
+ r.register "resolve", Space::Core::CLI::Repo::Resolve
166
+ end
167
+ Space::Core::CLI::Registry.register "shell" do |s|
168
+ s.register "init", Space::Core::CLI::Shell::ShellInit
169
+ s.register "fish", Space::Core::CLI::Shell::Fish
170
+ s.register "complete", Space::Core::CLI::Shell::Complete
171
+ end
@@ -3,7 +3,7 @@
3
3
  require "yaml"
4
4
  require "pathname"
5
5
 
6
- module SpaceArchitect
6
+ module Space::Core
7
7
  class Config
8
8
  DEFAULT_DATA = {
9
9
  "version" => 1,
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module SpaceArchitect
3
+ module Space::Core
4
4
  class Error < StandardError; end
5
5
  class NotFoundError < Error; end
6
6
  class AmbiguousSpaceError < Error; end
@@ -5,7 +5,7 @@ require "async/process"
5
5
  require "pathname"
6
6
  require "tempfile"
7
7
 
8
- module SpaceArchitect
8
+ module Space::Core
9
9
  class GitClient
10
10
  def init(path)
11
11
  path = Pathname.new(path)
@@ -4,7 +4,7 @@ require "async/process"
4
4
  require "pathname"
5
5
  require "tempfile"
6
6
 
7
- module SpaceArchitect
7
+ module Space::Core
8
8
  class MiseClient
9
9
  def trust(path)
10
10
  path = Pathname.new(path)
@@ -2,7 +2,7 @@
2
2
 
3
3
  require "pathname"
4
4
 
5
- module SpaceArchitect
5
+ module Space::Core
6
6
  RepoReference = Data.define(:provider, :owner, :name, :clone_url, :source) do
7
7
  def full_name
8
8
  "#{provider}/#{owner}/#{name}"
@@ -2,7 +2,7 @@
2
2
 
3
3
  require "uri"
4
4
 
5
- module SpaceArchitect
5
+ module Space::Core
6
6
  class RepoResolver
7
7
  SCP_LIKE_PATTERN = /\A(?:[^@\/]+@)?(?<provider>[^:\/]+):(?<path>.+)\z/
8
8
  URL_PATTERN = %r{\A[A-Za-z][A-Za-z0-9+\-.]*://}
@@ -2,7 +2,7 @@
2
2
 
3
3
  require "fileutils"
4
4
 
5
- module SpaceArchitect
5
+ module Space::Core
6
6
  module ShellIntegration
7
7
  FISH_TEMPLATE = <<~'FISH'
8
8
  # Generated by space-architect. Do not edit by hand.
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module SpaceArchitect
3
+ module Space::Core
4
4
  module Slugger
5
5
  module_function
6
6
 
@@ -4,7 +4,7 @@ require "yaml"
4
4
  require "pathname"
5
5
  require "time"
6
6
 
7
- module SpaceArchitect
7
+ module Space::Core
8
8
  class Space
9
9
  METADATA_FILE = "space.yaml"
10
10
  VALID_STATUSES = %w[active paused done archived].freeze