fun_ci 1.0.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: bacc0733c3648d5b5422304a7c319be52f1531e98f1c802e4a3d2a5280928ba9
4
+ data.tar.gz: '097d7eaea4e663259c0b745724e6a4d281af1aa60b0ebb3bd78a14c8add5bb57'
5
+ SHA512:
6
+ metadata.gz: feb14f7ff5ba1b67c66b3aa9cddeb9a160320aab302abf9aa60f3cb58b88395e0493abd50dd44db2ddd4030680d0a3c77e99f50cd7408309fe7e3cec9f692163
7
+ data.tar.gz: 8c3cf91d027932fbb998bb3f341cfced2752de43d926a10310f1900ce672de041f75b830379c665b3286daf93bb0c0b0364af47ba1dd116af8c1cc393481ea85
data/CHANGELOG.md ADDED
@@ -0,0 +1,19 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [1.0.0] - 2026-02-21
9
+
10
+ ### Added
11
+ - Four-stage local CI pipeline (lint, build, fast, slow) with strict time budgets (30s/30s/10s/5min)
12
+ - Unified CLI (`fun-ci`) with subcommands: trigger, console, init, install-hooks, check
13
+ - Pre-commit hook (background fork, non-blocking) and pre-push hook (synchronous, blocking)
14
+ - TUI dashboard with vim-style navigation (j/k/c/q), pagination, and project path display
15
+ - Project scaffolding with built-in templates for Ruby (Bundler), JVM (Gradle Kotlin/Groovy, Maven)
16
+ - SQLite persistence with WAL mode for pipeline results
17
+ - Parallel lint + build stages with background slow suite
18
+ - Smart Maven linter detection for template generation
19
+ - Stale pipeline auto-cancellation
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Erik T. Madsen
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,107 @@
1
+ # Fun-CI
2
+
3
+ Opinionated local CI that checks your code before it leaves your machine. Runs a four-stage pipeline on every commit with strict time budgets, so your feedback loop stays fast.
4
+
5
+ ## How It Works
6
+
7
+ Fun-CI hooks into git and runs your pipeline locally:
8
+
9
+ 1. **Lint + Build** (parallel, 30s budget each) -- static analysis and compilation
10
+ 2. **Slow suite** (spawned in background, 5min budget) -- integration/end-to-end tests
11
+ 3. **Fast suite** (synchronous, 10s budget) -- unit tests
12
+
13
+ On **pre-commit**, the entire pipeline forks to the background so your commit is not blocked. On **pre-push**, lint, build, and fast suite must pass before the push proceeds. The slow suite always runs in the background.
14
+
15
+ Results are stored in a local SQLite database. A terminal dashboard lets you monitor pipeline status across branches.
16
+
17
+ ## Getting Started
18
+
19
+ ```bash
20
+ gem install fun_ci
21
+ cd your-project
22
+ fun-ci init --everything
23
+ ```
24
+
25
+ This does three things:
26
+ 1. Detects your project type and creates `.fun-ci/` with template scripts
27
+ 2. Installs pre-commit and pre-push git hooks
28
+ 3. Verifies the setup is valid
29
+
30
+ `fun-ci init` has built-in templates for Ruby (Bundler), JVM (Gradle Kotlin, Gradle Groovy, Maven), but Fun-CI works with any project -- just write your own shell scripts.
31
+
32
+ ### Manual Setup
33
+
34
+ If you prefer to set things up step by step:
35
+
36
+ ```bash
37
+ fun-ci init # Create .fun-ci/ with template scripts
38
+ fun-ci install-hooks # Install git hooks
39
+ fun-ci check # Verify everything is configured
40
+ ```
41
+
42
+ Then edit the generated scripts in `.fun-ci/` to match your project:
43
+ - `lint.sh` -- linter/static analysis
44
+ - `build.sh` -- build/compile step
45
+ - `fast.sh` -- fast test suite (unit tests)
46
+ - `slow.sh` -- slow test suite (integration tests)
47
+
48
+ Each script receives the commit hash as its first argument.
49
+
50
+ ## Commands
51
+
52
+ ```
53
+ fun-ci trigger <commit> <branch> Run the full pipeline
54
+ fun-ci trigger --no-validate <commit> <branch> Fork pipeline to background (used by pre-commit)
55
+ fun-ci console Launch the TUI dashboard
56
+ fun-ci init Scaffold .fun-ci/ for detected project type
57
+ fun-ci init --everything init + install-hooks + check in one step
58
+ fun-ci install-hooks Install pre-commit and pre-push hooks
59
+ fun-ci install-hooks pre-commit Install a single hook type
60
+ fun-ci check Verify .fun-ci/ setup
61
+ ```
62
+
63
+ ## Git Hooks
64
+
65
+ After `fun-ci install-hooks`, two hooks are active:
66
+
67
+ **pre-commit** -- Runs `fun-ci trigger --no-validate <commit> <branch>`. This forks the pipeline into a background process and returns immediately, so commits are never blocked.
68
+
69
+ **pre-push** -- Runs `fun-ci trigger <commit> <branch>`. This validates the project config, runs lint + build + fast suite synchronously, and blocks the push if any stage fails. The slow suite runs in the background.
70
+
71
+ ## Admin Dashboard
72
+
73
+ ```bash
74
+ fun-ci console
75
+ ```
76
+
77
+ Opens a terminal UI showing pipeline status across all branches. Navigation:
78
+
79
+ - `j` / `k` -- scroll up/down
80
+ - `c` -- cancel a running pipeline
81
+ - `q` -- quit
82
+
83
+ ## Time Budgets
84
+
85
+ | Stage | Budget | Blocks |
86
+ |-------|--------|--------|
87
+ | Lint | 30s | push |
88
+ | Build | 30s | push |
89
+ | Fast | 10s | push |
90
+ | Slow | 5min | nothing (background) |
91
+
92
+ If a stage exceeds its budget, it is killed and reported as timed out.
93
+
94
+ ## Development
95
+
96
+ ```bash
97
+ bundle install
98
+ rake test
99
+ ```
100
+
101
+ ## Changelog
102
+
103
+ See [CHANGELOG.md](CHANGELOG.md) for release history.
104
+
105
+ ## License
106
+
107
+ MIT
data/exe/fun-ci ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../lib/fun_ci"
5
+ require_relative "../lib/fun_ci/cli"
6
+
7
+ exit FunCi::Cli.run(ARGV)
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../lib/fun_ci"
5
+ require_relative "../lib/fun_ci/trigger"
6
+ require_relative "../lib/fun_ci/pipeline_recorder"
7
+
8
+ require "tmpdir"
9
+
10
+ db_dir = File.join(Dir.tmpdir, "fun-ci")
11
+ Dir.mkdir(db_dir) unless Dir.exist?(db_dir)
12
+ db_path = File.join(db_dir, "db.sqlite3")
13
+
14
+ db = FunCi::Database.connection(db_path)
15
+ FunCi::Database.migrate!(db)
16
+
17
+ recorder = FunCi::DbRecorder.new(db)
18
+
19
+ exit FunCi::Trigger.run_from_args(ARGV, recorder: recorder)
data/exe/fun-ci-tui ADDED
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../lib/fun_ci"
5
+ require_relative "../lib/fun_ci/admin_tui"
6
+
7
+ require "tmpdir"
8
+ require "io/console"
9
+
10
+ db_dir = File.join(Dir.tmpdir, "fun-ci")
11
+ Dir.mkdir(db_dir) unless Dir.exist?(db_dir)
12
+ db_path = File.join(db_dir, "db.sqlite3")
13
+
14
+ db = FunCi::Database.connection(db_path)
15
+ FunCi::Database.migrate!(db)
16
+
17
+ tui = FunCi::AdminTui.new(db: db, width_provider: -> { IO.console&.winsize&.dig(1) || 80 })
18
+ tui.run
@@ -0,0 +1,226 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "board_data"
4
+ require_relative "row_formatter"
5
+ require_relative "screen"
6
+ require_relative "spinner"
7
+ require_relative "streak_counter"
8
+ require "io/console"
9
+
10
+ module FunCi
11
+ class AdminTui
12
+ FAST_REFRESH = 0.1 # seconds (spinner + timer)
13
+ SLOW_REFRESH = 5.0 # seconds (settled board)
14
+
15
+ def initialize(db:, output: $stdout, input: $stdin, width: 80, width_provider: nil, page_size: nil)
16
+ @board_data = BoardData.new(db, page_size: page_size)
17
+ @screen = Screen.new(output: output, width: width)
18
+ @output = output
19
+ @input = input
20
+ @width_provider = width_provider
21
+ @spinner = Spinner.new
22
+ @cursor_index = nil
23
+ @running = false
24
+ @confirm_cancel = nil
25
+ end
26
+
27
+ def render_once
28
+ update_width_from_provider
29
+ runs = @board_data.runs
30
+ streak = @board_data.streak
31
+ streak_text = StreakCounter.format_text(streak)
32
+
33
+ @screen.render_header(streak_text: streak_text)
34
+ @screen.println
35
+
36
+ if runs.empty?
37
+ @screen.render_empty_state
38
+ @screen.render_footer(empty: true)
39
+ else
40
+ rows = runs.map { |run| format_run(run) }
41
+ @screen.render_board(rows, cursor_index: @cursor_index)
42
+ @screen.println
43
+ @screen.render_footer(empty: false, confirming: confirming?)
44
+ end
45
+
46
+ @screen.clear_below
47
+ end
48
+
49
+ def run
50
+ @running = true
51
+ @screen.clear
52
+
53
+ begin
54
+ setup_raw_mode
55
+ setup_sigwinch_trap
56
+ loop do
57
+ render_frame
58
+ break unless @running
59
+ key = read_key_with_timeout(refresh_interval)
60
+ handle_key(key) if key
61
+ end
62
+ ensure
63
+ restore_terminal
64
+ end
65
+ end
66
+
67
+ def resize(new_width)
68
+ @screen.width = new_width
69
+ end
70
+
71
+ def confirming?
72
+ !@confirm_cancel.nil?
73
+ end
74
+
75
+ def confirmation_run
76
+ @confirm_cancel
77
+ end
78
+
79
+ def handle_key(key)
80
+ if @confirm_cancel
81
+ handle_confirm_key(key)
82
+ return
83
+ end
84
+
85
+ case key
86
+ when "q"
87
+ @running = false
88
+ when "j", :down
89
+ move_cursor_down
90
+ when "k", :up
91
+ move_cursor_up
92
+ when "c"
93
+ initiate_cancel
94
+ end
95
+ end
96
+
97
+ private
98
+
99
+ def format_run(run)
100
+ opts = {}
101
+ if run[:status] == "running"
102
+ active_stage = run[:stages].find { |s| s[:status] == "running" }
103
+ if active_stage && active_stage[:started_at]
104
+ opts[:elapsed_seconds] = Time.now - Time.parse(active_stage[:started_at])
105
+ end
106
+ opts[:spinner_frame] = @spinner.current_frame
107
+ end
108
+ RowFormatter.format(run, **opts)
109
+ end
110
+
111
+ def render_frame
112
+ @screen.move_cursor_home
113
+ @spinner.advance!
114
+ render_once
115
+ end
116
+
117
+ def refresh_interval
118
+ runs = @board_data.runs
119
+ any_running = runs.any? { |r| r[:status] == "running" }
120
+ any_running ? FAST_REFRESH : SLOW_REFRESH
121
+ end
122
+
123
+ def move_cursor_down
124
+ runs = @board_data.runs
125
+ return if runs.empty?
126
+
127
+ if @cursor_index.nil?
128
+ @cursor_index = 0
129
+ elsif @cursor_index < runs.length - 1
130
+ @cursor_index += 1
131
+ @board_data.load_more if @cursor_index == runs.length - 1
132
+ end
133
+ end
134
+
135
+ def move_cursor_up
136
+ return if @cursor_index.nil?
137
+
138
+ if @cursor_index > 0
139
+ @cursor_index -= 1
140
+ end
141
+ end
142
+
143
+ def initiate_cancel
144
+ return if @cursor_index.nil?
145
+
146
+ runs = @board_data.runs
147
+ return if @cursor_index >= runs.length
148
+
149
+ run = runs[@cursor_index]
150
+ case run[:status]
151
+ when "scheduled"
152
+ @board_data.cancel_run(run[:id])
153
+ when "running"
154
+ @confirm_cancel = run
155
+ end
156
+ end
157
+
158
+ def handle_confirm_key(key)
159
+ case key
160
+ when "y"
161
+ @board_data.cancel_run(@confirm_cancel[:id])
162
+ @confirm_cancel = nil
163
+ when "n", :escape
164
+ @confirm_cancel = nil
165
+ end
166
+ end
167
+
168
+ def update_width_from_provider
169
+ return unless @width_provider
170
+
171
+ new_width = @width_provider.call
172
+ resize(new_width) if new_width
173
+ end
174
+
175
+ def setup_sigwinch_trap
176
+ return unless @width_provider
177
+
178
+ @signal_read, @signal_write = IO.pipe
179
+ Signal.trap("WINCH") do
180
+ @signal_write.write_nonblock(".") rescue nil
181
+ end
182
+ end
183
+
184
+ def setup_raw_mode
185
+ @input.raw! if @input.respond_to?(:raw!)
186
+ rescue Errno::ENOTTY
187
+ # Not a terminal (testing)
188
+ end
189
+
190
+ def restore_terminal
191
+ @input.cooked! if @input.respond_to?(:cooked!)
192
+ rescue Errno::ENOTTY
193
+ # Not a terminal
194
+ end
195
+
196
+ def read_key_with_timeout(timeout)
197
+ return nil unless @input.respond_to?(:read_nonblock)
198
+
199
+ watched = [@input]
200
+ watched << @signal_read if @signal_read
201
+ ready = IO.select(watched, nil, nil, timeout)
202
+ return nil unless ready
203
+
204
+ # Drain signal pipe if it woke us
205
+ if @signal_read && ready[0].include?(@signal_read)
206
+ @signal_read.read_nonblock(1) rescue nil
207
+ return nil unless ready[0].include?(@input)
208
+ end
209
+
210
+ byte = @input.read_nonblock(1)
211
+ if byte == "\e"
212
+ # Escape sequence
213
+ seq = @input.read_nonblock(2) rescue ""
214
+ case seq
215
+ when "[A" then :up
216
+ when "[B" then :down
217
+ else :escape
218
+ end
219
+ else
220
+ byte
221
+ end
222
+ rescue IO::WaitReadable, EOFError
223
+ nil
224
+ end
225
+ end
226
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FunCi
4
+ module Ansi
5
+ RESET = "\e[0m"
6
+
7
+ def self.green(text) = "\e[32m#{text}#{RESET}"
8
+ def self.bold_green(text) = "\e[1;32m#{text}#{RESET}"
9
+ def self.bold_red(text) = "\e[1;31m#{text}#{RESET}"
10
+ def self.bold_yellow(text) = "\e[1;33m#{text}#{RESET}"
11
+ def self.cyan(text) = "\e[36m#{text}#{RESET}"
12
+ def self.bold_cyan(text) = "\e[1;36m#{text}#{RESET}"
13
+ def self.dim(text) = "\e[2m#{text}#{RESET}"
14
+ def self.white(text) = "\e[37m#{text}#{RESET}"
15
+ def self.bg_charcoal(text) = "\e[48;5;236m#{text}#{RESET}"
16
+
17
+ def self.strip(text)
18
+ text.gsub(/\e\[[0-9;]*[A-Za-z]/, "")
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "timeout"
4
+
5
+ module FunCi
6
+ class BackgroundWrapper
7
+ def initialize(recorder:, job_id:, executor:)
8
+ @recorder = recorder
9
+ @job_id = job_id
10
+ @executor = executor
11
+ end
12
+
13
+ def run
14
+ _output, status = @executor.call
15
+ if status.success?
16
+ @recorder.end_stage(@job_id, "completed")
17
+ @recorder.complete_run
18
+ else
19
+ @recorder.end_stage(@job_id, "failed")
20
+ @recorder.fail_run
21
+ end
22
+ rescue Timeout::Error
23
+ @recorder.end_stage(@job_id, "timed_out")
24
+ @recorder.fail_run
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+ require_relative "pipeline_run"
5
+ require_relative "stage_job"
6
+ require_relative "streak_counter"
7
+
8
+ module FunCi
9
+ class BoardData
10
+ def initialize(db, limit: 15, page_size: nil)
11
+ @db = db
12
+ @page_size = page_size || limit
13
+ @limit = @page_size
14
+ end
15
+
16
+ def load_more
17
+ @limit += @page_size
18
+ end
19
+
20
+ def runs
21
+ pipeline_runs = PipelineRun.recent(@db, limit: @limit)
22
+ pipeline_runs.map { |run| enrich_with_stages(run) }
23
+ end
24
+
25
+ def streak
26
+ pipeline_runs = PipelineRun.recent(@db, limit: @limit)
27
+ StreakCounter.count(pipeline_runs)
28
+ end
29
+
30
+ def cancel_run(run_id)
31
+ PipelineRun.update_status(@db, run_id, "cancelled")
32
+ end
33
+
34
+ private
35
+
36
+ def enrich_with_stages(run)
37
+ rows = @db.execute(
38
+ "SELECT id, pipeline_run_id, stage, status, started_at, completed_at FROM stage_jobs WHERE pipeline_run_id = ? ORDER BY id",
39
+ [run[:id]]
40
+ )
41
+
42
+ stages = rows.map do |row|
43
+ job = { id: row[0], pipeline_run_id: row[1], stage: row[2], status: row[3], started_at: row[4], completed_at: row[5] }
44
+ duration = StageJob.elapsed_duration(job)
45
+ { stage: job[:stage], status: job[:status], duration: duration, started_at: job[:started_at] }
46
+ end
47
+
48
+ run.merge(stages: stages)
49
+ end
50
+ end
51
+ end
data/lib/fun_ci/cli.rb ADDED
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tmpdir"
4
+ require_relative "../fun_ci"
5
+
6
+ module FunCi
7
+ class Cli
8
+ ROUTES = {
9
+ "trigger" => :run_trigger,
10
+ "console" => :run_console,
11
+ "init" => :run_init,
12
+ "install-hooks" => :run_install_hooks,
13
+ "check" => :run_check
14
+ }.freeze
15
+
16
+ def self.run(args, stdout: $stdout, stderr: $stderr, handlers: {})
17
+ new(stdout: stdout, stderr: stderr, handlers: handlers).run(args)
18
+ end
19
+
20
+ def initialize(stdout:, stderr:, handlers:)
21
+ @stdout = stdout
22
+ @stderr = stderr
23
+ @handlers = handlers
24
+ end
25
+
26
+ def run(args)
27
+ subcommand = args.first
28
+ return help(0) if %w[-h --help].include?(subcommand)
29
+ return version if subcommand == "--version"
30
+ unless ROUTES.key?(subcommand)
31
+ print_error(subcommand)
32
+ return 1
33
+ end
34
+ dispatch(subcommand, args.drop(1))
35
+ end
36
+
37
+ private
38
+
39
+ def dispatch(subcommand, args)
40
+ return @handlers[subcommand].call(args) if @handlers.key?(subcommand)
41
+ send(ROUTES[subcommand], args)
42
+ end
43
+
44
+ def run_trigger(args)
45
+ require_relative "trigger"
46
+ require_relative "pipeline_recorder"
47
+ db = setup_db
48
+ recorder = DbRecorder.new(db)
49
+ Trigger.run_from_args(args, stdout: @stdout, stderr: @stderr, recorder: recorder)
50
+ end
51
+
52
+ def run_console(_args)
53
+ require_relative "admin_tui"
54
+ require "io/console"
55
+ db = setup_db
56
+ tui = AdminTui.new(db: db, width_provider: -> { IO.console&.winsize&.dig(1) || 80 })
57
+ tui.run
58
+ 0
59
+ end
60
+
61
+ def run_init(args)
62
+ require_relative "installer"
63
+ code = Installer.run(project_root: Dir.pwd, stdout: @stdout)
64
+ return code if code != 0 || !args.include?("--everything")
65
+ code = run_install_hooks([])
66
+ return code unless code == 0
67
+ run_check([])
68
+ end
69
+
70
+ def run_install_hooks(args)
71
+ require_relative "hook_writer"
72
+ types = args.any? ? [args.first] : %w[pre-commit pre-push]
73
+ types.each do |type|
74
+ code = HookWriter.run(project_root: Dir.pwd, hook_type: type, stdout: @stdout)
75
+ return code unless code == 0
76
+ end
77
+ 0
78
+ end
79
+
80
+ def run_check(_args)
81
+ require_relative "setup_checker"
82
+ SetupChecker.run(project_root: Dir.pwd, stdout: @stdout, stderr: @stderr)
83
+ end
84
+
85
+ def setup_db
86
+ db_dir = File.join(Dir.tmpdir, "fun-ci")
87
+ Dir.mkdir(db_dir) unless Dir.exist?(db_dir)
88
+ db_path = File.join(db_dir, "db.sqlite3")
89
+ db = Database.connection(db_path)
90
+ Database.migrate!(db)
91
+ db
92
+ end
93
+
94
+ HELP_TEXT = <<~HELP
95
+ fun-ci - Opinionated, local-first CI for your projects
96
+
97
+ Usage:
98
+ fun-ci <command> [options]
99
+
100
+ Commands:
101
+ trigger Run CI pipeline for a commit
102
+ console Launch the admin TUI dashboard
103
+ init Initialize .fun-ci/ with template scripts
104
+ install-hooks Install pre-commit and pre-push git hooks
105
+ check Verify project setup
106
+
107
+ Options:
108
+ -h, --help Show this help message
109
+ --version Show version
110
+
111
+ Trigger options:
112
+ --no-validate Fork pipeline to background and return immediately
113
+
114
+ Init options:
115
+ --everything Run init + install-hooks + check in one step
116
+
117
+ Examples:
118
+ fun-ci init --everything
119
+ fun-ci trigger abc1234 main
120
+ fun-ci trigger --no-validate abc1234 main
121
+ fun-ci console
122
+ HELP
123
+
124
+ def help(exit_code)
125
+ @stdout.puts HELP_TEXT
126
+ exit_code
127
+ end
128
+
129
+ def version
130
+ @stdout.puts "fun-ci #{FunCi::VERSION}"
131
+ 0
132
+ end
133
+
134
+ def print_error(subcommand)
135
+ if subcommand
136
+ @stderr.puts "fun-ci: unknown command '#{subcommand}'"
137
+ @stderr.puts ""
138
+ end
139
+ @stderr.puts "Usage: fun-ci <command> [options]"
140
+ @stderr.puts ""
141
+ @stderr.puts "Run 'fun-ci --help' for available commands."
142
+ end
143
+ end
144
+ end