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,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "async/http/client"
|
|
4
|
+
require "async/http/endpoint"
|
|
5
|
+
require "json"
|
|
6
|
+
|
|
7
|
+
module Space::Architect
|
|
8
|
+
class RunCreator
|
|
9
|
+
def initialize(host, token, client: nil)
|
|
10
|
+
@host = host.chomp("/")
|
|
11
|
+
@token = token
|
|
12
|
+
@client = client
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# POSTs to /runs and returns the integer run id.
|
|
16
|
+
# Raises Space::Core::Error on any failure — never returns nil.
|
|
17
|
+
def create
|
|
18
|
+
Sync do
|
|
19
|
+
if @client
|
|
20
|
+
response = @client.post("/runs", headers: headers, body: nil)
|
|
21
|
+
parse_response(response)
|
|
22
|
+
else
|
|
23
|
+
Async::HTTP::Client.open(Async::HTTP::Endpoint.parse(@host)) do |c|
|
|
24
|
+
response = c.post("/runs", headers: headers, body: nil)
|
|
25
|
+
parse_response(response)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def headers
|
|
34
|
+
[
|
|
35
|
+
["authorization", "Bearer #{@token}"],
|
|
36
|
+
["content-type", "application/json"]
|
|
37
|
+
]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def parse_response(response)
|
|
41
|
+
status = response.status
|
|
42
|
+
body = response.read || ""
|
|
43
|
+
raise Space::Core::Error, "POST /runs failed (#{status}): #{body[0, 200]}" unless status == 201
|
|
44
|
+
|
|
45
|
+
parsed = JSON.parse(body)
|
|
46
|
+
id = parsed["id"]
|
|
47
|
+
raise Space::Core::Error, "POST /runs: missing or non-integer id in response: #{body[0, 200]}" \
|
|
48
|
+
unless id.is_a?(Integer)
|
|
49
|
+
|
|
50
|
+
id
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -3,103 +3,105 @@
|
|
|
3
3
|
require "fileutils"
|
|
4
4
|
require "pathname"
|
|
5
5
|
|
|
6
|
-
module
|
|
7
|
-
module
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def dest_root(provider, project:, env:, cwd: Dir.pwd)
|
|
16
|
-
case provider.to_s
|
|
17
|
-
when "claude"
|
|
18
|
-
base = project ? Pathname.new(cwd) : Pathname.new(XDG.home(env: env))
|
|
19
|
-
base.join(".claude", "skills")
|
|
20
|
-
when "codex"
|
|
21
|
-
base = project ? Pathname.new(cwd) : Pathname.new(XDG.home(env: env))
|
|
22
|
-
base.join(".agents", "skills")
|
|
23
|
-
when "opencode"
|
|
24
|
-
project ? Pathname.new(cwd).join(".opencode", "skills") : XDG.config_home(env: env).join("skills")
|
|
25
|
-
when "pi"
|
|
26
|
-
base = project ? Pathname.new(cwd) : Pathname.new(pi_agent_dir(env: env))
|
|
27
|
-
base.join("skills")
|
|
28
|
-
else
|
|
29
|
-
raise Error, "Unknown provider '#{provider}'. Expected one of: #{PROVIDERS.join(', ')}"
|
|
6
|
+
module Space
|
|
7
|
+
module Architect
|
|
8
|
+
module SkillInstaller
|
|
9
|
+
PROVIDERS = %w[claude codex opencode pi].freeze
|
|
10
|
+
|
|
11
|
+
class << self
|
|
12
|
+
def source_root
|
|
13
|
+
Pathname.new(__dir__).parent.parent.join("skill")
|
|
30
14
|
end
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def install(provider, project:, force:, env:, cwd: Dir.pwd, dry_run: false)
|
|
34
|
-
validate_provider!(provider)
|
|
35
|
-
dest = dest_root(provider, project: project, env: env, cwd: cwd)
|
|
36
|
-
results = []
|
|
37
15
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
16
|
+
def dest_root(provider, project:, env:, cwd: Dir.pwd)
|
|
17
|
+
case provider.to_s
|
|
18
|
+
when "claude"
|
|
19
|
+
base = project ? Pathname.new(cwd) : Pathname.new(Space::Core::XDG.home(env: env))
|
|
20
|
+
base.join(".claude", "skills")
|
|
21
|
+
when "codex"
|
|
22
|
+
base = project ? Pathname.new(cwd) : Pathname.new(Space::Core::XDG.home(env: env))
|
|
23
|
+
base.join(".agents", "skills")
|
|
24
|
+
when "opencode"
|
|
25
|
+
project ? Pathname.new(cwd).join(".opencode", "skills") : Space::Core::XDG.config_home(env: env).join("skills")
|
|
26
|
+
when "pi"
|
|
27
|
+
base = project ? Pathname.new(cwd) : Pathname.new(pi_agent_dir(env: env))
|
|
28
|
+
base.join("skills")
|
|
29
|
+
else
|
|
30
|
+
raise Space::Core::Error, "Unknown provider '#{provider}'. Expected one of: #{PROVIDERS.join(', ')}"
|
|
31
|
+
end
|
|
42
32
|
end
|
|
43
33
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
source_root.children.select(&:directory?)
|
|
49
|
-
end
|
|
34
|
+
def install(provider, project:, force:, env:, cwd: Dir.pwd, dry_run: false)
|
|
35
|
+
validate_provider!(provider)
|
|
36
|
+
dest = dest_root(provider, project: project, env: env, cwd: cwd)
|
|
37
|
+
results = []
|
|
50
38
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
39
|
+
source_skills.each do |skill_dir|
|
|
40
|
+
name = skill_dir.basename.to_s
|
|
41
|
+
skill_dest = dest.join(name)
|
|
42
|
+
results << install_skill(skill_dir, skill_dest, force: force, dry_run: dry_run)
|
|
43
|
+
end
|
|
55
44
|
|
|
56
|
-
|
|
57
|
-
|
|
45
|
+
{ dest_root: dest, skills: results, dry_run: dry_run }
|
|
46
|
+
end
|
|
58
47
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
48
|
+
def source_skills
|
|
49
|
+
source_root.children.select(&:directory?)
|
|
50
|
+
end
|
|
62
51
|
|
|
63
|
-
|
|
64
|
-
name = source.basename.to_s
|
|
52
|
+
private
|
|
65
53
|
|
|
66
|
-
|
|
67
|
-
if
|
|
68
|
-
return { name: name, action: :unchanged, path: dest }
|
|
69
|
-
end
|
|
54
|
+
def validate_provider!(provider)
|
|
55
|
+
return if PROVIDERS.include?(provider.to_s)
|
|
70
56
|
|
|
71
|
-
|
|
72
|
-
|
|
57
|
+
raise Space::Core::Error, "Unknown provider '#{provider}'. Expected one of: #{PROVIDERS.join(', ')}"
|
|
58
|
+
end
|
|
73
59
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
60
|
+
def pi_agent_dir(env:)
|
|
61
|
+
Pathname.new(env.fetch("PI_CODING_AGENT_DIR", File.join(Space::Core::XDG.home(env: env), ".pi", "agent")))
|
|
62
|
+
end
|
|
77
63
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
64
|
+
def install_skill(source, dest, force:, dry_run:)
|
|
65
|
+
name = source.basename.to_s
|
|
66
|
+
|
|
67
|
+
if dest.exist?
|
|
68
|
+
if same_content?(source, dest)
|
|
69
|
+
return { name: name, action: :unchanged, path: dest }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
unless force
|
|
73
|
+
return { name: name, action: :conflict, path: dest } if dry_run
|
|
74
|
+
|
|
75
|
+
raise Space::Core::Error,
|
|
76
|
+
"Refusing to overwrite existing skill at #{dest}. Re-run with --force."
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
unless dry_run
|
|
80
|
+
FileUtils.rm_rf(dest)
|
|
81
|
+
FileUtils.cp_r(source, dest)
|
|
82
|
+
end
|
|
83
|
+
{ name: name, action: dry_run ? :would_update : :updated, path: dest }
|
|
84
|
+
else
|
|
85
|
+
unless dry_run
|
|
86
|
+
FileUtils.mkdir_p(dest.parent)
|
|
87
|
+
FileUtils.cp_r(source, dest)
|
|
88
|
+
end
|
|
89
|
+
{ name: name, action: dry_run ? :would_install : :installed, path: dest }
|
|
87
90
|
end
|
|
88
|
-
{ name: name, action: dry_run ? :would_install : :installed, path: dest }
|
|
89
91
|
end
|
|
90
|
-
end
|
|
91
92
|
|
|
92
|
-
|
|
93
|
-
|
|
93
|
+
def same_content?(source, dest)
|
|
94
|
+
return false unless dest.directory?
|
|
94
95
|
|
|
95
|
-
|
|
96
|
-
|
|
96
|
+
source_files = Dir.glob("#{source}/**/*").reject { |f| File.directory?(f) }
|
|
97
|
+
dest_files = Dir.glob("#{dest}/**/*").reject { |f| File.directory?(f) }
|
|
97
98
|
|
|
98
|
-
|
|
99
|
+
return false if source_files.length != dest_files.length
|
|
99
100
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
101
|
+
source_files.sort.zip(dest_files.sort).all? do |sf, df|
|
|
102
|
+
rel = sf.sub("#{source}/", "")
|
|
103
|
+
df.end_with?(rel) && File.read(sf) == File.read(df)
|
|
104
|
+
end
|
|
103
105
|
end
|
|
104
106
|
end
|
|
105
107
|
end
|
data/lib/space_architect.rb
CHANGED
|
@@ -1,27 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
require "
|
|
3
|
+
require "space_core"
|
|
4
|
+
require "space_src"
|
|
5
5
|
|
|
6
|
-
require_relative "space_architect/version"
|
|
7
|
-
require_relative "space_architect/errors"
|
|
8
|
-
require_relative "space_architect/warnings"
|
|
9
|
-
SpaceArchitect::Warnings.disable_experimental!
|
|
10
|
-
require_relative "space_architect/atomic_write"
|
|
11
|
-
require_relative "space_architect/xdg"
|
|
12
|
-
require_relative "space_architect/config"
|
|
13
|
-
require_relative "space_architect/state"
|
|
14
|
-
require_relative "space_architect/slugger"
|
|
15
|
-
require_relative "space_architect/space"
|
|
16
|
-
require_relative "space_architect/repo_reference"
|
|
17
|
-
require_relative "space_architect/repo_resolver"
|
|
18
|
-
require_relative "space_architect/git_client"
|
|
19
|
-
require_relative "space_architect/mise_client"
|
|
20
|
-
require_relative "space_architect/space_store"
|
|
21
|
-
require_relative "space_architect/shell_integration"
|
|
22
|
-
require_relative "space_architect/skill_installer"
|
|
23
|
-
require_relative "space_architect/terminal"
|
|
24
6
|
require_relative "space_architect/harness"
|
|
7
|
+
require_relative "space_architect/run_creator"
|
|
25
8
|
require_relative "space_architect/dispatcher"
|
|
26
9
|
require_relative "space_architect/architect_mission"
|
|
10
|
+
require_relative "space_architect/skill_installer"
|
|
11
|
+
require_relative "space_architect/research"
|
|
27
12
|
require_relative "space_architect/cli"
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "dry/cli"
|
|
4
|
+
|
|
5
|
+
module Space::Core::CLI
|
|
6
|
+
# Base for every space/architect command. dry-cli (>= 0.7.0) copies a
|
|
7
|
+
# superclass's options to its subclasses, so the global colour options are
|
|
8
|
+
# declared once here and inherited everywhere instead of being mixed in per
|
|
9
|
+
# command. Helpers (terminal/store/render) ride along by inheritance too.
|
|
10
|
+
#
|
|
11
|
+
# The `src` binary has its own output-mode system (--plain/--json) and does
|
|
12
|
+
# NOT inherit from this base.
|
|
13
|
+
class BaseCommand < Dry::CLI::Command
|
|
14
|
+
include Helpers
|
|
15
|
+
|
|
16
|
+
option :color, type: :string, default: "auto", desc: "Color output: auto, always, never"
|
|
17
|
+
option :colors, type: :string, desc: "Alias for --color"
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Space::Core::CLI
|
|
4
|
+
module Config
|
|
5
|
+
class Show < BaseCommand
|
|
6
|
+
desc "Show current config"
|
|
7
|
+
|
|
8
|
+
def call(**opts)
|
|
9
|
+
setup_terminal(**opts.slice(:color, :colors))
|
|
10
|
+
handle_errors do
|
|
11
|
+
rows = Space::Core::Config::EDITABLE_KEYS.map do |key|
|
|
12
|
+
value = project_config.data[key]
|
|
13
|
+
[key, value.nil? ? "" : value.to_s]
|
|
14
|
+
end
|
|
15
|
+
terminal.say terminal.table(%w[Key Value], rows)
|
|
16
|
+
CLI.record_outcome(Outcome.new(exit_code: 0))
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
class ConfigPath < BaseCommand
|
|
22
|
+
desc "Print the config file path"
|
|
23
|
+
|
|
24
|
+
def call(**opts)
|
|
25
|
+
setup_terminal(**opts.slice(:color, :colors))
|
|
26
|
+
handle_errors do
|
|
27
|
+
terminal.say terminal.path(project_config.path)
|
|
28
|
+
CLI.record_outcome(Outcome.new(exit_code: 0))
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
class Set < BaseCommand
|
|
34
|
+
desc "Set a config key"
|
|
35
|
+
argument :key, required: true, desc: "Config key"
|
|
36
|
+
argument :value, required: true, desc: "Config value"
|
|
37
|
+
|
|
38
|
+
def call(key:, value:, **opts)
|
|
39
|
+
setup_terminal(**opts.slice(:color, :colors))
|
|
40
|
+
handle_errors do
|
|
41
|
+
project_config.set(key, value)
|
|
42
|
+
stored = project_config.data[key]
|
|
43
|
+
terminal.success "Set #{key}=#{stored.nil? ? '' : stored.to_s}"
|
|
44
|
+
CLI.record_outcome(Outcome.new(exit_code: 0))
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Space::Core::CLI
|
|
4
|
+
class Current < BaseCommand
|
|
5
|
+
desc "Show the current space"
|
|
6
|
+
|
|
7
|
+
def call(**opts)
|
|
8
|
+
setup_terminal(**opts.slice(:color, :colors))
|
|
9
|
+
render(store.find) do |space|
|
|
10
|
+
terminal.say space.id.to_s
|
|
11
|
+
terminal.say terminal.path(space.path)
|
|
12
|
+
CLI.record_outcome(Outcome.new(exit_code: 0))
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "dry/cli"
|
|
4
|
+
require "pastel"
|
|
5
|
+
|
|
6
|
+
module Space::Core::CLI
|
|
7
|
+
# Colourful replacement for dry-cli's plain `Usage` listing — the "global
|
|
8
|
+
# help" shown by `space`, `architect`, and every bare namespace (`space repo`,
|
|
9
|
+
# `worktree`, ...). Per-command help still flows through dry-cli's Banner,
|
|
10
|
+
# whose content we like; only the listing is reskinned.
|
|
11
|
+
#
|
|
12
|
+
# We reopen Dry::CLI::Usage.call (below) to delegate here, so BOTH the
|
|
13
|
+
# intercepted top-level help and dry-cli's own bare-namespace path get the
|
|
14
|
+
# same treatment from one place. Colour follows CLI.help_pastel, set once per
|
|
15
|
+
# invocation from the output stream's tty-ness and --color, so piped and test
|
|
16
|
+
# output stay plain. The `src` binary never loads space_core, so its own plain
|
|
17
|
+
# Usage is untouched.
|
|
18
|
+
module Help
|
|
19
|
+
TAGLINE = "date-prefixed workspaces; repos provisioned on fibers at copy-on-write speed"
|
|
20
|
+
|
|
21
|
+
module_function
|
|
22
|
+
|
|
23
|
+
def call(result, pastel: CLI.help_pastel)
|
|
24
|
+
rows = listing(result)
|
|
25
|
+
width = rows.map { |label, _| label.length }.max || 0
|
|
26
|
+
|
|
27
|
+
lines = rows.map do |label, description|
|
|
28
|
+
painted = pastel.cyan(label.ljust(width))
|
|
29
|
+
description ? " #{painted} #{pastel.dim("# #{description}")}" : " #{painted}"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
[header(result, pastel), pastel.bold("Commands:"), *lines, footer(result, pastel)]
|
|
33
|
+
.compact.join("\n")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# The richer header only makes sense at the true root (`space` / `architect`),
|
|
37
|
+
# not on every sub-namespace listing.
|
|
38
|
+
def header(result, pastel)
|
|
39
|
+
return unless result.names.empty?
|
|
40
|
+
|
|
41
|
+
"#{pastel.bold.cyan("space-architect")} #{pastel.dim(Space::Core::VERSION)} " \
|
|
42
|
+
"#{pastel.dim("— #{TAGLINE}")}\n"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def footer(result, pastel)
|
|
46
|
+
"\n#{pastel.dim("Run `#{program_prefix(result)} <command> --help` for details on a command.")}"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# "space" at the root, "space repo" inside a namespace. The `space`/`architect`
|
|
50
|
+
# binaries inject their name into ARGV, so $PROGRAM_NAME and the leading
|
|
51
|
+
# namespace segment can collide ("space space ..."); drop the duplicate.
|
|
52
|
+
def program_prefix(result)
|
|
53
|
+
prog = File.basename($PROGRAM_NAME)
|
|
54
|
+
names = result.names.dup
|
|
55
|
+
names.shift if names.first == prog
|
|
56
|
+
[prog, *names].join(" ")
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# [[label_with_banner, description_or_nil], ...] sorted by command name.
|
|
60
|
+
def listing(result)
|
|
61
|
+
result.children.sort_by { |name, _| name }.filter_map do |name, node|
|
|
62
|
+
next if node.hidden
|
|
63
|
+
|
|
64
|
+
[label(result, name, node), description(node)]
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def label(result, name, node)
|
|
69
|
+
"#{program_prefix(result)} #{name}#{banner(node)}"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def banner(node)
|
|
73
|
+
if node.command && node.leaf? && node.children?
|
|
74
|
+
" [ARGUMENT|SUBCOMMAND]"
|
|
75
|
+
elsif node.leaf?
|
|
76
|
+
arguments(node.command)
|
|
77
|
+
else
|
|
78
|
+
" [SUBCOMMAND]"
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def arguments(command)
|
|
83
|
+
return "" unless command.respond_to?(:required_arguments)
|
|
84
|
+
|
|
85
|
+
names = command.required_arguments.map { |arg| arg.name.to_s.upcase }
|
|
86
|
+
names += command.optional_arguments.map { |arg| "[#{arg.name.to_s.upcase}]" }
|
|
87
|
+
names.empty? ? "" : " #{names.join(" ")}"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def description(node)
|
|
91
|
+
return unless node.leaf? && node.command.respond_to?(:description)
|
|
92
|
+
|
|
93
|
+
node.command.description
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Route dry-cli's plain namespace/root listing through our colourful renderer.
|
|
99
|
+
# We replace Usage.call wholesale and depend only on the LookupResult/Node API
|
|
100
|
+
# (children, command, leaf?/children?/hidden, names) rather than copying Usage's
|
|
101
|
+
# internals — see notes/ruby-cli-gems-report.md.
|
|
102
|
+
module Dry
|
|
103
|
+
class CLI
|
|
104
|
+
module Usage
|
|
105
|
+
def self.call(result)
|
|
106
|
+
Space::Core::CLI::Help.call(result)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -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
|