[object Object]

← back to Cli Printing Press

refactor(cli): use ClaimOutputDir for atomic output directory claiming

da9500e4b2056803b8112fd296cc4f95bc9b6281 · 2026-03-27 19:40:01 -0700 · Trevin Chow

Replace inline collision checks in generate command with claimOrForce helper.
--force claims the exact base slot. --output errors on collision. Default
auto-increments via os.Mkdir for race-safe concurrent generation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit da9500e4b2056803b8112fd296cc4f95bc9b6281
Author: Trevin Chow <trevin@trevinchow.com>
Date:   Fri Mar 27 19:40:01 2026 -0700

    refactor(cli): use ClaimOutputDir for atomic output directory claiming
    
    Replace inline collision checks in generate command with claimOrForce helper.
    --force claims the exact base slot. --output errors on collision. Default
    auto-increments via os.Mkdir for race-safe concurrent generation.
    
    Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---
 internal/cli/root.go | 53 ++++++++++++++++++++++++++++++++++------------------
 1 file changed, 35 insertions(+), 18 deletions(-)

diff --git a/internal/cli/root.go b/internal/cli/root.go
index 07fedae2..45fc17b4 100644
--- a/internal/cli/root.go
+++ b/internal/cli/root.go
@@ -118,15 +118,9 @@ func newGenerateCmd() *cobra.Command {
 				if err != nil {
 					return fmt.Errorf("resolving output path: %w", err)
 				}
-				if force {
-					if err := os.RemoveAll(absOut); err != nil {
-						return fmt.Errorf("removing existing output dir: %w", err)
-					}
-				} else if info, err := os.Stat(absOut); err == nil && info.IsDir() {
-					entries, _ := os.ReadDir(absOut)
-					if len(entries) > 0 {
-						return fmt.Errorf("output directory %s already exists (use --force to overwrite)", absOut)
-					}
+				absOut, err = claimOrForce(absOut, force, outputDir != "")
+				if err != nil {
+					return &ExitError{Code: ExitInputError, Err: err}
 				}
 
 				gen := generator.New(parsed, absOut)
@@ -221,15 +215,9 @@ func newGenerateCmd() *cobra.Command {
 			if dryRun {
 				return printDryRun(apiSpec, absOut, specFiles)
 			}
-			if force {
-				if err := os.RemoveAll(absOut); err != nil {
-					return fmt.Errorf("removing existing output dir: %w", err)
-				}
-			} else if info, err := os.Stat(absOut); err == nil && info.IsDir() {
-				entries, _ := os.ReadDir(absOut)
-				if len(entries) > 0 {
-					return fmt.Errorf("output directory %s already exists (use --force to overwrite)", absOut)
-				}
+			absOut, err = claimOrForce(absOut, force, outputDir != "")
+			if err != nil {
+				return &ExitError{Code: ExitInputError, Err: err}
 			}
 
 			gen := generator.New(apiSpec, absOut)
@@ -342,6 +330,35 @@ func mergeSpecs(specs []*spec.APISpec, name string) *spec.APISpec {
 	return merged
 }
 
+// claimOrForce resolves the output directory based on --force and --output flags.
+//
+//   - force=true:  RemoveAll the target, then create it fresh (claims exact slot)
+//   - explicit output (--output set) without force: error if exists and non-empty
+//   - default (no --output, no --force): auto-increment via ClaimOutputDir
+func claimOrForce(absOut string, force bool, explicitOutput bool) (string, error) {
+	if force {
+		if err := os.RemoveAll(absOut); err != nil {
+			return "", fmt.Errorf("removing existing output dir: %w", err)
+		}
+		if err := os.MkdirAll(absOut, 0o755); err != nil {
+			return "", fmt.Errorf("creating output dir: %w", err)
+		}
+		return absOut, nil
+	}
+
+	if explicitOutput {
+		if info, err := os.Stat(absOut); err == nil && info.IsDir() {
+			entries, _ := os.ReadDir(absOut)
+			if len(entries) > 0 {
+				return "", fmt.Errorf("output directory %s already exists (use --force to overwrite)", absOut)
+			}
+		}
+		return absOut, nil
+	}
+
+	return pipeline.ClaimOutputDir(absOut)
+}
+
 func fetchOrCacheSpec(specURL string, refresh bool, skipCache bool) ([]byte, error) {
 	sum := sha256.Sum256([]byte(specURL))
 	cacheKey := hex.EncodeToString(sum[:])

← 0fd4cba6 test(pipeline): add concurrency test for ClaimOutputDir  ·  back to Cli Printing Press  ·  fix(pipeline): backfill PlanStatus in migration and persist 3a9e977b →