markymark 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/.rspec +3 -0
- data/CHANGELOG.md +29 -0
- data/LICENSE.txt +21 -0
- data/README.md +255 -0
- data/Rakefile +8 -0
- data/assets/.gitkeep +0 -0
- data/assets/Markymark.icns +0 -0
- data/assets/Markymark.iconset/icon_128x128.png +0 -0
- data/assets/Markymark.iconset/icon_128x128@2x.png +0 -0
- data/assets/Markymark.iconset/icon_16x16.png +0 -0
- data/assets/Markymark.iconset/icon_16x16@2x.png +0 -0
- data/assets/Markymark.iconset/icon_256x256.png +0 -0
- data/assets/Markymark.iconset/icon_256x256@2x.png +0 -0
- data/assets/Markymark.iconset/icon_32x32.png +0 -0
- data/assets/Markymark.iconset/icon_32x32@2x.png +0 -0
- data/assets/Markymark.iconset/icon_512x512.png +0 -0
- data/assets/Markymark.iconset/icon_512x512@2x.png +0 -0
- data/assets/README.md +3 -0
- data/assets/marky-mark-dj.jpg +0 -0
- data/assets/marky-mark-icon.png +0 -0
- data/assets/marky-mark-icon2.png +0 -0
- data/config.ru +19 -0
- data/docs/for_llms.md +141 -0
- data/docs/plans/2025-12-18-macos-app-installer-design.md +149 -0
- data/exe/markymark +5 -0
- data/lib/markymark/app_installer.rb +437 -0
- data/lib/markymark/cli.rb +497 -0
- data/lib/markymark/init_wizard.rb +186 -0
- data/lib/markymark/pumadev_manager.rb +194 -0
- data/lib/markymark/server_simple.rb +452 -0
- data/lib/markymark/version.rb +5 -0
- data/lib/markymark.rb +12 -0
- data/lib/public/css/style.css +350 -0
- data/lib/public/js/app.js +186 -0
- data/lib/public/js/theme.js +79 -0
- data/lib/public/js/tree.js +124 -0
- data/lib/views/browse.erb +225 -0
- data/lib/views/index.erb +37 -0
- data/lib/views/simple.erb +806 -0
- data/sig/markymark.rbs +4 -0
- metadata +242 -0
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'optparse'
|
|
4
|
+
require 'net/http'
|
|
5
|
+
require 'json'
|
|
6
|
+
require 'socket'
|
|
7
|
+
|
|
8
|
+
module Markymark
|
|
9
|
+
# Command-line interface handler
|
|
10
|
+
class CLI
|
|
11
|
+
DEFAULT_PORT = 4545
|
|
12
|
+
|
|
13
|
+
attr_reader :root_path, :port, :open_browser, :initial_file
|
|
14
|
+
|
|
15
|
+
def initialize(args)
|
|
16
|
+
@root_path = Dir.pwd
|
|
17
|
+
@port = DEFAULT_PORT
|
|
18
|
+
@open_browser = true
|
|
19
|
+
@initial_file = nil
|
|
20
|
+
|
|
21
|
+
parse_options(args)
|
|
22
|
+
validate!
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.run(args)
|
|
26
|
+
cli = new(args)
|
|
27
|
+
|
|
28
|
+
# Check if pumadev mode is active
|
|
29
|
+
if PumadevManager.active?
|
|
30
|
+
puts "Pumadev mode is active"
|
|
31
|
+
# In pumadev mode - switch directory via pumadev manager
|
|
32
|
+
if PumadevManager.switch_directory(cli.root_path)
|
|
33
|
+
puts "Switched to #{cli.root_path}"
|
|
34
|
+
puts "Access markymark at: http://markymark.test"
|
|
35
|
+
|
|
36
|
+
# Open browser if requested
|
|
37
|
+
if cli.open_browser
|
|
38
|
+
require 'launchy'
|
|
39
|
+
url = if cli.initial_file
|
|
40
|
+
"http://markymark.test/?file=#{CGI.escape(cli.initial_file)}"
|
|
41
|
+
else
|
|
42
|
+
'http://markymark.test'
|
|
43
|
+
end
|
|
44
|
+
Launchy.open(url)
|
|
45
|
+
end
|
|
46
|
+
else
|
|
47
|
+
warn "Failed to switch directory in pumadev mode"
|
|
48
|
+
exit 1
|
|
49
|
+
end
|
|
50
|
+
return
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Check if a standalone server is already running
|
|
54
|
+
existing_server = cli.send(:detect_existing_server)
|
|
55
|
+
|
|
56
|
+
if existing_server
|
|
57
|
+
# Server exists - switch it to the new directory
|
|
58
|
+
cli.send(:handle_existing_server, existing_server)
|
|
59
|
+
else
|
|
60
|
+
# No server running - start a new one (check for port conflicts)
|
|
61
|
+
if cli.send(:port_available?, cli.port)
|
|
62
|
+
ServerSimple.launch(cli)
|
|
63
|
+
else
|
|
64
|
+
# Port is busy - prompt user
|
|
65
|
+
cli.send(:handle_port_conflict)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
rescue => e
|
|
69
|
+
warn "Error: #{e.message}"
|
|
70
|
+
warn e.backtrace.join("\n")
|
|
71
|
+
exit 1
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def self.stop_server
|
|
75
|
+
# Check if pumadev mode is active first
|
|
76
|
+
if PumadevManager.active?
|
|
77
|
+
PumadevManager.teardown
|
|
78
|
+
exit 0
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Check for standalone server
|
|
82
|
+
pid_file = File.expand_path('~/.markymark/server.pid')
|
|
83
|
+
|
|
84
|
+
unless File.exist?(pid_file)
|
|
85
|
+
puts "No markymark server is running"
|
|
86
|
+
exit 0
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Read PID file
|
|
90
|
+
content = File.read(pid_file)
|
|
91
|
+
port = content[/port=(\d+)/, 1]&.to_i
|
|
92
|
+
pid = content[/pid=(\d+)/, 1]&.to_i
|
|
93
|
+
|
|
94
|
+
unless pid
|
|
95
|
+
puts "Invalid PID file"
|
|
96
|
+
File.delete(pid_file)
|
|
97
|
+
exit 1
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Check if process is still running
|
|
101
|
+
begin
|
|
102
|
+
Process.kill(0, pid)
|
|
103
|
+
rescue Errno::ESRCH
|
|
104
|
+
puts "Server process not found (PID: #{pid})"
|
|
105
|
+
File.delete(pid_file)
|
|
106
|
+
exit 0
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
puts "Stopping markymark server (PID: #{pid}, port: #{port})..."
|
|
110
|
+
|
|
111
|
+
# Try graceful shutdown first (SIGTERM)
|
|
112
|
+
begin
|
|
113
|
+
Process.kill('TERM', pid)
|
|
114
|
+
|
|
115
|
+
# Wait up to 3 seconds for graceful shutdown
|
|
116
|
+
3.times do
|
|
117
|
+
sleep 1
|
|
118
|
+
begin
|
|
119
|
+
Process.kill(0, pid)
|
|
120
|
+
rescue Errno::ESRCH
|
|
121
|
+
# Process has exited
|
|
122
|
+
puts "Server stopped successfully"
|
|
123
|
+
File.delete(pid_file) if File.exist?(pid_file)
|
|
124
|
+
exit 0
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# If still running, force kill
|
|
129
|
+
puts "Server didn't stop gracefully, forcing shutdown..."
|
|
130
|
+
Process.kill('KILL', pid)
|
|
131
|
+
sleep 1
|
|
132
|
+
|
|
133
|
+
puts "Server stopped (forced)"
|
|
134
|
+
File.delete(pid_file) if File.exist?(pid_file)
|
|
135
|
+
rescue Errno::ESRCH
|
|
136
|
+
puts "Server stopped successfully"
|
|
137
|
+
File.delete(pid_file) if File.exist?(pid_file)
|
|
138
|
+
rescue => e
|
|
139
|
+
warn "Error stopping server: #{e.message}"
|
|
140
|
+
exit 1
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def self.show_status
|
|
145
|
+
puts "Markymark Status"
|
|
146
|
+
puts "=" * 40
|
|
147
|
+
|
|
148
|
+
# Check macOS app status
|
|
149
|
+
if RUBY_PLATFORM.include?('darwin')
|
|
150
|
+
app_status = AppInstaller.status
|
|
151
|
+
if app_status
|
|
152
|
+
puts ""
|
|
153
|
+
puts "macOS App:"
|
|
154
|
+
puts " #{app_status[:message]}"
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Check pumadev
|
|
159
|
+
pumadev_status = PumadevManager.status
|
|
160
|
+
if pumadev_status
|
|
161
|
+
puts ""
|
|
162
|
+
puts "Server Mode: Pumadev"
|
|
163
|
+
puts " #{pumadev_status[:message]}"
|
|
164
|
+
return
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Check standalone server
|
|
168
|
+
pid_file = File.expand_path('~/.markymark/server.pid')
|
|
169
|
+
unless File.exist?(pid_file)
|
|
170
|
+
puts ""
|
|
171
|
+
puts "Server: Not running"
|
|
172
|
+
return
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
content = File.read(pid_file)
|
|
176
|
+
port = content[/port=(\d+)/, 1]&.to_i
|
|
177
|
+
pid = content[/pid=(\d+)/, 1]&.to_i
|
|
178
|
+
|
|
179
|
+
# Check if process is still running
|
|
180
|
+
begin
|
|
181
|
+
Process.kill(0, pid)
|
|
182
|
+
puts ""
|
|
183
|
+
puts "Server Mode: Standalone"
|
|
184
|
+
puts " Running (PID: #{pid}, port: #{port})"
|
|
185
|
+
puts " Access at: http://localhost:#{port}"
|
|
186
|
+
rescue Errno::ESRCH
|
|
187
|
+
puts ""
|
|
188
|
+
puts "Server: Not running (stale PID file removed)"
|
|
189
|
+
File.delete(pid_file)
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def self.show_pumadev_instructions
|
|
194
|
+
puts <<~INSTRUCTIONS
|
|
195
|
+
Pumadev Setup for markymark
|
|
196
|
+
============================
|
|
197
|
+
|
|
198
|
+
Pumadev allows you to access markymark via a .test domain instead of remembering ports.
|
|
199
|
+
|
|
200
|
+
Quick Setup:
|
|
201
|
+
markymark --setup-pumadev [PATH]
|
|
202
|
+
|
|
203
|
+
Manual Setup Instructions:
|
|
204
|
+
|
|
205
|
+
1. Install pumadev (if not already installed):
|
|
206
|
+
gem install puma-dev
|
|
207
|
+
|
|
208
|
+
2. Set up pumadev:
|
|
209
|
+
sudo puma-dev -setup
|
|
210
|
+
puma-dev -install
|
|
211
|
+
|
|
212
|
+
3. Find your markymark gem installation:
|
|
213
|
+
gem which markymark
|
|
214
|
+
|
|
215
|
+
4. Create a symlink in ~/.puma-dev/:
|
|
216
|
+
cd ~/.puma-dev
|
|
217
|
+
ln -s /path/to/markymark/gem markymark
|
|
218
|
+
|
|
219
|
+
5. Access markymark at:
|
|
220
|
+
http://markymark.test
|
|
221
|
+
|
|
222
|
+
Notes for Ruby Version Manager Users:
|
|
223
|
+
- Install markymark in your global gemset of your default Ruby
|
|
224
|
+
- This ensures the command is available across all Ruby versions
|
|
225
|
+
- The symlink points to the gem location, which pumadev will use
|
|
226
|
+
|
|
227
|
+
Smart Directory Switching:
|
|
228
|
+
- The smart directory switching feature works with pumadev
|
|
229
|
+
- Run 'markymark [PATH]' from any directory to switch the server
|
|
230
|
+
- The directory setting persists across requests
|
|
231
|
+
|
|
232
|
+
For more information: https://github.com/puma/puma-dev
|
|
233
|
+
INSTRUCTIONS
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
private
|
|
237
|
+
|
|
238
|
+
def parse_options(args)
|
|
239
|
+
parser = OptionParser.new do |opts|
|
|
240
|
+
opts.banner = <<~BANNER
|
|
241
|
+
markymark - Browse markdown documentation with live reload
|
|
242
|
+
|
|
243
|
+
Usage: markymark [PATH] [OPTIONS]
|
|
244
|
+
markymark init [-y]
|
|
245
|
+
|
|
246
|
+
Arguments:
|
|
247
|
+
PATH Directory or markdown file to browse (default: current directory)
|
|
248
|
+
If a file is specified, opens that directory with the file displayed
|
|
249
|
+
|
|
250
|
+
Commands:
|
|
251
|
+
init Interactive setup wizard (use -y to accept defaults)
|
|
252
|
+
|
|
253
|
+
Options:
|
|
254
|
+
BANNER
|
|
255
|
+
|
|
256
|
+
opts.on('-p', '--port PORT', Integer, "Port to run server on (default: #{DEFAULT_PORT})") do |p|
|
|
257
|
+
@port = p
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
opts.on('--no-browser', 'Do not auto-open browser on startup') do
|
|
261
|
+
@open_browser = false
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
opts.on('--stop', 'Stop the running markymark server') do
|
|
265
|
+
CLI.stop_server
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
opts.on('-h', '--help', 'Show this help message') do
|
|
269
|
+
puts opts
|
|
270
|
+
exit
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
opts.on('-v', '--version', 'Show version') do
|
|
274
|
+
puts "markymark #{Markymark::VERSION}"
|
|
275
|
+
exit
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
opts.on('--setup-pumadev [PATH]', 'Setup pumadev integration with optional path') do |path|
|
|
279
|
+
if PumadevManager.setup(path)
|
|
280
|
+
require 'launchy'
|
|
281
|
+
Launchy.open('http://markymark.test')
|
|
282
|
+
else
|
|
283
|
+
exit 1
|
|
284
|
+
end
|
|
285
|
+
exit 0
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
opts.on('--status', 'Show current server status') do
|
|
289
|
+
CLI.show_status
|
|
290
|
+
exit
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
opts.on('--pumadev-info', 'Show pumadev setup instructions') do
|
|
294
|
+
CLI.show_pumadev_instructions
|
|
295
|
+
exit
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
opts.on('--install-app', 'Install macOS app bundle to ~/Applications') do
|
|
299
|
+
if AppInstaller.install
|
|
300
|
+
exit 0
|
|
301
|
+
else
|
|
302
|
+
exit 1
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
opts.on('--uninstall-app', 'Remove macOS app bundle') do
|
|
307
|
+
if AppInstaller.uninstall
|
|
308
|
+
exit 0
|
|
309
|
+
else
|
|
310
|
+
exit 1
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
opts.on('--set-default', 'Set Markymark as default handler for .md files') do
|
|
315
|
+
if AppInstaller.set_default_handler
|
|
316
|
+
exit 0
|
|
317
|
+
else
|
|
318
|
+
exit 1
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
parser.parse!(args)
|
|
324
|
+
|
|
325
|
+
# Handle 'init' subcommand
|
|
326
|
+
if args.first == 'init'
|
|
327
|
+
args.shift
|
|
328
|
+
accept_defaults = args.include?('-y') || args.include?('--yes')
|
|
329
|
+
wizard = InitWizard.new(accept_defaults: accept_defaults)
|
|
330
|
+
wizard.run
|
|
331
|
+
exit 0
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
# First non-option argument is the path
|
|
335
|
+
@root_path = args.first if args.any?
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
def validate!
|
|
339
|
+
expanded_path = File.expand_path(@root_path)
|
|
340
|
+
|
|
341
|
+
unless File.exist?(expanded_path)
|
|
342
|
+
raise ArgumentError, "Path does not exist: #{@root_path}"
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
# If path is a file, extract directory and filename
|
|
346
|
+
if File.file?(expanded_path)
|
|
347
|
+
unless expanded_path =~ /\.(md|markdown)$/i
|
|
348
|
+
raise ArgumentError, "File must be a markdown file (.md or .markdown): #{@root_path}"
|
|
349
|
+
end
|
|
350
|
+
@initial_file = File.basename(expanded_path)
|
|
351
|
+
@root_path = File.realpath(File.dirname(expanded_path))
|
|
352
|
+
else
|
|
353
|
+
@root_path = File.realpath(expanded_path)
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
unless @port.between?(1, 65535)
|
|
357
|
+
raise ArgumentError, "Port must be between 1 and 65535"
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
# Detect if a markymark server is already running
|
|
362
|
+
def detect_existing_server
|
|
363
|
+
pid_file = File.expand_path('~/.markymark/server.pid')
|
|
364
|
+
return nil unless File.exist?(pid_file)
|
|
365
|
+
|
|
366
|
+
# Parse PID file
|
|
367
|
+
content = File.read(pid_file)
|
|
368
|
+
port = content[/port=(\d+)/, 1]&.to_i
|
|
369
|
+
pid = content[/pid=(\d+)/, 1]&.to_i
|
|
370
|
+
|
|
371
|
+
return nil unless port && pid
|
|
372
|
+
|
|
373
|
+
# Check if process is still running
|
|
374
|
+
begin
|
|
375
|
+
Process.kill(0, pid)
|
|
376
|
+
rescue Errno::ESRCH
|
|
377
|
+
# Process not found - clean up stale PID file
|
|
378
|
+
File.delete(pid_file)
|
|
379
|
+
return nil
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
# Verify it's actually a markymark server by checking /api/status
|
|
383
|
+
begin
|
|
384
|
+
uri = URI("http://localhost:#{port}/api/status")
|
|
385
|
+
response = Net::HTTP.get_response(uri)
|
|
386
|
+
if response.is_a?(Net::HTTPSuccess)
|
|
387
|
+
data = JSON.parse(response.body)
|
|
388
|
+
return { port: port, pid: pid } if data['app'] == 'markymark'
|
|
389
|
+
end
|
|
390
|
+
rescue => e
|
|
391
|
+
# Server not responding or not markymark - ignore
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
nil
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
# Handle existing server - switch directory and open browser
|
|
398
|
+
def handle_existing_server(server_info)
|
|
399
|
+
puts "Found existing markymark server on port #{server_info[:port]}"
|
|
400
|
+
puts "Switching to #{@root_path}..."
|
|
401
|
+
|
|
402
|
+
if switch_server_directory(server_info[:port], @root_path)
|
|
403
|
+
puts "Successfully switched to #{@root_path}"
|
|
404
|
+
|
|
405
|
+
# Open browser if requested
|
|
406
|
+
if @open_browser
|
|
407
|
+
base_url = "http://localhost:#{server_info[:port]}"
|
|
408
|
+
url = if @initial_file
|
|
409
|
+
"#{base_url}/?file=#{CGI.escape(@initial_file)}"
|
|
410
|
+
else
|
|
411
|
+
base_url
|
|
412
|
+
end
|
|
413
|
+
require 'launchy'
|
|
414
|
+
Launchy.open(url)
|
|
415
|
+
end
|
|
416
|
+
else
|
|
417
|
+
warn "Failed to switch directory. Starting new server instead..."
|
|
418
|
+
# Port might be available now if switch failed
|
|
419
|
+
if port_available?(@port)
|
|
420
|
+
ServerSimple.launch(self)
|
|
421
|
+
else
|
|
422
|
+
handle_port_conflict
|
|
423
|
+
end
|
|
424
|
+
end
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
# Handle port conflict with non-markymark app
|
|
428
|
+
def handle_port_conflict
|
|
429
|
+
puts "Port #{@port} is already in use by another application."
|
|
430
|
+
puts "What would you like to do?"
|
|
431
|
+
puts " 1) Start markymark on a different port"
|
|
432
|
+
puts " 2) Cancel"
|
|
433
|
+
print "Choice (1-2): "
|
|
434
|
+
|
|
435
|
+
choice = $stdin.gets&.strip
|
|
436
|
+
|
|
437
|
+
case choice
|
|
438
|
+
when "1"
|
|
439
|
+
new_port = find_available_port(@port + 1)
|
|
440
|
+
if new_port
|
|
441
|
+
puts "Starting markymark on port #{new_port}..."
|
|
442
|
+
@port = new_port
|
|
443
|
+
ServerSimple.launch(self)
|
|
444
|
+
else
|
|
445
|
+
warn "Could not find an available port"
|
|
446
|
+
exit 1
|
|
447
|
+
end
|
|
448
|
+
else
|
|
449
|
+
puts "Cancelled"
|
|
450
|
+
exit 0
|
|
451
|
+
end
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
# Switch directory on existing server
|
|
455
|
+
def switch_server_directory(port, new_path)
|
|
456
|
+
uri = URI("http://localhost:#{port}/change-dir")
|
|
457
|
+
request = Net::HTTP::Post.new(uri)
|
|
458
|
+
request.set_form_data('path' => new_path)
|
|
459
|
+
request['Accept'] = 'application/json'
|
|
460
|
+
|
|
461
|
+
begin
|
|
462
|
+
response = Net::HTTP.start(uri.hostname, uri.port, read_timeout: 5) do |http|
|
|
463
|
+
http.request(request)
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
# Server returns 303 redirect on successful directory change
|
|
467
|
+
if response.is_a?(Net::HTTPSuccess) || response.is_a?(Net::HTTPRedirection)
|
|
468
|
+
return true
|
|
469
|
+
else
|
|
470
|
+
warn "Server returned error: #{response.code}"
|
|
471
|
+
return false
|
|
472
|
+
end
|
|
473
|
+
rescue => e
|
|
474
|
+
warn "Failed to communicate with server: #{e.message}"
|
|
475
|
+
return false
|
|
476
|
+
end
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
# Check if a port is available
|
|
480
|
+
def port_available?(port)
|
|
481
|
+
TCPServer.open('127.0.0.1', port) do |server|
|
|
482
|
+
server.close
|
|
483
|
+
true
|
|
484
|
+
end
|
|
485
|
+
rescue Errno::EADDRINUSE, Errno::EACCES
|
|
486
|
+
false
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
# Find next available port starting from given port
|
|
490
|
+
def find_available_port(start_port)
|
|
491
|
+
(start_port..65535).each do |port|
|
|
492
|
+
return port if port_available?(port)
|
|
493
|
+
end
|
|
494
|
+
nil
|
|
495
|
+
end
|
|
496
|
+
end
|
|
497
|
+
end
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Markymark
|
|
4
|
+
# Interactive setup wizard for Markymark
|
|
5
|
+
class InitWizard
|
|
6
|
+
def initialize(accept_defaults: false)
|
|
7
|
+
@accept_defaults = accept_defaults
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def run
|
|
11
|
+
puts "Markymark Setup Wizard"
|
|
12
|
+
puts "=" * 40
|
|
13
|
+
puts ""
|
|
14
|
+
|
|
15
|
+
results = {
|
|
16
|
+
app_installed: false,
|
|
17
|
+
default_set: false,
|
|
18
|
+
server_mode: nil
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
# Step 1: Install macOS app (macOS only)
|
|
22
|
+
if macos?
|
|
23
|
+
results[:app_installed] = step_install_app
|
|
24
|
+
|
|
25
|
+
# Step 2: Set as default handler (only if app was installed)
|
|
26
|
+
if results[:app_installed]
|
|
27
|
+
results[:default_set] = step_set_default
|
|
28
|
+
end
|
|
29
|
+
else
|
|
30
|
+
puts "Skipping macOS app installation (not on macOS)"
|
|
31
|
+
puts ""
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Step 3: Server mode setup
|
|
35
|
+
results[:server_mode] = step_server_mode
|
|
36
|
+
|
|
37
|
+
# Summary
|
|
38
|
+
print_summary(results)
|
|
39
|
+
|
|
40
|
+
results
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def macos?
|
|
46
|
+
RUBY_PLATFORM.include?('darwin')
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def step_install_app
|
|
50
|
+
if AppInstaller.installed?
|
|
51
|
+
status = AppInstaller.status
|
|
52
|
+
if status[:valid]
|
|
53
|
+
puts "Markymark.app is already installed and valid."
|
|
54
|
+
return prompt_yes_no("Reinstall anyway?", default: false)
|
|
55
|
+
else
|
|
56
|
+
puts "Warning: #{status[:message]}"
|
|
57
|
+
reinstall = prompt_yes_no("Reinstall Markymark.app?", default: true)
|
|
58
|
+
if reinstall
|
|
59
|
+
return AppInstaller.install(force: true)
|
|
60
|
+
end
|
|
61
|
+
return false
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
install = prompt_yes_no("Install Markymark.app for macOS file handling?", default: true)
|
|
66
|
+
return false unless install
|
|
67
|
+
|
|
68
|
+
AppInstaller.install(force: true)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def step_set_default
|
|
72
|
+
set_default = prompt_yes_no("Set Markymark as default app for .md files?", default: true)
|
|
73
|
+
return false unless set_default
|
|
74
|
+
|
|
75
|
+
AppInstaller.set_default_handler
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def step_server_mode
|
|
79
|
+
puts "Server mode setup:"
|
|
80
|
+
puts " 1) Standalone (port 4545) - run 'markymark' to start"
|
|
81
|
+
puts " 2) Pumadev (.test domain) - always available at markymark.test"
|
|
82
|
+
puts " 3) Skip - configure later"
|
|
83
|
+
puts ""
|
|
84
|
+
|
|
85
|
+
if @accept_defaults
|
|
86
|
+
puts "Using default: Skip (run 'markymark' or 'markymark --setup-pumadev' later)"
|
|
87
|
+
return :skip
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
print "Choice [1/2/3, default: 3]: "
|
|
91
|
+
choice = $stdin.gets&.strip
|
|
92
|
+
|
|
93
|
+
case choice
|
|
94
|
+
when '1'
|
|
95
|
+
puts ""
|
|
96
|
+
puts "Standalone mode selected."
|
|
97
|
+
puts "Run 'markymark' or 'markymark <directory>' to start the server."
|
|
98
|
+
:standalone
|
|
99
|
+
when '2'
|
|
100
|
+
puts ""
|
|
101
|
+
setup_pumadev
|
|
102
|
+
else
|
|
103
|
+
puts ""
|
|
104
|
+
puts "Skipped. Run 'markymark' or 'markymark --setup-pumadev' later."
|
|
105
|
+
:skip
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def setup_pumadev
|
|
110
|
+
if PumadevManager.active?
|
|
111
|
+
puts "Pumadev is already configured for Markymark."
|
|
112
|
+
puts "Access at: http://markymark.test"
|
|
113
|
+
return :pumadev
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
puts "Setting up pumadev..."
|
|
117
|
+
if PumadevManager.setup
|
|
118
|
+
puts "Pumadev configured successfully!"
|
|
119
|
+
puts "Access at: http://markymark.test"
|
|
120
|
+
:pumadev
|
|
121
|
+
else
|
|
122
|
+
puts "Pumadev setup failed. You can try again with: markymark --setup-pumadev"
|
|
123
|
+
:skip
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def prompt_yes_no(question, default: true)
|
|
128
|
+
if @accept_defaults
|
|
129
|
+
puts "#{question} [Y/n] #{default ? 'Y' : 'n'} (auto)"
|
|
130
|
+
return default
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
default_hint = default ? "[Y/n]" : "[y/N]"
|
|
134
|
+
print "#{question} #{default_hint} "
|
|
135
|
+
response = $stdin.gets&.strip&.downcase
|
|
136
|
+
|
|
137
|
+
return default if response.nil? || response.empty?
|
|
138
|
+
|
|
139
|
+
case response
|
|
140
|
+
when 'y', 'yes'
|
|
141
|
+
true
|
|
142
|
+
when 'n', 'no'
|
|
143
|
+
false
|
|
144
|
+
else
|
|
145
|
+
default
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def print_summary(results)
|
|
150
|
+
puts ""
|
|
151
|
+
puts "=" * 40
|
|
152
|
+
puts "Setup Summary"
|
|
153
|
+
puts "=" * 40
|
|
154
|
+
|
|
155
|
+
if macos?
|
|
156
|
+
if results[:app_installed]
|
|
157
|
+
puts " [x] Markymark.app installed to ~/Applications"
|
|
158
|
+
else
|
|
159
|
+
puts " [ ] Markymark.app not installed"
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
if results[:default_set]
|
|
163
|
+
puts " [x] Set as default handler for .md files"
|
|
164
|
+
else
|
|
165
|
+
puts " [ ] Not set as default handler"
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
case results[:server_mode]
|
|
170
|
+
when :standalone
|
|
171
|
+
puts " [x] Standalone mode (run 'markymark' to start)"
|
|
172
|
+
when :pumadev
|
|
173
|
+
puts " [x] Pumadev mode (http://markymark.test)"
|
|
174
|
+
else
|
|
175
|
+
puts " [ ] Server mode not configured"
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
puts ""
|
|
179
|
+
puts "You can always reconfigure with:"
|
|
180
|
+
puts " markymark --install-app # Install/reinstall macOS app"
|
|
181
|
+
puts " markymark --set-default # Set as default .md handler"
|
|
182
|
+
puts " markymark --setup-pumadev # Setup pumadev integration"
|
|
183
|
+
puts ""
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|