feat: Add cross-platform detection (Claude Code + OpenCode support)#16
feat: Add cross-platform detection (Claude Code + OpenCode support)#16Steffen025 wants to merge 1 commit intojcfischer:mainfrom
Conversation
- 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
jcfischer
left a comment
There was a problem hiding this comment.
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.
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)detectPlatform()getRootDir()~/.claudeor~/.opencode)getPlatformInfo()resolvePath()isOpenCode()/isClaudeCode()packages/specflow/tests/lib/platform.test.ts(303 lines)Detection Priority
PAI_DIRenvironment variable (explicit override)OPENCODE_DIRenvironment variable (OpenCode specific)~/.claudevs~/.opencode)Usage Example
Testing
Contributed by @Steffen025 + Jeremy (OpenCode/Claude Opus)