|
| 1 | +#!/usr/bin/env node |
| 2 | +/** |
| 3 | + * Check for Hardcoded Platform Paths |
| 4 | + * Scans plugins for hardcoded .claude/, .opencode/, .codex/ paths |
| 5 | + * |
| 6 | + * CRITICAL: Per CLAUDE.md cross-platform requirement - all 3 platforms must work |
| 7 | + * |
| 8 | + * Usage: node scripts/check-hardcoded-paths.js |
| 9 | + * Exit code: 0 if clean, 1 if issues found |
| 10 | + * |
| 11 | + * @author Avi Fenesh |
| 12 | + * @license MIT |
| 13 | + */ |
| 14 | + |
| 15 | +const fs = require('fs'); |
| 16 | +const path = require('path'); |
| 17 | + |
| 18 | +const REPO_ROOT = path.resolve(__dirname, '..'); |
| 19 | + |
| 20 | +// Patterns to detect hardcoded platform paths |
| 21 | +const HARDCODED_PATTERNS = [ |
| 22 | + { |
| 23 | + pattern: /\.claude\/(?!.*\(example\)|.*for example|.*Platform|.*State directory)/, |
| 24 | + platform: '.claude/', |
| 25 | + severity: 'ERROR' |
| 26 | + }, |
| 27 | + { |
| 28 | + pattern: /\.opencode\/(?!.*\(example\)|.*for example|.*Platform|.*State directory)/, |
| 29 | + platform: '.opencode/', |
| 30 | + severity: 'ERROR' |
| 31 | + }, |
| 32 | + { |
| 33 | + pattern: /\.codex\/(?!.*\(example\)|.*for example|.*Platform|.*State directory)/, |
| 34 | + platform: '.codex/', |
| 35 | + severity: 'ERROR' |
| 36 | + } |
| 37 | +]; |
| 38 | + |
| 39 | +// Files to exclude from checks (docs, examples, research) |
| 40 | +const EXCLUDE_PATTERNS = [ |
| 41 | + /RESEARCH\.md$/, |
| 42 | + /CLAUDE\.md$/, |
| 43 | + /AGENTS\.md$/, |
| 44 | + /README\.md$/, |
| 45 | + /INSTALLATION\.md$/, |
| 46 | + /CROSS_PLATFORM\.md$/, |
| 47 | + /ARCHITECTURE\.md$/, |
| 48 | + /MCP-TOOLS\.md$/, |
| 49 | + /examples?\//, |
| 50 | + /\.git\//, |
| 51 | + /node_modules\//, |
| 52 | + /__tests__\//, |
| 53 | + /\.json$/, |
| 54 | + // Enhance skills document platform differences - OK to have hardcoded paths in docs |
| 55 | + /plugins\/enhance\/skills\/.*\/SKILL\.md$/ |
| 56 | +]; |
| 57 | + |
| 58 | +// Lines that are OK to have hardcoded paths (documentation examples) |
| 59 | +const SAFE_CONTEXTS = [ |
| 60 | + 'State stored in', |
| 61 | + 'State directory:', |
| 62 | + 'example', |
| 63 | + 'Example', |
| 64 | + 'Platform', |
| 65 | + '| State Dir |', |
| 66 | + 'State Dir |', |
| 67 | + 'detected by', |
| 68 | + 'Detected by', |
| 69 | + 'Override with', |
| 70 | + 'Personal:', |
| 71 | + 'Project:', |
| 72 | + 'User settings', |
| 73 | + 'Project settings', |
| 74 | + 'Local settings', |
| 75 | + '| Claude Code |', |
| 76 | + '| OpenCode |', |
| 77 | + '| Codex |', |
| 78 | + 'Claude Code:', |
| 79 | + 'OpenCode:', |
| 80 | + 'Codex CLI:', |
| 81 | + 'Codex:', |
| 82 | + "Don't hardcode", |
| 83 | + 'Support ', |
| 84 | + '~/.claude/', |
| 85 | + '~/.opencode/', |
| 86 | + '~/.codex/', |
| 87 | + 'MCP in', |
| 88 | + 'or `~/', |
| 89 | + '$CLAUDE_PROJECT_DIR' |
| 90 | +]; |
| 91 | + |
| 92 | +function shouldExcludeFile(filePath) { |
| 93 | + // Normalize path separators for cross-platform regex matching |
| 94 | + const normalizedPath = filePath.replace(/\\/g, '/'); |
| 95 | + return EXCLUDE_PATTERNS.some(pattern => pattern.test(normalizedPath)); |
| 96 | +} |
| 97 | + |
| 98 | +function isSafeContext(line) { |
| 99 | + return SAFE_CONTEXTS.some(ctx => line.includes(ctx)); |
| 100 | +} |
| 101 | + |
| 102 | +function scanFile(filePath) { |
| 103 | + const content = fs.readFileSync(filePath, 'utf8'); |
| 104 | + const lines = content.split('\n'); |
| 105 | + const issues = []; |
| 106 | + |
| 107 | + lines.forEach((line, index) => { |
| 108 | + // Skip if line is in safe context |
| 109 | + if (isSafeContext(line)) { |
| 110 | + return; |
| 111 | + } |
| 112 | + |
| 113 | + HARDCODED_PATTERNS.forEach(({ pattern, platform, severity }) => { |
| 114 | + if (pattern.test(line)) { |
| 115 | + issues.push({ |
| 116 | + file: path.relative(REPO_ROOT, filePath), |
| 117 | + line: index + 1, |
| 118 | + platform, |
| 119 | + severity, |
| 120 | + content: line.trim() |
| 121 | + }); |
| 122 | + } |
| 123 | + }); |
| 124 | + }); |
| 125 | + |
| 126 | + return issues; |
| 127 | +} |
| 128 | + |
| 129 | +function scanDirectory(dir, issues = []) { |
| 130 | + const entries = fs.readdirSync(dir, { withFileTypes: true }); |
| 131 | + |
| 132 | + for (const entry of entries) { |
| 133 | + const fullPath = path.join(dir, entry.name); |
| 134 | + |
| 135 | + if (shouldExcludeFile(fullPath)) { |
| 136 | + continue; |
| 137 | + } |
| 138 | + |
| 139 | + if (entry.isDirectory()) { |
| 140 | + scanDirectory(fullPath, issues); |
| 141 | + } else if (entry.isFile() && entry.name.endsWith('.md')) { |
| 142 | + const fileIssues = scanFile(fullPath); |
| 143 | + issues.push(...fileIssues); |
| 144 | + } |
| 145 | + } |
| 146 | + |
| 147 | + return issues; |
| 148 | +} |
| 149 | + |
| 150 | +function formatIssue(issue) { |
| 151 | + return `${issue.severity}: ${issue.file}:${issue.line} |
| 152 | + Hardcoded path: ${issue.platform} |
| 153 | + Line: ${issue.content} |
| 154 | + Fix: Use platform-aware state directory variable (${issue.platform === '.claude/' ? 'STATE_DIR or stateDir' : 'stateDir'}) |
| 155 | +`; |
| 156 | +} |
| 157 | + |
| 158 | +// Main execution |
| 159 | +if (require.main === module) { |
| 160 | + console.log('[OK] Scanning for hardcoded platform paths...\n'); |
| 161 | + |
| 162 | + const pluginsDir = path.join(REPO_ROOT, 'plugins'); |
| 163 | + const issues = scanDirectory(pluginsDir); |
| 164 | + |
| 165 | + if (issues.length === 0) { |
| 166 | + console.log('[OK] No hardcoded platform paths found\n'); |
| 167 | + console.log('All files use platform-aware state directory variables.'); |
| 168 | + process.exit(0); |
| 169 | + } |
| 170 | + |
| 171 | + console.error(`[ERROR] Found ${issues.length} hardcoded platform path(s):\n`); |
| 172 | + |
| 173 | + issues.forEach(issue => { |
| 174 | + console.error(formatIssue(issue)); |
| 175 | + }); |
| 176 | + |
| 177 | + console.error(` |
| 178 | +CLAUDE.md Critical Rule: |
| 179 | +> 3 platforms: Claude Code + OpenCode + Codex - ALL must work |
| 180 | +
|
| 181 | +Fix guide: |
| 182 | +1. Replace hardcoded paths with platform-aware variables: |
| 183 | + - In agents/commands: Use workflowState.getStateDir() |
| 184 | + - In prompts: Use \${stateDir} or \${STATE_DIR} template variables |
| 185 | +
|
| 186 | +2. Examples: |
| 187 | + BAD: await Task({ prompt: "State file: .claude/flow.json" }); |
| 188 | + GOOD: await Task({ prompt: \`State file: \${stateDir}/flow.json\` }); |
| 189 | +
|
| 190 | + BAD: State files in .claude/tasks.json |
| 191 | + GOOD: State files in {stateDir}/tasks.json |
| 192 | +
|
| 193 | +See: checklists/cross-platform-compatibility.md |
| 194 | +`); |
| 195 | + |
| 196 | + process.exit(1); |
| 197 | +} |
| 198 | + |
| 199 | +module.exports = { scanDirectory, scanFile, HARDCODED_PATTERNS }; |
0 commit comments