soba-cli 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/.claude/commands/osoba/add-backlog.md +173 -0
- data/.claude/commands/osoba/implement.md +151 -0
- data/.claude/commands/osoba/plan.md +217 -0
- data/.claude/commands/osoba/review.md +133 -0
- data/.claude/commands/osoba/revise.md +176 -0
- data/.claude/commands/soba/implement.md +88 -0
- data/.claude/commands/soba/plan.md +93 -0
- data/.claude/commands/soba/review.md +91 -0
- data/.claude/commands/soba/revise.md +76 -0
- data/.devcontainer/.env +2 -0
- data/.devcontainer/Dockerfile +3 -0
- data/.devcontainer/LICENSE +21 -0
- data/.devcontainer/README.md +85 -0
- data/.devcontainer/bin/devcontainer-common.sh +50 -0
- data/.devcontainer/bin/down +35 -0
- data/.devcontainer/bin/rebuild +10 -0
- data/.devcontainer/bin/up +11 -0
- data/.devcontainer/compose.yaml +28 -0
- data/.devcontainer/devcontainer.json +53 -0
- data/.devcontainer/post-attach.sh +29 -0
- data/.devcontainer/post-create.sh +62 -0
- data/.devcontainer/setup/01-os-package.sh +19 -0
- data/.devcontainer/setup/02-npm-package.sh +22 -0
- data/.devcontainer/setup/03-mcp-server.sh +33 -0
- data/.devcontainer/setup/04-tool.sh +17 -0
- data/.devcontainer/setup/05-soba-setup.sh +66 -0
- data/.devcontainer/setup/scripts/functions/install_apt.sh +77 -0
- data/.devcontainer/setup/scripts/functions/install_npm.sh +71 -0
- data/.devcontainer/setup/scripts/functions/mcp_config.sh +14 -0
- data/.devcontainer/setup/scripts/functions/print_message.sh +59 -0
- data/.devcontainer/setup/scripts/setup/mcp-markdownify.sh +39 -0
- data/.devcontainer/sync-envs.sh +58 -0
- data/.envrc.sample +7 -0
- data/.rspec +4 -0
- data/.rubocop.yml +70 -0
- data/.rubocop_airbnb.yml +2 -0
- data/.rubocop_todo.yml +74 -0
- data/.tool-versions +1 -0
- data/CLAUDE.md +20 -0
- data/LICENSE +21 -0
- data/README.md +384 -0
- data/README_ja.md +384 -0
- data/Rakefile +18 -0
- data/bin/soba +120 -0
- data/config/config.yml.example +36 -0
- data/docs/business/INDEX.md +6 -0
- data/docs/business/overview.md +42 -0
- data/docs/business/workflow.md +143 -0
- data/docs/development/INDEX.md +10 -0
- data/docs/development/architecture.md +69 -0
- data/docs/development/coding-standards.md +152 -0
- data/docs/development/distribution.md +26 -0
- data/docs/development/implementation-guide.md +103 -0
- data/docs/development/testing-strategy.md +128 -0
- data/docs/development/tmux-management.md +253 -0
- data/docs/document_system.md +58 -0
- data/lib/soba/commands/config/show.rb +63 -0
- data/lib/soba/commands/init.rb +778 -0
- data/lib/soba/commands/open.rb +144 -0
- data/lib/soba/commands/start.rb +442 -0
- data/lib/soba/commands/status.rb +175 -0
- data/lib/soba/commands/stop.rb +147 -0
- data/lib/soba/config_loader.rb +32 -0
- data/lib/soba/configuration.rb +268 -0
- data/lib/soba/container.rb +48 -0
- data/lib/soba/domain/issue.rb +38 -0
- data/lib/soba/domain/phase_strategy.rb +74 -0
- data/lib/soba/infrastructure/errors.rb +23 -0
- data/lib/soba/infrastructure/github_client.rb +399 -0
- data/lib/soba/infrastructure/lock_manager.rb +129 -0
- data/lib/soba/infrastructure/tmux_client.rb +331 -0
- data/lib/soba/services/ansi_processor.rb +92 -0
- data/lib/soba/services/auto_merge_service.rb +133 -0
- data/lib/soba/services/closed_issue_window_cleaner.rb +96 -0
- data/lib/soba/services/daemon_service.rb +83 -0
- data/lib/soba/services/git_workspace_manager.rb +102 -0
- data/lib/soba/services/issue_monitor.rb +29 -0
- data/lib/soba/services/issue_processor.rb +215 -0
- data/lib/soba/services/issue_watcher.rb +193 -0
- data/lib/soba/services/pid_manager.rb +87 -0
- data/lib/soba/services/process_info.rb +58 -0
- data/lib/soba/services/queueing_service.rb +98 -0
- data/lib/soba/services/session_logger.rb +111 -0
- data/lib/soba/services/session_resolver.rb +72 -0
- data/lib/soba/services/slack_notifier.rb +121 -0
- data/lib/soba/services/status_manager.rb +74 -0
- data/lib/soba/services/test_process_manager.rb +84 -0
- data/lib/soba/services/tmux_session_manager.rb +251 -0
- data/lib/soba/services/workflow_blocking_checker.rb +73 -0
- data/lib/soba/services/workflow_executor.rb +256 -0
- data/lib/soba/services/workflow_integrity_checker.rb +151 -0
- data/lib/soba/templates/claude_commands/implement.md +88 -0
- data/lib/soba/templates/claude_commands/plan.md +93 -0
- data/lib/soba/templates/claude_commands/review.md +91 -0
- data/lib/soba/templates/claude_commands/revise.md +76 -0
- data/lib/soba/version.rb +5 -0
- data/lib/soba.rb +44 -0
- data/lib/tasks/gem.rake +75 -0
- data/soba-cli.gemspec +59 -0
- metadata +430 -0
@@ -0,0 +1,778 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/core_ext/object/blank"
|
4
|
+
require "active_support/core_ext/string/exclude"
|
5
|
+
require "active_support/core_ext/object/deep_dup"
|
6
|
+
require "pathname"
|
7
|
+
require "yaml"
|
8
|
+
require "io/console"
|
9
|
+
require_relative "../infrastructure/github_client"
|
10
|
+
|
11
|
+
module Soba
|
12
|
+
module Commands
|
13
|
+
class Init
|
14
|
+
DEFAULT_CONFIG = {
|
15
|
+
'github' => {
|
16
|
+
'token' => '${GITHUB_TOKEN}',
|
17
|
+
},
|
18
|
+
'workflow' => {
|
19
|
+
'interval' => 20,
|
20
|
+
'auto_merge_enabled' => true,
|
21
|
+
'closed_issue_cleanup_enabled' => true,
|
22
|
+
'closed_issue_cleanup_interval' => 300,
|
23
|
+
'tmux_command_delay' => 3,
|
24
|
+
'phase_labels' => {
|
25
|
+
'todo' => 'soba:todo',
|
26
|
+
'queued' => 'soba:queued',
|
27
|
+
'planning' => 'soba:planning',
|
28
|
+
'ready' => 'soba:ready',
|
29
|
+
'doing' => 'soba:doing',
|
30
|
+
'review_requested' => 'soba:review-requested',
|
31
|
+
'reviewing' => 'soba:reviewing',
|
32
|
+
'done' => 'soba:done',
|
33
|
+
'requires_changes' => 'soba:requires-changes',
|
34
|
+
'revising' => 'soba:revising',
|
35
|
+
'merged' => 'soba:merged',
|
36
|
+
},
|
37
|
+
},
|
38
|
+
'slack' => {
|
39
|
+
'webhook_url' => '${SLACK_WEBHOOK_URL}',
|
40
|
+
'notifications_enabled' => false,
|
41
|
+
},
|
42
|
+
}.freeze
|
43
|
+
|
44
|
+
LABEL_COLORS = {
|
45
|
+
'todo' => '808080', # Gray
|
46
|
+
'queued' => '9370db', # Medium Purple
|
47
|
+
'planning' => '1e90ff', # Blue
|
48
|
+
'ready' => '228b22', # Green
|
49
|
+
'doing' => 'ffd700', # Yellow
|
50
|
+
'review_requested' => 'ff8c00', # Orange
|
51
|
+
'reviewing' => 'ff6347', # Tomato
|
52
|
+
'done' => '32cd32', # Lime Green
|
53
|
+
'requires_changes' => 'dc143c', # Crimson
|
54
|
+
'revising' => 'ff1493', # Deep Pink
|
55
|
+
'merged' => '6b8e23', # Olive Drab
|
56
|
+
'lgtm' => '00ff00', # Pure Green
|
57
|
+
}.freeze
|
58
|
+
|
59
|
+
LABEL_DESCRIPTIONS = {
|
60
|
+
'todo' => 'To-do task waiting to be queued',
|
61
|
+
'queued' => 'Queued for processing',
|
62
|
+
'planning' => 'Planning phase',
|
63
|
+
'ready' => 'Ready for implementation',
|
64
|
+
'doing' => 'In progress',
|
65
|
+
'review_requested' => 'Review requested',
|
66
|
+
'reviewing' => 'Under review',
|
67
|
+
'done' => 'Review completed',
|
68
|
+
'requires_changes' => 'Changes requested',
|
69
|
+
'revising' => 'Revising based on review feedback',
|
70
|
+
'merged' => 'PR merged and issue closed',
|
71
|
+
'lgtm' => 'PR approved for auto-merge',
|
72
|
+
}.freeze
|
73
|
+
|
74
|
+
DEFAULT_PHASE_CONFIG = {
|
75
|
+
'plan' => {
|
76
|
+
'command' => 'claude',
|
77
|
+
'options' => ['--dangerously-skip-permissions'],
|
78
|
+
'parameter' => '/soba:plan {{issue-number}}',
|
79
|
+
},
|
80
|
+
'implement' => {
|
81
|
+
'command' => 'claude',
|
82
|
+
'options' => ['--dangerously-skip-permissions'],
|
83
|
+
'parameter' => '/soba:implement {{issue-number}}',
|
84
|
+
},
|
85
|
+
'review' => {
|
86
|
+
'command' => 'claude',
|
87
|
+
'options' => ['--dangerously-skip-permissions'],
|
88
|
+
'parameter' => '/soba:review {{issue-number}}',
|
89
|
+
},
|
90
|
+
'revise' => {
|
91
|
+
'command' => 'claude',
|
92
|
+
'options' => ['--dangerously-skip-permissions'],
|
93
|
+
'parameter' => '/soba:revise {{issue-number}}',
|
94
|
+
},
|
95
|
+
}.freeze
|
96
|
+
|
97
|
+
def initialize(interactive: false)
|
98
|
+
@interactive = interactive
|
99
|
+
end
|
100
|
+
|
101
|
+
def execute
|
102
|
+
puts "🚀 Initializing soba configuration..."
|
103
|
+
puts ""
|
104
|
+
|
105
|
+
config_path = Pathname.pwd.join('.soba', 'config.yml')
|
106
|
+
|
107
|
+
if config_path.exist?
|
108
|
+
puts "⚠️ Configuration file already exists at: #{config_path}"
|
109
|
+
print "Do you want to overwrite it? (y/N): "
|
110
|
+
response = $stdin.gets.chomp.downcase
|
111
|
+
if response != 'y' && response != 'yes'
|
112
|
+
puts "✅ Configuration unchanged."
|
113
|
+
return
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
if @interactive
|
118
|
+
execute_interactive(config_path)
|
119
|
+
else
|
120
|
+
execute_non_interactive(config_path)
|
121
|
+
end
|
122
|
+
rescue Interrupt
|
123
|
+
puts "\n\n❌ Setup cancelled."
|
124
|
+
raise Soba::CommandError, "Setup cancelled"
|
125
|
+
rescue StandardError => e
|
126
|
+
puts "\n❌ Error: #{e.message}"
|
127
|
+
raise
|
128
|
+
end
|
129
|
+
|
130
|
+
private
|
131
|
+
|
132
|
+
def execute_non_interactive(config_path)
|
133
|
+
# GitHub repository - auto-detect from git remote
|
134
|
+
repository = detect_github_repository
|
135
|
+
|
136
|
+
unless repository
|
137
|
+
puts "❌ Error: Cannot detect GitHub repository from git remote."
|
138
|
+
puts " Please run 'soba init --interactive' for manual setup."
|
139
|
+
raise Soba::CommandError, "Cannot detect GitHub repository"
|
140
|
+
end
|
141
|
+
|
142
|
+
# Create configuration with default values
|
143
|
+
config = DEFAULT_CONFIG.deep_dup
|
144
|
+
config['github']['repository'] = repository
|
145
|
+
|
146
|
+
# Add default phase configuration
|
147
|
+
config['phase'] = DEFAULT_PHASE_CONFIG.deep_dup
|
148
|
+
|
149
|
+
# Slack configuration is already included in DEFAULT_CONFIG
|
150
|
+
|
151
|
+
# Write configuration file
|
152
|
+
write_config_file(config_path, config)
|
153
|
+
|
154
|
+
puts ""
|
155
|
+
puts "✅ Configuration created successfully!"
|
156
|
+
puts "📁 Location: #{config_path}"
|
157
|
+
puts "📦 Repository: #{repository}"
|
158
|
+
|
159
|
+
check_github_token(token: '${GITHUB_TOKEN}')
|
160
|
+
check_slack_webhook_url(config: config)
|
161
|
+
deploy_claude_templates
|
162
|
+
handle_gitignore
|
163
|
+
create_github_labels(config)
|
164
|
+
|
165
|
+
puts ""
|
166
|
+
puts "🎉 Setup complete! You can now use:"
|
167
|
+
puts " soba config - View current configuration"
|
168
|
+
puts " soba issue list #{config['github']['repository']} - List repository issues"
|
169
|
+
end
|
170
|
+
|
171
|
+
def execute_interactive(config_path)
|
172
|
+
# Collect configuration values
|
173
|
+
puts "Let's set up your GitHub configuration:"
|
174
|
+
puts ""
|
175
|
+
|
176
|
+
# GitHub repository - auto-detect from git remote
|
177
|
+
default_repo = detect_github_repository
|
178
|
+
|
179
|
+
if default_repo
|
180
|
+
print "Enter GitHub repository (format: owner/repo) [#{default_repo}]: "
|
181
|
+
else
|
182
|
+
print "Enter GitHub repository (format: owner/repo): "
|
183
|
+
end
|
184
|
+
|
185
|
+
repository = $stdin.gets.chomp
|
186
|
+
if repository.empty? && default_repo
|
187
|
+
repository = default_repo
|
188
|
+
end
|
189
|
+
|
190
|
+
while repository.blank? || repository.exclude?('/')
|
191
|
+
puts "❌ Invalid format. Please use: owner/repo"
|
192
|
+
print "Enter GitHub repository: "
|
193
|
+
repository = $stdin.gets.chomp
|
194
|
+
end
|
195
|
+
|
196
|
+
# GitHub token
|
197
|
+
puts ""
|
198
|
+
puts "GitHub Personal Access Token (PAT) setup:"
|
199
|
+
puts " 1. Use environment variable ${GITHUB_TOKEN} (recommended)"
|
200
|
+
puts " 2. Enter token directly (will be visible in config file)"
|
201
|
+
print "Choose option (1-2) [1]: "
|
202
|
+
token_option = $stdin.gets.chomp
|
203
|
+
token_option = '1' if token_option.empty?
|
204
|
+
|
205
|
+
token = if token_option == '2'
|
206
|
+
print "Enter GitHub token: "
|
207
|
+
# Hide input for security
|
208
|
+
$stdin.noecho(&:gets).chomp.tap { puts }
|
209
|
+
else
|
210
|
+
'${GITHUB_TOKEN}'
|
211
|
+
end
|
212
|
+
|
213
|
+
# Polling interval
|
214
|
+
puts ""
|
215
|
+
print "Enter polling interval in seconds [20]: "
|
216
|
+
interval = $stdin.gets.chomp
|
217
|
+
interval = '20' if interval.empty?
|
218
|
+
interval = interval.to_i
|
219
|
+
interval = 20 if interval <= 0
|
220
|
+
|
221
|
+
# Phase labels configuration
|
222
|
+
puts ""
|
223
|
+
puts "Phase labels configuration:"
|
224
|
+
puts "These labels are used to track issue progress through the workflow"
|
225
|
+
puts ""
|
226
|
+
|
227
|
+
# Planning phase label
|
228
|
+
print "Enter planning phase label [soba:planning]: "
|
229
|
+
planning_label = $stdin.gets.chomp
|
230
|
+
planning_label = 'soba:planning' if planning_label.empty?
|
231
|
+
|
232
|
+
# Ready phase label
|
233
|
+
print "Enter ready phase label [soba:ready]: "
|
234
|
+
ready_label = $stdin.gets.chomp
|
235
|
+
ready_label = 'soba:ready' if ready_label.empty?
|
236
|
+
|
237
|
+
# Doing phase label
|
238
|
+
print "Enter doing phase label [soba:doing]: "
|
239
|
+
doing_label = $stdin.gets.chomp
|
240
|
+
doing_label = 'soba:doing' if doing_label.empty?
|
241
|
+
|
242
|
+
# Review requested phase label
|
243
|
+
print "Enter review requested phase label [soba:review-requested]: "
|
244
|
+
review_label = $stdin.gets.chomp
|
245
|
+
review_label = 'soba:review-requested' if review_label.empty?
|
246
|
+
|
247
|
+
# Auto-merge configuration
|
248
|
+
puts ""
|
249
|
+
print "Enable auto-merge for PRs with soba:lgtm label? (y/n) [y]: "
|
250
|
+
auto_merge = $stdin.gets.chomp.downcase
|
251
|
+
auto_merge = 'y' if auto_merge.empty?
|
252
|
+
auto_merge_enabled = auto_merge != 'n'
|
253
|
+
|
254
|
+
# Slack notification configuration
|
255
|
+
puts ""
|
256
|
+
print "Enable Slack notifications for phase starts? (y/N): "
|
257
|
+
slack_enabled = $stdin.gets.chomp.downcase
|
258
|
+
slack_enabled = 'n' if slack_enabled.empty?
|
259
|
+
slack_notifications_enabled = slack_enabled == 'y'
|
260
|
+
|
261
|
+
slack_webhook_url = '${SLACK_WEBHOOK_URL}'
|
262
|
+
if slack_notifications_enabled
|
263
|
+
puts "Slack Webhook URL setup:"
|
264
|
+
puts " 1. Use environment variable ${SLACK_WEBHOOK_URL} (recommended)"
|
265
|
+
puts " 2. Enter webhook URL directly (will be visible in config file)"
|
266
|
+
print "Choose option (1-2) [1]: "
|
267
|
+
slack_option = $stdin.gets.chomp
|
268
|
+
slack_option = '1' if slack_option.empty?
|
269
|
+
|
270
|
+
if slack_option == '2'
|
271
|
+
print "Enter Slack webhook URL: "
|
272
|
+
# Hide input for security
|
273
|
+
slack_webhook_url = $stdin.noecho(&:gets).chomp.tap { puts }
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
# Workflow commands configuration
|
278
|
+
puts ""
|
279
|
+
puts "Workflow commands configuration (optional):"
|
280
|
+
puts "These commands will be executed during each phase"
|
281
|
+
puts ""
|
282
|
+
|
283
|
+
# Plan phase command
|
284
|
+
puts "Planning phase command:"
|
285
|
+
print "Enter command (e.g., claude) [#{DEFAULT_PHASE_CONFIG['plan']['command']}]: "
|
286
|
+
plan_command = $stdin.gets.chomp
|
287
|
+
plan_command = DEFAULT_PHASE_CONFIG['plan']['command'] if plan_command.empty?
|
288
|
+
if plan_command.downcase == 'skip'
|
289
|
+
plan_command = nil
|
290
|
+
end
|
291
|
+
|
292
|
+
plan_options = []
|
293
|
+
plan_parameter = nil
|
294
|
+
if plan_command
|
295
|
+
default_options = DEFAULT_PHASE_CONFIG['plan']['options'].join(',')
|
296
|
+
print "Enter options (comma-separated, e.g., --dangerously-skip-permissions) [#{default_options}]: "
|
297
|
+
options_input = $stdin.gets.chomp
|
298
|
+
if options_input.empty?
|
299
|
+
plan_options = DEFAULT_PHASE_CONFIG['plan']['options']
|
300
|
+
else
|
301
|
+
plan_options = options_input.split(',').map(&:strip).reject(&:empty?)
|
302
|
+
end
|
303
|
+
|
304
|
+
default_param = DEFAULT_PHASE_CONFIG['plan']['parameter']
|
305
|
+
print "Enter parameter (use {{issue-number}} for issue number) [#{default_param}]: "
|
306
|
+
plan_parameter = $stdin.gets.chomp
|
307
|
+
plan_parameter = DEFAULT_PHASE_CONFIG['plan']['parameter'] if plan_parameter.empty?
|
308
|
+
end
|
309
|
+
|
310
|
+
# Implement phase command
|
311
|
+
puts ""
|
312
|
+
puts "Implementation phase command:"
|
313
|
+
print "Enter command (e.g., claude) [#{DEFAULT_PHASE_CONFIG['implement']['command']}]: "
|
314
|
+
implement_command = $stdin.gets.chomp
|
315
|
+
implement_command = DEFAULT_PHASE_CONFIG['implement']['command'] if implement_command.empty?
|
316
|
+
if implement_command.downcase == 'skip'
|
317
|
+
implement_command = nil
|
318
|
+
end
|
319
|
+
|
320
|
+
implement_options = []
|
321
|
+
implement_parameter = nil
|
322
|
+
if implement_command
|
323
|
+
default_options = DEFAULT_PHASE_CONFIG['implement']['options'].join(',')
|
324
|
+
print "Enter options (comma-separated, e.g., --dangerously-skip-permissions) [#{default_options}]: "
|
325
|
+
options_input = $stdin.gets.chomp
|
326
|
+
if options_input.empty?
|
327
|
+
implement_options = DEFAULT_PHASE_CONFIG['implement']['options']
|
328
|
+
else
|
329
|
+
implement_options = options_input.split(',').map(&:strip).reject(&:empty?)
|
330
|
+
end
|
331
|
+
|
332
|
+
default_param = DEFAULT_PHASE_CONFIG['implement']['parameter']
|
333
|
+
print "Enter parameter (use {{issue-number}} for issue number) [#{default_param}]: "
|
334
|
+
implement_parameter = $stdin.gets.chomp
|
335
|
+
implement_parameter = DEFAULT_PHASE_CONFIG['implement']['parameter'] if implement_parameter.empty?
|
336
|
+
end
|
337
|
+
|
338
|
+
# Review phase command
|
339
|
+
puts ""
|
340
|
+
puts "Review phase command:"
|
341
|
+
print "Enter command (e.g., claude) [#{DEFAULT_PHASE_CONFIG['review']['command']}]: "
|
342
|
+
review_command = $stdin.gets.chomp
|
343
|
+
review_command = DEFAULT_PHASE_CONFIG['review']['command'] if review_command.empty?
|
344
|
+
if review_command.downcase == 'skip'
|
345
|
+
review_command = nil
|
346
|
+
end
|
347
|
+
|
348
|
+
review_options = []
|
349
|
+
review_parameter = nil
|
350
|
+
if review_command
|
351
|
+
default_options = DEFAULT_PHASE_CONFIG['review']['options'].join(',')
|
352
|
+
print "Enter options (comma-separated, e.g., --dangerously-skip-permissions) [#{default_options}]: "
|
353
|
+
options_input = $stdin.gets.chomp
|
354
|
+
if options_input.empty?
|
355
|
+
review_options = DEFAULT_PHASE_CONFIG['review']['options']
|
356
|
+
else
|
357
|
+
review_options = options_input.split(',').map(&:strip).reject(&:empty?)
|
358
|
+
end
|
359
|
+
|
360
|
+
default_param = DEFAULT_PHASE_CONFIG['review']['parameter']
|
361
|
+
print "Enter parameter (use {{issue-number}} for issue number) [#{default_param}]: "
|
362
|
+
review_parameter = $stdin.gets.chomp
|
363
|
+
review_parameter = DEFAULT_PHASE_CONFIG['review']['parameter'] if review_parameter.empty?
|
364
|
+
end
|
365
|
+
|
366
|
+
# Create configuration
|
367
|
+
config = {
|
368
|
+
'github' => {
|
369
|
+
'token' => token,
|
370
|
+
'repository' => repository,
|
371
|
+
},
|
372
|
+
'workflow' => {
|
373
|
+
'interval' => interval,
|
374
|
+
'auto_merge_enabled' => auto_merge_enabled,
|
375
|
+
'closed_issue_cleanup_enabled' => true,
|
376
|
+
'closed_issue_cleanup_interval' => 300,
|
377
|
+
'tmux_command_delay' => 3,
|
378
|
+
'phase_labels' => {
|
379
|
+
'todo' => 'soba:todo',
|
380
|
+
'queued' => 'soba:queued',
|
381
|
+
'planning' => planning_label,
|
382
|
+
'ready' => ready_label,
|
383
|
+
'doing' => doing_label,
|
384
|
+
'review_requested' => review_label,
|
385
|
+
'reviewing' => 'soba:reviewing',
|
386
|
+
'done' => 'soba:done',
|
387
|
+
'requires_changes' => 'soba:requires-changes',
|
388
|
+
'revising' => 'soba:revising',
|
389
|
+
'merged' => 'soba:merged',
|
390
|
+
},
|
391
|
+
},
|
392
|
+
'slack' => {
|
393
|
+
'webhook_url' => slack_webhook_url,
|
394
|
+
'notifications_enabled' => slack_notifications_enabled,
|
395
|
+
},
|
396
|
+
}
|
397
|
+
|
398
|
+
# Add phase configuration if provided
|
399
|
+
if plan_command || implement_command || review_command
|
400
|
+
config['phase'] = {}
|
401
|
+
|
402
|
+
if plan_command
|
403
|
+
config['phase']['plan'] = {
|
404
|
+
'command' => plan_command,
|
405
|
+
'options' => plan_options,
|
406
|
+
'parameter' => plan_parameter,
|
407
|
+
}
|
408
|
+
end
|
409
|
+
|
410
|
+
if implement_command
|
411
|
+
config['phase']['implement'] = {
|
412
|
+
'command' => implement_command,
|
413
|
+
'options' => implement_options,
|
414
|
+
'parameter' => implement_parameter,
|
415
|
+
}
|
416
|
+
end
|
417
|
+
|
418
|
+
if review_command
|
419
|
+
config['phase']['review'] = {
|
420
|
+
'command' => review_command,
|
421
|
+
'options' => review_options,
|
422
|
+
'parameter' => review_parameter,
|
423
|
+
}
|
424
|
+
end
|
425
|
+
end
|
426
|
+
|
427
|
+
# Write configuration file
|
428
|
+
write_config_file(config_path, config)
|
429
|
+
|
430
|
+
puts ""
|
431
|
+
puts "✅ Configuration created successfully!"
|
432
|
+
puts "📁 Location: #{config_path}"
|
433
|
+
|
434
|
+
check_github_token(token: token)
|
435
|
+
check_slack_webhook_url(config: config)
|
436
|
+
deploy_claude_templates
|
437
|
+
handle_gitignore
|
438
|
+
create_github_labels(config)
|
439
|
+
|
440
|
+
puts ""
|
441
|
+
puts "🎉 Setup complete! You can now use:"
|
442
|
+
puts " soba config - View current configuration"
|
443
|
+
puts " soba issue list #{config['github']['repository']} - List repository issues"
|
444
|
+
end
|
445
|
+
|
446
|
+
def write_config_file(config_path, config)
|
447
|
+
config_path.dirname.mkpath
|
448
|
+
config_content = <<~YAML
|
449
|
+
# soba CLI configuration
|
450
|
+
# Generated by: soba init
|
451
|
+
# Date: #{Time.now}
|
452
|
+
|
453
|
+
github:
|
454
|
+
# GitHub Personal Access Token
|
455
|
+
# Can use environment variable: ${GITHUB_TOKEN}
|
456
|
+
token: #{config['github']['token']}
|
457
|
+
|
458
|
+
# Target repository (format: owner/repo)
|
459
|
+
repository: #{config['github']['repository']}
|
460
|
+
|
461
|
+
workflow:
|
462
|
+
# Issue polling interval in seconds
|
463
|
+
interval: #{config['workflow']['interval']}
|
464
|
+
|
465
|
+
# Enable automatic merging of PRs with soba:lgtm label
|
466
|
+
auto_merge_enabled: #{config['workflow']['auto_merge_enabled']}
|
467
|
+
|
468
|
+
# Enable automatic cleanup of tmux windows for closed issues
|
469
|
+
closed_issue_cleanup_enabled: #{config['workflow']['closed_issue_cleanup_enabled']}
|
470
|
+
|
471
|
+
# Cleanup check interval in seconds
|
472
|
+
closed_issue_cleanup_interval: #{config['workflow']['closed_issue_cleanup_interval']}
|
473
|
+
|
474
|
+
# Delay (in seconds) before sending commands to new tmux panes/windows
|
475
|
+
tmux_command_delay: #{config['workflow']['tmux_command_delay']}
|
476
|
+
|
477
|
+
# Phase labels for tracking issue progress
|
478
|
+
phase_labels:
|
479
|
+
todo: #{config['workflow']['phase_labels']['todo']}
|
480
|
+
queued: #{config['workflow']['phase_labels']['queued']}
|
481
|
+
planning: #{config['workflow']['phase_labels']['planning']}
|
482
|
+
ready: #{config['workflow']['phase_labels']['ready']}
|
483
|
+
doing: #{config['workflow']['phase_labels']['doing']}
|
484
|
+
review_requested: #{config['workflow']['phase_labels']['review_requested']}
|
485
|
+
reviewing: #{config['workflow']['phase_labels']['reviewing']}
|
486
|
+
done: #{config['workflow']['phase_labels']['done']}
|
487
|
+
requires_changes: #{config['workflow']['phase_labels']['requires_changes']}
|
488
|
+
revising: #{config['workflow']['phase_labels']['revising']}
|
489
|
+
merged: #{config['workflow']['phase_labels']['merged']}
|
490
|
+
|
491
|
+
# Slack notification configuration
|
492
|
+
slack:
|
493
|
+
# Slack Webhook URL for notifications
|
494
|
+
# Can use environment variable: ${SLACK_WEBHOOK_URL}
|
495
|
+
webhook_url: #{config['slack']['webhook_url']}
|
496
|
+
|
497
|
+
# Enable Slack notifications for phase starts
|
498
|
+
notifications_enabled: #{config['slack']['notifications_enabled']}
|
499
|
+
YAML
|
500
|
+
|
501
|
+
# Add phase configuration if present
|
502
|
+
if config['phase']
|
503
|
+
phase_content = "\n # Phase command configuration\n phase:\n"
|
504
|
+
|
505
|
+
if config['phase']['plan']
|
506
|
+
phase_content += " plan:\n"
|
507
|
+
phase_content += " command: #{config['phase']['plan']['command']}\n"
|
508
|
+
if config['phase']['plan']['options'].present?
|
509
|
+
phase_content += " options:\n"
|
510
|
+
config['phase']['plan']['options'].each do |opt|
|
511
|
+
phase_content += " - #{opt}\n"
|
512
|
+
end
|
513
|
+
end
|
514
|
+
if config['phase']['plan']['parameter']
|
515
|
+
phase_content += " parameter: '#{config['phase']['plan']['parameter']}'\n"
|
516
|
+
end
|
517
|
+
end
|
518
|
+
|
519
|
+
if config['phase']['implement']
|
520
|
+
phase_content += " implement:\n"
|
521
|
+
phase_content += " command: #{config['phase']['implement']['command']}\n"
|
522
|
+
if config['phase']['implement']['options'].present?
|
523
|
+
phase_content += " options:\n"
|
524
|
+
config['phase']['implement']['options'].each do |opt|
|
525
|
+
phase_content += " - #{opt}\n"
|
526
|
+
end
|
527
|
+
end
|
528
|
+
if config['phase']['implement']['parameter']
|
529
|
+
phase_content += " parameter: '#{config['phase']['implement']['parameter']}'\n"
|
530
|
+
end
|
531
|
+
end
|
532
|
+
|
533
|
+
if config['phase']['review']
|
534
|
+
phase_content += " review:\n"
|
535
|
+
phase_content += " command: #{config['phase']['review']['command']}\n"
|
536
|
+
if config['phase']['review']['options'].present?
|
537
|
+
phase_content += " options:\n"
|
538
|
+
config['phase']['review']['options'].each do |opt|
|
539
|
+
phase_content += " - #{opt}\n"
|
540
|
+
end
|
541
|
+
end
|
542
|
+
if config['phase']['review']['parameter']
|
543
|
+
phase_content += " parameter: '#{config['phase']['review']['parameter']}'\n"
|
544
|
+
end
|
545
|
+
end
|
546
|
+
|
547
|
+
if config['phase']['revise']
|
548
|
+
phase_content += " revise:\n"
|
549
|
+
phase_content += " command: #{config['phase']['revise']['command']}\n"
|
550
|
+
if config['phase']['revise']['options'].present?
|
551
|
+
phase_content += " options:\n"
|
552
|
+
config['phase']['revise']['options'].each do |opt|
|
553
|
+
phase_content += " - #{opt}\n"
|
554
|
+
end
|
555
|
+
end
|
556
|
+
if config['phase']['revise']['parameter']
|
557
|
+
phase_content += " parameter: '#{config['phase']['revise']['parameter']}'\n"
|
558
|
+
end
|
559
|
+
end
|
560
|
+
|
561
|
+
# Remove extra indentation to match YAML structure
|
562
|
+
phase_content = phase_content.gsub(/^ /, '')
|
563
|
+
config_content += phase_content
|
564
|
+
end
|
565
|
+
|
566
|
+
File.write(config_path, config_content)
|
567
|
+
end
|
568
|
+
|
569
|
+
def check_github_token(token: '${GITHUB_TOKEN}')
|
570
|
+
# Verify token if environment variable is used
|
571
|
+
if token == '${GITHUB_TOKEN}'
|
572
|
+
puts ""
|
573
|
+
if ENV['GITHUB_TOKEN']
|
574
|
+
puts "✅ GITHUB_TOKEN environment variable is set"
|
575
|
+
else
|
576
|
+
puts "⚠️ GITHUB_TOKEN environment variable is not set"
|
577
|
+
puts " Please set it before running soba commands:"
|
578
|
+
puts " export GITHUB_TOKEN='your-token-here'"
|
579
|
+
end
|
580
|
+
end
|
581
|
+
end
|
582
|
+
|
583
|
+
def check_slack_webhook_url(config:)
|
584
|
+
# Verify Slack webhook URL if environment variable is used and notifications are enabled
|
585
|
+
if config['slack'] && config['slack']['notifications_enabled']
|
586
|
+
webhook_url = config['slack']['webhook_url']
|
587
|
+
if webhook_url == '${SLACK_WEBHOOK_URL}'
|
588
|
+
puts ""
|
589
|
+
if ENV['SLACK_WEBHOOK_URL']
|
590
|
+
puts "✅ SLACK_WEBHOOK_URL environment variable is set"
|
591
|
+
else
|
592
|
+
puts "⚠️ SLACK_WEBHOOK_URL environment variable is not set"
|
593
|
+
puts " Slack notifications are enabled but webhook URL is missing."
|
594
|
+
puts " Please set it before running soba commands:"
|
595
|
+
puts " export SLACK_WEBHOOK_URL='your-webhook-url-here'"
|
596
|
+
end
|
597
|
+
end
|
598
|
+
end
|
599
|
+
end
|
600
|
+
|
601
|
+
def handle_gitignore
|
602
|
+
# Add .soba to .gitignore if needed
|
603
|
+
gitignore_path = Pathname.pwd.join('.gitignore')
|
604
|
+
if gitignore_path.exist?
|
605
|
+
gitignore_content = File.read(gitignore_path)
|
606
|
+
unless gitignore_content.include?('.soba')
|
607
|
+
puts ""
|
608
|
+
print "Add .soba/ to .gitignore? (Y/n): "
|
609
|
+
response = $stdin.gets.chomp.downcase
|
610
|
+
if response != 'n' && response != 'no'
|
611
|
+
File.open(gitignore_path, 'a') do |f|
|
612
|
+
f.puts "" unless gitignore_content.end_with?("\n")
|
613
|
+
f.puts "# soba configuration directory"
|
614
|
+
f.puts ".soba/"
|
615
|
+
end
|
616
|
+
puts "✅ Added .soba/ to .gitignore"
|
617
|
+
end
|
618
|
+
end
|
619
|
+
end
|
620
|
+
end
|
621
|
+
|
622
|
+
def deploy_claude_templates
|
623
|
+
# Deploy Claude command templates to .claude/commands/soba/
|
624
|
+
claude_dir = Pathname.pwd.join('.claude', 'commands', 'soba')
|
625
|
+
template_dir = Pathname.new(__dir__).join('..', 'templates', 'claude_commands')
|
626
|
+
|
627
|
+
# Create directory if it doesn't exist
|
628
|
+
claude_dir.mkpath
|
629
|
+
|
630
|
+
# List of template files to deploy
|
631
|
+
template_files = ['plan.md', 'implement.md', 'review.md', 'revise.md']
|
632
|
+
|
633
|
+
template_files.each do |filename|
|
634
|
+
source_file = template_dir.join(filename)
|
635
|
+
target_file = claude_dir.join(filename)
|
636
|
+
|
637
|
+
# Check if file already exists
|
638
|
+
if target_file.exist?
|
639
|
+
if @interactive
|
640
|
+
puts ""
|
641
|
+
puts "⚠️ Claude command template already exists: #{target_file.relative_path_from(Pathname.pwd)}"
|
642
|
+
print "Do you want to overwrite it? (y/N): "
|
643
|
+
response = $stdin.gets
|
644
|
+
next unless response # Skip if no response
|
645
|
+
response = response.chomp.downcase
|
646
|
+
if response != 'y' && response != 'yes'
|
647
|
+
puts "✅ Keeping existing template: #{filename}"
|
648
|
+
next
|
649
|
+
else
|
650
|
+
puts "✅ Overwriting template: #{filename}"
|
651
|
+
end
|
652
|
+
end
|
653
|
+
end
|
654
|
+
|
655
|
+
# Copy the template file
|
656
|
+
FileUtils.cp(source_file, target_file)
|
657
|
+
Soba.logger.debug "Deployed Claude template: #{filename}"
|
658
|
+
end
|
659
|
+
|
660
|
+
puts ""
|
661
|
+
puts "✅ Claude command templates deployed to .claude/commands/soba/"
|
662
|
+
end
|
663
|
+
|
664
|
+
def detect_github_repository
|
665
|
+
return nil unless Dir.exist?('.git')
|
666
|
+
|
667
|
+
# Try to get remote origin URL
|
668
|
+
remote_url = `git config --get remote.origin.url 2>/dev/null`.chomp
|
669
|
+
return nil if remote_url.empty?
|
670
|
+
|
671
|
+
# Parse GitHub repository from various URL formats
|
672
|
+
# https://github.com/owner/repo.git
|
673
|
+
# git@github.com:owner/repo.git
|
674
|
+
# ssh://git@github.com/owner/repo.git
|
675
|
+
case remote_url
|
676
|
+
when %r{github\.com[:/]([^/]+)/([^/]+?)(?:\.git)?$}
|
677
|
+
"#{Regexp.last_match(1)}/#{Regexp.last_match(2)}"
|
678
|
+
when %r{^git@github\.com:([^/]+)/([^/]+?)(?:\.git)?$}
|
679
|
+
"#{Regexp.last_match(1)}/#{Regexp.last_match(2)}"
|
680
|
+
else
|
681
|
+
nil
|
682
|
+
end
|
683
|
+
rescue StandardError
|
684
|
+
nil
|
685
|
+
end
|
686
|
+
|
687
|
+
def create_github_labels(config)
|
688
|
+
repository = config['github']['repository']
|
689
|
+
phase_labels = config['workflow']['phase_labels']
|
690
|
+
|
691
|
+
# Only ask for confirmation in interactive mode
|
692
|
+
if @interactive
|
693
|
+
puts ""
|
694
|
+
print "Create GitHub labels for workflow phases? (Y/n): "
|
695
|
+
response = $stdin.gets
|
696
|
+
return unless response
|
697
|
+
response = response.chomp.downcase
|
698
|
+
if response == 'n' || response == 'no'
|
699
|
+
puts "✅ Skipping label creation."
|
700
|
+
return
|
701
|
+
end
|
702
|
+
end
|
703
|
+
|
704
|
+
puts ""
|
705
|
+
puts "🏷️ Creating GitHub labels..."
|
706
|
+
|
707
|
+
begin
|
708
|
+
# Initialize GitHub client
|
709
|
+
github_client = Infrastructure::GitHubClient.new
|
710
|
+
|
711
|
+
# Get existing labels
|
712
|
+
existing_labels = github_client.list_labels(repository)
|
713
|
+
existing_label_names = existing_labels.map { |label| label[:name] }
|
714
|
+
|
715
|
+
# Create labels for each phase
|
716
|
+
created_count = 0
|
717
|
+
skipped_count = 0
|
718
|
+
|
719
|
+
# Create phase labels
|
720
|
+
phase_labels.each do |phase, label_name|
|
721
|
+
if existing_label_names.include?(label_name)
|
722
|
+
Soba.logger.debug "Label '#{label_name}' already exists, skipping"
|
723
|
+
skipped_count += 1
|
724
|
+
else
|
725
|
+
color = LABEL_COLORS[phase]
|
726
|
+
description = LABEL_DESCRIPTIONS[phase]
|
727
|
+
|
728
|
+
result = github_client.create_label(repository, label_name, color, description)
|
729
|
+
if result
|
730
|
+
Soba.logger.info "Label '#{label_name}' created"
|
731
|
+
created_count += 1
|
732
|
+
else
|
733
|
+
Soba.logger.warn "Label '#{label_name}' could not be created (may already exist)"
|
734
|
+
skipped_count += 1
|
735
|
+
end
|
736
|
+
end
|
737
|
+
end
|
738
|
+
|
739
|
+
# Create additional PR label
|
740
|
+
additional_labels = [
|
741
|
+
{ name: 'soba:lgtm', phase: 'lgtm' },
|
742
|
+
]
|
743
|
+
|
744
|
+
additional_labels.each do |label_info|
|
745
|
+
label_name = label_info[:name]
|
746
|
+
if existing_label_names.include?(label_name)
|
747
|
+
Soba.logger.debug "Label '#{label_name}' already exists, skipping"
|
748
|
+
skipped_count += 1
|
749
|
+
else
|
750
|
+
color = LABEL_COLORS[label_info[:phase]]
|
751
|
+
description = LABEL_DESCRIPTIONS[label_info[:phase]]
|
752
|
+
|
753
|
+
result = github_client.create_label(repository, label_name, color, description)
|
754
|
+
if result
|
755
|
+
Soba.logger.info "Label '#{label_name}' created"
|
756
|
+
created_count += 1
|
757
|
+
else
|
758
|
+
Soba.logger.warn "Label '#{label_name}' could not be created (may already exist)"
|
759
|
+
skipped_count += 1
|
760
|
+
end
|
761
|
+
end
|
762
|
+
end
|
763
|
+
|
764
|
+
puts ""
|
765
|
+
puts "✅ Label creation complete: #{created_count} created, #{skipped_count} skipped"
|
766
|
+
rescue Infrastructure::AuthenticationError => e
|
767
|
+
Soba.logger.error "Authentication failed: #{e.message}"
|
768
|
+
Soba.logger.error "Please ensure your GitHub token has 'repo' permission"
|
769
|
+
rescue Infrastructure::GitHubClientError => e
|
770
|
+
Soba.logger.error "Failed to create labels: #{e.message}"
|
771
|
+
Soba.logger.error "Please check your repository permissions"
|
772
|
+
rescue StandardError => e
|
773
|
+
Soba.logger.error "Unexpected error: #{e.message}"
|
774
|
+
end
|
775
|
+
end
|
776
|
+
end
|
777
|
+
end
|
778
|
+
end
|