| import { readFileSync, existsSync, mkdirSync, writeFileSync, readdirSync, statSync, unlinkSync } from 'fs'; | ||
| import { join } from 'path'; | ||
| import { execSync } from 'child_process'; | ||
| import { join, resolve, isAbsolute, relative } from 'path'; | ||
| import { execFileSync } from 'child_process'; | ||
| import { tmpdir } from 'os'; | ||
| import { readJsonSafe } from '../utils/json-safe.js'; | ||
| const MMDC_TIMEOUT_MS = 60_000; | ||
| export default async function exportDiagrams(args) { | ||
@@ -44,5 +46,11 @@ const { default: chalk } = await import('chalk'); | ||
| const diagramsDir = customOutput | ||
| ? join(process.cwd(), customOutput) | ||
| : join(sddPath, 'diagrams'); | ||
| let diagramsDir; | ||
| try { | ||
| diagramsDir = customOutput | ||
| ? resolveOutputDir(customOutput) | ||
| : join(sddPath, 'diagrams'); | ||
| } catch (err) { | ||
| console.error(chalk.red(`\n ${err.message}\n`)); | ||
| process.exit(1); | ||
| } | ||
@@ -90,7 +98,8 @@ mkdirSync(diagramsDir, { recursive: true }); | ||
| writeFileSync(tmpFile, diagram, 'utf8'); | ||
| execSync(`${mmdc} -i "${tmpFile}" -o "${outPath}"`, { stdio: 'pipe' }); | ||
| runMmdc(mmdc, ['-i', tmpFile, '-o', outPath]); | ||
| spin.succeed(chalk.hex('#ffa203')(` ✓ ${outName}`)); | ||
| success++; | ||
| } catch (err) { | ||
| spin.fail(chalk.red(` ✗ ${outName} — ${err.stderr?.toString().split('\n')[0] || err.message}`)); | ||
| const detail = err.stderr?.toString().split('\n')[0] || err.message; | ||
| spin.fail(chalk.red(` ✗ ${outName}: ${detail}`)); | ||
| failed++; | ||
@@ -108,11 +117,56 @@ } finally { | ||
| function resolveOutputDir(customOutput) { | ||
| if (/[\n\r\0]/.test(customOutput)) { | ||
| throw new Error('Invalid --output: contains control characters'); | ||
| } | ||
| const target = isAbsolute(customOutput) | ||
| ? resolve(customOutput) | ||
| : resolve(process.cwd(), customOutput); | ||
| const rel = relative(process.cwd(), target); | ||
| if (rel.startsWith('..') || isAbsolute(rel)) { | ||
| throw new Error(`Invalid --output: path must stay inside the project (${target})`); | ||
| } | ||
| return target; | ||
| } | ||
| function runMmdc(mmdc, extraArgs) { | ||
| execFileSync(mmdc.command, [...mmdc.prefixArgs, ...extraArgs], { | ||
| stdio: 'pipe', | ||
| shell: mmdc.shell, | ||
| timeout: MMDC_TIMEOUT_MS, | ||
| cwd: process.cwd(), | ||
| windowsHide: true, | ||
| }); | ||
| } | ||
| function findMmdc() { | ||
| const localJs = join(process.cwd(), 'node_modules', '@mermaid-js', 'mermaid-cli', 'src', 'cli.js'); | ||
| if (existsSync(localJs)) { | ||
| return { command: process.execPath, prefixArgs: [localJs], shell: false }; | ||
| } | ||
| const localBin = join(process.cwd(), 'node_modules', '.bin', 'mmdc'); | ||
| if (process.platform === 'win32') { | ||
| const localCmd = localBin + '.cmd'; | ||
| if (existsSync(localCmd)) { | ||
| return { command: localCmd, prefixArgs: [], shell: true }; | ||
| } | ||
| } | ||
| if (existsSync(localBin)) { | ||
| return { command: localBin, prefixArgs: [], shell: false }; | ||
| } | ||
| try { | ||
| execSync('mmdc --version', { stdio: 'pipe' }); | ||
| return 'mmdc'; | ||
| const finder = process.platform === 'win32' ? 'where' : 'which'; | ||
| const out = execFileSync(finder, ['mmdc'], { stdio: ['ignore', 'pipe', 'ignore'] }) | ||
| .toString().trim().split(/\r?\n/)[0]; | ||
| if (out && existsSync(out)) { | ||
| const isWinScript = process.platform === 'win32' && /\.(cmd|bat)$/i.test(out); | ||
| return { command: out, prefixArgs: [], shell: isWinScript }; | ||
| } | ||
| } catch {} | ||
| const local = join(process.cwd(), 'node_modules', '.bin', 'mmdc'); | ||
| if (existsSync(local)) return `"${local}"`; | ||
| return null; | ||
@@ -119,0 +173,0 @@ } |
| import { existsSync } from 'fs'; | ||
| import { join } from 'path'; | ||
| import { execSync } from 'child_process'; | ||
| import { execFileSync } from 'child_process'; | ||
@@ -126,7 +126,6 @@ export const ENGINES = [ | ||
| function commandExists(cmd) { | ||
| if (!/^[a-zA-Z0-9_-]+$/.test(cmd)) return false; | ||
| try { | ||
| execSync( | ||
| process.platform === 'win32' ? `where ${cmd}` : `which ${cmd}`, | ||
| { stdio: 'pipe' } | ||
| ); | ||
| const finder = process.platform === 'win32' ? 'where' : 'which'; | ||
| execFileSync(finder, [cmd], { stdio: 'pipe' }); | ||
| return true; | ||
@@ -133,0 +132,0 @@ } catch { |
+2
-2
| { | ||
| "name": "reversa", | ||
| "version": "1.2.37", | ||
| "version": "1.2.38", | ||
| "description": "Transform legacy systems into executable specifications for AI coding agents", | ||
@@ -39,3 +39,3 @@ "bin": { | ||
| "engines": { | ||
| "node": ">=18.0.0" | ||
| "node": ">=18.20.2" | ||
| }, | ||
@@ -42,0 +42,0 @@ "dependencies": { |
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Found 1 instance in 1 package
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Found 1 instance in 1 package
630021
0.28%2442
1.92%