• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

happy-sdk / happy / 15987602525

01 Jul 2025 01:35AM UTC coverage: 46.009% (-0.7%) from 46.673%
15987602525

push

github

mkungla
feat: add prj info, test and lint cmds

Signed-off-by: Marko Kungla <marko.kungla@gmail.com>

0 of 130 new or added lines in 3 files covered. (0.0%)

433 existing lines in 8 files now uncovered.

7954 of 17288 relevant lines covered (46.01%)

96150.19 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

26.74
/sdk/cli/command/command.go
1
// SPDX-License-Identifier: Apache-2.0
2
//
3
// Copyright © 2024 The Happy Authors
4

5
package command
6

7
import (
8
        "errors"
9
        "fmt"
10
        "log/slog"
11
        "runtime/debug"
12
        "strings"
13
        "sync"
14

15
        "github.com/happy-sdk/happy/pkg/logging"
16
        "github.com/happy-sdk/happy/pkg/settings"
17
        "github.com/happy-sdk/happy/pkg/vars/varflag"
18
        "github.com/happy-sdk/happy/sdk/action"
19
        "github.com/happy-sdk/happy/sdk/internal"
20
)
21

22
var (
23
        Error                = errors.New("command")
24
        ErrFlags             = fmt.Errorf("%w flags error", Error)
25
        ErrHasNoParent       = fmt.Errorf("%w has no parent command", Error)
26
        ErrCommandNotAllowed = fmt.Errorf("%w not allowed", Error)
27
)
28

29
type Config struct {
30
        Usage            settings.String `key:"usage" mutation:"once"`
31
        HideDefaultUsage settings.Bool   `key:"hide_default_usage" default:"false"`
32
        Category         settings.String `key:"category"`
33
        Description      settings.String `key:"description"`
34
        // MinArgs Minimum argument count for command
35
        MinArgs    settings.Uint `key:"min_args" default:"0" mutation:"once"`
36
        MinArgsErr settings.String
37
        // MaxArgs Maximum argument count for command
38
        MaxArgs    settings.Uint `key:"max_args" default:"0" mutation:"once"`
39
        MaxArgsErr settings.String
40
        // SharedBeforeAction share Before action for all its subcommands
41
        SharedBeforeAction settings.Bool `key:"shared_before_action" default:"false"`
42
        // Indicates that the command should be executed immediately, without waiting for the full runtime setup.
43
        Immediate settings.Bool `key:"immediate" default:"false"`
44
        // SkipSharedBefore indicates that the BeforeAlways any shared before actions provided
45
        // by parent commands should be skipped.
46
        SkipSharedBefore settings.Bool `key:"skip_shared_before" default:"false"`
47
        // Disabled indicates that the command should be disabled in the command list.
48
        Disabled settings.Bool `key:"disabled" default:"false" mutation:"once"`
49
        // FailDisabled indicates that the command should fail when disabled.
50
        // If Disable action is set, the command will fail with an error message returned by action.
51
        // If Disable action is not set, but Disabled is true, the command will fail with an error message ErrCommandNotAllowed.
52
        FailDisabled settings.Bool `key:"fail_disabled" default:"false"`
53
}
54

55
func (s Config) Blueprint() (*settings.Blueprint, error) {
12✔
56

12✔
57
        b, err := settings.New(s)
12✔
58
        if err != nil {
12✔
59
                return nil, err
×
UNCOV
60
        }
×
61

62
        return b, nil
12✔
63
}
64

65
type originalSource struct {
66
        Before       string
67
        Do           string
68
        AfterSuccess string
69
        AfterFailure string
70
        AfterAlways  string
71
}
72

73
type Command struct {
74
        mu    sync.Mutex
75
        name  string
76
        cnf   *settings.Profile
77
        info  []string
78
        usage []string
79

80
        flags       varflag.Flags
81
        parent      *Command
82
        subCommands map[string]*Command
83

84
        beforeAction       action.WithArgs
85
        disableAction      action.Action
86
        doAction           action.WithArgs
87
        afterSuccessAction action.Action
88
        afterFailureAction action.WithPrevErr
89
        afterAlwaysAction  action.WithPrevErr
90

91
        isWrapperCommand bool
92

93
        parents []string
94

95
        catdesc map[string]string
96

97
        err error
98

99
        cnflog *logging.QueueLogger
100

101
        extraUsage []string
102

103
        sources originalSource
104
}
105

106
func New(name string, cnf Config) *Command {
12✔
107
        c := &Command{
12✔
108
                name:    name,
12✔
109
                catdesc: make(map[string]string),
12✔
110
                cnflog:  logging.NewQueueLogger(),
12✔
111
        }
12✔
112

12✔
113
        if err := c.configure(&cnf); err != nil {
12✔
114
                c.error(fmt.Errorf("%w: %s", Error, err.Error()))
×
115
                return c.toInvalid()
×
116
        }
×
117

118
        maxArgs := c.cnf.Get("max_args").Value().Int()
12✔
119

12✔
120
        flags, err := varflag.NewFlagSet(c.name, maxArgs)
12✔
121
        if err != nil {
12✔
UNCOV
122
                if errors.Is(err, varflag.ErrInvalidFlagSetName) {
×
UNCOV
123
                        c.error(fmt.Errorf("%w: invalid command name %q", Error, c.name))
×
124
                } else {
×
125
                        c.error(fmt.Errorf("%w: failed to create FlagSet: %v", Error, err))
×
126
                }
×
127
                return c.toInvalid()
×
128
        }
129

130
        c.flags = flags
12✔
131

12✔
132
        return c
12✔
133
}
134

135
func (c *Command) DescribeCategory(cat, desc string) *Command {
×
136
        if !c.tryLock("DescribeCategory") {
×
137
                return c
×
UNCOV
138
        }
×
UNCOV
139
        defer c.mu.Unlock()
×
140
        c.catdesc[strings.ToLower(cat)] = desc
×
141
        return c
×
142
}
143

144
func (c *Command) WithFlags(ffns ...varflag.FlagCreateFunc) *Command {
×
145
        for _, fn := range ffns {
×
146
                c.withFlag(fn)
×
147
        }
×
UNCOV
148
        return c
×
149
}
150

UNCOV
151
func (c *Command) AddInfo(paragraph string) *Command {
×
152
        if !c.tryLock("AddInfo") {
×
153
                return c
×
UNCOV
154
        }
×
UNCOV
155
        defer c.mu.Unlock()
×
UNCOV
156

×
157
        c.info = append(c.info, paragraph)
×
158
        return c
×
159
}
160

161
func (c *Command) WithSubCommands(cmds ...*Command) *Command {
9✔
162
        for _, cmd := range cmds {
9✔
UNCOV
163
                c.withSubCommand(cmd)
×
UNCOV
164
        }
×
165
        return c
9✔
166
}
167

168
func (c *Command) AddUsage(usage string) {
×
169
        if !c.tryLock("Usage") {
×
170
                return
×
171
        }
×
172
        defer c.mu.Unlock()
×
173
        c.extraUsage = append(c.extraUsage, usage)
×
174
}
175

176
func (c *Command) Disable(a action.Action) *Command {
×
UNCOV
177
        if !c.tryLock("Hide") {
×
UNCOV
178
                return c
×
UNCOV
179
        }
×
180
        defer c.mu.Unlock()
×
181

×
UNCOV
182
        if c.disableAction != nil {
×
UNCOV
183
                c.error(fmt.Errorf("%w: attempt to override Hide action for %s", Error, c.name))
×
UNCOV
184
                return c
×
185
        }
×
186
        c.disableAction = a
×
187
        return c
×
188
}
189
func (c *Command) Before(a action.WithArgs) *Command {
3✔
190
        if !c.tryLock("Before") {
3✔
UNCOV
191
                return c
×
UNCOV
192
        }
×
193
        defer c.mu.Unlock()
3✔
194

3✔
195
        if c.beforeAction != nil {
3✔
UNCOV
196
                c.error(fmt.Errorf("%w: attempt to override Before action for %s", Error, c.name))
×
UNCOV
197
                return c
×
198
        }
×
199
        c.beforeAction = a
3✔
200
        return c
3✔
201
}
202

203
func (c *Command) Do(action action.WithArgs) *Command {
9✔
204
        if !c.tryLock("Do") {
9✔
UNCOV
205
                return c
×
UNCOV
206
        }
×
207
        defer c.mu.Unlock()
9✔
208
        if c.doAction != nil {
9✔
UNCOV
209
                c.error(fmt.Errorf("%w: attempt to override Do action for %s", Error, c.name))
×
UNCOV
210
                return c
×
211
        }
×
212
        src, _ := internal.RuntimeCallerStr(2)
9✔
213
        c.sources.Do = src
9✔
214
        c.doAction = action
9✔
215
        return c
9✔
216
}
217

218
func (c *Command) AfterSuccess(a action.Action) *Command {
3✔
219
        if !c.tryLock("AfterSuccess") {
3✔
220
                return c
×
221
        }
×
222
        defer c.mu.Unlock()
3✔
223
        if c.afterSuccessAction != nil {
3✔
224
                c.error(fmt.Errorf("%w: attempt to override AfterSuccess action for %s", Error, c.name))
×
225
                return c
×
226
        }
×
227
        c.afterSuccessAction = a
3✔
228
        return c
3✔
229
}
230

UNCOV
231
func (c *Command) AfterFailure(a action.WithPrevErr) *Command {
×
UNCOV
232
        if !c.tryLock("AfterFailure") {
×
233
                return c
×
234
        }
×
UNCOV
235
        defer c.mu.Unlock()
×
UNCOV
236
        if c.afterFailureAction != nil {
×
237
                c.error(fmt.Errorf("%w: attempt to override AfterFailure action for %s", Error, c.name))
×
238
                return c
×
239
        }
×
UNCOV
240
        c.afterFailureAction = a
×
UNCOV
241
        return c
×
242
}
243

244
func (c *Command) AfterAlways(a action.WithPrevErr) *Command {
3✔
245
        if !c.tryLock("AfterAlways") {
3✔
246
                return c
×
247
        }
×
248
        defer c.mu.Unlock()
3✔
249
        if c.afterAlwaysAction != nil {
3✔
250
                c.error(fmt.Errorf("%w: attempt to override AfterAlways action for %s", Error, c.name))
×
251
                return c
×
UNCOV
252
        }
×
253
        c.afterAlwaysAction = a
3✔
254
        return c
3✔
255
}
256

257
func (c *Command) withSubCommand(cmd *Command) *Command {
×
258
        if !c.tryLock("WithSubCommand") {
×
259
                return c
×
260
        }
×
261
        defer c.mu.Unlock()
×
262
        if cmd == nil {
×
263
                return c
×
264
        }
×
265

266
        if cmd.err != nil {
×
267
                c.error(cmd.err)
×
268
                return c
×
269
        }
×
270
        if c.subCommands == nil {
×
271
                c.subCommands = make(map[string]*Command)
×
272
        }
×
UNCOV
273
        if err := c.flags.AddSet(cmd.flags); err != nil {
×
UNCOV
274
                c.error(fmt.Errorf(
×
275
                        "%w: failed to attach subcommand %s flags to %s",
×
276
                        Error,
×
277
                        cmd.name,
×
278
                        c.name,
×
279
                ))
×
280
                return c
×
281
        }
×
282
        cmd.parent = c
×
283

×
284
        c.subCommands[cmd.name] = cmd
×
UNCOV
285
        return c
×
286
}
287

UNCOV
288
func (c *Command) Err() error {
×
289
        c.mu.Lock()
×
290
        defer c.mu.Unlock()
×
291
        if c.err != nil {
×
292
                return c.err
×
293
        }
×
294
        for _, scmd := range c.subCommands {
×
295
                if err := scmd.Err(); err != nil {
×
296
                        return err
×
297
                }
×
298
        }
299
        return nil
×
300
}
301

302
func (c *Command) withFlag(ffn varflag.FlagCreateFunc) *Command {
×
303
        if !c.tryLock("WithFlag") {
×
304
                return c
×
UNCOV
305
        }
×
UNCOV
306
        defer c.mu.Unlock()
×
UNCOV
307

×
UNCOV
308
        f, cerr := ffn()
×
309
        if cerr != nil {
×
310
                c.error(fmt.Errorf("%w: %s", ErrFlags, cerr.Error()))
×
311
                return c
×
312
        }
×
313

314
        if err := c.flags.Add(f); err != nil {
×
315
                c.error(fmt.Errorf("%w: %s", ErrFlags, err.Error()))
×
UNCOV
316
        }
×
UNCOV
317
        return c
×
318
}
319

320
func (c *Command) tryLock(method string) bool {
36✔
321
        if !c.mu.TryLock() {
36✔
UNCOV
322
                c.cnflog.BUG(
×
323
                        "command configuration failed",
×
324
                        slog.String("command", c.name),
×
325
                        slog.String("method", method),
×
UNCOV
326
                )
×
UNCOV
327
                return false
×
UNCOV
328
        }
×
329
        return true
36✔
330
}
331

332
// configure is called in New so defering outside of lock is ok.
333
func (c *Command) configure(s *Config) error {
12✔
334
        defer func() {
24✔
335
                if c.cnf == nil {
12✔
UNCOV
336
                        // set empty profile when settings have failed to load
×
337
                        c.cnf = &settings.Profile{}
×
338
                }
×
339
        }()
340

341
        c.mu.Lock()
12✔
342

12✔
343
        bp, err := s.Blueprint()
12✔
344
        if err != nil {
12✔
345
                return err
×
346
        }
×
347

348
        schema, err := bp.Schema("cmd", "v1")
12✔
349
        if err != nil {
12✔
UNCOV
350
                return err
×
UNCOV
351
        }
×
352
        c.cnf, err = schema.Profile("cli", nil)
12✔
353
        if err != nil {
12✔
UNCOV
354
                return err
×
UNCOV
355
        }
×
356

357
        if minargs := c.cnf.Get("min_args").Value().Int(); minargs > c.cnf.Get("max_args").Value().Int() {
12✔
UNCOV
358
                if err := c.cnf.Set("max_args", minargs); err != nil {
×
UNCOV
359
                        return err
×
360
                }
×
361
        }
362

363
        // we dont defer unlock here because we want to keep it locked for tryLock on error.
364
        c.mu.Unlock()
12✔
365
        return nil
12✔
366
}
367

368
// Verify veifies command,  flags and the sub commands
369
//   - verify that commands are valid and have atleast Do function
370
//   - verify that subcommand do not shadow flags of any parent command
371
func (c *Command) verify() error {
9✔
372
        if !c.tryLock("verify") {
9✔
373
                return fmt.Errorf("%w: failed to obtain lock to verify command (%s)", Error, c.name)
×
UNCOV
374
        }
×
375
        defer c.mu.Unlock()
9✔
376

9✔
377
        if c.err != nil {
9✔
UNCOV
378
                return c.err
×
UNCOV
379
        }
×
380

381
        var usage []string
9✔
382
        usage = append(usage, c.parents...)
9✔
383
        usage = append(usage, c.name)
9✔
384
        if c.flags.Len() > 0 {
9✔
385
                usage = append(usage, "[flags]")
×
386
        }
×
387
        if c.subCommands != nil {
9✔
388
                usage = append(usage, "[subcommand]")
×
389
        }
×
390
        c.usage = append(c.usage, strings.Join(usage, " "))
9✔
391

9✔
392
        if c.flags.AcceptsArgs() {
9✔
UNCOV
393
                var withargs []string
×
394
                withargs = append(withargs, c.parents...)
×
395
                withargs = append(withargs, c.name)
×
396
                withargs = append(withargs, "[args...]")
×
397
                withargs = append(withargs, fmt.Sprintf(
×
398
                        " // min %d max %d",
×
399
                        c.cnf.Get("min_args").Value().Int(),
×
400
                        c.cnf.Get("max_args").Value().Int(),
×
401
                ))
×
402
                c.usage = append(c.usage, strings.Join(withargs, " "))
×
UNCOV
403
        }
×
404

405
        defineUsage := c.cnf.Get("usage").String()
9✔
406
        if defineUsage != "" {
9✔
407
                var usage []string
×
408
                usage = append(usage, c.parents...)
×
409
                usage = append(usage, c.name)
×
410
                usage = append(usage, defineUsage)
×
411
                if c.cnf.Get("hide_default_usage").Value().Bool() {
×
412
                        c.usage = []string{strings.Join(usage, " ")}
×
UNCOV
413
                } else {
×
UNCOV
414
                        c.usage = append(c.usage, strings.Join(usage, " "))
×
UNCOV
415
                }
×
416
        }
417

418
        if len(c.extraUsage) > 0 {
9✔
UNCOV
419
                for _, u := range c.extraUsage {
×
420
                        var usage []string
×
421
                        usage = append(usage, c.parents...)
×
422
                        usage = append(usage, c.name)
×
UNCOV
423
                        usage = append(usage, u)
×
424
                        c.usage = append(c.usage, strings.Join(usage, " "))
×
425
                }
×
426
        }
427

428
        if c.err != nil {
9✔
UNCOV
429
                return c.err
×
UNCOV
430
        }
×
431

432
        if c.doAction == nil {
9✔
433
                if !c.isWrapperCommand {
×
434
                        c.isWrapperCommand = len(c.subCommands) > 0
×
435
                }
×
436

437
                if c.subCommands != nil {
×
438
                        goto SubCommands
×
439
                } else {
×
440
                        return fmt.Errorf("%w: command (%s) must have Do action or atleeast one subcommand", Error, c.name)
×
441
                }
×
442
        }
443

444
SubCommands:
445
        if c.subCommands != nil {
9✔
UNCOV
446
                for _, cmd := range c.subCommands {
×
UNCOV
447
                        // Add subcommand loogs to parent command log queue
×
UNCOV
448
                        if err := c.cnflog.ConsumeQueue(cmd.cnflog); err != nil {
×
UNCOV
449
                                return err
×
UNCOV
450
                        }
×
UNCOV
451
                        cmd.parents = append(c.parents, c.name)
×
UNCOV
452
                        if err := cmd.verify(); err != nil {
×
UNCOV
453
                                return err
×
454
                        }
×
455
                }
456
        }
457
        return nil
9✔
458
}
459

460
func (c *Command) getActiveCommand() (*Command, error) {
9✔
461
        subtree := c.flags.GetActiveSets()
9✔
462

9✔
463
        // Skip self
9✔
464
        for _, subset := range subtree {
18✔
465
                cmd, exists := c.getSubCommand(subset.Name())
9✔
466
                if exists {
9✔
UNCOV
467
                        return cmd.getActiveCommand()
×
468
                }
×
469
        }
470

471
        args := c.flags.Args()
9✔
472
        if !c.flags.AcceptsArgs() && len(args) > 0 {
9✔
UNCOV
473
                return nil, fmt.Errorf("%w: unknown subcommand: %s for %s", Error, args[0].String(), c.name)
×
UNCOV
474
        }
×
475

476
        return c, nil
9✔
477
}
478

479
func (c *Command) getSubCommand(name string) (cmd *Command, exists bool) {
9✔
480
        if cmd, exists := c.subCommands[name]; exists {
9✔
481
                return cmd, exists
×
482
        }
×
483
        return
9✔
484
}
485

486
func (c *Command) getGlobalFlags() varflag.Flags {
9✔
487
        if c.parent == nil {
18✔
488
                return c.flags
9✔
489
        }
9✔
490
        return c.parent.getGlobalFlags()
×
491
}
492

493
func (c *Command) getSharedFlags() (varflag.Flags, error) {
×
494
        // ignore global flags
×
495
        if c.parent == nil || c.parent.parent == nil {
×
496
                flags, err := varflag.NewFlagSet("x-"+c.name+"-noparent", 0)
×
497
                if err != nil {
×
UNCOV
498
                        return nil, err
×
499
                }
×
500
                return flags, ErrHasNoParent
×
501
        }
502

503
        flags := c.parent.getFlags()
×
UNCOV
504
        if flags == nil {
×
UNCOV
505
                flags, _ = varflag.NewFlagSet(c.parent.name, 0)
×
UNCOV
506
        }
×
507
        parentFlags, err := c.parent.getSharedFlags()
×
UNCOV
508
        if err != nil && !errors.Is(err, ErrHasNoParent) {
×
UNCOV
509
                return nil, err
×
510
        }
×
511

512
        if parentFlags != nil {
×
513
                for _, flag := range parentFlags.Flags() {
×
514
                        if err := flags.Add(flag); err != nil {
×
515
                                return nil, err
×
UNCOV
516
                        }
×
517
                }
518
        }
519

520
        return flags, nil
×
521
}
522

523
func (c *Command) getFlags() varflag.Flags {
×
524
        c.mu.Lock()
×
525
        defer c.mu.Unlock()
×
526

×
UNCOV
527
        return c.flags
×
UNCOV
528
}
×
529

530
func (c *Command) toInvalid() *Command {
×
531
        c.mu.Lock()
×
UNCOV
532
        defer c.mu.Unlock()
×
UNCOV
533

×
534
        defer func() {
×
535
                stackTrace := debug.Stack()
×
536
                fmt.Println("HAPPY COMMAND")
×
537
                fmt.Println("err: ", c.err.Error())
×
538
                fmt.Println(string(stackTrace))
×
539
        }()
×
540

541
        // Ensure that the error field is set.
542
        if c.err == nil {
×
543
                c.error(fmt.Errorf("%w: command marked invalid", Error))
×
544
        }
×
545

546
        // Clear all actions to avoid any execution.
547
        c.beforeAction = nil
×
548
        c.doAction = nil
×
549
        c.afterSuccessAction = nil
×
550
        c.afterFailureAction = nil
×
551
        c.afterAlwaysAction = nil
×
552

×
553
        // Remove any subcommands.
×
554
        for _, subCommand := range c.subCommands {
×
555
                subCommand.toInvalid()
×
UNCOV
556
        }
×
UNCOV
557
        c.subCommands = nil
×
558

×
UNCOV
559
        // If flags is still nil, assign a dummy flag set to avoid nil dereference later.
×
UNCOV
560
        if c.flags == nil {
×
561
                // Use a dummy flag set. We assume that this call will succeed for a command marked as invalid.
×
562
                if dummy, err := varflag.NewFlagSet("invalid", 0); err == nil {
×
563
                        c.flags = dummy
×
564
                } else {
×
565
                        // If even this fails, log the error.
×
UNCOV
566
                        c.error(fmt.Errorf("failed to create dummy flag set for invalid command %q",
×
UNCOV
567
                                err.Error()))
×
UNCOV
568
                }
×
569
        }
570

UNCOV
571
        return c
×
572
}
573

UNCOV
574
func (c *Command) error(err error) {
×
UNCOV
575
        if c.cnflog != nil {
×
UNCOV
576
                c.cnflog.Error(err.Error())
×
UNCOV
577
        }
×
UNCOV
578
        c.err = err
×
579
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc