ruflet_cli 0.0.3 → 0.0.5
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/lib/ruflet/cli/build_command.rb +182 -8
- data/lib/ruflet/cli/extra_command.rb +132 -0
- data/lib/ruflet/cli/flutter_sdk.rb +275 -0
- data/lib/ruflet/cli/new_command.rb +36 -3
- data/lib/ruflet/cli/run_command.rb +465 -5
- data/lib/ruflet/cli/templates.rb +8 -14
- data/lib/ruflet/cli.rb +18 -0
- data/lib/ruflet/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 83772c8986411d4680455374f1f350c015183318e3c090426ea53edfb4ed2a89
|
|
4
|
+
data.tar.gz: 45f42f969bc487714f24c0833ca8fdbf2236e789622deb604a231d1a254ea483
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 412a6524bff1cdf335256193390aca08c71b299f517da00af1da6efada84c7fc0ebd0d399be9e92235d6713ebcfbd8c8f9695695b68e3465242ed998deecf1bb
|
|
7
|
+
data.tar.gz: 0f77d2ee780f5d443e58d356ac02382ae0e5a53144ac64d9df5bf8d4e9bec2fcf51646f8d92fdc69e527259a7100e772fafcd26861c2d63ba829606531b03824
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "yaml"
|
|
5
|
+
|
|
3
6
|
module Ruflet
|
|
4
7
|
module CLI
|
|
5
8
|
module BuildCommand
|
|
9
|
+
include FlutterSdk
|
|
10
|
+
|
|
6
11
|
def command_build(args)
|
|
7
12
|
platform = (args.shift || "").downcase
|
|
8
13
|
if platform.empty?
|
|
@@ -23,7 +28,11 @@ module Ruflet
|
|
|
23
28
|
return 1
|
|
24
29
|
end
|
|
25
30
|
|
|
26
|
-
|
|
31
|
+
tools = ensure_flutter!("build", client_dir: client_dir)
|
|
32
|
+
ok = prepare_flutter_client(client_dir, tools: tools)
|
|
33
|
+
return 1 unless ok
|
|
34
|
+
|
|
35
|
+
ok = system(tools[:env], tools[:flutter], *flutter_cmd, *args, chdir: client_dir)
|
|
27
36
|
ok ? 0 : 1
|
|
28
37
|
end
|
|
29
38
|
|
|
@@ -36,25 +45,190 @@ module Ruflet
|
|
|
36
45
|
local = File.expand_path("ruflet_client", Dir.pwd)
|
|
37
46
|
return local if Dir.exist?(local)
|
|
38
47
|
|
|
48
|
+
template = File.expand_path("templates/ruflet_flutter_template", Dir.pwd)
|
|
49
|
+
return template if Dir.exist?(template)
|
|
50
|
+
|
|
39
51
|
nil
|
|
40
52
|
end
|
|
41
53
|
|
|
54
|
+
def prepare_flutter_client(client_dir, tools:)
|
|
55
|
+
apply_build_config(client_dir)
|
|
56
|
+
unless system(tools[:env], tools[:flutter], "pub", "get", chdir: client_dir)
|
|
57
|
+
warn "flutter pub get failed"
|
|
58
|
+
return false
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
unless system(tools[:env], tools[:dart], "run", "flutter_native_splash:create", chdir: client_dir)
|
|
62
|
+
warn "flutter_native_splash failed"
|
|
63
|
+
return false
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
unless system(tools[:env], tools[:dart], "run", "flutter_launcher_icons", chdir: client_dir)
|
|
67
|
+
warn "flutter_launcher_icons failed"
|
|
68
|
+
return false
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
true
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def apply_build_config(client_dir)
|
|
75
|
+
config_path = ENV["RUFLET_CONFIG"] || "ruflet.yaml"
|
|
76
|
+
unless File.file?(config_path)
|
|
77
|
+
alt = "ruflet.yml"
|
|
78
|
+
config_path = alt if File.file?(alt)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
config_present = File.file?(config_path)
|
|
82
|
+
config = config_present ? (YAML.load_file(config_path) || {}) : {}
|
|
83
|
+
build = config["build"] || {}
|
|
84
|
+
assets = config["assets"] || {}
|
|
85
|
+
config_dir = config_present ? File.dirname(File.expand_path(config_path)) : Dir.pwd
|
|
86
|
+
|
|
87
|
+
assets_root = build["assets_dir"] || assets["dir"] || config["assets_dir"] || "assets"
|
|
88
|
+
assets_root = File.expand_path(assets_root, config_dir)
|
|
89
|
+
|
|
90
|
+
unless config_present || Dir.exist?(assets_root) || ENV["RUFLET_SPLASH"] || ENV["RUFLET_ICON"]
|
|
91
|
+
return
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
resolve_asset = lambda do |path|
|
|
95
|
+
return nil if path.nil? || path.to_s.strip.empty?
|
|
96
|
+
full = File.expand_path(path.to_s, config_dir)
|
|
97
|
+
File.file?(full) ? full : nil
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
find_first = lambda do |dir, names|
|
|
101
|
+
names.each do |name|
|
|
102
|
+
candidate = File.join(dir, name)
|
|
103
|
+
return candidate if File.file?(candidate)
|
|
104
|
+
end
|
|
105
|
+
nil
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
splash = resolve_asset.call(build["splash"] || assets["splash"] || ENV["RUFLET_SPLASH"])
|
|
109
|
+
splash_dark = resolve_asset.call(build["splash_dark"] || build["splash_dark_image"] || assets["splash_dark"])
|
|
110
|
+
icon = resolve_asset.call(build["icon"] || assets["icon"] || ENV["RUFLET_ICON"])
|
|
111
|
+
icon_android = resolve_asset.call(build["icon_android"] || assets["icon_android"])
|
|
112
|
+
icon_ios = resolve_asset.call(build["icon_ios"] || assets["icon_ios"])
|
|
113
|
+
icon_web = resolve_asset.call(build["icon_web"] || assets["icon_web"])
|
|
114
|
+
icon_windows = resolve_asset.call(build["icon_windows"] || assets["icon_windows"])
|
|
115
|
+
icon_macos = resolve_asset.call(build["icon_macos"] || assets["icon_macos"])
|
|
116
|
+
|
|
117
|
+
if Dir.exist?(assets_root)
|
|
118
|
+
splash ||= find_first.call(assets_root, ["splash.png", "splash.jpg", "splash.webp", "splash.bmp"])
|
|
119
|
+
splash_dark ||= find_first.call(assets_root, ["splash_dark.png", "splash_dark.jpg", "splash_dark.webp", "splash_dark.bmp"])
|
|
120
|
+
icon ||= find_first.call(assets_root, ["icon.png", "icon.jpg", "icon.webp", "icon.bmp"])
|
|
121
|
+
icon_android ||= find_first.call(assets_root, ["icon_android.png", "icon_android.jpg", "icon_android.webp"])
|
|
122
|
+
icon_ios ||= find_first.call(assets_root, ["icon_ios.png", "icon_ios.jpg", "icon_ios.webp"])
|
|
123
|
+
icon_web ||= find_first.call(assets_root, ["icon_web.png", "icon_web.jpg", "icon_web.webp"])
|
|
124
|
+
icon_windows ||= find_first.call(assets_root, ["icon_windows.ico", "icon_windows.png"])
|
|
125
|
+
icon_macos ||= find_first.call(assets_root, ["icon_macos.png", "icon_macos.jpg", "icon_macos.webp"])
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
splash_color = build["splash_color"]
|
|
129
|
+
splash_dark_color = build["splash_dark_color"] || build["splash_color_dark"]
|
|
130
|
+
icon_background = build["icon_background"]
|
|
131
|
+
theme_color = build["theme_color"]
|
|
132
|
+
|
|
133
|
+
assets_dir = File.join(client_dir, "assets")
|
|
134
|
+
FileUtils.mkdir_p(assets_dir)
|
|
135
|
+
|
|
136
|
+
copy_asset = lambda do |src, dest|
|
|
137
|
+
return unless src
|
|
138
|
+
FileUtils.cp(src, File.join(assets_dir, dest))
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
copy_asset.call(splash, "splash.png")
|
|
142
|
+
copy_asset.call(splash_dark, "splash_dark.png")
|
|
143
|
+
copy_asset.call(icon, "icon.png")
|
|
144
|
+
copy_asset.call(icon_android, "icon_android.png")
|
|
145
|
+
copy_asset.call(icon_ios, "icon_ios.png")
|
|
146
|
+
copy_asset.call(icon_web, "icon_web.png")
|
|
147
|
+
if icon_windows
|
|
148
|
+
ext = File.extname(icon_windows).downcase
|
|
149
|
+
copy_asset.call(icon_windows, ext == ".ico" ? "icon_windows.ico" : "icon_windows.png")
|
|
150
|
+
end
|
|
151
|
+
copy_asset.call(icon_macos, "icon_macos.png")
|
|
152
|
+
|
|
153
|
+
pubspec_path = File.join(client_dir, "pubspec.yaml")
|
|
154
|
+
return unless File.file?(pubspec_path)
|
|
155
|
+
|
|
156
|
+
if icon
|
|
157
|
+
update_pubspec_value(pubspec_path, "flutter_launcher_icons", "image_path", "\"assets/icon.png\"", multiple: true)
|
|
158
|
+
end
|
|
159
|
+
update_pubspec_value(pubspec_path, "flutter_launcher_icons", "image_path_android", "\"assets/icon_android.png\"", multiple: true) if icon_android
|
|
160
|
+
update_pubspec_value(pubspec_path, "flutter_launcher_icons", "image_path_ios", "\"assets/icon_ios.png\"", multiple: true) if icon_ios
|
|
161
|
+
update_pubspec_value(pubspec_path, "flutter_launcher_icons", "image_path_web", "\"assets/icon_web.png\"", multiple: true) if icon_web
|
|
162
|
+
if icon_windows
|
|
163
|
+
ext = File.extname(icon_windows).downcase
|
|
164
|
+
value = ext == ".ico" ? "\"assets/icon_windows.ico\"" : "\"assets/icon_windows.png\""
|
|
165
|
+
update_pubspec_value(pubspec_path, "flutter_launcher_icons", "image_path_windows", value, multiple: true)
|
|
166
|
+
end
|
|
167
|
+
update_pubspec_value(pubspec_path, "flutter_launcher_icons", "image_path_macos", "\"assets/icon_macos.png\"", multiple: true) if icon_macos
|
|
168
|
+
update_pubspec_value(pubspec_path, "flutter_launcher_icons", "background_color", "\"#{icon_background}\"") if icon_background
|
|
169
|
+
update_pubspec_value(pubspec_path, "flutter_launcher_icons", "theme_color", "\"#{theme_color}\"") if theme_color
|
|
170
|
+
|
|
171
|
+
update_pubspec_value(pubspec_path, "flutter_native_splash", "image", "\"assets/splash.png\"") if splash
|
|
172
|
+
update_pubspec_value(pubspec_path, "flutter_native_splash", "image_dark", "\"assets/splash_dark.png\"") if splash_dark
|
|
173
|
+
update_pubspec_value(pubspec_path, "flutter_native_splash", "color", "\"#{splash_color}\"") if splash_color
|
|
174
|
+
update_pubspec_value(pubspec_path, "flutter_native_splash", "color_dark", "\"#{splash_dark_color}\"") if splash_dark_color
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def update_pubspec_value(path, block, key, value, multiple: false)
|
|
178
|
+
lines = File.read(path).split("\n", -1)
|
|
179
|
+
out = []
|
|
180
|
+
in_block = false
|
|
181
|
+
replaced = false
|
|
182
|
+
block_indent = nil
|
|
183
|
+
lines.each do |line|
|
|
184
|
+
if line.start_with?("#{block}:")
|
|
185
|
+
in_block = true
|
|
186
|
+
block_indent = line[/^\s*/] + " "
|
|
187
|
+
out << line
|
|
188
|
+
next
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
if in_block
|
|
192
|
+
if line =~ /^\S/ && !line.start_with?("#{block}:")
|
|
193
|
+
unless replaced
|
|
194
|
+
out << "#{block_indent}#{key}: #{value}"
|
|
195
|
+
replaced = true
|
|
196
|
+
end
|
|
197
|
+
in_block = false
|
|
198
|
+
else
|
|
199
|
+
if line.strip.start_with?("#{key}:")
|
|
200
|
+
indent = line[/^\s*/]
|
|
201
|
+
out << "#{indent}#{key}: #{value}"
|
|
202
|
+
replaced = true
|
|
203
|
+
next
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
out << line
|
|
209
|
+
end
|
|
210
|
+
if in_block && !replaced
|
|
211
|
+
out << "#{block_indent}#{key}: #{value}"
|
|
212
|
+
end
|
|
213
|
+
File.write(path, out.join("\n"))
|
|
214
|
+
end
|
|
215
|
+
|
|
42
216
|
def flutter_build_command(platform)
|
|
43
217
|
case platform
|
|
44
218
|
when "apk", "android"
|
|
45
|
-
["
|
|
219
|
+
["build", "apk"]
|
|
46
220
|
when "aab", "appbundle"
|
|
47
|
-
["
|
|
221
|
+
["build", "appbundle"]
|
|
48
222
|
when "ios"
|
|
49
|
-
["
|
|
223
|
+
["build", "ios", "--no-codesign"]
|
|
50
224
|
when "web"
|
|
51
|
-
["
|
|
225
|
+
["build", "web"]
|
|
52
226
|
when "macos"
|
|
53
|
-
["
|
|
227
|
+
["build", "macos"]
|
|
54
228
|
when "windows"
|
|
55
|
-
["
|
|
229
|
+
["build", "windows"]
|
|
56
230
|
when "linux"
|
|
57
|
-
["
|
|
231
|
+
["build", "linux"]
|
|
58
232
|
else
|
|
59
233
|
nil
|
|
60
234
|
end
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
|
|
5
|
+
module Ruflet
|
|
6
|
+
module CLI
|
|
7
|
+
module ExtraCommand
|
|
8
|
+
include FlutterSdk
|
|
9
|
+
|
|
10
|
+
def command_create(args)
|
|
11
|
+
command_new(args)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def command_doctor(args)
|
|
15
|
+
verbose = args.delete("--verbose") || args.delete("-v")
|
|
16
|
+
puts "Ruflet doctor"
|
|
17
|
+
puts " Ruby: #{RUBY_VERSION}"
|
|
18
|
+
tools = ensure_flutter!("doctor")
|
|
19
|
+
puts " Flutter: #{tools[:flutter]}"
|
|
20
|
+
system(tools[:env], tools[:flutter], "doctor", *(verbose ? ["-v"] : []))
|
|
21
|
+
$?.exitstatus || 1
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def command_devices(args)
|
|
25
|
+
tools = ensure_flutter!("devices")
|
|
26
|
+
system(tools[:env], tools[:flutter], "devices", *args)
|
|
27
|
+
$?.exitstatus || 1
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def command_emulators(args)
|
|
31
|
+
tools = ensure_flutter!("emulators")
|
|
32
|
+
action = nil
|
|
33
|
+
emulator_id = nil
|
|
34
|
+
verbose = false
|
|
35
|
+
parser = OptionParser.new do |o|
|
|
36
|
+
o.on("--create") { action = "create" }
|
|
37
|
+
o.on("--delete") { action = "delete" }
|
|
38
|
+
o.on("--start") { action = "start" }
|
|
39
|
+
o.on("--emulator ID") { |v| emulator_id = v }
|
|
40
|
+
o.on("-v", "--verbose") { verbose = true }
|
|
41
|
+
end
|
|
42
|
+
parser.parse!(args)
|
|
43
|
+
|
|
44
|
+
case action
|
|
45
|
+
when "start"
|
|
46
|
+
unless emulator_id
|
|
47
|
+
warn "Missing --emulator for start"
|
|
48
|
+
return 1
|
|
49
|
+
end
|
|
50
|
+
cmd = [tools[:flutter], "emulators", "--launch", emulator_id]
|
|
51
|
+
cmd << "-v" if verbose
|
|
52
|
+
system(tools[:env], *cmd)
|
|
53
|
+
$?.exitstatus || 1
|
|
54
|
+
when "create", "delete"
|
|
55
|
+
warn "ruflet emulators --#{action} is not implemented yet. Use your platform tools."
|
|
56
|
+
1
|
|
57
|
+
else
|
|
58
|
+
cmd = [tools[:flutter], "emulators"]
|
|
59
|
+
cmd << "-v" if verbose
|
|
60
|
+
system(tools[:env], *cmd)
|
|
61
|
+
$?.exitstatus || 1
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def command_debug(args)
|
|
66
|
+
options = {
|
|
67
|
+
platform: nil,
|
|
68
|
+
device_id: nil,
|
|
69
|
+
release: false,
|
|
70
|
+
verbose: false,
|
|
71
|
+
web_renderer: nil
|
|
72
|
+
}
|
|
73
|
+
parser = OptionParser.new do |o|
|
|
74
|
+
o.on("--platform NAME") { |v| options[:platform] = v }
|
|
75
|
+
o.on("--device-id ID") { |v| options[:device_id] = v }
|
|
76
|
+
o.on("--release") { options[:release] = true }
|
|
77
|
+
o.on("-v", "--verbose") { options[:verbose] = true }
|
|
78
|
+
o.on("--web-renderer NAME") { |v| options[:web_renderer] = v }
|
|
79
|
+
end
|
|
80
|
+
parser.parse!(args)
|
|
81
|
+
|
|
82
|
+
options[:platform] ||= args.shift
|
|
83
|
+
client_dir = detect_client_dir
|
|
84
|
+
unless client_dir
|
|
85
|
+
warn "Could not find Flutter client directory."
|
|
86
|
+
warn "Set RUFLET_CLIENT_DIR or place client at ./ruflet_client"
|
|
87
|
+
warn "`ruflet debug` requires Flutter client source code."
|
|
88
|
+
warn "For prebuilt clients, use: `ruflet run --web` or `ruflet run --desktop`."
|
|
89
|
+
return 1
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
tools = ensure_flutter!("debug", client_dir: client_dir)
|
|
93
|
+
cmd = [tools[:flutter], "run"]
|
|
94
|
+
cmd << "--release" if options[:release]
|
|
95
|
+
cmd << "-v" if options[:verbose]
|
|
96
|
+
cmd += ["--web-renderer", options[:web_renderer]] if options[:web_renderer]
|
|
97
|
+
|
|
98
|
+
if options[:device_id]
|
|
99
|
+
cmd += ["-d", options[:device_id]]
|
|
100
|
+
else
|
|
101
|
+
case options[:platform]
|
|
102
|
+
when "web"
|
|
103
|
+
cmd += ["-d", "chrome"]
|
|
104
|
+
when "macos", "windows", "linux"
|
|
105
|
+
cmd += ["-d", options[:platform]]
|
|
106
|
+
when "ios", "android"
|
|
107
|
+
# let flutter pick the default device
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
system(tools[:env], *cmd, chdir: client_dir)
|
|
112
|
+
$?.exitstatus || 1
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
private
|
|
116
|
+
|
|
117
|
+
def detect_client_dir
|
|
118
|
+
env_dir = ENV["RUFLET_CLIENT_DIR"]
|
|
119
|
+
return env_dir if env_dir && Dir.exist?(env_dir)
|
|
120
|
+
|
|
121
|
+
local = File.expand_path("ruflet_client", Dir.pwd)
|
|
122
|
+
return local if Dir.exist?(local)
|
|
123
|
+
|
|
124
|
+
template = File.expand_path("templates/ruflet_flutter_template", Dir.pwd)
|
|
125
|
+
return template if Dir.exist?(template)
|
|
126
|
+
|
|
127
|
+
nil
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "json"
|
|
5
|
+
require "net/http"
|
|
6
|
+
require "rbconfig"
|
|
7
|
+
require "tmpdir"
|
|
8
|
+
require "uri"
|
|
9
|
+
|
|
10
|
+
module Ruflet
|
|
11
|
+
module CLI
|
|
12
|
+
module FlutterSdk
|
|
13
|
+
RELEASES_BASE = "https://storage.googleapis.com/flutter_infra_release/releases".freeze
|
|
14
|
+
DEFAULT_FLUTTER_CHANNEL = "stable".freeze
|
|
15
|
+
|
|
16
|
+
def ensure_flutter!(command_name, client_dir: nil)
|
|
17
|
+
tools = flutter_tools(client_dir: client_dir)
|
|
18
|
+
return tools if tools
|
|
19
|
+
|
|
20
|
+
warn "Flutter is required for `ruflet #{command_name}` and FVM bootstrap failed."
|
|
21
|
+
warn "Set RUFLET_FLUTTER_VERSION or add .fvmrc to the project."
|
|
22
|
+
exit 1
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def flutter_tools(client_dir: nil)
|
|
28
|
+
# Always use FVM so Flutter/Dart match pinned SDK.
|
|
29
|
+
fvm_tools = flutter_tools_via_fvm(client_dir: client_dir)
|
|
30
|
+
return fvm_tools if fvm_tools
|
|
31
|
+
|
|
32
|
+
nil
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def flutter_tools_via_fvm(client_dir: nil)
|
|
36
|
+
version = desired_flutter_version(client_dir: client_dir)
|
|
37
|
+
return nil if version.to_s.strip.empty?
|
|
38
|
+
|
|
39
|
+
project_dir = fvm_project_dir(client_dir: client_dir)
|
|
40
|
+
fvm = ensure_fvm_available(client_dir: client_dir)
|
|
41
|
+
return nil unless fvm
|
|
42
|
+
|
|
43
|
+
FileUtils.mkdir_p(project_dir)
|
|
44
|
+
fvmrc_path = File.join(project_dir, ".fvmrc")
|
|
45
|
+
unless File.file?(fvmrc_path)
|
|
46
|
+
File.write(fvmrc_path, "{\"flutter\":\"#{version}\"}\n")
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
system(fvm_env, fvm, "install", version.to_s, chdir: project_dir, out: File::NULL, err: File::NULL)
|
|
50
|
+
system(fvm_env, fvm, "use", "--force", version.to_s, chdir: project_dir, out: File::NULL, err: File::NULL)
|
|
51
|
+
|
|
52
|
+
flutter = File.join(project_dir, ".fvm", "flutter_sdk", "bin", windows_host? ? "flutter.bat" : "flutter")
|
|
53
|
+
return nil unless File.executable?(flutter)
|
|
54
|
+
|
|
55
|
+
tools_from_flutter_bin(flutter)
|
|
56
|
+
rescue StandardError => e
|
|
57
|
+
warn "FVM bootstrap failed: #{e.class}: #{e.message}"
|
|
58
|
+
nil
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def ensure_fvm_available(client_dir: nil)
|
|
62
|
+
fvm = which_command("fvm")
|
|
63
|
+
return fvm if fvm
|
|
64
|
+
|
|
65
|
+
dart = which_command("dart")
|
|
66
|
+
unless dart
|
|
67
|
+
sdk_root = ensure_flutter_sdk_downloaded(client_dir: client_dir)
|
|
68
|
+
dart = sdk_root ? File.join(sdk_root, "bin", windows_host? ? "dart.bat" : "dart") : nil
|
|
69
|
+
end
|
|
70
|
+
return nil unless dart && File.executable?(dart)
|
|
71
|
+
|
|
72
|
+
system(dart, "pub", "global", "activate", "fvm", out: File::NULL, err: File::NULL)
|
|
73
|
+
which_command("fvm")
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def fvm_env
|
|
77
|
+
pub_bin = File.join(Dir.home, ".pub-cache", "bin")
|
|
78
|
+
{ "PATH" => "#{pub_bin}#{File::PATH_SEPARATOR}#{ENV.fetch('PATH', '')}" }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def tools_from_flutter_bin(flutter_bin)
|
|
82
|
+
return nil unless File.executable?(flutter_bin)
|
|
83
|
+
|
|
84
|
+
bin_dir = File.dirname(flutter_bin)
|
|
85
|
+
dart = File.join(bin_dir, windows_host? ? "dart.bat" : "dart")
|
|
86
|
+
{
|
|
87
|
+
flutter: flutter_bin,
|
|
88
|
+
dart: (File.executable?(dart) ? dart : "dart"),
|
|
89
|
+
env: { "PATH" => "#{bin_dir}#{File::PATH_SEPARATOR}#{ENV.fetch('PATH', '')}" }
|
|
90
|
+
}
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def ensure_flutter_sdk_downloaded(client_dir: nil)
|
|
94
|
+
release_info = resolve_flutter_release(client_dir: client_dir)
|
|
95
|
+
return nil unless release_info
|
|
96
|
+
|
|
97
|
+
release = release_info[:release]
|
|
98
|
+
host = release_info[:host]
|
|
99
|
+
archive = release.fetch("archive")
|
|
100
|
+
install_root = File.join(Dir.home, ".ruflet", "flutter", release.fetch("version"), host)
|
|
101
|
+
sdk_root = File.join(install_root, "flutter")
|
|
102
|
+
flutter_bin = File.join(sdk_root, "bin", windows_host? ? "flutter.bat" : "flutter")
|
|
103
|
+
return sdk_root if File.executable?(flutter_bin)
|
|
104
|
+
|
|
105
|
+
FileUtils.mkdir_p(install_root)
|
|
106
|
+
Dir.mktmpdir("ruflet-flutter-sdk-") do |tmpdir|
|
|
107
|
+
archive_path = File.join(tmpdir, File.basename(archive))
|
|
108
|
+
download_file("#{RELEASES_BASE}/#{archive}", archive_path)
|
|
109
|
+
extract_archive(archive_path, install_root)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
return sdk_root if File.executable?(flutter_bin)
|
|
113
|
+
|
|
114
|
+
# Some archives may unpack into a different folder name.
|
|
115
|
+
guessed = Dir.glob(File.join(install_root, "**", windows_host? ? "flutter.bat" : "flutter"))
|
|
116
|
+
.map { |p| File.expand_path("../..", p) }
|
|
117
|
+
.find { |root| File.executable?(File.join(root, "bin", windows_host? ? "flutter.bat" : "flutter")) }
|
|
118
|
+
return guessed if guessed
|
|
119
|
+
|
|
120
|
+
nil
|
|
121
|
+
rescue StandardError => e
|
|
122
|
+
warn "Flutter auto-install failed: #{e.class}: #{e.message}"
|
|
123
|
+
nil
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def resolve_flutter_release(client_dir: nil)
|
|
127
|
+
host = flutter_host
|
|
128
|
+
return nil unless host
|
|
129
|
+
|
|
130
|
+
manifest = fetch_releases_manifest(host)
|
|
131
|
+
return nil unless manifest
|
|
132
|
+
|
|
133
|
+
version = desired_flutter_version(client_dir: client_dir)
|
|
134
|
+
release = pick_release(manifest, version: version)
|
|
135
|
+
return nil unless release
|
|
136
|
+
|
|
137
|
+
{ release: release, host: host }
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def desired_flutter_version(client_dir: nil)
|
|
141
|
+
env = ENV["RUFLET_FLUTTER_VERSION"].to_s.strip
|
|
142
|
+
return env unless env.empty?
|
|
143
|
+
|
|
144
|
+
fvm = parse_fvmrc(find_fvmrc(client_dir))
|
|
145
|
+
return fvm if fvm
|
|
146
|
+
|
|
147
|
+
DEFAULT_FLUTTER_CHANNEL
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def fvm_project_dir(client_dir: nil)
|
|
151
|
+
return client_dir if client_dir
|
|
152
|
+
|
|
153
|
+
File.join(Dir.home, ".ruflet", "fvm_project")
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def find_fvmrc(client_dir)
|
|
157
|
+
candidates = []
|
|
158
|
+
candidates << File.join(client_dir, ".fvmrc") if client_dir
|
|
159
|
+
candidates << File.join(Dir.pwd, ".fvmrc")
|
|
160
|
+
candidates.find { |p| File.file?(p) }
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def parse_fvmrc(path)
|
|
164
|
+
return nil unless path && File.file?(path)
|
|
165
|
+
|
|
166
|
+
raw = File.read(path).strip
|
|
167
|
+
return nil if raw.empty?
|
|
168
|
+
|
|
169
|
+
if raw.start_with?("{")
|
|
170
|
+
json = JSON.parse(raw) rescue {}
|
|
171
|
+
val = json["flutter"] || json["flutterSdkVersion"] || json["flutter_version"]
|
|
172
|
+
return val.to_s.strip unless val.to_s.strip.empty?
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
raw
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def fetch_releases_manifest(host)
|
|
179
|
+
url = "#{RELEASES_BASE}/releases_#{host}.json"
|
|
180
|
+
uri = URI(url)
|
|
181
|
+
response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
|
|
182
|
+
req = Net::HTTP::Get.new(uri)
|
|
183
|
+
req["User-Agent"] = "ruflet-cli"
|
|
184
|
+
http.request(req)
|
|
185
|
+
end
|
|
186
|
+
return JSON.parse(response.body) if response.is_a?(Net::HTTPSuccess)
|
|
187
|
+
|
|
188
|
+
nil
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def pick_release(manifest, version: nil)
|
|
192
|
+
releases = manifest.fetch("releases", [])
|
|
193
|
+
if version
|
|
194
|
+
pinned = releases.find { |r| r["channel"] == "stable" && r["version"] == version }
|
|
195
|
+
return pinned if pinned
|
|
196
|
+
warn "Requested Flutter #{version} not found in stable releases; falling back to latest stable."
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
current = manifest.fetch("current_release", {})["stable"]
|
|
200
|
+
if current
|
|
201
|
+
by_hash = releases.find { |r| r["hash"] == current }
|
|
202
|
+
return by_hash if by_hash
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
releases.reverse.find { |r| r["channel"] == "stable" }
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def flutter_host
|
|
209
|
+
os = RbConfig::CONFIG["host_os"]
|
|
210
|
+
if os.match?(/darwin/i)
|
|
211
|
+
return machine_arch.include?("arm") ? "macos_arm64" : "macos"
|
|
212
|
+
end
|
|
213
|
+
return "linux" if os.match?(/linux/i)
|
|
214
|
+
return "windows" if os.match?(/mswin|mingw|cygwin/i)
|
|
215
|
+
|
|
216
|
+
nil
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def machine_arch
|
|
220
|
+
RbConfig::CONFIG["host_cpu"].to_s.downcase
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def windows_host?
|
|
224
|
+
RbConfig::CONFIG["host_os"].match?(/mswin|mingw|cygwin/i)
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def which_command(name)
|
|
228
|
+
exts = windows_host? ? ENV.fetch("PATHEXT", ".EXE;.BAT;.CMD").split(";") : [""]
|
|
229
|
+
ENV.fetch("PATH", "").split(File::PATH_SEPARATOR).each do |dir|
|
|
230
|
+
exts.each do |ext|
|
|
231
|
+
candidate = File.join(dir, "#{name}#{ext}")
|
|
232
|
+
return candidate if File.file?(candidate) && File.executable?(candidate)
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
nil
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def download_file(url, destination, limit: 5)
|
|
239
|
+
raise "Too many redirects while downloading #{url}" if limit <= 0
|
|
240
|
+
|
|
241
|
+
uri = URI(url)
|
|
242
|
+
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
|
|
243
|
+
req = Net::HTTP::Get.new(uri)
|
|
244
|
+
req["User-Agent"] = "ruflet-cli"
|
|
245
|
+
http.request(req) do |res|
|
|
246
|
+
case res
|
|
247
|
+
when Net::HTTPSuccess
|
|
248
|
+
File.open(destination, "wb") { |f| res.read_body { |chunk| f.write(chunk) } }
|
|
249
|
+
return destination
|
|
250
|
+
when Net::HTTPRedirection
|
|
251
|
+
return download_file(res["location"], destination, limit: limit - 1)
|
|
252
|
+
else
|
|
253
|
+
raise "Download failed (#{res.code})"
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def extract_archive(archive, destination)
|
|
260
|
+
if archive.end_with?(".zip")
|
|
261
|
+
if windows_host?
|
|
262
|
+
return system("powershell", "-NoProfile", "-Command", "Expand-Archive -Path '#{archive}' -DestinationPath '#{destination}' -Force")
|
|
263
|
+
end
|
|
264
|
+
return system("unzip", "-oq", archive, "-d", destination)
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
if archive.end_with?(".tar.xz") || archive.end_with?(".tar.gz") || archive.end_with?(".tgz")
|
|
268
|
+
return system("tar", "-xf", archive, "-C", destination)
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
false
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
end
|
|
@@ -19,23 +19,56 @@ module Ruflet
|
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
FileUtils.mkdir_p(root)
|
|
22
|
-
FileUtils.mkdir_p(File.join(root, ".bundle"))
|
|
23
22
|
File.write(File.join(root, "main.rb"), format(Ruflet::CLI::MAIN_TEMPLATE, app_title: humanize_name(File.basename(root))))
|
|
24
23
|
File.write(File.join(root, "Gemfile"), Ruflet::CLI::GEMFILE_TEMPLATE)
|
|
25
|
-
File.write(File.join(root, ".bundle", "config"), Ruflet::CLI::BUNDLE_CONFIG_TEMPLATE)
|
|
26
24
|
File.write(File.join(root, "README.md"), format(Ruflet::CLI::README_TEMPLATE, app_name: File.basename(root)))
|
|
25
|
+
copy_ruflet_client_template(root)
|
|
27
26
|
|
|
28
27
|
project_name = File.basename(root)
|
|
29
28
|
puts "Ruflet app created: #{project_name}"
|
|
30
29
|
puts "Run:"
|
|
31
30
|
puts " cd #{project_name}"
|
|
32
31
|
puts " bundle install"
|
|
33
|
-
puts " bundle exec ruflet run main"
|
|
32
|
+
puts " bundle exec ruflet run main.rb"
|
|
33
|
+
puts
|
|
34
|
+
puts "Client template:"
|
|
35
|
+
puts " cd ruflet_client"
|
|
36
|
+
puts " flutter pub get"
|
|
37
|
+
puts " flutter run"
|
|
34
38
|
0
|
|
35
39
|
end
|
|
36
40
|
|
|
37
41
|
private
|
|
38
42
|
|
|
43
|
+
def copy_ruflet_client_template(root)
|
|
44
|
+
template_root = File.expand_path("../../../../../ruflet_client", __dir__)
|
|
45
|
+
return unless Dir.exist?(template_root)
|
|
46
|
+
|
|
47
|
+
target = File.join(root, "ruflet_client")
|
|
48
|
+
FileUtils.cp_r(template_root, target)
|
|
49
|
+
prune_client_template(target)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def prune_client_template(target)
|
|
53
|
+
paths = %w[
|
|
54
|
+
.dart_tool
|
|
55
|
+
.idea
|
|
56
|
+
build
|
|
57
|
+
ios/Pods
|
|
58
|
+
ios/.symlinks
|
|
59
|
+
ios/Podfile.lock
|
|
60
|
+
macos/Pods
|
|
61
|
+
macos/Podfile.lock
|
|
62
|
+
android/.gradle
|
|
63
|
+
android/.kotlin
|
|
64
|
+
android/local.properties
|
|
65
|
+
]
|
|
66
|
+
paths.each do |path|
|
|
67
|
+
full = File.join(target, path)
|
|
68
|
+
FileUtils.rm_rf(full) if File.exist?(full)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
39
72
|
def humanize_name(name)
|
|
40
73
|
name.to_s.gsub(/[_-]+/, " ").split.map(&:capitalize).join(" ")
|
|
41
74
|
end
|
|
@@ -3,6 +3,12 @@
|
|
|
3
3
|
require "optparse"
|
|
4
4
|
require "rbconfig"
|
|
5
5
|
require "socket"
|
|
6
|
+
require "timeout"
|
|
7
|
+
require "tmpdir"
|
|
8
|
+
require "fileutils"
|
|
9
|
+
require "json"
|
|
10
|
+
require "net/http"
|
|
11
|
+
require "uri"
|
|
6
12
|
|
|
7
13
|
module Ruflet
|
|
8
14
|
module CLI
|
|
@@ -24,20 +30,23 @@ module Ruflet
|
|
|
24
30
|
return 1
|
|
25
31
|
end
|
|
26
32
|
|
|
27
|
-
selected_port =
|
|
33
|
+
selected_port = resolve_backend_port(options[:target])
|
|
34
|
+
return 1 unless selected_port
|
|
28
35
|
env = {
|
|
29
36
|
"RUFLET_TARGET" => options[:target],
|
|
30
37
|
"RUFLET_SUPPRESS_SERVER_BANNER" => "1",
|
|
31
38
|
"RUFLET_PORT" => selected_port.to_s
|
|
32
39
|
}
|
|
40
|
+
assets_dir = File.join(File.dirname(script_path), "assets")
|
|
41
|
+
env["RUFLET_ASSETS_DIR"] = assets_dir if File.directory?(assets_dir)
|
|
33
42
|
|
|
34
|
-
|
|
43
|
+
print_run_banner(target: options[:target], port: selected_port)
|
|
35
44
|
print_mobile_qr_hint(port: selected_port) if options[:target] == "mobile"
|
|
36
45
|
|
|
46
|
+
gemfile_path = find_nearest_gemfile(Dir.pwd)
|
|
37
47
|
cmd =
|
|
38
|
-
if
|
|
39
|
-
env["
|
|
40
|
-
env["BUNDLE_DISABLE_SHARED_GEMS"] = "true"
|
|
48
|
+
if gemfile_path
|
|
49
|
+
env["BUNDLE_GEMFILE"] = gemfile_path
|
|
41
50
|
bundle_ready = system(env, "bundle", "check", out: File::NULL, err: File::NULL)
|
|
42
51
|
return 1 unless bundle_ready || system(env, "bundle", "install")
|
|
43
52
|
|
|
@@ -47,6 +56,7 @@ module Ruflet
|
|
|
47
56
|
end
|
|
48
57
|
|
|
49
58
|
child_pid = Process.spawn(env, *cmd, pgroup: true)
|
|
59
|
+
launched_client_pids = launch_target_client(options[:target], selected_port)
|
|
50
60
|
forward_signal = lambda do |signal|
|
|
51
61
|
begin
|
|
52
62
|
Process.kill(signal, -child_pid)
|
|
@@ -71,6 +81,18 @@ module Ruflet
|
|
|
71
81
|
nil
|
|
72
82
|
end
|
|
73
83
|
end
|
|
84
|
+
|
|
85
|
+
Array(defined?(launched_client_pids) ? launched_client_pids : nil).compact.each do |pid|
|
|
86
|
+
begin
|
|
87
|
+
Process.kill("TERM", -pid)
|
|
88
|
+
rescue Errno::ESRCH
|
|
89
|
+
begin
|
|
90
|
+
Process.kill("TERM", pid)
|
|
91
|
+
rescue Errno::ESRCH
|
|
92
|
+
nil
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
74
96
|
end
|
|
75
97
|
|
|
76
98
|
private
|
|
@@ -85,6 +107,418 @@ module Ruflet
|
|
|
85
107
|
nil
|
|
86
108
|
end
|
|
87
109
|
|
|
110
|
+
def find_nearest_gemfile(start_dir)
|
|
111
|
+
current = File.expand_path(start_dir)
|
|
112
|
+
loop do
|
|
113
|
+
candidate = File.join(current, "Gemfile")
|
|
114
|
+
return candidate if File.file?(candidate)
|
|
115
|
+
|
|
116
|
+
parent = File.expand_path("..", current)
|
|
117
|
+
return nil if parent == current
|
|
118
|
+
|
|
119
|
+
current = parent
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def print_run_banner(target:, port:)
|
|
124
|
+
if target == "mobile" && port != 8550
|
|
125
|
+
puts "Requested port 8550 is busy; bound to #{port}"
|
|
126
|
+
end
|
|
127
|
+
if target == "desktop"
|
|
128
|
+
puts "Ruflet desktop URL: http://localhost:#{port}"
|
|
129
|
+
else
|
|
130
|
+
puts "Ruflet target: #{target}"
|
|
131
|
+
puts "Ruflet URL: http://localhost:#{port}"
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def launch_target_client(target, port)
|
|
136
|
+
wait_for_server_boot(port)
|
|
137
|
+
|
|
138
|
+
case target
|
|
139
|
+
when "web"
|
|
140
|
+
launch_web_client(port)
|
|
141
|
+
when "desktop"
|
|
142
|
+
launch_desktop_client("http://localhost:#{port}")
|
|
143
|
+
else
|
|
144
|
+
[]
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def launch_web_client(port)
|
|
149
|
+
web_dir = detect_web_client_dir
|
|
150
|
+
unless web_dir
|
|
151
|
+
warn "Web client build not found and prebuilt download failed."
|
|
152
|
+
return []
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
web_port = find_available_port(port + 1)
|
|
156
|
+
web_pid = Process.spawn("python3", "-m", "http.server", web_port.to_s, "--bind", "127.0.0.1", chdir: web_dir, out: File::NULL, err: File::NULL)
|
|
157
|
+
Process.detach(web_pid)
|
|
158
|
+
wait_for_server_boot(web_port)
|
|
159
|
+
browser_pid = open_in_browser_app_mode("http://localhost:#{web_port}")
|
|
160
|
+
open_in_browser("http://localhost:#{web_port}") if browser_pid.nil?
|
|
161
|
+
puts "Ruflet web client: http://localhost:#{web_port}"
|
|
162
|
+
puts "Ruflet backend ws: ws://localhost:#{port}/ws"
|
|
163
|
+
[web_pid, browser_pid].compact
|
|
164
|
+
rescue Errno::ENOENT
|
|
165
|
+
warn "python3 is required to host web client locally."
|
|
166
|
+
warn "Install Python 3 and rerun."
|
|
167
|
+
[]
|
|
168
|
+
rescue StandardError => e
|
|
169
|
+
warn "Failed to launch web client: #{e.class}: #{e.message}"
|
|
170
|
+
[]
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def wait_for_server_boot(port, timeout_seconds: 10)
|
|
174
|
+
Timeout.timeout(timeout_seconds) do
|
|
175
|
+
loop do
|
|
176
|
+
begin
|
|
177
|
+
sock = TCPSocket.new("127.0.0.1", port)
|
|
178
|
+
sock.write("GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n")
|
|
179
|
+
sock.close
|
|
180
|
+
break
|
|
181
|
+
rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH
|
|
182
|
+
sleep 0.15
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
rescue Timeout::Error
|
|
187
|
+
warn "Server did not become reachable at http://localhost:#{port} yet."
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def open_in_browser(url)
|
|
191
|
+
cmd =
|
|
192
|
+
case RbConfig::CONFIG["host_os"]
|
|
193
|
+
when /darwin/i
|
|
194
|
+
["open", url]
|
|
195
|
+
when /mswin|mingw|cygwin/i
|
|
196
|
+
["cmd", "/c", "start", "", url]
|
|
197
|
+
else
|
|
198
|
+
["xdg-open", url]
|
|
199
|
+
end
|
|
200
|
+
if system(*cmd, out: File::NULL, err: File::NULL)
|
|
201
|
+
puts "Opened browser at #{url}"
|
|
202
|
+
else
|
|
203
|
+
warn "Could not auto-open browser. Open manually: #{url}"
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def open_in_browser_app_mode(url)
|
|
208
|
+
host_os = RbConfig::CONFIG["host_os"]
|
|
209
|
+
if host_os.match?(/darwin/i)
|
|
210
|
+
chrome = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
|
|
211
|
+
chromium = "/Applications/Chromium.app/Contents/MacOS/Chromium"
|
|
212
|
+
browser = [chrome, chromium].find { |p| File.file?(p) && File.executable?(p) }
|
|
213
|
+
return nil unless browser
|
|
214
|
+
|
|
215
|
+
profile_dir = Dir.mktmpdir("ruflet-webapp-")
|
|
216
|
+
pid = Process.spawn(
|
|
217
|
+
browser,
|
|
218
|
+
"--new-window",
|
|
219
|
+
"--no-first-run",
|
|
220
|
+
"--no-default-browser-check",
|
|
221
|
+
"--user-data-dir=#{profile_dir}",
|
|
222
|
+
"--app=#{url}",
|
|
223
|
+
pgroup: true,
|
|
224
|
+
out: File::NULL,
|
|
225
|
+
err: File::NULL
|
|
226
|
+
)
|
|
227
|
+
Process.detach(pid)
|
|
228
|
+
return pid
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
if host_os.match?(/linux/i)
|
|
232
|
+
browser = %w[google-chrome chromium chromium-browser].find { |cmd| system("which", cmd, out: File::NULL, err: File::NULL) }
|
|
233
|
+
return nil unless browser
|
|
234
|
+
|
|
235
|
+
profile_dir = Dir.mktmpdir("ruflet-webapp-")
|
|
236
|
+
pid = Process.spawn(
|
|
237
|
+
browser,
|
|
238
|
+
"--new-window",
|
|
239
|
+
"--no-first-run",
|
|
240
|
+
"--no-default-browser-check",
|
|
241
|
+
"--user-data-dir=#{profile_dir}",
|
|
242
|
+
"--app=#{url}",
|
|
243
|
+
pgroup: true,
|
|
244
|
+
out: File::NULL,
|
|
245
|
+
err: File::NULL
|
|
246
|
+
)
|
|
247
|
+
Process.detach(pid)
|
|
248
|
+
return pid
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
nil
|
|
252
|
+
rescue StandardError
|
|
253
|
+
nil
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def launch_desktop_client(url)
|
|
257
|
+
cmd = detect_desktop_client_command(url)
|
|
258
|
+
unless cmd
|
|
259
|
+
warn "Desktop client executable not found."
|
|
260
|
+
warn "Set RUFLET_CLIENT_DIR to your client path."
|
|
261
|
+
warn "Example: export RUFLET_CLIENT_DIR=/path/to/ruflet_client"
|
|
262
|
+
return
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
pid = Process.spawn(*cmd, out: File::NULL, err: File::NULL)
|
|
266
|
+
Process.detach(pid)
|
|
267
|
+
if !pid
|
|
268
|
+
warn "Failed to launch desktop client: #{cmd.first}"
|
|
269
|
+
warn "Start it manually with URL: #{url}"
|
|
270
|
+
end
|
|
271
|
+
[pid]
|
|
272
|
+
rescue StandardError => e
|
|
273
|
+
warn "Failed to launch desktop client: #{e.class}: #{e.message}"
|
|
274
|
+
warn "Start it manually with URL: #{url}"
|
|
275
|
+
[]
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def detect_desktop_client_command(url)
|
|
279
|
+
root = ENV["RUFLET_CLIENT_DIR"]
|
|
280
|
+
root = File.expand_path("ruflet_client", Dir.pwd) if root.to_s.strip.empty?
|
|
281
|
+
root = nil unless Dir.exist?(root)
|
|
282
|
+
root ||= ensure_prebuilt_client(desktop: true)
|
|
283
|
+
return nil unless root && Dir.exist?(root)
|
|
284
|
+
|
|
285
|
+
host_os = RbConfig::CONFIG["host_os"]
|
|
286
|
+
if host_os.match?(/darwin/i)
|
|
287
|
+
release_bin = File.join(root, "build", "macos", "Build", "Products", "Release", "ruflet_client.app", "Contents", "MacOS", "ruflet_client")
|
|
288
|
+
debug_bin = File.join(root, "build", "macos", "Build", "Products", "Debug", "ruflet_client.app", "Contents", "MacOS", "ruflet_client")
|
|
289
|
+
prebuilt_bin = File.join(root, "desktop", "ruflet_client.app", "Contents", "MacOS", "ruflet_client")
|
|
290
|
+
executable = [release_bin, debug_bin].find { |p| File.file?(p) && File.executable?(p) }
|
|
291
|
+
executable ||= prebuilt_bin if File.file?(prebuilt_bin) && File.executable?(prebuilt_bin)
|
|
292
|
+
return [executable, url] if executable
|
|
293
|
+
elsif host_os.match?(/mswin|mingw|cygwin/i)
|
|
294
|
+
exe = File.join(root, "build", "windows", "x64", "runner", "Release", "ruflet_client.exe")
|
|
295
|
+
prebuilt = File.join(root, "desktop", "ruflet_client.exe")
|
|
296
|
+
exe = prebuilt if !File.file?(exe) && File.file?(prebuilt)
|
|
297
|
+
return [exe, url] if File.file?(exe)
|
|
298
|
+
else
|
|
299
|
+
direct = File.join(root, "build", "linux", "x64", "release", "bundle", "ruflet_client")
|
|
300
|
+
prebuilt_direct = File.join(root, "desktop", "ruflet_client")
|
|
301
|
+
direct = prebuilt_direct if !File.file?(direct) && File.file?(prebuilt_direct)
|
|
302
|
+
return [direct, url] if File.file?(direct)
|
|
303
|
+
bundle_dir = File.join(root, "build", "linux", "x64", "release", "bundle")
|
|
304
|
+
if Dir.exist?(bundle_dir)
|
|
305
|
+
candidate = Dir.children(bundle_dir).map { |f| File.join(bundle_dir, f) }
|
|
306
|
+
.find { |path| File.file?(path) && File.executable?(path) }
|
|
307
|
+
return [candidate, url] if candidate
|
|
308
|
+
end
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
nil
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def detect_web_client_dir
|
|
315
|
+
root = ENV["RUFLET_CLIENT_DIR"]
|
|
316
|
+
root = File.expand_path("ruflet_client", Dir.pwd) if root.to_s.strip.empty?
|
|
317
|
+
root = nil unless Dir.exist?(root)
|
|
318
|
+
root ||= ensure_prebuilt_client(web: true)
|
|
319
|
+
return nil unless root && Dir.exist?(root)
|
|
320
|
+
|
|
321
|
+
built = File.join(root, "build", "web")
|
|
322
|
+
return built if Dir.exist?(built) && File.file?(File.join(built, "index.html"))
|
|
323
|
+
prebuilt = File.join(root, "web")
|
|
324
|
+
return prebuilt if Dir.exist?(prebuilt) && File.file?(File.join(prebuilt, "index.html"))
|
|
325
|
+
|
|
326
|
+
nil
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def ensure_prebuilt_client(web: false, desktop: false)
|
|
330
|
+
platform = host_platform_name
|
|
331
|
+
return nil if platform.nil?
|
|
332
|
+
|
|
333
|
+
cache_root = File.join(Dir.home, ".ruflet", "client", ruflet_version, platform)
|
|
334
|
+
FileUtils.mkdir_p(cache_root)
|
|
335
|
+
|
|
336
|
+
wanted_assets = []
|
|
337
|
+
wanted_assets << { kind: :web, name: "ruflet_client-web.tar.gz" } if web
|
|
338
|
+
if desktop
|
|
339
|
+
desktop_asset = desktop_asset_name_for(platform)
|
|
340
|
+
return nil if desktop_asset.nil?
|
|
341
|
+
wanted_assets << { kind: :desktop, name: desktop_asset, platform: platform }
|
|
342
|
+
end
|
|
343
|
+
return cache_root if wanted_assets.empty? || prebuilt_assets_present?(cache_root, web: web, desktop: desktop)
|
|
344
|
+
|
|
345
|
+
release = fetch_release_for_version
|
|
346
|
+
return nil unless release
|
|
347
|
+
|
|
348
|
+
assets = release.fetch("assets", [])
|
|
349
|
+
asset_names = assets.map { |a| a["name"].to_s }
|
|
350
|
+
Dir.mktmpdir("ruflet-prebuilt-") do |tmpdir|
|
|
351
|
+
wanted_assets.each do |wanted|
|
|
352
|
+
asset_name = wanted.fetch(:name)
|
|
353
|
+
asset = assets.find { |a| a["name"] == asset_name }
|
|
354
|
+
asset ||= fallback_release_asset(assets, wanted)
|
|
355
|
+
unless asset
|
|
356
|
+
warn "Missing release asset: #{asset_name}"
|
|
357
|
+
warn "Available assets: #{asset_names.join(', ')}" unless asset_names.empty?
|
|
358
|
+
return nil
|
|
359
|
+
end
|
|
360
|
+
resolved_name = asset.fetch("name")
|
|
361
|
+
puts "Downloading prebuilt client asset: #{resolved_name}"
|
|
362
|
+
archive_path = File.join(tmpdir, resolved_name)
|
|
363
|
+
download_file(asset.fetch("browser_download_url"), archive_path)
|
|
364
|
+
subdir = wanted[:kind] == :web ? "web" : "desktop"
|
|
365
|
+
target = File.join(cache_root, subdir)
|
|
366
|
+
FileUtils.mkdir_p(target)
|
|
367
|
+
unless extract_archive(archive_path, target)
|
|
368
|
+
warn "Failed to extract asset: #{resolved_name}"
|
|
369
|
+
return nil
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
return cache_root if prebuilt_assets_present?(cache_root, web: web, desktop: desktop)
|
|
375
|
+
|
|
376
|
+
nil
|
|
377
|
+
rescue StandardError => e
|
|
378
|
+
warn "Prebuilt client bootstrap failed: #{e.class}: #{e.message}"
|
|
379
|
+
nil
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
def prebuilt_assets_present?(root, web:, desktop:)
|
|
383
|
+
ok_web = !web || File.file?(File.join(root, "web", "index.html"))
|
|
384
|
+
ok_desktop = !desktop || prebuilt_desktop_present?(root)
|
|
385
|
+
ok_web && ok_desktop
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
def prebuilt_desktop_present?(root)
|
|
389
|
+
platform = host_platform_name
|
|
390
|
+
return false if platform.nil?
|
|
391
|
+
|
|
392
|
+
case platform
|
|
393
|
+
when "macos"
|
|
394
|
+
File.file?(File.join(root, "desktop", "ruflet_client.app", "Contents", "MacOS", "ruflet_client"))
|
|
395
|
+
when "linux"
|
|
396
|
+
File.file?(File.join(root, "desktop", "ruflet_client"))
|
|
397
|
+
when "windows"
|
|
398
|
+
File.file?(File.join(root, "desktop", "ruflet_client.exe"))
|
|
399
|
+
else
|
|
400
|
+
false
|
|
401
|
+
end
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
def host_platform_name
|
|
405
|
+
host_os = RbConfig::CONFIG["host_os"]
|
|
406
|
+
return "macos" if host_os.match?(/darwin/i)
|
|
407
|
+
return "linux" if host_os.match?(/linux/i)
|
|
408
|
+
return "windows" if host_os.match?(/mswin|mingw|cygwin/i)
|
|
409
|
+
|
|
410
|
+
nil
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
def desktop_asset_name_for(platform)
|
|
414
|
+
case platform
|
|
415
|
+
when "macos" then "ruflet_client-macos-universal.zip"
|
|
416
|
+
when "linux" then "ruflet_client-linux-x64.tar.gz"
|
|
417
|
+
when "windows" then "ruflet_client-windows-x64.zip"
|
|
418
|
+
end
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
def fetch_release_for_version
|
|
422
|
+
release_by_tag("v#{ruflet_version}") ||
|
|
423
|
+
release_by_tag(ruflet_version) ||
|
|
424
|
+
release_by_tag("prebuild") ||
|
|
425
|
+
release_by_tag("prebuild-main") ||
|
|
426
|
+
release_latest
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
def ruflet_version
|
|
430
|
+
return Ruflet::VERSION if Ruflet.const_defined?(:VERSION)
|
|
431
|
+
|
|
432
|
+
require_relative "../version"
|
|
433
|
+
Ruflet::VERSION
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
def release_latest
|
|
437
|
+
github_get_json("https://api.github.com/repos/AdamMusa/Ruflet/releases/latest")
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
def release_by_tag(tag)
|
|
441
|
+
github_get_json("https://api.github.com/repos/AdamMusa/Ruflet/releases/tags/#{tag}")
|
|
442
|
+
rescue StandardError
|
|
443
|
+
nil
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
def fallback_release_asset(assets, wanted)
|
|
447
|
+
kind = wanted[:kind]
|
|
448
|
+
platform = wanted[:platform]
|
|
449
|
+
candidates = assets.select { |asset| release_asset_matches?(asset.fetch("name", ""), kind, platform) }
|
|
450
|
+
candidates.first
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
def release_asset_matches?(name, kind, platform)
|
|
454
|
+
n = name.to_s.downcase
|
|
455
|
+
return false unless n.include?("ruflet_client")
|
|
456
|
+
|
|
457
|
+
if kind == :web
|
|
458
|
+
return n.include?("web") && (n.end_with?(".tar.gz") || n.end_with?(".zip"))
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
case platform
|
|
462
|
+
when "macos"
|
|
463
|
+
n.include?("macos") && n.end_with?(".zip")
|
|
464
|
+
when "linux"
|
|
465
|
+
n.include?("linux") && (n.end_with?(".tar.gz") || n.end_with?(".tgz"))
|
|
466
|
+
when "windows"
|
|
467
|
+
n.include?("windows") && n.end_with?(".zip")
|
|
468
|
+
else
|
|
469
|
+
false
|
|
470
|
+
end
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
def github_get_json(url)
|
|
474
|
+
uri = URI(url)
|
|
475
|
+
response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
|
|
476
|
+
req = Net::HTTP::Get.new(uri)
|
|
477
|
+
req["Accept"] = "application/vnd.github+json"
|
|
478
|
+
req["User-Agent"] = "ruflet-cli"
|
|
479
|
+
http.request(req)
|
|
480
|
+
end
|
|
481
|
+
return JSON.parse(response.body) if response.is_a?(Net::HTTPSuccess)
|
|
482
|
+
|
|
483
|
+
raise "GitHub API failed (#{response.code})"
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
def download_file(url, destination, limit: 5)
|
|
487
|
+
raise "Too many redirects while downloading #{url}" if limit <= 0
|
|
488
|
+
|
|
489
|
+
uri = URI(url)
|
|
490
|
+
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
|
|
491
|
+
req = Net::HTTP::Get.new(uri)
|
|
492
|
+
req["User-Agent"] = "ruflet-cli"
|
|
493
|
+
http.request(req) do |res|
|
|
494
|
+
case res
|
|
495
|
+
when Net::HTTPSuccess
|
|
496
|
+
File.open(destination, "wb") { |f| res.read_body { |chunk| f.write(chunk) } }
|
|
497
|
+
return destination
|
|
498
|
+
when Net::HTTPRedirection
|
|
499
|
+
return download_file(res["location"], destination, limit: limit - 1)
|
|
500
|
+
else
|
|
501
|
+
raise "Download failed (#{res.code})"
|
|
502
|
+
end
|
|
503
|
+
end
|
|
504
|
+
end
|
|
505
|
+
end
|
|
506
|
+
|
|
507
|
+
def extract_archive(archive, destination)
|
|
508
|
+
if archive.end_with?(".tar.gz")
|
|
509
|
+
return system("tar", "-xzf", archive, "-C", destination, out: File::NULL, err: File::NULL)
|
|
510
|
+
end
|
|
511
|
+
if archive.end_with?(".zip")
|
|
512
|
+
host_os = RbConfig::CONFIG["host_os"]
|
|
513
|
+
if host_os.match?(/darwin/i)
|
|
514
|
+
return system("ditto", "-x", "-k", archive, destination, out: File::NULL, err: File::NULL)
|
|
515
|
+
end
|
|
516
|
+
return system("unzip", "-oq", archive, "-d", destination, out: File::NULL, err: File::NULL)
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
false
|
|
520
|
+
end
|
|
521
|
+
|
|
88
522
|
def print_mobile_qr_hint(port: 8550)
|
|
89
523
|
host = best_lan_host
|
|
90
524
|
payload = "http://#{host}:#{port}"
|
|
@@ -121,6 +555,32 @@ module Ruflet
|
|
|
121
555
|
start_port
|
|
122
556
|
end
|
|
123
557
|
|
|
558
|
+
def resolve_backend_port(target)
|
|
559
|
+
return find_available_port(8550) if target == "mobile"
|
|
560
|
+
|
|
561
|
+
return 8550 if port_available?(8550)
|
|
562
|
+
|
|
563
|
+
warn "Port 8550 is required for `ruflet run --#{target}`."
|
|
564
|
+
warn "Stop the process using 8550 and run again."
|
|
565
|
+
nil
|
|
566
|
+
end
|
|
567
|
+
|
|
568
|
+
def port_available?(port)
|
|
569
|
+
probe = nil
|
|
570
|
+
begin
|
|
571
|
+
begin
|
|
572
|
+
probe = TCPServer.new("0.0.0.0", port)
|
|
573
|
+
rescue Errno::EACCES, Errno::EPERM
|
|
574
|
+
probe = TCPServer.new("127.0.0.1", port)
|
|
575
|
+
end
|
|
576
|
+
true
|
|
577
|
+
rescue Errno::EADDRINUSE
|
|
578
|
+
false
|
|
579
|
+
ensure
|
|
580
|
+
probe&.close
|
|
581
|
+
end
|
|
582
|
+
end
|
|
583
|
+
|
|
124
584
|
def best_lan_host
|
|
125
585
|
ips = Socket.ip_address_list
|
|
126
586
|
addr = ips.find { |ip| ip.ipv4_private? && !ip.ipv4_loopback? }
|
data/lib/ruflet/cli/templates.rb
CHANGED
|
@@ -15,28 +15,28 @@ module Ruflet
|
|
|
15
15
|
page.title = "Counter Demo"
|
|
16
16
|
page.vertical_alignment = Ruflet::MainAxisAlignment::CENTER
|
|
17
17
|
page.horizontal_alignment = Ruflet::CrossAxisAlignment::CENTER
|
|
18
|
-
count_text =
|
|
18
|
+
count_text = text(value: @count.to_s, size: 40)
|
|
19
19
|
|
|
20
20
|
page.add(
|
|
21
|
-
|
|
21
|
+
container(
|
|
22
22
|
expand: true,
|
|
23
23
|
padding: 24,
|
|
24
|
-
content:
|
|
24
|
+
content: column(
|
|
25
25
|
expand: true,
|
|
26
26
|
alignment: Ruflet::MainAxisAlignment::CENTER,
|
|
27
27
|
horizontal_alignment: Ruflet::CrossAxisAlignment::CENTER,
|
|
28
28
|
spacing: 12,
|
|
29
29
|
controls: [
|
|
30
|
-
|
|
30
|
+
text(value: "You have pushed the button this many times:"),
|
|
31
31
|
count_text
|
|
32
32
|
]
|
|
33
33
|
)
|
|
34
34
|
),
|
|
35
|
-
appbar:
|
|
36
|
-
title:
|
|
35
|
+
appbar: app_bar(
|
|
36
|
+
title: text(value: "Counter Demo")
|
|
37
37
|
),
|
|
38
|
-
floating_action_button:
|
|
39
|
-
|
|
38
|
+
floating_action_button: fab(
|
|
39
|
+
icon(icon: Ruflet::MaterialIcons::ADD),
|
|
40
40
|
on_click: ->(_e) {
|
|
41
41
|
@count += 1
|
|
42
42
|
page.update(count_text, value: @count.to_s)
|
|
@@ -57,12 +57,6 @@ module Ruflet
|
|
|
57
57
|
gem "ruflet_server", ">= 0.0.3"
|
|
58
58
|
GEMFILE
|
|
59
59
|
|
|
60
|
-
BUNDLE_CONFIG_TEMPLATE = <<~YAML
|
|
61
|
-
---
|
|
62
|
-
BUNDLE_PATH: "vendor/bundle"
|
|
63
|
-
BUNDLE_DISABLE_SHARED_GEMS: "true"
|
|
64
|
-
YAML
|
|
65
|
-
|
|
66
60
|
README_TEMPLATE = <<~MD
|
|
67
61
|
# %<app_name>s
|
|
68
62
|
|
data/lib/ruflet/cli.rb
CHANGED
|
@@ -5,7 +5,9 @@ require "optparse"
|
|
|
5
5
|
require_relative "cli/templates"
|
|
6
6
|
require_relative "cli/new_command"
|
|
7
7
|
require_relative "cli/run_command"
|
|
8
|
+
require_relative "cli/flutter_sdk"
|
|
8
9
|
require_relative "cli/build_command"
|
|
10
|
+
require_relative "cli/extra_command"
|
|
9
11
|
|
|
10
12
|
module Ruflet
|
|
11
13
|
module CLI
|
|
@@ -13,17 +15,28 @@ module Ruflet
|
|
|
13
15
|
extend NewCommand
|
|
14
16
|
extend RunCommand
|
|
15
17
|
extend BuildCommand
|
|
18
|
+
extend ExtraCommand
|
|
16
19
|
|
|
17
20
|
def run(argv = ARGV)
|
|
18
21
|
command = (argv.shift || "help").downcase
|
|
19
22
|
|
|
20
23
|
case command
|
|
24
|
+
when "create"
|
|
25
|
+
command_create(argv)
|
|
21
26
|
when "new", "bootstrap", "init"
|
|
22
27
|
command_new(argv)
|
|
23
28
|
when "run"
|
|
24
29
|
command_run(argv)
|
|
30
|
+
when "debug"
|
|
31
|
+
command_debug(argv)
|
|
25
32
|
when "build"
|
|
26
33
|
command_build(argv)
|
|
34
|
+
when "devices"
|
|
35
|
+
command_devices(argv)
|
|
36
|
+
when "emulators"
|
|
37
|
+
command_emulators(argv)
|
|
38
|
+
when "doctor"
|
|
39
|
+
command_doctor(argv)
|
|
27
40
|
when "help", "-h", "--help"
|
|
28
41
|
print_help
|
|
29
42
|
0
|
|
@@ -39,9 +52,14 @@ module Ruflet
|
|
|
39
52
|
Ruflet CLI
|
|
40
53
|
|
|
41
54
|
Commands:
|
|
55
|
+
ruflet create <appname>
|
|
42
56
|
ruflet new <appname>
|
|
43
57
|
ruflet run [scriptname|path] [--web|--mobile|--desktop]
|
|
58
|
+
ruflet debug [scriptname|path]
|
|
44
59
|
ruflet build <apk|ios|aab|web|macos|windows|linux>
|
|
60
|
+
ruflet devices
|
|
61
|
+
ruflet emulators
|
|
62
|
+
ruflet doctor
|
|
45
63
|
HELP
|
|
46
64
|
end
|
|
47
65
|
|
data/lib/ruflet/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ruflet_cli
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0.
|
|
4
|
+
version: 0.0.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- AdamMusa
|
|
@@ -35,6 +35,8 @@ files:
|
|
|
35
35
|
- bin/ruflet
|
|
36
36
|
- lib/ruflet/cli.rb
|
|
37
37
|
- lib/ruflet/cli/build_command.rb
|
|
38
|
+
- lib/ruflet/cli/extra_command.rb
|
|
39
|
+
- lib/ruflet/cli/flutter_sdk.rb
|
|
38
40
|
- lib/ruflet/cli/new_command.rb
|
|
39
41
|
- lib/ruflet/cli/run_command.rb
|
|
40
42
|
- lib/ruflet/cli/templates.rb
|