space-architect 8.0.0 → 9.0.0

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 (102) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +54 -0
  3. data/README.md +53 -243
  4. data/lib/space_architect/bug_report.rb +2 -1
  5. data/lib/space_architect/cli/architect.rb +20 -7
  6. data/lib/space_architect/cli.rb +9 -14
  7. data/lib/space_architect/session_sync/bin_path.rb +1 -1
  8. data/lib/space_architect/session_sync/cursor.rb +1 -1
  9. data/lib/space_architect/session_sync/plist.rb +1 -1
  10. data/lib/{space_core → space_architect}/version.rb +2 -2
  11. data/lib/space_architect.rb +1 -1
  12. data/skill/architect/SKILL.md +3 -2
  13. data/skill/architect-vocabulary/SKILL.md +2 -2
  14. metadata +18 -177
  15. data/exe/space +0 -13
  16. data/exe/src +0 -13
  17. data/lib/space_architect/cli/space.rb +0 -29
  18. data/lib/space_architect/cli/src.rb +0 -38
  19. data/lib/space_core/atomic_write.rb +0 -21
  20. data/lib/space_core/cli/base_command.rb +0 -19
  21. data/lib/space_core/cli/build.rb +0 -27
  22. data/lib/space_core/cli/config.rb +0 -49
  23. data/lib/space_core/cli/current.rb +0 -16
  24. data/lib/space_core/cli/help.rb +0 -195
  25. data/lib/space_core/cli/helpers.rb +0 -115
  26. data/lib/space_core/cli/init.rb +0 -29
  27. data/lib/space_core/cli/list.rb +0 -24
  28. data/lib/space_core/cli/loop_status.rb +0 -47
  29. data/lib/space_core/cli/new.rb +0 -38
  30. data/lib/space_core/cli/pack.rb +0 -29
  31. data/lib/space_core/cli/path.rb +0 -16
  32. data/lib/space_core/cli/repeatable_options.rb +0 -84
  33. data/lib/space_core/cli/repo.rb +0 -76
  34. data/lib/space_core/cli/run.rb +0 -44
  35. data/lib/space_core/cli/shell.rb +0 -125
  36. data/lib/space_core/cli/show.rb +0 -21
  37. data/lib/space_core/cli/status.rb +0 -74
  38. data/lib/space_core/cli/use.rb +0 -17
  39. data/lib/space_core/cli.rb +0 -177
  40. data/lib/space_core/commands.rb +0 -51
  41. data/lib/space_core/config.rb +0 -152
  42. data/lib/space_core/errors.rb +0 -14
  43. data/lib/space_core/git_client.rb +0 -49
  44. data/lib/space_core/mise_client.rb +0 -37
  45. data/lib/space_core/oci_builder.rb +0 -56
  46. data/lib/space_core/oci_packer.rb +0 -103
  47. data/lib/space_core/oci_runner.rb +0 -89
  48. data/lib/space_core/paths.rb +0 -58
  49. data/lib/space_core/repo_reference.rb +0 -19
  50. data/lib/space_core/repo_resolver.rb +0 -167
  51. data/lib/space_core/shell_integration.rb +0 -438
  52. data/lib/space_core/slugger.rb +0 -16
  53. data/lib/space_core/space.rb +0 -174
  54. data/lib/space_core/space_store.rb +0 -319
  55. data/lib/space_core/state.rb +0 -86
  56. data/lib/space_core/templates/oci/dockerfile.erb +0 -73
  57. data/lib/space_core/templates/oci/dockerignore.erb +0 -17
  58. data/lib/space_core/templates/oci/entrypoint.sh.erb +0 -16
  59. data/lib/space_core/terminal.rb +0 -161
  60. data/lib/space_core/warnings.rb +0 -13
  61. data/lib/space_core/xdg.rb +0 -33
  62. data/lib/space_core.rb +0 -26
  63. data/lib/space_src/cli/clone.rb +0 -54
  64. data/lib/space_src/cli/config.rb +0 -65
  65. data/lib/space_src/cli/daemon.rb +0 -362
  66. data/lib/space_src/cli/options.rb +0 -21
  67. data/lib/space_src/cli/org.rb +0 -199
  68. data/lib/space_src/cli/repo.rb +0 -169
  69. data/lib/space_src/cli/shell.rb +0 -121
  70. data/lib/space_src/cli/status.rb +0 -75
  71. data/lib/space_src/cli/sync.rb +0 -161
  72. data/lib/space_src/cli.rb +0 -159
  73. data/lib/space_src/cloner.rb +0 -77
  74. data/lib/space_src/config/contract.rb +0 -54
  75. data/lib/space_src/config/duration.rb +0 -79
  76. data/lib/space_src/config/model.rb +0 -49
  77. data/lib/space_src/config/store.rb +0 -156
  78. data/lib/space_src/forge/client.rb +0 -31
  79. data/lib/space_src/forge/github.rb +0 -98
  80. data/lib/space_src/launchd/agent.rb +0 -195
  81. data/lib/space_src/launchd/plist.rb +0 -129
  82. data/lib/space_src/log_rotator.rb +0 -46
  83. data/lib/space_src/migration.rb +0 -43
  84. data/lib/space_src/nav.rb +0 -99
  85. data/lib/space_src/paths.rb +0 -72
  86. data/lib/space_src/scm/client.rb +0 -87
  87. data/lib/space_src/scm/git.rb +0 -232
  88. data/lib/space_src/scm/status.rb +0 -24
  89. data/lib/space_src/shell.rb +0 -90
  90. data/lib/space_src/shell_integration.rb +0 -321
  91. data/lib/space_src/state/lock.rb +0 -59
  92. data/lib/space_src/state/store.rb +0 -140
  93. data/lib/space_src/sync/engine.rb +0 -503
  94. data/lib/space_src/sync/repo_plan.rb +0 -215
  95. data/lib/space_src/sync/report.rb +0 -18
  96. data/lib/space_src/ui/interactive_reporter.rb +0 -280
  97. data/lib/space_src/ui/json_reporter.rb +0 -39
  98. data/lib/space_src/ui/mode.rb +0 -68
  99. data/lib/space_src/ui/plain_reporter.rb +0 -53
  100. data/lib/space_src/ui/reporter.rb +0 -48
  101. data/lib/space_src/version.rb +0 -7
  102. data/lib/space_src.rb +0 -38
@@ -1,49 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "fileutils"
4
- require "async/process"
5
- require "pathname"
6
- require "tempfile"
7
-
8
- module Space::Core
9
- class GitClient
10
- def init(path)
11
- path = Pathname.new(path)
12
- stdout, stderr, status = capture("git", "-C", path.to_s, "init")
13
- return true if status.success?
14
-
15
- output = [stdout, stderr].reject(&:empty?).join("\n").strip
16
- message = "git init failed for #{path}"
17
- message = "#{message}: #{output}" unless output.empty?
18
- raise GitError, message
19
- end
20
-
21
- # Best-effort: a missing git identity (user.name/user.email) must not abort
22
- # space creation. Returns false on failure, leaving the repo initialized but
23
- # uncommitted.
24
- def commit_all(path, message)
25
- path = Pathname.new(path)
26
- _, _, add_status = capture("git", "-C", path.to_s, "add", "-A")
27
- return false unless add_status.success?
28
-
29
- _, _, commit_status = capture("git", "-C", path.to_s, "commit", "-m", message)
30
- commit_status.success?
31
- end
32
-
33
- private
34
-
35
- def capture(*command)
36
- stdout = Tempfile.new("project-spaces-git-stdout")
37
- stderr = Tempfile.new("project-spaces-git-stderr")
38
- status = Async::Process.spawn(*command, out: stdout, err: stderr)
39
-
40
- stdout.rewind
41
- stderr.rewind
42
- [stdout.read, stderr.read, status]
43
- ensure
44
- stdout&.close!
45
- stderr&.close!
46
- end
47
-
48
- end
49
- end
@@ -1,37 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "async/process"
4
- require "pathname"
5
- require "tempfile"
6
-
7
- module Space::Core
8
- class MiseClient
9
- def trust(path)
10
- path = Pathname.new(path)
11
- stdout, stderr, status = capture("mise", "trust", "--yes", "--quiet", "--cd", path.to_s)
12
- return true if status.success?
13
-
14
- output = [stdout, stderr].reject(&:empty?).join("\n").strip
15
- message = "mise trust failed for #{path}"
16
- message = "#{message}: #{output}" unless output.empty?
17
- raise MiseError, message
18
- rescue Errno::ENOENT
19
- raise MiseError, "mise executable not found; install mise or make sure it is on PATH"
20
- end
21
-
22
- private
23
-
24
- def capture(*command)
25
- stdout = Tempfile.new("project-spaces-mise-stdout")
26
- stderr = Tempfile.new("project-spaces-mise-stderr")
27
- status = Async::Process.spawn(*command, out: stdout, err: stderr)
28
-
29
- stdout.rewind
30
- stderr.rewind
31
- [stdout.read, stderr.read, status]
32
- ensure
33
- stdout&.close!
34
- stderr&.close!
35
- end
36
- end
37
- end
@@ -1,56 +0,0 @@
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
@@ -1,103 +0,0 @@
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 local_space_architect?
88
- space.path.join("repos/space-architect").directory?
89
- end
90
-
91
- def render_template(name)
92
- template_path = TEMPLATE_DIR.join(name)
93
- ERB.new(template_path.read, trim_mode: "-").result(binding)
94
- end
95
-
96
- def write(filename, content, mode: 0o644)
97
- path = @output_dir.join(filename)
98
- File.write(path, content)
99
- File.chmod(mode, path)
100
- path
101
- end
102
- end
103
- end
@@ -1,89 +0,0 @@
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
- # Always-forwarded substrate auth (the in-image architect/claude reach for these).
11
- # Payload-specific vars are declared per-space via `run.env:` or the --env flag.
12
- AUTH_ENV = %w[ANTHROPIC_API_KEY CLAUDE_CODE_OAUTH_TOKEN ANTHROPIC_BASE_URL].freeze
13
-
14
- def initialize(space:, env: ENV.to_h, interactive: true, env_vars: [])
15
- @space = space
16
- @env = env
17
- @interactive = interactive
18
- @env_vars = (space.run_env + Array(env_vars)).map(&:to_s)
19
- end
20
-
21
- def image
22
- "#{space.id}:latest"
23
- end
24
-
25
- def mounts
26
- space.persist_paths.map do |guest|
27
- [space.path.join(".state" + guest), guest]
28
- end
29
- end
30
-
31
- def host_dirs
32
- mounts.map(&:first)
33
- end
34
-
35
- def command(extra = [])
36
- validated = validate_persist_paths(space.persist_paths)
37
- return validated if validated.failure?
38
-
39
- argv = [
40
- "container", "run", "--rm",
41
- *(@interactive ? ["-i", "-t"] : []),
42
- *env_flags,
43
- *mount_flags,
44
- image,
45
- *extra
46
- ]
47
- Success(argv)
48
- end
49
-
50
- # Explicitly-requested vars (run.env: + --env) that are unset/empty in the host
51
- # env, so the caller can warn — silence here would let a missing key fail opaquely
52
- # inside the guest. AUTH_ENV is opportunistic and intentionally excluded.
53
- def missing_env
54
- @env_vars.uniq.reject { |var| present?(var) }
55
- end
56
-
57
- private
58
-
59
- attr_reader :space, :env, :interactive
60
-
61
- # Bare `-e VAR` passthrough (no =value on the command line, so the secret never
62
- # lands in argv/ps): substrate auth first, then the space's declared payload vars.
63
- def env_flags
64
- (AUTH_ENV + @env_vars).uniq.each_with_object([]) do |var, flags|
65
- flags.push("-e", var) if present?(var)
66
- end
67
- end
68
-
69
- def present?(var)
70
- val = env[var]
71
- val && !val.empty?
72
- end
73
-
74
- def mount_flags
75
- mounts.each_with_object([]) do |(host, guest), flags|
76
- flags.push("-v", "#{host}:#{guest}")
77
- end
78
- end
79
-
80
- def validate_persist_paths(paths)
81
- paths.each do |path|
82
- unless Pathname.new(path).absolute?
83
- return Failure("persist path '#{path}' must be an absolute path")
84
- end
85
- end
86
- Success(paths)
87
- end
88
- end
89
- end
@@ -1,58 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "pathname"
4
-
5
- module Space::Core
6
- module Paths
7
- # paths:exempt-file - the shared module itself — this is the one home the guard defends
8
- module_function
9
-
10
- # Flags for matching a changed path against a lane's touch_set globs.
11
- # PATHNAME keeps a single `*` from crossing `/`; EXTGLOB enables `{a,b}`;
12
- # DOTMATCH lets a glob reach dotfile segments, so a `dir/**` touch set covers
13
- # `dir/.github/workflows/ci.yml` — the standard deliverable for a lane preparing
14
- # a directory to become a repo root.
15
- TOUCH_FNM = File::FNM_PATHNAME | File::FNM_EXTGLOB | File::FNM_DOTMATCH
16
-
17
- # Every path beneath root, at every depth: files, directories, dotfiles, and
18
- # dot-directory contents all included. `File::FNM_DOTMATCH` is what makes
19
- # dotfiles visible to `Dir.glob`, but it also emits a bogus `root/.`
20
- # self-entry — that trap is absorbed here so no callsite has to know about it.
21
- # Returns Array<String> — Dir.glob's native return shape.
22
- def content_tree(root)
23
- Dir.glob(File.join(root.to_s, "**", "*"), File::FNM_DOTMATCH).reject { |p| File.basename(p) == "." }
24
- end
25
-
26
- # The direct children of a fixed-depth structured directory layout (one
27
- # entry per iteration/skill/lane/space) where a leading dot never names a
28
- # real layout member — only tooling junk (.git, .DS_Store). Excludes them.
29
- # Returns Array<Pathname> — Pathname#children's native return shape.
30
- def layout_children(dir)
31
- Pathname.new(dir).children.reject { |c| c.basename.to_s.start_with?(".") }
32
- end
33
-
34
- # Does a single touch_set glob match path? A trailing `dir/**` is matched
35
- # twice: bare (PATHNAME stops it at direct children) and as `dir/**/*`,
36
- # whose whole-component `**/` does cross `/`.
37
- def touch_match?(glob, path)
38
- File.fnmatch(glob, path, TOUCH_FNM) ||
39
- (glob.end_with?("/**") && File.fnmatch("#{glob}/*", path, TOUCH_FNM))
40
- end
41
-
42
- def contract(path, env: ENV)
43
- value = path.to_s
44
- home = XDG.home(env: env)
45
- [home, realpath_or_nil(home)].compact.uniq.each do |h|
46
- return "~" if value == h
47
- return "~#{value.delete_prefix(h)}" if value.start_with?("#{h}/")
48
- end
49
- value
50
- end
51
-
52
- def realpath_or_nil(path)
53
- File.realpath(path)
54
- rescue SystemCallError
55
- nil
56
- end
57
- end
58
- end
@@ -1,19 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "pathname"
4
-
5
- module Space::Core
6
- RepoReference = Data.define(:provider, :owner, :name, :clone_url, :source) do
7
- def full_name
8
- "#{provider}/#{owner}/#{name}"
9
- end
10
-
11
- def directory_name
12
- name
13
- end
14
-
15
- def src_path(root)
16
- Pathname.new(root).join(provider, owner, name)
17
- end
18
- end
19
- end
@@ -1,167 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "uri"
4
-
5
- module Space::Core
6
- class RepoResolver
7
- SCP_LIKE_PATTERN = /\A(?:[^@\/]+@)?(?<provider>[^:\/]+):(?<path>.+)\z/
8
- URL_PATTERN = %r{\A[A-Za-z][A-Za-z0-9+\-.]*://}
9
-
10
- attr_reader :config
11
-
12
- def initialize(config)
13
- @config = config
14
- end
15
-
16
- def resolve(spec)
17
- value = spec.to_s.strip
18
- raise RepoResolutionError, "Repo cannot be blank" if value.empty?
19
-
20
- if url_like?(value)
21
- resolve_url(value)
22
- elsif (match = value.match(SCP_LIKE_PATTERN))
23
- reference_from_parts(
24
- provider: match[:provider],
25
- path_parts: split_repo_path(match[:path]),
26
- clone_url: value,
27
- source: value
28
- )
29
- else
30
- resolve_shorthand(value)
31
- end
32
- end
33
-
34
- private
35
-
36
- def resolve_url(value)
37
- uri = URI.parse(value)
38
- provider = uri.host
39
- path = uri.path.to_s.delete_prefix("/")
40
- raise RepoResolutionError, "Could not determine provider from '#{value}'" if provider.to_s.empty?
41
-
42
- reference_from_parts(
43
- provider: provider,
44
- path_parts: split_repo_path(path),
45
- clone_url: value,
46
- source: value
47
- )
48
- rescue URI::InvalidURIError
49
- raise RepoResolutionError, "Could not parse repo URL '#{value}'"
50
- end
51
-
52
- def resolve_shorthand(value)
53
- parts = split_repo_path(value)
54
-
55
- if parts.length == 1
56
- resolve_default_organization_repo(parts.first, value)
57
- elsif provider_like?(parts.first) && parts.length >= 3
58
- reference_from_parts(
59
- provider: parts.first,
60
- path_parts: parts[1..],
61
- clone_url: nil,
62
- source: value
63
- )
64
- else
65
- resolve_default_provider_repo(parts, value)
66
- end
67
- end
68
-
69
- def resolve_default_organization_repo(name, source)
70
- provider = require_default_provider(source)
71
- owner = config.default_organization
72
- unless owner
73
- raise RepoResolutionError,
74
- "Repo '#{source}' needs an organization. Set one with: space config set default_organization ORG"
75
- end
76
-
77
- reference(provider:, owner:, name:, clone_url: nil, source:)
78
- end
79
-
80
- def resolve_default_provider_repo(parts, source)
81
- provider = require_default_provider(source)
82
- reference_from_parts(provider:, path_parts: parts, clone_url: nil, source:)
83
- end
84
-
85
- def reference_from_parts(provider:, path_parts:, clone_url:, source:)
86
- if path_parts.length < 2
87
- raise RepoResolutionError, "Repo '#{source}' must include an organization and repo name"
88
- end
89
-
90
- name = path_parts.last
91
- owner = path_parts[0...-1].join("/")
92
- reference(provider:, owner:, name:, clone_url:, source:)
93
- end
94
-
95
- def reference(provider:, owner:, name:, clone_url:, source:)
96
- normalized_provider = normalize_provider(provider)
97
- normalized_owner = normalize_path_part(owner)
98
- normalized_name = normalize_repo_name(name)
99
-
100
- RepoReference.new(
101
- provider: normalized_provider,
102
- owner: normalized_owner,
103
- name: normalized_name,
104
- clone_url: clone_url || clone_url_for(normalized_provider, normalized_owner, normalized_name),
105
- source: source
106
- )
107
- end
108
-
109
- def clone_url_for(provider, owner, name)
110
- case config.git_clone_protocol
111
- when "ssh"
112
- "git@#{provider}:#{owner}/#{name}.git"
113
- when "https"
114
- "https://#{provider}/#{owner}/#{name}.git"
115
- end
116
- end
117
-
118
- def split_repo_path(value)
119
- normalized = value.to_s.strip.delete_prefix("/").delete_suffix("/")
120
- normalized = normalized.delete_suffix(".git")
121
- parts = normalized.split("/").reject(&:empty?)
122
- raise RepoResolutionError, "Repo '#{value}' must include a repo name" if parts.empty?
123
-
124
- parts
125
- end
126
-
127
- def require_default_provider(source)
128
- config.default_provider || raise(
129
- RepoResolutionError,
130
- "Repo '#{source}' needs a provider. Set one with: space config set default_provider PROVIDER"
131
- )
132
- end
133
-
134
- def normalize_provider(value)
135
- normalized = value.to_s.strip
136
- normalized = normalized.delete_prefix("https://")
137
- normalized = normalized.delete_prefix("http://")
138
- normalized = normalized.delete_prefix("ssh://")
139
- normalized = normalized.delete_suffix("/")
140
- raise RepoResolutionError, "Provider cannot be blank" if normalized.empty?
141
-
142
- normalized
143
- end
144
-
145
- def normalize_path_part(value)
146
- normalized = value.to_s.strip.delete_prefix("/").delete_suffix("/")
147
- raise RepoResolutionError, "Organization cannot be blank" if normalized.empty?
148
-
149
- normalized
150
- end
151
-
152
- def normalize_repo_name(value)
153
- normalized = value.to_s.strip.delete_suffix(".git")
154
- raise RepoResolutionError, "Repo name cannot be blank" if normalized.empty?
155
-
156
- normalized
157
- end
158
-
159
- def url_like?(value)
160
- value.match?(URL_PATTERN)
161
- end
162
-
163
- def provider_like?(value)
164
- value.include?(".") || value.include?(":") || value == "localhost"
165
- end
166
- end
167
- end