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
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "dry/monads"
|
|
4
|
-
require "
|
|
5
|
-
require "
|
|
6
|
-
require "
|
|
7
|
-
require "
|
|
8
|
-
require "
|
|
9
|
-
require "
|
|
10
|
-
require "
|
|
11
|
-
|
|
12
|
-
module
|
|
4
|
+
require "space_src/cli"
|
|
5
|
+
require "space_src/cli/repo" # for Repo::Helpers.parse_ref
|
|
6
|
+
require "space_src/cli/options"
|
|
7
|
+
require "space_src/ui/mode"
|
|
8
|
+
require "space_src/ui/plain_reporter"
|
|
9
|
+
require "space_src/ui/json_reporter"
|
|
10
|
+
require "space_src/ui/interactive_reporter"
|
|
11
|
+
|
|
12
|
+
module Space::Src
|
|
13
13
|
module CLI
|
|
14
14
|
# `sync` command: invoke Sync::Engine over the full config, or
|
|
15
15
|
# scope to a single repo with --repo.
|
|
@@ -74,7 +74,7 @@ module SpaceArchitect::Pristine
|
|
|
74
74
|
UI::PlainReporter.new(out, mode: mode)
|
|
75
75
|
end
|
|
76
76
|
|
|
77
|
-
result =
|
|
77
|
+
result = Space::Src::Sync::Engine.new(reporter: reporter).call(config: config, paths: paths)
|
|
78
78
|
if result.failure?
|
|
79
79
|
return fail_with(self, "sync failed: #{format_failure(result.failure)}")
|
|
80
80
|
end
|
|
@@ -95,11 +95,11 @@ module SpaceArchitect::Pristine
|
|
|
95
95
|
|
|
96
96
|
def fail_with(cmd, msg)
|
|
97
97
|
cmd.send(:err).puts msg
|
|
98
|
-
|
|
98
|
+
Space::Src::CLI.record_outcome(Outcome.new(exit_code: 1, message: msg))
|
|
99
99
|
end
|
|
100
100
|
|
|
101
101
|
# Default log-rotation threshold: 10 MiB. Tunable via the
|
|
102
|
-
# env var `
|
|
102
|
+
# env var `SPACE_SRC_LOG_MAX_BYTES` (introspection /
|
|
103
103
|
# ops escape hatch). The LogRotator itself is unit-tested
|
|
104
104
|
# with an injected threshold (gate G5).
|
|
105
105
|
DEFAULT_LOG_MAX_BYTES = 10 * 1024 * 1024
|
|
@@ -109,12 +109,12 @@ module SpaceArchitect::Pristine
|
|
|
109
109
|
label = Launchd::Agent::DEFAULT_LABEL
|
|
110
110
|
[File.join(paths.log_dir, "#{label}.out.log"),
|
|
111
111
|
File.join(paths.log_dir, "#{label}.err.log")].each do |p|
|
|
112
|
-
|
|
112
|
+
Space::Src::LogRotator.call(p, threshold_bytes: threshold)
|
|
113
113
|
end
|
|
114
114
|
end
|
|
115
115
|
|
|
116
116
|
# CF6 (Slice 5): defensively parse the
|
|
117
|
-
# `
|
|
117
|
+
# `SPACE_SRC_LOG_MAX_BYTES` env var so a malformed
|
|
118
118
|
# operator value (e.g. `"10MB"`) falls back to the
|
|
119
119
|
# 10 MiB default instead of raising `ArgumentError`
|
|
120
120
|
# and crashing the entire `sync` run before any repo
|
|
@@ -131,13 +131,13 @@ module SpaceArchitect::Pristine
|
|
|
131
131
|
# tests can pass arbitrary values without mutating
|
|
132
132
|
# the real `ENV`; production callers invoke with
|
|
133
133
|
# no args and the method reads `ENV` itself.
|
|
134
|
-
def log_max_bytes(env_value = ENV["
|
|
134
|
+
def log_max_bytes(env_value = ENV["SPACE_SRC_LOG_MAX_BYTES"])
|
|
135
135
|
return DEFAULT_LOG_MAX_BYTES if env_value.nil? || env_value.strip.empty?
|
|
136
136
|
|
|
137
137
|
parsed = Integer(env_value, 10, exception: false)
|
|
138
138
|
return parsed if parsed.is_a?(Integer) && parsed.positive?
|
|
139
139
|
|
|
140
|
-
warn "
|
|
140
|
+
warn "src: SPACE_SRC_LOG_MAX_BYTES=#{env_value.inspect} is invalid; " \
|
|
141
141
|
"falling back to #{DEFAULT_LOG_MAX_BYTES} bytes"
|
|
142
142
|
DEFAULT_LOG_MAX_BYTES
|
|
143
143
|
end
|
|
@@ -146,4 +146,4 @@ module SpaceArchitect::Pristine
|
|
|
146
146
|
end
|
|
147
147
|
end
|
|
148
148
|
|
|
149
|
-
|
|
149
|
+
Space::Src::CLI::Registry.register "sync", Space::Src::CLI::Sync::Run
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "dry/cli"
|
|
4
|
+
require "space_src"
|
|
5
|
+
require "space_src/migration"
|
|
4
6
|
|
|
5
|
-
module
|
|
7
|
+
module Space::Src
|
|
6
8
|
# CLI surface — thin translation layer between argv and the
|
|
7
9
|
# existing Config::Store / State::Store / Sync::Engine boundaries.
|
|
8
10
|
#
|
|
@@ -16,7 +18,7 @@ module SpaceArchitect::Pristine
|
|
|
16
18
|
#
|
|
17
19
|
# Exit-code seam: each command records an `Outcome(exit_code:,
|
|
18
20
|
# message:)` (the thread-local stash) and writes the user-facing
|
|
19
|
-
# message to `out`/`err` via the injected IOs. The `
|
|
21
|
+
# message to `out`/`err` via the injected IOs. The `exe/src`
|
|
20
22
|
# entrypoint reads the recorded Outcome and calls Kernel.exit with
|
|
21
23
|
# the code — see CLI.run below. Tests can inspect last_outcome
|
|
22
24
|
# in-process (no subprocess needed for unit tests); a subprocess
|
|
@@ -33,21 +35,21 @@ module SpaceArchitect::Pristine
|
|
|
33
35
|
end
|
|
34
36
|
|
|
35
37
|
# Thread-local env hash. Defaults to ENV. Tests inject a temp
|
|
36
|
-
# HOME / XDG_* hash via Thread.current[:
|
|
38
|
+
# HOME / XDG_* hash via Thread.current[:space_src_cli_env] =
|
|
37
39
|
# env_hash. The CLI's `make_paths` reads this to resolve the
|
|
38
40
|
# config/state file locations under the test's temp home.
|
|
39
41
|
def self.env
|
|
40
|
-
Thread.current[:
|
|
42
|
+
Thread.current[:space_src_cli_env] || ENV
|
|
41
43
|
end
|
|
42
44
|
|
|
43
45
|
# Thread-local Outcome stash. The most recent command's Outcome
|
|
44
46
|
# is read by CLI.run to set the process exit code.
|
|
45
47
|
def self.record_outcome(outcome)
|
|
46
|
-
Thread.current[:
|
|
48
|
+
Thread.current[:space_src_cli_outcome] = outcome
|
|
47
49
|
end
|
|
48
50
|
|
|
49
51
|
def self.last_outcome
|
|
50
|
-
Thread.current[:
|
|
52
|
+
Thread.current[:space_src_cli_outcome]
|
|
51
53
|
end
|
|
52
54
|
|
|
53
55
|
# Program-name-level invocations that must succeed (exit 0) with
|
|
@@ -61,7 +63,17 @@ module SpaceArchitect::Pristine
|
|
|
61
63
|
TOP_LEVEL_HELP = [[], ["--help"], ["-h"], ["help"]].freeze
|
|
62
64
|
VERSION_REQUEST = [["version"], ["--version"]].freeze
|
|
63
65
|
|
|
64
|
-
#
|
|
66
|
+
# True when argv is a single token that is not a registered top-level
|
|
67
|
+
# command or group, and not already handled by the help/version intercepts.
|
|
68
|
+
# Both CLI.run and dispatch_src delegate to this shared predicate so the
|
|
69
|
+
# routing logic is defined exactly once.
|
|
70
|
+
def self.bare_query?(argv)
|
|
71
|
+
return false if TOP_LEVEL_HELP.include?(argv)
|
|
72
|
+
return false if VERSION_REQUEST.include?(argv)
|
|
73
|
+
argv.length == 1 && !Registry.get([]).children.key?(argv[0])
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Entrypoint. Called by exe/src. Intercepts the top-level
|
|
65
77
|
# help/version forms (stdout, exit 0), otherwise hands argv to
|
|
66
78
|
# Dry::CLI for command dispatch and translates the last Outcome to
|
|
67
79
|
# a process exit code. A `Interrupt` raised from inside command
|
|
@@ -71,11 +83,20 @@ module SpaceArchitect::Pristine
|
|
|
71
83
|
# with a single human line on stderr — the G2 ^C-hygiene fix
|
|
72
84
|
# (Slice 6). The reader-thread `IOError` noise that Open3 emits
|
|
73
85
|
# in the same scenario is suppressed at the `Shell.run` seam
|
|
74
|
-
# (see `lib/
|
|
86
|
+
# (see `lib/space_src/shell.rb`).
|
|
75
87
|
def self.run(argv, stdout, stderr)
|
|
76
88
|
return print_usage(stdout) if TOP_LEVEL_HELP.include?(argv)
|
|
77
89
|
return print_version(stdout) if VERSION_REQUEST.include?(argv)
|
|
78
90
|
|
|
91
|
+
Migration.run(paths: make_paths, err: stderr)
|
|
92
|
+
|
|
93
|
+
if bare_query?(argv)
|
|
94
|
+
paths = make_paths
|
|
95
|
+
config = Config::Store.load(paths.config_file).success
|
|
96
|
+
exit_code = Nav.dispatch(argv[0], stdout, stderr, config.base_dir)
|
|
97
|
+
Kernel.exit(exit_code)
|
|
98
|
+
end
|
|
99
|
+
|
|
79
100
|
begin
|
|
80
101
|
Dry::CLI.new(Registry).call(arguments: argv, out: stdout, err: stderr)
|
|
81
102
|
outcome = last_outcome
|
|
@@ -104,12 +125,12 @@ module SpaceArchitect::Pristine
|
|
|
104
125
|
|
|
105
126
|
# Print the gem version to stdout and exit 0.
|
|
106
127
|
def self.print_version(stdout)
|
|
107
|
-
stdout.puts
|
|
128
|
+
stdout.puts Space::Src::VERSION
|
|
108
129
|
Kernel.exit(0)
|
|
109
130
|
end
|
|
110
131
|
|
|
111
132
|
# Internal: build a Paths instance scoped to the active env
|
|
112
|
-
# (Thread.current[:
|
|
133
|
+
# (Thread.current[:space_src_cli_env] || ENV). Every command
|
|
113
134
|
# uses this so tests can inject a temp home without mutating
|
|
114
135
|
# the real ENV.
|
|
115
136
|
def self.make_paths
|
|
@@ -125,4 +146,14 @@ module SpaceArchitect::Pristine
|
|
|
125
146
|
end
|
|
126
147
|
end
|
|
127
148
|
|
|
128
|
-
|
|
149
|
+
# Subcommand files — each defines its command classes and
|
|
150
|
+
# registers them under their group prefix.
|
|
151
|
+
require "space_src/cli/repo"
|
|
152
|
+
require "space_src/cli/org"
|
|
153
|
+
require "space_src/cli/sync"
|
|
154
|
+
require "space_src/cli/status"
|
|
155
|
+
require "space_src/cli/config"
|
|
156
|
+
require "space_src/cli/daemon"
|
|
157
|
+
require "space_src/cli/clone"
|
|
158
|
+
require "space_src/cli/shell"
|
|
159
|
+
require "space_src/nav"
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
require "async"
|
|
4
4
|
require "dry/monads"
|
|
5
|
-
require "
|
|
5
|
+
require "space_src/shell"
|
|
6
6
|
|
|
7
|
-
module
|
|
7
|
+
module Space::Src
|
|
8
8
|
# Resolution + COW-copy boundary for `clone`. Returns Result; no
|
|
9
9
|
# side effects on Failure. Injected shell seam defaults to ShellRunner
|
|
10
10
|
# (wraps Shell.run in Sync{} so the Fiber-scheduler requirement is met
|
|
@@ -14,7 +14,7 @@ module SpaceArchitect::Pristine
|
|
|
14
14
|
|
|
15
15
|
class ShellRunner
|
|
16
16
|
def run(*argv)
|
|
17
|
-
Sync {
|
|
17
|
+
Sync { Space::Src::Shell.run(*argv) }
|
|
18
18
|
end
|
|
19
19
|
end
|
|
20
20
|
|
|
@@ -4,7 +4,7 @@ require "dry/validation"
|
|
|
4
4
|
require "dry/validation/extensions/monads"
|
|
5
5
|
require "dry/monads"
|
|
6
6
|
|
|
7
|
-
module
|
|
7
|
+
module Space::Src
|
|
8
8
|
module Config
|
|
9
9
|
# Validates the raw YAML hash before it is built into a Config struct.
|
|
10
10
|
# Returns a Dry::Monads::Result (via the :monads extension):
|
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
require "yaml"
|
|
4
4
|
require "fileutils"
|
|
5
5
|
require "dry/monads"
|
|
6
|
-
require "
|
|
7
|
-
require "
|
|
6
|
+
require "space_src/config/model"
|
|
7
|
+
require "space_src/config/contract"
|
|
8
8
|
|
|
9
|
-
module
|
|
9
|
+
module Space::Src
|
|
10
10
|
module Config
|
|
11
11
|
# Load/validate/write-back the YAML config file.
|
|
12
12
|
#
|
|
@@ -18,7 +18,7 @@ module SpaceArchitect::Pristine
|
|
|
18
18
|
class Store
|
|
19
19
|
extend Dry::Monads[:result]
|
|
20
20
|
|
|
21
|
-
DEFAULT_BASE_DIR =
|
|
21
|
+
DEFAULT_BASE_DIR = Space::Src::Paths::DEFAULT_BASE_DIR
|
|
22
22
|
DEFAULT_REFRESH_INTERVAL = 6 * 3600
|
|
23
23
|
DEFAULT_CONCURRENCY = 8
|
|
24
24
|
|
|
@@ -31,7 +31,7 @@ module SpaceArchitect::Pristine
|
|
|
31
31
|
# The contract stays integer-typed (:integer, gt?: 0); this
|
|
32
32
|
# is a load-layer normalization that lets a hand-edited
|
|
33
33
|
# config.yaml round-trip without rejecting "6h" as a
|
|
34
|
-
# non-integer. See lib/
|
|
34
|
+
# non-integer. See lib/space_src/config/duration.rb.
|
|
35
35
|
if hash.key?(:refresh_interval)
|
|
36
36
|
result = Duration.parse(hash[:refresh_interval])
|
|
37
37
|
return result if result.failure?
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "dry/monads"
|
|
4
|
-
require "
|
|
4
|
+
require "space_src/config/model"
|
|
5
5
|
|
|
6
|
-
module
|
|
6
|
+
module Space::Src
|
|
7
7
|
module Forge
|
|
8
8
|
# Abstract forge interface. The GitHub implementation lists the
|
|
9
9
|
# repos belonging to an OrgRef. The interface is intentionally
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
|
-
require "
|
|
5
|
-
require "
|
|
6
|
-
require "
|
|
4
|
+
require "space_src/forge/client"
|
|
5
|
+
require "space_src/shell"
|
|
6
|
+
require "space_src/config/model"
|
|
7
7
|
|
|
8
|
-
module
|
|
8
|
+
module Space::Src
|
|
9
9
|
module Forge
|
|
10
10
|
# `gh repo list <org> --json …` implementation of Forge::Client.
|
|
11
11
|
#
|
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
require "dry/monads"
|
|
4
4
|
require "async"
|
|
5
|
-
require "
|
|
6
|
-
require "
|
|
5
|
+
require "space_src/shell"
|
|
6
|
+
require "space_src/launchd/plist"
|
|
7
7
|
|
|
8
|
-
module
|
|
8
|
+
module Space::Src
|
|
9
9
|
module Launchd
|
|
10
10
|
# launchctl wrapper. Holds an injected command runner (the
|
|
11
|
-
# real default goes through `
|
|
11
|
+
# real default goes through `Space::Src::Shell` inside a
|
|
12
12
|
# `Sync{}` block; tests inject a `RecordingRunner` that
|
|
13
13
|
# captures argv and returns canned output — gate G2).
|
|
14
14
|
#
|
|
@@ -26,7 +26,7 @@ module SpaceArchitect::Pristine
|
|
|
26
26
|
class Agent
|
|
27
27
|
extend Dry::Monads[:result]
|
|
28
28
|
|
|
29
|
-
DEFAULT_LABEL = "io.github.jetpks.
|
|
29
|
+
DEFAULT_LABEL = "io.github.jetpks.space-src.sync"
|
|
30
30
|
|
|
31
31
|
# The default real-runner. Wraps `Shell.run` in a `Sync{}`
|
|
32
32
|
# block so the Fiber-scheduler requirement is satisfied.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
module
|
|
3
|
+
module Space::Src
|
|
4
4
|
module Launchd
|
|
5
5
|
# Hand-rolled launchd plist emitter. The slice forbids a plist
|
|
6
6
|
# gem (PRD §2, AGENTS.md) — this class emits an XML property
|
|
@@ -8,7 +8,7 @@ module SpaceArchitect::Pristine
|
|
|
8
8
|
#
|
|
9
9
|
# The plist produced here is a fixed-shape StartInterval-driven
|
|
10
10
|
# agent that:
|
|
11
|
-
# * runs `
|
|
11
|
+
# * runs `src sync` non-interactively under the
|
|
12
12
|
# repo's mise-managed Ruby (so the right toolchain is in
|
|
13
13
|
# effect without `mise activate`, which is broken
|
|
14
14
|
# non-interactively);
|
|
@@ -44,7 +44,7 @@ module SpaceArchitect::Pristine
|
|
|
44
44
|
# @param mise_toml [String] Absolute path to mise.toml (pinned via EnvironmentVariables.MISE_CONFIG_FILE).
|
|
45
45
|
# @param mise_bin [String] Absolute path to the mise binary (ProgramArguments[0]).
|
|
46
46
|
# @param ruby_bin [String] Absolute path to the ruby to run the script under.
|
|
47
|
-
# @param bin_path [String] Absolute path to the
|
|
47
|
+
# @param bin_path [String] Absolute path to the src bin script.
|
|
48
48
|
# @return [String] The full plist XML, ready to be written to disk and `plutil -lint`-validated.
|
|
49
49
|
def call(label:, refresh_interval:, log_dir:, repo_root:, mise_toml:, mise_bin:, ruby_bin:, bin_path:)
|
|
50
50
|
raise ArgumentError, "label is required" if label.to_s.empty?
|
|
@@ -4,7 +4,7 @@ require "fileutils"
|
|
|
4
4
|
require "time"
|
|
5
5
|
require "dry/monads"
|
|
6
6
|
|
|
7
|
-
module
|
|
7
|
+
module Space::Src
|
|
8
8
|
# Rotates a log file when it exceeds a byte threshold. The
|
|
9
9
|
# archive's filename embeds an ISO-8601-compact timestamp
|
|
10
10
|
# (`YYYYMMDDTHHMMSSZ`) of the rotation event (the injected
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module Space::Src
|
|
6
|
+
# One-shot data-preserving migration from the `repo-tender` identity
|
|
7
|
+
# to `space-src`. Invoked from CLI.run before dispatch on every run;
|
|
8
|
+
# all operations are idempotent so repeated invocations are safe.
|
|
9
|
+
class Migration
|
|
10
|
+
OLD_APP_NAME = "repo-tender"
|
|
11
|
+
OLD_LABEL = "io.github.jetpks.repo-tender.sync"
|
|
12
|
+
|
|
13
|
+
# Move old-identity XDG dirs to new-identity locations if the old
|
|
14
|
+
# ones exist and the new ones do not (no-clobber — no data loss).
|
|
15
|
+
# Print a one-line notice to `err` only when something is actually
|
|
16
|
+
# moved. Also warn if the old-label launchd plist is still present
|
|
17
|
+
# so the user knows to run `src daemon install`.
|
|
18
|
+
def self.run(paths:, err:)
|
|
19
|
+
moved = false
|
|
20
|
+
|
|
21
|
+
old_config = File.join(paths.config_home, OLD_APP_NAME)
|
|
22
|
+
new_config = paths.config_dir
|
|
23
|
+
if File.directory?(old_config) && !File.exist?(new_config)
|
|
24
|
+
FileUtils.mv(old_config, new_config)
|
|
25
|
+
moved = true
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
old_state = File.join(paths.state_home, OLD_APP_NAME)
|
|
29
|
+
new_state = paths.state_dir
|
|
30
|
+
if File.directory?(old_state) && !File.exist?(new_state)
|
|
31
|
+
FileUtils.mv(old_state, new_state)
|
|
32
|
+
moved = true
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
err.puts "space-src: migrated config/state from #{OLD_APP_NAME}" if moved
|
|
36
|
+
|
|
37
|
+
old_plist = File.join(paths.launch_agents_dir, "#{OLD_LABEL}.plist")
|
|
38
|
+
if File.exist?(old_plist)
|
|
39
|
+
err.puts "space-src: stale launchd agent found (#{OLD_LABEL}); run `src daemon install` to upgrade"
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Space::Src
|
|
4
|
+
# Fuzzy navigator over on-disk source checkouts.
|
|
5
|
+
#
|
|
6
|
+
# Checkouts live at base_dir/<host>/<owner>/<name> (depth-3 dirs).
|
|
7
|
+
# Matching is a case-insensitive SUBSEQUENCE against "owner/name" —
|
|
8
|
+
# host is excluded from the match but is part of the resolved path.
|
|
9
|
+
#
|
|
10
|
+
# Ranking is fzf-inspired: contiguity bonus, word-boundary bonus,
|
|
11
|
+
# earliness (earlier first match wins). Ties break by target string
|
|
12
|
+
# asc then host asc — deterministic total order.
|
|
13
|
+
module Nav
|
|
14
|
+
# Enumerate all depth-3 directories under base_dir.
|
|
15
|
+
# Returns array of hashes: {host:, owner:, name:, target:, path:}.
|
|
16
|
+
def self.scan(base_dir)
|
|
17
|
+
pattern = File.join(base_dir, "*", "*", "*")
|
|
18
|
+
prefix = base_dir.chomp("/") + "/"
|
|
19
|
+
Dir.glob(pattern).filter_map do |path|
|
|
20
|
+
next unless File.directory?(path)
|
|
21
|
+
relative = path.delete_prefix(prefix)
|
|
22
|
+
parts = relative.split("/")
|
|
23
|
+
next unless parts.length == 3
|
|
24
|
+
host, owner, name = parts
|
|
25
|
+
{host:, owner:, name:, target: "#{owner}/#{name}", path:}
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Pure: find the leftmost match positions of query chars (case-insensitive)
|
|
30
|
+
# as a subsequence into target. Returns an array of integer indices, or nil
|
|
31
|
+
# if the query is not a subsequence of the target.
|
|
32
|
+
def self.match_positions(query, target)
|
|
33
|
+
q = query.downcase
|
|
34
|
+
t = target.downcase
|
|
35
|
+
positions = []
|
|
36
|
+
qi = 0
|
|
37
|
+
t.each_char.with_index do |c, i|
|
|
38
|
+
if c == q[qi]
|
|
39
|
+
positions << i
|
|
40
|
+
qi += 1
|
|
41
|
+
return positions if qi == q.length
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
nil
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Pure: compute a score for a set of match positions within target_lower.
|
|
48
|
+
# Higher score = better match.
|
|
49
|
+
# contiguity: each consecutive pair of matched indices scores +10
|
|
50
|
+
# word boundary: each position at start of string or right after
|
|
51
|
+
# '/', '-', '_' scores +5
|
|
52
|
+
# earliness: subtract the first matched position (earlier = higher score)
|
|
53
|
+
def self.score_match(positions, target_lower)
|
|
54
|
+
contiguity = positions.each_cons(2).count { |a, b| b == a + 1 } * 10
|
|
55
|
+
boundary = positions.count do |p|
|
|
56
|
+
p == 0 || "/\\-_".include?(target_lower[p - 1])
|
|
57
|
+
end * 5
|
|
58
|
+
earliness = -positions.first
|
|
59
|
+
contiguity + boundary + earliness
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Pure: match and rank a list of entry hashes against query.
|
|
63
|
+
# Returns entries annotated with :score, sorted best-first.
|
|
64
|
+
# Tie-break: target asc, then host asc.
|
|
65
|
+
def self.rank(entries, query)
|
|
66
|
+
scored = entries.filter_map do |e|
|
|
67
|
+
t = e[:target].downcase
|
|
68
|
+
positions = match_positions(query, t)
|
|
69
|
+
next unless positions
|
|
70
|
+
score = score_match(positions, t)
|
|
71
|
+
e.merge(score:)
|
|
72
|
+
end
|
|
73
|
+
scored.sort_by { |e| [-e[:score], e[:target], e[:host]] }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Cd-contract executor. Scans base_dir for checkouts, fuzzy-matches
|
|
77
|
+
# query, applies the 0/1/many contract:
|
|
78
|
+
# - exactly one match → absolute path on last stdout line, returns 0
|
|
79
|
+
# - zero matches → message on stderr, returns 1
|
|
80
|
+
# - multiple matches → ranked candidates on stdout, returns 1
|
|
81
|
+
def self.dispatch(query, stdout, stderr, base_dir)
|
|
82
|
+
entries = scan(base_dir)
|
|
83
|
+
matches = rank(entries, query)
|
|
84
|
+
|
|
85
|
+
case matches.length
|
|
86
|
+
when 0
|
|
87
|
+
stderr.puts "src: no match for '#{query}'"
|
|
88
|
+
1
|
|
89
|
+
when 1
|
|
90
|
+
stdout.puts matches.first[:path]
|
|
91
|
+
0
|
|
92
|
+
else
|
|
93
|
+
matches.each { |m| stdout.puts "#{m[:host]}/#{m[:target]}" }
|
|
94
|
+
1
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
require "xdg"
|
|
4
4
|
require "fileutils"
|
|
5
5
|
|
|
6
|
-
module
|
|
6
|
+
module Space::Src
|
|
7
7
|
# XDG-aware path resolution. Honors $XDG_CONFIG_HOME / $XDG_STATE_HOME
|
|
8
8
|
# overrides (and a caller-supplied environment hash for testability);
|
|
9
9
|
# otherwise falls back to the XDG defaults (~/.config, ~/.local/state).
|
|
@@ -13,7 +13,7 @@ module SpaceArchitect::Pristine
|
|
|
13
13
|
# resolved from the config at call time (passed in as an argument here
|
|
14
14
|
# so this module owns nothing about config storage).
|
|
15
15
|
class Paths
|
|
16
|
-
APP_NAME = "
|
|
16
|
+
APP_NAME = "space-src"
|
|
17
17
|
|
|
18
18
|
DEFAULT_BASE_DIR = File.expand_path("~/architect/src")
|
|
19
19
|
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
require "fileutils"
|
|
4
4
|
require "time"
|
|
5
|
-
require "
|
|
6
|
-
require "
|
|
7
|
-
require "
|
|
5
|
+
require "space_src/scm/client"
|
|
6
|
+
require "space_src/scm/status"
|
|
7
|
+
require "space_src/shell"
|
|
8
8
|
|
|
9
|
-
module
|
|
9
|
+
module Space::Src
|
|
10
10
|
module SCM
|
|
11
11
|
# Git CLI implementation of SCM::Client. All subprocess work is
|
|
12
12
|
# delegated to Shell.run (which requires an ambient Async::Task).
|
|
@@ -4,7 +4,7 @@ require "open3"
|
|
|
4
4
|
require "async"
|
|
5
5
|
require "dry/monads"
|
|
6
6
|
|
|
7
|
-
module
|
|
7
|
+
module Space::Src
|
|
8
8
|
# Thin Open3.capture3 wrapper that:
|
|
9
9
|
# * requires an ambient Async::Task (so subprocess I/O flows through
|
|
10
10
|
# Ruby's Fiber scheduler → kqueue on macOS and is non-blocking);
|