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,177 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "dry/cli"
|
|
4
|
+
require "pastel"
|
|
5
|
+
require_relative "cli/repeatable_options"
|
|
6
|
+
require_relative "cli/help"
|
|
7
|
+
|
|
8
|
+
module Space::Core
|
|
9
|
+
module CLI
|
|
10
|
+
CLI = self
|
|
11
|
+
|
|
12
|
+
Outcome = Data.define(:exit_code, :message) do
|
|
13
|
+
def initialize(exit_code:, message: nil) = super
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.record_outcome(o) = (Thread.current[:space_core_cli_outcome] = o)
|
|
17
|
+
def self.last_outcome = Thread.current[:space_core_cli_outcome]
|
|
18
|
+
|
|
19
|
+
# Pastel used by the colourful help listing (Help / the Usage reopen). Set
|
|
20
|
+
# per-invocation in .call from the output stream and --color; defaults to a
|
|
21
|
+
# disabled instance so non-CLI callers and tests render plain text.
|
|
22
|
+
def self.help_pastel = @help_pastel ||= Pastel.new(enabled: false)
|
|
23
|
+
|
|
24
|
+
def self.help_pastel=(pastel)
|
|
25
|
+
@help_pastel = pastel
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Whether the help listing should be colourised: honour an explicit
|
|
29
|
+
# --color/--colors (always/never), otherwise auto-detect from the streams the
|
|
30
|
+
# listing can land on (stdout for top-level help, stderr for bare namespaces).
|
|
31
|
+
def self.help_colors?(argv, out, err)
|
|
32
|
+
case color_mode(argv)
|
|
33
|
+
when "always" then true
|
|
34
|
+
when "never" then false
|
|
35
|
+
else tty?(out) || tty?(err)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.tty?(io) = io.respond_to?(:tty?) && io.tty?
|
|
40
|
+
|
|
41
|
+
def self.color_mode(argv)
|
|
42
|
+
argv.each_with_index do |arg, i|
|
|
43
|
+
return arg.split("=", 2)[1].to_s.downcase if arg.start_with?("--color=", "--colors=")
|
|
44
|
+
return argv[i + 1].to_s.downcase if %w[--color --colors].include?(arg)
|
|
45
|
+
end
|
|
46
|
+
"auto"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
module Registry
|
|
50
|
+
extend Dry::CLI::Registry
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
TOP_LEVEL_HELP = [[], ["--help"], ["-h"], ["help"]].freeze
|
|
54
|
+
VERSION_REQUEST = [["version"], ["--version"]].freeze
|
|
55
|
+
|
|
56
|
+
def self.call(argv, out = $stdout, err = $stderr)
|
|
57
|
+
Thread.current[:space_core_cli_outcome] = nil
|
|
58
|
+
self.help_pastel = Pastel.new(enabled: help_colors?(argv, out, err))
|
|
59
|
+
|
|
60
|
+
if TOP_LEVEL_HELP.include?(argv)
|
|
61
|
+
print_usage(out)
|
|
62
|
+
return 0
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
if VERSION_REQUEST.include?(argv)
|
|
66
|
+
print_version(out)
|
|
67
|
+
return 0
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
Dry::CLI.new(Registry).call(arguments: normalize_args(argv), out: out, err: err)
|
|
71
|
+
last_outcome&.exit_code || 0
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Move --color/--colors options to the end of the argument list so dry-cli's
|
|
75
|
+
# command routing is not confused by options before the subcommand name.
|
|
76
|
+
#
|
|
77
|
+
# Two passes:
|
|
78
|
+
# 1. Leading: extract two-token form (--color VALUE) and =-form from the
|
|
79
|
+
# front while args still look like options.
|
|
80
|
+
# 2. Non-leading: extract =-form (--color=VALUE / --colors=VALUE) from any
|
|
81
|
+
# position before the -- separator. The bare two-token form is ambiguous
|
|
82
|
+
# with a subcommand name in non-leading position and is left in place.
|
|
83
|
+
def self.normalize_args(argv)
|
|
84
|
+
args = argv.dup
|
|
85
|
+
extracted = []
|
|
86
|
+
|
|
87
|
+
# Pass 1: leading two-token and =-form (existing behavior, unchanged)
|
|
88
|
+
while (arg = args.first) && arg != "--" && arg.start_with?("-")
|
|
89
|
+
if %w[--color --colors].include?(arg)
|
|
90
|
+
extracted << args.shift
|
|
91
|
+
extracted << args.shift if args.first && !args.first.start_with?("-")
|
|
92
|
+
elsif arg.start_with?("--color=", "--colors=")
|
|
93
|
+
extracted << args.shift
|
|
94
|
+
else
|
|
95
|
+
break
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Pass 2: =-form from any non-leading position, stop at --
|
|
100
|
+
sep = args.index("--")
|
|
101
|
+
head = sep ? args[0, sep] : args
|
|
102
|
+
tail = sep ? args[sep..] : []
|
|
103
|
+
mid_color, head = head.partition { |a| a.start_with?("--color=", "--colors=") }
|
|
104
|
+
extracted += mid_color
|
|
105
|
+
args = head + tail
|
|
106
|
+
|
|
107
|
+
extracted.empty? ? args : args + extracted
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def self.print_usage(out)
|
|
111
|
+
out.puts Dry::CLI::Usage.call(Registry.get([]))
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def self.print_version(out)
|
|
115
|
+
out.puts ::Space::Core::VERSION
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def self.run(argv, out = $stdout, err = $stderr)
|
|
119
|
+
Kernel.exit(call(argv, out, err))
|
|
120
|
+
rescue Interrupt
|
|
121
|
+
err.puts "interrupted"
|
|
122
|
+
Kernel.exit(130)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
require_relative "cli/helpers"
|
|
128
|
+
require_relative "cli/base_command"
|
|
129
|
+
require_relative "cli/init"
|
|
130
|
+
require_relative "cli/new"
|
|
131
|
+
require_relative "cli/list"
|
|
132
|
+
require_relative "cli/show"
|
|
133
|
+
require_relative "cli/path"
|
|
134
|
+
require_relative "cli/use"
|
|
135
|
+
require_relative "cli/current"
|
|
136
|
+
require_relative "cli/status"
|
|
137
|
+
require_relative "cli/config"
|
|
138
|
+
require_relative "cli/repo"
|
|
139
|
+
require_relative "cli/shell"
|
|
140
|
+
require_relative "cli/pack"
|
|
141
|
+
require_relative "cli/build"
|
|
142
|
+
require_relative "cli/run"
|
|
143
|
+
|
|
144
|
+
Space::Core::CLI::Registry.register "init", Space::Core::CLI::Init
|
|
145
|
+
Space::Core::CLI::Registry.register "new", Space::Core::CLI::New
|
|
146
|
+
Space::Core::CLI::Registry.register "list", Space::Core::CLI::List
|
|
147
|
+
Space::Core::CLI::Registry.register "ls", Space::Core::CLI::List
|
|
148
|
+
Space::Core::CLI::Registry.register "show", Space::Core::CLI::Show
|
|
149
|
+
Space::Core::CLI::Registry.register "path", Space::Core::CLI::Path
|
|
150
|
+
Space::Core::CLI::Registry.register "use", Space::Core::CLI::Use
|
|
151
|
+
Space::Core::CLI::Registry.register "current", Space::Core::CLI::Current
|
|
152
|
+
Space::Core::CLI::Registry.register "status", Space::Core::CLI::Status
|
|
153
|
+
Space::Core::CLI::Registry.register "config" do |c|
|
|
154
|
+
c.register "show", Space::Core::CLI::Config::Show
|
|
155
|
+
c.register "path", Space::Core::CLI::Config::ConfigPath
|
|
156
|
+
c.register "set", Space::Core::CLI::Config::Set
|
|
157
|
+
end
|
|
158
|
+
Space::Core::CLI::Registry.register "repo" do |r|
|
|
159
|
+
r.register "add", Space::Core::CLI::Repo::Add
|
|
160
|
+
r.register "list", Space::Core::CLI::Repo::RepoList
|
|
161
|
+
r.register "ls", Space::Core::CLI::Repo::RepoList
|
|
162
|
+
r.register "resolve", Space::Core::CLI::Repo::Resolve
|
|
163
|
+
end
|
|
164
|
+
Space::Core::CLI::Registry.register "repos" do |r|
|
|
165
|
+
r.register "add", Space::Core::CLI::Repo::Add
|
|
166
|
+
r.register "list", Space::Core::CLI::Repo::RepoList
|
|
167
|
+
r.register "ls", Space::Core::CLI::Repo::RepoList
|
|
168
|
+
r.register "resolve", Space::Core::CLI::Repo::Resolve
|
|
169
|
+
end
|
|
170
|
+
Space::Core::CLI::Registry.register "shell" do |s|
|
|
171
|
+
s.register "init", Space::Core::CLI::Shell::ShellInit
|
|
172
|
+
s.register "fish", Space::Core::CLI::Shell::Fish
|
|
173
|
+
s.register "complete", Space::Core::CLI::Shell::Complete
|
|
174
|
+
end
|
|
175
|
+
Space::Core::CLI::Registry.register "pack", Space::Core::CLI::Pack
|
|
176
|
+
Space::Core::CLI::Registry.register "build", Space::Core::CLI::Build
|
|
177
|
+
Space::Core::CLI::Registry.register "run", Space::Core::CLI::Run
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
require "pathname"
|
|
5
|
+
require "dry/monads"
|
|
6
|
+
|
|
7
|
+
module Space::Core
|
|
8
|
+
class OciBuilder
|
|
9
|
+
include Dry::Monads[:result]
|
|
10
|
+
|
|
11
|
+
def initialize(space:, output_dir:)
|
|
12
|
+
@space = space
|
|
13
|
+
@output_dir = Pathname.new(output_dir)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def version
|
|
17
|
+
compute_version.value!
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def image
|
|
21
|
+
"#{space.id}:#{version}"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def latest
|
|
25
|
+
"#{space.id}:latest"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def command
|
|
29
|
+
compute_version.fmap do |ver|
|
|
30
|
+
[
|
|
31
|
+
"container", "build",
|
|
32
|
+
"-f", @output_dir.join("Dockerfile").to_s,
|
|
33
|
+
"-t", "#{space.id}:#{ver}",
|
|
34
|
+
"-t", latest,
|
|
35
|
+
space.path.to_s
|
|
36
|
+
]
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
attr_reader :space
|
|
43
|
+
|
|
44
|
+
def compute_version
|
|
45
|
+
sha_out, _, sha_status = Open3.capture3("git", "-C", space.path.to_s, "rev-parse", "--short=12", "HEAD")
|
|
46
|
+
unless sha_status.success?
|
|
47
|
+
return Failure("space is not a git repository with a commit; cannot compute a version tag")
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
sha = sha_out.strip
|
|
51
|
+
status_out, _, _ = Open3.capture3("git", "-C", space.path.to_s, "status", "--porcelain")
|
|
52
|
+
dirty = !status_out.strip.empty?
|
|
53
|
+
Success(dirty ? "#{sha}-dirty" : sha)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "erb"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require "pathname"
|
|
6
|
+
require "dry/monads"
|
|
7
|
+
|
|
8
|
+
module Space::Core
|
|
9
|
+
class OciPacker
|
|
10
|
+
include Dry::Monads[:result]
|
|
11
|
+
|
|
12
|
+
TEMPLATE_DIR = Pathname.new(__dir__).join("templates", "oci").freeze
|
|
13
|
+
|
|
14
|
+
def initialize(space:, output_dir:)
|
|
15
|
+
@space = space
|
|
16
|
+
@output_dir = Pathname.new(output_dir)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def generate
|
|
20
|
+
FileUtils.mkdir_p(@output_dir)
|
|
21
|
+
validated = validate_provision_scripts(space.provision_scripts)
|
|
22
|
+
return validated if validated.failure?
|
|
23
|
+
validated = validate_persist_paths(space.persist_paths)
|
|
24
|
+
return validated if validated.failure?
|
|
25
|
+
write("Dockerfile", render_template("dockerfile.erb"))
|
|
26
|
+
write("entrypoint.sh", entrypoint_content, mode: 0o755)
|
|
27
|
+
write("Dockerfile.dockerignore", render_template("dockerignore.erb"))
|
|
28
|
+
Success(@output_dir)
|
|
29
|
+
rescue StandardError => e
|
|
30
|
+
Failure(e)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
attr_reader :space, :output_dir
|
|
36
|
+
|
|
37
|
+
def space_id
|
|
38
|
+
@space.id
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def repos
|
|
42
|
+
@space.repos
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def provision_scripts
|
|
46
|
+
@space.provision_scripts
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def persist_paths
|
|
50
|
+
@space.persist_paths
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def validate_provision_scripts(scripts)
|
|
54
|
+
scripts.each do |rel_path|
|
|
55
|
+
if Pathname.new(rel_path).absolute?
|
|
56
|
+
return Failure("provision script '#{rel_path}' must not be an absolute path")
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
resolved = space.path.join(rel_path).cleanpath
|
|
60
|
+
space_root = space.path.cleanpath
|
|
61
|
+
unless resolved.to_s.start_with?("#{space_root}/") || resolved == space_root
|
|
62
|
+
return Failure("provision script '#{rel_path}' escapes the space root")
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
return Failure("provision script '#{rel_path}' does not exist under the space root") unless resolved.exist?
|
|
66
|
+
end
|
|
67
|
+
Success(scripts)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def validate_persist_paths(paths)
|
|
71
|
+
paths.each do |path|
|
|
72
|
+
unless Pathname.new(path).absolute?
|
|
73
|
+
return Failure("persist path '#{path}' must be an absolute path")
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
Success(paths)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def entrypoint_content
|
|
80
|
+
@entrypoint_content ||= render_template("entrypoint.sh.erb")
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def entrypoint_b64
|
|
84
|
+
[entrypoint_content].pack("m0")
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def render_template(name)
|
|
88
|
+
template_path = TEMPLATE_DIR.join(name)
|
|
89
|
+
ERB.new(template_path.read, trim_mode: "-").result(binding)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def write(filename, content, mode: 0o644)
|
|
93
|
+
path = @output_dir.join(filename)
|
|
94
|
+
File.write(path, content)
|
|
95
|
+
File.chmod(mode, path)
|
|
96
|
+
path
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
require "dry/monads"
|
|
5
|
+
|
|
6
|
+
module Space::Core
|
|
7
|
+
class OciRunner
|
|
8
|
+
include Dry::Monads[:result]
|
|
9
|
+
|
|
10
|
+
AUTH_ENV = %w[ANTHROPIC_API_KEY CLAUDE_CODE_OAUTH_TOKEN ANTHROPIC_BASE_URL].freeze
|
|
11
|
+
|
|
12
|
+
def initialize(space:, env: ENV.to_h, interactive: true)
|
|
13
|
+
@space = space
|
|
14
|
+
@env = env
|
|
15
|
+
@interactive = interactive
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def image
|
|
19
|
+
"#{space.id}:latest"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def mounts
|
|
23
|
+
space.persist_paths.map do |guest|
|
|
24
|
+
[space.path.join(".state" + guest), guest]
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def host_dirs
|
|
29
|
+
mounts.map(&:first)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def command(extra = [])
|
|
33
|
+
validated = validate_persist_paths(space.persist_paths)
|
|
34
|
+
return validated if validated.failure?
|
|
35
|
+
|
|
36
|
+
argv = [
|
|
37
|
+
"container", "run", "--rm",
|
|
38
|
+
*(@interactive ? ["-i", "-t"] : []),
|
|
39
|
+
*auth_flags,
|
|
40
|
+
*mount_flags,
|
|
41
|
+
image,
|
|
42
|
+
*extra
|
|
43
|
+
]
|
|
44
|
+
Success(argv)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
attr_reader :space, :env, :interactive
|
|
50
|
+
|
|
51
|
+
def auth_flags
|
|
52
|
+
AUTH_ENV.each_with_object([]) do |var, flags|
|
|
53
|
+
val = env[var]
|
|
54
|
+
flags.push("-e", var) if val && !val.empty?
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def mount_flags
|
|
59
|
+
mounts.each_with_object([]) do |(host, guest), flags|
|
|
60
|
+
flags.push("-v", "#{host}:#{guest}")
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def validate_persist_paths(paths)
|
|
65
|
+
paths.each do |path|
|
|
66
|
+
unless Pathname.new(path).absolute?
|
|
67
|
+
return Failure("persist path '#{path}' must be an absolute path")
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
Success(paths)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -4,7 +4,7 @@ require "yaml"
|
|
|
4
4
|
require "pathname"
|
|
5
5
|
require "time"
|
|
6
6
|
|
|
7
|
-
module
|
|
7
|
+
module Space::Core
|
|
8
8
|
class Space
|
|
9
9
|
METADATA_FILE = "space.yaml"
|
|
10
10
|
VALID_STATUSES = %w[active paused done archived].freeze
|
|
@@ -48,12 +48,20 @@ module SpaceArchitect
|
|
|
48
48
|
end
|
|
49
49
|
end
|
|
50
50
|
|
|
51
|
+
def provision_scripts
|
|
52
|
+
Array(data.dig("pack", "provision")).map(&:to_s)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def persist_paths
|
|
56
|
+
Array(data.dig("pack", "persist")).map(&:to_s)
|
|
57
|
+
end
|
|
58
|
+
|
|
51
59
|
def architect
|
|
52
|
-
data["
|
|
60
|
+
data["project"]
|
|
53
61
|
end
|
|
54
62
|
|
|
55
63
|
def architect=(val)
|
|
56
|
-
data["
|
|
64
|
+
data["project"] = val
|
|
57
65
|
end
|
|
58
66
|
|
|
59
67
|
def metadata_path
|
|
@@ -6,10 +6,10 @@ require "async/semaphore"
|
|
|
6
6
|
require "pathname"
|
|
7
7
|
require "time"
|
|
8
8
|
require "dry/monads"
|
|
9
|
-
require "
|
|
10
|
-
require "
|
|
9
|
+
require "space_src/scm/git"
|
|
10
|
+
require "space_src/cloner"
|
|
11
11
|
|
|
12
|
-
module
|
|
12
|
+
module Space::Core
|
|
13
13
|
class SpaceStore
|
|
14
14
|
include Dry::Monads[:result, :maybe]
|
|
15
15
|
|
|
@@ -46,7 +46,7 @@ module SpaceArchitect
|
|
|
46
46
|
init_git(path:, id:, git_client:) if git
|
|
47
47
|
state.touch_recent(id)
|
|
48
48
|
Success(space)
|
|
49
|
-
rescue
|
|
49
|
+
rescue Error => e
|
|
50
50
|
Failure(e)
|
|
51
51
|
end
|
|
52
52
|
|
|
@@ -67,7 +67,7 @@ module SpaceArchitect
|
|
|
67
67
|
if looks_like_path?(value)
|
|
68
68
|
begin
|
|
69
69
|
return Success(Space.load(File.expand_path(value)))
|
|
70
|
-
rescue
|
|
70
|
+
rescue Error => e
|
|
71
71
|
return Failure(e)
|
|
72
72
|
end
|
|
73
73
|
end
|
|
@@ -80,13 +80,13 @@ module SpaceArchitect
|
|
|
80
80
|
end
|
|
81
81
|
|
|
82
82
|
Failure(AmbiguousSpaceError.new("Space '#{value}' is ambiguous: #{matches.map(&:id).join(', ')}"))
|
|
83
|
-
rescue
|
|
83
|
+
rescue Error => e
|
|
84
84
|
Failure(e)
|
|
85
85
|
end
|
|
86
86
|
|
|
87
87
|
def current(from: Dir.pwd)
|
|
88
88
|
current_from_pwd(from:).to_result(CurrentSpaceMissingError.new("No current space found from #{from}. Run this inside a space or pass a space id."))
|
|
89
|
-
rescue
|
|
89
|
+
rescue Error => e
|
|
90
90
|
Failure(e)
|
|
91
91
|
end
|
|
92
92
|
|
|
@@ -112,15 +112,15 @@ module SpaceArchitect
|
|
|
112
112
|
find(identifier).fmap { |space| state.touch_recent(space.id); space }
|
|
113
113
|
end
|
|
114
114
|
|
|
115
|
-
def add_repo(spec, from: Dir.pwd, scm:
|
|
115
|
+
def add_repo(spec, from: Dir.pwd, scm: ::Space::Src::SCM::Git.new, cloner: nil, mise_client: MiseClient.new)
|
|
116
116
|
add_repos([spec], from:, scm:, cloner:, mise_client:).fmap(&:first)
|
|
117
117
|
end
|
|
118
118
|
|
|
119
|
-
def add_repos(specs, from: Dir.pwd, scm:
|
|
119
|
+
def add_repos(specs, from: Dir.pwd, scm: ::Space::Src::SCM::Git.new, cloner: nil, mise_client: MiseClient.new, reporter: nil)
|
|
120
120
|
current(from:).bind { |space| add_repos_to(space, specs, scm:, cloner:, mise_client:, reporter:) }
|
|
121
121
|
end
|
|
122
122
|
|
|
123
|
-
def add_repos_to(space, specs, scm:
|
|
123
|
+
def add_repos_to(space, specs, scm: ::Space::Src::SCM::Git.new, cloner: nil, mise_client: MiseClient.new, reporter: nil)
|
|
124
124
|
additions = prepare_repo_additions(space, specs)
|
|
125
125
|
first_error = nil
|
|
126
126
|
|
|
@@ -148,7 +148,7 @@ module SpaceArchitect
|
|
|
148
148
|
repo_data = space.add_repo(addition.fetch(:reference), relative_path: addition.fetch(:relative_path), now: now.call)
|
|
149
149
|
{ space: space, repo: repo_data, reference: addition.fetch(:reference), path: addition.fetch(:path) }
|
|
150
150
|
end)
|
|
151
|
-
rescue
|
|
151
|
+
rescue Error => e
|
|
152
152
|
Failure(e)
|
|
153
153
|
end
|
|
154
154
|
|
|
@@ -178,7 +178,7 @@ module SpaceArchitect
|
|
|
178
178
|
source = addition.fetch(:src_source)
|
|
179
179
|
|
|
180
180
|
if source&.directory?
|
|
181
|
-
actual_cloner = cloner ||
|
|
181
|
+
actual_cloner = cloner || ::Space::Src::Cloner.new(base_dir: config.src_dir)
|
|
182
182
|
result = actual_cloner.call(name: reference.full_name, into: destination.dirname.to_s)
|
|
183
183
|
raise GitError, "clone failed (copy): #{result.failure}" if result.failure?
|
|
184
184
|
else
|
|
@@ -257,7 +257,7 @@ module SpaceArchitect
|
|
|
257
257
|
- `space.yaml` tracks the space identity, status, and associated metadata.
|
|
258
258
|
- `repos/` contains cloned Git repositories for this work.
|
|
259
259
|
- `notes/` is for task notes, scratch docs, and thinking-in-progress.
|
|
260
|
-
- `architecture/` holds the architect
|
|
260
|
+
- `architecture/` holds the architect project memory (ARCHITECT.md and the per-iteration files).
|
|
261
261
|
- `tmp/` is the workspace-local scratch directory. Use it instead of `/tmp` or
|
|
262
262
|
`/var/tmp`; when using `mktemp`, use `tmp/` as the base directory.
|
|
263
263
|
- `build/` holds the architect loop's per-lane worktrees and scratch
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Generated by `space pack` for space: <%= space_id %>
|
|
2
|
+
# Build (run from the space root):
|
|
3
|
+
# docker build -f <output_dir>/Dockerfile -t <%= space_id %>:latest .
|
|
4
|
+
#
|
|
5
|
+
# Run (auth injected at runtime — never baked into layers):
|
|
6
|
+
# container run --rm -it \
|
|
7
|
+
# -e ANTHROPIC_API_KEY=<key> \
|
|
8
|
+
<% persist_paths.each do |guest_path| -%>
|
|
9
|
+
# -v .state<%= guest_path %>:<%= guest_path %> \
|
|
10
|
+
<% end -%>
|
|
11
|
+
# <%= space_id %>:latest
|
|
12
|
+
<% if persist_paths.any? -%>
|
|
13
|
+
# (Persist: host paths are relative to space root; create them before first run.)
|
|
14
|
+
<% end -%>
|
|
15
|
+
#
|
|
16
|
+
# Available env vars at runtime:
|
|
17
|
+
# ANTHROPIC_API_KEY — API key
|
|
18
|
+
# CLAUDE_CODE_OAUTH_TOKEN — alternative to API key
|
|
19
|
+
# ANTHROPIC_BASE_URL — optional proxy endpoint
|
|
20
|
+
|
|
21
|
+
FROM ruby:4.0.5
|
|
22
|
+
|
|
23
|
+
RUN apt-get update -qq \
|
|
24
|
+
&& apt-get install -y --no-install-recommends \
|
|
25
|
+
git \
|
|
26
|
+
curl \
|
|
27
|
+
ca-certificates \
|
|
28
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
29
|
+
|
|
30
|
+
# Claude Code CLI (https://claude.ai/install.sh)
|
|
31
|
+
RUN curl -fsSL https://claude.ai/install.sh | bash
|
|
32
|
+
|
|
33
|
+
# Copy the space tree into the image (filtered by Dockerfile.dockerignore alongside this Dockerfile)
|
|
34
|
+
COPY . /space
|
|
35
|
+
|
|
36
|
+
# Install space-architect gem: prefer in-space checkout for a pinned version, fall back to RubyGems
|
|
37
|
+
RUN if [ -d /space/repos/space-architect ]; then \
|
|
38
|
+
cd /space/repos/space-architect \
|
|
39
|
+
&& rake build \
|
|
40
|
+
&& gem install --no-document pkg/*.gem; \
|
|
41
|
+
else \
|
|
42
|
+
gem install --no-document space-architect; \
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
# Write entrypoint: sets git safe.directory, execs requested command or a login shell
|
|
46
|
+
RUN echo '<%= entrypoint_b64 %>' | base64 -d > /entrypoint.sh \
|
|
47
|
+
&& chmod +x /entrypoint.sh
|
|
48
|
+
|
|
49
|
+
# Prepend gem-exec and payload bin dirs so login shells (bash --login) resolve architect/space/claude
|
|
50
|
+
RUN printf 'export PATH="/usr/local/bundle/bin:/root/.local/bin:$PATH"\n' \
|
|
51
|
+
> /etc/profile.d/space-architect.sh
|
|
52
|
+
<% if provision_scripts.any? -%>
|
|
53
|
+
|
|
54
|
+
# Space-declared build-time provisioning (pack.provision)
|
|
55
|
+
<% provision_scripts.each do |script| -%>
|
|
56
|
+
RUN /space/<%= script %>
|
|
57
|
+
<% end -%>
|
|
58
|
+
<% end -%>
|
|
59
|
+
|
|
60
|
+
WORKDIR /space
|
|
61
|
+
|
|
62
|
+
ENTRYPOINT ["/entrypoint.sh"]
|
|
63
|
+
CMD ["bash", "--login"]
|