Skip to content

feat: add srtd doctor command for setup diagnostics#60

Merged
t1mmen merged 10 commits intomainfrom
feat/config-doctor
Dec 30, 2025
Merged

feat: add srtd doctor command for setup diagnostics#60
t1mmen merged 10 commits intomainfrom
feat/config-doctor

Conversation

@t1mmen
Copy link
Owner

@t1mmen t1mmen commented Dec 29, 2025

User description

Summary

New srtd doctor command that validates your SRTD configuration and diagnoses setup issues.

What it does

Runs 10 diagnostic checks in sequence:

  1. Config file exists - Verifies srtd.config.json is present
  2. Config schema valid - Validates config against Zod schema
  3. Template directory exists - Checks templateDir path exists
  4. Migration directory exists - Checks migrationDir path exists
  5. Template directory readable - Verifies read permissions
  6. Migration directory writable - Verifies write permissions
  7. Build log valid - Validates .srtd.buildlog.json integrity
  8. Local build log valid - Validates .srtd.buildlog.local.json integrity
  9. Database connection - Tests connectivity with 5s timeout
  10. Template count - Confirms at least one template exists

Output Example

srtd doctor
───────────────────────────────────────────────────────────────

✔ Config file exists
✔ Config schema valid
✔ Template directory exists
✔ Migration directory exists
✔ Template directory readable
✔ Migration directory writable
✔ Build log valid
✔ Local build log valid
✔ Database connection
✔ Template count

───────────────────────────────────────────────────────────────
10 checks passed, no issues found

Implementation

  • src/utils/doctorChecks.ts - Pure check functions (testable)
  • src/commands/doctor.ts - Command orchestration
  • Integrated into CLI and interactive menu

Test plan

  • 29 new unit tests for doctor check functions
  • All 528 tests pass
  • TypeScript compiles without errors
  • Manual testing confirms output format

PR Type

Enhancement


Description

  • Add srtd doctor command for setup diagnostics

    • Validates config file exists and schema is valid
    • Checks template/migration directories exist with correct permissions
    • Validates build log integrity
    • Tests database connectivity with 5s timeout
    • Confirms at least one SQL template exists
  • Implement 10 diagnostic checks as pure, testable functions

  • Integrate doctor command into CLI and interactive menu

  • Add 29 comprehensive unit tests for all check functions


Diagram Walkthrough

flowchart LR
  CLI["CLI Entry Point"]
  MENU["Interactive Menu"]
  DOCTOR["Doctor Command"]
  CHECKS["Doctor Checks Module"]
  CONFIG["Config Validation"]
  DB["Database Service"]
  
  CLI -- "imports" --> DOCTOR
  MENU -- "routes to" --> DOCTOR
  DOCTOR -- "orchestrates" --> CHECKS
  CHECKS -- "validates" --> CONFIG
  CHECKS -- "tests" --> DB
Loading

File Walkthrough

Relevant files
Tests
doctorChecks.test.ts
Comprehensive unit tests for doctor checks                             

src/tests/doctorChecks.test.ts

  • Add 449 lines of comprehensive unit tests for doctor check functions
  • Test all 10 diagnostic checks with passing and failing scenarios
  • Mock file system, database service, and config validation
  • Test runAllChecks orchestration function
  • Verify timeout behavior for database connections
+449/-0 
Enhancement
doctorChecks.ts
Pure doctor check functions implementation                             

src/utils/doctorChecks.ts

  • Implement 10 pure diagnostic check functions
  • Check config file existence and schema validity
  • Verify template/migration directory existence and permissions
  • Validate build log and local build log integrity
  • Test database connectivity with configurable timeout
  • Count SQL templates in template directory
  • Implement runAllChecks orchestration function
+335/-0 
doctor.ts
Doctor command orchestration and output                                   

src/commands/doctor.ts

  • Create new doctorCommand for CLI
  • Render formatted output with check results and summary
  • Display separator lines and pass/fail indicators
  • Handle errors gracefully with exit codes
  • Integrate with branding and project root detection
+91/-0   
menu.ts
Add doctor to interactive menu                                                     

src/commands/menu.ts

  • Add doctor option to interactive menu
  • Route doctor selection to doctor command execution
+6/-0     
Configuration changes
cli.ts
Register doctor command in CLI                                                     

src/cli.ts

  • Import new doctorCommand module
  • Register doctor command with CLI program
+2/-0     
Documentation
config-doctor.md
Changelog entry for doctor command                                             

.changeset/config-doctor.md

  • Document new srtd doctor command in changelog
  • List all diagnostic checks performed
  • Provide usage instructions
+14/-0   

New read-only diagnostic command that validates SRTD configuration:
- Config file exists and schema valid
- Template/migration directories exist with correct permissions
- Build log integrity checks
- Database connectivity test
- Template count verification

Includes 29 new unit tests for doctor check functions.
@changeset-bot
Copy link

changeset-bot bot commented Dec 29, 2025

🦋 Changeset detected

Latest commit: 34421b6

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@t1mmen/srtd Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@qodo-free-for-open-source-projects
Copy link
Contributor

qodo-free-for-open-source-projects bot commented Dec 29, 2025

PR Compliance Guide 🔍

Below is a summary of compliance checks for this PR:

Security Compliance
Incomplete cleanup handling

Description: The test file .srtd-doctor-test is created in the migration directory but may not be
cleaned up if the unlink operation fails, potentially leaving test artifacts in production
directories.
doctorChecks.ts [126-130]

Referred Code
const testFile = path.join(migrationPath, '.srtd-doctor-test');

try {
  await fs.writeFile(testFile, 'test');
  await fs.unlink(testFile);
Connection string exposure

Description: The pgConnection string from config is passed directly to DatabaseService.fromConfig()
without validation or sanitization, potentially exposing sensitive connection details in
error messages or logs if the connection fails.
doctorChecks.ts [39-39]

Referred Code
 */
export async function checkConfigSchemaValid(
Ticket Compliance
🎫 No ticket provided
  • Create ticket/issue
Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
🟢
Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status:
Error message exposure: The doctor command logs raw error messages to console which may contain sensitive database
connection details or internal paths

Referred Code
console.log(chalk.red(getErrorMessage(error)));
exitCode = 1;

Learn more about managing compliance generic rules or creating your own custom rules

  • Update
Compliance status legend 🟢 - Fully Compliant
🟡 - Partial Compliant
🔴 - Not Compliant
⚪ - Requires Further Human Verification
🏷️ - Compliance label

@codecov
Copy link

codecov bot commented Dec 29, 2025

Codecov Report

❌ Patch coverage is 68.98734% with 49 lines in your changes missing coverage. Please review.
✅ Project coverage is 79.85%. Comparing base (fed17ec) to head (34421b6).
⚠️ Report is 1 commits behind head on main.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
src/commands/doctor.ts 2.38% 41 Missing ⚠️
src/commands/menu.ts 0.00% 4 Missing ⚠️
src/utils/doctorChecks.ts 96.39% 4 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #60      +/-   ##
==========================================
- Coverage   80.68%   79.85%   -0.84%     
==========================================
  Files          44       46       +2     
  Lines        2061     2219     +158     
  Branches      513      546      +33     
==========================================
+ Hits         1663     1772     +109     
- Misses        398      447      +49     
Flag Coverage Δ
e2e 79.85% <68.98%> (-0.84%) ⬇️
unittests 79.85% <68.98%> (-0.84%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@qodo-free-for-open-source-projects
Copy link
Contributor

qodo-free-for-open-source-projects bot commented Dec 29, 2025

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
High-level
Run diagnostic checks in parallel

Refactor the runAllChecks function to execute all diagnostic checks concurrently
using Promise.all. This will improve performance by not waiting for each
I/O-bound operation, like the database check, to complete sequentially.

Examples:

src/utils/doctorChecks.ts [315-335]
export async function runAllChecks(
  projectRoot: string,
  config: CLIConfig,
  warnings: ValidationWarning[]
): Promise<DoctorCheckResult[]> {
  const results: DoctorCheckResult[] = [];

  // Run checks in order
  results.push(await checkConfigExists(projectRoot));
  results.push(await checkConfigSchemaValid(warnings));

 ... (clipped 11 lines)

Solution Walkthrough:

Before:

export async function runAllChecks(
  projectRoot: string,
  config: CLIConfig,
  warnings: ValidationWarning[]
): Promise<DoctorCheckResult[]> {
  const results: DoctorCheckResult[] = [];

  // Run checks in order, awaiting each one
  results.push(await checkConfigExists(projectRoot));
  results.push(await checkConfigSchemaValid(warnings));
  results.push(await checkTemplateDirExists(projectRoot, config));
  results.push(await checkMigrationDirExists(projectRoot, config));
  // ... and so on for all 10 checks
  results.push(await checkDatabaseConnection(config));
  results.push(await checkTemplateCount(projectRoot, config));

  return results;
}

After:

export async function runAllChecks(
  projectRoot: string,
  config: CLIConfig,
  warnings: ValidationWarning[]
): Promise<DoctorCheckResult[]> {
  const checkPromises = [
    checkConfigExists(projectRoot),
    checkConfigSchemaValid(warnings),
    checkTemplateDirExists(projectRoot, config),
    checkMigrationDirExists(projectRoot, config),
    checkTemplateDirReadable(projectRoot, config),
    checkMigrationDirWritable(projectRoot, config),
    checkBuildLogValid(projectRoot, config),
    checkLocalBuildLogValid(projectRoot, config),
    checkDatabaseConnection(config),
    checkTemplateCount(projectRoot, config),
  ];

  return Promise.all(checkPromises);
}
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies a performance bottleneck where independent, I/O-bound checks are run sequentially, and proposes a valid parallel execution strategy using Promise.all that would significantly speed up the doctor command.

Medium
General
Avoid using process.exit() directly
Suggestion Impact:The commit implemented the core suggestion by replacing process.exit(exitCode) with process.exitCode = exitCode. The implementation differs slightly from the suggestion - instead of removing the exitCode variable and setting process.exitCode conditionally, the commit kept the exitCode variable and assigned it to process.exitCode at the end. The commit also added explanatory comments about the change.

code diff:

-    process.exit(exitCode);
+    // Use process.exitCode to allow event loop to drain naturally
+    // This enables proper cleanup and makes testing easier
+    process.exitCode = exitCode;

Replace the direct process.exit() call in the doctor command's action handler
with process.exitCode to allow for graceful process termination and improve
testability.

src/commands/doctor.ts [51-91]

 export const doctorCommand = new Command('doctor')
   .description('Validate SRTD setup and configuration')
   .action(async () => {
-    let exitCode = 0;
-
     try {
       await renderBranding({ subtitle: 'Doctor' });
 
       const projectRoot = await findProjectRoot();
       const { config, warnings } = await getConfig(projectRoot);
 
       console.log();
       renderSeparator();
       console.log();
 
       // Run all checks
       const results = await runAllChecks(projectRoot, config, warnings);
 
       // Display results
       for (const result of results) {
         renderCheckResult(result);
       }
 
       console.log();
       renderSeparator();
 
       // Summary
       renderSummary(results);
 
       // Set exit code based on failures
       const hasFailures = results.some(r => !r.passed);
-      exitCode = hasFailures ? 1 : 0;
+      if (hasFailures) {
+        process.exitCode = 1;
+      }
     } catch (error) {
       console.log();
       console.log(chalk.red(`${figures.cross} Error running doctor:`));
       console.log(chalk.red(getErrorMessage(error)));
-      exitCode = 1;
+      process.exitCode = 1;
     }
-
-    process.exit(exitCode);
   });

[Suggestion processed]

Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies that using process.exit() is a bad practice that can interfere with test runners and proposes the modern process.exitCode alternative, which improves testability and robustness.

Medium
Possible issue
Fix incorrect writability check logic
Suggestion Impact:The commit directly implements the suggestion by adding a fileWritten flag, moving the unlink operation to a finally block with error handling, and ensuring cleanup failures don't affect the writability check result.

code diff:

+  let fileWritten = false;
   try {
     await fs.writeFile(testFile, 'test');
-    await fs.unlink(testFile);
+    fileWritten = true;
     return {
       name: 'Migration directory writable',
       passed: true,
@@ -138,6 +139,13 @@
       passed: false,
       message: error instanceof Error ? error.message : 'Cannot write to migration directory',
     };
+  } finally {
+    // Cleanup test file if it was written (ignore cleanup errors)
+    if (fileWritten) {
+      await fs.unlink(testFile).catch(() => {
+        // Intentionally ignore - cleanup failure doesn't affect writability result
+      });
+    }
   }

Refactor checkMigrationDirWritable to separate the write check from the file
cleanup. Use a finally block to ensure the test file is removed without
affecting the check's outcome.

src/utils/doctorChecks.ts [121-142]

 export async function checkMigrationDirWritable(
   projectRoot: string,
   config: CLIConfig
 ): Promise<DoctorCheckResult> {
   const migrationPath = path.join(projectRoot, config.migrationDir);
   const testFile = path.join(migrationPath, '.srtd-doctor-test');
+  let fileWritten = false;
 
   try {
     await fs.writeFile(testFile, 'test');
-    await fs.unlink(testFile);
+    fileWritten = true;
     return {
       name: 'Migration directory writable',
       passed: true,
     };
   } catch (error) {
     return {
       name: 'Migration directory writable',
       passed: false,
       message: error instanceof Error ? error.message : 'Cannot write to migration directory',
     };
+  } finally {
+    if (fileWritten) {
+      await fs.unlink(testFile).catch(() => {
+        /* ignore cleanup errors */
+      });
+    }
   }
 }

[Suggestion processed]

Suggestion importance[1-10]: 6

__

Why: The suggestion correctly identifies a logic bug where a failing cleanup operation (unlink) would incorrectly cause the writability check to fail, and it provides a robust fix using a finally block.

Low
  • Update

- Add srtd doctor to readme demo video
- Add missing tests for checkLocalBuildLogValid (invalid JSON/schema)
- Use SEPARATOR constant for consistent UI width
@t1mmen
Copy link
Owner Author

t1mmen commented Dec 29, 2025

@claude review please

@claude
Copy link

claude bot commented Dec 29, 2025

Claude encountered an error —— View job


I'll analyze this and get back to you.

@t1mmen
Copy link
Owner Author

t1mmen commented Dec 29, 2025

@claude review please

@claude

This comment was marked as outdated.

Address code review feedback: if write succeeds but cleanup fails,
the directory is still writable. Use finally block with ignored
cleanup errors to prevent incorrect failure reports.
@t1mmen
Copy link
Owner Author

t1mmen commented Dec 29, 2025

@claude review please - addressed writability check feedback

@claude

This comment was marked as outdated.

- Add E2E tests covering help, all 10 checks, exit code, summary
- Replace process.exit() with process.exitCode for graceful cleanup
- Add hint field to DoctorCheckResult for actionable suggestions
- Add actionable hints to all 10 doctor checks
- Add test for cleanup failure scenario (write succeeds, unlink fails)

Addresses Claude code review feedback on PR #60.
@t1mmen
Copy link
Owner Author

t1mmen commented Dec 29, 2025

/review

@qodo-free-for-open-source-projects
Copy link
Contributor

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 3 🔵🔵🔵⚪⚪
🧪 PR contains tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Race Condition Risk

The database connection check uses Promise.race() with a timeout, but doesn't cancel the ongoing connection attempt. If the timeout fires first, the connection promise continues running in the background, potentially causing resource leaks or unexpected behavior. Consider implementing proper cancellation or cleanup of the connection attempt when timeout occurs.

const timeoutPromise = new Promise<'timeout'>(resolve => {
  setTimeout(() => resolve('timeout'), timeoutMs);
});

const connectionPromise = db.testConnection();

const result = await Promise.race([connectionPromise, timeoutPromise]);

if (result === 'timeout') {
  return {
    name: 'Database connection',
    passed: false,
    message: `Connection timed out after ${timeoutMs}ms`,
    hint: 'Check if database is running: `supabase status` or `docker ps`',
  };
}
Exit Code Handling

The command sets process.exitCode but continues execution. While the comment mentions this allows event loop to drain, there's no guarantee that async cleanup completes before the process exits. Consider using explicit async cleanup or documenting the expected behavior more clearly.

  // Set exit code based on failures
  const hasFailures = results.some(r => !r.passed);
  exitCode = hasFailures ? 1 : 0;
} catch (error) {
  console.log();
  console.log(chalk.red(`${figures.cross} Error running doctor:`));
  console.log(chalk.red(getErrorMessage(error)));
  exitCode = 1;
}

// Use process.exitCode to allow event loop to drain naturally
// This enables proper cleanup and makes testing easier
process.exitCode = exitCode;
Silent Error Handling

In checkTemplateCount, the catch block at line 327 catches all errors without distinguishing between different failure modes (permissions vs. non-existent directory). This could mask the actual problem. Consider providing more specific error messages based on error type.

} catch {
  return {
    name: 'Template count',
    passed: false,
    message: `Cannot read template directory: ${config.templateDir}`,
    hint: 'Check directory permissions or ensure it exists',
  };
}

@t1mmen
Copy link
Owner Author

t1mmen commented Dec 29, 2025

@claude review

@claude
Copy link

claude bot commented Dec 29, 2025

Claude finished @t1mmen's task in 1m 58s —— View job


Code Review: srtd doctor Command

Tasks

  • Read CLAUDE.md for project context
  • Load srtd-dev skill
  • Load requesting-code-review skill
  • Review core implementation files
  • Review test files
  • Check bot feedback concerns
  • Provide comprehensive feedback

Summary

Great implementation overall! The srtd doctor command is well-structured with pure, testable check functions and comprehensive test coverage. You've properly addressed the writability check feedback from the bots. I found a few issues worth addressing:

Critical Issues

1. Database connection resource leak 🔴

Location: src/utils/doctorChecks.ts:258-299

The database connection promise continues running even after timeout, potentially leaking connections.

const result = await Promise.race([connectionPromise, timeoutPromise]);

if (result === 'timeout') {
  // connectionPromise is still running! db.dispose() happens in finally,
  // but the connection attempt may still be in progress
  return { ... };
}

Issue: When timeout fires, the connection promise keeps running in the background. The db.dispose() in the finally block may try to dispose a pool that's still initializing.

Suggested fix:

export async function checkDatabaseConnection(
  config: CLIConfig,
  timeoutMs = 5000
): Promise<DoctorCheckResult> {
  const db = DatabaseService.fromConfig(config);
  let timedOut = false;

  try {
    const timeoutPromise = new Promise<'timeout'>(resolve => {
      setTimeout(() => {
        timedOut = true;
        resolve('timeout');
      }, timeoutMs);
    });

    const result = await Promise.race([db.testConnection(), timeoutPromise]);

    if (result === 'timeout') {
      // Give a moment for connection cleanup before disposal
      await new Promise(resolve => setTimeout(resolve, 100));
      return {
        name: 'Database connection',
        passed: false,
        message: `Connection timed out after ${timeoutMs}ms`,
        hint: 'Check if database is running: `supabase status` or `docker ps`',
      };
    }

    // ... rest of logic
  } finally {
    // Ensure we give racing promise time to settle before disposal
    if (timedOut) {
      await new Promise(resolve => setTimeout(resolve, 50));
    }
    await db.dispose();
  }
}

High Priority

2. Parallel check execution for better performance 🟡

Location: src/utils/doctorChecks.ts:340-360

The bot suggestion to run checks in parallel is valid. Most checks are independent I/O operations that could run concurrently.

However, I'd recommend a hybrid approach instead of pure Promise.all:

export async function runAllChecks(
  projectRoot: string,
  config: CLIConfig,
  warnings: ValidationWarning[]
): Promise<DoctorCheckResult[]> {
  // Phase 1: Quick filesystem checks (parallel)
  const [
    configExistsResult,
    configSchemaResult,
    templateExistsResult,
    migrationExistsResult,
  ] = await Promise.all([
    checkConfigExists(projectRoot),
    checkConfigSchemaValid(warnings),
    checkTemplateDirExists(projectRoot, config),
    checkMigrationDirExists(projectRoot, config),
  ]);

  // Phase 2: Permission checks (parallel, but after existence checks)
  const [templateReadableResult, migrationWritableResult] = await Promise.all([
    checkTemplateDirReadable(projectRoot, config),
    checkMigrationDirWritable(projectRoot, config),
  ]);

  // Phase 3: File validation + DB + template count (parallel)
  const [buildLogResult, localBuildLogResult, dbResult, templateCountResult] = await Promise.all([
    checkBuildLogValid(projectRoot, config),
    checkLocalBuildLogValid(projectRoot, config),
    checkDatabaseConnection(config),
    checkTemplateCount(projectRoot, config),
  ]);

  // Return in display order
  return [
    configExistsResult,
    configSchemaResult,
    templateExistsResult,
    migrationExistsResult,
    templateReadableResult,
    migrationWritableResult,
    buildLogResult,
    localBuildLogResult,
    dbResult,
    templateCountResult,
  ];
}

Rationale: This gives you ~3x speedup while maintaining logical grouping and proper display order.

Medium Priority

3. Better error context for template count check 🟡

Location: src/utils/doctorChecks.ts:327-334

The catch-all error handler loses valuable debugging info:

} catch {  // ← error is swallowed
  return {
    name: 'Template count',
    passed: false,
    message: `Cannot read template directory: ${config.templateDir}`,
    hint: 'Check directory permissions or ensure it exists',
  };
}

Suggestion: Capture and use the error:

} catch (error) {
  const errorMsg = error instanceof Error ? error.message : 'Unknown error';
  return {
    name: 'Template count',
    passed: false,
    message: `Cannot read template directory: ${errorMsg}`,
    hint: 'Check directory permissions or ensure it exists',
  };
}

4. Writability check cleanup - good fix!

Location: src/utils/doctorChecks.ts:128-158

Your fix for the writability check is excellent and addresses the bot feedback perfectly. The finally block with proper error handling ensures cleanup doesn't affect the check result. Well done!

Low Priority / Style

5. Consistent error message format 🟢

Some checks use error.message directly, others provide custom messages. Consider standardizing:

  • src/utils/doctorChecks.ts:119 - Uses error.message
  • src/utils/doctorChecks.ts:147 - Uses error.message
  • src/utils/doctorChecks.ts:293 - Uses error.message

This is fine, but consider adding more context like: "Cannot read template directory: ${error.message}" for better UX.

6. Code style compliance

All code follows the project's Biome config perfectly:

  • Single quotes ✅
  • 2-space indent ✅
  • 100 char line width ✅
  • Proper semicolons ✅

Test Quality

Excellent test coverage! ✅

  • 29 unit tests covering all 10 checks with both pass/fail scenarios
  • E2E tests verify actual CLI execution
  • Edge cases well covered (missing files, timeouts, cleanup failures)
  • Mocking strategy is clean and effective

The test at src/__tests__/doctorChecks.test.ts:227-237 specifically tests the cleanup failure scenario - great work!

Documentation

Changeset ✅

Well-written, follows conventional commit format, appropriate minor version bump.

Bot Feedback Review

  1. Qodo compliance concerns - Mostly false positives:

    • "Connection string exposure" - Not an issue, error handling is appropriate
    • "Error message exposure" in doctor.ts:86 - This is a diagnostic tool, showing errors is expected
    • "Incomplete cleanup handling" - You fixed this! ✅
  2. Codecov: 67.76% patch coverage - The missing lines are mostly in doctor.ts presentation logic (rendering functions). This is acceptable for a diagnostic command.

Recommendations

  1. Must fix: Database connection timeout race condition (Issue Refactoring #1)
  2. Should consider: Parallel check execution for 2-3x speed improvement (Issue Update readme #2)
  3. Nice to have: Better error context in template count check (Issue CI, testing, prep for release #3)

Overall Assessment

This is a high-quality PR that adds genuine value. The diagnostic command is well-architected, thoroughly tested, and follows project conventions. The writability check fix shows you're responsive to feedback. Once the database timeout issue is addressed, this will be production-ready.

Recommendation: Approve after fixing the database connection race condition.


Capture setTimeout ID and clear it after Promise.race completes to
prevent timer callbacks from holding references after the function
returns. Also ensure cleanup in finally block for edge cases.
@t1mmen
Copy link
Owner Author

t1mmen commented Dec 29, 2025

@claude /review

@claude
Copy link

claude bot commented Dec 29, 2025

Claude finished @t1mmen's task in 1m 44s —— View job


Code Review: srtd doctor Command

Tasks

  • Read CLAUDE.md for project context
  • Load srtd-dev skill
  • Load requesting-code-review skill
  • Review core implementation files
  • Review test files
  • Check bot feedback concerns
  • Provide comprehensive feedback

Summary

Great implementation overall! The srtd doctor command is well-structured with pure, testable check functions and comprehensive test coverage. You've properly addressed the writability check feedback from the bots. I found a few issues worth addressing:

Critical Issues

1. Database connection resource leak 🔴

Location: src/utils/doctorChecks.ts:258-299

The database connection promise continues running even after timeout, potentially leaking connections.

const result = await Promise.race([connectionPromise, timeoutPromise]);

if (result === 'timeout') {
  // connectionPromise is still running! db.dispose() happens in finally,
  // but the connection attempt may still be in progress
  return { ... };
}

Issue: When timeout fires, the connection promise keeps running in the background. The db.dispose() in the finally block may try to dispose a pool that's still initializing.

Suggested fix:

export async function checkDatabaseConnection(
  config: CLIConfig,
  timeoutMs = 5000
): Promise<DoctorCheckResult> {
  const db = DatabaseService.fromConfig(config);
  let timedOut = false;

  try {
    const timeoutPromise = new Promise<'timeout'>(resolve => {
      setTimeout(() => {
        timedOut = true;
        resolve('timeout');
      }, timeoutMs);
    });

    const result = await Promise.race([db.testConnection(), timeoutPromise]);

    if (result === 'timeout') {
      // Give a moment for connection cleanup before disposal
      await new Promise(resolve => setTimeout(resolve, 100));
      return {
        name: 'Database connection',
        passed: false,
        message: `Connection timed out after ${timeoutMs}ms`,
        hint: 'Check if database is running: `supabase status` or `docker ps`',
      };
    }

    // ... rest of logic
  } finally {
    // Ensure we give racing promise time to settle before disposal
    if (timedOut) {
      await new Promise(resolve => setTimeout(resolve, 50));
    }
    await db.dispose();
  }
}

High Priority

2. Parallel check execution for better performance 🟡

Location: src/utils/doctorChecks.ts:340-360

The bot suggestion to run checks in parallel is valid. Most checks are independent I/O operations that could run concurrently.

However, I'd recommend a hybrid approach instead of pure Promise.all:

export async function runAllChecks(
  projectRoot: string,
  config: CLIConfig,
  warnings: ValidationWarning[]
): Promise<DoctorCheckResult[]> {
  // Phase 1: Quick filesystem checks (parallel)
  const [
    configExistsResult,
    configSchemaResult,
    templateExistsResult,
    migrationExistsResult,
  ] = await Promise.all([
    checkConfigExists(projectRoot),
    checkConfigSchemaValid(warnings),
    checkTemplateDirExists(projectRoot, config),
    checkMigrationDirExists(projectRoot, config),
  ]);

  // Phase 2: Permission checks (parallel, but after existence checks)
  const [templateReadableResult, migrationWritableResult] = await Promise.all([
    checkTemplateDirReadable(projectRoot, config),
    checkMigrationDirWritable(projectRoot, config),
  ]);

  // Phase 3: File validation + DB + template count (parallel)
  const [buildLogResult, localBuildLogResult, dbResult, templateCountResult] = await Promise.all([
    checkBuildLogValid(projectRoot, config),
    checkLocalBuildLogValid(projectRoot, config),
    checkDatabaseConnection(config),
    checkTemplateCount(projectRoot, config),
  ]);

  // Return in display order
  return [
    configExistsResult,
    configSchemaResult,
    templateExistsResult,
    migrationExistsResult,
    templateReadableResult,
    migrationWritableResult,
    buildLogResult,
    localBuildLogResult,
    dbResult,
    templateCountResult,
  ];
}

Rationale: This gives you ~3x speedup while maintaining logical grouping and proper display order.

Medium Priority

3. Better error context for template count check 🟡

Location: src/utils/doctorChecks.ts:327-334

The catch-all error handler loses valuable debugging info:

} catch {  // ← error is swallowed
  return {
    name: 'Template count',
    passed: false,
    message: `Cannot read template directory: ${config.templateDir}`,
    hint: 'Check directory permissions or ensure it exists',
  };
}

Suggestion: Capture and use the error:

} catch (error) {
  const errorMsg = error instanceof Error ? error.message : 'Unknown error';
  return {
    name: 'Template count',
    passed: false,
    message: `Cannot read template directory: ${errorMsg}`,
    hint: 'Check directory permissions or ensure it exists',
  };
}

4. Writability check cleanup - good fix!

Location: src/utils/doctorChecks.ts:128-158

Your fix for the writability check is excellent and addresses the bot feedback perfectly. The finally block with proper error handling ensures cleanup doesn't affect the check result. Well done!

Low Priority / Style

5. Consistent error message format 🟢

Some checks use error.message directly, others provide custom messages. Consider standardizing:

  • src/utils/doctorChecks.ts:119 - Uses error.message
  • src/utils/doctorChecks.ts:147 - Uses error.message
  • src/utils/doctorChecks.ts:293 - Uses error.message

This is fine, but consider adding more context like: "Cannot read template directory: ${error.message}" for better UX.

6. Code style compliance

All code follows the project's Biome config perfectly:

  • Single quotes ✅
  • 2-space indent ✅
  • 100 char line width ✅
  • Proper semicolons ✅

Test Quality

Excellent test coverage! ✅

  • 29 unit tests covering all 10 checks with both pass/fail scenarios
  • E2E tests verify actual CLI execution
  • Edge cases well covered (missing files, timeouts, cleanup failures)
  • Mocking strategy is clean and effective

The test at src/__tests__/doctorChecks.test.ts:227-237 specifically tests the cleanup failure scenario - great work!

Documentation

Changeset ✅

Well-written, follows conventional commit format, appropriate minor version bump.

Bot Feedback Review

  1. Qodo compliance concerns - Mostly false positives:

    • "Connection string exposure" - Not an issue, error handling is appropriate
    • "Error message exposure" in doctor.ts:86 - This is a diagnostic tool, showing errors is expected
    • "Incomplete cleanup handling" - You fixed this! ✅
  2. Codecov: 67.76% patch coverage - The missing lines are mostly in doctor.ts presentation logic (rendering functions). This is acceptable for a diagnostic command.

Recommendations

  1. Must fix: Database connection timeout race condition (Issue Refactoring #1)
  2. Should consider: Parallel check execution for 2-3x speed improvement (Issue Update readme #2)
  3. Nice to have: Better error context in template count check (Issue CI, testing, prep for release #3)

Overall Assessment

This is a high-quality PR that adds genuine value. The diagnostic command is well-architected, thoroughly tested, and follows project conventions. The writability check fix shows you're responsive to feedback. Once the database timeout issue is addressed, this will be production-ready.

Recommendation: Approve after fixing the database connection race condition.


Wait for connection promise to settle before disposing to prevent
pool.end() racing with pool.connect() when timeout fires first.
The previous fix to wait for connectionPromise to settle before dispose
caused unexplained E2E test failures in CI. The dispose() method already
handles cleanup gracefully with its own timeout, making the additional
wait unnecessary for practical purposes.

Updated doc comment to clarify the disposal behavior.
@t1mmen
Copy link
Owner Author

t1mmen commented Dec 30, 2025

@claude re-review please

1 similar comment
@t1mmen

This comment was marked as outdated.

@claude
Copy link

claude bot commented Dec 30, 2025

Claude encountered an error —— View job


I'll analyze this and get back to you.

@claude
Copy link

claude bot commented Dec 30, 2025

Claude finished @t1mmen's task in 2m 31s —— View job


Re-review: srtd doctor Command

Summary

Excellent progress since the last review! You've successfully addressed several key issues:

Fixed: Writability check cleanup logic (src/utils/doctorChecks.ts:128-158)
Fixed: Replaced process.exit() with process.exitCode (src/commands/doctor.ts:94-96)
Fixed: Added comprehensive E2E tests
Passing: All 528 tests pass, CI is green

What Changed Since Last Review

I reviewed commits 9e88adb through 8bfd7f2 and noticed:

  1. Database timeout race condition fix attempt (9e88adb) - You tried to fix the race condition I mentioned by waiting for the connection promise to settle before disposal. Good instinct!

  2. Revert of race condition fix (0496080) - You reverted it because it broke CI tests with fake timers. This was the right call - the fix was causing test hangs.

  3. Current state - The code relies on DatabaseService.dispose() having its own timeout protection (1000ms), which I verified it does have (src/services/DatabaseService.ts:391-419).

Current Assessment

The implementation is production-ready as-is. Here's why:

The "Race Condition" Is Actually Fine ✅

After deeper analysis, the database connection timeout handling is correct:

Why it's safe:

  • DatabaseService.dispose() already has timeout protection (1000ms) at src/services/DatabaseService.ts:404-411
  • The pool's end() method is race-safe - it can be called while connections are pending
  • The disposed flag prevents double-disposal (line 392-393)
  • Error suppression during shutdown prevents crashes (lines 398-416)

The current code:

const result = await Promise.race([connectionPromise, timeoutPromise]);
// ... handle timeout result ...
finally {
  await db.dispose(); // ← This is safe even if connectionPromise is still running
}

This is the correct pattern for this use case. Your instinct to fix it was good, but the revert was the right decision.


Remaining Opportunities (Optional)

These are nice-to-haves, not blockers:

1. Performance: Parallel Check Execution 🟡

Impact: Medium (2-3x speed improvement)
Location: src/utils/doctorChecks.ts:355-375

The checks run sequentially, but most are independent I/O operations. Consider this hybrid approach:

export async function runAllChecks(
  projectRoot: string,
  config: CLIConfig,
  warnings: ValidationWarning[]
): Promise<DoctorCheckResult[]> {
  // Phase 1: Quick filesystem checks (parallel)
  const [
    configExistsResult,
    configSchemaResult,
    templateExistsResult,
    migrationExistsResult,
  ] = await Promise.all([
    checkConfigExists(projectRoot),
    checkConfigSchemaValid(warnings),
    checkTemplateDirExists(projectRoot, config),
    checkMigrationDirExists(projectRoot, config),
  ]);

  // Phase 2: Permission checks (parallel)
  const [templateReadableResult, migrationWritableResult] = await Promise.all([
    checkTemplateDirReadable(projectRoot, config),
    checkMigrationDirWritable(projectRoot, config),
  ]);

  // Phase 3: Everything else (parallel)
  const [buildLogResult, localBuildLogResult, dbResult, templateCountResult] = await Promise.all([
    checkBuildLogValid(projectRoot, config),
    checkLocalBuildLogValid(projectRoot, config),
    checkDatabaseConnection(config),
    checkTemplateCount(projectRoot, config),
  ]);

  // Return in display order
  return [
    configExistsResult,
    configSchemaResult,
    templateExistsResult,
    migrationExistsResult,
    templateReadableResult,
    migrationWritableResult,
    buildLogResult,
    localBuildLogResult,
    dbResult,
    templateCountResult,
  ];
}

Why this approach:

  • Maintains logical grouping and display order
  • ~3x faster execution (most checks complete in <50ms, DB check takes 100-5000ms)
  • Still safe - no dependencies between phases

2. Error Context: Template Count Check 🟢

Impact: Low (better debugging)
Location: src/utils/doctorChecks.ts:342-348

Currently swallows the error:

} catch {  // ← Error details lost
  return {
    name: 'Template count',
    passed: false,
    message: `Cannot read template directory: ${config.templateDir}`,

Suggestion:

} catch (error) {
  const errorMsg = error instanceof Error ? error.message : 'Unknown error';
  return {
    name: 'Template count',
    passed: false,
    message: `Cannot read template directory: ${errorMsg}`,
    hint: 'Check directory permissions or ensure it exists',
  };
}

This helps users diagnose issues like "EACCES: permission denied" vs "ENOENT: no such file".


Code Quality Assessment

✅ Strengths

  1. Pure, testable functions - Each check is isolated and mockable
  2. Comprehensive test coverage - 29 unit tests + 4 E2E tests covering all scenarios
  3. Excellent UX - Clear output, helpful hints, proper exit codes
  4. Project consistency - Follows Biome style, uses existing patterns
  5. Good error handling - Graceful degradation, no crashes
  6. Smart cleanup logic - Writability check properly separates write success from cleanup

📊 Test Quality

  • Unit tests: 29 tests covering all 10 checks with pass/fail scenarios
  • E2E tests: Actual CLI execution verification
  • Edge cases: Timeouts, missing files, cleanup failures all tested
  • Coverage: 68.78% patch coverage (missing lines are presentation logic in doctor.ts)

The low patch coverage in doctor.ts is acceptable - it's mostly rendering code that's covered by E2E tests.


Final Verdict

Status:APPROVED - Ready to Merge

This PR delivers a valuable diagnostic tool that will help users troubleshoot setup issues. The code is well-tested, follows project conventions, and handles edge cases gracefully.

What you did well:

  • Listened to bot feedback and fixed the writability check properly
  • Replaced process.exit() with process.exitCode for testability
  • Attempted to fix the race condition, but correctly reverted when tests broke
  • Added comprehensive E2E tests with good documentation

Optional improvements for follow-up:

  1. Parallel check execution for better performance
  2. Better error messages in template count check

But these are not blockers - the current implementation is solid.


Recommendation: ✅ Merge as-is or apply optional improvements

The template directory was removed in main but is needed for the doctor
command E2E tests to pass (checkTemplateCount requires at least one template).
@t1mmen t1mmen force-pushed the feat/config-doctor branch from 8bfd7f2 to b857e27 Compare December 30, 2025 17:24
Capture and display the actual error message instead of a generic
message, improving debugging when the template directory check fails.
@t1mmen t1mmen merged commit 79f8254 into main Dec 30, 2025
4 of 6 checks passed
@t1mmen t1mmen deleted the feat/config-doctor branch December 30, 2025 17:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant