Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions Packages/src/Cli~/src/port-resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
/* eslint-disable security/detect-non-literal-fs-filename */

import { readFile } from 'fs/promises';
import { existsSync } from 'fs';
import { join } from 'path';
import { findUnityProjectRoot } from './project-root.js';

Expand All @@ -22,20 +23,25 @@ export async function resolveUnityPort(explicitPort?: number): Promise<number> {
return explicitPort;
}

const settingsPort = await readPortFromSettings();
const projectRoot = findUnityProjectRoot();
if (projectRoot === null) {
throw new Error('Unity project not found. Use --port option to specify the port explicitly.');
}

const settingsPort = await readPortFromSettings(projectRoot);
if (settingsPort !== null) {
return settingsPort;
}

return DEFAULT_PORT;
}

async function readPortFromSettings(): Promise<number | null> {
const projectRoot = findUnityProjectRoot();
if (projectRoot === null) {
async function readPortFromSettings(projectRoot: string): Promise<number | null> {
const settingsPath = join(projectRoot, 'UserSettings/UnityMcpSettings.json');

if (!existsSync(settingsPath)) {
return null;
}
const settingsPath = join(projectRoot, 'UserSettings/UnityMcpSettings.json');

let content: string;
try {
Expand Down
116 changes: 103 additions & 13 deletions Packages/src/Cli~/src/project-root.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,124 @@
/**
* Unity project root detection utility.
* Searches upward from current directory to find Unity project markers.
* Searches child directories first (up to 3 levels deep), then parent directories.
*/

// Path traversal is intentional for finding Unity project root by walking up directory tree
// Path traversal is intentional for finding Unity project root
/* eslint-disable security/detect-non-literal-fs-filename */

import { existsSync } from 'fs';
import { existsSync, readdirSync } from 'fs';
import { join, dirname } from 'path';

/**
* Find Unity project root by searching upward from start path.
* A Unity project is identified by having both Assets/ and ProjectSettings/ directories.
* Returns null if not inside a Unity project.
*/
export function findUnityProjectRoot(startPath: string = process.cwd()): string | null {
const CHILD_SEARCH_MAX_DEPTH = 3;

const EXCLUDED_DIRS = new Set([
'node_modules',
'.git',
'Temp',
'obj',
'Build',
'Builds',
'Logs',
'Library',
]);

function isUnityProjectWithUloop(dirPath: string): boolean {
const hasAssets = existsSync(join(dirPath, 'Assets'));
const hasProjectSettings = existsSync(join(dirPath, 'ProjectSettings'));
const hasUloopSettings = existsSync(join(dirPath, 'UserSettings/UnityMcpSettings.json'));
return hasAssets && hasProjectSettings && hasUloopSettings;
}

function findUnityProjectsInChildren(startPath: string, maxDepth: number): string[] {
const projects: string[] = [];

function scan(currentPath: string, depth: number): void {
if (depth > maxDepth) {
return;
}

if (!existsSync(currentPath)) {
return;
}

if (isUnityProjectWithUloop(currentPath)) {
projects.push(currentPath);
return;
}

let entries: ReturnType<typeof readdirSync>;
try {
entries = readdirSync(currentPath, { withFileTypes: true });
} catch {
return;
}

for (const entry of entries) {
if (!entry.isDirectory()) {
continue;
}

if (EXCLUDED_DIRS.has(entry.name)) {
continue;
}

const fullPath = join(currentPath, entry.name);
scan(fullPath, depth + 1);
}
}

scan(startPath, 0);
return projects.sort();
}

function findUnityProjectInParents(startPath: string): string | null {
let currentPath = startPath;

while (true) {
const hasAssets = existsSync(join(currentPath, 'Assets'));
const hasProjectSettings = existsSync(join(currentPath, 'ProjectSettings'));

if (hasAssets && hasProjectSettings) {
if (isUnityProjectWithUloop(currentPath)) {
return currentPath;
}

const isGitRoot = existsSync(join(currentPath, '.git'));
if (isGitRoot) {
return null;
}

const parentPath = dirname(currentPath);
if (parentPath === currentPath) {
return null;
}
currentPath = parentPath;
}
}

/**
* Find Unity project root by searching child directories first, then parent directories.
* A Unity project is identified by having both Assets/ and ProjectSettings/ directories.
*
* Search order:
* 1. Child directories (up to 3 levels deep)
* 2. Parent directories (up to root)
*
* If multiple Unity projects are found in child search, a warning is printed
* and the first one (alphabetically) is used.
*
* Returns null if no Unity project is found.
*/
export function findUnityProjectRoot(startPath: string = process.cwd()): string | null {
const childProjects = findUnityProjectsInChildren(startPath, CHILD_SEARCH_MAX_DEPTH);

if (childProjects.length > 0) {
if (childProjects.length > 1) {
console.error('\x1b[33mWarning: Multiple Unity projects found in child directories:\x1b[0m');
for (const project of childProjects) {
console.error(` - ${project}`);
}
console.error(`\x1b[33mUsing: ${childProjects[0]}\x1b[0m`);
console.error('');
}
return childProjects[0];
}

return findUnityProjectInParents(startPath);
}