whoosh 1.5.0 → 1.6.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 +4 -4
- data/README.md +40 -1
- data/lib/whoosh/cli/client_generator.rb +237 -0
- data/lib/whoosh/cli/main.rb +10 -0
- data/lib/whoosh/client_gen/base_generator.rb +84 -0
- data/lib/whoosh/client_gen/dependency_checker.rb +49 -0
- data/lib/whoosh/client_gen/fallback_backend.rb +292 -0
- data/lib/whoosh/client_gen/generators/expo.rb +1038 -0
- data/lib/whoosh/client_gen/generators/flutter.rb +915 -0
- data/lib/whoosh/client_gen/generators/htmx.rb +498 -0
- data/lib/whoosh/client_gen/generators/ios.rb +832 -0
- data/lib/whoosh/client_gen/generators/react_spa.rb +932 -0
- data/lib/whoosh/client_gen/generators/telegram_bot.rb +624 -0
- data/lib/whoosh/client_gen/generators/telegram_mini_app.rb +844 -0
- data/lib/whoosh/client_gen/introspector.rb +178 -0
- data/lib/whoosh/client_gen/ir.rb +37 -0
- data/lib/whoosh/version.rb +1 -1
- metadata +14 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f93acb5a2f85aecd9f3feafc165962aa721a762dad30caa2d2550ed500afbec8
|
|
4
|
+
data.tar.gz: e44f3b1217d9177417b21820abd6d1788969556603c2d9aff7f26d7351c703d2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 77dd95d3cefc8053ff9c183207273adc8b99e4df806ad1374b277dfde6a64ffb31f82765c502c05d81eb4d4a9b4f6d681a2ffb0ebaf75353369e5179eeb17758
|
|
7
|
+
data.tar.gz: ea988031ce4f4dd47959bd84782caa4ad019e7570f5cd2d17cba754b3851955a4520c7d5ee76815b53da7e381a9fde0a833895a7cc59386e2354dfc6f7604f74
|
data/README.md
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
<img src="https://img.shields.io/badge/ruby-%3E%3D%203.4.0-red" alt="Ruby">
|
|
14
14
|
<img src="https://img.shields.io/badge/rack-3.0-blue" alt="Rack">
|
|
15
15
|
<img src="https://img.shields.io/badge/license-MIT-green" alt="License">
|
|
16
|
-
<img src="https://img.shields.io/badge/tests-
|
|
16
|
+
<img src="https://img.shields.io/badge/tests-659%20passing-brightgreen" alt="Tests">
|
|
17
17
|
<img src="https://img.shields.io/badge/overhead-2.5%C2%B5s-orange" alt="Performance">
|
|
18
18
|
</p>
|
|
19
19
|
|
|
@@ -346,6 +346,43 @@ app.docs enabled: true, redoc: true
|
|
|
346
346
|
- `/redoc` — ReDoc
|
|
347
347
|
- `/openapi.json` — Machine-readable spec
|
|
348
348
|
|
|
349
|
+
### Client Generator
|
|
350
|
+
|
|
351
|
+
Generate complete, typed, ready-to-run client apps from your Whoosh API — one command.
|
|
352
|
+
|
|
353
|
+
```sh
|
|
354
|
+
whoosh generate client react_spa # React + Vite + TypeScript
|
|
355
|
+
whoosh generate client expo # Expo + React Native
|
|
356
|
+
whoosh generate client ios # SwiftUI + MVVM
|
|
357
|
+
whoosh generate client flutter # Dart + Riverpod + GoRouter
|
|
358
|
+
whoosh generate client htmx # Plain HTML + htmx, no build step
|
|
359
|
+
whoosh generate client telegram_bot # Ruby Telegram bot
|
|
360
|
+
whoosh generate client telegram_mini_app # React + Telegram WebApp SDK
|
|
361
|
+
|
|
362
|
+
whoosh generate client react_spa --oauth # Add Google/GitHub/Apple login
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
The generator **introspects your Whoosh app** via OpenAPI — it reads your routes, schemas, and auth config, then produces a typed client with:
|
|
366
|
+
|
|
367
|
+
- API client with auth headers and automatic token refresh
|
|
368
|
+
- Model types matching your schemas
|
|
369
|
+
- Auth screens (login, register, logout)
|
|
370
|
+
- CRUD screens for every resource
|
|
371
|
+
- Navigation and routing
|
|
372
|
+
- Starter tests
|
|
373
|
+
|
|
374
|
+
If no Whoosh app exists yet, it scaffolds a standard backend (JWT auth + tasks CRUD) alongside the client.
|
|
375
|
+
|
|
376
|
+
| Client | Stack | Token Storage |
|
|
377
|
+
|--------|-------|---------------|
|
|
378
|
+
| `react_spa` | React 19, Vite, TypeScript, React Router | localStorage |
|
|
379
|
+
| `expo` | Expo SDK 52, Expo Router, TypeScript | SecureStore |
|
|
380
|
+
| `ios` | SwiftUI, async/await, MVVM | Keychain |
|
|
381
|
+
| `flutter` | Dart, Dio, Riverpod, GoRouter | flutter_secure_storage |
|
|
382
|
+
| `htmx` | HTML, htmx 2.x, vanilla JS | localStorage |
|
|
383
|
+
| `telegram_bot` | Ruby, telegram-bot-ruby | In-memory session |
|
|
384
|
+
| `telegram_mini_app` | React, Telegram WebApp SDK | Telegram initData |
|
|
385
|
+
|
|
349
386
|
### Health Checks
|
|
350
387
|
|
|
351
388
|
```ruby
|
|
@@ -377,6 +414,8 @@ whoosh generate model User name:string email:string
|
|
|
377
414
|
whoosh generate migration add_email_to_users
|
|
378
415
|
whoosh generate plugin my_tool # plugin boilerplate
|
|
379
416
|
whoosh generate proto ChatRequest # .proto file
|
|
417
|
+
whoosh generate client react_spa # full client app (7 types)
|
|
418
|
+
whoosh generate client expo --oauth # with OAuth2 social login
|
|
380
419
|
|
|
381
420
|
whoosh db migrate # run migrations
|
|
382
421
|
whoosh db rollback # rollback
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "whoosh/client_gen/ir"
|
|
4
|
+
require "whoosh/client_gen/introspector"
|
|
5
|
+
require "whoosh/client_gen/base_generator"
|
|
6
|
+
require "whoosh/client_gen/dependency_checker"
|
|
7
|
+
require "whoosh/client_gen/fallback_backend"
|
|
8
|
+
|
|
9
|
+
module Whoosh
|
|
10
|
+
module ClientGen
|
|
11
|
+
class Error < StandardError; end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
module CLI
|
|
15
|
+
class ClientGenerator
|
|
16
|
+
CLIENT_TYPES = %i[react_spa expo ios flutter htmx telegram_bot telegram_mini_app].freeze
|
|
17
|
+
|
|
18
|
+
attr_reader :type, :oauth, :output_dir
|
|
19
|
+
|
|
20
|
+
def self.client_types
|
|
21
|
+
CLIENT_TYPES
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def initialize(type:, oauth:, dir:, root: Dir.pwd)
|
|
25
|
+
@type = type.to_sym
|
|
26
|
+
@oauth = oauth
|
|
27
|
+
@output_dir = dir || default_output_dir
|
|
28
|
+
@root = root
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def run
|
|
32
|
+
validate!
|
|
33
|
+
check_dependencies!
|
|
34
|
+
result = introspect_or_fallback
|
|
35
|
+
|
|
36
|
+
case result[:mode]
|
|
37
|
+
when :introspected
|
|
38
|
+
display_found(result[:ir])
|
|
39
|
+
ir = confirm_selection(result[:ir])
|
|
40
|
+
generate_client(ir)
|
|
41
|
+
when :fallback
|
|
42
|
+
display_fallback_prompt
|
|
43
|
+
generate_fallback_backend
|
|
44
|
+
ir = build_fallback_ir
|
|
45
|
+
generate_client(ir)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
display_success
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def validate!
|
|
52
|
+
unless CLIENT_TYPES.include?(@type)
|
|
53
|
+
raise ClientGen::Error, "Unknown client type: #{@type}. Supported: #{CLIENT_TYPES.join(", ")}"
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def default_output_dir
|
|
58
|
+
"clients/#{@type}"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def check_dependencies!
|
|
62
|
+
result = ClientGen::DependencyChecker.check(@type)
|
|
63
|
+
return if result[:ok]
|
|
64
|
+
|
|
65
|
+
puts "\n⚠️ Missing dependencies for #{@type}:"
|
|
66
|
+
result[:missing].each do |dep|
|
|
67
|
+
msg = " - #{dep[:cmd]} (check: #{dep[:check]})"
|
|
68
|
+
msg += " — found v#{dep[:found_version]}, need v#{dep[:min_version]}+" if dep[:found_version]
|
|
69
|
+
puts msg
|
|
70
|
+
end
|
|
71
|
+
puts "\nInstall the missing dependencies and try again."
|
|
72
|
+
exit 1
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def introspect_or_fallback
|
|
76
|
+
app = load_app
|
|
77
|
+
if app
|
|
78
|
+
introspector = ClientGen::Introspector.new(app, base_url: detect_base_url(app))
|
|
79
|
+
ir = introspector.introspect
|
|
80
|
+
if ir.has_resources? || ir.has_auth?
|
|
81
|
+
{ mode: :introspected, ir: ir }
|
|
82
|
+
else
|
|
83
|
+
{ mode: :fallback }
|
|
84
|
+
end
|
|
85
|
+
else
|
|
86
|
+
{ mode: :fallback }
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
def load_app
|
|
93
|
+
app_file = File.join(@root, "app.rb")
|
|
94
|
+
return nil unless File.exist?(app_file)
|
|
95
|
+
|
|
96
|
+
require app_file
|
|
97
|
+
ObjectSpace.each_object(Whoosh::App).first
|
|
98
|
+
rescue => e
|
|
99
|
+
puts "⚠️ Failed to load app: #{e.message}"
|
|
100
|
+
puts "Run `whoosh check` to debug."
|
|
101
|
+
nil
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def detect_base_url(app)
|
|
105
|
+
config = app.instance_variable_get(:@config)
|
|
106
|
+
port = config&.respond_to?(:port) ? config.port : 9292
|
|
107
|
+
host = config&.respond_to?(:host) ? config.host : "localhost"
|
|
108
|
+
"http://#{host}:#{port}"
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def display_found(ir)
|
|
112
|
+
puts "\n🔍 Inspecting Whoosh app...\n\n"
|
|
113
|
+
puts "Found:"
|
|
114
|
+
puts " Auth: #{ir.auth&.type || "none"}"
|
|
115
|
+
ir.resources.each do |r|
|
|
116
|
+
puts " Resource: #{r.name} (#{r.endpoints.length} endpoints)"
|
|
117
|
+
end
|
|
118
|
+
ir.streaming.each do |s|
|
|
119
|
+
puts " Streaming: #{s[:type]} on #{s[:path]}"
|
|
120
|
+
end
|
|
121
|
+
puts
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def confirm_selection(ir)
|
|
125
|
+
ir
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def display_fallback_prompt
|
|
129
|
+
puts "\n⚠️ No Whoosh app found (or no routes defined).\n\n"
|
|
130
|
+
puts "Generating standard starter with:"
|
|
131
|
+
puts " - JWT auth (email/password login + register)"
|
|
132
|
+
puts " - Tasks CRUD (title, description, status, due_date)"
|
|
133
|
+
puts " - Matching backend endpoints"
|
|
134
|
+
if @oauth
|
|
135
|
+
puts " - OAuth2 (Google, GitHub, Apple)"
|
|
136
|
+
end
|
|
137
|
+
puts
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def generate_fallback_backend
|
|
141
|
+
ClientGen::FallbackBackend.generate(root: @root, oauth: @oauth)
|
|
142
|
+
puts "✅ Backend endpoints generated"
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def build_fallback_ir
|
|
146
|
+
ClientGen::IR::AppSpec.new(
|
|
147
|
+
auth: ClientGen::IR::Auth.new(
|
|
148
|
+
type: :jwt,
|
|
149
|
+
endpoints: {
|
|
150
|
+
login: { method: :post, path: "/auth/login" },
|
|
151
|
+
register: { method: :post, path: "/auth/register" },
|
|
152
|
+
refresh: { method: :post, path: "/auth/refresh" },
|
|
153
|
+
logout: { method: :delete, path: "/auth/logout" },
|
|
154
|
+
me: { method: :get, path: "/auth/me" }
|
|
155
|
+
},
|
|
156
|
+
oauth_providers: @oauth ? %i[google github apple] : []
|
|
157
|
+
),
|
|
158
|
+
resources: [
|
|
159
|
+
ClientGen::IR::Resource.new(
|
|
160
|
+
name: :tasks,
|
|
161
|
+
endpoints: [
|
|
162
|
+
ClientGen::IR::Endpoint.new(method: :get, path: "/tasks", action: :index, pagination: true),
|
|
163
|
+
ClientGen::IR::Endpoint.new(method: :get, path: "/tasks/:id", action: :show),
|
|
164
|
+
ClientGen::IR::Endpoint.new(method: :post, path: "/tasks", action: :create),
|
|
165
|
+
ClientGen::IR::Endpoint.new(method: :put, path: "/tasks/:id", action: :update),
|
|
166
|
+
ClientGen::IR::Endpoint.new(method: :delete, path: "/tasks/:id", action: :destroy)
|
|
167
|
+
],
|
|
168
|
+
fields: [
|
|
169
|
+
{ name: :title, type: :string, required: true },
|
|
170
|
+
{ name: :description, type: :string, required: false },
|
|
171
|
+
{ name: :status, type: :string, required: false, enum: %w[pending in_progress done], default: "pending" },
|
|
172
|
+
{ name: :due_date, type: :string, required: false }
|
|
173
|
+
]
|
|
174
|
+
)
|
|
175
|
+
],
|
|
176
|
+
streaming: [],
|
|
177
|
+
base_url: "http://localhost:9292"
|
|
178
|
+
)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def generate_client(ir)
|
|
182
|
+
generator_class = load_generator_class
|
|
183
|
+
output = File.join(@root, @output_dir)
|
|
184
|
+
|
|
185
|
+
if Dir.exist?(output) && !Dir.empty?(output)
|
|
186
|
+
puts "⚠️ #{@output_dir}/ already exists."
|
|
187
|
+
print "Overwrite? [y/N] "
|
|
188
|
+
answer = $stdin.gets&.strip&.downcase
|
|
189
|
+
unless answer == "y"
|
|
190
|
+
puts "Aborted."
|
|
191
|
+
exit 0
|
|
192
|
+
end
|
|
193
|
+
FileUtils.rm_rf(output)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
generator_class.new(ir: ir, output_dir: output, platform: platform_for_type).generate
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def load_generator_class
|
|
200
|
+
require "whoosh/client_gen/generators/#{@type}"
|
|
201
|
+
Whoosh::ClientGen::Generators.const_get(camelize(@type.to_s))
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def platform_for_type
|
|
205
|
+
case @type
|
|
206
|
+
when :react_spa, :expo, :telegram_mini_app then :typescript
|
|
207
|
+
when :ios then :swift
|
|
208
|
+
when :flutter then :dart
|
|
209
|
+
when :htmx then :html
|
|
210
|
+
when :telegram_bot then :ruby
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def camelize(str)
|
|
215
|
+
str.split("_").map(&:capitalize).join
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def display_success
|
|
219
|
+
puts "\n✅ Generated #{@type} client in #{@output_dir}/"
|
|
220
|
+
case @type
|
|
221
|
+
when :react_spa, :telegram_mini_app
|
|
222
|
+
puts " Run: cd #{@output_dir} && npm install && npm run dev"
|
|
223
|
+
when :expo
|
|
224
|
+
puts " Run: cd #{@output_dir} && npm install && npx expo start"
|
|
225
|
+
when :ios
|
|
226
|
+
puts " Run: open #{@output_dir}/WhooshApp.xcodeproj"
|
|
227
|
+
when :flutter
|
|
228
|
+
puts " Run: cd #{@output_dir} && flutter pub get && flutter run"
|
|
229
|
+
when :htmx
|
|
230
|
+
puts " Run: cd #{@output_dir} && open index.html (or any static server)"
|
|
231
|
+
when :telegram_bot
|
|
232
|
+
puts " Run: cd #{@output_dir} && bundle install && ruby bot.rb"
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
end
|
data/lib/whoosh/cli/main.rb
CHANGED
|
@@ -600,6 +600,16 @@ module Whoosh
|
|
|
600
600
|
require "whoosh/cli/generators"
|
|
601
601
|
Whoosh::CLI::Generators.proto(name)
|
|
602
602
|
end
|
|
603
|
+
|
|
604
|
+
desc "client TYPE", "Generate a client app (react_spa, expo, ios, flutter, htmx, telegram_bot, telegram_mini_app)"
|
|
605
|
+
option :oauth, type: :boolean, default: false, desc: "Include OAuth2 social login"
|
|
606
|
+
option :dir, type: :string, desc: "Output directory (default: clients/<type>)"
|
|
607
|
+
def client(type)
|
|
608
|
+
require "whoosh/cli/client_generator"
|
|
609
|
+
Whoosh::CLI::ClientGenerator.new(
|
|
610
|
+
type: type, oauth: options[:oauth], dir: options[:dir]
|
|
611
|
+
).run
|
|
612
|
+
end
|
|
603
613
|
}
|
|
604
614
|
end
|
|
605
615
|
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# lib/whoosh/client_gen/base_generator.rb
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require "whoosh/client_gen/ir"
|
|
6
|
+
|
|
7
|
+
module Whoosh
|
|
8
|
+
module ClientGen
|
|
9
|
+
class BaseGenerator
|
|
10
|
+
TYPE_MAPS = {
|
|
11
|
+
typescript: {
|
|
12
|
+
string: "string", integer: "number", number: "number",
|
|
13
|
+
boolean: "boolean", array: "any[]", object: "Record<string, any>"
|
|
14
|
+
},
|
|
15
|
+
swift: {
|
|
16
|
+
string: "String", integer: "Int", number: "Double",
|
|
17
|
+
boolean: "Bool", array: "[Any]", object: "[String: Any]"
|
|
18
|
+
},
|
|
19
|
+
dart: {
|
|
20
|
+
string: "String", integer: "int", number: "double",
|
|
21
|
+
boolean: "bool", array: "List<dynamic>", object: "Map<String, dynamic>"
|
|
22
|
+
},
|
|
23
|
+
ruby: {
|
|
24
|
+
string: "String", integer: "Integer", number: "Float",
|
|
25
|
+
boolean: "Boolean", array: "Array", object: "Hash"
|
|
26
|
+
},
|
|
27
|
+
html: {
|
|
28
|
+
string: "text", integer: "number", number: "number",
|
|
29
|
+
boolean: "checkbox", array: "text", object: "text"
|
|
30
|
+
}
|
|
31
|
+
}.freeze
|
|
32
|
+
|
|
33
|
+
attr_reader :ir, :output_dir, :platform
|
|
34
|
+
|
|
35
|
+
def initialize(ir:, output_dir:, platform:)
|
|
36
|
+
@ir = ir
|
|
37
|
+
@output_dir = output_dir
|
|
38
|
+
@platform = platform
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def generate
|
|
42
|
+
raise NotImplementedError, "Subclasses must implement #generate"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def type_for(ir_type)
|
|
46
|
+
TYPE_MAPS.dig(@platform, ir_type.to_sym) || "string"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def write_file(relative_path, content)
|
|
50
|
+
full_path = File.join(@output_dir, relative_path)
|
|
51
|
+
FileUtils.mkdir_p(File.dirname(full_path))
|
|
52
|
+
File.write(full_path, content)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def classify(name)
|
|
56
|
+
singular = singularize(name.to_s)
|
|
57
|
+
singular.split(/[-_]/).map(&:capitalize).join
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def singularize(word)
|
|
61
|
+
w = word.to_s
|
|
62
|
+
if w.end_with?("ies")
|
|
63
|
+
w[0..-4] + "y"
|
|
64
|
+
elsif w.end_with?("ses") || w.end_with?("xes") || w.end_with?("zes") || w.end_with?("ches") || w.end_with?("shes")
|
|
65
|
+
w[0..-3]
|
|
66
|
+
elsif w.end_with?("sses")
|
|
67
|
+
w[0..-3]
|
|
68
|
+
elsif w.end_with?("s") && !w.end_with?("ss") && !w.end_with?("us")
|
|
69
|
+
w[0..-2]
|
|
70
|
+
else
|
|
71
|
+
w
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def camelize(name)
|
|
76
|
+
name.to_s.split(/[-_]/).map(&:capitalize).join
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def snake_case(name)
|
|
80
|
+
name.to_s.gsub(/([A-Z])/, '_\1').downcase.sub(/^_/, "")
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# lib/whoosh/client_gen/dependency_checker.rb
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Whoosh
|
|
5
|
+
module ClientGen
|
|
6
|
+
class DependencyChecker
|
|
7
|
+
DEPENDENCIES = {
|
|
8
|
+
react_spa: [{ cmd: "node", check: "node --version", min_version: "18" }],
|
|
9
|
+
expo: [
|
|
10
|
+
{ cmd: "node", check: "node --version", min_version: "18" },
|
|
11
|
+
{ cmd: "npx", check: "npx expo --version", min_version: nil }
|
|
12
|
+
],
|
|
13
|
+
ios: [{ cmd: "xcodebuild", check: "xcodebuild -version", min_version: "15" }],
|
|
14
|
+
flutter: [{ cmd: "flutter", check: "flutter --version", min_version: "3" }],
|
|
15
|
+
htmx: [],
|
|
16
|
+
telegram_bot: [{ cmd: "ruby", check: "ruby --version", min_version: "3.2" }],
|
|
17
|
+
telegram_mini_app: [{ cmd: "node", check: "node --version", min_version: "18" }]
|
|
18
|
+
}.freeze
|
|
19
|
+
|
|
20
|
+
def self.check(client_type)
|
|
21
|
+
deps = DEPENDENCIES[client_type.to_sym] || []
|
|
22
|
+
return { ok: true, dependencies: [], missing: [] } if deps.empty?
|
|
23
|
+
|
|
24
|
+
missing = []
|
|
25
|
+
deps.each do |dep|
|
|
26
|
+
output = `#{dep[:check]} 2>/dev/null`.strip
|
|
27
|
+
if output.empty?
|
|
28
|
+
missing << dep
|
|
29
|
+
elsif dep[:min_version]
|
|
30
|
+
version = output.scan(/(\d+)\./)[0]&.first
|
|
31
|
+
if version && version.to_i < dep[:min_version].to_i
|
|
32
|
+
missing << dep.merge(found_version: version)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
{
|
|
38
|
+
ok: missing.empty?,
|
|
39
|
+
dependencies: deps.map { |d| d[:cmd] },
|
|
40
|
+
missing: missing
|
|
41
|
+
}
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def self.dependency_for(client_type)
|
|
45
|
+
DEPENDENCIES[client_type.to_sym] || []
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|