fantasy-cli 1.2.6

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 (112) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +456 -0
  4. data/bin/gsd +8 -0
  5. data/bin/gsd-core-darwin-amd64 +0 -0
  6. data/bin/gsd-core-darwin-arm64 +0 -0
  7. data/bin/gsd-core-linux-amd64 +0 -0
  8. data/bin/gsd-core-linux-arm64 +0 -0
  9. data/bin/gsd-core-windows-amd64.exe +0 -0
  10. data/bin/gsd-core-windows-arm64.exe +0 -0
  11. data/bin/gsd-core.exe +0 -0
  12. data/lib/gsd/agents/coordinator.rb +195 -0
  13. data/lib/gsd/agents/task_manager.rb +158 -0
  14. data/lib/gsd/agents/worker.rb +162 -0
  15. data/lib/gsd/agents.rb +30 -0
  16. data/lib/gsd/ai/chat.rb +486 -0
  17. data/lib/gsd/ai/cli.rb +248 -0
  18. data/lib/gsd/ai/command_parser.rb +97 -0
  19. data/lib/gsd/ai/commands/base.rb +42 -0
  20. data/lib/gsd/ai/commands/clear.rb +20 -0
  21. data/lib/gsd/ai/commands/context.rb +30 -0
  22. data/lib/gsd/ai/commands/cost.rb +30 -0
  23. data/lib/gsd/ai/commands/export.rb +42 -0
  24. data/lib/gsd/ai/commands/help.rb +61 -0
  25. data/lib/gsd/ai/commands/model.rb +67 -0
  26. data/lib/gsd/ai/commands/reset.rb +22 -0
  27. data/lib/gsd/ai/config.rb +256 -0
  28. data/lib/gsd/ai/context.rb +324 -0
  29. data/lib/gsd/ai/cost_tracker.rb +361 -0
  30. data/lib/gsd/ai/git_context.rb +169 -0
  31. data/lib/gsd/ai/history.rb +384 -0
  32. data/lib/gsd/ai/providers/anthropic.rb +429 -0
  33. data/lib/gsd/ai/providers/base.rb +282 -0
  34. data/lib/gsd/ai/providers/lmstudio.rb +279 -0
  35. data/lib/gsd/ai/providers/ollama.rb +336 -0
  36. data/lib/gsd/ai/providers/openai.rb +396 -0
  37. data/lib/gsd/ai/providers/openrouter.rb +429 -0
  38. data/lib/gsd/ai/reference_resolver.rb +225 -0
  39. data/lib/gsd/ai/repl.rb +349 -0
  40. data/lib/gsd/ai/streaming.rb +438 -0
  41. data/lib/gsd/ai/ui.rb +429 -0
  42. data/lib/gsd/buddy/cli.rb +284 -0
  43. data/lib/gsd/buddy/gacha.rb +148 -0
  44. data/lib/gsd/buddy/renderer.rb +108 -0
  45. data/lib/gsd/buddy/species.rb +190 -0
  46. data/lib/gsd/buddy/stats.rb +156 -0
  47. data/lib/gsd/buddy.rb +28 -0
  48. data/lib/gsd/cli.rb +455 -0
  49. data/lib/gsd/commands.rb +198 -0
  50. data/lib/gsd/config.rb +183 -0
  51. data/lib/gsd/error.rb +188 -0
  52. data/lib/gsd/frontmatter.rb +123 -0
  53. data/lib/gsd/go/bridge.rb +173 -0
  54. data/lib/gsd/history.rb +76 -0
  55. data/lib/gsd/milestone.rb +75 -0
  56. data/lib/gsd/output.rb +184 -0
  57. data/lib/gsd/phase.rb +102 -0
  58. data/lib/gsd/plugins/base.rb +92 -0
  59. data/lib/gsd/plugins/cli.rb +330 -0
  60. data/lib/gsd/plugins/config.rb +164 -0
  61. data/lib/gsd/plugins/hooks.rb +132 -0
  62. data/lib/gsd/plugins/installer.rb +158 -0
  63. data/lib/gsd/plugins/loader.rb +122 -0
  64. data/lib/gsd/plugins/manager.rb +187 -0
  65. data/lib/gsd/plugins/marketplace.rb +142 -0
  66. data/lib/gsd/plugins/sandbox.rb +114 -0
  67. data/lib/gsd/plugins/search.rb +131 -0
  68. data/lib/gsd/plugins/validator.rb +157 -0
  69. data/lib/gsd/plugins.rb +48 -0
  70. data/lib/gsd/profile.rb +127 -0
  71. data/lib/gsd/research.rb +85 -0
  72. data/lib/gsd/roadmap.rb +90 -0
  73. data/lib/gsd/skills/bundled/commit.md +58 -0
  74. data/lib/gsd/skills/bundled/debug.md +28 -0
  75. data/lib/gsd/skills/bundled/explain.md +41 -0
  76. data/lib/gsd/skills/bundled/plan.md +42 -0
  77. data/lib/gsd/skills/bundled/verify.md +26 -0
  78. data/lib/gsd/skills/loader.rb +189 -0
  79. data/lib/gsd/state.rb +102 -0
  80. data/lib/gsd/template.rb +106 -0
  81. data/lib/gsd/tools/ask_user_question.rb +179 -0
  82. data/lib/gsd/tools/base.rb +204 -0
  83. data/lib/gsd/tools/bash.rb +246 -0
  84. data/lib/gsd/tools/file_edit.rb +297 -0
  85. data/lib/gsd/tools/file_read.rb +199 -0
  86. data/lib/gsd/tools/file_write.rb +153 -0
  87. data/lib/gsd/tools/glob.rb +202 -0
  88. data/lib/gsd/tools/grep.rb +227 -0
  89. data/lib/gsd/tools/gsd_frontmatter.rb +165 -0
  90. data/lib/gsd/tools/gsd_phase.rb +140 -0
  91. data/lib/gsd/tools/gsd_roadmap.rb +108 -0
  92. data/lib/gsd/tools/gsd_state.rb +143 -0
  93. data/lib/gsd/tools/gsd_template.rb +157 -0
  94. data/lib/gsd/tools/gsd_verify.rb +159 -0
  95. data/lib/gsd/tools/registry.rb +103 -0
  96. data/lib/gsd/tools/task.rb +235 -0
  97. data/lib/gsd/tools/todo_write.rb +290 -0
  98. data/lib/gsd/tools/web.rb +260 -0
  99. data/lib/gsd/tui/app.rb +366 -0
  100. data/lib/gsd/tui/auto_complete.rb +79 -0
  101. data/lib/gsd/tui/colors.rb +111 -0
  102. data/lib/gsd/tui/command_palette.rb +126 -0
  103. data/lib/gsd/tui/header.rb +38 -0
  104. data/lib/gsd/tui/input_box.rb +199 -0
  105. data/lib/gsd/tui/spinner.rb +40 -0
  106. data/lib/gsd/tui/status_bar.rb +51 -0
  107. data/lib/gsd/tui.rb +17 -0
  108. data/lib/gsd/validator.rb +216 -0
  109. data/lib/gsd/verify.rb +175 -0
  110. data/lib/gsd/version.rb +5 -0
  111. data/lib/gsd/workstream.rb +91 -0
  112. metadata +231 -0
data/lib/gsd/config.rb ADDED
@@ -0,0 +1,183 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'fileutils'
5
+
6
+ module Gsd
7
+ # Config operations for .planning/config.json
8
+ class Config
9
+ class << self
10
+ # Load config.json
11
+ #
12
+ # @param cwd [String] Working directory
13
+ # @return [Hash] Config hash
14
+ def load(cwd: nil)
15
+ cwd ||= Dir.pwd
16
+ config_path = File.join(cwd, '.planning', 'config.json')
17
+
18
+ return default_config unless File.exist?(config_path)
19
+
20
+ JSON.parse(File.read(config_path))
21
+ end
22
+
23
+ # Get specific key
24
+ #
25
+ # @param key [String] Config key
26
+ # @param cwd [String] Working directory
27
+ # @return [Object] Config value or nil
28
+ def get(key, cwd: nil)
29
+ load(cwd: cwd)[key]
30
+ end
31
+
32
+ # Set specific key
33
+ #
34
+ # @param key [String] Config key
35
+ # @param value [Object] Config value
36
+ # @param cwd [String] Working directory
37
+ def set(key, value, cwd: nil)
38
+ config = load(cwd: cwd)
39
+ config[key] = value
40
+ write(config, cwd: cwd)
41
+ end
42
+
43
+ # Ensure section exists
44
+ #
45
+ # @param section [String] Section name
46
+ # @param cwd [String] Working directory
47
+ # @return [Hash] Section hash
48
+ def ensure_section(section, cwd: nil)
49
+ config = load(cwd: cwd)
50
+ config[section] ||= {}
51
+ write(config, cwd: cwd)
52
+ config[section]
53
+ end
54
+
55
+ # Get model profile for agent type
56
+ #
57
+ # @param agent_type [String] Agent type (optional)
58
+ # @param cwd [String] Working directory
59
+ # @return [String] Model profile name
60
+ def model_profile(agent_type = nil, cwd: nil)
61
+ profiles = load(cwd: cwd)['model_profiles'] || {}
62
+ return profiles['default'] unless agent_type
63
+
64
+ profiles[agent_type] || profiles['default']
65
+ end
66
+
67
+ # Set model profile for agent type
68
+ #
69
+ # @param agent_type [String] Agent type
70
+ # @param profile [String] Model profile name
71
+ # @param cwd [String] Working directory
72
+ def set_model_profile(agent_type, profile, cwd: nil)
73
+ config = load(cwd: cwd)
74
+ config['model_profiles'] ||= {}
75
+ config['model_profiles'][agent_type] = profile
76
+ write(config, cwd: cwd)
77
+ end
78
+
79
+ # Get sub-repos list
80
+ #
81
+ # @param cwd [String] Working directory
82
+ # @return [Array<String>] Sub-repo names
83
+ def sub_repos(cwd: nil)
84
+ load(cwd: cwd)['sub_repos'] || []
85
+ end
86
+
87
+ # Add sub-repo to config
88
+ #
89
+ # @param repo [String] Sub-repo name
90
+ # @param cwd [String] Working directory
91
+ def add_sub_repo(repo, cwd: nil)
92
+ config = load(cwd: cwd)
93
+ config['sub_repos'] ||= []
94
+ config['sub_repos'] << repo unless config['sub_repos'].include?(repo)
95
+ write(config, cwd: cwd)
96
+ end
97
+
98
+ # Get commit docs setting
99
+ #
100
+ # @param cwd [String] Working directory
101
+ # @return [Boolean] Commit docs enabled
102
+ def commit_docs(cwd: nil)
103
+ load(cwd: cwd)['commit_docs'] != false
104
+ end
105
+
106
+ # Get branching strategy
107
+ #
108
+ # @param cwd [String] Working directory
109
+ # @return [String] Branching strategy
110
+ def branching_strategy(cwd: nil)
111
+ load(cwd: cwd)['branching_strategy'] || 'feature'
112
+ end
113
+
114
+ # Get parallelization setting
115
+ #
116
+ # @param cwd [String] Working directory
117
+ # @return [Boolean] Parallelization enabled
118
+ def parallelization(cwd: nil)
119
+ load(cwd: cwd)['parallelization'] != false
120
+ end
121
+
122
+ # Get research setting
123
+ #
124
+ # @param cwd [String] Working directory
125
+ # @return [Boolean] Research enabled
126
+ def research(cwd: nil)
127
+ load(cwd: cwd)['research'] != false
128
+ end
129
+
130
+ # Get plan checker setting
131
+ #
132
+ # @param cwd [String] Working directory
133
+ # @return [Boolean] Plan checker enabled
134
+ def plan_checker(cwd: nil)
135
+ load(cwd: cwd)['plan_checker'] != false
136
+ end
137
+
138
+ # Get verifier setting
139
+ #
140
+ # @param cwd [String] Working directory
141
+ # @return [Boolean] Verifier enabled
142
+ def verifier(cwd: nil)
143
+ load(cwd: cwd)['verifier'] != false
144
+ end
145
+
146
+ # Initialize config with defaults
147
+ #
148
+ # @param cwd [String] Working directory
149
+ # @return [Hash] Initial config
150
+ def init(cwd: nil)
151
+ config = default_config
152
+ write(config, cwd: cwd)
153
+ config
154
+ end
155
+
156
+ private
157
+
158
+ def default_config
159
+ {
160
+ 'model_profile' => 'default',
161
+ 'model_profiles' => {
162
+ 'default' => 'claude-sonnet-4-5-20250929'
163
+ },
164
+ 'commit_docs' => true,
165
+ 'branching_strategy' => 'feature',
166
+ 'phase_branch_template' => 'phase-{phase}',
167
+ 'milestone_branch_template' => 'milestone-{version}',
168
+ 'parallelization' => true,
169
+ 'research' => true,
170
+ 'plan_checker' => true,
171
+ 'verifier' => true,
172
+ 'sub_repos' => []
173
+ }
174
+ end
175
+
176
+ def write(config, cwd: nil)
177
+ config_path = File.join(cwd, '.planning', 'config.json')
178
+ FileUtils.mkdir_p(File.dirname(config_path))
179
+ File.write(config_path, JSON.pretty_generate(config))
180
+ end
181
+ end
182
+ end
183
+ end
data/lib/gsd/error.rb ADDED
@@ -0,0 +1,188 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gsd
4
+ # Base error class para todos os erros do GSD
5
+ class Error < StandardError; end
6
+
7
+ # ============================================================================
8
+ # State Errors
9
+ # ============================================================================
10
+
11
+ class StateError < Error; end
12
+ class StateNotFoundError < StateError; end
13
+ class StateInvalidError < StateError; end
14
+ class StateFieldError < StateError; end
15
+
16
+ # ============================================================================
17
+ # Phase Errors
18
+ # ============================================================================
19
+
20
+ class PhaseError < Error; end
21
+ class PhaseNotFoundError < PhaseError; end
22
+ class PhaseExistsError < PhaseError; end
23
+ class PhaseInvalidError < PhaseError; end
24
+
25
+ # ============================================================================
26
+ # Roadmap Errors
27
+ # ============================================================================
28
+
29
+ class RoadmapError < Error; end
30
+ class RoadmapNotFoundError < RoadmapError; end
31
+ class RoadmapInvalidError < RoadmapError; end
32
+
33
+ # ============================================================================
34
+ # Bridge Errors (Go communication)
35
+ # ============================================================================
36
+
37
+ class BridgeError < Error; end
38
+ class BridgeNotAvailableError < BridgeError; end
39
+ class BridgeTimeoutError < BridgeError; end
40
+ class BridgeExecutionError < BridgeError; end
41
+
42
+ # ============================================================================
43
+ # Validation Errors
44
+ # ============================================================================
45
+
46
+ class ValidationError < Error
47
+ attr_reader :field, :value
48
+
49
+ def initialize(field, value, message)
50
+ @field = field
51
+ @value = value
52
+ super(message)
53
+ end
54
+ end
55
+
56
+ # ============================================================================
57
+ # Not Found Errors
58
+ # ============================================================================
59
+
60
+ class NotFoundError < Error
61
+ attr_reader :resource, :id
62
+
63
+ def initialize(resource, id)
64
+ @resource = resource
65
+ @id = id
66
+ super("#{resource} not found: #{id}")
67
+ end
68
+ end
69
+
70
+ # ============================================================================
71
+ # File/Path Errors
72
+ # ============================================================================
73
+
74
+ class FileError < Error; end
75
+ class FileNotFoundError < FileError; end
76
+ class FileNotReadableError < FileError; end
77
+ class FileNotWritableError < FileError; end
78
+
79
+ class DirectoryError < Error; end
80
+ class DirectoryNotFoundError < DirectoryError; end
81
+
82
+ # ============================================================================
83
+ # Config Errors
84
+ # ============================================================================
85
+
86
+ class ConfigError < Error; end
87
+ class ConfigNotFoundError < ConfigError; end
88
+ class ConfigInvalidError < ConfigError; end
89
+
90
+ # ============================================================================
91
+ # Global Error Handler
92
+ # ============================================================================
93
+
94
+ class ErrorHandler
95
+ class << self
96
+ # Handle global de erros com mensagens amigáveis
97
+ #
98
+ # @param error [Exception] Error to handle
99
+ # @param verbose [Boolean] Show full backtrace
100
+ # @return [Integer] Exit code
101
+ def handle(error, verbose: false)
102
+ case error
103
+ when ValidationError
104
+ handle_validation_error(error)
105
+ when NotFoundError
106
+ handle_not_found_error(error)
107
+ when BridgeError
108
+ handle_bridge_error(error)
109
+ when StateError, PhaseError, RoadmapError
110
+ handle_domain_error(error)
111
+ when FileError, DirectoryError
112
+ handle_file_error(error)
113
+ when ConfigError
114
+ handle_config_error(error)
115
+ else
116
+ handle_unknown_error(error, verbose: verbose)
117
+ end
118
+
119
+ 1
120
+ end
121
+
122
+ private
123
+
124
+ def handle_validation_error(error)
125
+ warn "⚠️ Validation Error"
126
+ warn " Field: #{error.field}"
127
+ warn " Value: #{error.value.inspect}"
128
+ warn " Message: #{error.message}"
129
+ end
130
+
131
+ def handle_not_found_error(error)
132
+ warn "❌ Not Found"
133
+ warn " Resource: #{error.resource}"
134
+ warn " ID: #{error.id}"
135
+ end
136
+
137
+ def handle_bridge_error(error)
138
+ warn "🔌 Go Bridge Error"
139
+ warn " #{error.class}: #{error.message}"
140
+ warn "\n Tip: Make sure gsd-core binary is built and in PATH"
141
+ warn " Run: make build"
142
+ end
143
+
144
+ def handle_domain_error(error)
145
+ warn "📄 #{error.class}"
146
+ warn " #{error.message}"
147
+ end
148
+
149
+ def handle_file_error(error)
150
+ warn "📁 #{error.class}"
151
+ warn " #{error.message}"
152
+ end
153
+
154
+ def handle_config_error(error)
155
+ warn "⚙️ Config Error"
156
+ warn " #{error.message}"
157
+ warn "\n Tip: Check .planning/config.json"
158
+ end
159
+
160
+ def handle_unknown_error(error, verbose: false)
161
+ warn "💥 Unexpected Error"
162
+ warn " #{error.class}: #{error.message}"
163
+
164
+ if verbose
165
+ warn "\nBacktrace:"
166
+ error.backtrace.first(10).each { |line| warn " #{line}" }
167
+ end
168
+ end
169
+ end
170
+ end
171
+
172
+ # ============================================================================
173
+ # Error Messages (centralized)
174
+ # ============================================================================
175
+
176
+ module ErrorMessages
177
+ STATE_NOT_FOUND = "STATE.md not found in .planning directory"
178
+ STATE_INVALID = "Invalid STATE.md format"
179
+ PHASE_NOT_FOUND = "Phase directory not found"
180
+ PHASE_EXISTS = "Phase already exists"
181
+ ROADMAP_NOT_FOUND = "ROADMAP.md not found in .planning directory"
182
+ CONFIG_NOT_FOUND = "config.json not found in .planning directory"
183
+ BINARY_NOT_FOUND = "gsd-core binary not found. Run `make build`"
184
+ PATH_NOT_FOUND = "Path does not exist"
185
+ FIELD_INVALID = "Invalid field name"
186
+ VALUE_INVALID = "Invalid value"
187
+ end
188
+ end
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'go/bridge'
4
+ require 'json'
5
+
6
+ module Gsd
7
+ # Frontmatter - Wrapper Ruby para operações CRUD de frontmatter via Go
8
+ module Frontmatter
9
+ class << self
10
+ # Extrai frontmatter de um arquivo
11
+ #
12
+ # @param file [String] Caminho para o arquivo
13
+ # @param field [String] Campo específico para extrair (opcional)
14
+ # @param cwd [String] Diretório de trabalho
15
+ # @return [Hash] Frontmatter data
16
+ def get(file:, field: nil, cwd: nil)
17
+ cwd ||= Dir.pwd
18
+ args = { 'get' => file }
19
+ args['field'] = field if field
20
+ result = Go::Bridge.call('frontmatter', args, cwd: cwd)
21
+
22
+ if result['success']
23
+ result['data']
24
+ else
25
+ raise FrontmatterError, result['error']
26
+ end
27
+ end
28
+
29
+ # Atualiza um campo no frontmatter
30
+ #
31
+ # @param file [String] Caminho para o arquivo
32
+ # @param field [String] Campo para atualizar
33
+ # @param value [String] Valor para definir
34
+ # @param cwd [String] Diretório de trabalho
35
+ # @return [Hash] Resultado da operação
36
+ def set(file:, field:, value:, cwd: nil)
37
+ cwd ||= Dir.pwd
38
+ args = { 'set' => file, 'field' => field, 'value' => value }
39
+ result = Go::Bridge.call('frontmatter', args, cwd: cwd)
40
+
41
+ if result['success']
42
+ result['data']
43
+ else
44
+ raise FrontmatterError, result['error']
45
+ end
46
+ end
47
+
48
+ # Mescla dados JSON no frontmatter
49
+ #
50
+ # @param file [String] Caminho para o arquivo
51
+ # @param data [Hash] Dados para mesclar
52
+ # @param cwd [String] Diretório de trabalho
53
+ # @return [Hash] Resultado da operação
54
+ def merge(file:, data:, cwd: nil)
55
+ cwd ||= Dir.pwd
56
+ args = { 'merge' => file, 'data' => data.to_json }
57
+ result = Go::Bridge.call('frontmatter', args, cwd: cwd)
58
+
59
+ if result['success']
60
+ result['data']
61
+ else
62
+ raise FrontmatterError, result['error']
63
+ end
64
+ end
65
+
66
+ # Valida frontmatter contra um schema
67
+ #
68
+ # @param file [String] Caminho para o arquivo
69
+ # @param schema [String] Schema para validar (plan, summary, verification)
70
+ # @param cwd [String] Diretório de trabalho
71
+ # @return [Hash] Resultado da validação
72
+ def validate(file:, schema:, cwd: nil)
73
+ cwd ||= Dir.pwd
74
+ args = { 'validate' => file, 'schema' => schema }
75
+ result = Go::Bridge.call('frontmatter', args, cwd: cwd)
76
+
77
+ if result['success']
78
+ result['data']
79
+ else
80
+ raise FrontmatterError, result['error']
81
+ end
82
+ end
83
+
84
+ # Extrai frontmatter e campos específicos
85
+ #
86
+ # @param file [String] Caminho para o arquivo
87
+ # @param fields [Array<String>] Campos para extrair
88
+ # @param cwd [String] Diretório de trabalho
89
+ # @return [Hash] Frontmatter data
90
+ def extract(file:, fields: [], cwd: nil)
91
+ cwd ||= Dir.pwd
92
+ args = { 'extract' => file }
93
+ args['fields'] = fields.join(',') unless fields.empty?
94
+ result = Go::Bridge.call('frontmatter', args, cwd: cwd)
95
+
96
+ if result['success']
97
+ result['data']
98
+ else
99
+ raise FrontmatterError, result['error']
100
+ end
101
+ end
102
+
103
+ # Helper: Verifica se frontmatter é válido
104
+ #
105
+ # @param result [Hash] Resultado da validação
106
+ # @return [Boolean] true se válido
107
+ def valid?(result)
108
+ result.is_a?(Hash) && result['valid'] == true
109
+ end
110
+
111
+ # Helper: Obtém erros da validação
112
+ #
113
+ # @param result [Hash] Resultado da validação
114
+ # @return [Array<String>] Lista de erros
115
+ def errors(result)
116
+ return [] unless result.is_a?(Hash)
117
+ result['errors'] || result['data']&.dig('errors') || []
118
+ end
119
+ end
120
+
121
+ class FrontmatterError < StandardError; end
122
+ end
123
+ end
@@ -0,0 +1,173 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'open3'
5
+
6
+ module Gsd
7
+ module Go
8
+ # Bridge para comunicação com binário Go via subprocess
9
+ class Bridge
10
+ class << self
11
+ attr_accessor :binary_path
12
+
13
+ # Caminho para o binário gsd-core
14
+ def binary_path
15
+ @binary_path ||= find_binary
16
+ end
17
+
18
+ # Chama um comando no binário Go
19
+ #
20
+ # @param command [String] comando a executar
21
+ # @param args [Hash] argumentos nomeados
22
+ # @param cwd [String, nil] diretório de trabalho
23
+ # @return [Hash] resultado parseado como JSON
24
+ def call(command, args = {}, cwd: nil)
25
+ cmd = build_command(command, args)
26
+
27
+ log_debug("Executing: #{cmd.join(' ')}")
28
+
29
+ opts = cwd ? { chdir: cwd } : {}
30
+ stdout, stderr, status = Open3.capture3(*cmd, opts)
31
+
32
+ if status && !status.success?
33
+ raise BridgeError, "Command failed: #{stderr}"
34
+ end
35
+
36
+ begin
37
+ JSON.parse(stdout)
38
+ rescue JSON::ParserError => e
39
+ raise BridgeError, "Failed to parse JSON response: #{e.message}"
40
+ end
41
+ rescue Errno::ENOENT => e
42
+ raise BridgeError, "gsd-core binary not found: #{e.message}"
43
+ end
44
+
45
+ # Retorna a versão do gsd-core
46
+ def core_version
47
+ result = call('version')
48
+ result.dig('data', 'version') || 'unknown'
49
+ rescue BridgeError
50
+ 'unknown'
51
+ end
52
+
53
+ # State commands
54
+ def state_load(cwd:)
55
+ call('state', { 'load' => true }, cwd: cwd)
56
+ end
57
+
58
+ def state_json(cwd:)
59
+ call('state', { 'json' => true }, cwd: cwd)
60
+ end
61
+
62
+ def state_update(field:, value:, cwd:)
63
+ call('state', { 'update' => true, 'field' => field, 'value' => value }, cwd: cwd)
64
+ end
65
+
66
+ def state_patch(fields:, cwd:)
67
+ args = { 'patch' => true }
68
+ fields.each { |k, v| args[k.to_s] = v.to_s }
69
+ call('state', args, cwd: cwd)
70
+ end
71
+
72
+ def state_get(section: nil, cwd:)
73
+ args = section ? { 'get' => section.to_s } : { 'get' => true }
74
+ call('state', args, cwd: cwd)
75
+ end
76
+
77
+ # Phase commands
78
+ def phase_find(phase, cwd:)
79
+ call('phase', { 'find' => phase.to_s }, cwd: cwd)
80
+ end
81
+
82
+ def phase_list(cwd:)
83
+ call('phase', { 'list' => true }, cwd: cwd)
84
+ end
85
+
86
+ def phase_next_decimal(phase, cwd:)
87
+ call('phase', { 'next-decimal' => phase.to_s }, cwd: cwd)
88
+ end
89
+
90
+ def phase_add(description, cwd:)
91
+ call('phase', { 'add' => true, 'description' => description }, cwd: cwd)
92
+ end
93
+
94
+ # Roadmap commands
95
+ def roadmap_get_phase(phase, cwd:)
96
+ call('roadmap', { 'get-phase' => phase.to_s }, cwd: cwd)
97
+ end
98
+
99
+ def roadmap_analyze(cwd:)
100
+ call('roadmap', { 'analyze' => true }, cwd: cwd)
101
+ end
102
+
103
+ private
104
+
105
+ # Constrói o comando para execução
106
+ def build_command(command, args)
107
+ cmd = [binary_path, command]
108
+
109
+ # Handle subcommands - move to front as positional arg
110
+ subcommand = nil
111
+ subcommand_keys = %w[load json update patch get find list next-decimal add analyze]
112
+ args.each do |key, value|
113
+ if subcommand_keys.include?(key.to_s)
114
+ subcommand = value.to_s if value != true
115
+ subcommand = key.to_s if value == true
116
+ break
117
+ end
118
+ end
119
+
120
+ cmd << subcommand if subcommand
121
+
122
+ # Add named arguments (excluding subcommand keys)
123
+ args.each do |key, value|
124
+ next if subcommand_keys.include?(key.to_s)
125
+ next if value.nil?
126
+
127
+ if value == true
128
+ cmd << "--#{key}"
129
+ elsif value.is_a?(String) && !value.empty?
130
+ cmd << "--#{key}=#{value}"
131
+ end
132
+ end
133
+
134
+ cmd
135
+ end
136
+
137
+ # Encontra o binário gsd-core
138
+ def find_binary
139
+ # Prioridade 1: bin/ no root do projeto (Windows e Unix)
140
+ # lib/gsd/go/bridge.rb -> ../../../bin/gsd-core
141
+ project_root = File.expand_path('../../../..', __dir__)
142
+ local_bin = File.join(project_root, 'bin', 'gsd-core')
143
+ local_bin_exe = "#{local_bin}.exe"
144
+
145
+ if File.executable?(local_bin)
146
+ return local_bin
147
+ elsif File.executable?(local_bin_exe)
148
+ return local_bin_exe
149
+ end
150
+
151
+ # Prioridade 2: bin/ relativo ao diretório atual
152
+ cwd_bin = File.join(Dir.pwd, 'bin', 'gsd-core')
153
+ cwd_bin_exe = "#{cwd_bin}.exe"
154
+ return cwd_bin if File.executable?(cwd_bin)
155
+ return cwd_bin_exe if File.executable?(cwd_bin_exe)
156
+
157
+ # Prioridade 3: PATH do sistema
158
+ in_path = `where gsd-core 2>nul`.strip
159
+ in_path = `which gsd-core 2>/dev/null`.strip if in_path.empty?
160
+ return in_path unless in_path.empty?
161
+
162
+ raise BridgeError, 'gsd-core binary not found. Run `make build` first.'
163
+ end
164
+
165
+ def log_debug(msg)
166
+ warn "[gsd-go] #{msg}" if ENV['GSD_DEBUG']
167
+ end
168
+ end
169
+
170
+ class BridgeError < StandardError; end
171
+ end
172
+ end
173
+ end