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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +456 -0
- data/bin/gsd +8 -0
- data/bin/gsd-core-darwin-amd64 +0 -0
- data/bin/gsd-core-darwin-arm64 +0 -0
- data/bin/gsd-core-linux-amd64 +0 -0
- data/bin/gsd-core-linux-arm64 +0 -0
- data/bin/gsd-core-windows-amd64.exe +0 -0
- data/bin/gsd-core-windows-arm64.exe +0 -0
- data/bin/gsd-core.exe +0 -0
- data/lib/gsd/agents/coordinator.rb +195 -0
- data/lib/gsd/agents/task_manager.rb +158 -0
- data/lib/gsd/agents/worker.rb +162 -0
- data/lib/gsd/agents.rb +30 -0
- data/lib/gsd/ai/chat.rb +486 -0
- data/lib/gsd/ai/cli.rb +248 -0
- data/lib/gsd/ai/command_parser.rb +97 -0
- data/lib/gsd/ai/commands/base.rb +42 -0
- data/lib/gsd/ai/commands/clear.rb +20 -0
- data/lib/gsd/ai/commands/context.rb +30 -0
- data/lib/gsd/ai/commands/cost.rb +30 -0
- data/lib/gsd/ai/commands/export.rb +42 -0
- data/lib/gsd/ai/commands/help.rb +61 -0
- data/lib/gsd/ai/commands/model.rb +67 -0
- data/lib/gsd/ai/commands/reset.rb +22 -0
- data/lib/gsd/ai/config.rb +256 -0
- data/lib/gsd/ai/context.rb +324 -0
- data/lib/gsd/ai/cost_tracker.rb +361 -0
- data/lib/gsd/ai/git_context.rb +169 -0
- data/lib/gsd/ai/history.rb +384 -0
- data/lib/gsd/ai/providers/anthropic.rb +429 -0
- data/lib/gsd/ai/providers/base.rb +282 -0
- data/lib/gsd/ai/providers/lmstudio.rb +279 -0
- data/lib/gsd/ai/providers/ollama.rb +336 -0
- data/lib/gsd/ai/providers/openai.rb +396 -0
- data/lib/gsd/ai/providers/openrouter.rb +429 -0
- data/lib/gsd/ai/reference_resolver.rb +225 -0
- data/lib/gsd/ai/repl.rb +349 -0
- data/lib/gsd/ai/streaming.rb +438 -0
- data/lib/gsd/ai/ui.rb +429 -0
- data/lib/gsd/buddy/cli.rb +284 -0
- data/lib/gsd/buddy/gacha.rb +148 -0
- data/lib/gsd/buddy/renderer.rb +108 -0
- data/lib/gsd/buddy/species.rb +190 -0
- data/lib/gsd/buddy/stats.rb +156 -0
- data/lib/gsd/buddy.rb +28 -0
- data/lib/gsd/cli.rb +455 -0
- data/lib/gsd/commands.rb +198 -0
- data/lib/gsd/config.rb +183 -0
- data/lib/gsd/error.rb +188 -0
- data/lib/gsd/frontmatter.rb +123 -0
- data/lib/gsd/go/bridge.rb +173 -0
- data/lib/gsd/history.rb +76 -0
- data/lib/gsd/milestone.rb +75 -0
- data/lib/gsd/output.rb +184 -0
- data/lib/gsd/phase.rb +102 -0
- data/lib/gsd/plugins/base.rb +92 -0
- data/lib/gsd/plugins/cli.rb +330 -0
- data/lib/gsd/plugins/config.rb +164 -0
- data/lib/gsd/plugins/hooks.rb +132 -0
- data/lib/gsd/plugins/installer.rb +158 -0
- data/lib/gsd/plugins/loader.rb +122 -0
- data/lib/gsd/plugins/manager.rb +187 -0
- data/lib/gsd/plugins/marketplace.rb +142 -0
- data/lib/gsd/plugins/sandbox.rb +114 -0
- data/lib/gsd/plugins/search.rb +131 -0
- data/lib/gsd/plugins/validator.rb +157 -0
- data/lib/gsd/plugins.rb +48 -0
- data/lib/gsd/profile.rb +127 -0
- data/lib/gsd/research.rb +85 -0
- data/lib/gsd/roadmap.rb +90 -0
- data/lib/gsd/skills/bundled/commit.md +58 -0
- data/lib/gsd/skills/bundled/debug.md +28 -0
- data/lib/gsd/skills/bundled/explain.md +41 -0
- data/lib/gsd/skills/bundled/plan.md +42 -0
- data/lib/gsd/skills/bundled/verify.md +26 -0
- data/lib/gsd/skills/loader.rb +189 -0
- data/lib/gsd/state.rb +102 -0
- data/lib/gsd/template.rb +106 -0
- data/lib/gsd/tools/ask_user_question.rb +179 -0
- data/lib/gsd/tools/base.rb +204 -0
- data/lib/gsd/tools/bash.rb +246 -0
- data/lib/gsd/tools/file_edit.rb +297 -0
- data/lib/gsd/tools/file_read.rb +199 -0
- data/lib/gsd/tools/file_write.rb +153 -0
- data/lib/gsd/tools/glob.rb +202 -0
- data/lib/gsd/tools/grep.rb +227 -0
- data/lib/gsd/tools/gsd_frontmatter.rb +165 -0
- data/lib/gsd/tools/gsd_phase.rb +140 -0
- data/lib/gsd/tools/gsd_roadmap.rb +108 -0
- data/lib/gsd/tools/gsd_state.rb +143 -0
- data/lib/gsd/tools/gsd_template.rb +157 -0
- data/lib/gsd/tools/gsd_verify.rb +159 -0
- data/lib/gsd/tools/registry.rb +103 -0
- data/lib/gsd/tools/task.rb +235 -0
- data/lib/gsd/tools/todo_write.rb +290 -0
- data/lib/gsd/tools/web.rb +260 -0
- data/lib/gsd/tui/app.rb +366 -0
- data/lib/gsd/tui/auto_complete.rb +79 -0
- data/lib/gsd/tui/colors.rb +111 -0
- data/lib/gsd/tui/command_palette.rb +126 -0
- data/lib/gsd/tui/header.rb +38 -0
- data/lib/gsd/tui/input_box.rb +199 -0
- data/lib/gsd/tui/spinner.rb +40 -0
- data/lib/gsd/tui/status_bar.rb +51 -0
- data/lib/gsd/tui.rb +17 -0
- data/lib/gsd/validator.rb +216 -0
- data/lib/gsd/verify.rb +175 -0
- data/lib/gsd/version.rb +5 -0
- data/lib/gsd/workstream.rb +91 -0
- 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
|