Skip to content

Conversation

@jhaynie
Copy link
Member

@jhaynie jhaynie commented Aug 19, 2025

Summary by CodeRabbit

  • New Features

    • Cluster commands: create, list, remove, status (interactive and non-interactive)
    • Machine commands: create, list, remove, status with cluster-aware flows
    • Provider support: AWS and GCP provisioning options
    • Interactive TUI for guided command execution and editable command flow
  • Improvements

    • Custom, organized CLI help output
    • JSON and table output formats across commands
    • Better validation and clearer CLI error messages (including new invalid/missing-argument codes)

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 19, 2025

Walkthrough

Adds CLI commands for cluster and machine management, a provider plugin framework (AWS/GCP) for cluster/machine provisioning, an execution DSL and TUI for running multi-step specs, API-layer infrastructure CRUD, new error codes, and related utilities and dependencies.

Changes

Cohort / File(s) Summary
CLI: cluster & machine commands
cmd/cluster.go, cmd/machine.go
New hidden Cobra command trees for cluster and machine management (create/list/remove/status), interactive and non-interactive flows, input validation, organization resolution, JSON/table output, and spinner-based progress.
CLI: help rendering
cmd/root.go
Replaces default help with a custom, categorized help renderer and registers it as the global help function.
Infrastructure API layer
internal/infrastructure/infrastructure.go
New domain types and API wrappers: Cluster, Machine, CreateClusterArgs, CreateMachineResponse, Response[T]; CRUD functions for clusters and machines; clustering enablement checks, guards, and rollback behavior for CreateMachine.
Provider framework & utils
internal/infrastructure/cluster.go, internal/infrastructure/spec.go
Adds ClusterSetup interface and registry/dispatcher; utilities for node naming and ECDSA key generation; execution DSL types (ExecutionContext, ExecutionSpec, ExecutionCommand, Validation) with interpolation, validation, and skip logic.
AWS provider implementation
internal/infrastructure/aws.go
AWS-specific ClusterSetup implementation: Setup and CreateMachine using JSON step specifications, AWS CLI detection/auth prompts, command templates for IAM/VPC/EC2/Secrets Manager, and step execution with contextual errors.
GCP provider implementation
internal/infrastructure/gcp.go
GCP-specific ClusterSetup implementation with gcloud detection/auth, JSON step specification for service account, secret/encryption key, and instance creation; CreateMachine is a no-op.
TUI and execution utilities
internal/infrastructure/tui.go, internal/infrastructure/util.go
Terminal UI for user confirmation/editing/clipboard integration and execAction flows; runCommand and command-sequencing helpers with spinner, piped sequences, output capture, and AWS "already exists" handling.
Spec helpers & orchestration
internal/infrastructure/spec.go
Execution framework for running interpolated commands with regex validation and conditional skip semantics (interacts with runCommand/execAction).
Error codes
error_codes.yaml, internal/errsystem/errorcodes.go
Adds CLI-0032 (Invalid argument provided) and CLI-0033 (Missing required argument) with exported error variables.
Dependencies & minor edits
go.mod, internal/util/api.go
Adds direct dependencies (shellwords, reflow, go-quote, clipboard, golang.org/x/term) and updates transitive versions; trivial whitespace change in API util.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant CLI as cmd/{cluster,machine}.go
    participant API as internal/infrastructure/infrastructure.go
    participant Dispatcher as internal/infrastructure/cluster.go
    participant Provider as internal/infrastructure/{aws,gcp}.go
    participant TUI as internal/infrastructure/tui.go

    User->>CLI: invoke create/list/remove/status
    CLI->>API: call Create/List/Get/Delete endpoints
    API-->>CLI: return response
    CLI->>Dispatcher: request provider Setup/CreateMachine (when provisioning)
    Dispatcher->>Provider: delegate to registered provider
    Provider->>TUI: prompt/confirm/run steps
    TUI->>User: show command, allow run/edit/skip
    User->>TUI: respond
    Provider->>Provider: run ExecutionSpec via util.runCommand
    Provider-->>Dispatcher: success/failure
    Dispatcher-->>API: finalize (e.g., return created resource or rollback)
    CLI-->>User: formatted output (JSON/table) or error
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐰 I nudge the keys and tap the shell,

Steps and specs weave their little spell,
AWS, GCP, and prompts so spry,
Machines hum and clusters fly,
Hops of code — deployment, well!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.51% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The PR title "Add infra commands" is directly related to the main changes in the changeset. The pull request introduces two significant new CLI command groups: cluster management (with create, list, remove, and status subcommands) and machine management (with create, list, remove, and status subcommands), along with supporting infrastructure implementations for AWS and GCP providers. The title accurately summarizes that infrastructure-related commands are being added as the primary objective of this changeset. While "infra" is abbreviated, it remains clear and specific enough in a technical context for a teammate scanning the history to understand that this PR adds infrastructure functionality to the CLI.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch infra

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

@jhaynie jhaynie added the do not merge Do not merge yet! label Aug 19, 2025
jhaynie and others added 19 commits August 18, 2025 22:34
* AWS initil implementation

* more work on getting aws working from scratch

* refactor awsSpecification commands

* More work on gettting AWS working

* cluster create and machine create commands fixes

* checks if clustering is enabled for the authenticated user

* remove debug logs

* Small nits from Coder Rabbit

* cleanup
@blacksmith-sh blacksmith-sh bot deleted a comment from jhaynie Oct 15, 2025
@robindiddams robindiddams removed the do not merge Do not merge yet! label Oct 24, 2025
@robindiddams robindiddams marked this pull request as ready for review October 24, 2025 16:55
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: 16

🧹 Nitpick comments (13)
internal/infrastructure/gcp.go (2)

211-213: Hard-coded zone, machine type, and image family limit flexibility.

Lines 211-213 hard-code the zone (us-central1-a), machine type (e2-standard-4), and image family (hadron). These should be parameterized based on the cluster's region and size to match user selections.

Consider:

  • Mapping cluster.Region to appropriate zones
  • Mapping cluster.Type (dev/small/medium/large) to machine types
  • Making image family/project configurable

88-90: Document why CreateMachine is a no-op for GCP.

The CreateMachine method returns nil without implementation. If GCP machine provisioning is handled differently or deferred to a future iteration, add a comment explaining the rationale.

 func (s *gcpSetup) CreateMachine(ctx context.Context, logger logger.Logger, region string, token string, clusterID string) error {
+	// TODO: Implement GCP machine provisioning or document if not applicable
 	return nil
 }
internal/infrastructure/util.go (2)

69-76: AWS-specific logic in generic command utility.

Lines 69-76 contain AWS-specific error handling (EntityAlreadyExists, AlreadyExists) in a generic command execution function. This creates coupling between the utility and AWS provider details.

Consider moving provider-specific error handling to the provider implementations (aws.go, gcp.go) or making error handling pluggable via a callback.


45-83: Consider adding command timeout handling.

The runCommand function uses exec.CommandContext which respects the context, but infrastructure commands can be long-running. If the context doesn't have a timeout, commands could hang indefinitely.

Consider documenting expected timeout behavior or adding explicit timeout recommendations for callers.

cmd/cluster.go (1)

28-54: Hard-coded provider regions should be externalized.

The providerRegions map hard-codes region lists for each provider. As cloud providers add/remove regions frequently, this data will become stale and require code changes to update.

Consider:

  • Loading regions from a configuration file
  • Fetching regions dynamically from provider APIs (with caching)
  • Or documenting the maintenance burden
internal/infrastructure/tui.go (3)

187-191: Duplicate stdin assignment

c.Stdin is set twice.

-		c.Stdin = os.Stdin
-		c.Stdout = os.Stdout
-		c.Stderr = os.Stderr
-		c.Stdin = os.Stdin
+		c.Stdin = os.Stdin
+		c.Stdout = os.Stdout
+		c.Stderr = os.Stderr

143-156: Nil runner would panic; add guard

execAction assumes runner != nil when canExecute. Add a defensive check.

 	case run:
 		var skip bool
 		var err error
 		if skipFunc != nil {
 			skip, err = skipFunc(ctx)
 			if err != nil {
 				return err
 			}
 		}
 		if !skip {
-			if err := runner(ctx, cmd, args); err != nil {
+			if runner == nil {
+				return fmt.Errorf("internal error: runner is nil")
+			}
+			if err := runner(ctx, cmd, args); err != nil {
 				return err
 			}
 		}

162-165: Library helper calling os.Exit reduces composability

Returning an error lets callers decide exit behavior; os.Exit(1) here hard-exits from deep inside.

-	case cancelled:
-		os.Exit(1)
+	case cancelled:
+		return fmt.Errorf("cancelled")
cmd/machine.go (2)

331-340: Interactive region selection cancellation not handled

If user cancels Select, region == "". Bail out cleanly.

 		} else if tui.HasTTY {
 			// Interactive mode - prompt for missing values
 			cluster := promptForClusterSelection(ctx, logger, apiUrl, apikey)
 			provider = cluster.Provider
 			region = promptForRegionSelection(ctx, logger, provider)
 			clusterID = cluster.ID
+			if region == "" {
+				tui.ShowWarning("cancelled")
+				return
+			}
 		} else {

299-305: Command group likely “management”, not “info”

Create is a mutating operation; align with the “Machine Management” group.

-	GroupID: "info",
+	GroupID: "management",
internal/infrastructure/aws.go (1)

118-158: When AWS CLI is present but unauthenticated, region may remain empty

canExecute() sets skipFailedDetection and may not prompt for region; ensure a fallback from env or a prompt even when aws is found but not usable.

 	_, err = exec.LookPath("aws")
 	if err == nil {
 		_, err := runCommand(ctx, logger, "Checking AWS authentication...", "aws", "sts", "get-caller-identity")
 		authenticated := err == nil
 		if authenticated {
 			val, err := runCommand(ctx, logger, "Checking AWS region...", "aws", "configure", "get", "region")
 			if err == nil {
 				canExecuteAWS = true
 				region = strings.TrimSpace(val)
-				if region == "" {
-					region = "us-east-1" // default region
-				}
+				if region == "" {
+					region = "us-east-1"
+				}
 				tui.ShowBanner("AWS Tools Detected", "I'll show you the command to run against the AWS account in region "+region+". You can choose to have me execute it for you, or run it yourself. If you prefer to run it on your own, the command will automatically be copied to your clipboard at each step.", false)
 			}
 		}
-		if !canExecuteAWS && region != "" {
+		if !canExecuteAWS {
+			if region == "" {
+				if v := os.Getenv("AWS_DEFAULT_REGION"); v != "" {
+					region = v
+				} else if v := os.Getenv("AWS_REGION"); v != "" {
+					region = v
+				}
+				if region == "" {
+					region = tui.Input(logger, "Please enter your AWS region:", "us-east-1")
+				}
+			}
 			tui.ShowBanner("AWS Tools Detected but not Authenticated", "I'll show you the command to run against AWS region "+region+". You can choose to have me execute it for you, or run it yourself. If you prefer to run it on your own, the command will automatically be copied to your clipboard at each step.", false)
 		}
 		skipFailedDetection = true
 	}
internal/infrastructure/spec.go (1)

64-76: Double interpolation and runner ignoring args

args are interpolated for display, but Execute.Run re-interpolates and ignores the args passed to execAction. This can drift and adds cost.

-	args, err := ctx.Interpolate(s.Execute.Arguments...)
+	args, err := ctx.Interpolate(s.Execute.Arguments...)
 	if err != nil {
 		return err
 	}
 	return execAction(
 		ctx.Context,
 		ctx.Runnable,
 		s.Title,
 		s.Description,
 		s.Execute.Command,
 		args,
-		func(_ctx context.Context, cmd string, args []string) error {
-			return s.Execute.Run(ctx)
-		},
+		func(_ctx context.Context, cmd string, args []string) error {
+			// execute with already-interpolated args and apply validation
+			out, err := runCommand(ctx.Context, ctx.Logger, s.Execute.Message, cmd, args...)
+			if err != nil {
+				return err
+			}
+			return s.Execute.Validate.Matches(ctx, strings.TrimSpace(string(out)))
+		},
 		s.Execute.Success,
internal/infrastructure/infrastructure.go (1)

186-189: Message nit: “error checking cluster clustering enabled”

Minor wording cleanup for clarity.

-		return false, fmt.Errorf("error checking cluster clustering enabled: %w", err)
+		return false, fmt.Errorf("error checking clustering enabled: %w", err)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 97546d1 and 835f3a5.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (14)
  • cmd/cluster.go (1 hunks)
  • cmd/machine.go (1 hunks)
  • cmd/root.go (4 hunks)
  • error_codes.yaml (1 hunks)
  • go.mod (5 hunks)
  • internal/errsystem/errorcodes.go (1 hunks)
  • internal/infrastructure/aws.go (1 hunks)
  • internal/infrastructure/cluster.go (1 hunks)
  • internal/infrastructure/gcp.go (1 hunks)
  • internal/infrastructure/infrastructure.go (1 hunks)
  • internal/infrastructure/spec.go (1 hunks)
  • internal/infrastructure/tui.go (1 hunks)
  • internal/infrastructure/util.go (1 hunks)
  • internal/util/api.go (0 hunks)
💤 Files with no reviewable changes (1)
  • internal/util/api.go
🧰 Additional context used
📓 Path-based instructions (1)
cmd/*.go

📄 CodeRabbit inference engine (.cursor/rules/code-generation.mdc)

Ensure CLI commands build and test the pipeline (generation → SDK loading → agent usage) without writing into source SDK

Files:

  • cmd/root.go
  • cmd/machine.go
  • cmd/cluster.go
🧬 Code graph analysis (7)
internal/infrastructure/cluster.go (1)
internal/infrastructure/infrastructure.go (2)
  • Cluster (21-33)
  • CreateMachine (242-271)
internal/infrastructure/gcp.go (3)
internal/infrastructure/cluster.go (2)
  • ClusterSetup (19-22)
  • Setup (33-38)
internal/infrastructure/infrastructure.go (2)
  • Cluster (21-33)
  • CreateMachine (242-271)
internal/infrastructure/spec.go (2)
  • ExecutionContext (98-103)
  • ExecutionSpec (57-62)
cmd/root.go (4)
internal/bundler/bundler.go (1)
  • Version (31-31)
internal/util/api.go (1)
  • Version (30-30)
internal/project/project.go (1)
  • Version (32-32)
internal/errsystem/console.go (1)
  • Version (24-24)
cmd/machine.go (4)
internal/util/api.go (2)
  • EnsureLoggedIn (389-406)
  • GetURLs (268-300)
internal/infrastructure/infrastructure.go (8)
  • EnsureMachineClusteringEnabled (226-234)
  • Machine (36-54)
  • ListMachines (134-148)
  • DeleteMachine (167-180)
  • GetMachine (151-164)
  • CreateMachine (242-271)
  • Cluster (21-33)
  • ListClusters (90-99)
internal/errsystem/errorcodes.go (3)
  • ErrInvalidArgumentProvided (129-132)
  • ErrApiRequest (21-24)
  • ErrMissingRequiredArgument (133-136)
internal/errsystem/errsystem.go (2)
  • WithContextMessage (120-124)
  • WithUserMessage (70-74)
internal/infrastructure/aws.go (3)
internal/infrastructure/cluster.go (2)
  • ClusterSetup (19-22)
  • Setup (33-38)
internal/infrastructure/infrastructure.go (2)
  • Cluster (21-33)
  • CreateMachine (242-271)
internal/infrastructure/spec.go (2)
  • ExecutionSpec (57-62)
  • ExecutionContext (98-103)
cmd/cluster.go (6)
internal/organization/org.go (1)
  • ListOrganizations (26-39)
internal/errsystem/errorcodes.go (4)
  • ErrApiRequest (21-24)
  • ErrInvalidArgumentProvided (129-132)
  • ErrMissingRequiredArgument (133-136)
  • ErrCreateProject (9-12)
internal/errsystem/errsystem.go (2)
  • WithContextMessage (120-124)
  • WithUserMessage (70-74)
internal/util/api.go (2)
  • EnsureLoggedIn (389-406)
  • GetURLs (268-300)
internal/infrastructure/infrastructure.go (7)
  • EnsureClusteringEnabled (215-223)
  • Cluster (21-33)
  • CreateCluster (66-87)
  • CreateClusterArgs (57-63)
  • ListClusters (90-99)
  • DeleteCluster (118-131)
  • GetCluster (102-115)
internal/infrastructure/cluster.go (1)
  • Setup (33-38)
internal/infrastructure/infrastructure.go (3)
internal/util/api.go (1)
  • NewAPIClient (70-78)
internal/errsystem/errorcodes.go (1)
  • ErrApiRequest (21-24)
internal/errsystem/errsystem.go (2)
  • WithContextMessage (120-124)
  • WithUserMessage (70-74)
⏰ 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 (10)
go.mod (1)

21-30: LGTM! Dependency additions align with CLI enhancements.

The new direct dependencies support the infrastructure CLI features introduced in this PR:

  • go-shellwords and go-quote for command parsing and shell execution
  • reflow for text formatting in terminal output
  • clipboard for copying commands to clipboard
  • golang.org/x/term promoted to direct for terminal interaction
error_codes.yaml (1)

101-105: LGTM! Error codes are well-defined.

The new error codes CLI-0032 and CLI-0033 provide clear categorization for argument validation errors, which align with the cluster management CLI commands introduced in this PR.

internal/errsystem/errorcodes.go (1)

129-136: LGTM! Generated error codes follow the existing pattern.

The new error variables are correctly generated and consistent with the rest of the error system.

cmd/root.go (1)

49-144: LGTM! Custom help provides clear command organization.

The categorized help output groups commands logically (Core, Project, Infrastructure, Authentication, Tools) and includes a dynamic "Other Commands" section for extensibility. The implementation correctly handles both TTY and non-TTY scenarios.

internal/infrastructure/cluster.go (2)

19-38: LGTM! Clean provider registration pattern.

The ClusterSetup interface and registry provide a clean extensibility point for infrastructure providers. The dispatcher correctly returns an error for unregistered providers, and the duplicate-registration check in register() prevents initialization bugs.


44-66: LGTM! Secure key generation implementation.

The generateKey() function correctly generates ECDSA P-256 key pairs, marshals them to PEM format, and base64-encodes for transport. Error propagation is appropriate throughout.

cmd/cluster.go (4)

103-139: Organization selection logic handles single-org case well.

The promptForClusterOrganization function correctly handles both single and multi-organization scenarios, with appropriate fallbacks for non-TTY environments and preference persistence.


167-290: Cluster creation flow is well-structured.

The clusterNewCmd properly validates inputs, provides interactive prompts when available, and gracefully handles both TTY and non-TTY scenarios. Error handling uses the centralized error system consistently.

The confirmation step (tui.Ask) before cluster creation is a good safeguard against accidental infrastructure provisioning.


306-375: List command provides clear output formatting.

The clusterListCmd handles empty states well with helpful guidance, sorts output for consistency, and supports both JSON and table formats. The table columns are well-chosen for quick cluster overview.


394-423: Remove command includes appropriate safety checks.

The clusterRemoveCmd requires explicit confirmation unless --force is provided, which is a good safety pattern for destructive operations. The warning message clearly states the action is irreversible.

Comment on lines +259 to +263
ready := tui.Ask(logger, "Ready to create the cluster", true)
if !ready {
logger.Info("Cluster creation cancelled")
os.Exit(0)
}
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 | 🔴 Critical

🧩 Analysis chain

Confirmation prompt can be bypassed in non-interactive mode.

Lines 259-263 show a confirmation prompt with tui.Ask, but in non-interactive mode (no TTY), this would need to default to true or require a --yes flag. Verify that tui.Ask handles non-TTY appropriately to prevent silent failures or forced confirmations.


🏁 Script executed:

#!/bin/bash
# Check how tui.Ask handles non-TTY environments
ast-grep --pattern $'func Ask($$$) $_ {
  $$$
}'

Length of output: 0


I'll search for the tui.Ask function and understand how it handles non-TTY environments:

I need to search the codebase directly to find how tui.Ask is implemented and how it handles non-TTY environments:

</script>

🏁 Script executed:

#!/bin/bash
# Find imports of tui package and Ask function usage
echo "=== Searching for tui package imports ==="
rg -n "tui\." cmd/cluster.go

echo "=== Searching for tui package import statement ==="
rg -n "import.*tui" --type=go

echo "=== Searching for Ask function definition ==="
rg -nP "func\s+Ask\s*\(" --type=go

echo "=== Looking for non-TTY or TTY detection ==="
rg -nP "(TTY|isatty|interactive)" --type=go -C 2

Length of output: 20789


🏁 Script executed:

#!/bin/bash
# Search for tui.Ask usage and definition
echo "=== Searching for tui imports ==="
rg -n "import.*tui" --type=go

echo ""
echo "=== Searching for Ask function calls ==="
rg -nP "Ask\s*\(" --type=go

echo ""
echo "=== Look at the imports and context in cmd/cluster.go ==="
head -50 cmd/cluster.go

echo ""
echo "=== Look for tui package ==="
fd -t f "tui" --type=go | head -20

Length of output: 7323


</script>

🏁 Script executed:

#!/bin/bash
# Get detailed context around the problematic lines
echo "=== Full context of cmd/cluster.go around line 259 ==="
sed -n '200,270p' cmd/cluster.go

echo ""
echo "=== Check go.mod for tui dependency ==="
grep -i "go-common" go.mod

echo ""
echo "=== Check how tui.Ask is called at line 408 (deletion) ==="
sed -n '400,420p' cmd/cluster.go

Length of output: 3711


</script>

🏁 Script executed:

#!/bin/bash
# Look for external tui package source or documentation
echo "=== Check if go-common is vendored ==="
find . -type d -name "go-common" | head -5

echo ""
echo "=== Look for tui.Ask implementation ==="
find . -path "*/go-common/tui*" -type f | head -10

echo ""
echo "=== Check go.sum for go-common version ==="
grep "go-common" go.sum | head -5

echo ""
echo "=== Compare Ask usage patterns across codebase ==="
echo "Ask calls with force guard:"
rg -B2 "if !force.*Ask" --type=go

echo ""
echo "Ask calls without force guard:"
rg -B2 -A1 "^\s*ready :=" --type=go

Length of output: 912


I found evidence of the issue. Let me verify the external package behavior to be certain:

</script>

🏁 Script executed:

#!/bin/bash
# Search for any documentation or comments about tui.Ask in the codebase
echo "=== Look for comments around Ask calls ==="
rg -B5 -A2 "tui.Ask" --type=go | head -80

echo ""
echo "=== Check if there's a --force or --yes flag for cluster new ==="
grep -A20 "clusterNewCmd.*Flags" cmd/cluster.go | head -30

Length of output: 5104


🌐 Web query:

agentuity go-common tui.Ask non-TTY behavior

💡 Result:

Do you want me to fetch and read the tui.Ask source (ask.go) from agentuity/go-common on GitHub and explain exactly how it behaves when stdout/stdin is not a TTY?


Add tui.HasTTY guard before confirmation prompt or implement --force flag for non-interactive bypass.

The confirmation prompt at line 259 runs unconditionally in both TTY and non-TTY modes. In non-interactive environments (CI/automated pipelines), tui.Ask will block indefinitely or fail since stdin is not available.

Pattern mismatch identified:

  • Cluster deletion (line 408) uses if !force && !tui.Ask(...) with --force flag available
  • Agent deletion and project deletion follow the same guarded pattern
  • Version check (internal/util/version.go:67) guards with if tui.HasTTY { ... tui.Ask(...) }
  • Cluster creation has no such guard and no --force flag alternative

Recommended fix: Either wrap the Ask call with if tui.HasTTY { ... } or add a --force / --yes flag to clusterNewCmd (consistent with clusterRemoveCmd).

🤖 Prompt for AI Agents
In cmd/cluster.go around lines 259 to 263, the confirmation prompt is called
unconditionally which blocks in non-TTY environments; add the same guarded
pattern used by cluster deletion: introduce a boolean --force/--yes flag on
clusterNewCmd, read it into a local variable, and replace the current
unconditional Ask with a conditional check such as: if !force { if !tui.HasTTY {
logger.Error("non-interactive shell: use --force to bypass confirmation");
os.Exit(1) } if !tui.Ask(logger, "Ready to create the cluster", true) {
logger.Info("Cluster creation cancelled"); os.Exit(0) } } to ensure interactive
prompts only run in TTY and CI can bypass with --force.

Comment on lines +130 to +137
// Format started time or use created time
startedTime := ""
if machine.StartedAt != nil {
startedTime = (*machine.StartedAt)[:10] // show date only
} else {
startedTime = machine.CreatedAt[:10]
}

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

Unsafe substring on timestamps

Slicing [:10] can panic if the string is shorter. Guard length or parse time safely.

-			if machine.StartedAt != nil {
-				startedTime = (*machine.StartedAt)[:10] // show date only
-			} else {
-				startedTime = machine.CreatedAt[:10]
-			}
+			if machine.StartedAt != nil && len(*machine.StartedAt) >= 10 {
+				startedTime = (*machine.StartedAt)[:10]
+			} else if len(machine.CreatedAt) >= 10 {
+				startedTime = machine.CreatedAt[:10]
+			} else {
+				startedTime = machine.CreatedAt
+			}
📝 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
// Format started time or use created time
startedTime := ""
if machine.StartedAt != nil {
startedTime = (*machine.StartedAt)[:10] // show date only
} else {
startedTime = machine.CreatedAt[:10]
}
// Format started time or use created time
startedTime := ""
if machine.StartedAt != nil && len(*machine.StartedAt) >= 10 {
startedTime = (*machine.StartedAt)[:10]
} else if len(machine.CreatedAt) >= 10 {
startedTime = machine.CreatedAt[:10]
} else {
startedTime = machine.CreatedAt
}
🤖 Prompt for AI Agents
In cmd/machine.go around lines 130 to 137, the code slices timestamp strings
with [:10] which can panic if the string is shorter; instead, ensure you safely
obtain the date by either (a) checking the string length before slicing (e.g.,
if len(s) >= 10 { s[:10] } else { s }), or (b) preferably parse the timestamp
into a time.Time (using time.Parse with the known layout) and then format it
with t.Format("2006-01-02") for a robust date string; apply this to both
machine.StartedAt (after nil check) and machine.CreatedAt.

Comment on lines +342 to +347
resp, err := infrastructure.CreateMachine(ctx, logger, urls.API, apikey, clusterID, provider, region)
if err != nil {
logger.Fatal("error creating machine: %s", err)
}
fmt.Printf("Machine created successfully with ID: %s and Token: %s\n", resp.ID, resp.Token)
},
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

Token printed to stdout (potential secret leakage)

Printing join/provisioning tokens can leak via logs/shell history. Avoid by default; show only ID or gate behind a flag.

-		fmt.Printf("Machine created successfully with ID: %s and Token: %s\n", resp.ID, resp.Token)
+		fmt.Printf("Machine created successfully with ID: %s\n", resp.ID)
+		// If exposing the token is needed, add a --show-token flag and print explicitly.
📝 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
resp, err := infrastructure.CreateMachine(ctx, logger, urls.API, apikey, clusterID, provider, region)
if err != nil {
logger.Fatal("error creating machine: %s", err)
}
fmt.Printf("Machine created successfully with ID: %s and Token: %s\n", resp.ID, resp.Token)
},
resp, err := infrastructure.CreateMachine(ctx, logger, urls.API, apikey, clusterID, provider, region)
if err != nil {
logger.Fatal("error creating machine: %s", err)
}
fmt.Printf("Machine created successfully with ID: %s\n", resp.ID)
// If exposing the token is needed, add a --show-token flag and print explicitly.
},
🤖 Prompt for AI Agents
In cmd/machine.go around lines 342 to 347, the code prints the provisioning
token to stdout which risks secret leakage; change the output to only show the
Machine ID by default and do not print the token, and if the token must be
exposed provide an explicit boolean flag (e.g. --show-token) added to the
command that defaults to false; implement the flag handling, conditionally print
the token only when the flag is true, and ensure other logging paths do not
inadvertently log the token (use ID-only output or secure logger for any
sensitive data).

Comment on lines +50 to +56
func confirmAction(canExecute bool) actionType {
oldState, err := term.MakeRaw(int(os.Stdin.Fd()))
if err != nil {
panic(err)
}
defer term.Restore(int(os.Stdin.Fd()), oldState)
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
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

Raw-mode on non-TTY and panic-on-error

term.MakeRaw() will fail on non‑TTY; panic here kills the CLI. Detect TTY and degrade gracefully.

-func confirmAction(canExecute bool) actionType {
-	oldState, err := term.MakeRaw(int(os.Stdin.Fd()))
-	if err != nil {
-		panic(err)
-	}
+func confirmAction(canExecute bool) actionType {
+	fd := int(os.Stdin.Fd())
+	if !term.IsTerminal(fd) {
+		// no raw mode possible; prefer manual/skip flow without interaction
+		if canExecute {
+			return manual
+		}
+		return skip
+	}
+	oldState, err := term.MakeRaw(fd)
+	if err != nil {
+		return cancelled
+	}
 	defer term.Restore(int(os.Stdin.Fd()), oldState)
📝 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
func confirmAction(canExecute bool) actionType {
oldState, err := term.MakeRaw(int(os.Stdin.Fd()))
if err != nil {
panic(err)
}
defer term.Restore(int(os.Stdin.Fd()), oldState)
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
func confirmAction(canExecute bool) actionType {
fd := int(os.Stdin.Fd())
if !term.IsTerminal(fd) {
// no raw mode possible; prefer manual/skip flow without interaction
if canExecute {
return manual
}
return skip
}
oldState, err := term.MakeRaw(fd)
if err != nil {
return cancelled
}
defer term.Restore(int(os.Stdin.Fd()), oldState)
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
🤖 Prompt for AI Agents
internal/infrastructure/tui.go lines 50-56: term.MakeRaw is called
unconditionally and panics on non-TTY; change to detect whether stdin is a
terminal first (using term.IsTerminal or similar on os.Stdin.Fd()) and only call
term.MakeRaw when true, capturing the returned state and setting up defer
restore only when MakeRaw succeeded; if stdin is not a TTY or MakeRaw returns an
error, do not panic—handle it gracefully by falling back to non-raw input
handling (e.g., simple buffered read or default action) and ensure any
signal.NotifyContext usage and cancel are still set up correctly.

Comment on lines +60 to +64
buf := make([]byte, 1)
os.Stdin.Read(buf)
fmt.Print("\x1b[2K\x1b[2K\r") // erase current line and move cursor to beginning
ch <- buf[0]
}()
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 | 🟡 Minor

stdin read error unchecked

If Read fails or returns 0 bytes, buf[0] is invalid data. Check n/error.

-		buf := make([]byte, 1)
-		os.Stdin.Read(buf)
+		buf := make([]byte, 1)
+		n, _ := os.Stdin.Read(buf)
 		fmt.Print("\x1b[2K\x1b[2K\r") // erase current line and move cursor to beginning
-		ch <- buf[0]
+		if n > 0 {
+			ch <- buf[0]
+		} else {
+			ch <- 0
+		}
📝 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
buf := make([]byte, 1)
os.Stdin.Read(buf)
fmt.Print("\x1b[2K\x1b[2K\r") // erase current line and move cursor to beginning
ch <- buf[0]
}()
buf := make([]byte, 1)
n, _ := os.Stdin.Read(buf)
fmt.Print("\x1b[2K\x1b[2K\r") // erase current line and move cursor to beginning
if n > 0 {
ch <- buf[0]
} else {
ch <- 0
}
}()
🤖 Prompt for AI Agents
In internal/infrastructure/tui.go around lines 60-64, the goroutine reads into
buf but ignores the returned n and err; change the Read call to capture n,err :=
os.Stdin.Read(buf) and only use buf[0] when n == 1 and err == nil. On err or n
== 0, handle it explicitly (e.g. log/return, close the channel or send a
sentinel) instead of dereferencing invalid data, and preserve the existing
cleanup (the erase cursor print) only when appropriate.

Comment on lines +139 to +140
clipboard.Write(clipboard.FmtText, []byte(clipboardCmd))
fmt.Println(v)
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

Guard clipboard writes when init failed

Avoid panic if clipboard is unavailable.

-	clipboard.Write(clipboard.FmtText, []byte(clipboardCmd))
+	if clipReady {
+		clipboard.Write(clipboard.FmtText, []byte(clipboardCmd))
+	}
📝 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
clipboard.Write(clipboard.FmtText, []byte(clipboardCmd))
fmt.Println(v)
if clipReady {
clipboard.Write(clipboard.FmtText, []byte(clipboardCmd))
}
fmt.Println(v)
🤖 Prompt for AI Agents
internal/infrastructure/tui.go around lines 139-140: the code unconditionally
calls clipboard.Write which can panic if the clipboard is uninitialized or
unavailable; change it to attempt the write and handle the returned error (or
check an init flag) so clipboard failures are guarded — e.g. call
clipboard.Write and if it returns an error, swallow or log it and avoid calling
fmt.Println only if needed, ensuring no panic occurs when the clipboard is
unavailable.

Comment on lines +198 to +203
args, err := shellwords.Parse(strings.TrimSpace(string(newbuf)))
if err != nil {
return fmt.Errorf("error parsing edited command: %w", err)
}
return execAction(ctx, canExecute, instruction, help, args[0], args[1:], runner, success, skipFunc)
}
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 | 🔴 Critical

Possible index out of range if edited command is empty

If the user clears the file or leaves only whitespace, args[0] panics.

-		args, err := shellwords.Parse(strings.TrimSpace(string(newbuf)))
+		args, err := shellwords.Parse(strings.TrimSpace(string(newbuf)))
 		if err != nil {
 			return fmt.Errorf("error parsing edited command: %w", err)
 		}
-		return execAction(ctx, canExecute, instruction, help, args[0], args[1:], runner, success, skipFunc)
+		if len(args) == 0 {
+			return fmt.Errorf("error parsing edited command: no command provided")
+		}
+		return execAction(ctx, canExecute, instruction, help, args[0], args[1:], runner, success, skipFunc)
📝 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
args, err := shellwords.Parse(strings.TrimSpace(string(newbuf)))
if err != nil {
return fmt.Errorf("error parsing edited command: %w", err)
}
return execAction(ctx, canExecute, instruction, help, args[0], args[1:], runner, success, skipFunc)
}
args, err := shellwords.Parse(strings.TrimSpace(string(newbuf)))
if err != nil {
return fmt.Errorf("error parsing edited command: %w", err)
}
if len(args) == 0 {
return fmt.Errorf("error parsing edited command: no command provided")
}
return execAction(ctx, canExecute, instruction, help, args[0], args[1:], runner, success, skipFunc)
}
🤖 Prompt for AI Agents
internal/infrastructure/tui.go around lines 198 to 203: the code calls args[0]
without validating args, which will panic if the edited file is empty or only
whitespace; fix by checking len(args) before indexing and return a clear error
(or treat as user-cancel) when args is empty, e.g. if len(args) == 0 { return
fmt.Errorf("no command provided") } and only then call execAction with args[0]
and args[1:].

Comment on lines +19 to +43
func buildCommandSequences(command string, args []string) []sequenceCommand {
// If using sh -c, don't parse for pipes - let the shell handle it
if command == "sh" && len(args) > 0 && args[0] == "-c" {
return []sequenceCommand{{command: command, args: args}}
}

var sequences []sequenceCommand
current := sequenceCommand{
command: command,
}
for _, arg := range args {
if arg == "|" {
sequences = append(sequences, current)
current = sequenceCommand{}
} else if current.command == "" {
current.command = arg
} else {
current.args = append(current.args, arg)
}
}
if current.command != "" {
sequences = append(sequences, current)
}
return sequences
}
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 | 🟡 Minor

Pipeline parsing may not handle quoted arguments correctly.

The buildCommandSequences function splits on | but doesn't account for quoted strings that might contain pipe characters (e.g., echo "foo | bar" | grep foo). This could lead to incorrect command splitting.

Consider using a proper shell parser or documenting this limitation if complex pipelines are not expected.

🤖 Prompt for AI Agents
internal/infrastructure/util.go lines 19-43: the current split-on-arg=="|" logic
can mis-split when the input is a single raw command string containing quoted
pipes; update buildCommandSequences to be quote-aware by parsing the raw command
string (when args represent a single string or when you need to handle embedded
pipes) with a simple state machine that tracks single quotes, double quotes and
backslash escapes and only treats unquoted '|' as a pipeline separator, then
split each pipeline segment into command + args with a tokenizer that preserves
quoted tokens (or use a reliable shell parsing library) so pipes inside quotes
are not treated as separators.

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: 2

♻️ Duplicate comments (2)
cmd/machine.go (2)

130-137: Unsafe substring on timestamps

Slicing [:10] can panic if the string is shorter than 10 characters. Guard the length or parse the timestamp safely.

Apply this diff to fix:

-			// Format started time or use created time
-			startedTime := ""
-			if machine.StartedAt != nil {
-				startedTime = (*machine.StartedAt)[:10] // show date only
-			} else {
-				startedTime = machine.CreatedAt[:10]
-			}
+			// Format started time or use created time
+			startedTime := ""
+			if machine.StartedAt != nil && len(*machine.StartedAt) >= 10 {
+				startedTime = (*machine.StartedAt)[:10]
+			} else if len(machine.CreatedAt) >= 10 {
+				startedTime = machine.CreatedAt[:10]
+			} else {
+				startedTime = machine.CreatedAt
+			}

346-346: Token printed to stdout (potential secret leakage)

Printing the provisioning token can leak via logs or shell history. Avoid printing it by default.

Apply this diff to remove token from output:

-		fmt.Printf("Machine created successfully with ID: %s and Token: %s\n", resp.ID, resp.Token)
+		tui.ShowSuccess("Machine %s created successfully", resp.ID)
+		// Token is securely stored server-side and will be used during provisioning
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 835f3a5 and e8b8e4c.

📒 Files selected for processing (1)
  • cmd/machine.go (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
cmd/*.go

📄 CodeRabbit inference engine (.cursor/rules/code-generation.mdc)

Ensure CLI commands build and test the pipeline (generation → SDK loading → agent usage) without writing into source SDK

Files:

  • cmd/machine.go
🧬 Code graph analysis (1)
cmd/machine.go (4)
internal/util/api.go (2)
  • EnsureLoggedIn (389-406)
  • GetURLs (268-300)
internal/infrastructure/infrastructure.go (8)
  • EnsureMachineClusteringEnabled (226-234)
  • Machine (36-54)
  • ListMachines (134-148)
  • DeleteMachine (167-180)
  • GetMachine (151-164)
  • CreateMachine (242-271)
  • Cluster (21-33)
  • ListClusters (90-99)
internal/errsystem/errorcodes.go (3)
  • ErrInvalidArgumentProvided (129-132)
  • ErrApiRequest (21-24)
  • ErrMissingRequiredArgument (133-136)
internal/errsystem/errsystem.go (2)
  • WithContextMessage (120-124)
  • WithUserMessage (70-74)
⏰ 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). (2)
  • GitHub Check: Build and Test (macos-latest)
  • GitHub Check: Analyze (go)
🔇 Additional comments (6)
cmd/machine.go (6)

1-18: LGTM!

Imports are well-organized and appropriate for the machine management CLI commands.


20-30: LGTM!

Root command setup is appropriate with hidden flag and help fallback.


154-200: LGTM!

Machine removal command properly gates access, prompts for confirmation, and provides good user feedback.


202-295: LGTM!

Machine status command provides comprehensive information with proper format support and nil-safe field handling.


350-376: LGTM!

Command registration and flag setup follow standard Cobra patterns correctly.


344-344: Inconsistent error handling pattern

Line 344 uses logger.Fatal while the rest of the file consistently uses the errsystem.New().ShowErrorAndExit() pattern for error handling.

Apply this diff to align with the file's error handling pattern:

-		resp, err := infrastructure.CreateMachine(ctx, logger, urls.API, apikey, clusterID, provider, region)
-		if err != nil {
-			logger.Fatal("error creating machine: %s", err)
-		}
+		var resp *infrastructure.CreateMachineResponse
+		tui.ShowSpinner("Creating machine...", func() {
+			var err error
+			resp, err = infrastructure.CreateMachine(ctx, logger, urls.API, apikey, clusterID, provider, region)
+			if err != nil {
+				errsystem.New(errsystem.ErrApiRequest, err, errsystem.WithContextMessage("Failed to create machine")).ShowErrorAndExit()
+			}
+		})

Likely an incorrect or invalid review comment.


// Handle user cancellation (empty string)
if id == "" {
errsystem.New(errsystem.ErrApiRequest, fmt.Errorf("no cluster selected"), errsystem.WithUserMessage("No cluster selected")).ShowErrorAndExit()
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 | 🟡 Minor

Use appropriate error type for user cancellation

Using ErrApiRequest for user cancellation is semantically incorrect. This is a user input issue, not an API request failure.

Apply this diff:

-		errsystem.New(errsystem.ErrApiRequest, fmt.Errorf("no cluster selected"), errsystem.WithUserMessage("No cluster selected")).ShowErrorAndExit()
+		errsystem.New(errsystem.ErrInvalidArgumentProvided, fmt.Errorf("no cluster selected"), errsystem.WithUserMessage("No cluster selected")).ShowErrorAndExit()
📝 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
errsystem.New(errsystem.ErrApiRequest, fmt.Errorf("no cluster selected"), errsystem.WithUserMessage("No cluster selected")).ShowErrorAndExit()
errsystem.New(errsystem.ErrInvalidArgumentProvided, fmt.Errorf("no cluster selected"), errsystem.WithUserMessage("No cluster selected")).ShowErrorAndExit()
🤖 Prompt for AI Agents
In cmd/machine.go around line 414, the code uses errsystem.ErrApiRequest for a
user cancellation case; replace ErrApiRequest with the semantically correct
error type (e.g., errsystem.ErrUserCancelled or the project's
ErrUserInput/ErrUserCanceled constant) when calling errsystem.New(...), keep the
fmt.Errorf("no cluster selected") and WithUserMessage("No cluster selected")
as-is, and adjust imports or constant names if necessary to compile.

}

// This should never happen, but handle it as an impossible path
errsystem.New(errsystem.ErrApiRequest, fmt.Errorf("selected cluster not found: %s", id), errsystem.WithUserMessage("Selected cluster not found")).ShowErrorAndExit()
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 | 🟡 Minor

Use appropriate error type for internal validation error

Using ErrApiRequest for an internal state error (selected cluster not found in list) is semantically incorrect.

Apply this diff:

-		errsystem.New(errsystem.ErrApiRequest, fmt.Errorf("selected cluster not found: %s", id), errsystem.WithUserMessage("Selected cluster not found")).ShowErrorAndExit()
+		errsystem.New(errsystem.ErrInvalidArgumentProvided, fmt.Errorf("selected cluster not found: %s", id), errsystem.WithUserMessage("Selected cluster not found")).ShowErrorAndExit()
📝 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
errsystem.New(errsystem.ErrApiRequest, fmt.Errorf("selected cluster not found: %s", id), errsystem.WithUserMessage("Selected cluster not found")).ShowErrorAndExit()
errsystem.New(errsystem.ErrInvalidArgumentProvided, fmt.Errorf("selected cluster not found: %s", id), errsystem.WithUserMessage("Selected cluster not found")).ShowErrorAndExit()
🤖 Prompt for AI Agents
In cmd/machine.go around line 425, the error is created with
errsystem.ErrApiRequest which is wrong for an internal validation/state error;
replace the error type with the appropriate internal validation error constant
from the errsystem package (for example errsystem.ErrValidation or
errsystem.ErrInternal if that is the project's chosen constant), keeping the
fmt.Errorf detail and the user message ("Selected cluster not found"); ensure
the call uses the correct errsystem constant and retains the WithUserMessage and
ShowErrorAndExit usage.

@robindiddams robindiddams merged commit b15a1e3 into main Oct 24, 2025
14 checks passed
@robindiddams robindiddams deleted the infra branch October 24, 2025 17:30
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.

4 participants