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.
Files changed (113) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +103 -0
  3. data/README.md +248 -155
  4. data/exe/architect +1 -1
  5. data/exe/space +2 -2
  6. data/exe/src +13 -0
  7. data/lib/space_architect/architect_mission.rb +84 -53
  8. data/lib/space_architect/cli/architect.rb +92 -132
  9. data/lib/space_architect/cli/research.rb +94 -0
  10. data/lib/space_architect/cli/space.rb +25 -31
  11. data/lib/space_architect/cli/src.rb +20 -14
  12. data/lib/space_architect/cli.rb +22 -22
  13. data/lib/space_architect/dispatcher.rb +5 -1
  14. data/lib/space_architect/harness.rb +123 -16
  15. data/lib/space_architect/research/mux.rb +127 -0
  16. data/lib/space_architect/research/registry.rb +70 -0
  17. data/lib/space_architect/research/renderer.rb +101 -0
  18. data/lib/space_architect/research/run.rb +7 -0
  19. data/lib/space_architect/research/supervisor.rb +108 -0
  20. data/lib/space_architect/research.rb +13 -0
  21. data/lib/space_architect/run_creator.rb +53 -0
  22. data/lib/space_architect/skill_installer.rb +81 -79
  23. data/lib/space_architect.rb +5 -20
  24. data/lib/{space_architect → space_core}/atomic_write.rb +1 -1
  25. data/lib/space_core/cli/base_command.rb +19 -0
  26. data/lib/space_core/cli/config.rb +49 -0
  27. data/lib/space_core/cli/current.rb +16 -0
  28. data/lib/space_core/cli/help.rb +110 -0
  29. data/lib/space_core/cli/helpers.rb +115 -0
  30. data/lib/space_core/cli/init.rb +29 -0
  31. data/lib/space_core/cli/list.rb +24 -0
  32. data/lib/space_core/cli/new.rb +38 -0
  33. data/lib/space_core/cli/path.rb +16 -0
  34. data/lib/space_core/cli/repeatable_options.rb +75 -0
  35. data/lib/space_core/cli/repo.rb +76 -0
  36. data/lib/space_core/cli/shell.rb +125 -0
  37. data/lib/space_core/cli/show.rb +21 -0
  38. data/lib/space_core/cli/status.rb +33 -0
  39. data/lib/space_core/cli/use.rb +17 -0
  40. data/lib/space_core/cli.rb +171 -0
  41. data/lib/{space_architect → space_core}/config.rb +1 -1
  42. data/lib/{space_architect → space_core}/errors.rb +1 -1
  43. data/lib/{space_architect → space_core}/git_client.rb +1 -1
  44. data/lib/{space_architect → space_core}/mise_client.rb +1 -1
  45. data/lib/{space_architect → space_core}/repo_reference.rb +1 -1
  46. data/lib/{space_architect → space_core}/repo_resolver.rb +1 -1
  47. data/lib/{space_architect → space_core}/shell_integration.rb +1 -1
  48. data/lib/{space_architect → space_core}/slugger.rb +1 -1
  49. data/lib/{space_architect → space_core}/space.rb +1 -1
  50. data/lib/{space_architect → space_core}/space_store.rb +12 -12
  51. data/lib/{space_architect → space_core}/state.rb +1 -1
  52. data/lib/{space_architect → space_core}/terminal.rb +1 -1
  53. data/lib/space_core/version.rb +7 -0
  54. data/lib/{space_architect → space_core}/warnings.rb +1 -1
  55. data/lib/{space_architect → space_core}/xdg.rb +1 -1
  56. data/lib/space_core.rb +24 -0
  57. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/cli/clone.rb +5 -5
  58. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/cli/config.rb +7 -7
  59. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/cli/daemon.rb +46 -30
  60. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/cli/options.rb +1 -1
  61. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/cli/org.rb +9 -9
  62. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/cli/repo.rb +9 -9
  63. data/lib/space_src/cli/shell.rb +122 -0
  64. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/cli/status.rb +7 -7
  65. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/cli/sync.rb +17 -17
  66. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/cli.rb +42 -11
  67. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/cloner.rb +3 -3
  68. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/config/contract.rb +1 -1
  69. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/config/duration.rb +1 -1
  70. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/config/model.rb +1 -1
  71. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/config/store.rb +5 -5
  72. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/forge/client.rb +2 -2
  73. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/forge/github.rb +4 -4
  74. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/launchd/agent.rb +5 -5
  75. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/launchd/plist.rb +3 -3
  76. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/log_rotator.rb +1 -1
  77. data/lib/space_src/migration.rb +43 -0
  78. data/lib/space_src/nav.rb +98 -0
  79. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/paths.rb +2 -2
  80. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/scm/client.rb +1 -1
  81. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/scm/git.rb +4 -4
  82. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/scm/status.rb +1 -1
  83. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/shell.rb +1 -1
  84. data/lib/space_src/shell_integration.rb +321 -0
  85. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/state/lock.rb +1 -1
  86. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/state/store.rb +2 -2
  87. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/sync/engine.rb +12 -12
  88. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/sync/repo_plan.rb +3 -3
  89. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/ui/interactive_reporter.rb +1 -1
  90. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/ui/json_reporter.rb +1 -1
  91. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/ui/mode.rb +1 -1
  92. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/ui/plain_reporter.rb +1 -1
  93. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/ui/reporter.rb +1 -1
  94. data/{vendor/repo-tender/lib/space_architect/pristine → lib/space_src}/version.rb +2 -2
  95. data/lib/space_src.rb +37 -0
  96. data/skill/architect/SKILL.md +2 -2
  97. data/skill/architect/research.md +46 -37
  98. metadata +115 -67
  99. data/lib/space_architect/cli/config.rb +0 -61
  100. data/lib/space_architect/cli/current.rb +0 -22
  101. data/lib/space_architect/cli/helpers.rb +0 -117
  102. data/lib/space_architect/cli/init.rb +0 -35
  103. data/lib/space_architect/cli/list.rb +0 -30
  104. data/lib/space_architect/cli/new.rb +0 -43
  105. data/lib/space_architect/cli/options.rb +0 -12
  106. data/lib/space_architect/cli/path.rb +0 -22
  107. data/lib/space_architect/cli/repo.rb +0 -88
  108. data/lib/space_architect/cli/shell.rb +0 -137
  109. data/lib/space_architect/cli/show.rb +0 -27
  110. data/lib/space_architect/cli/status.rb +0 -39
  111. data/lib/space_architect/cli/use.rb +0 -23
  112. data/lib/space_architect/version.rb +0 -5
  113. 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 "space_architect/pristine/cli"
5
- require "space_architect/pristine/cli/repo" # for Repo::Helpers.parse_ref
6
- require "space_architect/pristine/cli/options"
7
- require "space_architect/pristine/ui/mode"
8
- require "space_architect/pristine/ui/plain_reporter"
9
- require "space_architect/pristine/ui/json_reporter"
10
- require "space_architect/pristine/ui/interactive_reporter"
11
-
12
- module SpaceArchitect::Pristine
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 = SpaceArchitect::Pristine::Sync::Engine.new(reporter: reporter).call(config: config, paths: paths)
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
- SpaceArchitect::Pristine::CLI.record_outcome(Outcome.new(exit_code: 1, message: msg))
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 `REPO_TENDER_LOG_MAX_BYTES` (introspection /
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
- SpaceArchitect::Pristine::LogRotator.call(p, threshold_bytes: threshold)
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
- # `REPO_TENDER_LOG_MAX_BYTES` env var so a malformed
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["REPO_TENDER_LOG_MAX_BYTES"])
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 "repo-tender: REPO_TENDER_LOG_MAX_BYTES=#{env_value.inspect} is invalid; " \
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
- SpaceArchitect::Pristine::CLI::Registry.register "sync", SpaceArchitect::Pristine::CLI::Sync::Run
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 SpaceArchitect::Pristine
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 `bin/repo-tender`
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[:repo_tender_cli_env] =
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[:repo_tender_cli_env] || ENV
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[:repo_tender_cli_outcome] = outcome
48
+ Thread.current[:space_src_cli_outcome] = outcome
47
49
  end
48
50
 
49
51
  def self.last_outcome
50
- Thread.current[:repo_tender_cli_outcome]
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
- # Entrypoint. Called by bin/repo-tender. Intercepts the top-level
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/repo_tender/shell.rb`).
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 SpaceArchitect::Pristine::VERSION
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[:repo_tender_cli_env] || ENV). Every command
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 "space_architect/pristine/shell"
5
+ require "space_src/shell"
6
6
 
7
- module SpaceArchitect::Pristine
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 { SpaceArchitect::Pristine::Shell.run(*argv) }
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 SpaceArchitect::Pristine
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):
@@ -2,7 +2,7 @@
2
2
 
3
3
  require "dry/monads"
4
4
 
5
- module SpaceArchitect::Pristine
5
+ module Space::Src
6
6
  module Config
7
7
  # CF1: parse a human-duration string into integer seconds.
8
8
  #
@@ -3,7 +3,7 @@
3
3
  require "dry/struct"
4
4
  require "dry/types"
5
5
 
6
- module SpaceArchitect::Pristine
6
+ module Space::Src
7
7
  module Config
8
8
  Types = Dry.Types()
9
9
 
@@ -3,10 +3,10 @@
3
3
  require "yaml"
4
4
  require "fileutils"
5
5
  require "dry/monads"
6
- require "space_architect/pristine/config/model"
7
- require "space_architect/pristine/config/contract"
6
+ require "space_src/config/model"
7
+ require "space_src/config/contract"
8
8
 
9
- module SpaceArchitect::Pristine
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 = SpaceArchitect::Pristine::Paths::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/repo_tender/config/duration.rb.
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 "space_architect/pristine/config/model"
4
+ require "space_src/config/model"
5
5
 
6
- module SpaceArchitect::Pristine
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 "space_architect/pristine/forge/client"
5
- require "space_architect/pristine/shell"
6
- require "space_architect/pristine/config/model"
4
+ require "space_src/forge/client"
5
+ require "space_src/shell"
6
+ require "space_src/config/model"
7
7
 
8
- module SpaceArchitect::Pristine
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 "space_architect/pristine/shell"
6
- require "space_architect/pristine/launchd/plist"
5
+ require "space_src/shell"
6
+ require "space_src/launchd/plist"
7
7
 
8
- module SpaceArchitect::Pristine
8
+ module Space::Src
9
9
  module Launchd
10
10
  # launchctl wrapper. Holds an injected command runner (the
11
- # real default goes through `SpaceArchitect::Pristine::Shell` inside a
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.repo-tender.sync"
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 SpaceArchitect::Pristine
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 `repo-tender sync` non-interactively under the
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 repo-tender bin script.
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 SpaceArchitect::Pristine
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 SpaceArchitect::Pristine
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 = "repo-tender"
16
+ APP_NAME = "space-src"
17
17
 
18
18
  DEFAULT_BASE_DIR = File.expand_path("~/architect/src")
19
19
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  require "dry/monads"
4
4
 
5
- module SpaceArchitect::Pristine
5
+ module Space::Src
6
6
  module SCM
7
7
  # Abstract SCM interface. The git CLI is the only implementation for
8
8
  # now (per AGENTS.md / PRD §1), but the sync engine + tests must
@@ -2,11 +2,11 @@
2
2
 
3
3
  require "fileutils"
4
4
  require "time"
5
- require "space_architect/pristine/scm/client"
6
- require "space_architect/pristine/scm/status"
7
- require "space_architect/pristine/shell"
5
+ require "space_src/scm/client"
6
+ require "space_src/scm/status"
7
+ require "space_src/shell"
8
8
 
9
- module SpaceArchitect::Pristine
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).
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module SpaceArchitect::Pristine
3
+ module Space::Src
4
4
  module SCM
5
5
  # Value object produced by parsing `git status --porcelain=v2
6
6
  # --branch --untracked-files=normal`. v2 is mandatory (per
@@ -4,7 +4,7 @@ require "open3"
4
4
  require "async"
5
5
  require "dry/monads"
6
6
 
7
- module SpaceArchitect::Pristine
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);