mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-31 06:42:03 +00:00
1313 lines
39 KiB
TypeScript
1313 lines
39 KiB
TypeScript
/**
|
|
* 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 NVM-installed Node.js bin paths for CLI tools
|
|
*/
|
|
function getNvmBinPaths(): string[] {
|
|
const nvmDir = process.env.NVM_DIR || path.join(os.homedir(), '.nvm');
|
|
const versionsDir = path.join(nvmDir, 'versions', 'node');
|
|
|
|
try {
|
|
if (!fsSync.existsSync(versionsDir)) {
|
|
return [];
|
|
}
|
|
const versions = fsSync.readdirSync(versionsDir);
|
|
return versions.map((version) => path.join(versionsDir, version, 'bin'));
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get fnm (Fast Node Manager) installed Node.js bin paths
|
|
*/
|
|
function getFnmBinPaths(): string[] {
|
|
const homeDir = os.homedir();
|
|
const possibleFnmDirs = [
|
|
path.join(homeDir, '.local', 'share', 'fnm', 'node-versions'),
|
|
path.join(homeDir, '.fnm', 'node-versions'),
|
|
// macOS
|
|
path.join(homeDir, 'Library', 'Application Support', 'fnm', 'node-versions'),
|
|
];
|
|
|
|
const binPaths: string[] = [];
|
|
|
|
for (const fnmDir of possibleFnmDirs) {
|
|
try {
|
|
if (!fsSync.existsSync(fnmDir)) {
|
|
continue;
|
|
}
|
|
const versions = fsSync.readdirSync(fnmDir);
|
|
for (const version of versions) {
|
|
binPaths.push(path.join(fnmDir, version, 'installation', 'bin'));
|
|
}
|
|
} catch {
|
|
// Ignore errors for this directory
|
|
}
|
|
}
|
|
|
|
return binPaths;
|
|
}
|
|
|
|
/**
|
|
* Get common paths where Codex CLI might be installed
|
|
*/
|
|
export function getCodexCliPaths(): string[] {
|
|
const isWindows = process.platform === 'win32';
|
|
const homeDir = os.homedir();
|
|
|
|
if (isWindows) {
|
|
const appData = process.env.APPDATA || path.join(homeDir, 'AppData', 'Roaming');
|
|
const localAppData = process.env.LOCALAPPDATA || path.join(homeDir, 'AppData', 'Local');
|
|
return [
|
|
path.join(homeDir, '.local', 'bin', 'codex.exe'),
|
|
path.join(appData, 'npm', 'codex.cmd'),
|
|
path.join(appData, 'npm', 'codex'),
|
|
path.join(appData, '.npm-global', 'bin', 'codex.cmd'),
|
|
path.join(appData, '.npm-global', 'bin', 'codex'),
|
|
// Volta on Windows
|
|
path.join(homeDir, '.volta', 'bin', 'codex.exe'),
|
|
// pnpm on Windows
|
|
path.join(localAppData, 'pnpm', 'codex.cmd'),
|
|
path.join(localAppData, 'pnpm', 'codex'),
|
|
];
|
|
}
|
|
|
|
// Include NVM bin paths for codex installed via npm global under NVM
|
|
const nvmBinPaths = getNvmBinPaths().map((binPath) => path.join(binPath, 'codex'));
|
|
|
|
// Include fnm bin paths
|
|
const fnmBinPaths = getFnmBinPaths().map((binPath) => path.join(binPath, 'codex'));
|
|
|
|
// pnpm global bin path
|
|
const pnpmHome = process.env.PNPM_HOME || path.join(homeDir, '.local', 'share', 'pnpm');
|
|
|
|
return [
|
|
// Standard locations
|
|
path.join(homeDir, '.local', 'bin', 'codex'),
|
|
'/opt/homebrew/bin/codex',
|
|
'/usr/local/bin/codex',
|
|
'/usr/bin/codex',
|
|
path.join(homeDir, '.npm-global', 'bin', 'codex'),
|
|
// Linuxbrew
|
|
'/home/linuxbrew/.linuxbrew/bin/codex',
|
|
// Volta
|
|
path.join(homeDir, '.volta', 'bin', 'codex'),
|
|
// pnpm global
|
|
path.join(pnpmHome, 'codex'),
|
|
// Yarn global
|
|
path.join(homeDir, '.yarn', 'bin', 'codex'),
|
|
path.join(homeDir, '.config', 'yarn', 'global', 'node_modules', '.bin', 'codex'),
|
|
// Snap packages
|
|
'/snap/bin/codex',
|
|
// NVM paths
|
|
...nvmBinPaths,
|
|
// fnm paths
|
|
...fnmBinPaths,
|
|
];
|
|
}
|
|
|
|
const CODEX_CONFIG_DIR_NAME = '.codex';
|
|
const CODEX_AUTH_FILENAME = 'auth.json';
|
|
const CODEX_TOKENS_KEY = 'tokens';
|
|
|
|
/**
|
|
* Get the Codex configuration directory path
|
|
*/
|
|
export function getCodexConfigDir(): string {
|
|
return path.join(os.homedir(), CODEX_CONFIG_DIR_NAME);
|
|
}
|
|
|
|
/**
|
|
* Get path to Codex auth file
|
|
*/
|
|
export function getCodexAuthPath(): string {
|
|
return path.join(getCodexConfigDir(), CODEX_AUTH_FILENAME);
|
|
}
|
|
|
|
/**
|
|
* 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');
|
|
}
|
|
|
|
/**
|
|
* Enumerate directories matching a prefix pattern and return full paths
|
|
* Used to resolve dynamic directory names like version numbers
|
|
*/
|
|
function enumerateMatchingPaths(
|
|
parentDir: string,
|
|
prefix: string,
|
|
...subPathParts: string[]
|
|
): string[] {
|
|
try {
|
|
if (!fsSync.existsSync(parentDir)) {
|
|
return [];
|
|
}
|
|
const entries = fsSync.readdirSync(parentDir);
|
|
const matching = entries.filter((entry) => entry.startsWith(prefix));
|
|
return matching.map((entry) => path.join(parentDir, entry, ...subPathParts));
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get common Git Bash installation paths on Windows
|
|
* Git Bash is needed for running shell scripts cross-platform
|
|
*/
|
|
export function getGitBashPaths(): string[] {
|
|
if (process.platform !== 'win32') {
|
|
return [];
|
|
}
|
|
|
|
const homeDir = os.homedir();
|
|
const localAppData = process.env.LOCALAPPDATA || '';
|
|
|
|
// Dynamic paths that require directory enumeration
|
|
// winget installs to: LocalAppData\Microsoft\WinGet\Packages\Git.Git_<hash>\bin\bash.exe
|
|
const wingetGitPaths = localAppData
|
|
? enumerateMatchingPaths(
|
|
path.join(localAppData, 'Microsoft', 'WinGet', 'Packages'),
|
|
'Git.Git_',
|
|
'bin',
|
|
'bash.exe'
|
|
)
|
|
: [];
|
|
|
|
// GitHub Desktop bundles Git at: LocalAppData\GitHubDesktop\app-<version>\resources\app\git\cmd\bash.exe
|
|
const githubDesktopPaths = localAppData
|
|
? enumerateMatchingPaths(
|
|
path.join(localAppData, 'GitHubDesktop'),
|
|
'app-',
|
|
'resources',
|
|
'app',
|
|
'git',
|
|
'cmd',
|
|
'bash.exe'
|
|
)
|
|
: [];
|
|
|
|
return [
|
|
// Standard Git for Windows installations
|
|
'C:\\Program Files\\Git\\bin\\bash.exe',
|
|
'C:\\Program Files (x86)\\Git\\bin\\bash.exe',
|
|
// User-local installations
|
|
path.join(localAppData, 'Programs', 'Git', 'bin', 'bash.exe'),
|
|
// Scoop package manager
|
|
path.join(homeDir, 'scoop', 'apps', 'git', 'current', 'bin', 'bash.exe'),
|
|
// Chocolatey
|
|
path.join(
|
|
process.env.ChocolateyInstall || 'C:\\ProgramData\\chocolatey',
|
|
'lib',
|
|
'git',
|
|
'tools',
|
|
'bin',
|
|
'bash.exe'
|
|
),
|
|
// winget installations (dynamically resolved)
|
|
...wingetGitPaths,
|
|
// GitHub Desktop bundled Git (dynamically resolved)
|
|
...githubDesktopPaths,
|
|
].filter(Boolean);
|
|
}
|
|
|
|
/**
|
|
* Get common shell paths for shell detection
|
|
* Includes both full paths and short names to match $SHELL or PATH entries
|
|
*/
|
|
export function getShellPaths(): string[] {
|
|
if (process.platform === 'win32') {
|
|
return [
|
|
// Full paths (most specific first)
|
|
'C:\\Program Files\\PowerShell\\7\\pwsh.exe',
|
|
'C:\\Program Files\\PowerShell\\7-preview\\pwsh.exe',
|
|
'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe',
|
|
// COMSPEC environment variable (typically cmd.exe)
|
|
process.env.COMSPEC || 'C:\\Windows\\System32\\cmd.exe',
|
|
// Short names (for PATH resolution)
|
|
'pwsh.exe',
|
|
'pwsh',
|
|
'powershell.exe',
|
|
'powershell',
|
|
'cmd.exe',
|
|
'cmd',
|
|
];
|
|
}
|
|
|
|
// POSIX (macOS, Linux)
|
|
return [
|
|
// Full paths
|
|
'/bin/zsh',
|
|
'/bin/bash',
|
|
'/bin/sh',
|
|
'/usr/bin/zsh',
|
|
'/usr/bin/bash',
|
|
'/usr/bin/sh',
|
|
'/usr/local/bin/zsh',
|
|
'/usr/local/bin/bash',
|
|
'/opt/homebrew/bin/zsh',
|
|
'/opt/homebrew/bin/bash',
|
|
// Short names (for PATH resolution or $SHELL matching)
|
|
'zsh',
|
|
'bash',
|
|
'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(),
|
|
// Codex CLI paths
|
|
...getCodexCliPaths(),
|
|
// Codex config directory and files
|
|
getCodexConfigDir(),
|
|
getCodexAuthPath(),
|
|
// OpenCode CLI paths
|
|
...getOpenCodeCliPaths(),
|
|
// OpenCode config directory and files
|
|
getOpenCodeConfigDir(),
|
|
getOpenCodeAuthPath(),
|
|
// Shell paths
|
|
...getShellPaths(),
|
|
// Git Bash paths (for Windows cross-platform shell script execution)
|
|
...getGitBashPaths(),
|
|
// 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(),
|
|
// Codex config
|
|
getCodexConfigDir(),
|
|
// OpenCode config
|
|
getOpenCodeConfigDir(),
|
|
// 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());
|
|
}
|
|
|
|
export async function findCodexCliPath(): Promise<string | null> {
|
|
return findFirstExistingPath(getCodexCliPaths());
|
|
}
|
|
|
|
/**
|
|
* Find Git Bash on Windows and return its path
|
|
*/
|
|
export async function findGitBashPath(): Promise<string | null> {
|
|
return findFirstExistingPath(getGitBashPaths());
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
// Support multiple credential formats:
|
|
// 1. Claude Code CLI format: { claudeAiOauth: { accessToken, refreshToken } }
|
|
// 2. Legacy format: { oauth_token } or { access_token }
|
|
// 3. API key format: { api_key }
|
|
const hasClaudeOauth = !!credentials.claudeAiOauth?.accessToken;
|
|
const hasLegacyOauth = !!(credentials.oauth_token || credentials.access_token);
|
|
result.credentials = {
|
|
hasOAuthToken: hasClaudeOauth || hasLegacyOauth,
|
|
hasApiKey: !!credentials.api_key,
|
|
};
|
|
break;
|
|
} catch {
|
|
// Continue to next path
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
export interface CodexAuthIndicators {
|
|
hasAuthFile: boolean;
|
|
hasOAuthToken: boolean;
|
|
hasApiKey: boolean;
|
|
}
|
|
|
|
const CODEX_OAUTH_KEYS = ['access_token', 'oauth_token'] as const;
|
|
const CODEX_API_KEY_KEYS = ['api_key', 'OPENAI_API_KEY'] as const;
|
|
|
|
function hasNonEmptyStringField(record: Record<string, unknown>, keys: readonly string[]): boolean {
|
|
return keys.some((key) => typeof record[key] === 'string' && record[key]);
|
|
}
|
|
|
|
function getNestedTokens(record: Record<string, unknown>): Record<string, unknown> | null {
|
|
const tokens = record[CODEX_TOKENS_KEY];
|
|
if (tokens && typeof tokens === 'object' && !Array.isArray(tokens)) {
|
|
return tokens as Record<string, unknown>;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export async function getCodexAuthIndicators(): Promise<CodexAuthIndicators> {
|
|
const result: CodexAuthIndicators = {
|
|
hasAuthFile: false,
|
|
hasOAuthToken: false,
|
|
hasApiKey: false,
|
|
};
|
|
|
|
try {
|
|
const authContent = await systemPathReadFile(getCodexAuthPath());
|
|
result.hasAuthFile = true;
|
|
|
|
try {
|
|
const authJson = JSON.parse(authContent) as Record<string, unknown>;
|
|
result.hasOAuthToken = hasNonEmptyStringField(authJson, CODEX_OAUTH_KEYS);
|
|
result.hasApiKey = hasNonEmptyStringField(authJson, CODEX_API_KEY_KEYS);
|
|
const nestedTokens = getNestedTokens(authJson);
|
|
if (nestedTokens) {
|
|
result.hasOAuthToken =
|
|
result.hasOAuthToken || hasNonEmptyStringField(nestedTokens, CODEX_OAUTH_KEYS);
|
|
result.hasApiKey =
|
|
result.hasApiKey || hasNonEmptyStringField(nestedTokens, CODEX_API_KEY_KEYS);
|
|
}
|
|
} catch {
|
|
// Ignore parse errors; file exists but contents are unreadable
|
|
}
|
|
} catch {
|
|
// Auth file not found or inaccessible
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
// =============================================================================
|
|
// OpenCode CLI Detection
|
|
// =============================================================================
|
|
|
|
const OPENCODE_DATA_DIR = '.local/share/opencode';
|
|
const OPENCODE_AUTH_FILENAME = 'auth.json';
|
|
const OPENCODE_TOKENS_KEY = 'tokens';
|
|
|
|
/**
|
|
* Get common paths where OpenCode CLI might be installed
|
|
*/
|
|
export function getOpenCodeCliPaths(): string[] {
|
|
const isWindows = process.platform === 'win32';
|
|
const homeDir = os.homedir();
|
|
|
|
if (isWindows) {
|
|
const appData = process.env.APPDATA || path.join(homeDir, 'AppData', 'Roaming');
|
|
const localAppData = process.env.LOCALAPPDATA || path.join(homeDir, 'AppData', 'Local');
|
|
return [
|
|
// OpenCode's default installation directory
|
|
path.join(homeDir, '.opencode', 'bin', 'opencode.exe'),
|
|
path.join(homeDir, '.local', 'bin', 'opencode.exe'),
|
|
path.join(appData, 'npm', 'opencode.cmd'),
|
|
path.join(appData, 'npm', 'opencode'),
|
|
path.join(appData, '.npm-global', 'bin', 'opencode.cmd'),
|
|
path.join(appData, '.npm-global', 'bin', 'opencode'),
|
|
// Volta on Windows
|
|
path.join(homeDir, '.volta', 'bin', 'opencode.exe'),
|
|
// pnpm on Windows
|
|
path.join(localAppData, 'pnpm', 'opencode.cmd'),
|
|
path.join(localAppData, 'pnpm', 'opencode'),
|
|
// Go installation (if OpenCode is a Go binary)
|
|
path.join(homeDir, 'go', 'bin', 'opencode.exe'),
|
|
path.join(process.env.GOPATH || path.join(homeDir, 'go'), 'bin', 'opencode.exe'),
|
|
];
|
|
}
|
|
|
|
// Include NVM bin paths for opencode installed via npm global under NVM
|
|
const nvmBinPaths = getNvmBinPaths().map((binPath) => path.join(binPath, 'opencode'));
|
|
|
|
// Include fnm bin paths
|
|
const fnmBinPaths = getFnmBinPaths().map((binPath) => path.join(binPath, 'opencode'));
|
|
|
|
// pnpm global bin path
|
|
const pnpmHome = process.env.PNPM_HOME || path.join(homeDir, '.local', 'share', 'pnpm');
|
|
|
|
return [
|
|
// OpenCode's default installation directory
|
|
path.join(homeDir, '.opencode', 'bin', 'opencode'),
|
|
// Standard locations
|
|
path.join(homeDir, '.local', 'bin', 'opencode'),
|
|
'/opt/homebrew/bin/opencode',
|
|
'/usr/local/bin/opencode',
|
|
'/usr/bin/opencode',
|
|
path.join(homeDir, '.npm-global', 'bin', 'opencode'),
|
|
// Linuxbrew
|
|
'/home/linuxbrew/.linuxbrew/bin/opencode',
|
|
// Volta
|
|
path.join(homeDir, '.volta', 'bin', 'opencode'),
|
|
// pnpm global
|
|
path.join(pnpmHome, 'opencode'),
|
|
// Yarn global
|
|
path.join(homeDir, '.yarn', 'bin', 'opencode'),
|
|
path.join(homeDir, '.config', 'yarn', 'global', 'node_modules', '.bin', 'opencode'),
|
|
// Go installation (if OpenCode is a Go binary)
|
|
path.join(homeDir, 'go', 'bin', 'opencode'),
|
|
path.join(process.env.GOPATH || path.join(homeDir, 'go'), 'bin', 'opencode'),
|
|
// Snap packages
|
|
'/snap/bin/opencode',
|
|
// NVM paths
|
|
...nvmBinPaths,
|
|
// fnm paths
|
|
...fnmBinPaths,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Get the OpenCode data directory path
|
|
* macOS/Linux: ~/.local/share/opencode
|
|
* Windows: %USERPROFILE%\.local\share\opencode
|
|
*/
|
|
export function getOpenCodeConfigDir(): string {
|
|
return path.join(os.homedir(), OPENCODE_DATA_DIR);
|
|
}
|
|
|
|
/**
|
|
* Get path to OpenCode auth file
|
|
*/
|
|
export function getOpenCodeAuthPath(): string {
|
|
return path.join(getOpenCodeConfigDir(), OPENCODE_AUTH_FILENAME);
|
|
}
|
|
|
|
/**
|
|
* Check if OpenCode CLI is installed and return its path
|
|
*/
|
|
export async function findOpenCodeCliPath(): Promise<string | null> {
|
|
return findFirstExistingPath(getOpenCodeCliPaths());
|
|
}
|
|
|
|
export interface OpenCodeAuthIndicators {
|
|
hasAuthFile: boolean;
|
|
hasOAuthToken: boolean;
|
|
hasApiKey: boolean;
|
|
}
|
|
|
|
const OPENCODE_OAUTH_KEYS = ['access_token', 'oauth_token'] as const;
|
|
const OPENCODE_API_KEY_KEYS = ['api_key', 'OPENAI_API_KEY', 'ANTHROPIC_API_KEY'] as const;
|
|
|
|
// Provider names that OpenCode uses for provider-specific auth entries
|
|
const OPENCODE_PROVIDERS = ['anthropic', 'openai', 'google', 'bedrock', 'amazon-bedrock'] as const;
|
|
|
|
function getOpenCodeNestedTokens(record: Record<string, unknown>): Record<string, unknown> | null {
|
|
const tokens = record[OPENCODE_TOKENS_KEY];
|
|
if (tokens && typeof tokens === 'object' && !Array.isArray(tokens)) {
|
|
return tokens as Record<string, unknown>;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Check if the auth JSON has provider-specific OAuth credentials
|
|
* OpenCode stores auth in format: { "anthropic": { "type": "oauth", "access": "...", "refresh": "..." } }
|
|
*/
|
|
function hasProviderOAuth(authJson: Record<string, unknown>): boolean {
|
|
for (const provider of OPENCODE_PROVIDERS) {
|
|
const providerAuth = authJson[provider];
|
|
if (providerAuth && typeof providerAuth === 'object' && !Array.isArray(providerAuth)) {
|
|
const auth = providerAuth as Record<string, unknown>;
|
|
// Check for OAuth type with access token
|
|
if (auth.type === 'oauth' && typeof auth.access === 'string' && auth.access) {
|
|
return true;
|
|
}
|
|
// Also check for access_token field directly
|
|
if (typeof auth.access_token === 'string' && auth.access_token) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Check if the auth JSON has provider-specific API key credentials
|
|
*/
|
|
function hasProviderApiKey(authJson: Record<string, unknown>): boolean {
|
|
for (const provider of OPENCODE_PROVIDERS) {
|
|
const providerAuth = authJson[provider];
|
|
if (providerAuth && typeof providerAuth === 'object' && !Array.isArray(providerAuth)) {
|
|
const auth = providerAuth as Record<string, unknown>;
|
|
// Check for API key type
|
|
if (auth.type === 'api_key' && typeof auth.key === 'string' && auth.key) {
|
|
return true;
|
|
}
|
|
// Also check for api_key field directly
|
|
if (typeof auth.api_key === 'string' && auth.api_key) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Get OpenCode authentication status by checking auth file indicators
|
|
*/
|
|
export async function getOpenCodeAuthIndicators(): Promise<OpenCodeAuthIndicators> {
|
|
const result: OpenCodeAuthIndicators = {
|
|
hasAuthFile: false,
|
|
hasOAuthToken: false,
|
|
hasApiKey: false,
|
|
};
|
|
|
|
try {
|
|
const authContent = await systemPathReadFile(getOpenCodeAuthPath());
|
|
result.hasAuthFile = true;
|
|
|
|
try {
|
|
const authJson = JSON.parse(authContent) as Record<string, unknown>;
|
|
|
|
// Check for legacy top-level keys
|
|
result.hasOAuthToken = hasNonEmptyStringField(authJson, OPENCODE_OAUTH_KEYS);
|
|
result.hasApiKey = hasNonEmptyStringField(authJson, OPENCODE_API_KEY_KEYS);
|
|
|
|
// Check for nested tokens object (legacy format)
|
|
const nestedTokens = getOpenCodeNestedTokens(authJson);
|
|
if (nestedTokens) {
|
|
result.hasOAuthToken =
|
|
result.hasOAuthToken || hasNonEmptyStringField(nestedTokens, OPENCODE_OAUTH_KEYS);
|
|
result.hasApiKey =
|
|
result.hasApiKey || hasNonEmptyStringField(nestedTokens, OPENCODE_API_KEY_KEYS);
|
|
}
|
|
|
|
// Check for provider-specific auth entries (current OpenCode format)
|
|
// Format: { "anthropic": { "type": "oauth", "access": "...", "refresh": "..." } }
|
|
result.hasOAuthToken = result.hasOAuthToken || hasProviderOAuth(authJson);
|
|
result.hasApiKey = result.hasApiKey || hasProviderApiKey(authJson);
|
|
} catch {
|
|
// Ignore parse errors; file exists but contents are unreadable
|
|
}
|
|
} catch {
|
|
// Auth file not found or inaccessible
|
|
}
|
|
|
|
return result;
|
|
}
|