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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +103 -0
- data/README.md +248 -155
- data/exe/architect +1 -1
- data/exe/space +2 -2
- data/exe/src +13 -0
- data/lib/space_architect/architect_mission.rb +84 -53
- data/lib/space_architect/cli/architect.rb +92 -132
- data/lib/space_architect/cli/research.rb +94 -0
- data/lib/space_architect/cli/space.rb +25 -31
- data/lib/space_architect/cli/src.rb +20 -14
- data/lib/space_architect/cli.rb +22 -22
- data/lib/space_architect/dispatcher.rb +5 -1
- data/lib/space_architect/harness.rb +123 -16
- data/lib/space_architect/research/mux.rb +127 -0
- data/lib/space_architect/research/registry.rb +70 -0
- data/lib/space_architect/research/renderer.rb +101 -0
- data/lib/space_architect/research/run.rb +7 -0
- data/lib/space_architect/research/supervisor.rb +108 -0
- data/lib/space_architect/research.rb +13 -0
- data/lib/space_architect/run_creator.rb +53 -0
- data/lib/space_architect/skill_installer.rb +81 -79
- data/lib/space_architect.rb +5 -20
- data/lib/{space_architect → space_core}/atomic_write.rb +1 -1
- data/lib/space_core/cli/base_command.rb +19 -0
- data/lib/space_core/cli/config.rb +49 -0
- data/lib/space_core/cli/current.rb +16 -0
- data/lib/space_core/cli/help.rb +110 -0
- data/lib/space_core/cli/helpers.rb +115 -0
- data/lib/space_core/cli/init.rb +29 -0
- data/lib/space_core/cli/list.rb +24 -0
- data/lib/space_core/cli/new.rb +38 -0
- data/lib/space_core/cli/path.rb +16 -0
- data/lib/space_core/cli/repeatable_options.rb +75 -0
- data/lib/space_core/cli/repo.rb +76 -0
- data/lib/space_core/cli/shell.rb +125 -0
- data/lib/space_core/cli/show.rb +21 -0
- data/lib/space_core/cli/status.rb +33 -0
- data/lib/space_core/cli/use.rb +17 -0
- data/lib/space_core/cli.rb +171 -0
- data/lib/{space_architect → space_core}/config.rb +1 -1
- data/lib/{space_architect → space_core}/errors.rb +1 -1
- data/lib/{space_architect → space_core}/git_client.rb +1 -1
- data/lib/{space_architect → space_core}/mise_client.rb +1 -1
- data/lib/{space_architect → space_core}/repo_reference.rb +1 -1
- data/lib/{space_architect → space_core}/repo_resolver.rb +1 -1
- data/lib/{space_architect → space_core}/shell_integration.rb +1 -1
- data/lib/{space_architect → space_core}/slugger.rb +1 -1
- data/lib/{space_architect → space_core}/space.rb +1 -1
- data/lib/{space_architect → space_core}/space_store.rb +12 -12
- data/lib/{space_architect → space_core}/state.rb +1 -1
- data/lib/{space_architect → space_core}/terminal.rb +1 -1
- data/lib/space_core/version.rb +7 -0
- data/lib/{space_architect → space_core}/warnings.rb +1 -1
- data/lib/{space_architect → space_core}/xdg.rb +1 -1
- data/lib/space_core.rb +24 -0
- data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/cli/clone.rb +5 -5
- data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/cli/config.rb +7 -7
- data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/cli/daemon.rb +46 -30
- data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/cli/options.rb +1 -1
- data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/cli/org.rb +9 -9
- data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/cli/repo.rb +9 -9
- data/lib/space_src/cli/shell.rb +122 -0
- data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/cli/status.rb +7 -7
- data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/cli/sync.rb +17 -17
- data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/cli.rb +42 -11
- data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/cloner.rb +3 -3
- data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/config/contract.rb +1 -1
- data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/config/duration.rb +1 -1
- data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/config/model.rb +1 -1
- data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/config/store.rb +5 -5
- data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/forge/client.rb +2 -2
- data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/forge/github.rb +4 -4
- data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/launchd/agent.rb +5 -5
- data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/launchd/plist.rb +3 -3
- data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/log_rotator.rb +1 -1
- data/lib/space_src/migration.rb +43 -0
- data/lib/space_src/nav.rb +98 -0
- data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/paths.rb +2 -2
- data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/scm/client.rb +1 -1
- data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/scm/git.rb +4 -4
- data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/scm/status.rb +1 -1
- data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/shell.rb +1 -1
- data/lib/space_src/shell_integration.rb +321 -0
- data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/state/lock.rb +1 -1
- data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/state/store.rb +2 -2
- data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/sync/engine.rb +12 -12
- data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/sync/repo_plan.rb +3 -3
- data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/ui/interactive_reporter.rb +1 -1
- data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/ui/json_reporter.rb +1 -1
- data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/ui/mode.rb +1 -1
- data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/ui/plain_reporter.rb +1 -1
- data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/ui/reporter.rb +1 -1
- data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/version.rb +2 -2
- data/lib/space_src.rb +37 -0
- data/skill/architect/SKILL.md +2 -2
- data/skill/architect/research.md +46 -37
- metadata +115 -67
- data/lib/space_architect/cli/config.rb +0 -61
- data/lib/space_architect/cli/current.rb +0 -22
- data/lib/space_architect/cli/helpers.rb +0 -117
- data/lib/space_architect/cli/init.rb +0 -35
- data/lib/space_architect/cli/list.rb +0 -30
- data/lib/space_architect/cli/new.rb +0 -43
- data/lib/space_architect/cli/options.rb +0 -12
- data/lib/space_architect/cli/path.rb +0 -22
- data/lib/space_architect/cli/repo.rb +0 -88
- data/lib/space_architect/cli/shell.rb +0 -137
- data/lib/space_architect/cli/show.rb +0 -27
- data/lib/space_architect/cli/status.rb +0 -39
- data/lib/space_architect/cli/use.rb +0 -23
- data/lib/space_architect/version.rb +0 -5
- 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
|