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/cli.rb
ADDED
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'version'
|
|
4
|
+
require_relative 'go/bridge'
|
|
5
|
+
require_relative 'state'
|
|
6
|
+
require_relative 'phase'
|
|
7
|
+
require_relative 'roadmap'
|
|
8
|
+
require_relative 'ai/cli'
|
|
9
|
+
require_relative 'tui/app'
|
|
10
|
+
|
|
11
|
+
module Gsd
|
|
12
|
+
# CLI parser e dispatcher principal
|
|
13
|
+
class CLI
|
|
14
|
+
def initialize(args = [])
|
|
15
|
+
@args = args
|
|
16
|
+
@command = args.first
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def run
|
|
20
|
+
case @command
|
|
21
|
+
when nil, 'help', '--help', '-h'
|
|
22
|
+
print_help
|
|
23
|
+
0
|
|
24
|
+
when 'version', '--version', '-v'
|
|
25
|
+
print_version
|
|
26
|
+
0
|
|
27
|
+
when 'hello'
|
|
28
|
+
run_hello
|
|
29
|
+
when 'ai'
|
|
30
|
+
run_ai(@args[1..])
|
|
31
|
+
when 'tui'
|
|
32
|
+
run_tui(@args[1..])
|
|
33
|
+
when 'state'
|
|
34
|
+
run_state(@args[1..])
|
|
35
|
+
when 'phase'
|
|
36
|
+
run_phase(@args[1..])
|
|
37
|
+
when 'roadmap'
|
|
38
|
+
run_roadmap(@args[1..])
|
|
39
|
+
else
|
|
40
|
+
warn "Unknown command: #{@command}"
|
|
41
|
+
print_help
|
|
42
|
+
1
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
# ========================================================================
|
|
49
|
+
# Hello Command
|
|
50
|
+
# ========================================================================
|
|
51
|
+
|
|
52
|
+
def run_hello
|
|
53
|
+
result = Go::Bridge.call('hello', parse_named_args(@args[1..]))
|
|
54
|
+
output_json(result)
|
|
55
|
+
0
|
|
56
|
+
rescue => e
|
|
57
|
+
warn "Error: #{e.message}"
|
|
58
|
+
1
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# ========================================================================
|
|
62
|
+
# AI Command
|
|
63
|
+
# ========================================================================
|
|
64
|
+
|
|
65
|
+
def run_ai(args)
|
|
66
|
+
ai_cli = Gsd::AI::CLI.new(args, cwd: parse_cwd(args))
|
|
67
|
+
ai_cli.run
|
|
68
|
+
0
|
|
69
|
+
rescue => e
|
|
70
|
+
warn "Error: #{e.message}"
|
|
71
|
+
1
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# ========================================================================
|
|
75
|
+
# TUI Command
|
|
76
|
+
# ========================================================================
|
|
77
|
+
|
|
78
|
+
def run_tui(args)
|
|
79
|
+
theme = :fantasy
|
|
80
|
+
header_style = :pixel
|
|
81
|
+
|
|
82
|
+
args.each do |arg|
|
|
83
|
+
case arg
|
|
84
|
+
when '--theme=kilo'
|
|
85
|
+
theme = :kilo
|
|
86
|
+
when '--theme=dark'
|
|
87
|
+
theme = :dark
|
|
88
|
+
when '--theme=light'
|
|
89
|
+
theme = :light
|
|
90
|
+
when '--no-header'
|
|
91
|
+
header_style = :simple
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
tui_app = Gsd::TUI::App.new(theme: theme, header_style: header_style)
|
|
96
|
+
tui_app.run
|
|
97
|
+
0
|
|
98
|
+
rescue => e
|
|
99
|
+
warn "Error: #{e.message}"
|
|
100
|
+
1
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# ========================================================================
|
|
104
|
+
# State Commands
|
|
105
|
+
# ========================================================================
|
|
106
|
+
|
|
107
|
+
def run_state(args)
|
|
108
|
+
args = args.compact.reject { |a| a.nil? }
|
|
109
|
+
subcommand = args.shift
|
|
110
|
+
cwd = parse_cwd(args)
|
|
111
|
+
|
|
112
|
+
case subcommand
|
|
113
|
+
when 'load'
|
|
114
|
+
cmd_state_load(cwd)
|
|
115
|
+
when 'json'
|
|
116
|
+
cmd_state_json(cwd)
|
|
117
|
+
when 'update'
|
|
118
|
+
cmd_state_update(args, cwd)
|
|
119
|
+
when 'patch'
|
|
120
|
+
cmd_state_patch(args, cwd)
|
|
121
|
+
when 'get'
|
|
122
|
+
cmd_state_get(args, cwd)
|
|
123
|
+
when nil
|
|
124
|
+
warn "Usage: gsd state <subcommand> [options]"
|
|
125
|
+
warn "Subcommands: load, json, update, patch, get"
|
|
126
|
+
1
|
|
127
|
+
else
|
|
128
|
+
warn "Unknown state subcommand: #{subcommand}"
|
|
129
|
+
warn "Available: load, json, update, patch, get"
|
|
130
|
+
1
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def cmd_state_load(cwd)
|
|
135
|
+
result = Gsd::State.load(cwd: cwd)
|
|
136
|
+
output_json({ 'success' => true, 'data' => result })
|
|
137
|
+
0
|
|
138
|
+
rescue => e
|
|
139
|
+
output_error("Failed to load state: #{e.message}")
|
|
140
|
+
1
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def cmd_state_json(cwd)
|
|
144
|
+
result = Gsd::State.json(cwd: cwd)
|
|
145
|
+
output_json({ 'success' => true, 'data' => result })
|
|
146
|
+
0
|
|
147
|
+
rescue => e
|
|
148
|
+
output_error("Failed to get state JSON: #{e.message}")
|
|
149
|
+
1
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def cmd_state_update(args, cwd)
|
|
153
|
+
field = parse_named_arg(args, 'field')
|
|
154
|
+
value = parse_named_arg(args, 'value')
|
|
155
|
+
|
|
156
|
+
unless field && value
|
|
157
|
+
output_error("field and value are required. Usage: gsd state update --field X --value Y")
|
|
158
|
+
return 1
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
result = Gsd::State.update(field: field, value: value, cwd: cwd)
|
|
162
|
+
output_json({ 'success' => true, 'data' => result })
|
|
163
|
+
0
|
|
164
|
+
rescue => e
|
|
165
|
+
output_error("Failed to update state: #{e.message}")
|
|
166
|
+
1
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def cmd_state_patch(args, cwd)
|
|
170
|
+
fields = parse_patch_args(args)
|
|
171
|
+
|
|
172
|
+
if fields.empty?
|
|
173
|
+
output_error("At least one field is required. Usage: gsd state patch --field1 val1 --field2 val2")
|
|
174
|
+
return 1
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
result = Gsd::State.patch(fields: fields, cwd: cwd)
|
|
178
|
+
output_json({ 'success' => true, 'data' => result })
|
|
179
|
+
0
|
|
180
|
+
rescue => e
|
|
181
|
+
output_error("Failed to patch state: #{e.message}")
|
|
182
|
+
1
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def cmd_state_get(args, cwd)
|
|
186
|
+
section = args.first
|
|
187
|
+
result = Gsd::State.get(section: section, cwd: cwd)
|
|
188
|
+
output_json({ 'success' => true, 'data' => result })
|
|
189
|
+
0
|
|
190
|
+
rescue => e
|
|
191
|
+
output_error("Failed to get state: #{e.message}")
|
|
192
|
+
1
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# ========================================================================
|
|
196
|
+
# Phase Commands
|
|
197
|
+
# ========================================================================
|
|
198
|
+
|
|
199
|
+
def run_phase(args)
|
|
200
|
+
subcommand = args.shift
|
|
201
|
+
cwd = parse_cwd(args)
|
|
202
|
+
|
|
203
|
+
case subcommand
|
|
204
|
+
when 'find'
|
|
205
|
+
cmd_phase_find(args.first, cwd)
|
|
206
|
+
when 'list'
|
|
207
|
+
cmd_phase_list(cwd)
|
|
208
|
+
when 'next-decimal'
|
|
209
|
+
cmd_phase_next_decimal(args.first, cwd)
|
|
210
|
+
when 'add'
|
|
211
|
+
cmd_phase_add(args, cwd)
|
|
212
|
+
when nil
|
|
213
|
+
warn "Usage: gsd phase <subcommand> [options]"
|
|
214
|
+
warn "Subcommands: find, list, next-decimal, add"
|
|
215
|
+
1
|
|
216
|
+
else
|
|
217
|
+
warn "Unknown phase subcommand: #{subcommand}"
|
|
218
|
+
warn "Available: find, list, next-decimal, add"
|
|
219
|
+
1
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def cmd_phase_find(phase_num, cwd)
|
|
224
|
+
unless phase_num
|
|
225
|
+
output_error("phase number is required. Usage: gsd phase find <phase>")
|
|
226
|
+
return 1
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
result = Gsd::Phase.find(phase_num, cwd: cwd)
|
|
230
|
+
output_json({ 'success' => true, 'data' => result })
|
|
231
|
+
0
|
|
232
|
+
rescue => e
|
|
233
|
+
output_error("Failed to find phase: #{e.message}")
|
|
234
|
+
1
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def cmd_phase_list(cwd)
|
|
238
|
+
result = Gsd::Phase.list(cwd: cwd)
|
|
239
|
+
output_json({ 'success' => true, 'data' => result })
|
|
240
|
+
0
|
|
241
|
+
rescue => e
|
|
242
|
+
output_error("Failed to list phases: #{e.message}")
|
|
243
|
+
1
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def cmd_phase_next_decimal(phase_num, cwd)
|
|
247
|
+
unless phase_num
|
|
248
|
+
output_error("phase number is required. Usage: gsd phase next-decimal <phase>")
|
|
249
|
+
return 1
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
result = Gsd::Phase.next_decimal(phase_num, cwd: cwd)
|
|
253
|
+
output_json({ 'success' => true, 'data' => result })
|
|
254
|
+
0
|
|
255
|
+
rescue => e
|
|
256
|
+
output_error("Failed to calculate next decimal: #{e.message}")
|
|
257
|
+
1
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def cmd_phase_add(args, cwd)
|
|
261
|
+
# Join all remaining args as description (handles multi-word descriptions)
|
|
262
|
+
description = args.join(' ').strip
|
|
263
|
+
|
|
264
|
+
unless description && !description.empty?
|
|
265
|
+
output_error("description is required. Usage: gsd phase add <description>")
|
|
266
|
+
return 1
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
result = Gsd::Phase.add(description, cwd: cwd)
|
|
270
|
+
output_json({ 'success' => true, 'data' => result })
|
|
271
|
+
0
|
|
272
|
+
rescue => e
|
|
273
|
+
output_error("Failed to add phase: #{e.message}")
|
|
274
|
+
1
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
# ========================================================================
|
|
278
|
+
# Roadmap Commands
|
|
279
|
+
# ========================================================================
|
|
280
|
+
|
|
281
|
+
def run_roadmap(args)
|
|
282
|
+
subcommand = args.shift
|
|
283
|
+
cwd = parse_cwd(args)
|
|
284
|
+
|
|
285
|
+
case subcommand
|
|
286
|
+
when 'get-phase'
|
|
287
|
+
cmd_roadmap_get_phase(args.first, cwd)
|
|
288
|
+
when 'analyze'
|
|
289
|
+
cmd_roadmap_analyze(cwd)
|
|
290
|
+
when nil
|
|
291
|
+
warn "Usage: gsd roadmap <subcommand> [options]"
|
|
292
|
+
warn "Subcommands: get-phase, analyze"
|
|
293
|
+
1
|
|
294
|
+
else
|
|
295
|
+
warn "Unknown roadmap subcommand: #{subcommand}"
|
|
296
|
+
warn "Available: get-phase, analyze"
|
|
297
|
+
1
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def cmd_roadmap_get_phase(phase_num, cwd)
|
|
302
|
+
unless phase_num
|
|
303
|
+
output_error("phase number is required. Usage: gsd roadmap get-phase <phase>")
|
|
304
|
+
return 1
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
result = Gsd::Roadmap.get_phase(phase_num, cwd: cwd)
|
|
308
|
+
output_json({ 'success' => true, 'data' => result })
|
|
309
|
+
0
|
|
310
|
+
rescue => e
|
|
311
|
+
output_error("Failed to get phase from roadmap: #{e.message}")
|
|
312
|
+
1
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def cmd_roadmap_analyze(cwd)
|
|
316
|
+
result = Gsd::Roadmap.analyze(cwd: cwd)
|
|
317
|
+
output_json({ 'success' => true, 'data' => result })
|
|
318
|
+
0
|
|
319
|
+
rescue => e
|
|
320
|
+
output_error("Failed to analyze roadmap: #{e.message}")
|
|
321
|
+
1
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
# ========================================================================
|
|
325
|
+
# Helpers
|
|
326
|
+
# ========================================================================
|
|
327
|
+
|
|
328
|
+
def parse_named_arg(args, name)
|
|
329
|
+
prefix = "--#{name}="
|
|
330
|
+
args.each do |arg|
|
|
331
|
+
if arg.start_with?(prefix)
|
|
332
|
+
return arg[prefix.length..]
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
nil
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
def parse_named_args(args)
|
|
339
|
+
result = {}
|
|
340
|
+
return result if args.nil? || args.empty?
|
|
341
|
+
|
|
342
|
+
args.each do |arg|
|
|
343
|
+
next unless arg.is_a?(String) && arg.start_with?('--')
|
|
344
|
+
key, value = arg[2..].split('=', 2)
|
|
345
|
+
result[key] = value || true
|
|
346
|
+
end
|
|
347
|
+
result
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
def parse_patch_args(args)
|
|
351
|
+
fields = {}
|
|
352
|
+
i = 0
|
|
353
|
+
while i < args.length
|
|
354
|
+
if args[i].start_with?('--') && i + 1 < args.length && !args[i + 1].start_with?('--')
|
|
355
|
+
key = args[i][2..]
|
|
356
|
+
value = args[i + 1]
|
|
357
|
+
fields[key] = value
|
|
358
|
+
i += 2
|
|
359
|
+
else
|
|
360
|
+
i += 1
|
|
361
|
+
end
|
|
362
|
+
end
|
|
363
|
+
fields
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def parse_cwd(args)
|
|
367
|
+
cwd = parse_named_arg(args, 'cwd')
|
|
368
|
+
cwd || Dir.pwd
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
def output_json(data)
|
|
372
|
+
puts JSON.pretty_generate(data)
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
def output_error(message)
|
|
376
|
+
puts JSON.pretty_generate({
|
|
377
|
+
'success' => false,
|
|
378
|
+
'error' => message
|
|
379
|
+
})
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
def print_help
|
|
383
|
+
puts <<~HELP
|
|
384
|
+
fantasy-cli v#{VERSION} - Fantasy CLI (Ruby + Go)
|
|
385
|
+
|
|
386
|
+
Usage:
|
|
387
|
+
gsd <command> [options]
|
|
388
|
+
|
|
389
|
+
Commands:
|
|
390
|
+
hello Print greeting from Go
|
|
391
|
+
ai AI Chat Interface (REPL + one-shot)
|
|
392
|
+
tui Terminal User Interface (TUI)
|
|
393
|
+
version Print version information
|
|
394
|
+
state State operations
|
|
395
|
+
phase Phase operations
|
|
396
|
+
roadmap Roadmap operations
|
|
397
|
+
help Show this help message
|
|
398
|
+
|
|
399
|
+
AI Subcommands:
|
|
400
|
+
gsd ai # Inicia REPL interativo
|
|
401
|
+
gsd ai "mensagem" # One-shot command
|
|
402
|
+
gsd ai --provider <name> Provider: anthropic, openai, ollama
|
|
403
|
+
gsd ai --model <name> Modelo do LLM
|
|
404
|
+
gsd ai --debug Habilita modo debug
|
|
405
|
+
|
|
406
|
+
TUI Options:
|
|
407
|
+
gsd tui # Launch TUI
|
|
408
|
+
gsd tui --theme=kilo # Kilo theme (yellow/black)
|
|
409
|
+
gsd tui --theme=dark # Dark theme
|
|
410
|
+
gsd tui --theme=light # Light theme
|
|
411
|
+
gsd tui --no-header # Without header
|
|
412
|
+
|
|
413
|
+
State Subcommands:
|
|
414
|
+
load Load STATE.md
|
|
415
|
+
json Output STATE.md frontmatter as JSON
|
|
416
|
+
update Update a STATE.md field (--field, --value)
|
|
417
|
+
patch Batch update STATE.md fields
|
|
418
|
+
get [section] Get STATE.md content or section
|
|
419
|
+
|
|
420
|
+
Phase Subcommands:
|
|
421
|
+
find <phase> Find phase directory
|
|
422
|
+
list List all phases
|
|
423
|
+
next-decimal <n> Calculate next decimal phase
|
|
424
|
+
add <description> Add new phase
|
|
425
|
+
|
|
426
|
+
Roadmap Subcommands:
|
|
427
|
+
get-phase <phase> Extract phase section from ROADMAP.md
|
|
428
|
+
analyze Full roadmap analysis
|
|
429
|
+
|
|
430
|
+
Options:
|
|
431
|
+
--cwd=<path> Working directory (default: current)
|
|
432
|
+
--field=<name> Field name (for state update)
|
|
433
|
+
--value=<val> Field value (for state update)
|
|
434
|
+
|
|
435
|
+
Examples:
|
|
436
|
+
gsd hello
|
|
437
|
+
gsd hello --name=World
|
|
438
|
+
gsd version
|
|
439
|
+
gsd state json --cwd /path/to/project
|
|
440
|
+
gsd state update --field current_phase --value 2
|
|
441
|
+
gsd phase find 1
|
|
442
|
+
gsd phase next-decimal 1
|
|
443
|
+
gsd roadmap get-phase 1
|
|
444
|
+
gsd roadmap analyze
|
|
445
|
+
|
|
446
|
+
For more information, see README.md
|
|
447
|
+
HELP
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
def print_version
|
|
451
|
+
puts "gsd v#{VERSION} (Ruby + Go)"
|
|
452
|
+
puts "gsd-core v#{Go::Bridge.core_version}"
|
|
453
|
+
end
|
|
454
|
+
end
|
|
455
|
+
end
|
data/lib/gsd/commands.rb
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'open3'
|
|
4
|
+
require 'date'
|
|
5
|
+
|
|
6
|
+
module Gsd
|
|
7
|
+
# Commands atômicos do GSD
|
|
8
|
+
module Commands
|
|
9
|
+
class << self
|
|
10
|
+
# Git commit wrapper
|
|
11
|
+
#
|
|
12
|
+
# @param message [String] Commit message
|
|
13
|
+
# @param files [Array<String>] Files to commit
|
|
14
|
+
# @param cwd [String] Working directory
|
|
15
|
+
# @param no_verify [Boolean] Skip pre-commit hooks
|
|
16
|
+
# @return [Hash] Result with success status and message/error
|
|
17
|
+
def commit(message, files: [], cwd: nil, no_verify: false)
|
|
18
|
+
cwd ||= Dir.pwd
|
|
19
|
+
cmd = ['git', 'commit', '-m', message]
|
|
20
|
+
cmd << '--no-verify' if no_verify
|
|
21
|
+
cmd << '--'
|
|
22
|
+
cmd += files unless files.empty?
|
|
23
|
+
|
|
24
|
+
stdout, stderr, status = Open3.capture3(*cmd, chdir: cwd)
|
|
25
|
+
|
|
26
|
+
if status.success?
|
|
27
|
+
{ success: true, message: stdout.strip }
|
|
28
|
+
else
|
|
29
|
+
{ success: false, error: stderr.strip }
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Generate URL-safe slug from text
|
|
34
|
+
#
|
|
35
|
+
# @param text [String] Text to convert to slug
|
|
36
|
+
# @return [String] URL-safe slug
|
|
37
|
+
def generate_slug(text)
|
|
38
|
+
text
|
|
39
|
+
.downcase
|
|
40
|
+
.gsub(/[^\w\s-]/, '')
|
|
41
|
+
.gsub(/\s+/, '-')
|
|
42
|
+
.gsub(/-+/, '-')
|
|
43
|
+
.strip('-')
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Get current timestamp in various formats
|
|
47
|
+
#
|
|
48
|
+
# @param format [String] Format type: 'full', 'date', 'filename'
|
|
49
|
+
# @return [String] Timestamp string
|
|
50
|
+
def current_timestamp(format = 'full')
|
|
51
|
+
case format
|
|
52
|
+
when 'date'
|
|
53
|
+
Date.today.to_s
|
|
54
|
+
when 'filename'
|
|
55
|
+
Time.now.strftime('%Y%m%d%H%M%S')
|
|
56
|
+
else
|
|
57
|
+
Time.now.iso8601
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Verify if a path exists
|
|
62
|
+
#
|
|
63
|
+
# @param path [String] Path to verify
|
|
64
|
+
# @return [Hash] Result with exists status and path
|
|
65
|
+
def verify_path_exists(path)
|
|
66
|
+
if File.exist?(path)
|
|
67
|
+
{ exists: true, path: path }
|
|
68
|
+
else
|
|
69
|
+
{ exists: false, path: path, error: "Path not found: #{path}" }
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# List pending todos
|
|
74
|
+
#
|
|
75
|
+
# @param area [String] Optional area filter
|
|
76
|
+
# @param cwd [String] Working directory
|
|
77
|
+
# @return [Hash] Result with count and todo list
|
|
78
|
+
def list_todos(area = nil, cwd: nil)
|
|
79
|
+
cwd ||= Dir.pwd
|
|
80
|
+
todos_dir = File.join(cwd, '.planning', 'todos')
|
|
81
|
+
return { count: 0, todos: [] } unless File.directory?(todos_dir)
|
|
82
|
+
|
|
83
|
+
pending_dir = File.join(todos_dir, 'pending')
|
|
84
|
+
return { count: 0, todos: [] } unless File.directory?(pending_dir)
|
|
85
|
+
|
|
86
|
+
files = Dir.glob('*.md', base: pending_dir)
|
|
87
|
+
files = files.select { |f| f.start_with?(area) } if area
|
|
88
|
+
|
|
89
|
+
{
|
|
90
|
+
count: files.length,
|
|
91
|
+
todos: files.map { |f| f.gsub('.md', '') }
|
|
92
|
+
}
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Complete a todo (move from pending to completed)
|
|
96
|
+
#
|
|
97
|
+
# @param filename [String] Todo filename (without .md)
|
|
98
|
+
# @param cwd [String] Working directory
|
|
99
|
+
# @return [Hash] Result with success status
|
|
100
|
+
def todo_complete(filename, cwd: nil)
|
|
101
|
+
cwd ||= Dir.pwd
|
|
102
|
+
todos_dir = File.join(cwd, '.planning', 'todos')
|
|
103
|
+
pending_dir = File.join(todos_dir, 'pending')
|
|
104
|
+
completed_dir = File.join(todos_dir, 'completed')
|
|
105
|
+
|
|
106
|
+
pending_file = File.join(pending_dir, "#{filename}.md")
|
|
107
|
+
completed_file = File.join(completed_dir, "#{filename}.md")
|
|
108
|
+
|
|
109
|
+
unless File.exist?(pending_file)
|
|
110
|
+
return { success: false, error: "Todo not found: #{filename}" }
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
FileUtils.mkdir_p(completed_dir) unless File.directory?(completed_dir)
|
|
114
|
+
FileUtils.mv(pending_file, completed_file)
|
|
115
|
+
|
|
116
|
+
{ success: true, message: "Todo completed: #{filename}" }
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Generate a unique slug with timestamp
|
|
120
|
+
#
|
|
121
|
+
# @param text [String] Base text for slug
|
|
122
|
+
# @return [String] Unique slug with timestamp
|
|
123
|
+
def generate_unique_slug(text)
|
|
124
|
+
base_slug = generate_slug(text)
|
|
125
|
+
timestamp = Time.now.strftime('%Y%m%d%H%M%S')
|
|
126
|
+
"#{base_slug}-#{timestamp}"
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Verify if a file has valid frontmatter
|
|
130
|
+
#
|
|
131
|
+
# @param path [String] File path to check
|
|
132
|
+
# @return [Hash] Result with valid status and frontmatter if present
|
|
133
|
+
def has_frontmatter?(path)
|
|
134
|
+
return { valid: false, error: 'File not found' } unless File.exist?(path)
|
|
135
|
+
|
|
136
|
+
content = File.read(path)
|
|
137
|
+
return { valid: false, error: 'Empty file' } if content.empty?
|
|
138
|
+
|
|
139
|
+
unless content.start_with?('---')
|
|
140
|
+
return { valid: false, frontmatter: nil }
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
lines = content.split("\n")
|
|
144
|
+
end_index = lines[1..-1]&.index { |line| line.strip == '---' }
|
|
145
|
+
|
|
146
|
+
if end_index
|
|
147
|
+
frontmatter_lines = lines[1..end_index]
|
|
148
|
+
frontmatter = {}
|
|
149
|
+
frontmatter_lines.each do |line|
|
|
150
|
+
key, value = line.split(':', 2)
|
|
151
|
+
if key && value
|
|
152
|
+
frontmatter[key.strip] = value.strip.gsub(/["']/, '')
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
{ valid: true, frontmatter: frontmatter }
|
|
156
|
+
else
|
|
157
|
+
{ valid: false, error: 'Unclosed frontmatter' }
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Extract text from markdown file (skip frontmatter)
|
|
162
|
+
#
|
|
163
|
+
# @param path [String] File path
|
|
164
|
+
# @return [Hash] Result with content or error
|
|
165
|
+
def extract_markdown_content(path)
|
|
166
|
+
return { error: 'File not found' } unless File.exist?(path)
|
|
167
|
+
|
|
168
|
+
content = File.read(path)
|
|
169
|
+
|
|
170
|
+
# Skip frontmatter if present
|
|
171
|
+
if content.start_with?('---')
|
|
172
|
+
lines = content.split("\n")
|
|
173
|
+
end_index = lines[1..-1]&.index { |line| line.strip == '---' }
|
|
174
|
+
if end_index
|
|
175
|
+
content = lines[end_index + 2..-1]&.join("\n") || ''
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
{ content: content.strip }
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Count lines in file
|
|
183
|
+
#
|
|
184
|
+
# @param path [String] File path
|
|
185
|
+
# @return [Hash] Result with line count
|
|
186
|
+
def count_lines(path)
|
|
187
|
+
return { error: 'File not found' } unless File.exist?(path)
|
|
188
|
+
|
|
189
|
+
lines = File.readlines(path)
|
|
190
|
+
{
|
|
191
|
+
total: lines.length,
|
|
192
|
+
non_empty: lines.count { |l| !l.strip.empty? },
|
|
193
|
+
code: lines.count { |l| !l.strip.empty? && !l.strip.start_with?('#') }
|
|
194
|
+
}
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|