right_now 0.1.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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +87 -0
- data/exe/rn +7 -0
- data/exe/rni +8 -0
- data/lib/rightnow/cli.rb +66 -0
- data/lib/rightnow/query.rb +83 -0
- data/lib/rightnow/resolver.rb +240 -0
- data/lib/rightnow/version.rb +3 -0
- data/lib/rightnow.rb +6 -0
- data/right_now.gemspec +21 -0
- metadata +53 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: a566156485512651cffed05ab55379f0cab3b025fdc07f209341eea69dd53352
|
|
4
|
+
data.tar.gz: c7699767b0dd24e0e119ea29ccf0fa2f2d94669330e7f0c146fa7c02b6c0f1f2
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: a17606397b14a73843e9942a339646b8d2698e405a69c846f5ede2b0a92c523311d8b053a3b55e2dd6a60a560908dfcf70ea06bc0bb0d9b0b43757eada5e8b9a
|
|
7
|
+
data.tar.gz: 53e202722d68b59fa46088fcce2395dcda200a909135bae509dfc7af8886204dd1b3d0d00424cc0e877f8b0ed41f573a8c1bb6715e602a74e0dc395cab4d866e
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 SEAHAL
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# rightnow
|
|
2
|
+
|
|
3
|
+
Rails-aware code resolver.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
gem install right_now
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Examples
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
rn user
|
|
15
|
+
rn m user
|
|
16
|
+
rn c admin/users
|
|
17
|
+
rn helper abc def ghi
|
|
18
|
+
rn app/controllers/application_controller.rb
|
|
19
|
+
rn public robots
|
|
20
|
+
rn user --print
|
|
21
|
+
rn user --json
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## What It Does
|
|
25
|
+
`rn` resolves a short query to the most likely existing file in a Rails project.
|
|
26
|
+
|
|
27
|
+
## Query Model
|
|
28
|
+
`rn` supports three basic query styles:
|
|
29
|
+
* path-like queries are strongest
|
|
30
|
+
* kind-prefixed queries narrow the search scope
|
|
31
|
+
* generic queries search across known Rails paths
|
|
32
|
+
|
|
33
|
+
Slash-separated and space-separated forms are equivalent:
|
|
34
|
+
```sh
|
|
35
|
+
rn c admin users
|
|
36
|
+
rn c admin/users
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Supported Kinds
|
|
40
|
+
The command feel is intentionally close to Rails generator conventions.
|
|
41
|
+
* `model` / `m`
|
|
42
|
+
* `controller` / `c`
|
|
43
|
+
* `helper` / `h`
|
|
44
|
+
* `view` / `v`
|
|
45
|
+
* `job` / `j`
|
|
46
|
+
* `service`
|
|
47
|
+
* `mailer`
|
|
48
|
+
* `channel`
|
|
49
|
+
* `public`
|
|
50
|
+
|
|
51
|
+
## Output Modes
|
|
52
|
+
```sh
|
|
53
|
+
rn user --print
|
|
54
|
+
rn user --json
|
|
55
|
+
```
|
|
56
|
+
`--print` prints the best path only.
|
|
57
|
+
`--json` prints scored candidate data.
|
|
58
|
+
|
|
59
|
+
## Search Scope
|
|
60
|
+
`rn` only searches Rails-relevant paths such as:
|
|
61
|
+
* `app/models`
|
|
62
|
+
* `app/controllers`
|
|
63
|
+
* `app/helpers`
|
|
64
|
+
* `app/views`
|
|
65
|
+
* `app/services`
|
|
66
|
+
* `app/jobs`
|
|
67
|
+
* `app/mailers`
|
|
68
|
+
* `app/channels`
|
|
69
|
+
* `public`
|
|
70
|
+
|
|
71
|
+
It does not search log files, `tmp`, `vendor`, `node_modules`, or arbitrary file contents.
|
|
72
|
+
|
|
73
|
+
## Philosophy
|
|
74
|
+
* resolver, not search engine
|
|
75
|
+
* Rails-aware
|
|
76
|
+
* finite candidate set
|
|
77
|
+
* predictable heuristics
|
|
78
|
+
* generator-like ergonomics
|
|
79
|
+
|
|
80
|
+
## Non-Goals
|
|
81
|
+
* no grep-style full text search
|
|
82
|
+
* no TUI yet
|
|
83
|
+
* no LSP integration
|
|
84
|
+
* no log viewer
|
|
85
|
+
|
|
86
|
+
## Future
|
|
87
|
+
Possible future work includes `rni`, better ranking, and optional indexing.
|
data/exe/rn
ADDED
data/exe/rni
ADDED
data/lib/rightnow/cli.rb
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "shellwords"
|
|
5
|
+
|
|
6
|
+
module RightNow
|
|
7
|
+
class CLI
|
|
8
|
+
def self.start(argv = ARGV)
|
|
9
|
+
new(argv).run
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def initialize(argv)
|
|
13
|
+
@query = Query.parse(argv)
|
|
14
|
+
@resolver = Resolver.new
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def run
|
|
18
|
+
return handle_empty_query if query.empty?
|
|
19
|
+
|
|
20
|
+
results = resolver.resolve(query)
|
|
21
|
+
|
|
22
|
+
if query.json
|
|
23
|
+
puts JSON.pretty_generate(results)
|
|
24
|
+
return 0
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
if query.print
|
|
28
|
+
best = results.first
|
|
29
|
+
if best
|
|
30
|
+
puts best[:path]
|
|
31
|
+
return 0
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
return 1
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
return open_in_editor(results.first[:path]) ? 0 : 1 if results.any?
|
|
38
|
+
|
|
39
|
+
1
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
attr_reader :query, :resolver
|
|
45
|
+
|
|
46
|
+
def handle_empty_query
|
|
47
|
+
if query.json
|
|
48
|
+
puts JSON.pretty_generate([])
|
|
49
|
+
return 0
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
1
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def open_in_editor(path)
|
|
56
|
+
command = Shellwords.split(editor_command)
|
|
57
|
+
return false if command.empty?
|
|
58
|
+
|
|
59
|
+
system(*command, File.join(resolver.root, path))
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def editor_command
|
|
63
|
+
ENV["CR_EDITOR"] || ENV["VISUAL"] || ENV["EDITOR"] || "vim"
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
|
|
5
|
+
module RightNow
|
|
6
|
+
class Query
|
|
7
|
+
KIND_ALIASES = {
|
|
8
|
+
"m" => "model",
|
|
9
|
+
"model" => "model",
|
|
10
|
+
"c" => "controller",
|
|
11
|
+
"controller" => "controller",
|
|
12
|
+
"h" => "helper",
|
|
13
|
+
"helper" => "helper",
|
|
14
|
+
"v" => "view",
|
|
15
|
+
"view" => "view",
|
|
16
|
+
"j" => "job",
|
|
17
|
+
"job" => "job",
|
|
18
|
+
"service" => "service",
|
|
19
|
+
"mailer" => "mailer",
|
|
20
|
+
"channel" => "channel",
|
|
21
|
+
"public" => "public"
|
|
22
|
+
}.freeze
|
|
23
|
+
|
|
24
|
+
attr_reader :print, :json, :kind, :terms, :path_mode, :original_tokens
|
|
25
|
+
|
|
26
|
+
def self.parse(argv)
|
|
27
|
+
options = { print: false, json: false }
|
|
28
|
+
|
|
29
|
+
parser = OptionParser.new do |opts|
|
|
30
|
+
opts.on("--print", "Print only the best path") { options[:print] = true }
|
|
31
|
+
opts.on("--json", "Print top candidates as JSON") { options[:json] = true }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
original_tokens = parser.parse(argv.dup)
|
|
35
|
+
normalized_tokens = original_tokens.flat_map { |token| token.split("/") }.reject(&:empty?)
|
|
36
|
+
path_mode = original_tokens.any? { |token| token.include?("/") }
|
|
37
|
+
|
|
38
|
+
kind = nil
|
|
39
|
+
terms = normalized_tokens.dup
|
|
40
|
+
|
|
41
|
+
if (first = terms.first) && KIND_ALIASES.key?(first.downcase)
|
|
42
|
+
kind = KIND_ALIASES.fetch(first.downcase)
|
|
43
|
+
terms = terms.drop(1)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
if !path_mode
|
|
47
|
+
terms = terms.select { |term| term.length >= 2 }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
new(
|
|
51
|
+
print: options[:print],
|
|
52
|
+
json: options[:json],
|
|
53
|
+
kind: kind,
|
|
54
|
+
terms: terms,
|
|
55
|
+
path_mode: path_mode,
|
|
56
|
+
original_tokens: original_tokens
|
|
57
|
+
)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def initialize(print:, json:, kind:, terms:, path_mode:, original_tokens:)
|
|
61
|
+
@print = print
|
|
62
|
+
@json = json
|
|
63
|
+
@kind = kind
|
|
64
|
+
@terms = terms
|
|
65
|
+
@path_mode = path_mode
|
|
66
|
+
@original_tokens = original_tokens
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def empty?
|
|
70
|
+
terms.empty? && kind.nil? && !path_mode
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def query_path
|
|
74
|
+
@query_path ||= normalized_path(terms)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
def normalized_path(parts)
|
|
80
|
+
parts.join("/").downcase
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RightNow
|
|
4
|
+
class Resolver
|
|
5
|
+
SEARCH_SCOPES = {
|
|
6
|
+
"model" => "app/models",
|
|
7
|
+
"controller" => "app/controllers",
|
|
8
|
+
"helper" => "app/helpers",
|
|
9
|
+
"view" => "app/views",
|
|
10
|
+
"job" => "app/jobs",
|
|
11
|
+
"service" => "app/services",
|
|
12
|
+
"mailer" => "app/mailers",
|
|
13
|
+
"channel" => "app/channels",
|
|
14
|
+
"public" => "public"
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
attr_reader :root
|
|
18
|
+
|
|
19
|
+
def initialize(root: detect_root)
|
|
20
|
+
@root = root
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def resolve(query)
|
|
24
|
+
candidates = collect_candidates(query)
|
|
25
|
+
ranked = candidates.map { |path| score_candidate(path, query) }
|
|
26
|
+
.sort_by { |candidate| [-candidate[:score], candidate[:path]] }
|
|
27
|
+
ranked.first(10)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def best_candidate(query)
|
|
31
|
+
resolve(query).first
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def detect_root
|
|
35
|
+
current = Dir.pwd
|
|
36
|
+
|
|
37
|
+
loop do
|
|
38
|
+
return current if rails_project_root?(current)
|
|
39
|
+
|
|
40
|
+
parent = File.dirname(current)
|
|
41
|
+
return Dir.pwd if parent == current
|
|
42
|
+
|
|
43
|
+
current = parent
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def collect_candidates(query)
|
|
50
|
+
scopes = query.kind ? [SEARCH_SCOPES.fetch(query.kind)] : SEARCH_SCOPES.values
|
|
51
|
+
scopes.flat_map { |scope| glob_scope(scope) }.uniq
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def glob_scope(scope)
|
|
55
|
+
pattern = File.join(root, scope, "**", "*")
|
|
56
|
+
Dir.glob(pattern, File::FNM_DOTMATCH)
|
|
57
|
+
.select { |path| File.file?(path) }
|
|
58
|
+
.map { |path| relative_path(path) }
|
|
59
|
+
.reject { |path| excluded_path?(path) }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def excluded_path?(path)
|
|
63
|
+
path.split(File::SEPARATOR).any? do |segment|
|
|
64
|
+
%w[node_modules vendor tmp log .git].include?(segment)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def relative_path(path)
|
|
69
|
+
path.sub(%r{\A#{Regexp.escape(root)}/?}, "")
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def score_candidate(path, query)
|
|
73
|
+
score = 0
|
|
74
|
+
downcased_path = path.downcase
|
|
75
|
+
terms = query.terms.map(&:downcase)
|
|
76
|
+
kind_scope = query.kind && path.start_with?(SEARCH_SCOPES.fetch(query.kind) + "/")
|
|
77
|
+
|
|
78
|
+
score += 200 if kind_scope
|
|
79
|
+
score += exact_path_score(path, query)
|
|
80
|
+
score += basename_score(path, query)
|
|
81
|
+
score += ordered_terms_score(downcased_path, terms)
|
|
82
|
+
score += unordered_terms_score(downcased_path, terms)
|
|
83
|
+
score += kind_hint_score(path, query)
|
|
84
|
+
score += path_mode_score(downcased_path, query)
|
|
85
|
+
|
|
86
|
+
{ path:, score: }
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def exact_path_score(path, query)
|
|
90
|
+
return 0 if query.query_path.empty?
|
|
91
|
+
|
|
92
|
+
downcased = path.downcase
|
|
93
|
+
if downcased == query.query_path
|
|
94
|
+
1_500
|
|
95
|
+
elsif downcased.end_with?(query.query_path)
|
|
96
|
+
1_000
|
|
97
|
+
elsif downcased.include?(query.query_path)
|
|
98
|
+
700
|
|
99
|
+
else
|
|
100
|
+
0
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def basename_score(path, query)
|
|
105
|
+
return 0 if query.terms.empty?
|
|
106
|
+
|
|
107
|
+
basename = File.basename(path, File.extname(path)).downcase
|
|
108
|
+
expected = expected_basename(query)
|
|
109
|
+
|
|
110
|
+
if expected && basename == expected
|
|
111
|
+
500
|
|
112
|
+
elsif query.terms.map(&:downcase).include?(basename)
|
|
113
|
+
250
|
|
114
|
+
else
|
|
115
|
+
0
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def ordered_terms_score(path, terms)
|
|
120
|
+
return 0 if terms.empty?
|
|
121
|
+
return 300 if subsequence?(terms, path_terms(path))
|
|
122
|
+
|
|
123
|
+
0
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def unordered_terms_score(path, terms)
|
|
127
|
+
return 0 if terms.empty?
|
|
128
|
+
return 100 if terms.all? { |term| path.include?(term) }
|
|
129
|
+
|
|
130
|
+
0
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def kind_hint_score(path, query)
|
|
134
|
+
return 0 unless query.kind
|
|
135
|
+
|
|
136
|
+
expected = expected_kind_path(query)
|
|
137
|
+
return 350 if expected && path.start_with?(expected)
|
|
138
|
+
|
|
139
|
+
0
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def path_mode_score(path, query)
|
|
143
|
+
return 0 unless query.path_mode
|
|
144
|
+
return 500 if path.include?(query.query_path)
|
|
145
|
+
|
|
146
|
+
0
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def expected_basename(query)
|
|
150
|
+
return nil if query.terms.empty?
|
|
151
|
+
|
|
152
|
+
case query.kind
|
|
153
|
+
when "model"
|
|
154
|
+
"#{singularize(query.terms.join("_"))}"
|
|
155
|
+
when "controller"
|
|
156
|
+
"#{pluralize(query.terms.last)}_controller"
|
|
157
|
+
when "helper"
|
|
158
|
+
"#{query.terms.last}_helper"
|
|
159
|
+
when "view"
|
|
160
|
+
query.terms.last
|
|
161
|
+
when "job"
|
|
162
|
+
"#{query.terms.join("_")}_job"
|
|
163
|
+
when "service"
|
|
164
|
+
"#{query.terms.join("_")}_service"
|
|
165
|
+
when "mailer"
|
|
166
|
+
"#{query.terms.join("_")}_mailer"
|
|
167
|
+
when "channel"
|
|
168
|
+
"#{query.terms.join("_")}_channel"
|
|
169
|
+
when "public"
|
|
170
|
+
query.terms.join("/")
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def expected_kind_path(query)
|
|
175
|
+
return nil if query.terms.empty?
|
|
176
|
+
|
|
177
|
+
case query.kind
|
|
178
|
+
when "model"
|
|
179
|
+
File.join("app/models", "#{expected_basename(query)}.rb")
|
|
180
|
+
when "controller"
|
|
181
|
+
dir = query.terms.length > 1 ? query.terms[0..-2].join("/") : ""
|
|
182
|
+
base = "#{pluralize(query.terms.last)}_controller.rb"
|
|
183
|
+
dir.empty? ? File.join("app/controllers", base) : File.join("app/controllers", dir, base)
|
|
184
|
+
when "helper"
|
|
185
|
+
dir = query.terms.length > 1 ? query.terms[0..-2].join("/") : ""
|
|
186
|
+
base = "#{query.terms.last}_helper.rb"
|
|
187
|
+
dir.empty? ? File.join("app/helpers", base) : File.join("app/helpers", dir, base)
|
|
188
|
+
when "view"
|
|
189
|
+
dir = query.terms.length > 1 ? query.terms[0..-2].join("/") : ""
|
|
190
|
+
base = query.terms.last
|
|
191
|
+
dir.empty? ? File.join("app/views", base) : File.join("app/views", dir, base)
|
|
192
|
+
when "job"
|
|
193
|
+
File.join("app/jobs", "#{query.terms.join("_")}_job.rb")
|
|
194
|
+
when "service"
|
|
195
|
+
File.join("app/services", "#{query.terms.join("_")}_service.rb")
|
|
196
|
+
when "mailer"
|
|
197
|
+
File.join("app/mailers", "#{query.terms.join("_")}_mailer.rb")
|
|
198
|
+
when "channel"
|
|
199
|
+
File.join("app/channels", "#{query.terms.join("_")}_channel.rb")
|
|
200
|
+
when "public"
|
|
201
|
+
File.join("public", query.terms.join("/"))
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def path_terms(path)
|
|
206
|
+
path.downcase.split(/[\/_.-]+/).reject(&:empty?)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def subsequence?(needles, haystack)
|
|
210
|
+
index = 0
|
|
211
|
+
needles.all? do |needle|
|
|
212
|
+
found = haystack[index..].find_index { |part| part.include?(needle) }
|
|
213
|
+
next false unless found
|
|
214
|
+
|
|
215
|
+
index += found + 1
|
|
216
|
+
true
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def pluralize(term)
|
|
221
|
+
return term if term.end_with?("s") && !term.end_with?("ss")
|
|
222
|
+
return term.sub(/y\z/, "ies") if term.match?(/[^aeiou]y\z/)
|
|
223
|
+
return "#{term}es" if term.match?(/(s|x|z|ch|sh)\z/)
|
|
224
|
+
|
|
225
|
+
"#{term}s"
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def singularize(term)
|
|
229
|
+
return term.sub(/ies\z/, "y") if term.end_with?("ies")
|
|
230
|
+
return term.sub(/ses\z/, "s") if term.end_with?("sses")
|
|
231
|
+
return term.delete_suffix("s") if term.end_with?("s") && !term.end_with?("ss") && !term.end_with?("us") && !term.end_with?("is")
|
|
232
|
+
|
|
233
|
+
term
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def rails_project_root?(path)
|
|
237
|
+
File.directory?(File.join(path, "app")) && File.exist?(File.join(path, "config", "application.rb"))
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
end
|
data/lib/rightnow.rb
ADDED
data/right_now.gemspec
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
require_relative "lib/rightnow/version"
|
|
2
|
+
|
|
3
|
+
Gem::Specification.new do |spec|
|
|
4
|
+
spec.name = "right_now"
|
|
5
|
+
spec.version = RightNow::VERSION
|
|
6
|
+
spec.summary = "Rails-aware file resolver"
|
|
7
|
+
spec.description = "rn resolves a user query to the most likely existing file in a Rails project."
|
|
8
|
+
spec.authors = ["SEAHAL"]
|
|
9
|
+
spec.homepage = "https://github.com/seahal/right_now"
|
|
10
|
+
spec.license = "MIT"
|
|
11
|
+
spec.required_ruby_version = Gem::Requirement.new(">= 3.0.0")
|
|
12
|
+
|
|
13
|
+
spec.files = (Dir.glob("{exe,lib}/**/*", File::FNM_DOTMATCH) +
|
|
14
|
+
%w[README.md LICENSE right_now.gemspec]).select { |path| File.file?(path) }
|
|
15
|
+
spec.bindir = "exe"
|
|
16
|
+
spec.executables = %w[rn rni]
|
|
17
|
+
spec.require_paths = ["lib"]
|
|
18
|
+
|
|
19
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
|
20
|
+
spec.metadata["source_code_uri"] = spec.homepage
|
|
21
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: right_now
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- SEAHAL
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: rn resolves a user query to the most likely existing file in a Rails
|
|
13
|
+
project.
|
|
14
|
+
executables:
|
|
15
|
+
- rn
|
|
16
|
+
- rni
|
|
17
|
+
extensions: []
|
|
18
|
+
extra_rdoc_files: []
|
|
19
|
+
files:
|
|
20
|
+
- LICENSE
|
|
21
|
+
- README.md
|
|
22
|
+
- exe/rn
|
|
23
|
+
- exe/rni
|
|
24
|
+
- lib/rightnow.rb
|
|
25
|
+
- lib/rightnow/cli.rb
|
|
26
|
+
- lib/rightnow/query.rb
|
|
27
|
+
- lib/rightnow/resolver.rb
|
|
28
|
+
- lib/rightnow/version.rb
|
|
29
|
+
- right_now.gemspec
|
|
30
|
+
homepage: https://github.com/seahal/right_now
|
|
31
|
+
licenses:
|
|
32
|
+
- MIT
|
|
33
|
+
metadata:
|
|
34
|
+
homepage_uri: https://github.com/seahal/right_now
|
|
35
|
+
source_code_uri: https://github.com/seahal/right_now
|
|
36
|
+
rdoc_options: []
|
|
37
|
+
require_paths:
|
|
38
|
+
- lib
|
|
39
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
40
|
+
requirements:
|
|
41
|
+
- - ">="
|
|
42
|
+
- !ruby/object:Gem::Version
|
|
43
|
+
version: 3.0.0
|
|
44
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
45
|
+
requirements:
|
|
46
|
+
- - ">="
|
|
47
|
+
- !ruby/object:Gem::Version
|
|
48
|
+
version: '0'
|
|
49
|
+
requirements: []
|
|
50
|
+
rubygems_version: 4.0.6
|
|
51
|
+
specification_version: 4
|
|
52
|
+
summary: Rails-aware file resolver
|
|
53
|
+
test_files: []
|