Skip to content

feat: Add cross-platform detection (Claude Code + OpenCode support)#16

Open
Steffen025 wants to merge 1 commit intojcfischer:mainfrom
Steffen025:feature/platform-detection
Open

feat: Add cross-platform detection (Claude Code + OpenCode support)#16
Steffen025 wants to merge 1 commit intojcfischer:mainfrom
Steffen025:feature/platform-detection

Conversation

@Steffen025
Copy link

Summary

Adds platform detection module enabling SpecFlow to work on both Claude Code and OpenCode platforms without hardcoded paths.

Reference: This addresses the platform detection feature discussed in #8 (SpecFirst Integration Proposal), which @jcfischer welcomed for contribution.

What's Included

packages/specflow/src/lib/platform.ts (172 lines)

Function Purpose
detectPlatform() Detect Claude Code vs OpenCode via env vars or directory structure
getRootDir() Get platform root directory (~/.claude or ~/.opencode)
getPlatformInfo() Complete platform info including standard directories
resolvePath() Resolve relative paths to absolute based on platform
isOpenCode() / isClaudeCode() Platform check helpers

packages/specflow/tests/lib/platform.test.ts (303 lines)

  • 28 tests covering all functions
  • Environment variable scenarios
  • Path resolution edge cases
  • All tests passing

Detection Priority

  1. PAI_DIR environment variable (explicit override)
  2. OPENCODE_DIR environment variable (OpenCode specific)
  3. Directory structure detection (~/.claude vs ~/.opencode)
  4. Error if neither found

Usage Example

import { getRootDir, isOpenCode } from "./lib/platform";

// Instead of hardcoded:
// const envPath = `${homedir()}/.claude/.env`;

// Platform-agnostic:
const envPath = `${getRootDir()}/.env`;

// Or check platform:
if (isOpenCode()) {
  // OpenCode-specific logic
}

Testing

cd packages/specflow
bun test tests/lib/platform.test.ts
# ✓ 28 pass, 0 fail

Contributed by @Steffen025 + Jeremy (OpenCode/Claude Opus)

- Add platform.ts for detecting Claude Code vs OpenCode
- Supports PAI_DIR and OPENCODE_DIR environment variables
- Provides platform-agnostic path resolution
- Comprehensive test suite with 28 tests, all passing

Resolves jcfischer#8
Copy link
Owner

@jcfischer jcfischer left a comment

Choose a reason for hiding this comment

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

Code Review: PR #16 — Cross-Platform Detection

Overall Assessment

Well-structured module with clean API design and good test coverage. However, there are several issues that should be addressed before merging — one security concern, one architectural observation, and several code quality items.


🔴 Must Fix

1. Path Traversal in resolvePath and directory helpers (Security)

File: packages/specflow/src/lib/platform.ts lines 107-120, 131, 140, 149

resolvePath("../../etc/passwd") resolves to a path outside the root directory. The same applies to getFeatureDir("../../../etc"), getProjectDir(), and getSpecsDir() — all pass unsanitized input into template literal path construction.

Fix: Use path.resolve() and validate the result stays within the expected root:

import { resolve, join } from "path";

export function resolvePath(relativePath: string): string {
  if (relativePath.startsWith("/")) return relativePath;
  if (relativePath.startsWith("~/")) return join(homedir(), relativePath.slice(2));
  
  const rootDir = getRootDir();
  const resolved = resolve(rootDir, relativePath);
  if (!resolved.startsWith(rootDir)) {
    throw new Error(`Path "${relativePath}" resolves outside root directory`);
  }
  return resolved;
}

2. Use path.join() instead of template literal concatenation (Correctness)

File: packages/specflow/src/lib/platform.ts — all path construction lines

Every path is built with template literals (\${rootDir}/${relativePath}``). This:

  • Doesn't normalize paths (double slashes, . segments)
  • Breaks on Windows Node.js environments
  • Is the root cause of the traversal issue above

Replace all instances with path.join():

// Before
return `${rootDir}/${relativePath}`;

// After  
return join(rootDir, relativePath);

🟡 Should Fix

3. Test process.env replacement pattern is fragile

File: packages/specflow/tests/lib/platform.test.ts line 42

afterEach(() => {
  process.env = { ...originalEnv };
});

Replacing the entire process.env object reference can cause subtle issues — Node.js/Bun maintains an internal reference to the original object. Better pattern:

afterEach(() => {
  // Restore only what we changed
  delete process.env.PAI_DIR;
  delete process.env.OPENCODE_DIR;
  // Restore any originals
  if (originalEnv.PAI_DIR) process.env.PAI_DIR = originalEnv.PAI_DIR;
  if (originalEnv.OPENCODE_DIR) process.env.OPENCODE_DIR = originalEnv.OPENCODE_DIR;
});

4. Two tests are effectively no-ops

File: packages/specflow/tests/lib/platform.test.ts

  • Line 73 — "should detect claudecode from directory structure": Asserts expect(["claudecode", "opencode", "unknown"]).toContain(result) which passes for ANY return value. This test cannot fail.

  • Line 99-108 — "should throw error when platform is unknown": The try/catch accepts both throwing and not throwing. On any machine with ~/.claude, this test passes without exercising the error path.

Both should mock existsSync to control filesystem detection, or be marked as integration tests that document their machine dependency.


🟢 Observations

5. Module is dead code — no integration with existing codebase

The PR adds the platform module but nothing in the existing codebase imports it. There are ~15 hardcoded ~/.claude references in eval.ts, model-based.ts, and migrate-registry.ts that this module is presumably meant to replace.

Is integration planned for a follow-up PR? If so, documenting that plan (e.g., a GitHub issue) would be helpful. As standalone dead code, this module adds maintenance burden without value.

6. Ambiguous detection when both platforms exist

When both ~/.claude AND ~/.opencode exist on the same machine, detectPlatform() silently prefers Claude Code (line 52 checks first). This is reasonable but should be documented in the JSDoc — OpenCode users who also have Claude installed might be surprised.

7. PAI_DIR"claudecode" mapping is misleading

PAI_DIR stands for "Personal AI Directory" — it's a general override, not Claude-specific. But detectPlatform() maps it to "claudecode" unconditionally. If an OpenCode user sets PAI_DIR to their OpenCode directory, detectPlatform() returns "claudecode". Consider: should PAI_DIR return a platform-agnostic result, or should the mapping be based on the directory path?


Summary

Category Count Items
🔴 Must Fix 2 Path traversal, string concatenation paths
🟡 Should Fix 2 Test env pattern, no-op tests
🟢 Observations 3 Dead code, ambiguous detection, PAI_DIR mapping

Recommendation: Request changes — the path traversal and path.join issues should be resolved before merge. The module design is sound and the contribution is welcome, but needs these safety and correctness improvements.

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.

2 participants