vpn-tool-cli 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (6) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +25 -0
  3. data/bin/vpn-tool +9 -0
  4. data/lib/vpn-tool.rb +806 -0
  5. data/vpn-tool.gemspec +25 -0
  6. metadata +66 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: cacab01f3fba575403bbef54bfd9c87dcf469c4f240d0016222134f86094beed
4
+ data.tar.gz: 6edecd43b7aa01d9b6fea40bef0220aee2641a5e214093a8b7133f1f2f98a169
5
+ SHA512:
6
+ metadata.gz: e70a31060db2057413acb51ae02349ffd58bfa6547fc316f420aae97c69240411d82aceb05254aa2f2b7a16ab4d1c586746231211ca00dced035765a72e43a40
7
+ data.tar.gz: 5cd147c4b208c79947ababd730c3f84bc759aef28b7628a265cdb0261f15380905e327a567d12e7a85646cd822a377f395f2bf0af37c3a8b79cce943d4a0bf59
data/README.md ADDED
@@ -0,0 +1,25 @@
1
+ # vpn-tool
2
+
3
+ Ruby CLI for VPN gateway management.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ gem install vpn-tool
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```bash
14
+ vpn-tool --version
15
+ vpn-tool auth register
16
+ vpn-tool auth login
17
+ vpn-tool auth list
18
+ vpn-tool start --daemon
19
+ vpn-tool status --daemon
20
+ vpn-tool countries
21
+ vpn-tool connect --country US
22
+ vpn-tool disconnect
23
+ ```
24
+
25
+ Repository: https://github.com/jjjm03299-wq/vpn_tool
data/bin/vpn-tool ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH.unshift(
4
+ File.expand_path("../lib", __dir__)
5
+ )
6
+
7
+ require "vpn-tool"
8
+
9
+ VpnTool.run
data/lib/vpn-tool.rb ADDED
@@ -0,0 +1,806 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "commander-tool"
4
+ require "json"
5
+ require "net/http"
6
+ require "uri"
7
+ require "fileutils"
8
+ require "socket"
9
+
10
+ module VpnTool
11
+ BASE_URL = "https://vpn-data-cleaner-api--hb044082.replit.app"
12
+ VERSION = "1.0.0"
13
+
14
+ CONFIG_DIR = File.join(Dir.home, ".vpn-tool")
15
+ PIN_FILE = File.join(CONFIG_DIR, "pin")
16
+ SESSION_FILE = File.join(CONFIG_DIR, "session")
17
+ DAEMON_PID_FILE = File.join(CONFIG_DIR, "daemon.pid")
18
+
19
+ DAEMON_HOST = "127.0.0.1"
20
+ DAEMON_PORT = 5900
21
+
22
+ module_function
23
+
24
+ def ensure_config
25
+ FileUtils.mkdir_p(CONFIG_DIR)
26
+ File.chmod(0o700, CONFIG_DIR)
27
+ end
28
+
29
+ def file_exists?(path)
30
+ File.file?(path)
31
+ end
32
+
33
+ def read_file(path)
34
+ return nil unless file_exists?(path)
35
+
36
+ File.read(path).strip
37
+ end
38
+
39
+ def write_file(path, value)
40
+ ensure_config
41
+
42
+ File.write(path, value)
43
+ File.chmod(0o600, path)
44
+
45
+ true
46
+ rescue StandardError => e
47
+ warn "Error: #{e.message}"
48
+ false
49
+ end
50
+
51
+ def remove_file(path)
52
+ File.delete(path) if File.exist?(path)
53
+ end
54
+
55
+ def valid_pin?(pin)
56
+ pin && pin.match?(/\A\d{4}\z/)
57
+ end
58
+
59
+ def prompt(message)
60
+ print message
61
+ $stdout.flush
62
+ STDIN.gets&.chomp
63
+ end
64
+
65
+ # ----------------------------------------------------------
66
+ # Authentication
67
+ # ----------------------------------------------------------
68
+
69
+ def require_login
70
+ return true if file_exists?(SESSION_FILE)
71
+
72
+ puts "Not logged in."
73
+ puts "Run: vpn-tool auth login"
74
+
75
+ false
76
+ end
77
+
78
+ def check_pin(message)
79
+ unless file_exists?(PIN_FILE)
80
+ puts "No PIN registered."
81
+ puts "Run: vpn-tool auth register"
82
+ return false
83
+ end
84
+
85
+ pin = prompt(message)
86
+ saved_pin = read_file(PIN_FILE)
87
+
88
+ unless pin == saved_pin
89
+ puts "Incorrect PIN."
90
+ return false
91
+ end
92
+
93
+ true
94
+ end
95
+
96
+ # ----------------------------------------------------------
97
+ # Daemon
98
+ # ----------------------------------------------------------
99
+
100
+ def daemon_pid
101
+ value = read_file(DAEMON_PID_FILE)
102
+
103
+ return nil unless value&.match?(/\A\d+\z/)
104
+
105
+ value.to_i
106
+ end
107
+
108
+ def daemon_running?
109
+ pid = daemon_pid
110
+
111
+ return false unless pid && pid > 0
112
+
113
+ begin
114
+ Process.kill(0, pid)
115
+ true
116
+ rescue Errno::ESRCH
117
+ false
118
+ rescue Errno::EPERM
119
+ true
120
+ rescue StandardError
121
+ false
122
+ end
123
+ end
124
+
125
+ def daemon_connection
126
+ TCPSocket.new(
127
+ DAEMON_HOST,
128
+ DAEMON_PORT
129
+ )
130
+ rescue StandardError
131
+ nil
132
+ end
133
+
134
+ def require_daemon
135
+ socket = daemon_connection
136
+
137
+ if socket
138
+ socket.close
139
+ return true
140
+ end
141
+
142
+ puts "Cannot connect to daemon server."
143
+ puts "Run: vpn-tool start --daemon"
144
+
145
+ false
146
+ end
147
+
148
+ def start_daemon
149
+ ensure_config
150
+
151
+ if daemon_running?
152
+ puts "Daemon already running (PID #{daemon_pid})."
153
+ puts "TCP: tcp://#{DAEMON_HOST}:#{DAEMON_PORT}"
154
+ return
155
+ end
156
+
157
+ remove_file(DAEMON_PID_FILE)
158
+
159
+ pid = fork do
160
+ Process.setsid
161
+
162
+ begin
163
+ server = TCPServer.new(
164
+ DAEMON_HOST,
165
+ DAEMON_PORT
166
+ )
167
+
168
+ write_file(
169
+ DAEMON_PID_FILE,
170
+ Process.pid.to_s
171
+ )
172
+
173
+ loop do
174
+ client = server.accept
175
+
176
+ begin
177
+ request = client.gets&.chomp
178
+
179
+ response =
180
+ case request
181
+ when "PING", "STATUS"
182
+ {
183
+ status: "running",
184
+ service: "vpn-tool-daemon",
185
+ version: VERSION,
186
+ pid: Process.pid,
187
+ host: DAEMON_HOST,
188
+ port: DAEMON_PORT
189
+ }
190
+ else
191
+ {
192
+ status: "ok",
193
+ service: "vpn-tool-daemon",
194
+ version: VERSION
195
+ }
196
+ end
197
+
198
+ client.puts(
199
+ JSON.generate(response)
200
+ )
201
+ rescue StandardError => e
202
+ client.puts(
203
+ JSON.generate(
204
+ status: "error",
205
+ message: e.message
206
+ )
207
+ )
208
+ ensure
209
+ client.close
210
+ end
211
+ end
212
+ rescue StandardError => e
213
+ warn "Daemon error: #{e.message}"
214
+ ensure
215
+ remove_file(DAEMON_PID_FILE)
216
+ end
217
+ end
218
+
219
+ Process.detach(pid)
220
+
221
+ sleep 0.2
222
+
223
+ if daemon_running?
224
+ puts "Daemon started."
225
+ puts "TCP: tcp://#{DAEMON_HOST}:#{DAEMON_PORT}"
226
+ puts "PID: #{daemon_pid}"
227
+ else
228
+ puts "Cannot start daemon."
229
+ end
230
+ rescue StandardError => e
231
+ warn "Error starting daemon: #{e.message}"
232
+ end
233
+
234
+ def stop_daemon
235
+ pid = daemon_pid
236
+
237
+ unless pid && daemon_running?
238
+ remove_file(DAEMON_PID_FILE)
239
+
240
+ puts "Daemon not running."
241
+ puts "Cannot connect to daemon server."
242
+ puts "Run: vpn-tool start --daemon"
243
+
244
+ return
245
+ end
246
+
247
+ begin
248
+ Process.kill("TERM", pid)
249
+
250
+ puts "Daemon stopped (PID #{pid})."
251
+ rescue Errno::ESRCH
252
+ puts "Daemon not running."
253
+ rescue Errno::EPERM
254
+ abort "Error: permission denied for daemon PID #{pid}."
255
+ rescue StandardError => e
256
+ abort "Error stopping daemon: #{e.message}"
257
+ ensure
258
+ remove_file(DAEMON_PID_FILE) unless daemon_running?
259
+ end
260
+ end
261
+
262
+ def daemon_status
263
+ if daemon_running?
264
+ socket = daemon_connection
265
+
266
+ if socket
267
+ socket.puts("STATUS")
268
+
269
+ response = socket.gets
270
+
271
+ socket.close
272
+
273
+ puts(
274
+ response || "Daemon running."
275
+ )
276
+ else
277
+ puts(
278
+ "Daemon PID #{daemon_pid} is running, " \
279
+ "but cannot connect to daemon server."
280
+ )
281
+ end
282
+ else
283
+ puts "Daemon not running."
284
+ puts "Cannot connect to daemon server."
285
+ puts "Run: vpn-tool start --daemon"
286
+ end
287
+ end
288
+
289
+ # ----------------------------------------------------------
290
+ # API
291
+ # ----------------------------------------------------------
292
+
293
+ def api_request(method, endpoint, data = nil)
294
+ uri = URI.join(BASE_URL, endpoint)
295
+
296
+ request =
297
+ case method.to_s.upcase
298
+ when "GET"
299
+ Net::HTTP::Get.new(uri)
300
+ when "POST"
301
+ Net::HTTP::Post.new(uri)
302
+ else
303
+ raise "Unsupported HTTP method: #{method}"
304
+ end
305
+
306
+ if data
307
+ request["Content-Type"] = "application/json"
308
+ request.body = JSON.generate(data)
309
+ end
310
+
311
+ http = Net::HTTP.new(
312
+ uri.host,
313
+ uri.port
314
+ )
315
+
316
+ http.use_ssl = uri.scheme == "https"
317
+ http.open_timeout = 20
318
+ http.read_timeout = 20
319
+
320
+ response = http.request(request)
321
+
322
+ puts response.body
323
+
324
+ response
325
+ rescue StandardError => e
326
+ warn "API error: #{e.message}"
327
+ nil
328
+ end
329
+
330
+ def api_get(endpoint)
331
+ api_request(:get, endpoint)
332
+ end
333
+
334
+ def api_post(endpoint, data = {})
335
+ api_request(:post, endpoint, data)
336
+ end
337
+
338
+ # ----------------------------------------------------------
339
+ # CLI
340
+ # ----------------------------------------------------------
341
+
342
+ def build_cli
343
+ cli = Commander.new
344
+
345
+ cli.add_command(
346
+ "version",
347
+ "Show vpn-tool version"
348
+ ) do
349
+ puts "vpn-tool #{VERSION}"
350
+ end
351
+
352
+ # --------------------------------------------------------
353
+ # Daemon commands
354
+ # --------------------------------------------------------
355
+
356
+ start =
357
+ cli.add_command(
358
+ "start",
359
+ "Start the vpn-tool daemon"
360
+ ) do |options, _args|
361
+ unless options[:daemon]
362
+ abort(
363
+ "Error: --daemon is required.\n" \
364
+ "Example: vpn-tool start --daemon"
365
+ )
366
+ end
367
+
368
+ start_daemon
369
+ end
370
+
371
+ start.add_option(
372
+ ["--daemon"],
373
+ "Start daemon"
374
+ )
375
+
376
+ stop =
377
+ cli.add_command(
378
+ "stop",
379
+ "Stop the vpn-tool daemon"
380
+ ) do |options, _args|
381
+ unless options[:daemon]
382
+ abort(
383
+ "Error: --daemon is required.\n" \
384
+ "Example: vpn-tool stop --daemon"
385
+ )
386
+ end
387
+
388
+ stop_daemon
389
+ end
390
+
391
+ stop.add_option(
392
+ ["--daemon"],
393
+ "Stop daemon"
394
+ )
395
+
396
+ status =
397
+ cli.add_command(
398
+ "status",
399
+ "Show VPN status"
400
+ ) do |options, _args|
401
+ if options[:daemon]
402
+ daemon_status
403
+ next
404
+ end
405
+
406
+ exit 1 unless require_daemon
407
+
408
+ api_get("/api/vpn/status")
409
+ end
410
+
411
+ status.add_option(
412
+ ["--daemon"],
413
+ "Show daemon status"
414
+ )
415
+
416
+ cli.add_command(
417
+ "status-daemon",
418
+ "Show daemon status"
419
+ ) do
420
+ daemon_status
421
+ end
422
+
423
+ # --------------------------------------------------------
424
+ # VPN commands
425
+ # --------------------------------------------------------
426
+
427
+ cli.add_command(
428
+ "countries",
429
+ "List VPN countries"
430
+ ) do
431
+ exit 1 unless require_daemon
432
+
433
+ api_get("/api/vpn/countries")
434
+ end
435
+
436
+ cli.add_command(
437
+ "ip",
438
+ "Show current IP"
439
+ ) do
440
+ exit 1 unless require_daemon
441
+
442
+ api_get("/api/vpn/ip")
443
+ end
444
+
445
+ cli.add_command(
446
+ "health",
447
+ "Show API health"
448
+ ) do
449
+ exit 1 unless require_daemon
450
+
451
+ api_get("/api/healthz")
452
+ end
453
+
454
+ connect =
455
+ cli.add_command(
456
+ "connect",
457
+ "Connect to a VPN server"
458
+ ) do |options, _args|
459
+ exit 1 unless require_daemon
460
+
461
+ unless require_login
462
+ exit 1
463
+ end
464
+
465
+ country = options[:country]
466
+
467
+ unless country
468
+ abort(
469
+ "Error: --country is required.\n" \
470
+ "Example: vpn-tool connect --country US"
471
+ )
472
+ end
473
+
474
+ country = country.upcase
475
+
476
+ unless country.match?(/\A[A-Z]{2}\z/)
477
+ abort(
478
+ "Country must be a 2-letter country code."
479
+ )
480
+ end
481
+
482
+ api_post(
483
+ "/api/vpn/connect",
484
+ {
485
+ "countryCode" => country
486
+ }
487
+ )
488
+ end
489
+
490
+ connect.add_option(
491
+ ["-c", "--country"],
492
+ "Country <value>"
493
+ )
494
+
495
+ cli.add_command(
496
+ "disconnect",
497
+ "Disconnect from VPN"
498
+ ) do
499
+ exit 1 unless require_daemon
500
+
501
+ if require_login
502
+ api_post(
503
+ "/api/vpn/disconnect",
504
+ {}
505
+ )
506
+ end
507
+ end
508
+
509
+ # --------------------------------------------------------
510
+ # Process commands
511
+ # --------------------------------------------------------
512
+
513
+ cli.add_command(
514
+ "ps",
515
+ "List running processes"
516
+ ) do
517
+ system("ps")
518
+ end
519
+
520
+ kill =
521
+ cli.add_command(
522
+ "kill",
523
+ "Kill a process by PID"
524
+ ) do |options, _args|
525
+ pid = options[:pid]
526
+
527
+ unless pid
528
+ abort(
529
+ "Error: --pid is required.\n" \
530
+ "Example: vpn-tool kill --pid 1234"
531
+ )
532
+ end
533
+
534
+ unless pid.match?(/\A\d+\z/)
535
+ abort "Error: PID must be a number."
536
+ end
537
+
538
+ pid = pid.to_i
539
+
540
+ if pid <= 0
541
+ abort "Error: PID must be greater than 0."
542
+ end
543
+
544
+ begin
545
+ Process.kill("TERM", pid)
546
+
547
+ puts "Process #{pid} terminated."
548
+ rescue Errno::ESRCH
549
+ abort(
550
+ "Error: process #{pid} was not found."
551
+ )
552
+ rescue Errno::EPERM
553
+ abort(
554
+ "Error: permission denied for process #{pid}."
555
+ )
556
+ rescue Errno::EINVAL
557
+ abort "Error: invalid PID."
558
+ rescue StandardError => e
559
+ abort "Error: #{e.message}"
560
+ end
561
+ end
562
+
563
+ kill.add_option(
564
+ ["--pid"],
565
+ "Process ID <value>"
566
+ )
567
+
568
+ # --------------------------------------------------------
569
+ # Auth
570
+ # --------------------------------------------------------
571
+
572
+ auth =
573
+ cli.add_command(
574
+ "auth",
575
+ "Authentication commands"
576
+ )
577
+
578
+ auth.add_subcommand("help") do
579
+ puts <<~HELP
580
+ Authentication commands:
581
+
582
+ vpn-tool auth help
583
+ vpn-tool auth register
584
+ vpn-tool auth login
585
+ vpn-tool auth status
586
+ vpn-tool auth list
587
+ vpn-tool auth reset
588
+ vpn-tool auth logout
589
+ vpn-tool auth remove
590
+ HELP
591
+ end
592
+
593
+ auth.add_subcommand("register") do
594
+ ensure_config
595
+
596
+ if file_exists?(PIN_FILE)
597
+ puts "PIN already registered."
598
+ puts "Run: vpn-tool auth reset"
599
+ next
600
+ end
601
+
602
+ pin = prompt("Enter new PIN: ")
603
+
604
+ unless valid_pin?(pin)
605
+ puts "PIN must be exactly 4 digits."
606
+ next
607
+ end
608
+
609
+ confirm = prompt("Confirm new PIN: ")
610
+
611
+ unless pin == confirm
612
+ puts "PIN confirmation does not match."
613
+ next
614
+ end
615
+
616
+ if write_file(PIN_FILE, pin)
617
+ puts "PIN registered successfully."
618
+ end
619
+ end
620
+
621
+ auth.add_subcommand("login") do
622
+ puts "login works"
623
+
624
+ next unless check_pin(
625
+ "Enter PIN to login: "
626
+ )
627
+
628
+ if write_file(
629
+ SESSION_FILE,
630
+ Time.now.to_i.to_s
631
+ )
632
+ puts "Login successful."
633
+ end
634
+ end
635
+
636
+ auth.add_subcommand("status") do
637
+ puts "status works"
638
+
639
+ next unless check_pin(
640
+ "Enter PIN to status: "
641
+ )
642
+
643
+ puts(
644
+ "PIN registered: #{file_exists?(PIN_FILE)}"
645
+ )
646
+
647
+ puts(
648
+ "Session active: #{file_exists?(SESSION_FILE)}"
649
+ )
650
+ end
651
+
652
+ # --------------------------------------------------------
653
+ # Auth list
654
+ #
655
+ # Metadata only.
656
+ # The PIN itself is never displayed.
657
+ # --------------------------------------------------------
658
+
659
+ auth.add_subcommand("list") do
660
+ ensure_config
661
+
662
+ puts "Authentication metadata:"
663
+ puts
664
+ puts "Config directory: #{CONFIG_DIR}"
665
+ puts "PIN registered: #{file_exists?(PIN_FILE)}"
666
+ puts "PIN file: #{PIN_FILE}"
667
+ puts "Session active: #{file_exists?(SESSION_FILE)}"
668
+ puts "Session file: #{SESSION_FILE}"
669
+
670
+ if file_exists?(SESSION_FILE)
671
+ session = read_file(SESSION_FILE)
672
+
673
+ if session&.match?(/\A\d+\z/)
674
+ puts(
675
+ "Session created: #{Time.at(session.to_i)}"
676
+ )
677
+ else
678
+ puts "Session created: unknown"
679
+ end
680
+ else
681
+ puts "Session created: none"
682
+ end
683
+
684
+ puts "PIN value: hidden"
685
+ end
686
+
687
+ auth.add_subcommand("reset") do
688
+ next unless check_pin(
689
+ "Enter PIN to reset: "
690
+ )
691
+
692
+ pin = prompt("Enter new PIN: ")
693
+
694
+ unless valid_pin?(pin)
695
+ puts "PIN must be exactly 4 digits."
696
+ next
697
+ end
698
+
699
+ confirm = prompt("Confirm new PIN: ")
700
+
701
+ unless pin == confirm
702
+ puts "PIN confirmation does not match."
703
+ next
704
+ end
705
+
706
+ if write_file(PIN_FILE, pin)
707
+ puts "PIN reset successfully."
708
+ end
709
+ end
710
+
711
+ auth.add_subcommand("logout") do
712
+ next unless check_pin(
713
+ "Enter PIN to logout: "
714
+ )
715
+
716
+ remove_file(SESSION_FILE)
717
+
718
+ puts "Logged out successfully."
719
+ end
720
+
721
+ auth.add_subcommand("remove") do
722
+ next unless check_pin(
723
+ "Enter PIN to remove: "
724
+ )
725
+
726
+ confirm = prompt(
727
+ "Type REMOVE to continue: "
728
+ )
729
+
730
+ unless confirm == "REMOVE"
731
+ puts "Remove cancelled."
732
+ next
733
+ end
734
+
735
+ remove_file(PIN_FILE)
736
+ remove_file(SESSION_FILE)
737
+
738
+ puts "PIN and session removed."
739
+ end
740
+
741
+ cli
742
+ end
743
+
744
+ def help_text
745
+ <<~HELP
746
+ vpn-tool #{VERSION}
747
+
748
+ Authentication:
749
+
750
+ vpn-tool auth help
751
+ vpn-tool auth register
752
+ vpn-tool auth login
753
+ vpn-tool auth status
754
+ vpn-tool auth list
755
+ vpn-tool auth reset
756
+ vpn-tool auth logout
757
+ vpn-tool auth remove
758
+
759
+ VPN:
760
+
761
+ vpn-tool countries
762
+ vpn-tool connect --country US
763
+ vpn-tool status
764
+ vpn-tool ip
765
+ vpn-tool disconnect
766
+ vpn-tool health
767
+
768
+ Process:
769
+
770
+ vpn-tool ps
771
+ vpn-tool kill --pid 1234
772
+
773
+ Daemon:
774
+
775
+ vpn-tool start --daemon
776
+ vpn-tool stop --daemon
777
+ vpn-tool status --daemon
778
+ vpn-tool status-daemon
779
+
780
+ Version:
781
+
782
+ vpn-tool --version
783
+ vpn-tool version
784
+ HELP
785
+ end
786
+
787
+ def run(argv = ARGV)
788
+ cli = build_cli
789
+
790
+ if argv.empty?
791
+ puts help_text
792
+ elsif argv.first == "--version" ||
793
+ argv.first == "-v"
794
+ puts "vpn-tool #{VERSION}"
795
+ elsif argv.first == "--help" ||
796
+ argv.first == "-h"
797
+ puts help_text
798
+ else
799
+ cli.parse(argv)
800
+ end
801
+ end
802
+ end
803
+
804
+ if __FILE__ == $PROGRAM_NAME
805
+ VpnTool.run
806
+ end
data/vpn-tool.gemspec ADDED
@@ -0,0 +1,25 @@
1
+ Gem::Specification.new do |spec|
2
+ spec.name = "vpn-tool-cli"
3
+ spec.version = "1.0.1"
4
+ spec.authors = ["Jjjm"]
5
+ spec.email = ["dodi66412@gmail.com"]
6
+ spec.summary = "VPN gateway management CLI"
7
+ spec.description = "Ruby command-line client for VPN gateway management with local authentication and daemon support."
8
+ spec.homepage = "https://github.com/jjjm03299-wq/vpn_tool"
9
+ spec.license = "MIT"
10
+ spec.required_ruby_version = ">= 3.0"
11
+
12
+ spec.files = Dir[
13
+ "lib/**/*.rb",
14
+ "bin/*",
15
+ "README.md",
16
+ "LICENSE",
17
+ "*.gemspec"
18
+ ]
19
+
20
+ spec.bindir = "bin"
21
+ spec.executables = ["vpn-tool"]
22
+ spec.require_paths = ["lib"]
23
+
24
+ spec.add_runtime_dependency "commander-tool", "~> 1.0", ">= 1.0.2"
25
+ end
metadata ADDED
@@ -0,0 +1,66 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: vpn-tool-cli
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Jjjm
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 2026-09-18 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: commander-tool
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '1.0'
19
+ - - ">="
20
+ - !ruby/object:Gem::Version
21
+ version: 1.0.2
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - "~>"
27
+ - !ruby/object:Gem::Version
28
+ version: '1.0'
29
+ - - ">="
30
+ - !ruby/object:Gem::Version
31
+ version: 1.0.2
32
+ description: Ruby command-line client for VPN gateway management with local authentication
33
+ and daemon support.
34
+ email:
35
+ - dodi66412@gmail.com
36
+ executables:
37
+ - vpn-tool
38
+ extensions: []
39
+ extra_rdoc_files: []
40
+ files:
41
+ - README.md
42
+ - bin/vpn-tool
43
+ - lib/vpn-tool.rb
44
+ - vpn-tool.gemspec
45
+ homepage: https://github.com/jjjm03299-wq/vpn_tool
46
+ licenses:
47
+ - MIT
48
+ metadata: {}
49
+ rdoc_options: []
50
+ require_paths:
51
+ - lib
52
+ required_ruby_version: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: '3.0'
57
+ required_rubygems_version: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ requirements: []
63
+ rubygems_version: 3.6.2
64
+ specification_version: 4
65
+ summary: VPN gateway management CLI
66
+ test_files: []