@ai-content-space/loopx 0.2.8 → 0.2.10
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.
- package/README.md +26 -9
- package/README.zh-CN.md +26 -9
- package/docs/loopx/design/loopx-skill-suite-v1-design.md +12 -0
- package/docs/loopx/plans/2026-06-14-loopx-spec-memory-context-loading.md +948 -0
- package/docs/loopx/plans/2026-06-15-support-lens-skills-migration.md +1153 -0
- package/package.json +6 -1
- package/plugins/loopx/.codex-plugin/plugin.json +1 -1
- package/plugins/loopx/skills/api-designer/SKILL.md +232 -0
- package/plugins/loopx/skills/api-designer/references/error-handling.md +541 -0
- package/plugins/loopx/skills/api-designer/references/openapi.md +824 -0
- package/plugins/loopx/skills/api-designer/references/pagination.md +494 -0
- package/plugins/loopx/skills/api-designer/references/rest-patterns.md +335 -0
- package/plugins/loopx/skills/api-designer/references/versioning.md +391 -0
- package/plugins/loopx/skills/architecture-designer/SKILL.md +117 -0
- package/plugins/loopx/skills/architecture-designer/references/adr-template.md +116 -0
- package/plugins/loopx/skills/architecture-designer/references/architecture-patterns.md +346 -0
- package/plugins/loopx/skills/architecture-designer/references/database-selection.md +102 -0
- package/plugins/loopx/skills/architecture-designer/references/nfr-checklist.md +212 -0
- package/plugins/loopx/skills/architecture-designer/references/system-design.md +313 -0
- package/plugins/loopx/skills/clarify/SKILL.md +12 -1
- package/plugins/loopx/skills/cli-developer/SKILL.md +124 -0
- package/plugins/loopx/skills/cli-developer/references/design-patterns.md +221 -0
- package/plugins/loopx/skills/cli-developer/references/go-cli.md +540 -0
- package/plugins/loopx/skills/cli-developer/references/node-cli.md +383 -0
- package/plugins/loopx/skills/cli-developer/references/python-cli.md +422 -0
- package/plugins/loopx/skills/cli-developer/references/ux-patterns.md +448 -0
- package/plugins/loopx/skills/debug/SKILL.md +1 -1
- package/plugins/loopx/skills/doc-readability/SKILL.md +1 -1
- package/plugins/loopx/skills/exec/SKILL.md +1 -1
- package/plugins/loopx/skills/final-review/SKILL.md +1 -1
- package/plugins/loopx/skills/finish/SKILL.md +1 -1
- package/plugins/loopx/skills/fix-review/SKILL.md +1 -1
- package/plugins/loopx/skills/go-style/SKILL.md +1 -1
- package/plugins/loopx/skills/kratos/SKILL.md +2 -1
- package/plugins/loopx/skills/plan-to-exec/SKILL.md +12 -1
- package/plugins/loopx/skills/refactor-plan/SKILL.md +1 -1
- package/plugins/loopx/skills/requirement-analyzer/SKILL.md +161 -0
- package/plugins/loopx/skills/requirement-analyzer/references/example-reports.md +170 -0
- package/plugins/loopx/skills/requirement-analyzer/references/prd-gap-checklist.md +167 -0
- package/plugins/loopx/skills/requirement-analyzer/references/readiness-rubric.md +70 -0
- package/plugins/loopx/skills/requirement-analyzer/references/report-template.md +83 -0
- package/plugins/loopx/skills/review/SKILL.md +1 -1
- package/plugins/loopx/skills/spec/SKILL.md +12 -1
- package/plugins/loopx/skills/sql-style/SKILL.md +108 -0
- package/plugins/loopx/skills/sql-style/references/database-design.md +402 -0
- package/plugins/loopx/skills/sql-style/references/dialect-differences.md +419 -0
- package/plugins/loopx/skills/sql-style/references/optimization.md +384 -0
- package/plugins/loopx/skills/sql-style/references/query-patterns.md +285 -0
- package/plugins/loopx/skills/sql-style/references/window-functions.md +328 -0
- package/plugins/loopx/skills/subagent-exec/SKILL.md +1 -1
- package/plugins/loopx/skills/tdd/SKILL.md +1 -1
- package/plugins/loopx/skills/verify/SKILL.md +1 -1
- package/scripts/verify-skills.mjs +0 -2
- package/skills/RESOLVER.md +8 -1
- package/skills/api-designer/SKILL.md +232 -0
- package/skills/api-designer/references/error-handling.md +541 -0
- package/skills/api-designer/references/openapi.md +824 -0
- package/skills/api-designer/references/pagination.md +494 -0
- package/skills/api-designer/references/rest-patterns.md +335 -0
- package/skills/api-designer/references/versioning.md +391 -0
- package/skills/architecture-designer/SKILL.md +117 -0
- package/skills/architecture-designer/references/adr-template.md +116 -0
- package/skills/architecture-designer/references/architecture-patterns.md +346 -0
- package/skills/architecture-designer/references/database-selection.md +102 -0
- package/skills/architecture-designer/references/nfr-checklist.md +212 -0
- package/skills/architecture-designer/references/system-design.md +313 -0
- package/skills/clarify/SKILL.md +12 -1
- package/skills/cli-developer/SKILL.md +124 -0
- package/skills/cli-developer/references/design-patterns.md +221 -0
- package/skills/cli-developer/references/go-cli.md +540 -0
- package/skills/cli-developer/references/node-cli.md +383 -0
- package/skills/cli-developer/references/python-cli.md +422 -0
- package/skills/cli-developer/references/ux-patterns.md +448 -0
- package/skills/debug/SKILL.md +1 -1
- package/skills/doc-readability/SKILL.md +1 -1
- package/skills/exec/SKILL.md +1 -1
- package/skills/final-review/SKILL.md +1 -1
- package/skills/finish/SKILL.md +1 -1
- package/skills/fix-review/SKILL.md +1 -1
- package/skills/go-style/SKILL.md +1 -1
- package/skills/kratos/SKILL.md +2 -1
- package/skills/plan-to-exec/SKILL.md +12 -1
- package/skills/refactor-plan/SKILL.md +1 -1
- package/skills/requirement-analyzer/SKILL.md +161 -0
- package/skills/requirement-analyzer/references/example-reports.md +170 -0
- package/skills/requirement-analyzer/references/prd-gap-checklist.md +167 -0
- package/skills/requirement-analyzer/references/readiness-rubric.md +70 -0
- package/skills/requirement-analyzer/references/report-template.md +83 -0
- package/skills/review/SKILL.md +1 -1
- package/skills/spec/SKILL.md +12 -1
- package/skills/sql-style/SKILL.md +108 -0
- package/skills/sql-style/references/database-design.md +402 -0
- package/skills/sql-style/references/dialect-differences.md +419 -0
- package/skills/sql-style/references/optimization.md +384 -0
- package/skills/sql-style/references/query-patterns.md +285 -0
- package/skills/sql-style/references/window-functions.md +328 -0
- package/skills/subagent-exec/SKILL.md +1 -1
- package/skills/tdd/SKILL.md +1 -1
- package/skills/verify/SKILL.md +1 -1
- package/src/cli.mjs +4 -1
- package/src/context-manifest.mjs +51 -1
- package/src/install-discovery.mjs +114 -0
- package/src/loopx-context-artifacts.mjs +114 -0
- package/src/project-discovery.mjs +1 -0
- package/src/workflow.mjs +47 -3
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
# Go CLI Development
|
|
2
|
+
|
|
3
|
+
## Cobra (Recommended)
|
|
4
|
+
|
|
5
|
+
Powerful CLI framework used by kubectl, hugo, docker.
|
|
6
|
+
|
|
7
|
+
```go
|
|
8
|
+
// cmd/root.go
|
|
9
|
+
package cmd
|
|
10
|
+
|
|
11
|
+
import (
|
|
12
|
+
"fmt"
|
|
13
|
+
"os"
|
|
14
|
+
"github.com/spf13/cobra"
|
|
15
|
+
"github.com/spf13/viper"
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
var (
|
|
19
|
+
cfgFile string
|
|
20
|
+
verbose bool
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
var rootCmd = &cobra.Command{
|
|
24
|
+
Use: "mycli",
|
|
25
|
+
Short: "My awesome CLI tool",
|
|
26
|
+
Long: `A longer description of your CLI application`,
|
|
27
|
+
Version: "1.0.0",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
func Execute() {
|
|
31
|
+
if err := rootCmd.Execute(); err != nil {
|
|
32
|
+
fmt.Fprintln(os.Stderr, err)
|
|
33
|
+
os.Exit(1)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
func init() {
|
|
38
|
+
cobra.OnInitialize(initConfig)
|
|
39
|
+
|
|
40
|
+
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file")
|
|
41
|
+
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "verbose output")
|
|
42
|
+
|
|
43
|
+
viper.BindPFlag("verbose", rootCmd.PersistentFlags().Lookup("verbose"))
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
func initConfig() {
|
|
47
|
+
if cfgFile != "" {
|
|
48
|
+
viper.SetConfigFile(cfgFile)
|
|
49
|
+
} else {
|
|
50
|
+
home, err := os.UserHomeDir()
|
|
51
|
+
cobra.CheckErr(err)
|
|
52
|
+
|
|
53
|
+
viper.AddConfigPath(home)
|
|
54
|
+
viper.AddConfigPath(".")
|
|
55
|
+
viper.SetConfigType("yaml")
|
|
56
|
+
viper.SetConfigName(".mycli")
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
viper.AutomaticEnv()
|
|
60
|
+
|
|
61
|
+
if err := viper.ReadInConfig(); err == nil {
|
|
62
|
+
fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed())
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// cmd/init.go
|
|
67
|
+
package cmd
|
|
68
|
+
|
|
69
|
+
import (
|
|
70
|
+
"fmt"
|
|
71
|
+
"github.com/spf13/cobra"
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
var (
|
|
75
|
+
template string
|
|
76
|
+
force bool
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
var initCmd = &cobra.Command{
|
|
80
|
+
Use: "init [name]",
|
|
81
|
+
Short: "Initialize a new project",
|
|
82
|
+
Args: cobra.ExactArgs(1),
|
|
83
|
+
RunE: func(cmd *cobra.Command, args []string) error {
|
|
84
|
+
name := args[0]
|
|
85
|
+
return initProject(name, template, force)
|
|
86
|
+
},
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
func init() {
|
|
90
|
+
rootCmd.AddCommand(initCmd)
|
|
91
|
+
|
|
92
|
+
initCmd.Flags().StringVarP(&template, "template", "t", "default", "Project template")
|
|
93
|
+
initCmd.Flags().BoolVarP(&force, "force", "f", false, "Overwrite existing")
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
func initProject(name, template string, force bool) error {
|
|
97
|
+
fmt.Printf("Creating %s from %s\n", name, template)
|
|
98
|
+
return nil
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// cmd/deploy.go
|
|
102
|
+
package cmd
|
|
103
|
+
|
|
104
|
+
import (
|
|
105
|
+
"fmt"
|
|
106
|
+
"github.com/spf13/cobra"
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
var (
|
|
110
|
+
dryRun bool
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
var deployCmd = &cobra.Command{
|
|
114
|
+
Use: "deploy [environment]",
|
|
115
|
+
Short: "Deploy to environment",
|
|
116
|
+
Args: cobra.ExactArgs(1),
|
|
117
|
+
ValidArgs: []string{"dev", "staging", "prod"},
|
|
118
|
+
RunE: func(cmd *cobra.Command, args []string) error {
|
|
119
|
+
env := args[0]
|
|
120
|
+
return deploy(env, dryRun)
|
|
121
|
+
},
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
func init() {
|
|
125
|
+
rootCmd.AddCommand(deployCmd)
|
|
126
|
+
deployCmd.Flags().BoolVar(&dryRun, "dry-run", false, "Preview only")
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
func deploy(env string, dryRun bool) error {
|
|
130
|
+
if dryRun {
|
|
131
|
+
fmt.Printf("Would deploy to: %s\n", env)
|
|
132
|
+
} else {
|
|
133
|
+
fmt.Printf("Deploying to %s...\n", env)
|
|
134
|
+
}
|
|
135
|
+
return nil
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// main.go
|
|
139
|
+
package main
|
|
140
|
+
|
|
141
|
+
import "mycli/cmd"
|
|
142
|
+
|
|
143
|
+
func main() {
|
|
144
|
+
cmd.Execute()
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Viper (Configuration)
|
|
149
|
+
|
|
150
|
+
Configuration management with multiple sources.
|
|
151
|
+
|
|
152
|
+
```go
|
|
153
|
+
package config
|
|
154
|
+
|
|
155
|
+
import (
|
|
156
|
+
"fmt"
|
|
157
|
+
"github.com/spf13/viper"
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
type Config struct {
|
|
161
|
+
Environment string `mapstructure:"environment"`
|
|
162
|
+
Timeout int `mapstructure:"timeout"`
|
|
163
|
+
Verbose bool `mapstructure:"verbose"`
|
|
164
|
+
API APIConfig `mapstructure:"api"`
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
type APIConfig struct {
|
|
168
|
+
Endpoint string `mapstructure:"endpoint"`
|
|
169
|
+
Token string `mapstructure:"token"`
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
func Load() (*Config, error) {
|
|
173
|
+
// Set defaults
|
|
174
|
+
viper.SetDefault("environment", "development")
|
|
175
|
+
viper.SetDefault("timeout", 30)
|
|
176
|
+
viper.SetDefault("verbose", false)
|
|
177
|
+
|
|
178
|
+
// Config file locations
|
|
179
|
+
viper.SetConfigName("config")
|
|
180
|
+
viper.SetConfigType("yaml")
|
|
181
|
+
viper.AddConfigPath("/etc/mycli/")
|
|
182
|
+
viper.AddConfigPath("$HOME/.config/mycli")
|
|
183
|
+
viper.AddConfigPath(".")
|
|
184
|
+
|
|
185
|
+
// Environment variables
|
|
186
|
+
viper.SetEnvPrefix("MYCLI")
|
|
187
|
+
viper.AutomaticEnv()
|
|
188
|
+
|
|
189
|
+
// Read config
|
|
190
|
+
if err := viper.ReadInConfig(); err != nil {
|
|
191
|
+
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
|
|
192
|
+
return nil, fmt.Errorf("failed to read config: %w", err)
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Unmarshal into struct
|
|
197
|
+
var cfg Config
|
|
198
|
+
if err := viper.Unmarshal(&cfg); err != nil {
|
|
199
|
+
return nil, fmt.Errorf("failed to unmarshal config: %w", err)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return &cfg, nil
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## Bubble Tea (Interactive TUI)
|
|
207
|
+
|
|
208
|
+
Modern terminal UI framework for interactive CLIs.
|
|
209
|
+
|
|
210
|
+
```go
|
|
211
|
+
package main
|
|
212
|
+
|
|
213
|
+
import (
|
|
214
|
+
"fmt"
|
|
215
|
+
"os"
|
|
216
|
+
|
|
217
|
+
tea "github.com/charmbracelet/bubbletea"
|
|
218
|
+
"github.com/charmbracelet/lipgloss"
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
// Model
|
|
222
|
+
type model struct {
|
|
223
|
+
choices []string
|
|
224
|
+
cursor int
|
|
225
|
+
selected map[int]struct{}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
func initialModel() model {
|
|
229
|
+
return model{
|
|
230
|
+
choices: []string{"TypeScript", "ESLint", "Prettier", "Jest"},
|
|
231
|
+
selected: make(map[int]struct{}),
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Init
|
|
236
|
+
func (m model) Init() tea.Cmd {
|
|
237
|
+
return nil
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Update
|
|
241
|
+
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
242
|
+
switch msg := msg.(type) {
|
|
243
|
+
case tea.KeyMsg:
|
|
244
|
+
switch msg.String() {
|
|
245
|
+
case "ctrl+c", "q":
|
|
246
|
+
return m, tea.Quit
|
|
247
|
+
|
|
248
|
+
case "up", "k":
|
|
249
|
+
if m.cursor > 0 {
|
|
250
|
+
m.cursor--
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
case "down", "j":
|
|
254
|
+
if m.cursor < len(m.choices)-1 {
|
|
255
|
+
m.cursor++
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
case " ":
|
|
259
|
+
_, ok := m.selected[m.cursor]
|
|
260
|
+
if ok {
|
|
261
|
+
delete(m.selected, m.cursor)
|
|
262
|
+
} else {
|
|
263
|
+
m.selected[m.cursor] = struct{}{}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
case "enter":
|
|
267
|
+
return m, tea.Quit
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return m, nil
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// View
|
|
275
|
+
func (m model) View() string {
|
|
276
|
+
s := "Select features:\n\n"
|
|
277
|
+
|
|
278
|
+
for i, choice := range m.choices {
|
|
279
|
+
cursor := " "
|
|
280
|
+
if m.cursor == i {
|
|
281
|
+
cursor = ">"
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
checked := " "
|
|
285
|
+
if _, ok := m.selected[i]; ok {
|
|
286
|
+
checked = "x"
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
s += fmt.Sprintf("%s [%s] %s\n", cursor, checked, choice)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
s += "\nPress space to select, enter to confirm, q to quit.\n"
|
|
293
|
+
|
|
294
|
+
return s
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
func main() {
|
|
298
|
+
p := tea.NewProgram(initialModel())
|
|
299
|
+
if _, err := p.Run(); err != nil {
|
|
300
|
+
fmt.Printf("Error: %v", err)
|
|
301
|
+
os.Exit(1)
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
## Progress Indicators
|
|
307
|
+
|
|
308
|
+
```go
|
|
309
|
+
package main
|
|
310
|
+
|
|
311
|
+
import (
|
|
312
|
+
"fmt"
|
|
313
|
+
"time"
|
|
314
|
+
|
|
315
|
+
"github.com/schollz/progressbar/v3"
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
func main() {
|
|
319
|
+
// Simple progress bar
|
|
320
|
+
bar := progressbar.Default(100, "Downloading")
|
|
321
|
+
for i := 0; i < 100; i++ {
|
|
322
|
+
bar.Add(1)
|
|
323
|
+
time.Sleep(40 * time.Millisecond)
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Custom progress bar
|
|
327
|
+
bar = progressbar.NewOptions(100,
|
|
328
|
+
progressbar.OptionEnableColorCodes(true),
|
|
329
|
+
progressbar.OptionShowBytes(true),
|
|
330
|
+
progressbar.OptionSetWidth(15),
|
|
331
|
+
progressbar.OptionSetDescription("[cyan][1/3][reset] Downloading..."),
|
|
332
|
+
progressbar.OptionSetTheme(progressbar.Theme{
|
|
333
|
+
Saucer: "[green]=[reset]",
|
|
334
|
+
SaucerHead: "[green]>[reset]",
|
|
335
|
+
SaucerPadding: " ",
|
|
336
|
+
BarStart: "[",
|
|
337
|
+
BarEnd: "]",
|
|
338
|
+
}),
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
for i := 0; i < 100; i++ {
|
|
342
|
+
bar.Add(1)
|
|
343
|
+
time.Sleep(40 * time.Millisecond)
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
## Spinner
|
|
349
|
+
|
|
350
|
+
```go
|
|
351
|
+
package main
|
|
352
|
+
|
|
353
|
+
import (
|
|
354
|
+
"fmt"
|
|
355
|
+
"time"
|
|
356
|
+
|
|
357
|
+
"github.com/briandowns/spinner"
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
func main() {
|
|
361
|
+
s := spinner.New(spinner.CharSets[11], 100*time.Millisecond)
|
|
362
|
+
s.Suffix = " Installing dependencies..."
|
|
363
|
+
s.Start()
|
|
364
|
+
|
|
365
|
+
time.Sleep(4 * time.Second)
|
|
366
|
+
|
|
367
|
+
s.UpdateCharSet(spinner.CharSets[9])
|
|
368
|
+
s.Suffix = " Processing..."
|
|
369
|
+
time.Sleep(2 * time.Second)
|
|
370
|
+
|
|
371
|
+
s.Stop()
|
|
372
|
+
fmt.Println("✓ Done!")
|
|
373
|
+
}
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
## Colored Output
|
|
377
|
+
|
|
378
|
+
```go
|
|
379
|
+
package main
|
|
380
|
+
|
|
381
|
+
import (
|
|
382
|
+
"github.com/fatih/color"
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
func main() {
|
|
386
|
+
// Basic colors
|
|
387
|
+
color.Blue("Info: Starting deployment...")
|
|
388
|
+
color.Green("Success: Deployment complete!")
|
|
389
|
+
color.Yellow("Warning: Deprecated flag used")
|
|
390
|
+
color.Red("Error: Deployment failed")
|
|
391
|
+
|
|
392
|
+
// Custom styles
|
|
393
|
+
success := color.New(color.FgGreen, color.Bold).PrintlnFunc()
|
|
394
|
+
error := color.New(color.FgRed, color.Bold).PrintlnFunc()
|
|
395
|
+
|
|
396
|
+
success("✓ Build successful")
|
|
397
|
+
error("✗ Build failed")
|
|
398
|
+
|
|
399
|
+
// Printf-style
|
|
400
|
+
color.Cyan("Processing %d files...\n", 42)
|
|
401
|
+
|
|
402
|
+
// Disable colors for CI
|
|
403
|
+
if os.Getenv("CI") != "" {
|
|
404
|
+
color.NoColor = true
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
## Error Handling
|
|
410
|
+
|
|
411
|
+
```go
|
|
412
|
+
package main
|
|
413
|
+
|
|
414
|
+
import (
|
|
415
|
+
"errors"
|
|
416
|
+
"fmt"
|
|
417
|
+
"os"
|
|
418
|
+
"syscall"
|
|
419
|
+
|
|
420
|
+
"github.com/spf13/cobra"
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
var deployCmd = &cobra.Command{
|
|
424
|
+
Use: "deploy",
|
|
425
|
+
Short: "Deploy application",
|
|
426
|
+
RunE: func(cmd *cobra.Command, args []string) error {
|
|
427
|
+
if err := deploy(); err != nil {
|
|
428
|
+
return handleError(err)
|
|
429
|
+
}
|
|
430
|
+
return nil
|
|
431
|
+
},
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
func handleError(err error) error {
|
|
435
|
+
var exitCode int
|
|
436
|
+
|
|
437
|
+
switch {
|
|
438
|
+
case errors.Is(err, os.ErrPermission):
|
|
439
|
+
fmt.Fprintln(os.Stderr, "Permission denied")
|
|
440
|
+
fmt.Fprintln(os.Stderr, "Try running with sudo or check file permissions")
|
|
441
|
+
exitCode = 77
|
|
442
|
+
|
|
443
|
+
case errors.Is(err, os.ErrNotExist):
|
|
444
|
+
fmt.Fprintf(os.Stderr, "File not found: %v\n", err)
|
|
445
|
+
exitCode = 127
|
|
446
|
+
|
|
447
|
+
default:
|
|
448
|
+
fmt.Fprintf(os.Stderr, "Deployment failed: %v\n", err)
|
|
449
|
+
if os.Getenv("DEBUG") != "" {
|
|
450
|
+
fmt.Fprintf(os.Stderr, "%+v\n", err)
|
|
451
|
+
}
|
|
452
|
+
exitCode = 1
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
os.Exit(exitCode)
|
|
456
|
+
return nil
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Handle SIGINT (Ctrl+C)
|
|
460
|
+
func main() {
|
|
461
|
+
// Setup signal handling
|
|
462
|
+
c := make(chan os.Signal, 1)
|
|
463
|
+
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
|
|
464
|
+
|
|
465
|
+
go func() {
|
|
466
|
+
<-c
|
|
467
|
+
fmt.Println("\nOperation cancelled")
|
|
468
|
+
os.Exit(130)
|
|
469
|
+
}()
|
|
470
|
+
|
|
471
|
+
cmd.Execute()
|
|
472
|
+
}
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
## Testing
|
|
476
|
+
|
|
477
|
+
```go
|
|
478
|
+
package cmd
|
|
479
|
+
|
|
480
|
+
import (
|
|
481
|
+
"bytes"
|
|
482
|
+
"testing"
|
|
483
|
+
|
|
484
|
+
"github.com/spf13/cobra"
|
|
485
|
+
"github.com/stretchr/testify/assert"
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
func TestInitCommand(t *testing.T) {
|
|
489
|
+
cmd := &cobra.Command{Use: "test"}
|
|
490
|
+
cmd.AddCommand(initCmd)
|
|
491
|
+
|
|
492
|
+
b := bytes.NewBufferString("")
|
|
493
|
+
cmd.SetOut(b)
|
|
494
|
+
cmd.SetArgs([]string{"init", "my-project"})
|
|
495
|
+
|
|
496
|
+
err := cmd.Execute()
|
|
497
|
+
assert.NoError(t, err)
|
|
498
|
+
assert.Contains(t, b.String(), "Creating my-project")
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
func TestInitWithTemplate(t *testing.T) {
|
|
502
|
+
cmd := &cobra.Command{Use: "test"}
|
|
503
|
+
cmd.AddCommand(initCmd)
|
|
504
|
+
|
|
505
|
+
b := bytes.NewBufferString("")
|
|
506
|
+
cmd.SetOut(b)
|
|
507
|
+
cmd.SetArgs([]string{"init", "my-project", "--template", "react"})
|
|
508
|
+
|
|
509
|
+
err := cmd.Execute()
|
|
510
|
+
assert.NoError(t, err)
|
|
511
|
+
assert.Contains(t, b.String(), "react")
|
|
512
|
+
}
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
## Build & Distribution
|
|
516
|
+
|
|
517
|
+
```makefile
|
|
518
|
+
# Makefile
|
|
519
|
+
VERSION := $(shell git describe --tags --always --dirty)
|
|
520
|
+
LDFLAGS := -ldflags "-X main.version=$(VERSION)"
|
|
521
|
+
|
|
522
|
+
.PHONY: build
|
|
523
|
+
build:
|
|
524
|
+
go build $(LDFLAGS) -o bin/mycli main.go
|
|
525
|
+
|
|
526
|
+
.PHONY: install
|
|
527
|
+
install:
|
|
528
|
+
go install $(LDFLAGS)
|
|
529
|
+
|
|
530
|
+
.PHONY: test
|
|
531
|
+
test:
|
|
532
|
+
go test -v ./...
|
|
533
|
+
|
|
534
|
+
.PHONY: release
|
|
535
|
+
release:
|
|
536
|
+
GOOS=linux GOARCH=amd64 go build $(LDFLAGS) -o bin/mycli-linux-amd64
|
|
537
|
+
GOOS=darwin GOARCH=amd64 go build $(LDFLAGS) -o bin/mycli-darwin-amd64
|
|
538
|
+
GOOS=darwin GOARCH=arm64 go build $(LDFLAGS) -o bin/mycli-darwin-arm64
|
|
539
|
+
GOOS=windows GOARCH=amd64 go build $(LDFLAGS) -o bin/mycli-windows-amd64.exe
|
|
540
|
+
```
|