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,44 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "fileutils"
4
- require_relative "../oci_runner"
5
-
6
- module Space::Core::CLI
7
- class Run < BaseCommand
8
- desc "Run the packed OCI image for the current space (auth injected at runtime). Pass a command and its arguments after `--` so they forward as separate argv tokens"
9
-
10
- argument :command, type: :array, required: false, desc: "Command to run in the container (default: login shell). Use `--` to pass a command with arguments, e.g. `space run -- hermes -z \"hello\"`"
11
- option :tty, type: :boolean, default: nil, desc: "Force interactive TTY (default: auto-detect)"
12
- option :env, type: :array, desc: "Host env var to forward into the container (repeatable; adds to run.env)"
13
- example "-- hermes -z \"What is 17 plus 4?\" # `--` forwards the command and its args as separate tokens (a quoted multi-word command arrives as one token and fails in-guest)"
14
-
15
- def call(command: [], tty: nil, env: [], **opts)
16
- setup_terminal(**opts.slice(:color, :colors))
17
- handle_errors do
18
- result = store.current.bind do |space|
19
- runner = Space::Core::OciRunner.new(
20
- space: space, interactive: tty.nil? ? CLI.tty?(out) : tty, env_vars: env
21
- )
22
- runner.command(command).fmap { |argv| { argv: argv, runner: runner } }
23
- end
24
- render(result) do |r|
25
- r[:runner].host_dirs.each { |d| FileUtils.mkdir_p(d) }
26
- warn_missing_env(r[:runner].missing_env)
27
- terminal.say "Running: #{r[:argv].join(' ')}"
28
- out.flush # Kernel.exec replaces the process without flushing buffered IO
29
- Kernel.exec(*r[:argv])
30
- end
31
- end
32
- end
33
-
34
- private
35
-
36
- # A requested var absent from the host env is almost always a forgotten export;
37
- # forwarding silently omits it and the payload fails opaquely in-guest. Warn, don't fail.
38
- def warn_missing_env(missing)
39
- return if missing.empty?
40
-
41
- terminal.error("Warning: requested env not set on host, not forwarded: #{missing.join(', ')}")
42
- end
43
- end
44
- end
@@ -1,125 +0,0 @@
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
@@ -1,21 +0,0 @@
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
@@ -1,74 +0,0 @@
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
- # One command, overloaded by its positional(s):
9
- # help token (`help`, or dry-cli's -h/--help which never reach here) → help
10
- # lone keyword / `<space> <keyword>` → set
11
- # bare, or a lone non-keyword identifier → report
12
- def call(rest: [], **opts)
13
- setup_terminal(**opts.slice(:color, :colors))
14
- args = Array(rest)
15
- return show_help if help_token?(args)
16
-
17
- handle_errors do
18
- if set_args?(args)
19
- set_status(args)
20
- elsif args.length <= 1
21
- report(args.first)
22
- else
23
- raise Space::Core::Error, "Usage: space status [SPACE] STATUS"
24
- end
25
- end
26
- end
27
-
28
- private
29
-
30
- # The bare word `help` shows the command help, like dry-cli's -h/--help (which
31
- # it intercepts before dispatch). It must never set status to "help" or report.
32
- def help_token?(args)
33
- args == ["help"]
34
- end
35
-
36
- def show_help
37
- out.puts Dry::CLI::Banner.call(self.class, Dry::CLI::ProgramName.call(["status"]))
38
- CLI.record_outcome(Outcome.new(exit_code: 0))
39
- end
40
-
41
- # A set request: `<space> <keyword>` (two args), or a lone status keyword. A
42
- # lone non-keyword arg is a space identifier to REPORT, not a malformed status.
43
- def set_args?(args)
44
- args.length == 2 ||
45
- (args.length == 1 && Space::Core::Space::VALID_STATUSES.include?(args.first))
46
- end
47
-
48
- def set_status(args)
49
- identifier, status_value = args.length == 2 ? args : [nil, args.first]
50
- render(store.find(identifier)) do |space|
51
- space.update_status(status_value)
52
- terminal.success "#{space.id} is #{space.status}"
53
- CLI.record_outcome(Outcome.new(exit_code: 0))
54
- end
55
- end
56
-
57
- def report(identifier)
58
- render(store.find(identifier)) do |space|
59
- terminal.say "ID: #{space.id}"
60
- terminal.say "Title: #{space.title}"
61
- terminal.say "Status: #{space.status}"
62
- terminal.say "Path: #{terminal.path(space.path)}"
63
- terminal.say "Created: #{space.data['created_at']}"
64
- terminal.say "Updated: #{space.data['updated_at']}"
65
- lines = LoopStatus.lines(space.data["project"])
66
- if lines
67
- terminal.say ""
68
- lines.each { |line| terminal.say line }
69
- end
70
- CLI.record_outcome(Outcome.new(exit_code: 0))
71
- end
72
- end
73
- end
74
- end
@@ -1,17 +0,0 @@
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
@@ -1,177 +0,0 @@
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
- require_relative "cli/pack"
141
- require_relative "cli/build"
142
- require_relative "cli/run"
143
-
144
- Space::Core::CLI::Registry.register "init", Space::Core::CLI::Init
145
- Space::Core::CLI::Registry.register "new", Space::Core::CLI::New
146
- Space::Core::CLI::Registry.register "list", Space::Core::CLI::List
147
- Space::Core::CLI::Registry.register "ls", Space::Core::CLI::List
148
- Space::Core::CLI::Registry.register "show", Space::Core::CLI::Show
149
- Space::Core::CLI::Registry.register "path", Space::Core::CLI::Path
150
- Space::Core::CLI::Registry.register "use", Space::Core::CLI::Use
151
- Space::Core::CLI::Registry.register "current", Space::Core::CLI::Current
152
- Space::Core::CLI::Registry.register "status", Space::Core::CLI::Status
153
- Space::Core::CLI::Registry.register "config" do |c|
154
- c.register "show", Space::Core::CLI::Config::Show
155
- c.register "path", Space::Core::CLI::Config::ConfigPath
156
- c.register "set", Space::Core::CLI::Config::Set
157
- end
158
- Space::Core::CLI::Registry.register "repo" do |r|
159
- r.register "add", Space::Core::CLI::Repo::Add
160
- r.register "list", Space::Core::CLI::Repo::RepoList
161
- r.register "ls", Space::Core::CLI::Repo::RepoList
162
- r.register "resolve", Space::Core::CLI::Repo::Resolve
163
- end
164
- Space::Core::CLI::Registry.register "repos" do |r|
165
- r.register "add", Space::Core::CLI::Repo::Add
166
- r.register "list", Space::Core::CLI::Repo::RepoList
167
- r.register "ls", Space::Core::CLI::Repo::RepoList
168
- r.register "resolve", Space::Core::CLI::Repo::Resolve
169
- end
170
- Space::Core::CLI::Registry.register "shell" do |s|
171
- s.register "init", Space::Core::CLI::Shell::ShellInit
172
- s.register "fish", Space::Core::CLI::Shell::Fish
173
- s.register "complete", Space::Core::CLI::Shell::Complete
174
- end
175
- Space::Core::CLI::Registry.register "pack", Space::Core::CLI::Pack
176
- Space::Core::CLI::Registry.register "build", Space::Core::CLI::Build
177
- Space::Core::CLI::Registry.register "run", Space::Core::CLI::Run
@@ -1,51 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Space::Core
4
- module Commands
5
- module_function
6
-
7
- # Render a multi-flag command with trailing " \" continuations broken at
8
- # "--flag" boundaries, continuation lines indented two spaces. Commands
9
- # without "--" flags are returned unchanged.
10
- def wrap(command)
11
- parts = split_at_flag_boundaries(command)
12
- return command if parts.size <= 1
13
-
14
- parts.each_with_index.map do |part, i|
15
- segment = i.zero? ? part.rstrip : " #{part.lstrip}"
16
- i < parts.size - 1 ? "#{segment} \\" : segment
17
- end.join("\n")
18
- end
19
-
20
- # Same split points as command.split(/(?= --)/), but blind to " --" that
21
- # falls inside a single- or double-quoted flag value, so a --title
22
- # containing " -- " isn't torn apart mid-argument.
23
- def split_at_flag_boundaries(command)
24
- parts = []
25
- start = 0
26
- in_squote = false
27
- in_dquote = false
28
- escaped = false
29
- command.each_char.with_index do |ch, i|
30
- if escaped
31
- escaped = false
32
- next
33
- end
34
- case ch
35
- # A backslash escapes the next character outside quotes and inside
36
- # double quotes, but is a literal character inside single quotes.
37
- when "\\" then escaped = true unless in_squote
38
- when "'" then in_squote = !in_squote unless in_dquote
39
- when '"' then in_dquote = !in_dquote unless in_squote
40
- when " "
41
- if !in_squote && !in_dquote && command[i + 1, 2] == "--"
42
- parts << command[start...i]
43
- start = i
44
- end
45
- end
46
- end
47
- parts << command[start..]
48
- parts
49
- end
50
- end
51
- end
@@ -1,152 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "yaml"
4
- require "pathname"
5
-
6
- module Space::Core
7
- class Config
8
- DEFAULT_DATA = {
9
- "version" => 1,
10
- "base_dir" => "~/architect",
11
- "default_provider" => "github.com",
12
- "default_organization" => nil,
13
- "git_clone_protocol" => "ssh"
14
- }.freeze
15
- EDITABLE_KEYS = %w[
16
- base_dir
17
- spaces_dir
18
- src_dir
19
- default_provider
20
- default_organization
21
- git_clone_protocol
22
- ].freeze
23
- VALID_GIT_CLONE_PROTOCOLS = %w[ssh https].freeze
24
-
25
- attr_reader :path, :data, :env
26
-
27
- def self.default_path(env: ENV)
28
- XDG.config_home(env: env).join("space-architect", "config.yml")
29
- end
30
-
31
- def self.load(env: ENV, path: default_path(env: env))
32
- new(env:, path:).load
33
- end
34
-
35
- def initialize(env: ENV, path: self.class.default_path(env: env), data: nil)
36
- @path = Pathname.new(path)
37
- @env = env
38
- @data = data ? DEFAULT_DATA.merge(stringify_keys(data)) : DEFAULT_DATA.dup
39
- end
40
-
41
- def load
42
- @data = if path.exist?
43
- parsed = YAML.safe_load(path.read, aliases: false) || {}
44
- unless parsed.is_a?(Hash)
45
- raise Error, "Config file must contain a YAML mapping: #{path}"
46
- end
47
-
48
- DEFAULT_DATA.merge(stringify_keys(parsed))
49
- else
50
- DEFAULT_DATA.dup
51
- end
52
- self
53
- end
54
-
55
- def ensure_exists!
56
- save unless path.exist?
57
- self
58
- end
59
-
60
- def save
61
- AtomicWrite.write(path, YAML.dump(data))
62
- self
63
- end
64
-
65
- def base_dir
66
- Pathname.new(XDG.expand_user(data.fetch("base_dir"), env: env))
67
- end
68
-
69
- def spaces_dir
70
- value = normalized_value(data["spaces_dir"])
71
- return base_dir.join("spaces") unless value
72
-
73
- Pathname.new(XDG.expand_user(value, env: env))
74
- end
75
-
76
- def src_dir
77
- return base_dir.join("src") unless data.key?("src_dir")
78
-
79
- value = normalized_value(data["src_dir"])
80
- return nil unless value
81
-
82
- Pathname.new(XDG.expand_user(value, env: env))
83
- end
84
-
85
- def default_provider
86
- normalize_provider(data["default_provider"])
87
- end
88
-
89
- def default_organization
90
- normalized_value(data["default_organization"])
91
- end
92
-
93
- def git_clone_protocol
94
- protocol = normalized_value(data["git_clone_protocol"]) || "ssh"
95
- unless VALID_GIT_CLONE_PROTOCOLS.include?(protocol)
96
- raise Error, "Invalid git_clone_protocol '#{protocol}'. Expected one of: #{VALID_GIT_CLONE_PROTOCOLS.join(', ')}"
97
- end
98
-
99
- protocol
100
- end
101
-
102
- def set(key, value)
103
- normalized_key = key.to_s
104
- unless EDITABLE_KEYS.include?(normalized_key)
105
- raise InvalidConfigKeyError, "Unknown config key '#{key}'. Expected one of: #{EDITABLE_KEYS.join(', ')}"
106
- end
107
-
108
- data[normalized_key] = normalize_config_value(normalized_key, value)
109
- save
110
- end
111
-
112
- private
113
-
114
- def normalize_config_value(key, value)
115
- case key
116
- when "default_provider"
117
- normalize_provider(value)
118
- when "default_organization"
119
- normalized_value(value)
120
- when "git_clone_protocol"
121
- protocol = normalized_value(value)
122
- unless VALID_GIT_CLONE_PROTOCOLS.include?(protocol)
123
- raise Error, "Invalid git_clone_protocol '#{value}'. Expected one of: #{VALID_GIT_CLONE_PROTOCOLS.join(', ')}"
124
- end
125
-
126
- protocol
127
- else
128
- value
129
- end
130
- end
131
-
132
- def normalize_provider(value)
133
- normalized = normalized_value(value)
134
- return nil unless normalized
135
-
136
- normalized
137
- .delete_prefix("https://")
138
- .delete_prefix("http://")
139
- .delete_prefix("ssh://")
140
- .delete_suffix("/")
141
- end
142
-
143
- def normalized_value(value)
144
- normalized = value.to_s.strip
145
- normalized.empty? ? nil : normalized
146
- end
147
-
148
- def stringify_keys(hash)
149
- hash.each_with_object({}) { |(key, value), result| result[key.to_s] = value }
150
- end
151
- end
152
- end
@@ -1,14 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Space::Core
4
- class Error < StandardError; end
5
- class NotFoundError < Error; end
6
- class AmbiguousSpaceError < Error; end
7
- class InvalidStatusError < Error; end
8
- class CurrentSpaceMissingError < Error; end
9
- class InvalidConfigKeyError < Error; end
10
- class RepoResolutionError < Error; end
11
- class RepoExistsError < Error; end
12
- class GitError < Error; end
13
- class MiseError < Error; end
14
- end