space-architect 1.3.0 → 2.0.0.rc2
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 +267 -158
- data/exe/architect +1 -1
- data/exe/space +2 -2
- data/exe/src +13 -0
- data/lib/space_architect/{architect_mission.rb → architect_project.rb} +475 -125
- data/lib/space_architect/cli/architect.rb +248 -178
- 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/gate_evaluator.rb +65 -0
- data/lib/space_architect/gate_lint.rb +140 -0
- data/lib/space_architect/harness.rb +145 -17
- 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/templates/architect.md.erb +15 -4
- data/lib/space_architect/templates/brief.md.erb +5 -5
- data/lib/space_architect/templates/iteration.md.erb +17 -6
- data/lib/space_architect.rb +8 -21
- 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/build.rb +27 -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 +123 -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/pack.rb +29 -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/run.rb +29 -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 +177 -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_core/oci_builder.rb +56 -0
- data/lib/space_core/oci_packer.rb +99 -0
- data/lib/space_core/oci_runner.rb +73 -0
- 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 +11 -3
- data/lib/{space_architect → space_core}/space_store.rb +13 -13
- data/lib/{space_architect → space_core}/state.rb +1 -1
- data/lib/space_core/templates/oci/dockerfile.erb +63 -0
- data/lib/space_core/templates/oci/dockerignore.erb +17 -0
- data/lib/space_core/templates/oci/entrypoint.sh.erb +10 -0
- 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 +110 -54
- data/skill/architect/dispatch.md +147 -39
- data/skill/architect/research.md +46 -37
- data/skill/architect-research/SKILL.md +2 -2
- data/skill/architect-vocabulary/SKILL.md +24 -21
- metadata +127 -68
- 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,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "dry/monads"
|
|
4
|
+
|
|
5
|
+
module Space::Core::CLI
|
|
6
|
+
module Helpers
|
|
7
|
+
include Dry::Monads[:result]
|
|
8
|
+
def project_config
|
|
9
|
+
@project_config ||= Space::Core::Config.load
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def state
|
|
13
|
+
@state ||= Space::Core::State.load
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def store
|
|
17
|
+
@store ||= Space::Core::SpaceStore.new(config: project_config, state: state)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def terminal
|
|
21
|
+
@terminal
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def setup_terminal(color: "auto", colors: nil)
|
|
25
|
+
@terminal = Space::Core::Terminal.new(
|
|
26
|
+
config: project_config,
|
|
27
|
+
stdout: out,
|
|
28
|
+
stderr: err,
|
|
29
|
+
color_mode: colors || color || "auto"
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def display_date(space)
|
|
34
|
+
id_date = space.id.match(/\A(\d{4})(\d{2})(\d{2})/)
|
|
35
|
+
return "#{id_date[1]}-#{id_date[2]}-#{id_date[3]}" if id_date
|
|
36
|
+
|
|
37
|
+
space.data["created_at"].to_s[0, 10]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def handle_errors
|
|
41
|
+
yield
|
|
42
|
+
rescue Space::Core::Error => e
|
|
43
|
+
if terminal
|
|
44
|
+
terminal.error(e.message)
|
|
45
|
+
else
|
|
46
|
+
err.puts e.message
|
|
47
|
+
end
|
|
48
|
+
CLI.record_outcome(Outcome.new(exit_code: 1, message: e.message))
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def render(result)
|
|
52
|
+
case result
|
|
53
|
+
when Dry::Monads::Result::Success
|
|
54
|
+
yield result.value! if block_given?
|
|
55
|
+
when Dry::Monads::Result::Failure
|
|
56
|
+
error = result.failure
|
|
57
|
+
message = error.respond_to?(:message) ? error.message : error.to_s
|
|
58
|
+
terminal ? terminal.error(message) : err.puts(message)
|
|
59
|
+
CLI.record_outcome(Outcome.new(exit_code: 1, message: message))
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
class RepoProgress
|
|
65
|
+
def initialize(total)
|
|
66
|
+
@total = total
|
|
67
|
+
@statuses = {}
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def start(addition)
|
|
71
|
+
source = addition[:src_source]
|
|
72
|
+
@statuses[addition.fetch(:reference).full_name] = source&.directory? ? :copying : :cloning
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def trust(addition)
|
|
76
|
+
@statuses[addition.fetch(:reference).full_name] = :trusting
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def finish(addition)
|
|
80
|
+
@statuses[addition.fetch(:reference).full_name] = :done
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def fail(addition)
|
|
84
|
+
@statuses[addition.fetch(:reference).full_name] = :failed
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def message
|
|
88
|
+
done = @statuses.count { |_repo, status| status == :done }
|
|
89
|
+
failed = @statuses.count { |_repo, status| status == :failed }
|
|
90
|
+
copying = @statuses.select { |_repo, status| status == :copying }.keys
|
|
91
|
+
cloning = @statuses.select { |_repo, status| status == :cloning }.keys
|
|
92
|
+
trusting = @statuses.select { |_repo, status| status == :trusting }.keys
|
|
93
|
+
|
|
94
|
+
if @total == 1
|
|
95
|
+
copying_repo = copying.first
|
|
96
|
+
cloning_repo = cloning.first
|
|
97
|
+
trusting_repo = trusting.first
|
|
98
|
+
return "Copying #{copying_repo}" if copying_repo
|
|
99
|
+
return "Cloning #{cloning_repo}" if cloning_repo
|
|
100
|
+
return "Trusting #{trusting_repo}" if trusting_repo
|
|
101
|
+
return "Fetch failed" if failed.positive?
|
|
102
|
+
|
|
103
|
+
"Preparing repos"
|
|
104
|
+
else
|
|
105
|
+
active = []
|
|
106
|
+
active << "copying #{copying.join(', ')}" unless copying.empty?
|
|
107
|
+
active << "cloning #{cloning.join(', ')}" unless cloning.empty?
|
|
108
|
+
active << "trusting #{trusting.join(', ')}" unless trusting.empty?
|
|
109
|
+
suffix = active.empty? ? nil : ": #{active.join('; ')}"
|
|
110
|
+
failed_text = failed.positive? ? ", #{failed} failed" : ""
|
|
111
|
+
"Fetching repos #{done}/#{@total}#{failed_text}#{suffix}"
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Space::Core::CLI
|
|
4
|
+
class Init < BaseCommand
|
|
5
|
+
desc "Create default XDG config and state files"
|
|
6
|
+
option :force, type: :boolean, default: false, desc: "Overwrite existing config and state files"
|
|
7
|
+
|
|
8
|
+
def call(force: false, **opts)
|
|
9
|
+
setup_terminal(**opts.slice(:color, :colors))
|
|
10
|
+
handle_errors do
|
|
11
|
+
if force
|
|
12
|
+
@project_config = Space::Core::Config.new
|
|
13
|
+
@state = Space::Core::State.new
|
|
14
|
+
project_config.save
|
|
15
|
+
state.save
|
|
16
|
+
else
|
|
17
|
+
project_config.ensure_exists!
|
|
18
|
+
state.ensure_exists!
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
FileUtils.mkdir_p(project_config.spaces_dir)
|
|
22
|
+
terminal.success "Config: #{terminal.path(project_config.path)}"
|
|
23
|
+
terminal.success "State: #{terminal.path(state.path)}"
|
|
24
|
+
terminal.success "Spaces: #{terminal.path(project_config.spaces_dir)}"
|
|
25
|
+
CLI.record_outcome(Outcome.new(exit_code: 0))
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Space::Core::CLI
|
|
4
|
+
class List < BaseCommand
|
|
5
|
+
desc "List spaces"
|
|
6
|
+
|
|
7
|
+
def call(**opts)
|
|
8
|
+
setup_terminal(**opts.slice(:color, :colors))
|
|
9
|
+
handle_errors do
|
|
10
|
+
spaces = store.list
|
|
11
|
+
if spaces.empty?
|
|
12
|
+
terminal.say "No spaces found in #{terminal.path(project_config.spaces_dir)}"
|
|
13
|
+
next
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
rows = spaces.map do |space|
|
|
17
|
+
[space.status, display_date(space), space.title, terminal.path(space.path)]
|
|
18
|
+
end
|
|
19
|
+
terminal.say terminal.table(%w[Status Date Title Path], rows)
|
|
20
|
+
CLI.record_outcome(Outcome.new(exit_code: 0))
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -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,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
require_relative "../oci_packer"
|
|
5
|
+
|
|
6
|
+
module Space::Core::CLI
|
|
7
|
+
class Pack < BaseCommand
|
|
8
|
+
desc "Generate a portable OCI build context for the current space"
|
|
9
|
+
option :output, aliases: ["-o"], type: :string, default: nil,
|
|
10
|
+
desc: "Output directory (default: build/oci/ under the space root)"
|
|
11
|
+
|
|
12
|
+
def call(output: nil, **opts)
|
|
13
|
+
setup_terminal(**opts.slice(:color, :colors))
|
|
14
|
+
handle_errors do
|
|
15
|
+
result = store.current.bind do |space|
|
|
16
|
+
out_dir = output ? Pathname.new(output).expand_path : space.path.join("build", "oci")
|
|
17
|
+
Space::Core::OciPacker.new(space: space, output_dir: out_dir).generate
|
|
18
|
+
.fmap { |dir| { space: space, dir: dir } }
|
|
19
|
+
end
|
|
20
|
+
render(result) do |r|
|
|
21
|
+
terminal.success "Generated OCI context: #{r[:dir]}"
|
|
22
|
+
terminal.say "Build: docker build -f #{r[:dir]}/Dockerfile -t #{r[:space].id}:latest ."
|
|
23
|
+
terminal.say " (run from: #{r[:space].path})"
|
|
24
|
+
CLI.record_outcome(Outcome.new(exit_code: 0))
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
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 "Add repos to the current space (copy-on-write from an evergreen checkout when available, else clone)"
|
|
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,29 @@
|
|
|
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)"
|
|
9
|
+
|
|
10
|
+
argument :command, type: :array, required: false, desc: "Command to run in the container (default: login shell)"
|
|
11
|
+
option :tty, type: :boolean, default: nil, desc: "Force interactive TTY (default: auto-detect)"
|
|
12
|
+
|
|
13
|
+
def call(command: [], tty: nil, **opts)
|
|
14
|
+
setup_terminal(**opts.slice(:color, :colors))
|
|
15
|
+
handle_errors do
|
|
16
|
+
result = store.current.bind do |space|
|
|
17
|
+
runner = Space::Core::OciRunner.new(space: space, interactive: tty.nil? ? CLI.tty?(out) : tty)
|
|
18
|
+
runner.command(command).fmap { |argv| { argv: argv, runner: runner } }
|
|
19
|
+
end
|
|
20
|
+
render(result) do |r|
|
|
21
|
+
r[:runner].host_dirs.each { |d| FileUtils.mkdir_p(d) }
|
|
22
|
+
terminal.say "Running: #{r[:argv].join(' ')}"
|
|
23
|
+
out.flush # Kernel.exec replaces the process without flushing buffered IO
|
|
24
|
+
Kernel.exec(*r[:argv])
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
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
|