Skip to content

Conversation

@dogancanbakir
Copy link
Member

@dogancanbakir dogancanbakir commented Jan 6, 2026

Summary

  • Add --tags/-t flag to start command for starting multiple templates by tag
  • Add --tags/-t flag to stop command for stopping multiple templates by tag
  • Add GetByTags() helper function for retrieving templates matching given tags

Usage

# Start all templates with sqli tag
vt start --tags sqli

# Start all templates with sqli OR xss tags
vt start --tags sqli,xss

# Stop all templates with web tag
vt stop -t web

Features

  • OR logic - Multiple comma-separated tags match templates with ANY of the tags
  • Case-insensitive - --tags SQLI matches templates with "sqli" tag
  • Substring matching - --tags owa matches templates with "owasp" tag
  • Mutually exclusive - --id and --tags cannot be used together
  • Error resilience - Continues on failure, reports failed templates at end

Test plan

  • All existing tests pass
  • New tests for GetByTags() function
  • New tests for templateMatchesTags() helper
  • Linter passes

Closes #101

Summary by CodeRabbit

  • New Features

    • Start and stop templates by comma-separated tags (new --tags / -t option); still possible by --id. --id and --tags are mutually exclusive and one is required.
  • Improvements

    • Clearer per-template progress messages, post-install instructions shown when present, and aggregated summaries of failures/successes for tag-based operations.
  • Tests

    • Added tests covering tag-matching behavior (case-insensitive and substring matches).

✏️ Tip: You can customize this high-level summary in your review settings.

Add the ability to start and stop multiple templates by tag filter.

Changes:
- Add GetByTags() function to retrieve templates matching given tags
- Add --tags/-t flag to start command for starting multiple templates
- Add --tags/-t flag to stop command for stopping multiple templates
- Tags support case-insensitive and substring matching
- --id and --tags are mutually exclusive
- Continue on failure and report failed templates at end

Usage:
  vt start --tags sqli,xss    # Start all templates with sqli OR xss tags
  vt stop --tags web          # Stop all templates with web tag

Closes #101
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 6, 2026

Warning

Rate limit exceeded

@dogancanbakir has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 20 minutes and 10 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 3dd738e and 7eb2b26.

📒 Files selected for processing (2)
  • pkg/template/template.go
  • pkg/template/template_test.go

Walkthrough

Adds tag-based selection to the start and stop CLI commands and a new template lookup by tags in the template package, with tests for tag matching and adjustments to per-template start/stop control flow and logging.

Changes

Cohort / File(s) Summary
CLI: start/stop commands
internal/cli/start.go, internal/cli/stop.go
Add --tags (-t) flag; validate mutual exclusivity with --id; branch to tag-based flows that fetch templates by tags, iterate starts/stops per template, aggregate failures, and log per-template post-install/instructions. Add startTemplate helper in start.go.
Template lookup
pkg/template/template.go
Add GetByTags(templates map[string]Template, tags []string) ([]*Template, error) and helper templateMatchesTags for case-insensitive and substring tag matching; validate non-empty tags and return error when no matches.
Tests
pkg/template/template_test.go
Add TestGetByTags and TestTemplateMatchesTags covering single/multiple tags, case-insensitivity, substring matches, whitespace handling, and no-match/error cases.

Sequence Diagram(s)

sequenceDiagram
    participant CLI as "CLI (start/stop)"
    participant TPL as "Template pkg (GetByTags / GetByID)"
    participant Provider as "Provider (Start/Stop)"
    CLI->>TPL: parse flags (--id or --tags)
    alt id provided
        CLI->>TPL: GetByID(id)
        TPL-->>CLI: template
        CLI->>Provider: lookup provider for template
        Provider-->>CLI: provider instance
        CLI->>Provider: Start/Stop(template)
        Provider-->>CLI: result
    else tags provided
        CLI->>TPL: GetByTags(tags)
        TPL-->>CLI: []*templates
        loop for each template
            CLI->>Provider: lookup provider for template
            Provider-->>CLI: provider instance
            CLI->>Provider: Start/Stop(template)
            Provider-->>CLI: result
            CLI->>CLI: log per-template post-install / errors
        end
        CLI-->>CLI: aggregate and log summary of failures
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I hopped through tags and found my crew,
One flag to start many, hooray—woohoo!
Per-template logs and post-install cheer,
I bundle the bunnies and start them near,
A rabbit's small hop brings order here.

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add --tags flag to start and stop commands' accurately summarizes the main changes in the pull request, which add tag-based template filtering to both start and stop commands.
Linked Issues check ✅ Passed The pull request implements all coding requirements from issue #101: adds --tags flag to start/stop commands, implements GetByTags helper with OR logic, case-insensitive and substring matching, mutual exclusion validation, and error resilience across multiple templates.
Out of Scope Changes check ✅ Passed All code changes are directly related to the linked issue #101 objectives: tag-based template operations, flag additions, helper functions, and supporting tests. No unrelated modifications were introduced.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/run-templates-by-tags

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (4)
pkg/template/template.go (1)

223-238: Minor: Redundant exact-match check.

The strings.EqualFold check on line 231 is redundant because the subsequent strings.Contains(strings.ToLower(...), strings.ToLower(...)) on line 232 already covers exact matches. However, the explicit check may improve readability by showing intent, so this is purely optional.

🔎 Optional simplification
 func templateMatchesTags(tmpl *Template, filterTags []string) bool {
 	for _, filterTag := range filterTags {
 		filterTag = strings.TrimSpace(filterTag)
 		if filterTag == "" {
 			continue
 		}
 		for _, templateTag := range tmpl.Info.Tags {
-			if strings.EqualFold(templateTag, filterTag) ||
-				strings.Contains(strings.ToLower(templateTag), strings.ToLower(filterTag)) {
+			if strings.Contains(strings.ToLower(templateTag), strings.ToLower(filterTag)) {
 				return true
 			}
 		}
 	}
 	return false
 }
internal/cli/stop.go (1)

66-83: Consider exiting with non-zero status on partial failures.

When some templates fail to stop, the command logs a warning but exits successfully. For CI/scripting scenarios, a non-zero exit code when len(failed) > 0 would better signal partial failure.

🔎 Proposed change
 			if len(failed) > 0 {
 				log.Warn().Msgf("failed to stop %d templates: %s", len(failed), strings.Join(failed, ", "))
+				log.Fatal().Msgf("completed with %d failures", len(failed))
 			}
 			if stopped > 0 {
 				log.Info().Msgf("successfully stopped %d templates", stopped)
 			}

Alternatively, use os.Exit(1) after logging to allow the success message to print first.

internal/cli/start.go (2)

79-81: Consider exiting with non-zero status on partial failures.

Same concern as in stop.go: when some templates fail to start, the command logs a warning but exits with status 0. This may cause issues in CI/automation scenarios.


99-114: Inconsistent post-install log message format.

In startTemplate (line 107), the log says "Post-installation instructions:" without the template ID, while in the tags-based loop (line 70), it includes the template ID: "Post-installation instructions for %s:". Consider aligning these for consistency.

🔎 Proposed fix
 func (c *CLI) startTemplate(provider interface{ Start(*tmpl.Template) error }, template *tmpl.Template, providerName string) {
 	err := provider.Start(template)
 	if err != nil {
 		log.Fatal().Msgf("%v", err)
 	}

 	if len(template.PostInstall) > 0 {
-		log.Info().Msg("Post-installation instructions:")
+		log.Info().Msgf("Post-installation instructions for %s:", template.ID)
 		for _, instruction := range template.PostInstall {
 			fmt.Printf("  %s\n", instruction)
 		}
 	}

 	log.Info().Msgf("%s template is running on %s", template.ID, providerName)
 }
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between db6ccbd and 3dd2eaf.

📒 Files selected for processing (4)
  • internal/cli/start.go
  • internal/cli/stop.go
  • pkg/template/template.go
  • pkg/template/template_test.go
🧰 Additional context used
🧬 Code graph analysis (2)
internal/cli/start.go (3)
pkg/provider/registry/registry.go (1)
  • GetProvider (19-22)
pkg/template/template.go (4)
  • GetByID (192-198)
  • GetByTags (203-221)
  • Info (27-39)
  • Template (17-24)
internal/cli/cli.go (1)
  • CLI (29-32)
internal/cli/stop.go (2)
pkg/provider/registry/registry.go (1)
  • GetProvider (19-22)
pkg/template/template.go (3)
  • GetByID (192-198)
  • Info (27-39)
  • GetByTags (203-221)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Analyze (go)
🔇 Additional comments (7)
pkg/template/template.go (1)

199-221: LGTM! Well-structured tag-based retrieval.

The implementation correctly:

  • Validates for empty tags input
  • Creates copies to avoid pointer aliasing issues when iterating over map values
  • Returns a clear error when no templates match
pkg/template/template_test.go (2)

34-101: Comprehensive test coverage for GetByTags.

The test cases cover all documented behaviors:

  • Single and multiple tag matching (OR logic)
  • Case-insensitive matching
  • Substring matching
  • Error cases (no matches, empty tags)
  • Whitespace handling

103-128: Good unit tests for templateMatchesTags.

The helper function is well tested with edge cases including empty and whitespace-only filter tags.

internal/cli/stop.go (2)

28-38: LGTM! Clear mutual exclusivity validation.

The validation correctly enforces that exactly one of --id or --tags must be provided.


95-96: LGTM! Flag definition is consistent with start command.

internal/cli/start.go (2)

28-38: LGTM! Consistent with stop command validation.


100-100: The current inline interface is the better approach. The function only uses the Start method, and the existing Provider interface in pkg/provider/provider.go includes four methods (Name(), Start(), Stop(), Status()). Using the full Provider interface would violate the Interface Segregation Principle by coupling the function to methods it doesn't use. The inline interface is cleaner and makes testing/mocking easier.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@internal/cli/stop.go`:
- Around line 58-64: The current splitting of tagsStr into tags can produce
empty or whitespace tokens that will match everything; instead, after splitting
tagsStr, trim each token and filter out empty strings to produce a cleaned tags
slice, then if the cleaned slice is empty fail with an error (or log.Fatal)
rather than calling tmpl.GetByTags; call tmpl.GetByTags(c.app.Templates,
cleanedTags) and update the log message to report cleanedTags (referencing
tagsStr, tags, tmpl.GetByTags and c.app.Templates).
🧹 Nitpick comments (2)
pkg/template/template_test.go (1)

36-103: Strengthen multi-match assertions to avoid false positives.

Right now the tests only check Len for the multi-tag cases, so duplicate or wrong matches could still pass. Consider asserting the exact set of IDs.

♻️ Proposed test tightening
  // Test multiple tags (OR logic)
  matched, err = GetByTags(templates, []string{"sqli", "xss"})
  assert.NoError(t, err)
  assert.Len(t, matched, 2)
+ assert.ElementsMatch(t,
+ 	[]string{"sqli-template", "xss-template"},
+ 	[]string{matched[0].ID, matched[1].ID},
+ )

  // Test tag that matches multiple templates
  matched, err = GetByTags(templates, []string{"web"})
  assert.NoError(t, err)
  assert.Len(t, matched, 3)
+ assert.ElementsMatch(t,
+ 	[]string{"sqli-template", "xss-template", "ssrf-template"},
+ 	[]string{matched[0].ID, matched[1].ID, matched[2].ID},
+ )

  // Test substring matching
  matched, err = GetByTags(templates, []string{"owa"})
  assert.NoError(t, err)
  assert.Len(t, matched, 2) // sqli-template and xss-template have "owasp"
+ assert.ElementsMatch(t,
+ 	[]string{"sqli-template", "xss-template"},
+ 	[]string{matched[0].ID, matched[1].ID},
+ )
internal/cli/start.go (1)

53-82: Consider reusing startTemplate to remove duplicated start/post-install logic.

The tag loop currently re-implements the same start + post-install + logging steps. A small refactor can keep behavior identical while reducing duplication and keeping single/multi flows consistent.

♻️ Suggested refactor (return error from helper)
-				for _, template := range templates {
-					if err := provider.Start(template); err != nil {
-						log.Error().Msgf("failed to start %s: %v", template.ID, err)
-						failed = append(failed, template.ID)
-						continue
-					}
-
-					if len(template.PostInstall) > 0 {
-						log.Info().Msgf("Post-installation instructions for %s:", template.ID)
-						for _, instruction := range template.PostInstall {
-							fmt.Printf("  %s\n", instruction)
-						}
-					}
-
-					log.Info().Msgf("%s template is running on %s", template.ID, providerName)
-				}
+				for _, template := range templates {
+					if err := c.startTemplate(provider, template, providerName); err != nil {
+						log.Error().Msgf("failed to start %s: %v", template.ID, err)
+						failed = append(failed, template.ID)
+						continue
+					}
+				}
-func (c *CLI) startTemplate(provider interface{ Start(*tmpl.Template) error }, template *tmpl.Template, providerName string) {
-	err := provider.Start(template)
-	if err != nil {
-		log.Fatal().Msgf("%v", err)
-	}
-
-	if len(template.PostInstall) > 0 {
-		log.Info().Msg("Post-installation instructions:")
-		for _, instruction := range template.PostInstall {
-			fmt.Printf("  %s\n", instruction)
-		}
-	}
-
-	log.Info().Msgf("%s template is running on %s", template.ID, providerName)
-}
+func (c *CLI) startTemplate(provider interface{ Start(*tmpl.Template) error }, template *tmpl.Template, providerName string) error {
+	if err := provider.Start(template); err != nil {
+		return err
+	}
+
+	if len(template.PostInstall) > 0 {
+		log.Info().Msgf("Post-installation instructions for %s:", template.ID)
+		for _, instruction := range template.PostInstall {
+			fmt.Printf("  %s\n", instruction)
+		}
+	}
+
+	log.Info().Msgf("%s template is running on %s", template.ID, providerName)
+	return nil
+}
-				c.startTemplate(provider, template, providerName)
+				if err := c.startTemplate(provider, template, providerName); err != nil {
+					log.Fatal().Msgf("%v", err)
+				}

Also applies to: 99-113

Comment on lines +58 to +64
tags := strings.Split(tagsStr, ",")
templates, err := tmpl.GetByTags(c.app.Templates, tags)
if err != nil {
log.Fatal().Msgf("%v", err)
}

log.Info().Msgf("found %d templates matching tags: %s", len(templates), tagsStr)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Sanitize tag tokens to avoid accidental “match all”.
strings.Split can yield empty/whitespace tags (e.g., --tags sqli, or --tags sqli, xss). With substring matching, an empty tag could unintentionally match every template. Trim and filter empties before calling GetByTags, and fail if none remain.

🛠️ Proposed fix
-				tags := strings.Split(tagsStr, ",")
-				templates, err := tmpl.GetByTags(c.app.Templates, tags)
+				rawTags := strings.Split(tagsStr, ",")
+				var tags []string
+				for _, t := range rawTags {
+					t = strings.TrimSpace(t)
+					if t != "" {
+						tags = append(tags, t)
+					}
+				}
+				if len(tags) == 0 {
+					log.Fatal().Msg("no valid tags provided")
+				}
+				templates, err := tmpl.GetByTags(c.app.Templates, tags)
 				if err != nil {
 					log.Fatal().Msgf("%v", err)
 				}
 
-				log.Info().Msgf("found %d templates matching tags: %s", len(templates), tagsStr)
+				log.Info().Msgf("found %d templates matching tags: %s", len(templates), strings.Join(tags, ","))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
tags := strings.Split(tagsStr, ",")
templates, err := tmpl.GetByTags(c.app.Templates, tags)
if err != nil {
log.Fatal().Msgf("%v", err)
}
log.Info().Msgf("found %d templates matching tags: %s", len(templates), tagsStr)
rawTags := strings.Split(tagsStr, ",")
var tags []string
for _, t := range rawTags {
t = strings.TrimSpace(t)
if t != "" {
tags = append(tags, t)
}
}
if len(tags) == 0 {
log.Fatal().Msg("no valid tags provided")
}
templates, err := tmpl.GetByTags(c.app.Templates, tags)
if err != nil {
log.Fatal().Msgf("%v", err)
}
log.Info().Msgf("found %d templates matching tags: %s", len(templates), strings.Join(tags, ","))
🤖 Prompt for AI Agents
In `@internal/cli/stop.go` around lines 58 - 64, The current splitting of tagsStr
into tags can produce empty or whitespace tokens that will match everything;
instead, after splitting tagsStr, trim each token and filter out empty strings
to produce a cleaned tags slice, then if the cleaned slice is empty fail with an
error (or log.Fatal) rather than calling tmpl.GetByTags; call
tmpl.GetByTags(c.app.Templates, cleanedTags) and update the log message to
report cleanedTags (referencing tagsStr, tags, tmpl.GetByTags and
c.app.Templates).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

run templates with tags

3 participants