mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-01 08:13:37 +00:00
feat: implement secure file system access and path validation
- Introduced a restricted file system wrapper to ensure all file operations are confined to the script's directory, enhancing security. - Updated various modules to utilize the new secure file system methods, replacing direct fs calls with validated operations. - Enhanced path validation in the server routes and context loaders to prevent unauthorized access to the file system. - Adjusted environment variable handling to use centralized methods for reading and writing API keys, ensuring consistent security practices. This change improves the overall security posture of the application by enforcing strict file access controls and validating paths before any operations are performed.
This commit is contained in:
@@ -55,3 +55,63 @@ export {
|
||||
type NodeFinderResult,
|
||||
type NodeFinderOptions,
|
||||
} from './node-finder.js';
|
||||
|
||||
// System paths for tool detection (GitHub CLI, Claude CLI, Node.js, etc.)
|
||||
export * as systemPaths from './system-paths.js';
|
||||
export {
|
||||
// CLI tool paths
|
||||
getGitHubCliPaths,
|
||||
getClaudeCliPaths,
|
||||
getClaudeConfigDir,
|
||||
getClaudeCredentialPaths,
|
||||
getClaudeSettingsPath,
|
||||
getClaudeStatsCachePath,
|
||||
getClaudeProjectsDir,
|
||||
getShellPaths,
|
||||
getExtendedPath,
|
||||
// Node.js paths
|
||||
getNvmPaths,
|
||||
getFnmPaths,
|
||||
getNodeSystemPaths,
|
||||
getScoopNodePath,
|
||||
getChocolateyNodePath,
|
||||
getWslVersionPath,
|
||||
// System path operations
|
||||
systemPathExists,
|
||||
systemPathAccess,
|
||||
systemPathIsExecutable,
|
||||
systemPathReadFile,
|
||||
systemPathReadFileSync,
|
||||
systemPathWriteFileSync,
|
||||
systemPathReaddir,
|
||||
systemPathReaddirSync,
|
||||
systemPathStatSync,
|
||||
systemPathStat,
|
||||
isAllowedSystemPath,
|
||||
// High-level methods
|
||||
findFirstExistingPath,
|
||||
findGitHubCliPath,
|
||||
findClaudeCliPath,
|
||||
getClaudeAuthIndicators,
|
||||
type ClaudeAuthIndicators,
|
||||
// Electron userData operations
|
||||
setElectronUserDataPath,
|
||||
getElectronUserDataPath,
|
||||
isElectronUserDataPath,
|
||||
electronUserDataReadFileSync,
|
||||
electronUserDataWriteFileSync,
|
||||
electronUserDataExists,
|
||||
// Script directory operations
|
||||
setScriptBaseDir,
|
||||
getScriptBaseDir,
|
||||
scriptDirExists,
|
||||
scriptDirMkdirSync,
|
||||
scriptDirCreateWriteStream,
|
||||
// Electron app bundle operations
|
||||
setElectronAppPaths,
|
||||
electronAppExists,
|
||||
electronAppReadFileSync,
|
||||
electronAppStatSync,
|
||||
electronAppStat,
|
||||
electronAppReadFile,
|
||||
} from './system-paths.js';
|
||||
|
||||
@@ -3,12 +3,25 @@
|
||||
*
|
||||
* Handles finding Node.js when the app is launched from desktop environments
|
||||
* (macOS Finder, Windows Explorer, Linux desktop) where PATH may be limited.
|
||||
*
|
||||
* Uses centralized system-paths module for all file system access.
|
||||
*/
|
||||
|
||||
import { execSync } from 'child_process';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
import {
|
||||
systemPathExists,
|
||||
systemPathIsExecutable,
|
||||
systemPathReaddirSync,
|
||||
systemPathReadFileSync,
|
||||
getNvmPaths,
|
||||
getFnmPaths,
|
||||
getNodeSystemPaths,
|
||||
getScoopNodePath,
|
||||
getChocolateyNodePath,
|
||||
getWslVersionPath,
|
||||
} from './system-paths.js';
|
||||
|
||||
/** Pattern to match version directories (e.g., "v18.17.0", "18.17.0", "v18") */
|
||||
const VERSION_DIR_PATTERN = /^v?\d+/;
|
||||
@@ -45,18 +58,11 @@ export interface NodeFinderOptions {
|
||||
|
||||
/**
|
||||
* Check if a file exists and is executable
|
||||
* On Windows, only checks existence (X_OK is not meaningful)
|
||||
* Uses centralized systemPathIsExecutable for path validation
|
||||
*/
|
||||
function isExecutable(filePath: string): boolean {
|
||||
try {
|
||||
if (process.platform === 'win32') {
|
||||
// On Windows, fs.constants.X_OK is not meaningful - just check existence
|
||||
fs.accessSync(filePath, fs.constants.F_OK);
|
||||
} else {
|
||||
// On Unix-like systems, check for execute permission
|
||||
fs.accessSync(filePath, fs.constants.X_OK);
|
||||
}
|
||||
return true;
|
||||
return systemPathIsExecutable(filePath);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
@@ -71,11 +77,14 @@ function findNodeFromVersionManager(
|
||||
basePath: string,
|
||||
binSubpath: string = 'bin/node'
|
||||
): string | null {
|
||||
if (!fs.existsSync(basePath)) return null;
|
||||
try {
|
||||
if (!systemPathExists(basePath)) return null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const allVersions = fs
|
||||
.readdirSync(basePath)
|
||||
const allVersions = systemPathReaddirSync(basePath)
|
||||
.filter((v) => VERSION_DIR_PATTERN.test(v))
|
||||
// Semantic version sort - newest first using localeCompare with numeric option
|
||||
.sort((a, b) => b.localeCompare(a, undefined, { numeric: true, sensitivity: 'base' }));
|
||||
@@ -101,39 +110,30 @@ function findNodeFromVersionManager(
|
||||
/**
|
||||
* Find Node.js on macOS
|
||||
*/
|
||||
function findNodeMacOS(homeDir: string): NodeFinderResult | null {
|
||||
// Check Homebrew paths in order of preference
|
||||
const homebrewPaths = [
|
||||
// Apple Silicon
|
||||
'/opt/homebrew/bin/node',
|
||||
// Intel
|
||||
'/usr/local/bin/node',
|
||||
];
|
||||
|
||||
for (const nodePath of homebrewPaths) {
|
||||
function findNodeMacOS(_homeDir: string): NodeFinderResult | null {
|
||||
// Check system paths (Homebrew, system)
|
||||
const systemPaths = getNodeSystemPaths();
|
||||
for (const nodePath of systemPaths) {
|
||||
if (isExecutable(nodePath)) {
|
||||
return { nodePath, source: 'homebrew' };
|
||||
// Determine source based on path
|
||||
if (nodePath.includes('homebrew') || nodePath === '/usr/local/bin/node') {
|
||||
return { nodePath, source: 'homebrew' };
|
||||
}
|
||||
return { nodePath, source: 'system' };
|
||||
}
|
||||
}
|
||||
|
||||
// System Node
|
||||
if (isExecutable('/usr/bin/node')) {
|
||||
return { nodePath: '/usr/bin/node', source: 'system' };
|
||||
}
|
||||
|
||||
// NVM installation
|
||||
const nvmPath = path.join(homeDir, '.nvm', 'versions', 'node');
|
||||
const nvmNode = findNodeFromVersionManager(nvmPath);
|
||||
if (nvmNode) {
|
||||
return { nodePath: nvmNode, source: 'nvm' };
|
||||
const nvmPaths = getNvmPaths();
|
||||
for (const nvmPath of nvmPaths) {
|
||||
const nvmNode = findNodeFromVersionManager(nvmPath);
|
||||
if (nvmNode) {
|
||||
return { nodePath: nvmNode, source: 'nvm' };
|
||||
}
|
||||
}
|
||||
|
||||
// fnm installation (multiple possible locations)
|
||||
const fnmPaths = [
|
||||
path.join(homeDir, '.local', 'share', 'fnm', 'node-versions'),
|
||||
path.join(homeDir, 'Library', 'Application Support', 'fnm', 'node-versions'),
|
||||
];
|
||||
|
||||
// fnm installation
|
||||
const fnmPaths = getFnmPaths();
|
||||
for (const fnmBasePath of fnmPaths) {
|
||||
const fnmNode = findNodeFromVersionManager(fnmBasePath);
|
||||
if (fnmNode) {
|
||||
@@ -147,15 +147,9 @@ function findNodeMacOS(homeDir: string): NodeFinderResult | null {
|
||||
/**
|
||||
* Find Node.js on Linux
|
||||
*/
|
||||
function findNodeLinux(homeDir: string): NodeFinderResult | null {
|
||||
// Common Linux paths
|
||||
const systemPaths = [
|
||||
'/usr/bin/node',
|
||||
'/usr/local/bin/node',
|
||||
// Snap installation
|
||||
'/snap/bin/node',
|
||||
];
|
||||
|
||||
function findNodeLinux(_homeDir: string): NodeFinderResult | null {
|
||||
// Check system paths
|
||||
const systemPaths = getNodeSystemPaths();
|
||||
for (const nodePath of systemPaths) {
|
||||
if (isExecutable(nodePath)) {
|
||||
return { nodePath, source: 'system' };
|
||||
@@ -163,18 +157,16 @@ function findNodeLinux(homeDir: string): NodeFinderResult | null {
|
||||
}
|
||||
|
||||
// NVM installation
|
||||
const nvmPath = path.join(homeDir, '.nvm', 'versions', 'node');
|
||||
const nvmNode = findNodeFromVersionManager(nvmPath);
|
||||
if (nvmNode) {
|
||||
return { nodePath: nvmNode, source: 'nvm' };
|
||||
const nvmPaths = getNvmPaths();
|
||||
for (const nvmPath of nvmPaths) {
|
||||
const nvmNode = findNodeFromVersionManager(nvmPath);
|
||||
if (nvmNode) {
|
||||
return { nodePath: nvmNode, source: 'nvm' };
|
||||
}
|
||||
}
|
||||
|
||||
// fnm installation
|
||||
const fnmPaths = [
|
||||
path.join(homeDir, '.local', 'share', 'fnm', 'node-versions'),
|
||||
path.join(homeDir, '.fnm', 'node-versions'),
|
||||
];
|
||||
|
||||
const fnmPaths = getFnmPaths();
|
||||
for (const fnmBasePath of fnmPaths) {
|
||||
const fnmNode = findNodeFromVersionManager(fnmBasePath);
|
||||
if (fnmNode) {
|
||||
@@ -188,40 +180,27 @@ function findNodeLinux(homeDir: string): NodeFinderResult | null {
|
||||
/**
|
||||
* Find Node.js on Windows
|
||||
*/
|
||||
function findNodeWindows(homeDir: string): NodeFinderResult | null {
|
||||
function findNodeWindows(_homeDir: string): NodeFinderResult | null {
|
||||
// Program Files paths
|
||||
const programFilesPaths = [
|
||||
path.join(process.env.PROGRAMFILES || 'C:\\Program Files', 'nodejs', 'node.exe'),
|
||||
path.join(process.env['PROGRAMFILES(X86)'] || 'C:\\Program Files (x86)', 'nodejs', 'node.exe'),
|
||||
];
|
||||
|
||||
for (const nodePath of programFilesPaths) {
|
||||
const systemPaths = getNodeSystemPaths();
|
||||
for (const nodePath of systemPaths) {
|
||||
if (isExecutable(nodePath)) {
|
||||
return { nodePath, source: 'program-files' };
|
||||
}
|
||||
}
|
||||
|
||||
// NVM for Windows
|
||||
const nvmWindowsPath = path.join(
|
||||
process.env.APPDATA || path.join(homeDir, 'AppData', 'Roaming'),
|
||||
'nvm'
|
||||
);
|
||||
const nvmNode = findNodeFromVersionManager(nvmWindowsPath, 'node.exe');
|
||||
if (nvmNode) {
|
||||
return { nodePath: nvmNode, source: 'nvm-windows' };
|
||||
const nvmPaths = getNvmPaths();
|
||||
for (const nvmPath of nvmPaths) {
|
||||
const nvmNode = findNodeFromVersionManager(nvmPath, 'node.exe');
|
||||
if (nvmNode) {
|
||||
return { nodePath: nvmNode, source: 'nvm-windows' };
|
||||
}
|
||||
}
|
||||
|
||||
// fnm on Windows (prioritize canonical installation path over shell shims)
|
||||
const fnmWindowsPaths = [
|
||||
path.join(homeDir, '.fnm', 'node-versions'),
|
||||
path.join(
|
||||
process.env.LOCALAPPDATA || path.join(homeDir, 'AppData', 'Local'),
|
||||
'fnm',
|
||||
'node-versions'
|
||||
),
|
||||
];
|
||||
|
||||
for (const fnmBasePath of fnmWindowsPaths) {
|
||||
// fnm on Windows
|
||||
const fnmPaths = getFnmPaths();
|
||||
for (const fnmBasePath of fnmPaths) {
|
||||
const fnmNode = findNodeFromVersionManager(fnmBasePath, 'node.exe');
|
||||
if (fnmNode) {
|
||||
return { nodePath: fnmNode, source: 'fnm' };
|
||||
@@ -229,17 +208,13 @@ function findNodeWindows(homeDir: string): NodeFinderResult | null {
|
||||
}
|
||||
|
||||
// Scoop installation
|
||||
const scoopPath = path.join(homeDir, 'scoop', 'apps', 'nodejs', 'current', 'node.exe');
|
||||
const scoopPath = getScoopNodePath();
|
||||
if (isExecutable(scoopPath)) {
|
||||
return { nodePath: scoopPath, source: 'scoop' };
|
||||
}
|
||||
|
||||
// Chocolatey installation
|
||||
const chocoPath = path.join(
|
||||
process.env.ChocolateyInstall || 'C:\\ProgramData\\chocolatey',
|
||||
'bin',
|
||||
'node.exe'
|
||||
);
|
||||
const chocoPath = getChocolateyNodePath();
|
||||
if (isExecutable(chocoPath)) {
|
||||
return { nodePath: chocoPath, source: 'chocolatey' };
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
*/
|
||||
|
||||
import fs from 'fs/promises';
|
||||
import type { Dirent } from 'fs';
|
||||
import fsSync, { type Dirent, type Stats } from 'fs';
|
||||
import path from 'path';
|
||||
import pLimit from 'p-limit';
|
||||
import { validatePath } from './security.js';
|
||||
@@ -305,3 +305,323 @@ export function joinPath(...pathSegments: string[]): string {
|
||||
export function resolvePath(...pathSegments: string[]): string {
|
||||
return path.resolve(...pathSegments);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Synchronous File System Methods
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Options for writeFileSync
|
||||
*/
|
||||
export interface WriteFileSyncOptions {
|
||||
encoding?: BufferEncoding;
|
||||
mode?: number;
|
||||
flag?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronous wrapper around fs.existsSync that validates path first
|
||||
*/
|
||||
export function existsSync(filePath: string): boolean {
|
||||
const validatedPath = validatePath(filePath);
|
||||
return fsSync.existsSync(validatedPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronous wrapper around fs.readFileSync that validates path first
|
||||
*/
|
||||
export function readFileSync(filePath: string, encoding?: BufferEncoding): string | Buffer {
|
||||
const validatedPath = validatePath(filePath);
|
||||
if (encoding) {
|
||||
return fsSync.readFileSync(validatedPath, encoding);
|
||||
}
|
||||
return fsSync.readFileSync(validatedPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronous wrapper around fs.writeFileSync that validates path first
|
||||
*/
|
||||
export function writeFileSync(
|
||||
filePath: string,
|
||||
data: string | Buffer,
|
||||
options?: WriteFileSyncOptions
|
||||
): void {
|
||||
const validatedPath = validatePath(filePath);
|
||||
fsSync.writeFileSync(validatedPath, data, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronous wrapper around fs.mkdirSync that validates path first
|
||||
*/
|
||||
export function mkdirSync(
|
||||
dirPath: string,
|
||||
options?: { recursive?: boolean; mode?: number }
|
||||
): string | undefined {
|
||||
const validatedPath = validatePath(dirPath);
|
||||
return fsSync.mkdirSync(validatedPath, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronous wrapper around fs.readdirSync that validates path first
|
||||
*/
|
||||
export function readdirSync(dirPath: string, options?: { withFileTypes?: false }): string[];
|
||||
export function readdirSync(dirPath: string, options: { withFileTypes: true }): Dirent[];
|
||||
export function readdirSync(
|
||||
dirPath: string,
|
||||
options?: { withFileTypes?: boolean }
|
||||
): string[] | Dirent[] {
|
||||
const validatedPath = validatePath(dirPath);
|
||||
if (options?.withFileTypes === true) {
|
||||
return fsSync.readdirSync(validatedPath, { withFileTypes: true });
|
||||
}
|
||||
return fsSync.readdirSync(validatedPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronous wrapper around fs.statSync that validates path first
|
||||
*/
|
||||
export function statSync(filePath: string): Stats {
|
||||
const validatedPath = validatePath(filePath);
|
||||
return fsSync.statSync(validatedPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronous wrapper around fs.accessSync that validates path first
|
||||
*/
|
||||
export function accessSync(filePath: string, mode?: number): void {
|
||||
const validatedPath = validatePath(filePath);
|
||||
fsSync.accessSync(validatedPath, mode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronous wrapper around fs.unlinkSync that validates path first
|
||||
*/
|
||||
export function unlinkSync(filePath: string): void {
|
||||
const validatedPath = validatePath(filePath);
|
||||
fsSync.unlinkSync(validatedPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronous wrapper around fs.rmSync that validates path first
|
||||
*/
|
||||
export function rmSync(filePath: string, options?: { recursive?: boolean; force?: boolean }): void {
|
||||
const validatedPath = validatePath(filePath);
|
||||
fsSync.rmSync(validatedPath, options);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Environment File Operations
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Read and parse an .env file from a validated path
|
||||
* Returns a record of key-value pairs
|
||||
*/
|
||||
export async function readEnvFile(envPath: string): Promise<Record<string, string>> {
|
||||
const validatedPath = validatePath(envPath);
|
||||
try {
|
||||
const content = await executeWithRetry(
|
||||
() => fs.readFile(validatedPath, 'utf-8'),
|
||||
`readEnvFile(${envPath})`
|
||||
);
|
||||
return parseEnvContent(content);
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
||||
return {};
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read and parse an .env file synchronously from a validated path
|
||||
*/
|
||||
export function readEnvFileSync(envPath: string): Record<string, string> {
|
||||
const validatedPath = validatePath(envPath);
|
||||
try {
|
||||
const content = fsSync.readFileSync(validatedPath, 'utf-8');
|
||||
return parseEnvContent(content);
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
||||
return {};
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse .env file content into a record
|
||||
*/
|
||||
function parseEnvContent(content: string): Record<string, string> {
|
||||
const result: Record<string, string> = {};
|
||||
const lines = content.split('\n');
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
// Skip empty lines and comments
|
||||
if (!trimmed || trimmed.startsWith('#')) {
|
||||
continue;
|
||||
}
|
||||
const equalIndex = trimmed.indexOf('=');
|
||||
if (equalIndex > 0) {
|
||||
const key = trimmed.slice(0, equalIndex).trim();
|
||||
const value = trimmed.slice(equalIndex + 1).trim();
|
||||
result[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write or update a key-value pair in an .env file
|
||||
* Preserves existing content and comments
|
||||
*/
|
||||
export async function writeEnvKey(envPath: string, key: string, value: string): Promise<void> {
|
||||
const validatedPath = validatePath(envPath);
|
||||
|
||||
let content = '';
|
||||
try {
|
||||
content = await executeWithRetry(
|
||||
() => fs.readFile(validatedPath, 'utf-8'),
|
||||
`readFile(${envPath})`
|
||||
);
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
|
||||
throw error;
|
||||
}
|
||||
// File doesn't exist, will create new one
|
||||
}
|
||||
|
||||
const newContent = updateEnvContent(content, key, value);
|
||||
await executeWithRetry(() => fs.writeFile(validatedPath, newContent), `writeFile(${envPath})`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Write or update a key-value pair in an .env file (synchronous)
|
||||
*/
|
||||
export function writeEnvKeySync(envPath: string, key: string, value: string): void {
|
||||
const validatedPath = validatePath(envPath);
|
||||
|
||||
let content = '';
|
||||
try {
|
||||
content = fsSync.readFileSync(validatedPath, 'utf-8');
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
|
||||
throw error;
|
||||
}
|
||||
// File doesn't exist, will create new one
|
||||
}
|
||||
|
||||
const newContent = updateEnvContent(content, key, value);
|
||||
fsSync.writeFileSync(validatedPath, newContent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a key from an .env file
|
||||
*/
|
||||
export async function removeEnvKey(envPath: string, key: string): Promise<void> {
|
||||
const validatedPath = validatePath(envPath);
|
||||
|
||||
let content = '';
|
||||
try {
|
||||
content = await executeWithRetry(
|
||||
() => fs.readFile(validatedPath, 'utf-8'),
|
||||
`readFile(${envPath})`
|
||||
);
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
||||
return; // File doesn't exist, nothing to remove
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
const newContent = removeEnvKeyFromContent(content, key);
|
||||
await executeWithRetry(() => fs.writeFile(validatedPath, newContent), `writeFile(${envPath})`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a key from an .env file (synchronous)
|
||||
*/
|
||||
export function removeEnvKeySync(envPath: string, key: string): void {
|
||||
const validatedPath = validatePath(envPath);
|
||||
|
||||
let content = '';
|
||||
try {
|
||||
content = fsSync.readFileSync(validatedPath, 'utf-8');
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
||||
return; // File doesn't exist, nothing to remove
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
const newContent = removeEnvKeyFromContent(content, key);
|
||||
fsSync.writeFileSync(validatedPath, newContent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update .env content with a new key-value pair
|
||||
*/
|
||||
function updateEnvContent(content: string, key: string, value: string): string {
|
||||
const lines = content.split('\n');
|
||||
const keyRegex = new RegExp(`^${escapeRegex(key)}=`);
|
||||
let found = false;
|
||||
|
||||
const newLines = lines.map((line) => {
|
||||
if (keyRegex.test(line.trim())) {
|
||||
found = true;
|
||||
return `${key}=${value}`;
|
||||
}
|
||||
return line;
|
||||
});
|
||||
|
||||
if (!found) {
|
||||
// Add the key at the end
|
||||
if (newLines.length > 0 && newLines[newLines.length - 1].trim() !== '') {
|
||||
newLines.push(`${key}=${value}`);
|
||||
} else {
|
||||
// Replace last empty line or add to empty file
|
||||
if (newLines.length === 0 || (newLines.length === 1 && newLines[0] === '')) {
|
||||
newLines[0] = `${key}=${value}`;
|
||||
} else {
|
||||
newLines[newLines.length - 1] = `${key}=${value}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure file ends with newline
|
||||
let result = newLines.join('\n');
|
||||
if (!result.endsWith('\n')) {
|
||||
result += '\n';
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a key from .env content
|
||||
*/
|
||||
function removeEnvKeyFromContent(content: string, key: string): string {
|
||||
const lines = content.split('\n');
|
||||
const keyRegex = new RegExp(`^${escapeRegex(key)}=`);
|
||||
const newLines = lines.filter((line) => !keyRegex.test(line.trim()));
|
||||
|
||||
// Remove trailing empty lines
|
||||
while (newLines.length > 0 && newLines[newLines.length - 1].trim() === '') {
|
||||
newLines.pop();
|
||||
}
|
||||
|
||||
// Ensure file ends with newline if there's content
|
||||
let result = newLines.join('\n');
|
||||
if (result.length > 0 && !result.endsWith('\n')) {
|
||||
result += '\n';
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape special regex characters in a string
|
||||
*/
|
||||
function escapeRegex(str: string): string {
|
||||
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
}
|
||||
|
||||
787
libs/platform/src/system-paths.ts
Normal file
787
libs/platform/src/system-paths.ts
Normal file
@@ -0,0 +1,787 @@
|
||||
/**
|
||||
* System Paths Configuration
|
||||
*
|
||||
* Centralized configuration for ALL system paths that automaker needs to access
|
||||
* outside of the ALLOWED_ROOT_DIRECTORY. These are well-known system paths for
|
||||
* tools like GitHub CLI, Claude CLI, Node.js version managers, etc.
|
||||
*
|
||||
* ALL file system access must go through this module or secureFs.
|
||||
* Direct fs imports are NOT allowed anywhere else in the codebase.
|
||||
*
|
||||
* Categories of system paths:
|
||||
* 1. CLI Tools: GitHub CLI, Claude CLI
|
||||
* 2. Version Managers: NVM, fnm, Volta
|
||||
* 3. Shells: /bin/zsh, /bin/bash, PowerShell
|
||||
* 4. Electron userData: API keys, window bounds, app settings
|
||||
* 5. Script directories: node_modules, logs (relative to script)
|
||||
*/
|
||||
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import fsSync from 'fs';
|
||||
import fs from 'fs/promises';
|
||||
|
||||
// =============================================================================
|
||||
// System Tool Path Definitions
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Get common paths where GitHub CLI might be installed
|
||||
*/
|
||||
export function getGitHubCliPaths(): string[] {
|
||||
const isWindows = process.platform === 'win32';
|
||||
|
||||
if (isWindows) {
|
||||
return [
|
||||
path.join(process.env.LOCALAPPDATA || '', 'Programs', 'gh', 'bin', 'gh.exe'),
|
||||
path.join(process.env.ProgramFiles || '', 'GitHub CLI', 'gh.exe'),
|
||||
].filter(Boolean);
|
||||
}
|
||||
|
||||
return [
|
||||
'/opt/homebrew/bin/gh',
|
||||
'/usr/local/bin/gh',
|
||||
path.join(os.homedir(), '.local', 'bin', 'gh'),
|
||||
'/home/linuxbrew/.linuxbrew/bin/gh',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get common paths where Claude CLI might be installed
|
||||
*/
|
||||
export function getClaudeCliPaths(): string[] {
|
||||
const isWindows = process.platform === 'win32';
|
||||
|
||||
if (isWindows) {
|
||||
const appData = process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming');
|
||||
return [
|
||||
path.join(os.homedir(), '.local', 'bin', 'claude.exe'),
|
||||
path.join(appData, 'npm', 'claude.cmd'),
|
||||
path.join(appData, 'npm', 'claude'),
|
||||
path.join(appData, '.npm-global', 'bin', 'claude.cmd'),
|
||||
path.join(appData, '.npm-global', 'bin', 'claude'),
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
path.join(os.homedir(), '.local', 'bin', 'claude'),
|
||||
path.join(os.homedir(), '.claude', 'local', 'claude'),
|
||||
'/usr/local/bin/claude',
|
||||
path.join(os.homedir(), '.npm-global', 'bin', 'claude'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Claude configuration directory path
|
||||
*/
|
||||
export function getClaudeConfigDir(): string {
|
||||
return path.join(os.homedir(), '.claude');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get paths to Claude credential files
|
||||
*/
|
||||
export function getClaudeCredentialPaths(): string[] {
|
||||
const claudeDir = getClaudeConfigDir();
|
||||
return [path.join(claudeDir, '.credentials.json'), path.join(claudeDir, 'credentials.json')];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get path to Claude settings file
|
||||
*/
|
||||
export function getClaudeSettingsPath(): string {
|
||||
return path.join(getClaudeConfigDir(), 'settings.json');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get path to Claude stats cache file
|
||||
*/
|
||||
export function getClaudeStatsCachePath(): string {
|
||||
return path.join(getClaudeConfigDir(), 'stats-cache.json');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get path to Claude projects/sessions directory
|
||||
*/
|
||||
export function getClaudeProjectsDir(): string {
|
||||
return path.join(getClaudeConfigDir(), 'projects');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get common shell paths for shell detection
|
||||
*/
|
||||
export function getShellPaths(): string[] {
|
||||
if (process.platform === 'win32') {
|
||||
return [
|
||||
process.env.COMSPEC || 'cmd.exe',
|
||||
'powershell.exe',
|
||||
'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe',
|
||||
'C:\\Program Files\\PowerShell\\7\\pwsh.exe',
|
||||
];
|
||||
}
|
||||
|
||||
return ['/bin/zsh', '/bin/bash', '/bin/sh'];
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Node.js Version Manager Paths
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Get NVM installation paths
|
||||
*/
|
||||
export function getNvmPaths(): string[] {
|
||||
const homeDir = os.homedir();
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
const appData = process.env.APPDATA || path.join(homeDir, 'AppData', 'Roaming');
|
||||
return [path.join(appData, 'nvm')];
|
||||
}
|
||||
|
||||
return [path.join(homeDir, '.nvm', 'versions', 'node')];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get fnm installation paths
|
||||
*/
|
||||
export function getFnmPaths(): string[] {
|
||||
const homeDir = os.homedir();
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
const localAppData = process.env.LOCALAPPDATA || path.join(homeDir, 'AppData', 'Local');
|
||||
return [
|
||||
path.join(homeDir, '.fnm', 'node-versions'),
|
||||
path.join(localAppData, 'fnm', 'node-versions'),
|
||||
];
|
||||
}
|
||||
|
||||
if (process.platform === 'darwin') {
|
||||
return [
|
||||
path.join(homeDir, '.local', 'share', 'fnm', 'node-versions'),
|
||||
path.join(homeDir, 'Library', 'Application Support', 'fnm', 'node-versions'),
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
path.join(homeDir, '.local', 'share', 'fnm', 'node-versions'),
|
||||
path.join(homeDir, '.fnm', 'node-versions'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get common Node.js installation paths (not version managers)
|
||||
*/
|
||||
export function getNodeSystemPaths(): string[] {
|
||||
if (process.platform === 'win32') {
|
||||
return [
|
||||
path.join(process.env.PROGRAMFILES || 'C:\\Program Files', 'nodejs', 'node.exe'),
|
||||
path.join(
|
||||
process.env['PROGRAMFILES(X86)'] || 'C:\\Program Files (x86)',
|
||||
'nodejs',
|
||||
'node.exe'
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
if (process.platform === 'darwin') {
|
||||
return ['/opt/homebrew/bin/node', '/usr/local/bin/node', '/usr/bin/node'];
|
||||
}
|
||||
|
||||
// Linux
|
||||
return ['/usr/bin/node', '/usr/local/bin/node', '/snap/bin/node'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Scoop installation path for Node.js (Windows)
|
||||
*/
|
||||
export function getScoopNodePath(): string {
|
||||
return path.join(os.homedir(), 'scoop', 'apps', 'nodejs', 'current', 'node.exe');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Chocolatey installation path for Node.js (Windows)
|
||||
*/
|
||||
export function getChocolateyNodePath(): string {
|
||||
return path.join(
|
||||
process.env.ChocolateyInstall || 'C:\\ProgramData\\chocolatey',
|
||||
'bin',
|
||||
'node.exe'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get WSL detection path
|
||||
*/
|
||||
export function getWslVersionPath(): string {
|
||||
return '/proc/version';
|
||||
}
|
||||
|
||||
/**
|
||||
* Extended PATH environment for finding system tools
|
||||
*/
|
||||
export function getExtendedPath(): string {
|
||||
const paths = [
|
||||
process.env.PATH,
|
||||
'/opt/homebrew/bin',
|
||||
'/usr/local/bin',
|
||||
'/home/linuxbrew/.linuxbrew/bin',
|
||||
`${process.env.HOME}/.local/bin`,
|
||||
];
|
||||
|
||||
return paths.filter(Boolean).join(process.platform === 'win32' ? ';' : ':');
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// System Path Access Methods (Unconstrained - only for system tool detection)
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Check if a file exists at a system path (synchronous)
|
||||
* IMPORTANT: This bypasses ALLOWED_ROOT_DIRECTORY restrictions.
|
||||
* Only use for checking system tool installation paths.
|
||||
*/
|
||||
export function systemPathExists(filePath: string): boolean {
|
||||
if (!isAllowedSystemPath(filePath)) {
|
||||
throw new Error(`[SystemPaths] Access denied: ${filePath} is not an allowed system path`);
|
||||
}
|
||||
return fsSync.existsSync(filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a file is accessible at a system path (async)
|
||||
* IMPORTANT: This bypasses ALLOWED_ROOT_DIRECTORY restrictions.
|
||||
* Only use for checking system tool installation paths.
|
||||
*/
|
||||
export async function systemPathAccess(filePath: string): Promise<boolean> {
|
||||
if (!isAllowedSystemPath(filePath)) {
|
||||
throw new Error(`[SystemPaths] Access denied: ${filePath} is not an allowed system path`);
|
||||
}
|
||||
try {
|
||||
await fs.access(filePath);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a file has execute permission (synchronous)
|
||||
* On Windows, only checks existence (X_OK is not meaningful)
|
||||
*/
|
||||
export function systemPathIsExecutable(filePath: string): boolean {
|
||||
if (!isAllowedSystemPath(filePath)) {
|
||||
throw new Error(`[SystemPaths] Access denied: ${filePath} is not an allowed system path`);
|
||||
}
|
||||
try {
|
||||
if (process.platform === 'win32') {
|
||||
fsSync.accessSync(filePath, fsSync.constants.F_OK);
|
||||
} else {
|
||||
fsSync.accessSync(filePath, fsSync.constants.X_OK);
|
||||
}
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a file from an allowed system path (async)
|
||||
* IMPORTANT: This bypasses ALLOWED_ROOT_DIRECTORY restrictions.
|
||||
* Only use for reading Claude config files and similar system configs.
|
||||
*/
|
||||
export async function systemPathReadFile(
|
||||
filePath: string,
|
||||
encoding: BufferEncoding = 'utf-8'
|
||||
): Promise<string> {
|
||||
if (!isAllowedSystemPath(filePath)) {
|
||||
throw new Error(`[SystemPaths] Access denied: ${filePath} is not an allowed system path`);
|
||||
}
|
||||
return fs.readFile(filePath, encoding);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a file from an allowed system path (synchronous)
|
||||
*/
|
||||
export function systemPathReadFileSync(
|
||||
filePath: string,
|
||||
encoding: BufferEncoding = 'utf-8'
|
||||
): string {
|
||||
if (!isAllowedSystemPath(filePath)) {
|
||||
throw new Error(`[SystemPaths] Access denied: ${filePath} is not an allowed system path`);
|
||||
}
|
||||
return fsSync.readFileSync(filePath, encoding);
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a file to an allowed system path (synchronous)
|
||||
*/
|
||||
export function systemPathWriteFileSync(
|
||||
filePath: string,
|
||||
data: string,
|
||||
options?: { encoding?: BufferEncoding; mode?: number }
|
||||
): void {
|
||||
if (!isAllowedSystemPath(filePath)) {
|
||||
throw new Error(`[SystemPaths] Access denied: ${filePath} is not an allowed system path`);
|
||||
}
|
||||
fsSync.writeFileSync(filePath, data, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read directory contents from an allowed system path (async)
|
||||
* IMPORTANT: This bypasses ALLOWED_ROOT_DIRECTORY restrictions.
|
||||
*/
|
||||
export async function systemPathReaddir(dirPath: string): Promise<string[]> {
|
||||
if (!isAllowedSystemPath(dirPath)) {
|
||||
throw new Error(`[SystemPaths] Access denied: ${dirPath} is not an allowed system path`);
|
||||
}
|
||||
return fs.readdir(dirPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read directory contents from an allowed system path (synchronous)
|
||||
*/
|
||||
export function systemPathReaddirSync(dirPath: string): string[] {
|
||||
if (!isAllowedSystemPath(dirPath)) {
|
||||
throw new Error(`[SystemPaths] Access denied: ${dirPath} is not an allowed system path`);
|
||||
}
|
||||
return fsSync.readdirSync(dirPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get file stats from a system path (synchronous)
|
||||
*/
|
||||
export function systemPathStatSync(filePath: string): fsSync.Stats {
|
||||
if (!isAllowedSystemPath(filePath)) {
|
||||
throw new Error(`[SystemPaths] Access denied: ${filePath} is not an allowed system path`);
|
||||
}
|
||||
return fsSync.statSync(filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get file stats from a system path (async)
|
||||
*/
|
||||
export async function systemPathStat(filePath: string): Promise<fsSync.Stats> {
|
||||
if (!isAllowedSystemPath(filePath)) {
|
||||
throw new Error(`[SystemPaths] Access denied: ${filePath} is not an allowed system path`);
|
||||
}
|
||||
return fs.stat(filePath);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Path Validation
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* All paths that are allowed for system tool detection
|
||||
*/
|
||||
function getAllAllowedSystemPaths(): string[] {
|
||||
return [
|
||||
// GitHub CLI paths
|
||||
...getGitHubCliPaths(),
|
||||
// Claude CLI paths
|
||||
...getClaudeCliPaths(),
|
||||
// Claude config directory and files
|
||||
getClaudeConfigDir(),
|
||||
...getClaudeCredentialPaths(),
|
||||
getClaudeSettingsPath(),
|
||||
getClaudeStatsCachePath(),
|
||||
getClaudeProjectsDir(),
|
||||
// Shell paths
|
||||
...getShellPaths(),
|
||||
// Node.js system paths
|
||||
...getNodeSystemPaths(),
|
||||
getScoopNodePath(),
|
||||
getChocolateyNodePath(),
|
||||
// WSL detection
|
||||
getWslVersionPath(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all allowed directories (for recursive access)
|
||||
*/
|
||||
function getAllAllowedSystemDirs(): string[] {
|
||||
return [
|
||||
// Claude config
|
||||
getClaudeConfigDir(),
|
||||
getClaudeProjectsDir(),
|
||||
// Version managers (need recursive access for version directories)
|
||||
...getNvmPaths(),
|
||||
...getFnmPaths(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a path is an allowed system path
|
||||
* Paths must either be exactly in the allowed list, or be inside an allowed directory
|
||||
*/
|
||||
export function isAllowedSystemPath(filePath: string): boolean {
|
||||
const normalizedPath = path.resolve(filePath);
|
||||
const allowedPaths = getAllAllowedSystemPaths();
|
||||
|
||||
// Check for exact match
|
||||
if (allowedPaths.includes(normalizedPath)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if the path is inside an allowed directory
|
||||
const allowedDirs = getAllAllowedSystemDirs();
|
||||
|
||||
for (const allowedDir of allowedDirs) {
|
||||
const normalizedAllowedDir = path.resolve(allowedDir);
|
||||
// Check if path is exactly the allowed dir or inside it
|
||||
if (
|
||||
normalizedPath === normalizedAllowedDir ||
|
||||
normalizedPath.startsWith(normalizedAllowedDir + path.sep)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Electron userData Operations
|
||||
// =============================================================================
|
||||
|
||||
// Store the Electron userData path (set by Electron main process)
|
||||
let electronUserDataPath: string | null = null;
|
||||
|
||||
/**
|
||||
* Set the Electron userData path (called from Electron main process)
|
||||
*/
|
||||
export function setElectronUserDataPath(userDataPath: string): void {
|
||||
electronUserDataPath = userDataPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Electron userData path
|
||||
*/
|
||||
export function getElectronUserDataPath(): string | null {
|
||||
return electronUserDataPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a path is within the Electron userData directory
|
||||
*/
|
||||
export function isElectronUserDataPath(filePath: string): boolean {
|
||||
if (!electronUserDataPath) return false;
|
||||
const normalizedPath = path.resolve(filePath);
|
||||
const normalizedUserData = path.resolve(electronUserDataPath);
|
||||
return (
|
||||
normalizedPath === normalizedUserData ||
|
||||
normalizedPath.startsWith(normalizedUserData + path.sep)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a file from Electron userData directory
|
||||
*/
|
||||
export function electronUserDataReadFileSync(
|
||||
relativePath: string,
|
||||
encoding: BufferEncoding = 'utf-8'
|
||||
): string {
|
||||
if (!electronUserDataPath) {
|
||||
throw new Error('[SystemPaths] Electron userData path not initialized');
|
||||
}
|
||||
const fullPath = path.join(electronUserDataPath, relativePath);
|
||||
return fsSync.readFileSync(fullPath, encoding);
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a file to Electron userData directory
|
||||
*/
|
||||
export function electronUserDataWriteFileSync(
|
||||
relativePath: string,
|
||||
data: string,
|
||||
options?: { encoding?: BufferEncoding; mode?: number }
|
||||
): void {
|
||||
if (!electronUserDataPath) {
|
||||
throw new Error('[SystemPaths] Electron userData path not initialized');
|
||||
}
|
||||
const fullPath = path.join(electronUserDataPath, relativePath);
|
||||
fsSync.writeFileSync(fullPath, data, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a file exists in Electron userData directory
|
||||
*/
|
||||
export function electronUserDataExists(relativePath: string): boolean {
|
||||
if (!electronUserDataPath) return false;
|
||||
const fullPath = path.join(electronUserDataPath, relativePath);
|
||||
return fsSync.existsSync(fullPath);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Script Directory Operations (for init.mjs and similar)
|
||||
// =============================================================================
|
||||
|
||||
// Store the script's base directory
|
||||
let scriptBaseDir: string | null = null;
|
||||
|
||||
/**
|
||||
* Set the script base directory
|
||||
*/
|
||||
export function setScriptBaseDir(baseDir: string): void {
|
||||
scriptBaseDir = baseDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the script base directory
|
||||
*/
|
||||
export function getScriptBaseDir(): string | null {
|
||||
return scriptBaseDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a file exists relative to script base directory
|
||||
*/
|
||||
export function scriptDirExists(relativePath: string): boolean {
|
||||
if (!scriptBaseDir) {
|
||||
throw new Error('[SystemPaths] Script base directory not initialized');
|
||||
}
|
||||
const fullPath = path.join(scriptBaseDir, relativePath);
|
||||
return fsSync.existsSync(fullPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a directory relative to script base directory
|
||||
*/
|
||||
export function scriptDirMkdirSync(relativePath: string, options?: { recursive?: boolean }): void {
|
||||
if (!scriptBaseDir) {
|
||||
throw new Error('[SystemPaths] Script base directory not initialized');
|
||||
}
|
||||
const fullPath = path.join(scriptBaseDir, relativePath);
|
||||
fsSync.mkdirSync(fullPath, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a write stream for a file relative to script base directory
|
||||
*/
|
||||
export function scriptDirCreateWriteStream(relativePath: string): fsSync.WriteStream {
|
||||
if (!scriptBaseDir) {
|
||||
throw new Error('[SystemPaths] Script base directory not initialized');
|
||||
}
|
||||
const fullPath = path.join(scriptBaseDir, relativePath);
|
||||
return fsSync.createWriteStream(fullPath);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Electron App Bundle Operations (for accessing app's own files)
|
||||
// =============================================================================
|
||||
|
||||
// Store the Electron app bundle paths (can have multiple allowed directories)
|
||||
let electronAppDirs: string[] = [];
|
||||
let electronResourcesPath: string | null = null;
|
||||
|
||||
/**
|
||||
* Set the Electron app directories (called from Electron main process)
|
||||
* In development mode, pass the project root to allow access to source files.
|
||||
* In production mode, pass __dirname and process.resourcesPath.
|
||||
*
|
||||
* @param appDirOrDirs - Single directory or array of directories to allow
|
||||
* @param resourcesPath - Optional resources path (for packaged apps)
|
||||
*/
|
||||
export function setElectronAppPaths(appDirOrDirs: string | string[], resourcesPath?: string): void {
|
||||
electronAppDirs = Array.isArray(appDirOrDirs) ? appDirOrDirs : [appDirOrDirs];
|
||||
electronResourcesPath = resourcesPath || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a path is within the Electron app bundle (any of the allowed directories)
|
||||
*/
|
||||
function isElectronAppPath(filePath: string): boolean {
|
||||
const normalizedPath = path.resolve(filePath);
|
||||
|
||||
// Check against all allowed app directories
|
||||
for (const appDir of electronAppDirs) {
|
||||
const normalizedAppDir = path.resolve(appDir);
|
||||
if (
|
||||
normalizedPath === normalizedAppDir ||
|
||||
normalizedPath.startsWith(normalizedAppDir + path.sep)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check against resources path (for packaged apps)
|
||||
if (electronResourcesPath) {
|
||||
const normalizedResources = path.resolve(electronResourcesPath);
|
||||
if (
|
||||
normalizedPath === normalizedResources ||
|
||||
normalizedPath.startsWith(normalizedResources + path.sep)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a file exists within the Electron app bundle
|
||||
*/
|
||||
export function electronAppExists(filePath: string): boolean {
|
||||
if (!isElectronAppPath(filePath)) {
|
||||
throw new Error(`[SystemPaths] Access denied: ${filePath} is not within Electron app bundle`);
|
||||
}
|
||||
return fsSync.existsSync(filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a file from the Electron app bundle
|
||||
*/
|
||||
export function electronAppReadFileSync(filePath: string): Buffer {
|
||||
if (!isElectronAppPath(filePath)) {
|
||||
throw new Error(`[SystemPaths] Access denied: ${filePath} is not within Electron app bundle`);
|
||||
}
|
||||
return fsSync.readFileSync(filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get file stats from the Electron app bundle
|
||||
*/
|
||||
export function electronAppStatSync(filePath: string): fsSync.Stats {
|
||||
if (!isElectronAppPath(filePath)) {
|
||||
throw new Error(`[SystemPaths] Access denied: ${filePath} is not within Electron app bundle`);
|
||||
}
|
||||
return fsSync.statSync(filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get file stats from the Electron app bundle (async with callback for compatibility)
|
||||
*/
|
||||
export function electronAppStat(
|
||||
filePath: string,
|
||||
callback: (err: NodeJS.ErrnoException | null, stats: fsSync.Stats | undefined) => void
|
||||
): void {
|
||||
if (!isElectronAppPath(filePath)) {
|
||||
callback(
|
||||
new Error(`[SystemPaths] Access denied: ${filePath} is not within Electron app bundle`),
|
||||
undefined
|
||||
);
|
||||
return;
|
||||
}
|
||||
fsSync.stat(filePath, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a file from the Electron app bundle (async with callback for compatibility)
|
||||
*/
|
||||
export function electronAppReadFile(
|
||||
filePath: string,
|
||||
callback: (err: NodeJS.ErrnoException | null, data: Buffer | undefined) => void
|
||||
): void {
|
||||
if (!isElectronAppPath(filePath)) {
|
||||
callback(
|
||||
new Error(`[SystemPaths] Access denied: ${filePath} is not within Electron app bundle`),
|
||||
undefined
|
||||
);
|
||||
return;
|
||||
}
|
||||
fsSync.readFile(filePath, callback);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// High-level Tool Detection Methods
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Find the first existing path from a list of system paths
|
||||
*/
|
||||
export async function findFirstExistingPath(paths: string[]): Promise<string | null> {
|
||||
for (const p of paths) {
|
||||
if (await systemPathAccess(p)) {
|
||||
return p;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if GitHub CLI is installed and return its path
|
||||
*/
|
||||
export async function findGitHubCliPath(): Promise<string | null> {
|
||||
return findFirstExistingPath(getGitHubCliPaths());
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Claude CLI is installed and return its path
|
||||
*/
|
||||
export async function findClaudeCliPath(): Promise<string | null> {
|
||||
return findFirstExistingPath(getClaudeCliPaths());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Claude authentication status by checking various indicators
|
||||
*/
|
||||
export interface ClaudeAuthIndicators {
|
||||
hasCredentialsFile: boolean;
|
||||
hasSettingsFile: boolean;
|
||||
hasStatsCacheWithActivity: boolean;
|
||||
hasProjectsSessions: boolean;
|
||||
credentials: {
|
||||
hasOAuthToken: boolean;
|
||||
hasApiKey: boolean;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export async function getClaudeAuthIndicators(): Promise<ClaudeAuthIndicators> {
|
||||
const result: ClaudeAuthIndicators = {
|
||||
hasCredentialsFile: false,
|
||||
hasSettingsFile: false,
|
||||
hasStatsCacheWithActivity: false,
|
||||
hasProjectsSessions: false,
|
||||
credentials: null,
|
||||
};
|
||||
|
||||
// Check settings file
|
||||
try {
|
||||
if (await systemPathAccess(getClaudeSettingsPath())) {
|
||||
result.hasSettingsFile = true;
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors
|
||||
}
|
||||
|
||||
// Check stats cache for recent activity
|
||||
try {
|
||||
const statsContent = await systemPathReadFile(getClaudeStatsCachePath());
|
||||
const stats = JSON.parse(statsContent);
|
||||
if (stats.dailyActivity && stats.dailyActivity.length > 0) {
|
||||
result.hasStatsCacheWithActivity = true;
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors
|
||||
}
|
||||
|
||||
// Check for sessions in projects directory
|
||||
try {
|
||||
const sessions = await systemPathReaddir(getClaudeProjectsDir());
|
||||
if (sessions.length > 0) {
|
||||
result.hasProjectsSessions = true;
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors
|
||||
}
|
||||
|
||||
// Check credentials files
|
||||
const credentialPaths = getClaudeCredentialPaths();
|
||||
for (const credPath of credentialPaths) {
|
||||
try {
|
||||
const content = await systemPathReadFile(credPath);
|
||||
const credentials = JSON.parse(content);
|
||||
result.hasCredentialsFile = true;
|
||||
result.credentials = {
|
||||
hasOAuthToken: !!(credentials.oauth_token || credentials.access_token),
|
||||
hasApiKey: !!credentials.api_key,
|
||||
};
|
||||
break;
|
||||
} catch {
|
||||
// Continue to next path
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
Reference in New Issue
Block a user