Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:19:28 +08:00
commit 1da7b24c8e
254 changed files with 43797 additions and 0 deletions

View File

@@ -0,0 +1,43 @@
import typescriptEslint from '@typescript-eslint/eslint-plugin';
import typescriptParser from '@typescript-eslint/parser';
export default [
{
ignores: [
'node_modules/**',
'dist/**',
'*.backup/**'
]
},
{
files: ['src/**/*.ts'],
languageOptions: {
parser: typescriptParser,
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
project: './tsconfig.json'
}
},
plugins: {
'@typescript-eslint': typescriptEslint
},
rules: {
// TypeScript 규칙
'@typescript-eslint/no-explicit-any': 'error', // any 사용 금지 - 타입 가드 또는 명시적 타입 사용
'@typescript-eslint/no-unused-vars': ['error', {
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
caughtErrorsIgnorePattern: '^_'
}],
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-non-null-assertion': 'warn',
// 일반 규칙
'no-console': 'off', // CLI 도구이므로 console 사용 허용
'prefer-const': 'error',
'no-var': 'error'
}
}
];

View File

@@ -0,0 +1,39 @@
{
"name": "unity-editor-toolkit-cli",
"version": "0.14.1",
"description": "Unity Editor control toolkit CLI with comprehensive editor automation",
"main": "dist/cli/cli.js",
"bin": {
"unity-editor": "./dist/cli/cli.js"
},
"scripts": {
"build": "tsc && tsc-alias",
"dev": "tsc --watch",
"clean": "rm -rf dist",
"lint": "eslint src/**/*.ts",
"lint:fix": "eslint src/**/*.ts --fix",
"type-check": "tsc --noEmit"
},
"keywords": [
"unity",
"websocket",
"editor",
"automation",
"gameobject"
],
"author": "Dev GOM",
"license": "Apache-2.0",
"devDependencies": {
"@types/node": "^24.9.2",
"@types/ws": "^8.18.1",
"@typescript-eslint/eslint-plugin": "^8.46.3",
"@typescript-eslint/parser": "^8.46.3",
"eslint": "^9.39.1",
"tsc-alias": "^1.8.16",
"typescript": "^5.9.3"
},
"dependencies": {
"commander": "^14.0.2",
"ws": "^8.18.3"
}
}

View File

@@ -0,0 +1,134 @@
#!/usr/bin/env node
/**
* Unity Editor Toolkit CLI
*
* Complete command-line interface for controlling Unity Editor with real-time automation.
*/
import { Command } from 'commander';
import * as logger from '@/utils/logger';
import * as config from '@/utils/config';
import { createUnityClient } from '@/unity/client';
// Import commands
import { registerHierarchyCommand } from './commands/hierarchy';
import { registerGameObjectCommand } from './commands/gameobject';
import { registerTransformCommand } from './commands/transform';
import { registerComponentCommand } from './commands/component';
import { registerSceneCommand } from './commands/scene';
import { registerConsoleCommand } from './commands/console';
import { registerEditorCommand } from './commands/editor';
import { registerPrefsCommand } from './commands/prefs';
import { registerWaitCommand } from './commands/wait';
import { registerChainCommand } from './commands/chain';
import { registerDatabaseCommand } from './commands/db';
import { registerSnapshotCommand } from './commands/snapshot';
import { registerTransformHistoryCommand } from './commands/transform-history';
import { registerSyncCommand } from './commands/sync';
import { registerAnalyticsCommand } from './commands/analytics';
import { registerMenuCommand } from './commands/menu';
import { registerAssetCommand } from './commands/asset';
import { registerPrefabCommand } from './commands/prefab';
import { registerMaterialCommand } from './commands/material';
import { registerAnimationCommand } from './commands/animation';
const program = new Command();
// CLI metadata
program
.name('unity-editor')
.description('Unity Editor Toolkit - Complete Unity Editor control and automation')
.version('0.1.0');
// Global options
program
.option('-v, --verbose', 'Enable verbose logging')
.option('-p, --port <number>', 'Unity WebSocket port', (value) => parseInt(value, 10))
.hook('preAction', (thisCommand) => {
const opts = thisCommand.opts();
if (opts.verbose) {
logger.setLogLevel(3); // DEBUG
}
});
// Register commands
registerHierarchyCommand(program);
registerGameObjectCommand(program);
registerTransformCommand(program);
registerComponentCommand(program);
registerSceneCommand(program);
registerConsoleCommand(program);
registerEditorCommand(program);
registerPrefsCommand(program);
registerWaitCommand(program);
registerChainCommand(program);
registerDatabaseCommand(program);
registerSnapshotCommand(program);
registerTransformHistoryCommand(program);
registerSyncCommand(program);
registerAnalyticsCommand(program);
registerMenuCommand(program);
registerAssetCommand(program);
registerPrefabCommand(program);
registerMaterialCommand(program);
registerAnimationCommand(program);
// Status command (built-in)
program
.command('status')
.description('Show Unity WebSocket connection status')
.action(async () => {
try {
const projectRoot = config.getProjectRoot();
const projectName = config.getProjectName(projectRoot);
// Read server status first
const serverStatus = config.readServerStatus(projectRoot);
const port = config.getUnityPort(projectRoot);
logger.info('✓ Unity WebSocket Status');
logger.info(` Project: ${projectName}`);
logger.info(` Root: ${projectRoot}`);
if (serverStatus) {
logger.info(` Port: ${serverStatus.port}`);
logger.info(` Running: ${serverStatus.isRunning ? '✓' : '❌'}`);
logger.info(` Unity Version: ${serverStatus.editorVersion}`);
logger.info(` Last Heartbeat: ${serverStatus.lastHeartbeat}`);
logger.info(` Status: ${config.isServerStatusStale(serverStatus) ? '⚠️ Stale' : '✓ Active'}`);
} else {
logger.info(` Port: ${port || 'Unknown'}`);
logger.info(` Status: ❌ No server status file found`);
}
if (!port) {
logger.info('');
logger.info('❌ Unity server not detected');
logger.info(' Make sure Unity Editor is running with WebSocket server enabled');
process.exit(1);
}
// Try to connect
const client = createUnityClient(port);
try {
await client.connect();
logger.info(' Connection: ✓ Connected');
client.disconnect();
} catch (error) {
logger.info(' Connection: ❌ Not connected');
logger.info(' Make sure Unity Editor is running with WebSocket server enabled');
}
} catch (error) {
logger.error('Failed to get status', error);
process.exit(1);
}
});
// Parse arguments
program.parse(process.argv);
// Show help if no command provided
if (!process.argv.slice(2).length) {
program.outputHelp();
}

View File

@@ -0,0 +1,404 @@
/**
* Analytics command
*
* Get project and scene analytics, manage cache
*/
import { Command } from 'commander';
import * as logger from '@/utils/logger';
import * as config from '@/utils/config';
import { createUnityClient } from '@/unity/client';
import { COMMANDS } from '@/constants';
import { outputJson } from '@/utils/output-formatter';
/**
* Analytics result interfaces
*/
interface ComponentStat {
componentType: string;
count: number;
}
interface ProjectStatsResult {
success: boolean;
totalScenes: number;
totalObjects: number;
totalComponents: number;
totalTransforms: number;
totalSnapshots: number;
commandHistoryCount: number;
topComponents: ComponentStat[];
}
interface SceneStatsResult {
success: boolean;
sceneName: string;
scenePath: string;
sceneId: number;
objectCount: number;
componentCount: number;
snapshotCount: number;
transformHistoryCount: number;
message: string;
}
interface CacheResult {
success: boolean;
key: string;
message: string;
}
interface GetCacheResult {
success: boolean;
key: string;
data: string | null;
message: string;
}
interface ClearCacheResult {
success: boolean;
deletedCount: number;
message: string;
}
interface CacheEntry {
cacheId: number;
cacheKey: string;
expiresAt: string;
createdAt: string;
isExpired: boolean;
}
interface ListCacheResult {
success: boolean;
count: number;
entries: CacheEntry[];
}
/**
* Register Analytics command
*/
export function registerAnalyticsCommand(program: Command): void {
const analyticsCmd = program
.command('analytics')
.description('Get project and scene analytics, manage cache');
// Get project-wide statistics
analyticsCmd
.command('project-stats')
.description('Get project-wide statistics')
.option('--json', 'Output in JSON format')
.action(async (options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
logger.info('Connecting to Unity Editor...');
await client.connect();
logger.info('Getting project statistics...');
const result = await client.sendRequest<ProjectStatsResult>(
COMMANDS.ANALYTICS_PROJECT_STATS
);
if (options.json) {
outputJson(result);
return;
}
logger.info('✓ Project Statistics:');
logger.info(` Scenes: ${result.totalScenes}`);
logger.info(` Objects: ${result.totalObjects}`);
logger.info(` Components: ${result.totalComponents}`);
logger.info(` Transforms: ${result.totalTransforms}`);
logger.info(` Snapshots: ${result.totalSnapshots}`);
logger.info(` Command History: ${result.commandHistoryCount}`);
if (result.topComponents && result.topComponents.length > 0) {
logger.info(' Top Components:');
result.topComponents.forEach((c, index) => {
logger.info(` ${index + 1}. ${c.componentType}: ${c.count}`);
});
}
} catch (error) {
logger.error('Failed to get project stats', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// Get current scene statistics
analyticsCmd
.command('scene-stats')
.description('Get current scene statistics')
.option('--json', 'Output in JSON format')
.action(async (options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
logger.info('Connecting to Unity Editor...');
await client.connect();
logger.info('Getting scene statistics...');
const result = await client.sendRequest<SceneStatsResult>(
COMMANDS.ANALYTICS_SCENE_STATS
);
if (options.json) {
outputJson(result);
return;
}
logger.info('✓ Scene Statistics:');
logger.info(` Scene: ${result.sceneName}`);
logger.info(` Path: ${result.scenePath}`);
logger.info(` Objects: ${result.objectCount}`);
logger.info(` Components: ${result.componentCount}`);
logger.info(` Snapshots: ${result.snapshotCount}`);
logger.info(` Transform History: ${result.transformHistoryCount}`);
logger.info(` ${result.message}`);
} catch (error) {
logger.error('Failed to get scene stats', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// Set cache data
analyticsCmd
.command('set-cache')
.description('Set cache data')
.argument('<key>', 'Cache key')
.argument('<data>', 'Cache data (JSON string)')
.option('-t, --ttl <seconds>', 'Time to live in seconds', '3600')
.option('--json', 'Output in JSON format')
.action(async (key: string, data: string, options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
logger.info('Connecting to Unity Editor...');
await client.connect();
logger.info(`Setting cache: ${key}`);
const result = await client.sendRequest<CacheResult>(
COMMANDS.ANALYTICS_SET_CACHE,
{
key,
data,
ttl: parseInt(options.ttl, 10),
}
);
if (options.json) {
outputJson(result);
return;
}
logger.info(`${result.message}`);
} catch (error) {
logger.error('Failed to set cache', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// Get cache data
analyticsCmd
.command('get-cache')
.description('Get cache data')
.argument('<key>', 'Cache key')
.option('--json', 'Output in JSON format')
.action(async (key: string, options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
logger.info('Connecting to Unity Editor...');
await client.connect();
logger.info(`Getting cache: ${key}`);
const result = await client.sendRequest<GetCacheResult>(
COMMANDS.ANALYTICS_GET_CACHE,
{ key }
);
if (options.json) {
outputJson(result);
return;
}
if (result.success && result.data) {
logger.info(`✓ Cache Data:`);
logger.info(result.data);
} else {
logger.info(`${result.message}`);
}
} catch (error) {
logger.error('Failed to get cache', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// Clear cache
analyticsCmd
.command('clear-cache')
.description('Clear cache data')
.argument('[key]', 'Cache key (omit to clear all)')
.option('--json', 'Output in JSON format')
.action(async (key: string | undefined, options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
logger.info('Connecting to Unity Editor...');
await client.connect();
logger.info('Clearing cache...');
const result = await client.sendRequest<ClearCacheResult>(
COMMANDS.ANALYTICS_CLEAR_CACHE,
key ? { key } : {}
);
if (options.json) {
outputJson(result);
return;
}
logger.info(`${result.message}`);
} catch (error) {
logger.error('Failed to clear cache', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// List all cache entries
analyticsCmd
.command('list-cache')
.description('List all cache entries')
.option('--json', 'Output in JSON format')
.action(async (options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
logger.info('Connecting to Unity Editor...');
await client.connect();
logger.info('Listing cache entries...');
const result = await client.sendRequest<ListCacheResult>(
COMMANDS.ANALYTICS_LIST_CACHE
);
if (options.json) {
outputJson(result);
return;
}
logger.info(`✓ Cache Entries (${result.count}):`);
result.entries.forEach((entry) => {
const expiredMark = entry.isExpired ? '⚠️ Expired' : '✓ Valid';
logger.info(` [${entry.cacheId}] ${entry.cacheKey} - ${expiredMark}`);
logger.info(` Created: ${entry.createdAt}`);
logger.info(` Expires: ${entry.expiresAt}`);
});
} catch (error) {
logger.error('Failed to list cache', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
}

View File

@@ -0,0 +1,602 @@
/**
* Animation command
*
* Animation control commands for Animator and legacy Animation
*/
import { Command } from 'commander';
import * as logger from '@/utils/logger';
import * as config from '@/utils/config';
import { createUnityClient } from '@/unity/client';
import { COMMANDS } from '@/constants';
import { outputJson } from '@/utils/output-formatter';
/**
* Register Animation command
*/
export function registerAnimationCommand(program: Command): void {
const animCmd = program
.command('animation')
.alias('anim')
.description('Animation control commands');
// Play animation
animCmd
.command('play')
.description('Play animation on a GameObject')
.argument('<gameObject>', 'GameObject name or path')
.option('--state <name>', 'State name to play (Animator)')
.option('--clip <name>', 'Clip name to play (legacy Animation)')
.option('--layer <n>', 'Layer index (default: 0)', '0')
.option('--time <value>', 'Normalized start time (0-1)')
.option('--speed <value>', 'Playback speed', '1')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '30000')
.action(async (gameObject, options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
await client.connect();
interface PlayResult {
success: boolean;
gameObject: string;
type: string;
stateName?: string;
clipName?: string;
message: string;
}
const params: Record<string, unknown> = {
gameObject,
layer: parseInt(options.layer, 10),
speed: parseFloat(options.speed),
};
if (options.state) params.stateName = options.state;
if (options.clip) params.clipName = options.clip;
if (options.time) params.normalizedTime = parseFloat(options.time);
const result = await client.sendRequest<PlayResult>(COMMANDS.ANIMATION_PLAY, params);
if (options.json) {
outputJson(result);
} else {
logger.info(`${result.message}`);
logger.info(` Type: ${result.type}`);
if (result.stateName) logger.info(` State: ${result.stateName}`);
if (result.clipName) logger.info(` Clip: ${result.clipName}`);
}
} catch (error) {
logger.error('Failed to play animation', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// Stop animation
animCmd
.command('stop')
.description('Stop animation on a GameObject')
.argument('<gameObject>', 'GameObject name or path')
.option('--clip <name>', 'Specific clip to stop (legacy Animation)')
.option('--reset', 'Reset to default pose')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '30000')
.action(async (gameObject, options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
await client.connect();
interface StopResult {
success: boolean;
gameObject: string;
type: string;
message: string;
}
const result = await client.sendRequest<StopResult>(COMMANDS.ANIMATION_STOP, {
gameObject,
clipName: options.clip,
resetToDefault: options.reset || false,
});
if (options.json) {
outputJson(result);
} else {
logger.info(`${result.message}`);
logger.info(` Type: ${result.type}`);
}
} catch (error) {
logger.error('Failed to stop animation', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// Get animation state
animCmd
.command('state')
.alias('get-state')
.description('Get current animation state')
.argument('<gameObject>', 'GameObject name or path')
.option('--layer <n>', 'Layer index (default: 0)', '0')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '30000')
.action(async (gameObject, options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
await client.connect();
interface AnimatorStateResult {
success: boolean;
gameObject: string;
type: string;
enabled: boolean;
speed: number;
layer: number;
currentState: {
clipName: string;
normalizedTime: number;
length: number;
speed: number;
isLooping: boolean;
};
isInTransition: boolean;
layerCount: number;
parameterCount: number;
}
interface AnimationStateResult {
success: boolean;
gameObject: string;
type: string;
isPlaying: boolean;
clipCount: number;
clips: Array<{
name: string;
length: number;
normalizedTime: number;
speed: number;
weight: number;
enabled: boolean;
wrapMode: string;
}>;
}
type StateResult = AnimatorStateResult | AnimationStateResult;
const result = await client.sendRequest<StateResult>(COMMANDS.ANIMATION_GET_STATE, {
gameObject,
layer: parseInt(options.layer, 10),
});
if (options.json) {
outputJson(result);
} else {
logger.info(`✓ Animation state for ${result.gameObject}:`);
logger.info(` Type: ${result.type}`);
if (result.type === 'Animator') {
const r = result as AnimatorStateResult;
logger.info(` Enabled: ${r.enabled}`);
logger.info(` Speed: ${r.speed}`);
logger.info(` Layer: ${r.layer}/${r.layerCount}`);
logger.info(` In Transition: ${r.isInTransition}`);
logger.info(` Parameters: ${r.parameterCount}`);
if (r.currentState) {
logger.info(` Current State:`);
logger.info(` Clip: ${r.currentState.clipName}`);
logger.info(` Time: ${(r.currentState.normalizedTime * 100).toFixed(1)}%`);
logger.info(` Length: ${r.currentState.length.toFixed(2)}s`);
logger.info(` Looping: ${r.currentState.isLooping}`);
}
} else {
const r = result as AnimationStateResult;
logger.info(` Playing: ${r.isPlaying}`);
logger.info(` Clips: ${r.clipCount}`);
for (const clip of r.clips) {
logger.info(`${clip.name} (${clip.length.toFixed(2)}s, ${clip.wrapMode})`);
}
}
}
} catch (error) {
logger.error('Failed to get animation state', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// Get all parameters
animCmd
.command('params')
.alias('get-params')
.description('Get all Animator parameters')
.argument('<gameObject>', 'GameObject name or path')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '30000')
.action(async (gameObject, options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
await client.connect();
interface ParametersResult {
success: boolean;
gameObject: string;
count: number;
parameters: Array<{
name: string;
type: string;
value: unknown;
}>;
}
const result = await client.sendRequest<ParametersResult>(COMMANDS.ANIMATION_GET_PARAMETERS, {
gameObject,
});
if (options.json) {
outputJson(result);
} else {
logger.info(`${result.count} parameter(s) on ${result.gameObject}:`);
for (const p of result.parameters) {
const valueStr = p.value !== null ? `= ${p.value}` : '';
logger.info(`${p.name} (${p.type}) ${valueStr}`);
}
}
} catch (error) {
logger.error('Failed to get parameters', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// Get single parameter
animCmd
.command('get-param')
.description('Get an Animator parameter value')
.argument('<gameObject>', 'GameObject name or path')
.argument('<parameter>', 'Parameter name')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '30000')
.action(async (gameObject, parameter, options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
await client.connect();
interface ParameterResult {
success: boolean;
gameObject: string;
parameterName: string;
parameterType: string;
value: unknown;
}
const result = await client.sendRequest<ParameterResult>(COMMANDS.ANIMATION_GET_PARAMETER, {
gameObject,
parameterName: parameter,
});
if (options.json) {
outputJson(result);
} else {
logger.info(`${result.parameterName} (${result.parameterType}): ${result.value}`);
}
} catch (error) {
logger.error('Failed to get parameter', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// Set parameter
animCmd
.command('set-param')
.description('Set an Animator parameter value')
.argument('<gameObject>', 'GameObject name or path')
.argument('<parameter>', 'Parameter name')
.argument('<value>', 'New value (float, int, bool)')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '30000')
.action(async (gameObject, parameter, value, options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
await client.connect();
interface SetParameterResult {
success: boolean;
gameObject: string;
parameterName: string;
parameterType: string;
value: unknown;
}
// Parse value based on type
let parsedValue: unknown = value;
if (value === 'true') parsedValue = true;
else if (value === 'false') parsedValue = false;
else if (!isNaN(parseFloat(value))) parsedValue = parseFloat(value);
const result = await client.sendRequest<SetParameterResult>(COMMANDS.ANIMATION_SET_PARAMETER, {
gameObject,
parameterName: parameter,
value: parsedValue,
});
if (options.json) {
outputJson(result);
} else {
logger.info(`✓ Set ${result.parameterName} = ${result.value}`);
}
} catch (error) {
logger.error('Failed to set parameter', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// Set trigger
animCmd
.command('trigger')
.alias('set-trigger')
.description('Set an Animator trigger')
.argument('<gameObject>', 'GameObject name or path')
.argument('<trigger>', 'Trigger name')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '30000')
.action(async (gameObject, trigger, options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
await client.connect();
interface TriggerResult {
success: boolean;
gameObject: string;
triggerName: string;
message: string;
}
const result = await client.sendRequest<TriggerResult>(COMMANDS.ANIMATION_SET_TRIGGER, {
gameObject,
triggerName: trigger,
});
if (options.json) {
outputJson(result);
} else {
logger.info(`✓ Trigger '${result.triggerName}' set`);
}
} catch (error) {
logger.error('Failed to set trigger', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// Reset trigger
animCmd
.command('reset-trigger')
.description('Reset an Animator trigger')
.argument('<gameObject>', 'GameObject name or path')
.argument('<trigger>', 'Trigger name')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '30000')
.action(async (gameObject, trigger, options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
await client.connect();
interface TriggerResult {
success: boolean;
gameObject: string;
triggerName: string;
message: string;
}
const result = await client.sendRequest<TriggerResult>(COMMANDS.ANIMATION_RESET_TRIGGER, {
gameObject,
triggerName: trigger,
});
if (options.json) {
outputJson(result);
} else {
logger.info(`✓ Trigger '${result.triggerName}' reset`);
}
} catch (error) {
logger.error('Failed to reset trigger', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// CrossFade
animCmd
.command('crossfade')
.description('CrossFade to a state')
.argument('<gameObject>', 'GameObject name or path')
.argument('<state>', 'State name to transition to')
.option('--duration <seconds>', 'Transition duration', '0.25')
.option('--layer <n>', 'Layer index (default: 0)', '0')
.option('--offset <value>', 'Normalized time offset')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '30000')
.action(async (gameObject, state, options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
await client.connect();
interface CrossFadeResult {
success: boolean;
gameObject: string;
stateName: string;
transitionDuration: number;
message: string;
}
const params: Record<string, unknown> = {
gameObject,
stateName: state,
transitionDuration: parseFloat(options.duration),
layer: parseInt(options.layer, 10),
};
if (options.offset) params.normalizedTimeOffset = parseFloat(options.offset);
const result = await client.sendRequest<CrossFadeResult>(COMMANDS.ANIMATION_CROSSFADE, params);
if (options.json) {
outputJson(result);
} else {
logger.info(`✓ CrossFade to '${result.stateName}' (${result.transitionDuration}s)`);
}
} catch (error) {
logger.error('Failed to crossfade', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
}

View File

@@ -0,0 +1,695 @@
/**
* Asset command
*
* Unity Asset management commands (ScriptableObject creation, modification, etc.)
*/
import { Command } from 'commander';
import * as logger from '@/utils/logger';
import * as config from '@/utils/config';
import { createUnityClient } from '@/unity/client';
import { COMMANDS } from '@/constants';
import { outputJson } from '@/utils/output-formatter';
/**
* Register Asset command
*/
export function registerAssetCommand(program: Command): void {
const assetCmd = program
.command('asset')
.description('Unity Asset management commands (ScriptableObject, etc.)');
// List ScriptableObject types
assetCmd
.command('list-types')
.description('List available ScriptableObject types')
.option('--filter <pattern>', 'Filter types by pattern (supports * wildcard)')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '30000')
.action(async (options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
logger.info('Connecting to Unity Editor...');
await client.connect();
interface TypeInfo {
fullName: string;
name: string;
assembly: string;
namespaceName: string;
}
interface ListTypesResponse {
success: boolean;
types: TypeInfo[];
count: number;
}
const params: { filter?: string } = {};
if (options.filter) {
params.filter = options.filter;
}
logger.info('Fetching ScriptableObject types...');
const timeout = parseInt(options.timeout, 10);
const result = await client.sendRequest<ListTypesResponse>(COMMANDS.ASSET_LIST_SO_TYPES, params, timeout);
// JSON output
if (options.json) {
outputJson(result);
} else {
logger.info(`✓ Found ${result.count} ScriptableObject type(s):`);
logger.info('');
// Group by namespace
const namespaces = new Map<string, TypeInfo[]>();
for (const type of result.types) {
const ns = type.namespaceName || '(No Namespace)';
const list = namespaces.get(ns) || [];
list.push(type);
namespaces.set(ns, list);
}
for (const [ns, types] of namespaces.entries()) {
logger.info(`[${ns}]`);
for (const type of types) {
logger.info(` ${type.name}`);
logger.info(` Full: ${type.fullName}`);
logger.info(` Assembly: ${type.assembly}`);
}
logger.info('');
}
}
} catch (error) {
logger.error('Failed to list ScriptableObject types', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// Create ScriptableObject
assetCmd
.command('create-so')
.description('Create a new ScriptableObject asset')
.argument('<typeName>', 'ScriptableObject type name (e.g., "GameConfig" or "MyGame.GameConfig")')
.argument('<path>', 'Asset path (e.g., "Assets/Config/game.asset")')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '30000')
.action(async (typeName, path, options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
logger.info('Connecting to Unity Editor...');
await client.connect();
interface CreateResponse {
success: boolean;
path: string;
typeName: string;
message: string;
}
logger.info(`Creating ScriptableObject: ${typeName} at ${path}`);
const timeout = parseInt(options.timeout, 10);
const result = await client.sendRequest<CreateResponse>(COMMANDS.ASSET_CREATE_SO, { typeName, path }, timeout);
// JSON output
if (options.json) {
outputJson(result);
} else {
logger.info(`${result.message}`);
logger.info(` Type: ${result.typeName}`);
logger.info(` Path: ${result.path}`);
}
} catch (error) {
logger.error(`Failed to create ScriptableObject '${typeName}'`, error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// Get ScriptableObject fields
assetCmd
.command('get-fields')
.description('Get fields of a ScriptableObject')
.argument('<path>', 'Asset path (e.g., "Assets/Config/game.asset")')
.option('--expand', 'Expand array/list elements')
.option('--depth <n>', 'Max depth for nested expansion', '3')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '30000')
.action(async (path, options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
logger.info('Connecting to Unity Editor...');
await client.connect();
interface FieldInfo {
name: string;
displayName: string;
type: string;
value: string;
isArray: boolean;
arraySize: number;
propertyPath?: string;
elementType?: string;
elements?: FieldInfo[];
}
interface GetFieldsResponse {
success: boolean;
path: string;
typeName: string;
fields: FieldInfo[];
count: number;
}
const params: { path: string; expandArrays?: boolean; maxDepth?: number } = { path };
if (options.expand) {
params.expandArrays = true;
params.maxDepth = parseInt(options.depth, 10) || 3;
}
logger.info(`Getting fields for: ${path}`);
const timeout = parseInt(options.timeout, 10);
const result = await client.sendRequest<GetFieldsResponse>(COMMANDS.ASSET_GET_FIELDS, params, timeout);
// JSON output
if (options.json) {
outputJson(result);
} else {
logger.info(`✓ Fields for ${result.typeName} (${result.path}):`);
logger.info('');
const printField = (field: FieldInfo, indent: string = ' ') => {
const arrayInfo = field.isArray ? ` [Array: ${field.arraySize}]` : '';
logger.info(`${indent}${field.displayName} (${field.name})`);
logger.info(`${indent} Type: ${field.type}${arrayInfo}`);
if (field.elementType) {
logger.info(`${indent} Element Type: ${field.elementType}`);
}
logger.info(`${indent} Value: ${field.value}`);
// Print nested elements
if (field.elements && field.elements.length > 0) {
for (const element of field.elements) {
printField(element, indent + ' ');
}
}
};
for (const field of result.fields) {
printField(field);
}
}
} catch (error) {
logger.error(`Failed to get fields for '${path}'`, error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// Set ScriptableObject field
assetCmd
.command('set-field')
.description('Set a field value of a ScriptableObject (supports array index: items[0], items[0].name)')
.argument('<path>', 'Asset path (e.g., "Assets/Config/game.asset")')
.argument('<fieldName>', 'Field name or path (e.g., "health", "items[0]", "items[2].name")')
.argument('<value>', 'New value (format depends on type: "10" for int, "1.5" for float, "text" for string, "true/false" for bool, "x,y,z" for Vector3)')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '30000')
.action(async (path, fieldName, value, options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
logger.info('Connecting to Unity Editor...');
await client.connect();
interface SetFieldResponse {
success: boolean;
path: string;
fieldName: string;
previousValue: string;
newValue: string;
message: string;
}
logger.info(`Setting field '${fieldName}' to '${value}' in ${path}`);
const timeout = parseInt(options.timeout, 10);
const result = await client.sendRequest<SetFieldResponse>(COMMANDS.ASSET_SET_FIELD, { path, fieldName, value }, timeout);
// JSON output
if (options.json) {
outputJson(result);
} else {
logger.info(`${result.message}`);
logger.info(` Previous: ${result.previousValue}`);
logger.info(` New: ${result.newValue}`);
}
} catch (error) {
logger.error(`Failed to set field '${fieldName}'`, error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// Inspect ScriptableObject
assetCmd
.command('inspect')
.description('Inspect a ScriptableObject with full details (metadata + fields)')
.argument('<path>', 'Asset path (e.g., "Assets/Config/game.asset")')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '30000')
.action(async (path, options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
logger.info('Connecting to Unity Editor...');
await client.connect();
interface FieldInfo {
name: string;
displayName: string;
type: string;
value: string;
isArray: boolean;
arraySize: number;
}
interface Metadata {
path: string;
guid: string;
typeName: string;
typeNameShort: string;
namespaceName: string;
assemblyName: string;
instanceId: number;
name: string;
hideFlags: string;
userData: string;
}
interface InspectResponse {
success: boolean;
metadata: Metadata;
fields: FieldInfo[];
fieldCount: number;
}
logger.info(`Inspecting: ${path}`);
const timeout = parseInt(options.timeout, 10);
const result = await client.sendRequest<InspectResponse>(COMMANDS.ASSET_INSPECT, { path }, timeout);
// JSON output
if (options.json) {
outputJson(result);
} else {
const meta = result.metadata;
logger.info(`✓ Asset Inspection: ${meta.name}`);
logger.info('');
logger.info('=== Metadata ===');
logger.info(` Path: ${meta.path}`);
logger.info(` GUID: ${meta.guid}`);
logger.info(` Type: ${meta.typeName}`);
logger.info(` Namespace: ${meta.namespaceName || '(none)'}`);
logger.info(` Assembly: ${meta.assemblyName}`);
logger.info(` Instance ID: ${meta.instanceId}`);
logger.info(` Hide Flags: ${meta.hideFlags}`);
logger.info('');
logger.info(`=== Fields (${result.fieldCount}) ===`);
for (const field of result.fields) {
const arrayInfo = field.isArray ? ` [Array: ${field.arraySize}]` : '';
logger.info(` ${field.displayName} (${field.name})`);
logger.info(` Type: ${field.type}${arrayInfo}`);
logger.info(` Value: ${field.value}`);
}
}
} catch (error) {
logger.error(`Failed to inspect '${path}'`, error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// Add array element
assetCmd
.command('add-element')
.description('Add an element to an array/list field')
.argument('<path>', 'Asset path (e.g., "Assets/Config/game.asset")')
.argument('<fieldName>', 'Array field name (e.g., "items", "enemies")')
.option('--value <value>', 'Initial value for the new element')
.option('--index <n>', 'Insert position (-1 = end)', '-1')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '30000')
.action(async (path, fieldName, options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
logger.info('Connecting to Unity Editor...');
await client.connect();
interface AddElementResponse {
success: boolean;
path: string;
fieldName: string;
index: number;
newSize: number;
message: string;
}
const params: { path: string; fieldName: string; index?: number; value?: string } = { path, fieldName };
const index = parseInt(options.index, 10);
if (!isNaN(index)) {
params.index = index;
}
if (options.value !== undefined) {
params.value = options.value;
}
logger.info(`Adding element to '${fieldName}' in ${path}`);
const timeout = parseInt(options.timeout, 10);
const result = await client.sendRequest<AddElementResponse>(COMMANDS.ASSET_ADD_ARRAY_ELEMENT, params, timeout);
if (options.json) {
outputJson(result);
} else {
logger.info(`${result.message}`);
logger.info(` New size: ${result.newSize}`);
}
} catch (error) {
logger.error(`Failed to add element to '${fieldName}'`, error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// Remove array element
assetCmd
.command('remove-element')
.description('Remove an element from an array/list field')
.argument('<path>', 'Asset path (e.g., "Assets/Config/game.asset")')
.argument('<fieldName>', 'Array field name (e.g., "items", "enemies")')
.argument('<index>', 'Index of element to remove')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '30000')
.action(async (path, fieldName, indexStr, options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
logger.info('Connecting to Unity Editor...');
await client.connect();
interface RemoveElementResponse {
success: boolean;
path: string;
fieldName: string;
removedIndex: number;
removedValue: string;
newSize: number;
message: string;
}
const index = parseInt(indexStr, 10);
if (isNaN(index) || index < 0) {
throw new Error(`Invalid index: ${indexStr}`);
}
logger.info(`Removing element at index ${index} from '${fieldName}' in ${path}`);
const timeout = parseInt(options.timeout, 10);
const result = await client.sendRequest<RemoveElementResponse>(COMMANDS.ASSET_REMOVE_ARRAY_ELEMENT, { path, fieldName, index }, timeout);
if (options.json) {
outputJson(result);
} else {
logger.info(`${result.message}`);
logger.info(` Removed value: ${result.removedValue}`);
logger.info(` New size: ${result.newSize}`);
}
} catch (error) {
logger.error(`Failed to remove element from '${fieldName}'`, error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// Get array element
assetCmd
.command('get-element')
.description('Get a specific element from an array/list field')
.argument('<path>', 'Asset path (e.g., "Assets/Config/game.asset")')
.argument('<fieldName>', 'Array field name (e.g., "items", "enemies")')
.argument('<index>', 'Index of element to get')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '30000')
.action(async (path, fieldName, indexStr, options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
logger.info('Connecting to Unity Editor...');
await client.connect();
interface FieldInfo {
name: string;
displayName: string;
type: string;
value: string;
isArray: boolean;
arraySize: number;
propertyPath?: string;
elementType?: string;
elements?: FieldInfo[];
}
interface GetElementResponse {
success: boolean;
path: string;
fieldName: string;
index: number;
element: FieldInfo;
}
const index = parseInt(indexStr, 10);
if (isNaN(index) || index < 0) {
throw new Error(`Invalid index: ${indexStr}`);
}
logger.info(`Getting element at index ${index} from '${fieldName}' in ${path}`);
const timeout = parseInt(options.timeout, 10);
const result = await client.sendRequest<GetElementResponse>(COMMANDS.ASSET_GET_ARRAY_ELEMENT, { path, fieldName, index }, timeout);
if (options.json) {
outputJson(result);
} else {
logger.info(`✓ Element [${result.index}] from '${result.fieldName}':`);
logger.info('');
const printField = (field: FieldInfo, indent: string = ' ') => {
const arrayInfo = field.isArray ? ` [Array: ${field.arraySize}]` : '';
logger.info(`${indent}${field.displayName} (${field.name})`);
logger.info(`${indent} Type: ${field.type}${arrayInfo}`);
logger.info(`${indent} Value: ${field.value}`);
if (field.elements && field.elements.length > 0) {
for (const element of field.elements) {
printField(element, indent + ' ');
}
}
};
printField(result.element);
}
} catch (error) {
logger.error(`Failed to get element from '${fieldName}'`, error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// Clear array
assetCmd
.command('clear-array')
.description('Clear all elements from an array/list field')
.argument('<path>', 'Asset path (e.g., "Assets/Config/game.asset")')
.argument('<fieldName>', 'Array field name (e.g., "items", "enemies")')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '30000')
.action(async (path, fieldName, options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
logger.info('Connecting to Unity Editor...');
await client.connect();
interface ClearArrayResponse {
success: boolean;
path: string;
fieldName: string;
previousSize: number;
newSize: number;
message: string;
}
logger.info(`Clearing array '${fieldName}' in ${path}`);
const timeout = parseInt(options.timeout, 10);
const result = await client.sendRequest<ClearArrayResponse>(COMMANDS.ASSET_CLEAR_ARRAY, { path, fieldName }, timeout);
if (options.json) {
outputJson(result);
} else {
logger.info(`${result.message}`);
logger.info(` Previous size: ${result.previousSize}`);
logger.info(` New size: ${result.newSize}`);
}
} catch (error) {
logger.error(`Failed to clear array '${fieldName}'`, error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
}

View File

@@ -0,0 +1,263 @@
/**
* Chain command
*
* Execute multiple Unity commands sequentially
*/
import { Command } from 'commander';
import * as logger from '@/utils/logger';
import * as config from '@/utils/config';
import { createUnityClient } from '@/unity/client';
import { COMMANDS } from '@/constants';
import { outputJson } from '@/utils/output-formatter';
import * as fs from 'fs';
import * as path from 'path';
interface CommandEntry {
method: string;
parameters?: object;
}
/**
* Register Chain command
*/
export function registerChainCommand(program: Command): void {
const chainCmd = program
.command('chain')
.description('Execute multiple Unity commands sequentially');
// Execute from JSON file
chainCmd
.command('execute <file>')
.description('Execute commands from JSON file')
.option('--json', 'Output in JSON format')
.option('--stop-on-error', 'Stop on first error (default: true)', true)
.option('--continue-on-error', 'Continue on error')
.option('--timeout <ms>', 'Timeout in milliseconds', '300000')
.action(async (file, options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
// Read and parse JSON file
const filePath = path.resolve(file);
if (!fs.existsSync(filePath)) {
logger.error(`File not found: ${filePath}`);
process.exit(1);
}
const fileContent = fs.readFileSync(filePath, 'utf-8');
let commands: CommandEntry[];
try {
const parsed = JSON.parse(fileContent);
commands = Array.isArray(parsed) ? parsed : parsed.commands;
if (!Array.isArray(commands)) {
logger.error('Invalid JSON format. Expected array or object with "commands" property.');
process.exit(1);
}
} catch (error) {
logger.error('Failed to parse JSON file', error);
process.exit(1);
}
// Validate commands
for (let i = 0; i < commands.length; i++) {
const cmd = commands[i];
if (!cmd.method) {
logger.error(`Command at index ${i} is missing "method" property`);
process.exit(1);
}
}
logger.info(`Executing ${commands.length} command(s) from ${file}...`);
client = createUnityClient(port);
await client.connect();
const timeout = parseInt(options.timeout, 10);
const stopOnError = options.continueOnError ? false : options.stopOnError;
interface ChainResult {
success: boolean;
totalCommands: number;
executedCommands: number;
totalElapsed: number;
results: Array<{
index: number;
method: string;
success: boolean;
result?: object;
error?: string;
elapsed: number;
}>;
}
const result = await client.sendRequest<ChainResult>(COMMANDS.CHAIN_EXECUTE, {
commands,
stopOnError,
}, timeout);
if (options.json) {
outputJson(result);
} else {
logger.info(`✓ Chain execution completed`);
logger.info(` Total commands: ${result.totalCommands}`);
logger.info(` Executed: ${result.executedCommands}`);
logger.info(` Total time: ${result.totalElapsed.toFixed(3)}s`);
// Display results
for (const cmdResult of result.results) {
const status = cmdResult.success ? '✓' : '✗';
const message = cmdResult.success ? 'Success' : `Error: ${cmdResult.error}`;
logger.info(` [${cmdResult.index + 1}] ${status} ${cmdResult.method} (${cmdResult.elapsed.toFixed(3)}s)`);
if (!cmdResult.success) {
logger.error(` ${message}`);
}
}
}
// Exit with error if any command failed
const anyFailed = result.results.some(r => !r.success);
if (anyFailed) {
process.exit(1);
}
} catch (error) {
logger.error('Failed to execute chain', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// Execute inline commands
chainCmd
.command('exec <commands...>')
.description('Execute commands inline (format: method:param1=value1,param2=value2)')
.option('--json', 'Output in JSON format')
.option('--stop-on-error', 'Stop on first error (default: true)', true)
.option('--continue-on-error', 'Continue on error')
.option('--timeout <ms>', 'Timeout in milliseconds', '300000')
.action(async (commandStrings, options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
// Parse inline commands
const commands: CommandEntry[] = commandStrings.map((cmdStr: string, index: number) => {
const parts = cmdStr.split(':');
if (parts.length < 1) {
logger.error(`Invalid command format at index ${index}: ${cmdStr}`);
process.exit(1);
}
const method = parts[0];
let parameters: object | undefined = undefined;
if (parts.length > 1) {
// Parse parameters (format: key1=value1,key2=value2)
const paramStr = parts.slice(1).join(':');
parameters = {};
const paramPairs = paramStr.split(',');
for (const pair of paramPairs) {
const [key, value] = pair.split('=');
if (key && value !== undefined) {
// Try to parse value as number or boolean
let parsedValue: string | number | boolean = value;
if (value === 'true') parsedValue = true;
else if (value === 'false') parsedValue = false;
else if (!isNaN(Number(value))) parsedValue = Number(value);
(parameters as Record<string, unknown>)[key] = parsedValue;
}
}
}
return { method, parameters };
});
logger.info(`Executing ${commands.length} inline command(s)...`);
client = createUnityClient(port);
await client.connect();
const timeout = parseInt(options.timeout, 10);
const stopOnError = options.continueOnError ? false : options.stopOnError;
interface ChainResult {
success: boolean;
totalCommands: number;
executedCommands: number;
totalElapsed: number;
results: Array<{
index: number;
method: string;
success: boolean;
result?: object;
error?: string;
elapsed: number;
}>;
}
const result = await client.sendRequest<ChainResult>(COMMANDS.CHAIN_EXECUTE, {
commands,
stopOnError,
}, timeout);
if (options.json) {
outputJson(result);
} else {
logger.info(`✓ Chain execution completed`);
logger.info(` Total commands: ${result.totalCommands}`);
logger.info(` Executed: ${result.executedCommands}`);
logger.info(` Total time: ${result.totalElapsed.toFixed(3)}s`);
// Display results
for (const cmdResult of result.results) {
const status = cmdResult.success ? '✓' : '✗';
const message = cmdResult.success ? 'Success' : `Error: ${cmdResult.error}`;
logger.info(` [${cmdResult.index + 1}] ${status} ${cmdResult.method} (${cmdResult.elapsed.toFixed(3)}s)`);
if (!cmdResult.success) {
logger.error(` ${message}`);
}
}
}
// Exit with error if any command failed
const anyFailed = result.results.some(r => !r.success);
if (anyFailed) {
process.exit(1);
}
} catch (error) {
logger.error('Failed to execute chain', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
}

View File

@@ -0,0 +1,743 @@
/**
* Component command
*
* Manipulate Unity Components on GameObjects.
*/
import { Command } from 'commander';
import * as logger from '@/utils/logger';
import * as config from '@/utils/config';
import { createUnityClient } from '@/unity/client';
import { COMMANDS } from '@/constants';
import type {
ComponentInfo,
ComponentListResult,
PropertyInfo,
GetComponentResult,
SetPropertyResult,
InspectComponentResult,
} from '@/unity/protocol';
import { output, outputJson } from '@/utils/output-formatter';
/**
* Register Component command
*/
export function registerComponentCommand(program: Command): void {
const compCmd = program
.command('component')
.alias('comp')
.description('Manipulate Unity Components on GameObjects');
// List components
compCmd
.command('list')
.description('List all components on a GameObject')
.argument('<gameobject>', 'GameObject name or path')
.option('--include-disabled', 'Include disabled components')
.option('--type-only', 'Show only component types')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '30000')
.action(async (gameobject, options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
await client.connect();
logger.info(`Listing components on '${gameobject}'...`);
const result = await client.sendRequest<ComponentListResult>(
COMMANDS.COMPONENT_LIST,
{ name: gameobject, includeDisabled: options.includeDisabled }
);
if (!result) {
logger.error('Failed to list components');
process.exit(1);
}
// JSON output
if (options.json) {
outputJson(result);
return;
}
// Text output
logger.info(`✓ Components on '${gameobject}':`);
if (result.count === 0) {
logger.info(' (No components)');
return;
}
for (const comp of result.components) {
let icon = '●'; // Active built-in component
if (!comp.enabled) icon = '○'; // Disabled component
if (comp.isMonoBehaviour) icon = '★'; // MonoBehaviour
if (options.typeOnly) {
logger.info(` ${icon} ${comp.type}`);
} else {
const status = comp.enabled ? '' : ' (disabled)';
logger.info(` ${icon} ${comp.type}${status}`);
}
}
logger.info(`\n Total: ${result.count} component(s)`);
} catch (error) {
logger.error('Failed to list components', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// Add component
compCmd
.command('add')
.description('Add a component to a GameObject')
.argument('<gameobject>', 'GameObject name or path')
.argument('<component>', 'Component type name (e.g., Rigidbody, BoxCollider, AudioSource)')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '30000')
.action(async (gameobject, component, options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
await client.connect();
logger.info(`Adding component to '${gameobject}': ${component}`);
const result = await client.sendRequest<ComponentInfo>(
COMMANDS.COMPONENT_ADD,
{ name: gameobject, componentType: component }
);
if (!result) {
logger.error('Failed to add component');
process.exit(1);
}
// JSON output
if (options.json) {
outputJson({ success: true, component: result });
return;
}
// Text output
logger.info('✓ Component added:');
logger.info(` Type: ${result.type}`);
logger.info(` Full Name: ${result.fullTypeName}`);
logger.info(` Enabled: ${result.enabled}`);
logger.info(` Is MonoBehaviour: ${result.isMonoBehaviour}`);
logger.info('\n Tip: Use Ctrl+Z in Unity Editor to undo');
} catch (error) {
logger.error('Failed to add component', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// Remove component
compCmd
.command('remove')
.description('Remove a component from a GameObject')
.argument('<gameobject>', 'GameObject name or path')
.argument('<component>', 'Component type name')
.option('--force', 'Confirm removal without prompt')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '30000')
.action(async (gameobject, component, options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
await client.connect();
logger.info(`Removing component from '${gameobject}': ${component}`);
const result = await client.sendRequest<{ success: boolean }>(
COMMANDS.COMPONENT_REMOVE,
{ name: gameobject, componentType: component }
);
if (!result?.success) {
logger.error('Failed to remove component');
process.exit(1);
}
// JSON output
if (options.json) {
outputJson({ success: true });
return;
}
// Text output
logger.info('✓ Component removed:');
logger.info(` Type: ${component}`);
logger.info('\n Tip: Use Ctrl+Z in Unity Editor to undo');
} catch (error) {
logger.error('Failed to remove component', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// Enable component
compCmd
.command('enable')
.description('Enable a component on a GameObject')
.argument('<gameobject>', 'GameObject name or path')
.argument('<component>', 'Component type name')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '30000')
.action(async (gameobject, component, options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
await client.connect();
logger.info(`Enabling component on '${gameobject}': ${component}`);
const result = await client.sendRequest<{ success: boolean; enabled: boolean }>(
COMMANDS.COMPONENT_SET_ENABLED,
{ name: gameobject, componentType: component, enabled: true }
);
if (!result?.success) {
logger.error('Failed to enable component');
process.exit(1);
}
// JSON output
if (options.json) {
outputJson({ success: true, enabled: result.enabled });
return;
}
// Text output
logger.info('✓ Component enabled:');
logger.info(` Type: ${component}`);
logger.info(` Enabled: ${result.enabled}`);
} catch (error) {
logger.error('Failed to enable component', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// Disable component
compCmd
.command('disable')
.description('Disable a component on a GameObject')
.argument('<gameobject>', 'GameObject name or path')
.argument('<component>', 'Component type name')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '30000')
.action(async (gameobject, component, options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
await client.connect();
logger.info(`Disabling component on '${gameobject}': ${component}`);
const result = await client.sendRequest<{ success: boolean; enabled: boolean }>(
COMMANDS.COMPONENT_SET_ENABLED,
{ name: gameobject, componentType: component, enabled: false }
);
if (!result?.success) {
logger.error('Failed to disable component');
process.exit(1);
}
// JSON output
if (options.json) {
outputJson({ success: true, enabled: result.enabled });
return;
}
// Text output
logger.info('✓ Component disabled:');
logger.info(` Type: ${component}`);
logger.info(` Enabled: ${result.enabled}`);
} catch (error) {
logger.error('Failed to disable component', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// Get component properties
compCmd
.command('get')
.description('Get component properties')
.argument('<gameobject>', 'GameObject name or path')
.argument('<component>', 'Component type name')
.argument('[property]', 'Optional: specific property name (if not specified, list all properties)')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '30000')
.action(async (gameobject, component, property, options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
await client.connect();
if (property) {
logger.info(`Getting property '${property}' from ${component} on '${gameobject}'...`);
} else {
logger.info(`Getting properties from ${component} on '${gameobject}'...`);
}
const result = property
? await client.sendRequest<PropertyInfo>(
COMMANDS.COMPONENT_GET,
{ name: gameobject, componentType: component, property }
)
: await client.sendRequest<GetComponentResult>(
COMMANDS.COMPONENT_GET,
{ name: gameobject, componentType: component }
);
if (!result) {
logger.error('Failed to get component properties');
process.exit(1);
}
// JSON output
if (options.json) {
outputJson(result);
return;
}
// Text output for single property
if (property && 'name' in result) {
logger.info('✓ Property value:');
logger.info(` Name: ${result.name}`);
logger.info(` Type: ${result.type}`);
logger.info(` Value: ${JSON.stringify(result.value)}`);
return;
}
// Text output for all properties
if ('properties' in result) {
logger.info(`✓ Properties of ${component}:`);
logger.info('');
const props = result.properties;
if (props.length === 0) {
logger.info(' (No properties)');
} else {
const maxNameLen = Math.max(...props.map((p) => p.name.length));
for (const prop of props) {
const paddedName = prop.name.padEnd(maxNameLen);
logger.info(` ${paddedName} : ${prop.type} = ${JSON.stringify(prop.value)}`);
}
}
}
} catch (error) {
logger.error('Failed to get component properties', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// Set component property
compCmd
.command('set')
.description('Set a component property')
.argument('<gameobject>', 'GameObject name or path')
.argument('<component>', 'Component type name')
.argument('<property>', 'Property name')
.argument('<value>', 'New value (parsed as JSON, or use string for text)')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '30000')
.action(async (gameobject, component, property, value, options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
await client.connect();
logger.info(`Setting ${component}.${property} = ${value} on '${gameobject}'...`);
const result = await client.sendRequest<SetPropertyResult>(
COMMANDS.COMPONENT_SET,
{ name: gameobject, componentType: component, property, value }
);
if (!result?.success) {
logger.error('Failed to set component property');
process.exit(1);
}
// JSON output
if (options.json) {
outputJson(result);
return;
}
// Text output
logger.info('✓ Property updated:');
logger.info(` Property: ${result.property}`);
logger.info(` Old Value: ${JSON.stringify(result.oldValue)}`);
logger.info(` New Value: ${JSON.stringify(result.newValue)}`);
logger.info('\n Tip: Use Ctrl+Z in Unity Editor to undo');
} catch (error) {
logger.error('Failed to set component property', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// Inspect component
compCmd
.command('inspect')
.description('Inspect a component (show all properties and state)')
.argument('<gameobject>', 'GameObject name or path')
.argument('<component>', 'Component type name')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '30000')
.action(async (gameobject, component, options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
await client.connect();
logger.info(`Inspecting ${component} on '${gameobject}'...`);
const result = await client.sendRequest<InspectComponentResult>(
COMMANDS.COMPONENT_INSPECT,
{ name: gameobject, componentType: component }
);
if (!result) {
logger.error('Failed to inspect component');
process.exit(1);
}
// JSON output
if (options.json) {
outputJson(result);
return;
}
// Text output
logger.info('✓ Component Inspection:');
logger.info('');
logger.info(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
logger.info(` ${result.componentType}`);
logger.info(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
const icon = result.enabled ? '●' : '○';
const typeLabel = result.isMonoBehaviour ? '★' : '●';
logger.info(` ${typeLabel} Type: ${result.fullTypeName}`);
logger.info(` ${icon} Enabled: ${result.enabled}`);
logger.info('');
if (result.properties.length === 0) {
logger.info(' (No properties)');
} else {
const maxNameLen = Math.max(...result.properties.map((p) => p.name.length));
for (const prop of result.properties) {
const paddedName = prop.name.padEnd(maxNameLen);
logger.info(` ${paddedName} : ${prop.type} = ${JSON.stringify(prop.value)}`);
}
}
logger.info('');
logger.info(` Total: ${result.propertyCount} properties`);
} catch (error) {
logger.error('Failed to inspect component', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// Move component up
compCmd
.command('move-up')
.description('Move a component up in the component list')
.argument('<gameobject>', 'GameObject name or path')
.argument('<component>', 'Component type name')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '30000')
.action(async (gameobject, component, options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
await client.connect();
logger.info(`Moving ${component} up on '${gameobject}'...`);
const result = await client.sendRequest<{ success: boolean }>(
COMMANDS.COMPONENT_MOVE_UP,
{ name: gameobject, componentType: component }
);
if (!result?.success) {
logger.error('Failed to move component up');
process.exit(1);
}
// JSON output
if (options.json) {
outputJson({ success: true });
return;
}
// Text output
logger.info('✓ Component moved up:');
logger.info(` Type: ${component}`);
logger.info(' Position: moved to higher index');
logger.info('\n Tip: Use Ctrl+Z in Unity Editor to undo');
} catch (error) {
logger.error('Failed to move component up', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// Move component down
compCmd
.command('move-down')
.description('Move a component down in the component list')
.argument('<gameobject>', 'GameObject name or path')
.argument('<component>', 'Component type name')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '30000')
.action(async (gameobject, component, options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
await client.connect();
logger.info(`Moving ${component} down on '${gameobject}'...`);
const result = await client.sendRequest<{ success: boolean }>(
COMMANDS.COMPONENT_MOVE_DOWN,
{ name: gameobject, componentType: component }
);
if (!result?.success) {
logger.error('Failed to move component down');
process.exit(1);
}
// JSON output
if (options.json) {
outputJson({ success: true });
return;
}
// Text output
logger.info('✓ Component moved down:');
logger.info(` Type: ${component}`);
logger.info(' Position: moved to lower index');
logger.info('\n Tip: Use Ctrl+Z in Unity Editor to undo');
} catch (error) {
logger.error('Failed to move component down', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// Copy component
compCmd
.command('copy')
.description('Copy a component from one GameObject to another')
.argument('<source>', 'Source GameObject name or path')
.argument('<component>', 'Component type name')
.argument('<target>', 'Target GameObject name or path')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '30000')
.action(async (source, component, target, options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
await client.connect();
logger.info(`Copying ${component} from '${source}' to '${target}'...`);
const result = await client.sendRequest<{ success: boolean }>(
COMMANDS.COMPONENT_COPY,
{ source, componentType: component, target }
);
if (!result?.success) {
logger.error('Failed to copy component');
process.exit(1);
}
// JSON output
if (options.json) {
outputJson({ success: true });
return;
}
// Text output
logger.info('✓ Component copied:');
logger.info(` Type: ${component}`);
logger.info(` From: ${source}`);
logger.info(` To: ${target}`);
logger.info('\n Tip: Use Ctrl+Z in Unity Editor to undo');
} catch (error) {
logger.error('Failed to copy component', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
}

View File

@@ -0,0 +1,254 @@
/**
* Console command
*
* Access Unity console logs.
*/
import { Command } from 'commander';
import * as logger from '@/utils/logger';
import * as config from '@/utils/config';
import { createUnityClient } from '@/unity/client';
import { COMMANDS } from '@/constants';
import { UnityLogType } from '@/constants';
import type { ConsoleLogEntry } from '@/unity/protocol';
import { outputJson } from '@/utils/output-formatter';
/**
* Get log type icon
*/
function getLogTypeIcon(type: number): string {
switch (type) {
case UnityLogType.ERROR:
case UnityLogType.EXCEPTION:
return '❌';
case UnityLogType.WARNING:
return '⚠️ ';
case UnityLogType.ASSERT:
return '🔴';
default:
return ' ';
}
}
/**
* Get log type name
*/
function getLogTypeName(type: number): string {
switch (type) {
case UnityLogType.ERROR:
return 'ERROR';
case UnityLogType.ASSERT:
return 'ASSERT';
case UnityLogType.WARNING:
return 'WARN';
case UnityLogType.LOG:
return 'LOG';
case UnityLogType.EXCEPTION:
return 'EXCEPTION';
default:
return 'UNKNOWN';
}
}
/**
* Register Console command
*/
export function registerConsoleCommand(program: Command): void {
const consoleCmd = program
.command('console')
.description('Access Unity console logs');
// Get logs
consoleCmd
.command('logs')
.description('Get Unity console logs')
.option('-n, --limit <number>', 'Number of recent logs to fetch', '50')
.option('-e, --errors-only', 'Show only errors and exceptions')
.option('-w, --warnings', 'Include warnings')
.option('-t, --type <type>', 'Filter by log type: error, warning, log, exception, assert')
.option('-f, --filter <text>', 'Filter logs by text (case-insensitive)')
.option('-s, --stack', 'Show stack traces (default: title only)')
.option('--stack-lines <number>', 'Number of stack trace lines to show (default: 5)', '5')
.option('-v, --verbose', 'Show full messages and complete stack traces')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '300000')
.action(async (options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
await client.connect();
let result = await client.sendRequest<ConsoleLogEntry[]>(
COMMANDS.CONSOLE_GET_LOGS,
{
count: parseInt(options.limit, 10),
errorsOnly: options.errorsOnly || false,
includeWarnings: options.warnings || false,
}
);
// Apply type filter if provided (overrides --errors-only and --warnings)
if (options.type) {
const typeFilter = options.type.toLowerCase();
const typeMap: { [key: string]: number } = {
'error': UnityLogType.ERROR,
'warning': UnityLogType.WARNING,
'log': UnityLogType.LOG,
'exception': UnityLogType.EXCEPTION,
'assert': UnityLogType.ASSERT,
};
const targetType = typeMap[typeFilter];
if (targetType === undefined) {
logger.error(`Invalid log type: ${options.type}. Valid types: error, warning, log, exception, assert`);
process.exit(1);
}
result = result.filter(log => log.type === targetType);
}
// Apply text filter if provided
if (options.filter) {
const filterText = options.filter.toLowerCase();
result = result.filter(log =>
log.message.toLowerCase().includes(filterText) ||
(log.stackTrace && log.stackTrace.toLowerCase().includes(filterText))
);
}
if (!result || result.length === 0) {
if (options.json) {
outputJson({ logs: [], total: 0, filter: options.filter || null });
} else {
logger.info(options.filter ? `No logs found matching filter: "${options.filter}"` : 'No logs found');
}
return;
}
// JSON output
if (options.json) {
outputJson({
logs: result.map(log => ({
type: getLogTypeName(log.type),
timestamp: log.timestamp,
message: log.message,
stackTrace: log.stackTrace || null,
})),
total: result.length,
filter: options.filter || null,
});
return;
}
// Text output
logger.info('✓ Unity Console Logs:');
const showStack = options.stack || options.verbose;
const stackLineCount = options.verbose ? Infinity : parseInt(options.stackLines, 10);
for (const log of result) {
const icon = getLogTypeIcon(log.type);
const typeName = getLogTypeName(log.type);
// Extract first line as title (or full message if --verbose)
const messageLines = log.message.split('\n');
const title = messageLines[0];
// 타임스탬프가 있으면 표시, 없으면 생략
const timestampPart = log.timestamp ? `[${log.timestamp}] ` : '';
if (options.verbose) {
// Show full message
logger.info(`${icon} ${timestampPart}[${typeName}]`);
logger.info('Stack Trace:');
for (const line of messageLines) {
logger.info(line);
}
} else {
// Show title only (first line) - single line format
logger.info(`${icon} ${timestampPart}[${typeName}] ${title}`);
}
// Show stack trace if --stack or --verbose
if (showStack && log.stackTrace && log.stackTrace.trim()) {
if (!options.verbose) {
logger.info('Stack Trace:');
}
const stackLines = log.stackTrace.split('\n').filter(line => line.trim());
// Show specified number of lines
const linesToShow = stackLineCount === Infinity ? stackLines : stackLines.slice(0, stackLineCount);
for (const line of linesToShow) {
logger.info(line);
}
if (stackLineCount !== Infinity && stackLines.length > stackLineCount) {
logger.info(`... (${stackLines.length - stackLineCount} more lines, use --verbose to see all)`);
}
}
}
logger.info(`Total: ${result.length} log(s)`);
} catch (error) {
logger.error('Failed to get console logs', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// Clear console
consoleCmd
.command('clear')
.description('Clear Unity console logs')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '300000')
.action(async (options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
await client.connect();
await client.sendRequest(COMMANDS.CONSOLE_CLEAR);
if (options.json) {
outputJson({ success: true, message: 'Console cleared' });
} else {
logger.info('✓ Console cleared');
}
} catch (error) {
logger.error('Failed to clear console', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
}

View File

@@ -0,0 +1,698 @@
/**
* Database command
*
* SQLite database management commands (connect, disconnect, reset, status)
*/
import { Command } from 'commander';
import * as logger from '@/utils/logger';
import * as config from '@/utils/config';
import { createUnityClient } from '@/unity/client';
import { COMMANDS } from '@/constants';
import { outputJson } from '@/utils/output-formatter';
// Response types
interface DatabaseStatusResponse {
isInitialized: boolean;
isConnected: boolean;
isEnabled: boolean;
databaseFilePath: string;
databaseFileExists: boolean;
undoCount: number;
redoCount: number;
}
interface OperationResponse {
success: boolean;
message: string;
}
interface MigrationResponse extends OperationResponse {
migrationsApplied: number;
}
interface UndoRedoResponse extends OperationResponse {
commandName: string;
remainingUndo: number;
remainingRedo: number;
}
interface HistoryEntry {
name: string;
timestamp: string;
canUndo: boolean;
}
interface HistoryResponse {
undoStack: HistoryEntry[];
redoStack: HistoryEntry[];
totalUndo: number;
totalRedo: number;
}
interface QueryResponse extends OperationResponse {
rows: Record<string, unknown>[];
columns: string[];
rowCount: number;
}
/**
* Register Database command
*/
export function registerDatabaseCommand(program: Command): void {
const dbCmd = program
.command('db')
.description('SQLite database management commands');
// Status
dbCmd
.command('status')
.description('Get database connection status')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '30000')
.action(async (options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
logger.info('Connecting to Unity Editor...');
await client.connect();
logger.info('Getting database status...');
const result = await client.sendRequest(COMMANDS.DATABASE_STATUS) as DatabaseStatusResponse;
if (options.json) {
outputJson(result);
} else {
logger.info('✓ Database Status');
logger.info(` Initialized: ${result.isInitialized ? '✓' : '❌'}`);
logger.info(` Connected: ${result.isConnected ? '✓' : '❌'}`);
logger.info(` Enabled: ${result.isEnabled ? '✓' : '❌'}`);
logger.info(` File Path: ${result.databaseFilePath}`);
logger.info(` File Exists: ${result.databaseFileExists ? '✓' : '❌'}`);
logger.info(` Undo Stack: ${result.undoCount}`);
logger.info(` Redo Stack: ${result.redoCount}`);
}
} catch (error) {
logger.error('Failed to get database status', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// Connect
dbCmd
.command('connect')
.description('Connect to SQLite database')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '30000')
.action(async (options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
logger.info('Connecting to Unity Editor...');
await client.connect();
logger.info('Connecting to database...');
const result = await client.sendRequest(COMMANDS.DATABASE_CONNECT) as OperationResponse;
if (options.json) {
outputJson(result);
} else {
if (result.success) {
logger.info(`${result.message}`);
} else {
logger.error(`${result.message}`);
process.exit(1);
}
}
} catch (error) {
logger.error('Failed to connect to database', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// Disconnect
dbCmd
.command('disconnect')
.description('Disconnect from SQLite database')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '30000')
.action(async (options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
logger.info('Connecting to Unity Editor...');
await client.connect();
logger.info('Disconnecting from database...');
const result = await client.sendRequest(COMMANDS.DATABASE_DISCONNECT) as OperationResponse;
if (options.json) {
outputJson(result);
} else {
if (result.success) {
logger.info(`${result.message}`);
} else {
logger.error(`${result.message}`);
process.exit(1);
}
}
} catch (error) {
logger.error('Failed to disconnect from database', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// Reset (delete and recreate)
dbCmd
.command('reset')
.description('Reset database (delete and recreate with fresh migrations)')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '60000')
.action(async (options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
logger.info('Connecting to Unity Editor...');
await client.connect();
logger.info('Resetting database (this will delete all data)...');
const timeout = parseInt(options.timeout, 10);
const result = await client.sendRequest(COMMANDS.DATABASE_RESET, undefined, timeout) as OperationResponse;
if (options.json) {
outputJson(result);
} else {
if (result.success) {
logger.info(`${result.message}`);
} else {
logger.error(`${result.message}`);
process.exit(1);
}
}
} catch (error) {
logger.error('Failed to reset database', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// Run migrations
dbCmd
.command('migrate')
.description('Run pending database migrations')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '60000')
.action(async (options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
logger.info('Connecting to Unity Editor...');
await client.connect();
logger.info('Running database migrations...');
const timeout = parseInt(options.timeout, 10);
const result = await client.sendRequest(COMMANDS.DATABASE_RUN_MIGRATIONS, undefined, timeout) as MigrationResponse;
if (options.json) {
outputJson(result);
} else {
if (result.success) {
logger.info(`${result.message}`);
if (result.migrationsApplied > 0) {
logger.info(` Applied: ${result.migrationsApplied} migration(s)`);
}
} else {
logger.error(`${result.message}`);
process.exit(1);
}
}
} catch (error) {
logger.error('Failed to run migrations', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// Clear migrations (for debugging)
dbCmd
.command('clear-migrations')
.description('Clear migration history (forces re-run on next migrate)')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '30000')
.action(async (options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
logger.info('Connecting to Unity Editor...');
await client.connect();
logger.info('Clearing migration history...');
const result = await client.sendRequest(COMMANDS.DATABASE_CLEAR_MIGRATIONS) as OperationResponse;
if (options.json) {
outputJson(result);
} else {
if (result.success) {
logger.info(`${result.message}`);
} else {
logger.error(`${result.message}`);
process.exit(1);
}
}
} catch (error) {
logger.error('Failed to clear migration history', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// Undo
dbCmd
.command('undo')
.description('Undo last command (Transform, GameObject changes)')
.option('-n, --count <number>', 'Number of commands to undo', '1')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '30000')
.action(async (options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
logger.info('Connecting to Unity Editor...');
await client.connect();
const count = parseInt(options.count, 10);
const results: UndoRedoResponse[] = [];
for (let i = 0; i < count; i++) {
logger.info(`Undoing command ${i + 1}/${count}...`);
const result = await client.sendRequest(COMMANDS.DATABASE_UNDO) as UndoRedoResponse;
results.push(result);
if (!result.success) {
if (options.json) {
outputJson({ results, totalUndone: i, error: result.message });
} else {
if (i === 0) {
logger.error(`${result.message}`);
} else {
logger.warn(`⚠️ Stopped after ${i} undo(s): ${result.message}`);
}
}
if (i === 0) process.exit(1);
break;
}
if (!options.json) {
logger.info(` ↩️ Undone: ${result.commandName}`);
}
}
if (options.json) {
outputJson({ results, totalUndone: results.filter(r => r.success).length });
} else {
const successCount = results.filter(r => r.success).length;
if (successCount > 0) {
const lastResult = results[results.length - 1];
logger.info(`✓ Undone ${successCount} command(s)`);
logger.info(` Remaining: Undo=${lastResult.remainingUndo}, Redo=${lastResult.remainingRedo}`);
}
}
} catch (error) {
logger.error('Failed to undo', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// Redo
dbCmd
.command('redo')
.description('Redo previously undone command')
.option('-n, --count <number>', 'Number of commands to redo', '1')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '30000')
.action(async (options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
logger.info('Connecting to Unity Editor...');
await client.connect();
const count = parseInt(options.count, 10);
const results: UndoRedoResponse[] = [];
for (let i = 0; i < count; i++) {
logger.info(`Redoing command ${i + 1}/${count}...`);
const result = await client.sendRequest(COMMANDS.DATABASE_REDO) as UndoRedoResponse;
results.push(result);
if (!result.success) {
if (options.json) {
outputJson({ results, totalRedone: i, error: result.message });
} else {
if (i === 0) {
logger.error(`${result.message}`);
} else {
logger.warn(`⚠️ Stopped after ${i} redo(s): ${result.message}`);
}
}
if (i === 0) process.exit(1);
break;
}
if (!options.json) {
logger.info(` ↪️ Redone: ${result.commandName}`);
}
}
if (options.json) {
outputJson({ results, totalRedone: results.filter(r => r.success).length });
} else {
const successCount = results.filter(r => r.success).length;
if (successCount > 0) {
const lastResult = results[results.length - 1];
logger.info(`✓ Redone ${successCount} command(s)`);
logger.info(` Remaining: Undo=${lastResult.remainingUndo}, Redo=${lastResult.remainingRedo}`);
}
}
} catch (error) {
logger.error('Failed to redo', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// History
dbCmd
.command('history')
.description('Show command history (undo/redo stacks)')
.option('-n, --limit <number>', 'Maximum entries to show per stack', '10')
.option('--clear', 'Clear all history')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '30000')
.action(async (options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
logger.info('Connecting to Unity Editor...');
await client.connect();
if (options.clear) {
logger.info('Clearing command history...');
const result = await client.sendRequest(COMMANDS.DATABASE_CLEAR_HISTORY) as OperationResponse;
if (options.json) {
outputJson(result);
} else {
if (result.success) {
logger.info(`${result.message}`);
} else {
logger.error(`${result.message}`);
process.exit(1);
}
}
return;
}
logger.info('Getting command history...');
const limit = parseInt(options.limit, 10);
const result = await client.sendRequest(COMMANDS.DATABASE_GET_HISTORY, { limit }) as HistoryResponse;
if (options.json) {
outputJson(result);
} else {
logger.info('✓ Command History');
// Undo stack (most recent first)
logger.info(`📜 Undo Stack (${result.totalUndo} total):`);
if (result.undoStack.length === 0) {
logger.info(' (empty)');
} else {
for (let i = 0; i < result.undoStack.length; i++) {
const entry = result.undoStack[i];
const marker = i === 0 ? '→' : ' ';
logger.info(` ${marker} ${i + 1}. ${entry.name} [${entry.timestamp}]`);
}
if (result.totalUndo > result.undoStack.length) {
logger.info(` ... and ${result.totalUndo - result.undoStack.length} more`);
}
}
// Redo stack
logger.info(`🔄 Redo Stack (${result.totalRedo} total):`);
if (result.redoStack.length === 0) {
logger.info(' (empty)');
} else {
for (let i = 0; i < result.redoStack.length; i++) {
const entry = result.redoStack[i];
const marker = i === 0 ? '→' : ' ';
logger.info(` ${marker} ${i + 1}. ${entry.name} [${entry.timestamp}]`);
}
if (result.totalRedo > result.redoStack.length) {
logger.info(` ... and ${result.totalRedo - result.redoStack.length} more`);
}
}
}
} catch (error) {
logger.error('Failed to get history', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// Query (table-based)
dbCmd
.command('query')
.description('Query database table (migrations, command_history)')
.argument('<table>', 'Table name to query (migrations, command_history)')
.option('-n, --limit <number>', 'Maximum rows to return', '100')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '30000')
.action(async (table: string, options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
logger.info('Connecting to Unity Editor...');
await client.connect();
logger.info(`Querying table: ${table}...`);
const limit = parseInt(options.limit, 10);
const result = await client.sendRequest(COMMANDS.DATABASE_QUERY, { table, limit }) as QueryResponse;
if (options.json) {
outputJson(result);
} else {
if (result.success) {
logger.info(`${result.message}`);
if (result.rowCount === 0) {
logger.info('(no rows returned)');
} else {
// Display as table
const columns = result.columns;
const rows = result.rows;
// Calculate column widths
const colWidths: number[] = columns.map(col => col.length);
for (const row of rows) {
for (let i = 0; i < columns.length; i++) {
const value = String(row[columns[i]] ?? 'NULL');
colWidths[i] = Math.max(colWidths[i], Math.min(value.length, 50));
}
}
// Print header
const header = columns.map((col, i) => col.padEnd(colWidths[i])).join(' | ');
logger.info(header);
logger.info('-'.repeat(header.length));
// Print rows
for (const row of rows) {
const rowStr = columns.map((col, i) => {
let value = String(row[col] ?? 'NULL');
if (value.length > 50) {
value = value.substring(0, 47) + '...';
}
return value.padEnd(colWidths[i]);
}).join(' | ');
logger.info(rowStr);
}
logger.info(`${result.rowCount} row(s) returned`);
}
} else {
logger.error(`${result.message}`);
process.exit(1);
}
}
} catch (error) {
logger.error('Failed to execute query', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
}

View File

@@ -0,0 +1,487 @@
/**
* Editor command
*
* Unity Editor utility commands (refresh, recompile, etc.)
*/
import { Command } from 'commander';
import * as logger from '@/utils/logger';
import * as config from '@/utils/config';
import { createUnityClient } from '@/unity/client';
import { COMMANDS } from '@/constants';
import { outputJson } from '@/utils/output-formatter';
/**
* Register Editor command
*/
export function registerEditorCommand(program: Command): void {
const editorCmd = program
.command('editor')
.description('Unity Editor utility commands');
// Refresh AssetDatabase
editorCmd
.command('refresh')
.description('Refresh Unity AssetDatabase')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '300000')
.action(async (options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
logger.info('Connecting to Unity Editor...');
await client.connect();
logger.info('Refreshing AssetDatabase...');
await client.sendRequest(COMMANDS.EDITOR_REFRESH);
// JSON output
if (options.json) {
outputJson({
success: true,
message: 'AssetDatabase refreshed',
});
} else {
logger.info('✓ AssetDatabase refreshed');
logger.warn('⚠ Please check Unity Editor for compilation status');
}
} catch (error) {
logger.error('Failed to refresh AssetDatabase', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// Recompile scripts
editorCmd
.command('recompile')
.description('Recompile Unity scripts')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '300000')
.action(async (options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
logger.info('Connecting to Unity Editor...');
await client.connect();
logger.info('Requesting script recompilation...');
await client.sendRequest(COMMANDS.EDITOR_RECOMPILE);
// JSON output
if (options.json) {
outputJson({
success: true,
message: 'Script recompilation requested',
});
} else {
logger.info('✓ Script recompilation requested');
logger.warn('⚠ Please check Unity Editor for compilation status');
}
} catch (error) {
logger.error('Failed to recompile scripts', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// Reimport asset
editorCmd
.command('reimport')
.description('Reimport asset at path')
.argument('<path>', 'Asset path relative to Assets folder')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '300000')
.action(async (path, options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
logger.info('Connecting to Unity Editor...');
await client.connect();
logger.info(`Reimporting asset: ${path}`);
await client.sendRequest(COMMANDS.EDITOR_REIMPORT, { path });
// JSON output
if (options.json) {
outputJson({
success: true,
path,
message: 'Asset reimported',
});
} else {
logger.info('✓ Asset reimported');
logger.warn('⚠ Please check Unity Editor for compilation status');
}
} catch (error) {
logger.error('Failed to reimport asset', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// Execute method
editorCmd
.command('execute')
.description('Execute a static method marked with [ExecutableMethod]')
.argument('<commandName>', 'Command name to execute (e.g., reinstall-cli)')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '300000')
.action(async (commandName, options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
await client.connect();
interface ExecuteResponse {
success: boolean;
commandName: string;
message: string;
}
const timeout = parseInt(options.timeout, 10);
const result = await client.sendRequest<ExecuteResponse>(COMMANDS.EDITOR_EXECUTE, { commandName }, timeout);
// JSON output
if (options.json) {
outputJson(result);
} else {
logger.info(`${result.message}`);
}
} catch (error) {
logger.error(`Failed to execute command '${commandName}'`, error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// Get current selection
editorCmd
.command('get-selection')
.alias('selection')
.description('Get currently selected objects in Unity Editor')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '30000')
.action(async (options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
await client.connect();
interface SelectionResult {
success: boolean;
count: number;
activeObject: { name: string; instanceId: number; type: string } | null;
selection: Array<{ name: string; instanceId: number; type: string }>;
}
const result = await client.sendRequest<SelectionResult>(COMMANDS.EDITOR_GET_SELECTION);
if (options.json) {
outputJson(result);
} else {
if (result.count === 0) {
logger.info('No objects selected');
} else {
logger.info(`${result.count} object(s) selected:`);
if (result.activeObject) {
logger.info(`${result.activeObject.name} (${result.activeObject.type}) [Active]`);
}
for (const obj of result.selection) {
if (!result.activeObject || obj.instanceId !== result.activeObject.instanceId) {
logger.info(`${obj.name} (${obj.type})`);
}
}
}
}
} catch (error) {
logger.error('Failed to get selection', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// Set selection
editorCmd
.command('set-selection')
.alias('select')
.description('Select objects in Unity Editor')
.argument('<objects...>', 'GameObject names or paths to select')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '30000')
.action(async (objects, options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
await client.connect();
interface SetSelectionResult {
success: boolean;
selectedCount: number;
selectedNames: string[];
}
const result = await client.sendRequest<SetSelectionResult>(
COMMANDS.EDITOR_SET_SELECTION,
{ names: objects }
);
if (options.json) {
outputJson(result);
} else {
if (result.selectedCount === 0) {
logger.warn('No objects were selected (not found)');
} else {
logger.info(`✓ Selected ${result.selectedCount} object(s):`);
for (const name of result.selectedNames) {
logger.info(`${name}`);
}
}
}
} catch (error) {
logger.error('Failed to set selection', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// Focus Game View
editorCmd
.command('focus-game')
.alias('game')
.description('Focus on Unity Game View')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '30000')
.action(async (options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
await client.connect();
await client.sendRequest(COMMANDS.EDITOR_FOCUS_GAME_VIEW);
if (options.json) {
outputJson({ success: true, message: 'Focused on Game View' });
} else {
logger.info('✓ Focused on Game View');
}
} catch (error) {
logger.error('Failed to focus Game View', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// Focus Scene View
editorCmd
.command('focus-scene')
.alias('scene')
.description('Focus on Unity Scene View')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '30000')
.action(async (options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
await client.connect();
await client.sendRequest(COMMANDS.EDITOR_FOCUS_SCENE_VIEW);
if (options.json) {
outputJson({ success: true, message: 'Focused on Scene View' });
} else {
logger.info('✓ Focused on Scene View');
}
} catch (error) {
logger.error('Failed to focus Scene View', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// List executable methods
editorCmd
.command('list')
.description('List all executable methods available via execute command')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '300000')
.action(async (options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
await client.connect();
interface ExecutableMethod {
commandName: string;
description: string;
className: string;
methodName: string;
}
interface ListResponse {
success: boolean;
count: number;
methods: ExecutableMethod[];
}
const result = await client.sendRequest<ListResponse>(COMMANDS.EDITOR_LIST_EXECUTABLE);
// JSON output
if (options.json) {
outputJson(result);
} else {
logger.info(`✓ Found ${result.count} executable method(s):`);
for (const method of result.methods) {
logger.info(` ${method.commandName}`);
if (method.description) {
logger.info(` ${method.description}`);
}
logger.info(` ${method.className}.${method.methodName}`);
}
}
} catch (error) {
logger.error('Failed to list executable methods', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
}

View File

@@ -0,0 +1,285 @@
/**
* GameObject command
*
* Manipulate Unity GameObjects.
*/
import { Command } from 'commander';
import * as logger from '@/utils/logger';
import * as config from '@/utils/config';
import { createUnityClient } from '@/unity/client';
import { COMMANDS } from '@/constants';
import type { GameObjectInfo } from '@/unity/protocol';
import { output, outputJson } from '@/utils/output-formatter';
/**
* Register GameObject command
*/
export function registerGameObjectCommand(program: Command): void {
const goCmd = program
.command('gameobject')
.alias('go')
.description('Manipulate Unity GameObjects');
// Find GameObject
goCmd
.command('find')
.description('Find GameObject by name or path')
.argument('<name>', 'GameObject name or path')
.option('-c, --with-components', 'Include component list')
.option('--with-children', 'Include children hierarchy')
.option('--full', 'Include all details (components + children)')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '300000')
.action(async (name, options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
logger.info(`Connecting to Unity Editor...`);
await client.connect();
logger.info(`Finding GameObject: ${name}`);
const result = await client.sendRequest<GameObjectInfo>(
COMMANDS.GAMEOBJECT_FIND,
{ name }
);
if (!result) {
if (options.json) {
outputJson({ found: false, gameObject: null });
} else {
logger.info('GameObject not found');
}
return;
}
// JSON output
if (options.json) {
outputJson({
found: true,
gameObject: result,
});
return;
}
// Text output
const showComponents = options.withComponents || options.full;
const showChildren = options.withChildren || options.full;
logger.info('✓ GameObject found:');
logger.info(` Name: ${result.name}`);
logger.info(` Instance ID: ${result.instanceId}`);
logger.info(` Path: ${result.path}`);
logger.info(` Active: ${result.active}`);
logger.info(` Tag: ${result.tag}`);
logger.info(` Layer: ${result.layer}`);
// Show components if requested
if (showComponents && result.components && result.components.length > 0) {
logger.info(` Components (${result.components.length}):`);
for (const component of result.components) {
logger.info(` - ${component}`);
}
}
// Show children if requested
if (showChildren && result.children && result.children.length > 0) {
logger.info(` Children (${result.children.length}):`);
const formatChild = (child: any, indent = 2): void => {
const prefix = ' '.repeat(indent);
const activeIcon = child.active ? '●' : '○';
logger.info(`${prefix}${activeIcon} ${child.name} (ID: ${child.instanceId})`);
if (child.children && child.children.length > 0) {
for (const grandChild of child.children) {
formatChild(grandChild, indent + 1);
}
}
};
for (const child of result.children) {
formatChild(child);
}
}
} catch (error) {
logger.error('Failed to find GameObject', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// Create GameObject
goCmd
.command('create')
.description('Create new GameObject')
.argument('<name>', 'GameObject name')
.option('-p, --parent <name>', 'Parent GameObject name or path')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '300000')
.action(async (name, options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
logger.info('Connecting to Unity Editor...');
await client.connect();
logger.info(`Creating GameObject: ${name}`);
const result = await client.sendRequest<GameObjectInfo>(
COMMANDS.GAMEOBJECT_CREATE,
{
name,
parent: options.parent,
}
);
// JSON output
if (options.json) {
outputJson({
success: true,
gameObject: result,
});
return;
}
// Text output
logger.info('✓ GameObject created:');
logger.info(` Name: ${result.name}`);
logger.info(` Instance ID: ${result.instanceId}`);
logger.info(` Path: ${result.path}`);
} catch (error) {
logger.error('Failed to create GameObject', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// Destroy GameObject
goCmd
.command('destroy')
.description('Destroy GameObject')
.argument('<name>', 'GameObject name or path')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '300000')
.action(async (name, options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
logger.info('Connecting to Unity Editor...');
await client.connect();
logger.info(`Destroying GameObject: ${name}`);
await client.sendRequest(COMMANDS.GAMEOBJECT_DESTROY, { name });
// JSON output
if (options.json) {
outputJson({ success: true, message: `GameObject '${name}' destroyed` });
} else {
logger.info('✓ GameObject destroyed');
}
} catch (error) {
logger.error('Failed to destroy GameObject', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// Set active state
goCmd
.command('set-active')
.description('Set GameObject active state')
.argument('<name>', 'GameObject name or path')
.argument('<active>', 'Active state (true/false)', (value) => value === 'true')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '300000')
.action(async (name, active, options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
logger.info('Connecting to Unity Editor...');
await client.connect();
logger.info(`Setting GameObject active state: ${name}${active}`);
await client.sendRequest(COMMANDS.GAMEOBJECT_SET_ACTIVE, {
name,
active,
});
// JSON output
if (options.json) {
outputJson({
success: true,
gameObject: name,
active: active,
});
} else {
logger.info(`✓ GameObject active state set to ${active}`);
}
} catch (error) {
logger.error('Failed to set active state', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
}

View File

@@ -0,0 +1,167 @@
/**
* Hierarchy command
*
* Query Unity GameObject hierarchy.
*/
import { Command } from 'commander';
import * as logger from '@/utils/logger';
import { getUnityPortOrExit, connectToUnity, disconnectUnity } from '@/utils/command-helpers';
import { COMMANDS, UNITY } from '@/constants';
import type { GameObjectInfo } from '@/unity/protocol';
import { output, outputJson } from '@/utils/output-formatter';
/**
* Format hierarchy tree
*/
function formatHierarchy(obj: GameObjectInfo, indent = 0, maxDepth?: number, withComponents = false): string {
const prefix = ' '.repeat(indent);
const activeIcon = obj.active ? '●' : '○';
let result = `${prefix}${activeIcon} ${obj.name} (ID: ${obj.instanceId})`;
// Add components if requested
if (withComponents && obj.components && obj.components.length > 0) {
result += ` [${obj.components.join(', ')}]`;
}
// Recurse into children if within depth limit
if (obj.children && obj.children.length > 0) {
if (maxDepth === undefined || indent < maxDepth - 1) {
for (const child of obj.children) {
result += '\n' + formatHierarchy(child, indent + 1, maxDepth, withComponents);
}
} else if (maxDepth !== undefined && indent >= maxDepth - 1) {
result += `\n${prefix} ... (${obj.children.length} children, use --depth to see more)`;
}
}
return result;
}
/**
* Register hierarchy command
*/
/**
* Filter hierarchy by name (recursively)
*/
function filterHierarchyByName(objects: GameObjectInfo[], nameFilter: string): GameObjectInfo[] {
const filterLower = nameFilter.toLowerCase();
const result: GameObjectInfo[] = [];
for (const obj of objects) {
// Check if this object matches
const matches = obj.name.toLowerCase().includes(filterLower);
// Filter children recursively
let filteredChildren: GameObjectInfo[] = [];
if (obj.children && obj.children.length > 0) {
filteredChildren = filterHierarchyByName(obj.children, nameFilter);
}
// Include this object if it matches or has matching children
if (matches || filteredChildren.length > 0) {
result.push({
...obj,
children: filteredChildren,
});
}
}
return result;
}
export function registerHierarchyCommand(program: Command): void {
const hierarchyCmd = program
.command('hierarchy')
.description('Query Unity GameObject hierarchy')
.option('-r, --root-only', 'Show only root GameObjects')
.option('-i, --include-inactive', 'Include inactive GameObjects')
.option('-a, --active-only', 'Show only active GameObjects (opposite of -i)')
.option('-d, --depth <n>', 'Limit hierarchy depth (e.g., 2 for 2 levels)')
.option('-l, --limit <n>', 'Limit number of root GameObjects to show')
.option('-n, --count <n>', 'Same as --limit (alternative alias)')
.option('-f, --filter <name>', 'Filter GameObjects by name (case-insensitive)')
.option('-c, --with-components', 'Include component information')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '300000')
.action(async (options) => {
let client = null;
try {
const port = getUnityPortOrExit(program);
client = await connectToUnity(port);
logger.info('Querying hierarchy...');
const timeout = options.timeout ? parseInt(options.timeout, 10) : UNITY.HIERARCHY_TIMEOUT;
// Handle active-only vs include-inactive
let includeInactive = options.includeInactive || false;
if (options.activeOnly) {
includeInactive = false; // --active-only overrides
}
let result = await client.sendRequest<GameObjectInfo[]>(
COMMANDS.HIERARCHY_GET,
{
rootOnly: options.rootOnly || false,
includeInactive,
},
timeout
);
// Apply name filter if provided
if (options.filter) {
result = filterHierarchyByName(result, options.filter);
}
// Apply limit if provided (--limit or --count)
const limitValue = options.limit || options.count;
if (limitValue) {
const limit = parseInt(limitValue, 10);
if (!isNaN(limit) && limit > 0) {
result = result.slice(0, limit);
}
}
if (!result || result.length === 0) {
if (options.json) {
outputJson({
hierarchy: [],
total: 0,
filter: options.filter || null,
depth: options.depth ? parseInt(options.depth, 10) : null,
});
} else {
logger.info(options.filter ? `No GameObjects found matching filter: "${options.filter}"` : 'No GameObjects found');
}
return;
}
// JSON output
if (options.json) {
outputJson({
hierarchy: result,
total: result.length,
});
return;
}
// Text output
const maxDepth = options.depth ? parseInt(options.depth, 10) : undefined;
const withComponents = options.withComponents || false;
logger.info('Unity Hierarchy:');
for (const obj of result) {
logger.info(formatHierarchy(obj, 0, maxDepth, withComponents));
}
let summary = `Total: ${result.length} root GameObject(s)`;
if (options.filter) summary += ` (filtered by: "${options.filter}")`;
if (maxDepth) summary += ` (depth: ${maxDepth})`;
logger.info(summary);
} catch (error) {
logger.error('Failed to query hierarchy', error);
process.exit(1);
} finally {
disconnectUnity(client);
}
});
}

View File

@@ -0,0 +1,628 @@
/**
* Material command
*
* Material property manipulation commands
*/
import { Command } from 'commander';
import * as logger from '@/utils/logger';
import * as config from '@/utils/config';
import { createUnityClient } from '@/unity/client';
import { COMMANDS } from '@/constants';
import { outputJson } from '@/utils/output-formatter';
/**
* Register Material command
*/
export function registerMaterialCommand(program: Command): void {
const materialCmd = program
.command('material')
.description('Material property manipulation commands');
// List materials on a GameObject
materialCmd
.command('list')
.description('List all materials on a GameObject')
.argument('<gameObject>', 'GameObject name or path')
.option('--shared', 'Use shared materials instead of instanced')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '30000')
.action(async (gameObject, options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
await client.connect();
interface MaterialInfo {
index: number;
name: string;
shader: string;
}
interface ListResult {
success: boolean;
gameObject: string;
count: number;
materials: MaterialInfo[];
}
const result = await client.sendRequest<ListResult>(COMMANDS.MATERIAL_LIST, {
gameObject,
useShared: options.shared || false,
});
if (options.json) {
outputJson(result);
} else {
logger.info(`${result.count} material(s) on ${result.gameObject}:`);
for (const mat of result.materials) {
logger.info(` [${mat.index}] ${mat.name} (${mat.shader})`);
}
}
} catch (error) {
logger.error('Failed to list materials', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// Get material property
materialCmd
.command('get')
.description('Get a material property value')
.argument('<gameObject>', 'GameObject name or path')
.argument('<property>', 'Property name (e.g., _Metallic, _Smoothness)')
.option('--index <n>', 'Material index (default: 0)', '0')
.option('--shared', 'Use shared material instead of instanced')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '30000')
.action(async (gameObject, property, options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
await client.connect();
interface PropertyResult {
success: boolean;
gameObject: string;
material: string;
propertyName: string;
propertyType: string;
value: unknown;
}
const result = await client.sendRequest<PropertyResult>(COMMANDS.MATERIAL_GET_PROPERTY, {
gameObject,
propertyName: property,
materialIndex: parseInt(options.index, 10),
useShared: options.shared || false,
});
if (options.json) {
outputJson(result);
} else {
logger.info(`${result.material}.${result.propertyName} (${result.propertyType}):`);
logger.info(` Value: ${JSON.stringify(result.value)}`);
}
} catch (error) {
logger.error('Failed to get material property', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// Set material property
materialCmd
.command('set')
.description('Set a material property value (float or int)')
.argument('<gameObject>', 'GameObject name or path')
.argument('<property>', 'Property name (e.g., _Metallic, _Smoothness)')
.argument('<value>', 'New value (number)')
.option('--index <n>', 'Material index (default: 0)', '0')
.option('--shared', 'Use shared material instead of instanced')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '30000')
.action(async (gameObject, property, value, options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
await client.connect();
interface SetResult {
success: boolean;
gameObject: string;
material: string;
propertyName: string;
propertyType: string;
value: number;
}
const result = await client.sendRequest<SetResult>(COMMANDS.MATERIAL_SET_PROPERTY, {
gameObject,
propertyName: property,
value: parseFloat(value),
materialIndex: parseInt(options.index, 10),
useShared: options.shared || false,
});
if (options.json) {
outputJson(result);
} else {
logger.info(`✓ Set ${result.material}.${result.propertyName} = ${result.value}`);
}
} catch (error) {
logger.error('Failed to set material property', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// Get material color
materialCmd
.command('get-color')
.alias('color')
.description('Get a material color property')
.argument('<gameObject>', 'GameObject name or path')
.option('--property <name>', 'Color property name (default: _Color)', '_Color')
.option('--index <n>', 'Material index (default: 0)', '0')
.option('--shared', 'Use shared material instead of instanced')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '30000')
.action(async (gameObject, options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
await client.connect();
interface ColorResult {
success: boolean;
gameObject: string;
material: string;
propertyName: string;
color: {
r: number;
g: number;
b: number;
a: number;
hex: string;
};
}
const result = await client.sendRequest<ColorResult>(COMMANDS.MATERIAL_GET_COLOR, {
gameObject,
propertyName: options.property,
materialIndex: parseInt(options.index, 10),
useShared: options.shared || false,
});
if (options.json) {
outputJson(result);
} else {
const c = result.color;
logger.info(`${result.material}.${result.propertyName}:`);
logger.info(` RGBA: (${c.r.toFixed(3)}, ${c.g.toFixed(3)}, ${c.b.toFixed(3)}, ${c.a.toFixed(3)})`);
logger.info(` Hex: #${c.hex}`);
}
} catch (error) {
logger.error('Failed to get material color', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// Set material color
materialCmd
.command('set-color')
.description('Set a material color property')
.argument('<gameObject>', 'GameObject name or path')
.option('--property <name>', 'Color property name (default: _Color)', '_Color')
.option('--r <value>', 'Red component (0-1)')
.option('--g <value>', 'Green component (0-1)')
.option('--b <value>', 'Blue component (0-1)')
.option('--a <value>', 'Alpha component (0-1)', '1')
.option('--hex <color>', 'Hex color (e.g., #FF0000 or FF0000)')
.option('--index <n>', 'Material index (default: 0)', '0')
.option('--shared', 'Use shared material instead of instanced')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '30000')
.action(async (gameObject, options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
// Validate input
if (!options.hex && options.r === undefined && options.g === undefined && options.b === undefined) {
logger.error('Please provide either --hex or --r, --g, --b values');
process.exit(1);
}
client = createUnityClient(port);
await client.connect();
interface SetColorResult {
success: boolean;
gameObject: string;
material: string;
propertyName: string;
color: {
r: number;
g: number;
b: number;
a: number;
hex: string;
};
}
const params: Record<string, unknown> = {
gameObject,
propertyName: options.property,
materialIndex: parseInt(options.index, 10),
useShared: options.shared || false,
};
if (options.hex) {
params.hex = options.hex;
} else {
if (options.r !== undefined) params.r = parseFloat(options.r);
if (options.g !== undefined) params.g = parseFloat(options.g);
if (options.b !== undefined) params.b = parseFloat(options.b);
if (options.a !== undefined) params.a = parseFloat(options.a);
}
const result = await client.sendRequest<SetColorResult>(COMMANDS.MATERIAL_SET_COLOR, params);
if (options.json) {
outputJson(result);
} else {
const c = result.color;
logger.info(`✓ Set ${result.material}.${result.propertyName}:`);
logger.info(` RGBA: (${c.r.toFixed(3)}, ${c.g.toFixed(3)}, ${c.b.toFixed(3)}, ${c.a.toFixed(3)})`);
logger.info(` Hex: #${c.hex}`);
}
} catch (error) {
logger.error('Failed to set material color', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// Get shader
materialCmd
.command('get-shader')
.alias('shader')
.description('Get the shader used by a material')
.argument('<gameObject>', 'GameObject name or path')
.option('--index <n>', 'Material index (default: 0)', '0')
.option('--shared', 'Use shared material instead of instanced')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '30000')
.action(async (gameObject, options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
await client.connect();
interface ShaderResult {
success: boolean;
gameObject: string;
material: string;
shader: {
name: string;
propertyCount: number;
} | null;
}
const result = await client.sendRequest<ShaderResult>(COMMANDS.MATERIAL_GET_SHADER, {
gameObject,
materialIndex: parseInt(options.index, 10),
useShared: options.shared || false,
});
if (options.json) {
outputJson(result);
} else {
if (result.shader) {
logger.info(`${result.material} shader:`);
logger.info(` Name: ${result.shader.name}`);
logger.info(` Properties: ${result.shader.propertyCount}`);
} else {
logger.info(`${result.material} has no shader`);
}
}
} catch (error) {
logger.error('Failed to get shader', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// Set shader
materialCmd
.command('set-shader')
.description('Set the shader used by a material')
.argument('<gameObject>', 'GameObject name or path')
.argument('<shaderName>', 'Shader name (e.g., Standard, Unlit/Color)')
.option('--index <n>', 'Material index (default: 0)', '0')
.option('--shared', 'Use shared material instead of instanced')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '30000')
.action(async (gameObject, shaderName, options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
await client.connect();
interface SetShaderResult {
success: boolean;
gameObject: string;
material: string;
shader: string;
}
const result = await client.sendRequest<SetShaderResult>(COMMANDS.MATERIAL_SET_SHADER, {
gameObject,
shaderName,
materialIndex: parseInt(options.index, 10),
useShared: options.shared || false,
});
if (options.json) {
outputJson(result);
} else {
logger.info(`✓ Set ${result.material} shader to: ${result.shader}`);
}
} catch (error) {
logger.error('Failed to set shader', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// Get texture
materialCmd
.command('get-texture')
.alias('texture')
.description('Get a material texture property')
.argument('<gameObject>', 'GameObject name or path')
.option('--property <name>', 'Texture property name (default: _MainTex)', '_MainTex')
.option('--index <n>', 'Material index (default: 0)', '0')
.option('--shared', 'Use shared material instead of instanced')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '30000')
.action(async (gameObject, options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
await client.connect();
interface TextureResult {
success: boolean;
gameObject: string;
material: string;
propertyName: string;
texture: {
name: string;
type: string;
width: number;
height: number;
} | null;
scale: { x: number; y: number };
offset: { x: number; y: number };
}
const result = await client.sendRequest<TextureResult>(COMMANDS.MATERIAL_GET_TEXTURE, {
gameObject,
propertyName: options.property,
materialIndex: parseInt(options.index, 10),
useShared: options.shared || false,
});
if (options.json) {
outputJson(result);
} else {
logger.info(`${result.material}.${result.propertyName}:`);
if (result.texture) {
logger.info(` Texture: ${result.texture.name} (${result.texture.type})`);
logger.info(` Size: ${result.texture.width}x${result.texture.height}`);
} else {
logger.info(' Texture: None');
}
logger.info(` Scale: (${result.scale.x}, ${result.scale.y})`);
logger.info(` Offset: (${result.offset.x}, ${result.offset.y})`);
}
} catch (error) {
logger.error('Failed to get texture', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// Set texture
materialCmd
.command('set-texture')
.description('Set a material texture property')
.argument('<gameObject>', 'GameObject name or path')
.argument('<texturePath>', 'Texture asset path (e.g., Assets/Textures/MyTex.png)')
.option('--property <name>', 'Texture property name (default: _MainTex)', '_MainTex')
.option('--scale-x <value>', 'Texture scale X')
.option('--scale-y <value>', 'Texture scale Y')
.option('--offset-x <value>', 'Texture offset X')
.option('--offset-y <value>', 'Texture offset Y')
.option('--index <n>', 'Material index (default: 0)', '0')
.option('--shared', 'Use shared material instead of instanced')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '30000')
.action(async (gameObject, texturePath, options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
await client.connect();
interface SetTextureResult {
success: boolean;
gameObject: string;
material: string;
propertyName: string;
texture: string;
}
const params: Record<string, unknown> = {
gameObject,
texturePath,
propertyName: options.property,
materialIndex: parseInt(options.index, 10),
useShared: options.shared || false,
};
if (options.scaleX !== undefined) params.scaleX = parseFloat(options.scaleX);
if (options.scaleY !== undefined) params.scaleY = parseFloat(options.scaleY);
if (options.offsetX !== undefined) params.offsetX = parseFloat(options.offsetX);
if (options.offsetY !== undefined) params.offsetY = parseFloat(options.offsetY);
const result = await client.sendRequest<SetTextureResult>(COMMANDS.MATERIAL_SET_TEXTURE, params);
if (options.json) {
outputJson(result);
} else {
logger.info(`✓ Set ${result.material}.${result.propertyName} to: ${result.texture}`);
}
} catch (error) {
logger.error('Failed to set texture', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
}

View File

@@ -0,0 +1,154 @@
/**
* Menu command
*
* Unity Editor menu execution and listing commands
*/
import { Command } from 'commander';
import * as logger from '@/utils/logger';
import * as config from '@/utils/config';
import { createUnityClient } from '@/unity/client';
import { COMMANDS } from '@/constants';
import { outputJson } from '@/utils/output-formatter';
/**
* Register Menu command
*/
export function registerMenuCommand(program: Command): void {
const menuCmd = program
.command('menu')
.description('Unity Editor menu commands');
// Run menu item
menuCmd
.command('run')
.description('Execute a Unity Editor menu item')
.argument('<menuPath>', 'Menu path (e.g., "Edit/Project Settings...")')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '30000')
.action(async (menuPath, options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
logger.info('Connecting to Unity Editor...');
await client.connect();
interface RunResponse {
success: boolean;
menuPath: string;
message: string;
}
logger.info(`Executing menu item: ${menuPath}`);
const timeout = parseInt(options.timeout, 10);
const result = await client.sendRequest<RunResponse>(COMMANDS.MENU_RUN, { menuPath }, timeout);
// JSON output
if (options.json) {
outputJson(result);
} else {
logger.info(`${result.message}`);
}
} catch (error) {
logger.error(`Failed to execute menu item '${menuPath}'`, error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// List menu items
menuCmd
.command('list')
.description('List available Unity Editor menu items')
.option('--filter <pattern>', 'Filter menu items by pattern (supports * wildcard)')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '30000')
.action(async (options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
logger.info('Connecting to Unity Editor...');
await client.connect();
interface MenuItem {
path: string;
category: string;
}
interface ListResponse {
success: boolean;
menus: MenuItem[];
count: number;
}
const params: { filter?: string } = {};
if (options.filter) {
params.filter = options.filter;
}
logger.info('Fetching menu items...');
const timeout = parseInt(options.timeout, 10);
const result = await client.sendRequest<ListResponse>(COMMANDS.MENU_LIST, params, timeout);
// JSON output
if (options.json) {
outputJson(result);
} else {
logger.info(`✓ Found ${result.count} menu item(s):`);
logger.info('');
// Group by category
const categories = new Map<string, string[]>();
for (const menu of result.menus) {
const list = categories.get(menu.category) || [];
list.push(menu.path);
categories.set(menu.category, list);
}
for (const [category, paths] of categories.entries()) {
logger.info(`[${category}]`);
for (const path of paths) {
logger.info(` ${path}`);
}
logger.info('');
}
}
} catch (error) {
logger.error('Failed to list menu items', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
}

View File

@@ -0,0 +1,810 @@
/**
* Prefab command
*
* Manipulate Unity Prefabs - instantiate, create, unpack, apply, revert, and more.
*/
import { Command } from 'commander';
import * as logger from '@/utils/logger';
import * as config from '@/utils/config';
import { createUnityClient } from '@/unity/client';
import { COMMANDS } from '@/constants';
import { output, outputJson } from '@/utils/output-formatter';
// Type definitions
interface Vector3Info {
x: number;
y: number;
z: number;
}
interface InstantiateResult {
success: boolean;
instanceName: string;
prefabPath: string;
position: Vector3Info;
}
interface CreateResult {
success: boolean;
prefabPath: string;
sourceName: string;
isConnected: boolean;
}
interface ApplyResult {
success: boolean;
instanceName: string;
prefabPath: string;
}
interface VariantResult {
success: boolean;
sourcePath: string;
variantPath: string;
}
interface OverrideInfo {
type: string;
targetName: string;
targetType: string;
}
interface GetOverridesResult {
instanceName: string;
hasOverrides: boolean;
overrideCount: number;
overrides: OverrideInfo[];
}
interface GetSourceResult {
instanceName: string;
isPrefabInstance: boolean;
prefabPath: string | null;
prefabType: string;
prefabStatus?: string;
}
interface IsInstanceResult {
name: string;
isPrefabInstance: boolean;
isPrefabAsset: boolean;
isOutermostRoot: boolean;
prefabType: string;
}
interface OpenResult {
success: boolean;
prefabPath: string;
prefabName: string;
stageRoot: string;
}
interface PrefabInfo {
name: string;
path: string;
type: string;
isVariant: boolean;
}
interface ListResult {
count: number;
searchPath: string;
prefabs: PrefabInfo[];
}
/**
* Register Prefab command
*/
export function registerPrefabCommand(program: Command): void {
const prefabCmd = program
.command('prefab')
.description('Manipulate Unity Prefabs');
// Instantiate prefab
prefabCmd
.command('instantiate')
.alias('inst')
.description('Instantiate a prefab in the scene')
.argument('<path>', 'Prefab asset path (e.g., "Assets/Prefabs/Player.prefab")')
.option('--name <name>', 'Name for the instantiated object')
.option('--position <x,y,z>', 'Position to spawn at (e.g., "0,1,0")')
.option('--rotation <x,y,z>', 'Rotation in euler angles (e.g., "0,90,0")')
.option('--parent <gameobject>', 'Parent GameObject name or path')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '30000')
.action(async (path, options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
await client.connect();
logger.info(`Instantiating prefab '${path}'...`);
const result = await client.sendRequest<InstantiateResult>(
COMMANDS.PREFAB_INSTANTIATE,
{
path,
name: options.name,
position: options.position,
rotation: options.rotation,
parent: options.parent,
}
);
if (!result) {
logger.error('Failed to instantiate prefab');
process.exit(1);
}
if (options.json) {
outputJson(result);
return;
}
logger.info(`✓ Prefab instantiated successfully`);
logger.info(` Instance: ${result.instanceName}`);
logger.info(` Prefab: ${result.prefabPath}`);
if (result.position) {
logger.info(` Position: (${result.position.x}, ${result.position.y}, ${result.position.z})`);
}
} catch (error) {
logger.error('Failed to instantiate prefab', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
// Ignore
}
}
}
});
// Create prefab from scene object
prefabCmd
.command('create')
.description('Create a prefab from a scene GameObject')
.argument('<gameobject>', 'GameObject name or path')
.argument('<path>', 'Path to save prefab (e.g., "Assets/Prefabs/MyPrefab.prefab")')
.option('--overwrite', 'Overwrite existing prefab')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '30000')
.action(async (gameobject, path, options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
await client.connect();
logger.info(`Creating prefab from '${gameobject}'...`);
const result = await client.sendRequest<CreateResult>(
COMMANDS.PREFAB_CREATE,
{ name: gameobject, path, overwrite: options.overwrite }
);
if (!result) {
logger.error('Failed to create prefab');
process.exit(1);
}
if (options.json) {
outputJson(result);
return;
}
logger.info(`✓ Prefab created successfully`);
logger.info(` Source: ${result.sourceName}`);
logger.info(` Saved to: ${result.prefabPath}`);
logger.info(` Connected: ${result.isConnected ? 'Yes' : 'No'}`);
} catch (error) {
logger.error('Failed to create prefab', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
// Ignore
}
}
}
});
// Unpack prefab instance
prefabCmd
.command('unpack')
.description('Unpack a prefab instance')
.argument('<gameobject>', 'Prefab instance name or path')
.option('--completely', 'Unpack completely (all nested prefabs)')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '30000')
.action(async (gameobject, options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
await client.connect();
logger.info(`Unpacking prefab instance '${gameobject}'...`);
const result = await client.sendRequest<{ success: boolean; unpackedObject: string; completely: boolean }>(
COMMANDS.PREFAB_UNPACK,
{ name: gameobject, completely: options.completely }
);
if (!result) {
logger.error('Failed to unpack prefab');
process.exit(1);
}
if (options.json) {
outputJson(result);
return;
}
logger.info(`✓ Prefab unpacked successfully`);
logger.info(` Object: ${result.unpackedObject}`);
logger.info(` Mode: ${result.completely ? 'Completely' : 'Outermost Root'}`);
} catch (error) {
logger.error('Failed to unpack prefab', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
// Ignore
}
}
}
});
// Apply prefab overrides
prefabCmd
.command('apply')
.description('Apply prefab instance overrides to source prefab')
.argument('<gameobject>', 'Prefab instance name or path')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '30000')
.action(async (gameobject, options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
await client.connect();
logger.info(`Applying overrides from '${gameobject}'...`);
const result = await client.sendRequest<ApplyResult>(
COMMANDS.PREFAB_APPLY,
{ name: gameobject }
);
if (!result) {
logger.error('Failed to apply prefab overrides');
process.exit(1);
}
if (options.json) {
outputJson(result);
return;
}
logger.info(`✓ Overrides applied to prefab`);
logger.info(` Instance: ${result.instanceName}`);
logger.info(` Prefab: ${result.prefabPath}`);
} catch (error) {
logger.error('Failed to apply prefab overrides', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
// Ignore
}
}
}
});
// Revert prefab overrides
prefabCmd
.command('revert')
.description('Revert prefab instance overrides')
.argument('<gameobject>', 'Prefab instance name or path')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '30000')
.action(async (gameobject, options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
await client.connect();
logger.info(`Reverting overrides on '${gameobject}'...`);
const result = await client.sendRequest<{ success: boolean; revertedObject: string }>(
COMMANDS.PREFAB_REVERT,
{ name: gameobject }
);
if (!result) {
logger.error('Failed to revert prefab overrides');
process.exit(1);
}
if (options.json) {
outputJson(result);
return;
}
logger.info(`✓ Prefab overrides reverted`);
logger.info(` Object: ${result.revertedObject}`);
} catch (error) {
logger.error('Failed to revert prefab overrides', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
// Ignore
}
}
}
});
// Create prefab variant
prefabCmd
.command('variant')
.description('Create a prefab variant from an existing prefab')
.argument('<sourcePath>', 'Source prefab path')
.argument('<variantPath>', 'Path to save variant')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '30000')
.action(async (sourcePath, variantPath, options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
await client.connect();
logger.info(`Creating variant of '${sourcePath}'...`);
const result = await client.sendRequest<VariantResult>(
COMMANDS.PREFAB_VARIANT,
{ sourcePath, variantPath }
);
if (!result) {
logger.error('Failed to create prefab variant');
process.exit(1);
}
if (options.json) {
outputJson(result);
return;
}
logger.info(`✓ Prefab variant created`);
logger.info(` Source: ${result.sourcePath}`);
logger.info(` Variant: ${result.variantPath}`);
} catch (error) {
logger.error('Failed to create prefab variant', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
// Ignore
}
}
}
});
// Get prefab overrides
prefabCmd
.command('overrides')
.alias('get-overrides')
.description('Get list of overrides on a prefab instance')
.argument('<gameobject>', 'Prefab instance name or path')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '30000')
.action(async (gameobject, options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
await client.connect();
logger.info(`Getting overrides on '${gameobject}'...`);
const result = await client.sendRequest<GetOverridesResult>(
COMMANDS.PREFAB_GET_OVERRIDES,
{ name: gameobject }
);
if (!result) {
logger.error('Failed to get prefab overrides');
process.exit(1);
}
if (options.json) {
outputJson(result);
return;
}
logger.info(`✓ Overrides on '${result.instanceName}':`);
if (!result.hasOverrides) {
logger.info(' (No overrides)');
return;
}
for (const ov of result.overrides) {
let icon = '●';
if (ov.type === 'AddedComponent') icon = '+';
if (ov.type === 'RemovedComponent') icon = '-';
if (ov.type === 'AddedGameObject') icon = '★';
logger.info(` ${icon} [${ov.type}] ${ov.targetName} (${ov.targetType})`);
}
logger.info(`\n Total: ${result.overrideCount} override(s)`);
} catch (error) {
logger.error('Failed to get prefab overrides', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
// Ignore
}
}
}
});
// Get source prefab
prefabCmd
.command('source')
.alias('get-source')
.description('Get source prefab path of a prefab instance')
.argument('<gameobject>', 'GameObject name or path')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '30000')
.action(async (gameobject, options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
await client.connect();
logger.info(`Getting source prefab of '${gameobject}'...`);
const result = await client.sendRequest<GetSourceResult>(
COMMANDS.PREFAB_GET_SOURCE,
{ name: gameobject }
);
if (!result) {
logger.error('Failed to get source prefab');
process.exit(1);
}
if (options.json) {
outputJson(result);
return;
}
logger.info(`✓ Prefab info for '${result.instanceName}':`);
logger.info(` Is Prefab Instance: ${result.isPrefabInstance ? 'Yes' : 'No'}`);
if (result.isPrefabInstance) {
logger.info(` Prefab Path: ${result.prefabPath}`);
logger.info(` Prefab Type: ${result.prefabType}`);
if (result.prefabStatus) {
logger.info(` Status: ${result.prefabStatus}`);
}
}
} catch (error) {
logger.error('Failed to get source prefab', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
// Ignore
}
}
}
});
// Check if is prefab instance
prefabCmd
.command('is-instance')
.description('Check if a GameObject is a prefab instance')
.argument('<gameobject>', 'GameObject name or path')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '30000')
.action(async (gameobject, options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
await client.connect();
logger.info(`Checking if '${gameobject}' is a prefab instance...`);
const result = await client.sendRequest<IsInstanceResult>(
COMMANDS.PREFAB_IS_INSTANCE,
{ name: gameobject }
);
if (!result) {
logger.error('Failed to check prefab status');
process.exit(1);
}
if (options.json) {
outputJson(result);
return;
}
logger.info(`✓ Prefab status for '${result.name}':`);
logger.info(` Is Prefab Instance: ${result.isPrefabInstance ? 'Yes' : 'No'}`);
logger.info(` Is Prefab Asset: ${result.isPrefabAsset ? 'Yes' : 'No'}`);
logger.info(` Is Outermost Root: ${result.isOutermostRoot ? 'Yes' : 'No'}`);
logger.info(` Prefab Type: ${result.prefabType}`);
} catch (error) {
logger.error('Failed to check prefab status', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
// Ignore
}
}
}
});
// Open prefab in edit mode
prefabCmd
.command('open')
.description('Open a prefab in prefab editing mode')
.argument('<path>', 'Prefab asset path')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '30000')
.action(async (path, options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
await client.connect();
logger.info(`Opening prefab '${path}'...`);
const result = await client.sendRequest<OpenResult>(
COMMANDS.PREFAB_OPEN,
{ path }
);
if (!result) {
logger.error('Failed to open prefab');
process.exit(1);
}
if (options.json) {
outputJson(result);
return;
}
logger.info(`✓ Prefab opened in edit mode`);
logger.info(` Prefab: ${result.prefabName}`);
logger.info(` Path: ${result.prefabPath}`);
if (result.stageRoot) {
logger.info(` Stage Root: ${result.stageRoot}`);
}
} catch (error) {
logger.error('Failed to open prefab', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
// Ignore
}
}
}
});
// Close prefab edit mode
prefabCmd
.command('close')
.description('Close prefab editing mode and return to scene')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '30000')
.action(async (options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
await client.connect();
logger.info('Closing prefab edit mode...');
const result = await client.sendRequest<{ success: boolean; closedPrefab?: string; message?: string }>(
COMMANDS.PREFAB_CLOSE,
{}
);
if (!result) {
logger.error('Failed to close prefab');
process.exit(1);
}
if (options.json) {
outputJson(result);
return;
}
if (result.closedPrefab) {
logger.info(`✓ Prefab edit mode closed`);
logger.info(` Closed: ${result.closedPrefab}`);
} else {
logger.info(`${result.message || 'No prefab was open'}`);
}
} catch (error) {
logger.error('Failed to close prefab', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
// Ignore
}
}
}
});
// List prefabs in folder
prefabCmd
.command('list')
.description('List all prefabs in a folder')
.option('--path <path>', 'Folder path to search (default: "Assets")')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '30000')
.action(async (options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
await client.connect();
const searchPath = options.path || 'Assets';
logger.info(`Listing prefabs in '${searchPath}'...`);
const result = await client.sendRequest<ListResult>(
COMMANDS.PREFAB_LIST,
{ path: options.path }
);
if (!result) {
logger.error('Failed to list prefabs');
process.exit(1);
}
if (options.json) {
outputJson(result);
return;
}
logger.info(`✓ Prefabs in '${result.searchPath}':`);
if (result.count === 0) {
logger.info(' (No prefabs found)');
return;
}
for (const prefab of result.prefabs) {
const icon = prefab.isVariant ? '◇' : '●';
const type = prefab.isVariant ? ' [Variant]' : '';
logger.info(` ${icon} ${prefab.name}${type}`);
logger.info(` ${prefab.path}`);
}
logger.info(`\n Total: ${result.count} prefab(s)`);
} catch (error) {
logger.error('Failed to list prefabs', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
// Ignore
}
}
}
});
}

View File

@@ -0,0 +1,341 @@
/**
* Prefs command
*
* Unity EditorPrefs management commands
*/
import { Command } from 'commander';
import * as logger from '@/utils/logger';
import * as config from '@/utils/config';
import { createUnityClient } from '@/unity/client';
import { outputJson } from '@/utils/output-formatter';
/**
* EditorPrefs response types
*/
interface PrefsGetResponse {
success: boolean;
key: string;
value: string | number | boolean;
type: string;
}
interface PrefsSetResponse {
success: boolean;
}
interface PrefsDeleteResponse {
success: boolean;
}
interface PrefsHasKeyResponse {
success: boolean;
hasKey: boolean;
type?: string;
value?: string | number | boolean;
}
interface PrefsClearResponse {
success: boolean;
}
/**
* Register Prefs command
*/
export function registerPrefsCommand(program: Command): void {
const prefsCmd = program
.command('prefs')
.description('Unity EditorPrefs management commands');
// Get EditorPrefs value
prefsCmd
.command('get')
.description('Get EditorPrefs value')
.argument('<key>', 'EditorPrefs key name')
.option('-t, --type <type>', 'Value type (string|int|float|bool)', 'string')
.option('-d, --default <value>', 'Default value if key does not exist')
.option('--json', 'Output in JSON format')
.action(async (key, options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
logger.info('Connecting to Unity Editor...');
await client.connect();
// Determine which command to use based on type
let command: string;
switch (options.type) {
case 'int':
command = 'Prefs.GetInt';
break;
case 'float':
command = 'Prefs.GetFloat';
break;
case 'bool':
command = 'Prefs.GetBool';
break;
default:
command = 'Prefs.GetString';
}
logger.info(`Getting EditorPrefs value: ${key}`);
const result = await client.sendRequest<PrefsGetResponse>(command, {
key,
defaultValue: options.default,
});
// JSON output
if (options.json) {
outputJson(result);
} else {
logger.info(`✓ EditorPrefs value retrieved`);
logger.info(`Key: ${result.key}`);
logger.info(`Type: ${result.type}`);
logger.info(`Value: ${result.value}`);
}
} catch (error) {
logger.error('Failed to get EditorPrefs value', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
logger.info('Disconnected from Unity Editor');
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// Set EditorPrefs value
prefsCmd
.command('set')
.description('Set EditorPrefs value')
.argument('<key>', 'EditorPrefs key name')
.argument('<value>', 'Value to set')
.option('-t, --type <type>', 'Value type (string|int|float|bool)', 'string')
.option('--json', 'Output in JSON format')
.action(async (key, value, options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
logger.info('Connecting to Unity Editor...');
await client.connect();
// Determine which command to use based on type
let command: string;
switch (options.type) {
case 'int':
command = 'Prefs.SetInt';
break;
case 'float':
command = 'Prefs.SetFloat';
break;
case 'bool':
command = 'Prefs.SetBool';
break;
default:
command = 'Prefs.SetString';
}
logger.info(`Setting EditorPrefs value: ${key} = ${value}`);
const result = await client.sendRequest<PrefsSetResponse>(command, { key, value });
// JSON output
if (options.json) {
outputJson(result);
} else {
logger.info(`✓ EditorPrefs value set`);
logger.info(`Key: ${key}`);
logger.info(`Value: ${value}`);
logger.info(`Type: ${options.type}`);
}
} catch (error) {
logger.error('Failed to set EditorPrefs value', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
logger.info('Disconnected from Unity Editor');
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// Delete EditorPrefs key
prefsCmd
.command('delete')
.description('Delete EditorPrefs key')
.argument('<key>', 'EditorPrefs key name')
.option('--json', 'Output in JSON format')
.action(async (key, options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
logger.info('Connecting to Unity Editor...');
await client.connect();
logger.info(`Deleting EditorPrefs key: ${key}`);
const result = await client.sendRequest<PrefsDeleteResponse>('Prefs.DeleteKey', { key });
// JSON output
if (options.json) {
outputJson(result);
} else {
logger.info(`✓ EditorPrefs key deleted`);
logger.info(`Key: ${key}`);
}
} catch (error) {
logger.error('Failed to delete EditorPrefs key', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
logger.info('Disconnected from Unity Editor');
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// Clear all EditorPrefs
prefsCmd
.command('clear')
.description('Delete all EditorPrefs (WARNING: irreversible)')
.option('--json', 'Output in JSON format')
.option('--force', 'Skip confirmation prompt')
.action(async (options) => {
let client = null;
try {
// Confirmation prompt (unless --force)
if (!options.force) {
logger.warn('WARNING: This will delete ALL EditorPrefs data!');
logger.warn('This action cannot be undone.');
logger.info('Use --force flag to skip this confirmation.');
process.exit(1);
}
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
logger.info('Connecting to Unity Editor...');
await client.connect();
logger.info('Deleting all EditorPrefs...');
const result = await client.sendRequest<PrefsClearResponse>('Prefs.DeleteAll');
// JSON output
if (options.json) {
outputJson(result);
} else {
logger.info('✓ All EditorPrefs deleted');
}
} catch (error) {
logger.error('Failed to delete all EditorPrefs', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
logger.info('Disconnected from Unity Editor');
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// Check if EditorPrefs key exists
prefsCmd
.command('has')
.description('Check if EditorPrefs key exists')
.argument('<key>', 'EditorPrefs key name')
.option('--json', 'Output in JSON format')
.action(async (key, options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
logger.info('Connecting to Unity Editor...');
await client.connect();
logger.info(`Checking if EditorPrefs key exists: ${key}`);
const result = await client.sendRequest<PrefsHasKeyResponse>('Prefs.HasKey', { key });
// JSON output
if (options.json) {
outputJson(result);
} else {
logger.info(`✓ Check complete`);
logger.info(`Key: ${key}`);
logger.info(`Exists: ${result.hasKey ? 'Yes' : 'No'}`);
// 키가 존재하고 값이 있으면 타입과 값도 출력
if (result.hasKey && result.type && result.value !== undefined) {
logger.info(`Type: ${result.type}`);
logger.info(`Value: ${result.value}`);
}
}
} catch (error) {
logger.error('Failed to check EditorPrefs key', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
logger.info('Disconnected from Unity Editor');
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
}

View File

@@ -0,0 +1,444 @@
/**
* Scene command
*
* Manipulate Unity scenes.
*/
import { Command } from 'commander';
import * as logger from '@/utils/logger';
import * as config from '@/utils/config';
import { createUnityClient } from '@/unity/client';
import { COMMANDS, UNITY } from '@/constants';
import type { SceneInfo } from '@/unity/protocol';
import { output, outputJson } from '@/utils/output-formatter';
/**
* Register Scene command
*/
export function registerSceneCommand(program: Command): void {
const sceneCmd = program
.command('scene')
.description('Manipulate Unity scenes');
// Get current scene
sceneCmd
.command('current')
.description('Get current active scene')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '300000')
.action(async (options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
logger.info('Connecting to Unity Editor...');
await client.connect();
logger.info('Getting current scene...');
const result = await client.sendRequest<SceneInfo>(
COMMANDS.SCENE_GET_CURRENT
);
// JSON output
if (options.json) {
outputJson({ scene: result });
return;
}
// Text output
logger.info('✓ Current Scene:');
logger.info(` Name: ${result.name}`);
logger.info(` Path: ${result.path}`);
logger.info(` Build Index: ${result.buildIndex}`);
logger.info(` Is Loaded: ${result.isLoaded}`);
logger.info(` Is Dirty: ${result.isDirty}`);
logger.info(` Root GameObjects: ${result.rootCount}`);
} catch (error) {
logger.error('Failed to get current scene', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// List all scenes
sceneCmd
.command('list')
.description('List all loaded scenes')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '300000')
.action(async (options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
logger.info('Connecting to Unity Editor...');
await client.connect();
logger.info('Getting all scenes...');
const result = await client.sendRequest<SceneInfo[]>(
COMMANDS.SCENE_GET_ALL
);
if (!result || result.length === 0) {
if (options.json) {
outputJson({ scenes: [], total: 0 });
} else {
logger.info('No scenes loaded');
}
return;
}
// JSON output
if (options.json) {
outputJson({
scenes: result,
total: result.length,
});
return;
}
// Text output
logger.info('✓ Loaded Scenes:');
for (const scene of result) {
const loadedIcon = scene.isLoaded ? '●' : '○';
const dirtyIcon = scene.isDirty ? '*' : ' ';
logger.info(`${loadedIcon}${dirtyIcon} ${scene.name}`);
logger.info(` Path: ${scene.path}`);
logger.info(` Build Index: ${scene.buildIndex}`);
logger.info(` Root GameObjects: ${scene.rootCount}`);
}
logger.info(`Total: ${result.length} scene(s)`);
} catch (error) {
logger.error('Failed to list scenes', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// Load scene
sceneCmd
.command('load')
.description('Load scene by name or path')
.argument('<name>', 'Scene name or path')
.option('-a, --additive', 'Load scene additively')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '300000')
.action(async (name, options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
logger.info('Connecting to Unity Editor...');
await client.connect();
logger.info(`Loading scene: ${name}${options.additive ? ' (additive)' : ''}`);
const timeout = options.timeout ? parseInt(options.timeout, 10) : UNITY.SCENE_LOAD_TIMEOUT;
await client.sendRequest(
COMMANDS.SCENE_LOAD,
{
name,
additive: options.additive || false,
},
timeout
);
// JSON output
if (options.json) {
outputJson({
success: true,
scene: name,
additive: options.additive || false,
});
} else {
logger.info('✓ Scene loaded');
}
} catch (error) {
logger.error('Failed to load scene', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// Create new scene
sceneCmd
.command('new')
.description('Create a new scene')
.option('-e, --empty', 'Create empty scene (no default objects)')
.option('-a, --additive', 'Add new scene without replacing current')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '300000')
.action(async (options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
logger.info('Connecting to Unity Editor...');
await client.connect();
logger.info(`Creating new scene${options.empty ? ' (empty)' : ''}${options.additive ? ' (additive)' : ''}...`);
const result = await client.sendRequest<{ success: boolean; scene: SceneInfo }>(
COMMANDS.SCENE_NEW,
{
empty: options.empty || false,
additive: options.additive || false,
}
);
// JSON output
if (options.json) {
outputJson(result);
return;
}
// Text output
logger.info('✓ New scene created');
if (result.scene) {
logger.info(` Name: ${result.scene.name || '(Untitled)'}`);
}
} catch (error) {
logger.error('Failed to create new scene', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// Save scene
sceneCmd
.command('save')
.description('Save scene')
.argument('[path]', 'Path to save scene (optional, for Save As)')
.option('-s, --scene <name>', 'Specific scene name to save (default: active scene)')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '300000')
.action(async (path, options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
logger.info('Connecting to Unity Editor...');
await client.connect();
logger.info(`Saving scene${path ? ` to ${path}` : ''}...`);
const result = await client.sendRequest<{ success: boolean; scene: SceneInfo }>(
COMMANDS.SCENE_SAVE,
{
sceneName: options.scene || '',
path: path || '',
}
);
// JSON output
if (options.json) {
outputJson(result);
return;
}
// Text output
if (result.success) {
logger.info('✓ Scene saved');
if (result.scene) {
logger.info(` Path: ${result.scene.path}`);
}
} else {
logger.error('Failed to save scene');
}
} catch (error) {
logger.error('Failed to save scene', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// Unload scene
sceneCmd
.command('unload')
.description('Unload a scene')
.argument('<name>', 'Scene name or path to unload')
.option('-r, --remove', 'Remove scene completely (default: just unload)')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '300000')
.action(async (name, options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
logger.info('Connecting to Unity Editor...');
await client.connect();
logger.info(`Unloading scene: ${name}...`);
const result = await client.sendRequest<{ success: boolean }>(
COMMANDS.SCENE_UNLOAD,
{
name,
removeScene: options.remove || false,
}
);
// JSON output
if (options.json) {
outputJson({ success: result.success, scene: name });
return;
}
// Text output
if (result.success) {
logger.info('✓ Scene unloaded');
} else {
logger.error('Failed to unload scene');
}
} catch (error) {
logger.error('Failed to unload scene', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// Set active scene
sceneCmd
.command('set-active')
.description('Set the active scene')
.argument('<name>', 'Scene name or path to set as active')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '300000')
.action(async (name, options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
logger.info('Connecting to Unity Editor...');
await client.connect();
logger.info(`Setting active scene: ${name}...`);
const result = await client.sendRequest<{ success: boolean; scene: SceneInfo }>(
COMMANDS.SCENE_SET_ACTIVE,
{ name }
);
// JSON output
if (options.json) {
outputJson(result);
return;
}
// Text output
if (result.success) {
logger.info('✓ Active scene changed');
if (result.scene) {
logger.info(` Name: ${result.scene.name}`);
logger.info(` Path: ${result.scene.path}`);
}
} else {
logger.error('Failed to set active scene');
}
} catch (error) {
logger.error('Failed to set active scene', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
}

View File

@@ -0,0 +1,380 @@
/**
* Snapshot command
*
* Save and restore Unity scene snapshots to/from database.
*/
import { Command } from 'commander';
import * as logger from '@/utils/logger';
import * as config from '@/utils/config';
import { createUnityClient } from '@/unity/client';
import { COMMANDS } from '@/constants';
import { outputJson } from '@/utils/output-formatter';
/**
* Snapshot result interfaces
*/
interface SnapshotSaveResult {
success: boolean;
snapshotId: number;
snapshotName: string;
sceneName: string;
scenePath: string;
objectCount: number;
message: string;
}
interface SnapshotInfo {
snapshotId: number;
sceneId: number;
sceneName: string;
scenePath: string;
snapshotName: string;
description: string;
createdAt: string;
}
interface SnapshotListResult {
success: boolean;
count: number;
snapshots: SnapshotInfo[];
}
interface SnapshotGetResult {
success: boolean;
snapshotId: number;
sceneId: number;
sceneName: string;
scenePath: string;
snapshotName: string;
description: string;
createdAt: string;
objectCount: number;
data: unknown;
}
interface SnapshotRestoreResult {
success: boolean;
snapshotId: number;
snapshotName: string;
sceneName: string;
restoredObjects: number;
message: string;
}
interface SnapshotDeleteResult {
success: boolean;
snapshotId: number;
snapshotName: string;
message: string;
}
/**
* Register Snapshot command
*/
export function registerSnapshotCommand(program: Command): void {
const snapshotCmd = program
.command('snapshot')
.description('Save and restore Unity scene snapshots');
// Save snapshot
snapshotCmd
.command('save')
.description('Save current scene state as a snapshot')
.argument('<name>', 'Snapshot name')
.option('-d, --description <text>', 'Snapshot description')
.option('--json', 'Output in JSON format')
.action(async (name: string, options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
logger.info('Connecting to Unity Editor...');
await client.connect();
logger.info(`Saving snapshot: ${name}`);
const result = await client.sendRequest<SnapshotSaveResult>(
COMMANDS.SNAPSHOT_SAVE,
{
name,
description: options.description || '',
}
);
if (options.json) {
outputJson(result);
return;
}
logger.info('✓ Snapshot saved successfully');
logger.info(` ID: ${result.snapshotId}`);
logger.info(` Name: ${result.snapshotName}`);
logger.info(` Scene: ${result.sceneName}`);
logger.info(` Objects: ${result.objectCount}`);
} catch (error) {
logger.error('Failed to save snapshot', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// List snapshots
snapshotCmd
.command('list')
.description('List all snapshots')
.option('-a, --all', 'List snapshots for all scenes')
.option('-n, --limit <number>', 'Maximum number of snapshots to show', '50')
.option('--json', 'Output in JSON format')
.action(async (options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
logger.info('Connecting to Unity Editor...');
await client.connect();
logger.info('Fetching snapshots...');
const result = await client.sendRequest<SnapshotListResult>(
COMMANDS.SNAPSHOT_LIST,
{
allScenes: options.all || false,
limit: parseInt(options.limit, 10),
}
);
if (options.json) {
outputJson(result);
return;
}
if (result.count === 0) {
logger.info('No snapshots found');
return;
}
logger.info(`✓ Found ${result.count} snapshot(s)`);
for (const snapshot of result.snapshots) {
logger.info(`[${snapshot.snapshotId}] ${snapshot.snapshotName}`);
logger.info(` Scene: ${snapshot.sceneName} (${snapshot.scenePath})`);
logger.info(` Created: ${snapshot.createdAt}`);
if (snapshot.description) {
logger.info(` Description: ${snapshot.description}`);
}
}
} catch (error) {
logger.error('Failed to list snapshots', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// Get snapshot details
snapshotCmd
.command('get')
.description('Get snapshot details by ID')
.argument('<id>', 'Snapshot ID')
.option('--json', 'Output in JSON format')
.action(async (id: string, options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
logger.info('Connecting to Unity Editor...');
await client.connect();
const snapshotId = parseInt(id, 10);
if (isNaN(snapshotId)) {
logger.error('Invalid snapshot ID');
process.exit(1);
}
logger.info(`Fetching snapshot ${snapshotId}...`);
const result = await client.sendRequest<SnapshotGetResult>(
COMMANDS.SNAPSHOT_GET,
{ snapshotId }
);
if (options.json) {
outputJson(result);
return;
}
logger.info('✓ Snapshot Details:');
logger.info(` ID: ${result.snapshotId}`);
logger.info(` Name: ${result.snapshotName}`);
logger.info(` Scene: ${result.sceneName} (${result.scenePath})`);
logger.info(` Objects: ${result.objectCount}`);
logger.info(` Created: ${result.createdAt}`);
if (result.description) {
logger.info(` Description: ${result.description}`);
}
} catch (error) {
logger.error('Failed to get snapshot', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// Restore snapshot
snapshotCmd
.command('restore')
.description('Restore scene from snapshot')
.argument('<id>', 'Snapshot ID')
.option('-c, --clear', 'Clear current scene before restoring')
.option('--json', 'Output in JSON format')
.action(async (id: string, options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
logger.info('Connecting to Unity Editor...');
await client.connect();
const snapshotId = parseInt(id, 10);
if (isNaN(snapshotId)) {
logger.error('Invalid snapshot ID');
process.exit(1);
}
logger.info(`Restoring snapshot ${snapshotId}...`);
const result = await client.sendRequest<SnapshotRestoreResult>(
COMMANDS.SNAPSHOT_RESTORE,
{
snapshotId,
clearScene: options.clear || false,
}
);
if (options.json) {
outputJson(result);
return;
}
logger.info('✓ Snapshot restored successfully');
logger.info(` Snapshot: ${result.snapshotName}`);
logger.info(` Scene: ${result.sceneName}`);
logger.info(` Restored Objects: ${result.restoredObjects}`);
} catch (error) {
logger.error('Failed to restore snapshot', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// Delete snapshot
snapshotCmd
.command('delete')
.description('Delete a snapshot')
.argument('<id>', 'Snapshot ID')
.option('--json', 'Output in JSON format')
.action(async (id: string, options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
logger.info('Connecting to Unity Editor...');
await client.connect();
const snapshotId = parseInt(id, 10);
if (isNaN(snapshotId)) {
logger.error('Invalid snapshot ID');
process.exit(1);
}
logger.info(`Deleting snapshot ${snapshotId}...`);
const result = await client.sendRequest<SnapshotDeleteResult>(
COMMANDS.SNAPSHOT_DELETE,
{ snapshotId }
);
if (options.json) {
outputJson(result);
return;
}
logger.info('✓ Snapshot deleted successfully');
logger.info(` ID: ${result.snapshotId}`);
logger.info(` Name: ${result.snapshotName}`);
} catch (error) {
logger.error('Failed to delete snapshot', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
}

View File

@@ -0,0 +1,441 @@
/**
* Sync command
*
* Sync Unity GameObjects and Components with database
*/
import { Command } from 'commander';
import * as logger from '@/utils/logger';
import * as config from '@/utils/config';
import { createUnityClient } from '@/unity/client';
import { COMMANDS } from '@/constants';
import { outputJson } from '@/utils/output-formatter';
/**
* Sync result interfaces
*/
interface SyncResult {
success: boolean;
sceneName: string;
sceneId: number;
syncedObjects: number;
syncedComponents: number;
closureRecords: number;
message: string;
}
interface SyncGameObjectResult {
success: boolean;
objectName: string;
objectId: number;
syncedComponents: number;
syncedChildren: number;
message: string;
}
interface SyncStatusResult {
success: boolean;
sceneName: string;
unityObjectCount: number;
dbObjectCount: number;
dbComponentCount: number;
closureRecordCount: number;
inSync: boolean;
}
interface ClearSyncResult {
success: boolean;
deletedObjects: number;
deletedComponents: number;
message: string;
}
interface AutoSyncResult {
success: boolean;
message: string;
isRunning: boolean;
}
interface AutoSyncStatusResult {
success: boolean;
isRunning: boolean;
isInitialized: boolean;
lastSyncTime: string | null;
successfulSyncCount: number;
failedSyncCount: number;
syncIntervalMs: number;
batchSize: number;
}
/**
* Register Sync command
*/
export function registerSyncCommand(program: Command): void {
const syncCmd = program
.command('sync')
.description('Sync Unity GameObjects and Components with database');
// Sync entire scene
syncCmd
.command('scene')
.description('Sync entire scene to database')
.option('--no-clear', 'Do not clear existing data before sync')
.option('--no-components', 'Do not sync components')
.option('--no-closure', 'Do not build closure table')
.option('--json', 'Output in JSON format')
.action(async (options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
logger.info('Connecting to Unity Editor...');
await client.connect();
logger.info('Syncing scene to database...');
const result = await client.sendRequest<SyncResult>(
COMMANDS.SYNC_SCENE,
{
clearExisting: options.clear !== false,
includeComponents: options.components !== false,
buildClosure: options.closure !== false,
}
);
if (options.json) {
outputJson(result);
return;
}
logger.info('✓ Scene synced successfully');
logger.info(` Scene: ${result.sceneName}`);
logger.info(` Objects: ${result.syncedObjects}`);
logger.info(` Components: ${result.syncedComponents}`);
logger.info(` Closure Records: ${result.closureRecords}`);
} catch (error) {
logger.error('Failed to sync scene', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// Sync specific GameObject
syncCmd
.command('object')
.description('Sync specific GameObject to database')
.argument('<target>', 'GameObject name or path')
.option('--no-components', 'Do not sync components')
.option('-c, --children', 'Include children')
.option('--json', 'Output in JSON format')
.action(async (target: string, options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
logger.info('Connecting to Unity Editor...');
await client.connect();
logger.info(`Syncing GameObject: ${target}`);
const result = await client.sendRequest<SyncGameObjectResult>(
COMMANDS.SYNC_GAMEOBJECT,
{
target,
includeComponents: options.components !== false,
includeChildren: options.children || false,
}
);
if (options.json) {
outputJson(result);
return;
}
logger.info('✓ GameObject synced successfully');
logger.info(` Object: ${result.objectName}`);
logger.info(` Components: ${result.syncedComponents}`);
logger.info(` Children: ${result.syncedChildren}`);
} catch (error) {
logger.error('Failed to sync GameObject', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// Get sync status
syncCmd
.command('status')
.description('Get sync status for current scene')
.option('--json', 'Output in JSON format')
.action(async (options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
logger.info('Connecting to Unity Editor...');
await client.connect();
logger.info('Getting sync status...');
const result = await client.sendRequest<SyncStatusResult>(
COMMANDS.SYNC_STATUS
);
if (options.json) {
outputJson(result);
return;
}
logger.info('✓ Sync Status:');
logger.info(` Scene: ${result.sceneName}`);
logger.info(` Unity Objects: ${result.unityObjectCount}`);
logger.info(` DB Objects: ${result.dbObjectCount}`);
logger.info(` DB Components: ${result.dbComponentCount}`);
logger.info(` Closure Records: ${result.closureRecordCount}`);
logger.info(` In Sync: ${result.inSync ? '✓' : '❌'}`);
} catch (error) {
logger.error('Failed to get sync status', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// Clear sync data
syncCmd
.command('clear')
.description('Clear sync data from database')
.option('--json', 'Output in JSON format')
.action(async (options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
logger.info('Connecting to Unity Editor...');
await client.connect();
logger.info('Clearing sync data...');
const result = await client.sendRequest<ClearSyncResult>(
COMMANDS.SYNC_CLEAR
);
if (options.json) {
outputJson(result);
return;
}
logger.info('✓ Sync data cleared');
logger.info(` Deleted Objects: ${result.deletedObjects}`);
logger.info(` Deleted Components: ${result.deletedComponents}`);
} catch (error) {
logger.error('Failed to clear sync data', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// Auto-sync start
syncCmd
.command('auto-start')
.description('Start automatic synchronization')
.option('--json', 'Output in JSON format')
.action(async (options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
logger.info('Connecting to Unity Editor...');
await client.connect();
logger.info('Starting automatic synchronization...');
const result = await client.sendRequest<AutoSyncResult>(
COMMANDS.SYNC_START_AUTO
);
if (options.json) {
outputJson(result);
return;
}
logger.info(`${result.message}`);
logger.info(` Running: ${result.isRunning ? 'Yes' : 'No'}`);
} catch (error) {
logger.error('Failed to start auto-sync', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// Auto-sync stop
syncCmd
.command('auto-stop')
.description('Stop automatic synchronization')
.option('--json', 'Output in JSON format')
.action(async (options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
logger.info('Connecting to Unity Editor...');
await client.connect();
logger.info('Stopping automatic synchronization...');
const result = await client.sendRequest<AutoSyncResult>(
COMMANDS.SYNC_STOP_AUTO
);
if (options.json) {
outputJson(result);
return;
}
logger.info(`${result.message}`);
logger.info(` Running: ${result.isRunning ? 'Yes' : 'No'}`);
} catch (error) {
logger.error('Failed to stop auto-sync', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// Auto-sync status
syncCmd
.command('auto-status')
.description('Get automatic synchronization status')
.option('--json', 'Output in JSON format')
.action(async (options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
logger.info('Connecting to Unity Editor...');
await client.connect();
logger.info('Getting auto-sync status...');
const result = await client.sendRequest<AutoSyncStatusResult>(
COMMANDS.SYNC_GET_AUTO_STATUS
);
if (options.json) {
outputJson(result);
return;
}
logger.info('✓ Auto-Sync Status:');
logger.info(` Initialized: ${result.isInitialized ? '✓' : '✗'}`);
logger.info(` Running: ${result.isRunning ? '✓' : '✗'}`);
if (result.lastSyncTime) {
logger.info(` Last Sync: ${result.lastSyncTime}`);
}
logger.info(` Successful Syncs: ${result.successfulSyncCount}`);
logger.info(` Failed Syncs: ${result.failedSyncCount}`);
logger.info(` Sync Interval: ${result.syncIntervalMs}ms`);
logger.info(` Batch Size: ${result.batchSize}`);
} catch (error) {
logger.error('Failed to get auto-sync status', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
}

View File

@@ -0,0 +1,383 @@
/**
* Transform History command
*
* Track and restore GameObject transform changes in database.
*/
import { Command } from 'commander';
import * as logger from '@/utils/logger';
import * as config from '@/utils/config';
import { createUnityClient } from '@/unity/client';
import { COMMANDS } from '@/constants';
import { outputJson } from '@/utils/output-formatter';
/**
* Transform history result interfaces
*/
interface Vector3Data {
x: number;
y: number;
z: number;
}
interface Vector4Data {
x: number;
y: number;
z: number;
w: number;
}
interface TransformHistoryEntry {
transformId: number;
position: Vector3Data;
rotation: Vector4Data;
scale: Vector3Data;
recordedAt: string;
}
interface RecordResult {
success: boolean;
transformId: number;
objectId: number;
objectName: string;
position: Vector3Data;
rotation: Vector4Data;
scale: Vector3Data;
message: string;
}
interface ListResult {
success: boolean;
objectName: string;
objectId: number;
count: number;
history: TransformHistoryEntry[];
}
interface RestoreResult {
success: boolean;
transformId: number;
objectName: string;
position: Vector3Data;
rotation: Vector4Data;
scale: Vector3Data;
message: string;
}
interface CompareResult {
success: boolean;
transform1: TransformHistoryEntry;
transform2: TransformHistoryEntry;
positionDifference: Vector3Data;
rotationAngleDifference: number;
scaleDifference: Vector3Data;
}
interface ClearResult {
success: boolean;
objectName: string;
objectId: number;
deletedCount: number;
message: string;
}
/**
* Register Transform History command
*/
export function registerTransformHistoryCommand(program: Command): void {
const historyCmd = program
.command('transform-history')
.alias('th')
.description('Track and restore GameObject transform changes');
// Record current transform
historyCmd
.command('record')
.description('Record current transform state')
.argument('<target>', 'GameObject name or path')
.option('--json', 'Output in JSON format')
.action(async (target: string, options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
logger.info('Connecting to Unity Editor...');
await client.connect();
logger.info(`Recording transform for: ${target}`);
const result = await client.sendRequest<RecordResult>(
COMMANDS.TRANSFORM_HISTORY_RECORD,
{ target }
);
if (options.json) {
outputJson(result);
return;
}
logger.info('✓ Transform recorded');
logger.info(` ID: ${result.transformId}`);
logger.info(` Object: ${result.objectName}`);
logger.info(` Position: (${result.position.x.toFixed(3)}, ${result.position.y.toFixed(3)}, ${result.position.z.toFixed(3)})`);
logger.info(` Scale: (${result.scale.x.toFixed(3)}, ${result.scale.y.toFixed(3)}, ${result.scale.z.toFixed(3)})`);
} catch (error) {
logger.error('Failed to record transform', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// List transform history
historyCmd
.command('list')
.description('List transform history for a GameObject')
.argument('<target>', 'GameObject name or path')
.option('-n, --limit <number>', 'Maximum number of entries', '50')
.option('--json', 'Output in JSON format')
.action(async (target: string, options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
logger.info('Connecting to Unity Editor...');
await client.connect();
logger.info(`Fetching transform history for: ${target}`);
const result = await client.sendRequest<ListResult>(
COMMANDS.TRANSFORM_HISTORY_LIST,
{ target, limit: parseInt(options.limit, 10) }
);
if (options.json) {
outputJson(result);
return;
}
if (result.count === 0) {
logger.info(`No transform history found for '${result.objectName}'`);
return;
}
logger.info(`✓ Transform history for '${result.objectName}' (${result.count} entries)`);
for (const entry of result.history) {
logger.info(`[${entry.transformId}] ${entry.recordedAt}`);
logger.info(` Position: (${entry.position.x.toFixed(3)}, ${entry.position.y.toFixed(3)}, ${entry.position.z.toFixed(3)})`);
logger.info(` Scale: (${entry.scale.x.toFixed(3)}, ${entry.scale.y.toFixed(3)}, ${entry.scale.z.toFixed(3)})`);
}
} catch (error) {
logger.error('Failed to list transform history', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// Restore transform
historyCmd
.command('restore')
.description('Restore transform from history')
.argument('<id>', 'Transform history ID')
.option('--json', 'Output in JSON format')
.action(async (id: string, options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
logger.info('Connecting to Unity Editor...');
await client.connect();
const transformId = parseInt(id, 10);
if (isNaN(transformId)) {
logger.error('Invalid transform ID');
process.exit(1);
}
logger.info(`Restoring transform ${transformId}...`);
const result = await client.sendRequest<RestoreResult>(
COMMANDS.TRANSFORM_HISTORY_RESTORE,
{ transformId }
);
if (options.json) {
outputJson(result);
return;
}
logger.info('✓ Transform restored');
logger.info(` Object: ${result.objectName}`);
logger.info(` Position: (${result.position.x.toFixed(3)}, ${result.position.y.toFixed(3)}, ${result.position.z.toFixed(3)})`);
logger.info(` Scale: (${result.scale.x.toFixed(3)}, ${result.scale.y.toFixed(3)}, ${result.scale.z.toFixed(3)})`);
} catch (error) {
logger.error('Failed to restore transform', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// Compare transforms
historyCmd
.command('compare')
.description('Compare two transform records')
.argument('<id1>', 'First transform history ID')
.argument('<id2>', 'Second transform history ID')
.option('--json', 'Output in JSON format')
.action(async (id1: string, id2: string, options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
logger.info('Connecting to Unity Editor...');
await client.connect();
const transformId1 = parseInt(id1, 10);
const transformId2 = parseInt(id2, 10);
if (isNaN(transformId1) || isNaN(transformId2)) {
logger.error('Invalid transform ID(s)');
process.exit(1);
}
logger.info(`Comparing transforms ${transformId1} and ${transformId2}...`);
const result = await client.sendRequest<CompareResult>(
COMMANDS.TRANSFORM_HISTORY_COMPARE,
{ transformId1, transformId2 }
);
if (options.json) {
outputJson(result);
return;
}
logger.info('✓ Transform Comparison');
logger.info(`Transform 1 (ID: ${result.transform1.transformId}):`);
logger.info(` Recorded: ${result.transform1.recordedAt}`);
logger.info(` Position: (${result.transform1.position.x.toFixed(3)}, ${result.transform1.position.y.toFixed(3)}, ${result.transform1.position.z.toFixed(3)})`);
logger.info(`Transform 2 (ID: ${result.transform2.transformId}):`);
logger.info(` Recorded: ${result.transform2.recordedAt}`);
logger.info(` Position: (${result.transform2.position.x.toFixed(3)}, ${result.transform2.position.y.toFixed(3)}, ${result.transform2.position.z.toFixed(3)})`);
logger.info('Differences:');
logger.info(` Position Δ: (${result.positionDifference.x.toFixed(3)}, ${result.positionDifference.y.toFixed(3)}, ${result.positionDifference.z.toFixed(3)})`);
logger.info(` Rotation Δ: ${result.rotationAngleDifference.toFixed(2)}°`);
logger.info(` Scale Δ: (${result.scaleDifference.x.toFixed(3)}, ${result.scaleDifference.y.toFixed(3)}, ${result.scaleDifference.z.toFixed(3)})`);
} catch (error) {
logger.error('Failed to compare transforms', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// Clear transform history
historyCmd
.command('clear')
.description('Clear transform history for a GameObject')
.argument('<target>', 'GameObject name or path')
.option('--json', 'Output in JSON format')
.action(async (target: string, options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
logger.info('Connecting to Unity Editor...');
await client.connect();
logger.info(`Clearing transform history for: ${target}`);
const result = await client.sendRequest<ClearResult>(
COMMANDS.TRANSFORM_HISTORY_CLEAR,
{ target }
);
if (options.json) {
outputJson(result);
return;
}
logger.info('✓ Transform history cleared');
logger.info(` Object: ${result.objectName}`);
logger.info(` Deleted: ${result.deletedCount} records`);
} catch (error) {
logger.error('Failed to clear transform history', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
}

View File

@@ -0,0 +1,436 @@
/**
* Transform command
*
* Manipulate Unity Transform components.
*/
import { Command } from 'commander';
import * as logger from '@/utils/logger';
import * as config from '@/utils/config';
import { createUnityClient } from '@/unity/client';
import { COMMANDS } from '@/constants';
import type { TransformInfo, Vector3 } from '@/unity/protocol';
import { output, outputJson } from '@/utils/output-formatter';
/**
* Parse Vector3 from string "x,y,z"
*/
function parseVector3(str: string): Vector3 {
const parts = str.split(',').map((s) => parseFloat(s.trim()));
if (parts.length !== 3 || parts.some(isNaN)) {
throw new Error('Invalid Vector3 format. Expected: x,y,z (e.g., "1,2,3")');
}
return { x: parts[0], y: parts[1], z: parts[2] };
}
/**
* Format Vector3 for display
*/
function formatVector3(v: Vector3): string {
return `(${v.x.toFixed(3)}, ${v.y.toFixed(3)}, ${v.z.toFixed(3)})`;
}
/**
* Register Transform command
*/
export function registerTransformCommand(program: Command): void {
const transformCmd = program
.command('transform')
.alias('tf')
.description('Manipulate Unity Transform components');
// Get transform
transformCmd
.command('get')
.description('Get Transform information')
.argument('<name>', 'GameObject name or path')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '300000')
.action(async (name, options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
logger.info('Connecting to Unity Editor...');
await client.connect();
logger.info(`Getting Transform: ${name}`);
// Get position, rotation, scale
const position = await client.sendRequest<Vector3>(
COMMANDS.TRANSFORM_GET_POSITION,
{ name }
);
const rotation = await client.sendRequest<Vector3>(
COMMANDS.TRANSFORM_GET_ROTATION,
{ name }
);
const scale = await client.sendRequest<Vector3>(
COMMANDS.TRANSFORM_GET_SCALE,
{ name }
);
// JSON output
if (options.json) {
outputJson({
gameObject: name,
transform: {
position,
rotation,
scale,
},
});
return;
}
// Text output
logger.info('✓ Transform:');
logger.info(` Position: ${formatVector3(position)}`);
logger.info(` Rotation: ${formatVector3(rotation)}°`);
logger.info(` Scale: ${formatVector3(scale)}`);
} catch (error) {
logger.error('Failed to get Transform', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// Get position
transformCmd
.command('get-position')
.description('Get Transform position')
.argument('<name>', 'GameObject name or path')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '300000')
.action(async (name, options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
logger.info('Connecting to Unity Editor...');
await client.connect();
logger.info(`Getting position: ${name}`);
const position = await client.sendRequest<Vector3>(
COMMANDS.TRANSFORM_GET_POSITION,
{ name }
);
// JSON output
if (options.json) {
outputJson({
gameObject: name,
position,
});
} else {
logger.info(`✓ Position: ${formatVector3(position)}`);
}
} catch (error) {
logger.error('Failed to get position', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// Get rotation
transformCmd
.command('get-rotation')
.description('Get Transform rotation (Euler angles)')
.argument('<name>', 'GameObject name or path')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '300000')
.action(async (name, options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
logger.info('Connecting to Unity Editor...');
await client.connect();
logger.info(`Getting rotation: ${name}`);
const rotation = await client.sendRequest<Vector3>(
COMMANDS.TRANSFORM_GET_ROTATION,
{ name }
);
// JSON output
if (options.json) {
outputJson({
gameObject: name,
rotation,
});
} else {
logger.info(`✓ Rotation: ${formatVector3(rotation)}°`);
}
} catch (error) {
logger.error('Failed to get rotation', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// Get scale
transformCmd
.command('get-scale')
.description('Get Transform scale')
.argument('<name>', 'GameObject name or path')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '300000')
.action(async (name, options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
logger.info('Connecting to Unity Editor...');
await client.connect();
logger.info(`Getting scale: ${name}`);
const scale = await client.sendRequest<Vector3>(
COMMANDS.TRANSFORM_GET_SCALE,
{ name }
);
// JSON output
if (options.json) {
outputJson({
gameObject: name,
scale,
});
} else {
logger.info(`✓ Scale: ${formatVector3(scale)}`);
}
} catch (error) {
logger.error('Failed to get scale', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// Set position
transformCmd
.command('set-position')
.description('Set Transform position')
.argument('<name>', 'GameObject name or path')
.argument('<position>', 'Position as "x,y,z" (e.g., "1,2,3")')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '300000')
.action(async (name, positionStr, options) => {
let client = null;
try {
const position = parseVector3(positionStr);
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
logger.info('Connecting to Unity Editor...');
await client.connect();
logger.info(`Setting position: ${name}${formatVector3(position)}`);
await client.sendRequest(COMMANDS.TRANSFORM_SET_POSITION, {
name,
position,
});
// JSON output
if (options.json) {
outputJson({
success: true,
gameObject: name,
position,
});
} else {
logger.info('✓ Position set');
}
} catch (error) {
logger.error('Failed to set position', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// Set rotation
transformCmd
.command('set-rotation')
.description('Set Transform rotation (Euler angles)')
.argument('<name>', 'GameObject name or path')
.argument('<rotation>', 'Rotation as "x,y,z" degrees (e.g., "0,90,0")')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '300000')
.action(async (name, rotationStr, options) => {
let client = null;
try {
const rotation = parseVector3(rotationStr);
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
logger.info('Connecting to Unity Editor...');
await client.connect();
logger.info(`Setting rotation: ${name}${formatVector3(rotation)}°`);
await client.sendRequest(COMMANDS.TRANSFORM_SET_ROTATION, {
name,
rotation,
});
// JSON output
if (options.json) {
outputJson({
success: true,
gameObject: name,
rotation,
});
} else {
logger.info('✓ Rotation set');
}
} catch (error) {
logger.error('Failed to set rotation', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// Set scale
transformCmd
.command('set-scale')
.description('Set Transform scale')
.argument('<name>', 'GameObject name or path')
.argument('<scale>', 'Scale as "x,y,z" (e.g., "1,1,1")')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'WebSocket connection timeout in milliseconds', '300000')
.action(async (name, scaleStr, options) => {
let client = null;
try {
const scale = parseVector3(scaleStr);
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
logger.info('Connecting to Unity Editor...');
await client.connect();
logger.info(`Setting scale: ${name}${formatVector3(scale)}`);
await client.sendRequest(COMMANDS.TRANSFORM_SET_SCALE, {
name,
scale,
});
// JSON output
if (options.json) {
outputJson({
success: true,
gameObject: name,
scale,
});
} else {
logger.info('✓ Scale set');
}
} catch (error) {
logger.error('Failed to set scale', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
}

View File

@@ -0,0 +1,209 @@
/**
* Wait command
*
* Wait for various Unity conditions (compilation, play mode, scene load, sleep)
*/
import { Command } from 'commander';
import * as logger from '@/utils/logger';
import * as config from '@/utils/config';
import { createUnityClient } from '@/unity/client';
import { COMMANDS } from '@/constants';
import { outputJson } from '@/utils/output-formatter';
/**
* Register Wait command
*/
export function registerWaitCommand(program: Command): void {
const waitCmd = program
.command('wait')
.description('Wait for Unity conditions');
// Wait for compilation
waitCmd
.command('compile')
.description('Wait for Unity compilation to complete')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'Timeout in milliseconds', '300000')
.action(async (options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
await client.connect();
const timeout = parseInt(options.timeout, 10);
const result = await client.sendRequest(COMMANDS.WAIT_WAIT, {
type: 'compile',
timeout: timeout / 1000, // Convert to seconds
}, timeout);
if (options.json) {
outputJson(result);
} else {
logger.info('✓ Compilation completed');
}
} catch (error) {
logger.error('Failed to wait for compilation', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// Wait for play mode
waitCmd
.command('playmode <state>')
.description('Wait for play mode state (enter, exit, pause)')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'Timeout in milliseconds', '300000')
.action(async (state, options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
await client.connect();
const timeout = parseInt(options.timeout, 10);
const result = await client.sendRequest(COMMANDS.WAIT_WAIT, {
type: 'playmode',
value: state,
timeout: timeout / 1000,
}, timeout);
if (options.json) {
outputJson(result);
} else {
logger.info(`✓ Play mode ${state} completed`);
}
} catch (error) {
logger.error(`Failed to wait for play mode ${state}`, error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// Sleep
waitCmd
.command('sleep <seconds>')
.description('Sleep for specified seconds')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'Timeout in milliseconds (must be > sleep duration)', '300000')
.action(async (seconds, options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
const sleepSeconds = parseFloat(seconds);
if (isNaN(sleepSeconds) || sleepSeconds <= 0) {
logger.error('Sleep duration must be a positive number');
process.exit(1);
}
client = createUnityClient(port);
await client.connect();
const timeout = parseInt(options.timeout, 10);
const result = await client.sendRequest(COMMANDS.WAIT_WAIT, {
type: 'sleep',
seconds: sleepSeconds,
timeout: timeout / 1000,
}, timeout);
if (options.json) {
outputJson(result);
} else {
logger.info(`✓ Slept for ${sleepSeconds} seconds`);
}
} catch (error) {
logger.error('Failed to sleep', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
// Wait for scene load
waitCmd
.command('scene')
.description('Wait for scene to finish loading (play mode only)')
.option('--json', 'Output in JSON format')
.option('--timeout <ms>', 'Timeout in milliseconds', '300000')
.action(async (options) => {
let client = null;
try {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
client = createUnityClient(port);
await client.connect();
const timeout = parseInt(options.timeout, 10);
const result = await client.sendRequest(COMMANDS.WAIT_WAIT, {
type: 'scene',
timeout: timeout / 1000,
}, timeout);
if (options.json) {
outputJson(result);
} else {
logger.info('✓ Scene loading completed');
}
} catch (error) {
logger.error('Failed to wait for scene load', error);
process.exit(1);
} finally {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}
});
}

View File

@@ -0,0 +1,256 @@
/**
* Unity WebSocket CLI Constants
*
* Centralized constants for Unity WebSocket communication.
* All magic numbers, timeouts, and configuration values should be defined here.
*/
/**
* Unity WebSocket connection settings
*/
export const UNITY = {
// Port range for Unity WebSocket servers (9500-9600)
DEFAULT_PORT: 9500,
MAX_PORT: 9600,
LOCALHOST: '127.0.0.1',
// WebSocket connection timeouts
WS_TIMEOUT: 30000, // 30 seconds
CONNECT_TIMEOUT: 10000, // 10 seconds
RECONNECT_DELAY: 2000, // 2 seconds
MAX_RECONNECT_ATTEMPTS: 3,
// Command execution timeouts
COMMAND_TIMEOUT: 30000, // 30 seconds for most commands
HIERARCHY_TIMEOUT: 30000, // 30 seconds for hierarchy queries
SCENE_LOAD_TIMEOUT: 60000, // 60 seconds for scene loading
} as const;
/**
* File system paths and directories
*/
export const FS = {
OUTPUT_DIR: '.unity-websocket',
GITIGNORE_CONTENT: '# Unity WebSocket generated files\n*\n',
} as const;
/**
* JSON-RPC 2.0 Protocol
*/
export const JSONRPC = {
VERSION: '2.0',
// Error codes (JSON-RPC standard + custom)
PARSE_ERROR: -32700,
INVALID_REQUEST: -32600,
METHOD_NOT_FOUND: -32601,
INVALID_PARAMS: -32602,
INTERNAL_ERROR: -32603,
// Custom Unity error codes
UNITY_NOT_CONNECTED: -32000,
UNITY_COMMAND_FAILED: -32001,
UNITY_OBJECT_NOT_FOUND: -32002,
UNITY_SCENE_NOT_FOUND: -32003,
UNITY_COMPONENT_NOT_FOUND: -32004,
} as const;
/**
* Unity command categories
*/
export const COMMANDS = {
// GameObject commands
HIERARCHY_GET: 'Hierarchy.Get',
GAMEOBJECT_FIND: 'GameObject.Find',
GAMEOBJECT_CREATE: 'GameObject.Create',
GAMEOBJECT_DESTROY: 'GameObject.Destroy',
GAMEOBJECT_SET_ACTIVE: 'GameObject.SetActive',
// Transform commands
TRANSFORM_GET_POSITION: 'Transform.GetPosition',
TRANSFORM_SET_POSITION: 'Transform.SetPosition',
TRANSFORM_GET_ROTATION: 'Transform.GetRotation',
TRANSFORM_SET_ROTATION: 'Transform.SetRotation',
TRANSFORM_GET_SCALE: 'Transform.GetScale',
TRANSFORM_SET_SCALE: 'Transform.SetScale',
// Component commands
COMPONENT_LIST: 'Component.List',
COMPONENT_ADD: 'Component.Add',
COMPONENT_REMOVE: 'Component.Remove',
COMPONENT_SET_ENABLED: 'Component.SetEnabled',
COMPONENT_GET: 'Component.Get',
COMPONENT_SET: 'Component.Set',
COMPONENT_INSPECT: 'Component.Inspect',
COMPONENT_MOVE_UP: 'Component.MoveUp',
COMPONENT_MOVE_DOWN: 'Component.MoveDown',
COMPONENT_COPY: 'Component.Copy',
// Material commands
MATERIAL_GET_PROPERTY: 'Material.GetProperty',
MATERIAL_SET_PROPERTY: 'Material.SetProperty',
MATERIAL_GET_COLOR: 'Material.GetColor',
MATERIAL_SET_COLOR: 'Material.SetColor',
MATERIAL_LIST: 'Material.List',
MATERIAL_GET_SHADER: 'Material.GetShader',
MATERIAL_SET_SHADER: 'Material.SetShader',
MATERIAL_GET_TEXTURE: 'Material.GetTexture',
MATERIAL_SET_TEXTURE: 'Material.SetTexture',
// Scene commands
SCENE_GET_CURRENT: 'Scene.GetCurrent',
SCENE_LOAD: 'Scene.Load',
SCENE_GET_ALL: 'Scene.GetAll',
SCENE_NEW: 'Scene.New',
SCENE_SAVE: 'Scene.Save',
SCENE_UNLOAD: 'Scene.Unload',
SCENE_SET_ACTIVE: 'Scene.SetActive',
// Console commands
CONSOLE_GET_LOGS: 'Console.GetLogs',
CONSOLE_CLEAR: 'Console.Clear',
// Editor commands
EDITOR_GET_SELECTION: 'Editor.GetSelection',
EDITOR_SET_SELECTION: 'Editor.SetSelection',
EDITOR_FOCUS_GAME_VIEW: 'Editor.FocusGameView',
EDITOR_FOCUS_SCENE_VIEW: 'Editor.FocusSceneView',
EDITOR_REFRESH: 'Editor.Refresh',
EDITOR_RECOMPILE: 'Editor.Recompile',
EDITOR_REIMPORT: 'Editor.Reimport',
EDITOR_EXECUTE: 'Editor.Execute',
EDITOR_LIST_EXECUTABLE: 'Editor.ListExecutable',
// Wait commands
WAIT_WAIT: 'Wait.Wait',
// Chain commands
CHAIN_EXECUTE: 'Chain.Execute',
// Animation commands
ANIMATION_PLAY: 'Animation.Play',
ANIMATION_STOP: 'Animation.Stop',
ANIMATION_GET_STATE: 'Animation.GetState',
ANIMATION_SET_PARAMETER: 'Animation.SetParameter',
ANIMATION_GET_PARAMETER: 'Animation.GetParameter',
ANIMATION_GET_PARAMETERS: 'Animation.GetParameters',
ANIMATION_SET_TRIGGER: 'Animation.SetTrigger',
ANIMATION_RESET_TRIGGER: 'Animation.ResetTrigger',
ANIMATION_CROSSFADE: 'Animation.CrossFade',
// Database commands
DATABASE_STATUS: 'Database.Status',
DATABASE_CONNECT: 'Database.Connect',
DATABASE_DISCONNECT: 'Database.Disconnect',
DATABASE_RESET: 'Database.Reset',
DATABASE_RUN_MIGRATIONS: 'Database.RunMigrations',
DATABASE_CLEAR_MIGRATIONS: 'Database.ClearMigrations',
DATABASE_UNDO: 'Database.Undo',
DATABASE_REDO: 'Database.Redo',
DATABASE_GET_HISTORY: 'Database.GetHistory',
DATABASE_CLEAR_HISTORY: 'Database.ClearHistory',
DATABASE_QUERY: 'Database.Query',
// Snapshot commands
SNAPSHOT_SAVE: 'Snapshot.Save',
SNAPSHOT_LIST: 'Snapshot.List',
SNAPSHOT_GET: 'Snapshot.Get',
SNAPSHOT_RESTORE: 'Snapshot.Restore',
SNAPSHOT_DELETE: 'Snapshot.Delete',
// Transform History commands
TRANSFORM_HISTORY_RECORD: 'TransformHistory.Record',
TRANSFORM_HISTORY_LIST: 'TransformHistory.List',
TRANSFORM_HISTORY_RESTORE: 'TransformHistory.Restore',
TRANSFORM_HISTORY_COMPARE: 'TransformHistory.Compare',
TRANSFORM_HISTORY_CLEAR: 'TransformHistory.Clear',
// Sync commands
SYNC_SCENE: 'Sync.SyncScene',
SYNC_GAMEOBJECT: 'Sync.SyncGameObject',
SYNC_STATUS: 'Sync.GetSyncStatus',
SYNC_CLEAR: 'Sync.ClearSync',
SYNC_START_AUTO: 'Sync.StartAutoSync',
SYNC_STOP_AUTO: 'Sync.StopAutoSync',
SYNC_GET_AUTO_STATUS: 'Sync.GetAutoSyncStatus',
// Analytics commands
ANALYTICS_PROJECT_STATS: 'Analytics.GetProjectStats',
ANALYTICS_SCENE_STATS: 'Analytics.GetSceneStats',
ANALYTICS_SET_CACHE: 'Analytics.SetCache',
ANALYTICS_GET_CACHE: 'Analytics.GetCache',
ANALYTICS_CLEAR_CACHE: 'Analytics.ClearCache',
ANALYTICS_LIST_CACHE: 'Analytics.ListCache',
// Menu commands
MENU_RUN: 'Menu.Run',
MENU_LIST: 'Menu.List',
// Asset commands
ASSET_LIST_SO_TYPES: 'Asset.ListScriptableObjectTypes',
ASSET_CREATE_SO: 'Asset.CreateScriptableObject',
ASSET_GET_FIELDS: 'Asset.GetFields',
ASSET_SET_FIELD: 'Asset.SetField',
ASSET_INSPECT: 'Asset.Inspect',
ASSET_ADD_ARRAY_ELEMENT: 'Asset.AddArrayElement',
ASSET_REMOVE_ARRAY_ELEMENT: 'Asset.RemoveArrayElement',
ASSET_GET_ARRAY_ELEMENT: 'Asset.GetArrayElement',
ASSET_CLEAR_ARRAY: 'Asset.ClearArray',
// Prefab commands
PREFAB_INSTANTIATE: 'Prefab.Instantiate',
PREFAB_CREATE: 'Prefab.Create',
PREFAB_UNPACK: 'Prefab.Unpack',
PREFAB_APPLY: 'Prefab.Apply',
PREFAB_REVERT: 'Prefab.Revert',
PREFAB_VARIANT: 'Prefab.Variant',
PREFAB_GET_OVERRIDES: 'Prefab.GetOverrides',
PREFAB_GET_SOURCE: 'Prefab.GetSource',
PREFAB_IS_INSTANCE: 'Prefab.IsInstance',
PREFAB_OPEN: 'Prefab.Open',
PREFAB_CLOSE: 'Prefab.Close',
PREFAB_LIST: 'Prefab.List',
} as const;
/**
* Logger levels
*/
export enum LogLevel {
ERROR = 0,
WARN = 1,
INFO = 2,
DEBUG = 3,
VERBOSE = 4,
}
/**
* Logger level names mapping
*/
export const LOG_LEVEL_NAMES: Record<LogLevel, string> = {
[LogLevel.ERROR]: 'ERROR',
[LogLevel.WARN]: 'WARN',
[LogLevel.INFO]: 'INFO',
[LogLevel.DEBUG]: 'DEBUG',
[LogLevel.VERBOSE]: 'VERBOSE',
};
/**
* Unity log type mapping
*/
export enum UnityLogType {
ERROR = 0,
ASSERT = 1,
WARNING = 2,
LOG = 3,
EXCEPTION = 4,
}
/**
* Environment variable names
*/
export const ENV = {
PROJECT_DIR: 'CLAUDE_PROJECT_DIR',
PLUGIN_ROOT: 'CLAUDE_PLUGIN_ROOT',
UNITY_WS_PORT: 'UNITY_WS_PORT',
LOG_LEVEL: 'UNITY_WS_LOG_LEVEL',
} as const;

View File

@@ -0,0 +1,343 @@
/**
* Unity WebSocket Client
*
* WebSocket client for communicating with Unity Editor via JSON-RPC 2.0 protocol.
*/
import WebSocket from 'ws';
import { UNITY, JSONRPC } from '@/constants';
import * as logger from '@/utils/logger';
import type {
JSONRPCRequest,
JSONRPCResponse,
JSONRPCErrorResponse,
isErrorResponse,
} from './protocol';
/**
* Custom error class for Unity RPC errors
*/
export class UnityRPCError extends Error {
code: number;
data?: unknown;
constructor(message: string, code: number, data?: unknown) {
super(message);
this.name = 'UnityRPCError';
this.code = code;
this.data = data;
}
}
/**
* Pending request information
*/
interface PendingRequest {
resolve: (value: any) => void;
reject: (error: Error) => void;
timer: NodeJS.Timeout;
method: string;
}
/**
* Unity WebSocket Client
*/
export class UnityWebSocketClient {
private ws: WebSocket | null = null;
private connected = false;
private host: string;
private port: number;
private reconnectAttempts = 0;
private maxReconnectAttempts = UNITY.MAX_RECONNECT_ATTEMPTS;
private reconnectDelay = UNITY.RECONNECT_DELAY;
private pendingRequests = new Map<string | number, PendingRequest>();
private requestIdCounter = 0;
/**
* Create Unity WebSocket Client
*/
constructor(port: number, host: string = UNITY.LOCALHOST) {
// Validate port range (security: prevent invalid connections)
if (port < UNITY.DEFAULT_PORT || port > UNITY.MAX_PORT) {
throw new Error(`Port must be between ${UNITY.DEFAULT_PORT} and ${UNITY.MAX_PORT}`);
}
// Validate host (security: only allow localhost)
if (host !== '127.0.0.1' && host !== 'localhost') {
throw new Error('Only localhost connections are allowed');
}
this.host = host;
this.port = port;
}
/**
* Connect to Unity Editor WebSocket server
*/
async connect(): Promise<void> {
return new Promise((resolve, reject) => {
const wsUrl = `ws://${this.host}:${this.port}`;
logger.debug(`Connecting to Unity Editor at ${wsUrl}...`);
const ws = new WebSocket(wsUrl, {
handshakeTimeout: UNITY.CONNECT_TIMEOUT,
});
const timeout = setTimeout(() => {
if (ws.readyState !== WebSocket.OPEN) {
ws.terminate();
reject(new Error(`Connection timeout after ${UNITY.CONNECT_TIMEOUT}ms`));
}
}, UNITY.CONNECT_TIMEOUT);
ws.on('open', () => {
clearTimeout(timeout);
this.ws = ws;
this.connected = true;
this.reconnectAttempts = 0;
logger.info('✓ Connected to Unity Editor');
resolve();
});
ws.on('message', (data: WebSocket.Data) => {
this.handleMessage(data);
});
ws.on('error', (error: Error) => {
clearTimeout(timeout);
logger.error('WebSocket error', error);
if (!this.connected) {
reject(error);
}
});
ws.on('close', (code: number, reason: string) => {
clearTimeout(timeout);
this.connected = false;
logger.warn(`WebSocket closed (code: ${code}, reason: ${reason})`);
// Reject all pending requests
this.rejectAllPendingRequests(new Error('WebSocket connection closed'));
// Auto-reconnect if needed
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.attemptReconnect();
}
});
});
}
/**
* Disconnect from Unity Editor
*/
disconnect(): void {
if (this.ws) {
// Reject all pending requests before closing
this.rejectAllPendingRequests(new Error('Client disconnected'));
// Close WebSocket connection
this.ws.close();
// Remove all event listeners
this.ws.removeAllListeners();
this.ws = null;
this.connected = false;
// Prevent reconnection
this.reconnectAttempts = this.maxReconnectAttempts;
logger.info('Disconnected from Unity Editor');
}
}
/**
* Check if connected
*/
isConnected(): boolean {
return this.connected && this.ws !== null && this.ws.readyState === WebSocket.OPEN;
}
/**
* Send JSON-RPC request to Unity Editor
*/
async sendRequest<T = unknown>(method: string, params?: unknown, timeout?: number): Promise<T> {
// Validate input parameters
if (!method || typeof method !== 'string' || method.trim() === '') {
throw new UnityRPCError('Method name is required and must be a non-empty string', JSONRPC.INVALID_PARAMS);
}
if (timeout !== undefined && (typeof timeout !== 'number' || timeout <= 0)) {
throw new UnityRPCError('Timeout must be a positive number', JSONRPC.INVALID_PARAMS);
}
if (!this.isConnected()) {
throw new UnityRPCError('Not connected to Unity Editor', JSONRPC.UNITY_NOT_CONNECTED);
}
const requestId = `req_${++this.requestIdCounter}`;
const request: JSONRPCRequest = {
jsonrpc: JSONRPC.VERSION,
id: requestId,
method: method.trim(),
params,
};
return new Promise((resolve, reject) => {
const requestTimeout = timeout || UNITY.COMMAND_TIMEOUT;
const timer = setTimeout(() => {
this.pendingRequests.delete(requestId);
// Log timeout with detailed information for debugging
logger.warn(`Request timeout: ${method} (ID: ${requestId}, timeout: ${requestTimeout}ms)`);
// Create detailed timeout error
const timeoutError = new UnityRPCError(
`Request timeout after ${requestTimeout}ms`,
JSONRPC.INTERNAL_ERROR,
{
method,
requestId,
timeout: requestTimeout,
params,
timestamp: new Date().toISOString(),
}
);
reject(timeoutError);
}, requestTimeout);
this.pendingRequests.set(requestId, {
resolve,
reject,
timer,
method,
});
const message = JSON.stringify(request);
logger.debug(`${method}: ${message}`);
this.ws!.send(message, (error) => {
if (error) {
clearTimeout(timer);
this.pendingRequests.delete(requestId);
const sendError = new UnityRPCError(
`Failed to send request: ${error.message}`,
JSONRPC.INTERNAL_ERROR,
{ method, requestId, originalError: error.message }
);
reject(sendError);
}
});
});
}
/**
* Handle incoming WebSocket message
*/
private handleMessage(data: WebSocket.Data): void {
let message: string;
let response: any;
try {
message = data.toString();
logger.debug(`${message}`);
response = JSON.parse(message);
} catch (error) {
logger.error('Failed to parse message', error);
return; // Parsing failed, pending requests will timeout
}
// Validate JSON-RPC structure
if (!response || typeof response !== 'object') {
logger.error('Invalid response structure');
return;
}
if (!response.id) {
logger.warn('Received response without ID, ignoring');
return;
}
const pending = this.pendingRequests.get(response.id);
if (!pending) {
logger.warn(`Received response for unknown request ID: ${response.id}`);
return;
}
clearTimeout(pending.timer);
this.pendingRequests.delete(response.id);
try {
if ('error' in response) {
const errorResponse = response as JSONRPCErrorResponse;
const error = new UnityRPCError(
errorResponse.error.message,
errorResponse.error.code,
errorResponse.error.data
);
pending.reject(error);
} else if ('result' in response) {
pending.resolve(response.result);
} else {
pending.reject(new Error('Invalid JSON-RPC response'));
}
} catch (error) {
logger.error('Error processing response', error);
pending.reject(error instanceof Error ? error : new Error(String(error)));
}
}
/**
* Attempt to reconnect
*/
private async attemptReconnect(): Promise<void> {
this.reconnectAttempts++;
logger.info(`Attempting to reconnect (${this.reconnectAttempts}/${this.maxReconnectAttempts})...`);
await new Promise((resolve) => setTimeout(resolve, this.reconnectDelay));
try {
await this.connect();
logger.info('✓ Reconnected successfully');
} catch (error) {
logger.error('Reconnection failed', error);
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.attemptReconnect();
} else {
logger.error('Max reconnection attempts reached');
}
}
}
/**
* Reject all pending requests
*/
private rejectAllPendingRequests(error: Error): void {
for (const [id, pending] of this.pendingRequests.entries()) {
clearTimeout(pending.timer);
pending.reject(error);
}
this.pendingRequests.clear();
}
/**
* Get connection info
*/
getConnectionInfo(): { host: string; port: number; connected: boolean } {
return {
host: this.host,
port: this.port,
connected: this.connected,
};
}
}
/**
* Create Unity WebSocket client
*/
export function createUnityClient(port: number, host?: string): UnityWebSocketClient {
return new UnityWebSocketClient(port, host);
}

View File

@@ -0,0 +1,205 @@
/**
* Unity WebSocket JSON-RPC 2.0 Protocol
*
* Type definitions for Unity WebSocket communication using JSON-RPC 2.0 protocol.
*/
import { JSONRPC } from '@/constants';
/**
* JSON-RPC 2.0 Request
*/
export interface JSONRPCRequest<T = unknown> {
jsonrpc: typeof JSONRPC.VERSION;
id: string | number;
method: string;
params?: T;
}
/**
* JSON-RPC 2.0 Success Response
*/
export interface JSONRPCSuccessResponse<T = unknown> {
jsonrpc: typeof JSONRPC.VERSION;
id: string | number;
result: T;
}
/**
* JSON-RPC 2.0 Error Object
*/
export interface JSONRPCError {
code: number;
message: string;
data?: unknown;
}
/**
* JSON-RPC 2.0 Error Response
*/
export interface JSONRPCErrorResponse {
jsonrpc: typeof JSONRPC.VERSION;
id: string | number | null;
error: JSONRPCError;
}
/**
* JSON-RPC 2.0 Response (success or error)
*/
export type JSONRPCResponse<T = unknown> = JSONRPCSuccessResponse<T> | JSONRPCErrorResponse;
/**
* Type guard for error response
*/
export function isErrorResponse(response: JSONRPCResponse): response is JSONRPCErrorResponse {
return 'error' in response;
}
/**
* Type guard for success response
*/
export function isSuccessResponse<T>(response: JSONRPCResponse<T>): response is JSONRPCSuccessResponse<T> {
return 'result' in response;
}
/**
* Unity GameObject information
*/
export interface GameObjectInfo {
name: string;
instanceId: number;
path: string;
active: boolean;
tag: string;
layer: number;
components?: string[]; // Component type names
children?: GameObjectInfo[];
}
/**
* Unity Transform information
*/
export interface TransformInfo {
position: Vector3;
rotation: Vector3; // Euler angles
scale: Vector3;
}
/**
* Unity Vector3
*/
export interface Vector3 {
x: number;
y: number;
z: number;
}
/**
* Unity Component information
*/
export interface ComponentInfo {
type: string;
fullTypeName: string;
enabled: boolean;
isMonoBehaviour: boolean;
properties?: Record<string, unknown>;
}
/**
* Component list result
*/
export interface ComponentListResult {
count: number;
components: ComponentInfo[];
}
/**
* Component property information
*/
export interface PropertyInfo {
name: string;
type: string;
value: unknown;
}
/**
* Get component result
*/
export interface GetComponentResult {
componentType: string;
properties: PropertyInfo[];
}
/**
* Set property result
*/
export interface SetPropertyResult {
success: boolean;
property: string;
oldValue: unknown;
newValue: unknown;
}
/**
* Inspect component result
*/
export interface InspectComponentResult {
componentType: string;
fullTypeName: string;
enabled: boolean;
isMonoBehaviour: boolean;
properties: PropertyInfo[];
propertyCount: number;
}
/**
* Unity Material property
*/
export interface MaterialProperty {
name: string;
type: string;
value: unknown;
}
/**
* Unity Scene information
*/
export interface SceneInfo {
name: string;
path: string;
buildIndex: number;
isLoaded: boolean;
isDirty: boolean;
rootCount: number;
}
/**
* Unity Console Log entry
*/
export interface ConsoleLogEntry {
type: number; // UnityLogType enum value
message: string;
stackTrace: string;
timestamp: string;
}
/**
* Unity Editor Selection
*/
export interface EditorSelection {
gameObjects: GameObjectInfo[];
assets: string[];
}
/**
* Animation state
*/
export interface AnimationState {
name: string;
enabled: boolean;
weight: number;
time: number;
length: number;
speed: number;
normalizedTime: number;
}

View File

@@ -0,0 +1,52 @@
/**
* Command Helper Utilities
*
* Common utilities for CLI commands to reduce code duplication.
*/
import { Command } from 'commander';
import * as config from './config';
import * as logger from './logger';
import { createUnityClient, UnityWebSocketClient } from '@/unity/client';
/**
* Get Unity port from command options or server status
* Exits process if port is not available
*/
export function getUnityPortOrExit(program: Command): number {
const projectRoot = config.getProjectRoot();
const port = program.opts().port || config.getUnityPort(projectRoot);
if (!port) {
logger.error('Unity server not running. Start Unity Editor with WebSocket server enabled.');
process.exit(1);
}
return port;
}
/**
* Create and connect to Unity Editor WebSocket client
*/
export async function connectToUnity(port: number): Promise<UnityWebSocketClient> {
const client = createUnityClient(port);
logger.info('Connecting to Unity Editor...');
await client.connect();
return client;
}
/**
* Safely disconnect Unity client
* Logs error if disconnect fails but doesn't throw
*/
export function disconnectUnity(client: UnityWebSocketClient | null): void {
if (client) {
try {
client.disconnect();
} catch (disconnectError) {
logger.debug(`Error during disconnect: ${disconnectError instanceof Error ? disconnectError.message : String(disconnectError)}`);
}
}
}

View File

@@ -0,0 +1,152 @@
/**
* Unity WebSocket Configuration Management
*
* Handles loading and saving shared configuration file.
* Config file location: ${CLAUDE_PLUGIN_ROOT}/skills/unity-websocket-config.json
*/
import * as fs from 'fs';
import * as path from 'path';
import { FS, ENV } from '@/constants';
import * as logger from './logger';
/**
* Constants
*/
const HEARTBEAT_STALE_SECONDS = 30;
/**
* Server status interface (from .unity-websocket/server-status.json)
*/
export interface ServerStatus {
version: string;
port: number;
isRunning: boolean;
pid: number;
editorVersion: string;
startedAt: string;
lastHeartbeat: string;
}
// Legacy shared config interfaces - deprecated, kept for reference only
// Server now uses .unity-websocket/server-status.json instead
// Legacy shared config functions removed - no longer needed
// Server status is now managed via .unity-websocket/server-status.json
/**
* Get project root directory
*/
export function getProjectRoot(): string {
const projectRoot = process.env[ENV.PROJECT_DIR];
if (!projectRoot) {
throw new Error(`${ENV.PROJECT_DIR} environment variable not set`);
}
return path.resolve(projectRoot);
}
/**
* Get project name from root directory
*/
export function getProjectName(projectRoot?: string): string {
const root = projectRoot || getProjectRoot();
return path.basename(root);
}
/**
* Check if current directory is a Unity project
*/
export function isUnityProject(projectRoot?: string): boolean {
const root = projectRoot || getProjectRoot();
const assetsDir = path.join(root, 'Assets');
const projectSettingsDir = path.join(root, 'ProjectSettings');
return fs.existsSync(assetsDir) && fs.existsSync(projectSettingsDir);
}
/**
* Get Unity project output directory
*/
export function getOutputDir(projectRoot?: string): string {
const root = projectRoot || getProjectRoot();
return path.join(root, FS.OUTPUT_DIR);
}
/**
* Get server status file path
*/
export function getServerStatusPath(projectRoot?: string): string {
const root = projectRoot || getProjectRoot();
return path.join(root, '.unity-websocket', 'server-status.json');
}
/**
* Read server status from .unity-websocket/server-status.json
*/
export function readServerStatus(projectRoot?: string): ServerStatus | null {
try {
const statusPath = getServerStatusPath(projectRoot);
if (!fs.existsSync(statusPath)) {
logger.debug('Server status file not found');
return null;
}
const data = fs.readFileSync(statusPath, 'utf-8');
const status = JSON.parse(data) as ServerStatus;
// Validate required fields
if (
typeof status.port !== 'number' ||
typeof status.isRunning !== 'boolean' ||
typeof status.lastHeartbeat !== 'string'
) {
logger.warn('Invalid server status structure');
return null;
}
return status;
} catch (error) {
logger.debug(`Failed to read server status: ${error instanceof Error ? error.message : String(error)}`);
return null;
}
}
/**
* Check if server status is stale (heartbeat > configured seconds old)
*/
export function isServerStatusStale(status: ServerStatus): boolean {
try {
if (!status.lastHeartbeat) {
return true;
}
const lastBeat = new Date(status.lastHeartbeat);
const now = new Date();
const secondsSinceLastBeat = (now.getTime() - lastBeat.getTime()) / 1000;
return secondsSinceLastBeat > HEARTBEAT_STALE_SECONDS;
} catch (error) {
logger.debug(`Failed to check heartbeat: ${error instanceof Error ? error.message : String(error)}`);
return true;
}
}
/**
* Get Unity WebSocket server port
*
* Reads port from server-status.json written by Unity server.
* Returns null if server is not running or status is stale.
*/
export function getUnityPort(projectRoot?: string): number | null {
const root = projectRoot || getProjectRoot();
// Read from server-status.json (current running server)
const status = readServerStatus(root);
if (status && status.isRunning && !isServerStatusStale(status)) {
logger.debug(`Using port ${status.port} from server status`);
return status.port;
}
logger.debug('No active Unity server detected');
return null;
}

View File

@@ -0,0 +1,144 @@
/**
* Unity WebSocket CLI Logger
*
* Centralized logging utility with configurable log levels.
* Supports ERROR, WARN, INFO, DEBUG, and VERBOSE levels.
*/
import { LogLevel, LOG_LEVEL_NAMES, ENV } from '@/constants';
/**
* Current log level (configurable via environment variable)
*/
let currentLogLevel: LogLevel = LogLevel.INFO;
// Read log level from environment
const envLogLevel = process.env[ENV.LOG_LEVEL];
if (envLogLevel) {
const level = parseInt(envLogLevel, 10);
if (level >= LogLevel.ERROR && level <= LogLevel.VERBOSE) {
currentLogLevel = level;
}
}
/**
* Set log level programmatically
*/
export function setLogLevel(level: LogLevel): void {
currentLogLevel = level;
}
/**
* Get current log level
*/
export function getLogLevel(): LogLevel {
return currentLogLevel;
}
/**
* Sanitize log message (prevents log injection)
*/
function sanitizeMessage(message: string): string {
// Remove or escape newline characters to prevent log injection
return message.replace(/[\r\n]/g, ' ').trim();
}
/**
* Format log message with timestamp and level
*/
function formatMessage(level: LogLevel, message: string): string {
const now = new Date();
const hours = String(now.getHours()).padStart(2, '0');
const minutes = String(now.getMinutes()).padStart(2, '0');
const seconds = String(now.getSeconds()).padStart(2, '0');
const ms = String(now.getMilliseconds()).padStart(3, '0');
const timestamp = `${hours}:${minutes}:${seconds}.${ms}`;
const levelName = LOG_LEVEL_NAMES[level];
const sanitized = sanitizeMessage(message);
return `[${timestamp}] [${levelName}] ${sanitized}`;
}
/**
* Log error message (always shown)
*/
export function error(message: string, err?: Error | unknown): void {
if (currentLogLevel >= LogLevel.ERROR) {
console.error(formatMessage(LogLevel.ERROR, message));
if (err instanceof Error) {
console.error(' Error:', err.message);
// Show UnityRPCError data if available
if ('data' in err && err.data) {
console.error(' Details:', err.data);
}
if (currentLogLevel >= LogLevel.DEBUG && err.stack) {
console.error(' Stack:', err.stack);
}
} else if (err) {
console.error(' Error:', String(err));
}
}
}
/**
* Log warning message
*/
export function warn(message: string): void {
if (currentLogLevel >= LogLevel.WARN) {
console.warn(formatMessage(LogLevel.WARN, message));
}
}
/**
* Log info message (default level)
*/
export function info(message: string): void {
if (currentLogLevel >= LogLevel.INFO) {
console.log(formatMessage(LogLevel.INFO, message));
}
}
/**
* Log debug message
*/
export function debug(message: string): void {
if (currentLogLevel >= LogLevel.DEBUG) {
console.log(formatMessage(LogLevel.DEBUG, message));
}
}
/**
* Log verbose message (detailed)
*/
export function verbose(message: string): void {
if (currentLogLevel >= LogLevel.VERBOSE) {
console.log(formatMessage(LogLevel.VERBOSE, message));
}
}
/**
* Log object in JSON format (debug level)
*/
export function debugObject(label: string, obj: unknown): void {
if (currentLogLevel >= LogLevel.DEBUG) {
console.log(formatMessage(LogLevel.DEBUG, `${label}:`));
console.log(JSON.stringify(obj, null, 2));
}
}
/**
* Logger interface (named exports above)
*/
export const logger = {
setLogLevel,
getLogLevel,
error,
warn,
info,
debug,
verbose,
debugObject,
};
export default logger;

View File

@@ -0,0 +1,75 @@
/**
* Output Formatter Utility
*
* Provides unified output formatting for CLI commands.
* Supports both JSON and human-readable text formats.
*/
/**
* Output data in JSON format
*/
export function outputJson(data: any): void {
console.log(JSON.stringify(data, null, 2));
}
/**
* Output data based on format flag
*/
export function output(data: any, isJson: boolean, textFormatter?: (data: any) => string): void {
if (isJson) {
outputJson(data);
} else if (textFormatter) {
console.log(textFormatter(data));
} else {
// Default: just stringify with indentation
console.log(JSON.stringify(data, null, 2));
}
}
/**
* Format array data as a simple table
*/
export function formatTable(headers: string[], rows: string[][]): string {
const columnWidths = headers.map((header, i) => {
const maxRowWidth = Math.max(...rows.map(row => (row[i] || '').length));
return Math.max(header.length, maxRowWidth);
});
const separator = columnWidths.map(w => '-'.repeat(w + 2)).join('+');
const headerRow = headers.map((h, i) => h.padEnd(columnWidths[i])).join(' | ');
const dataRows = rows.map(row =>
row.map((cell, i) => (cell || '').padEnd(columnWidths[i])).join(' | ')
);
return [headerRow, separator, ...dataRows].join('\n');
}
/**
* Format object as key-value pairs
*/
export function formatKeyValue(obj: Record<string, any>, indent: number = 0): string {
const indentStr = ' '.repeat(indent);
return Object.entries(obj)
.map(([key, value]) => {
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
return `${indentStr}${key}:\n${formatKeyValue(value, indent + 2)}`;
}
return `${indentStr}${key}: ${value}`;
})
.join('\n');
}
/**
* Format list with bullets
*/
export function formatList(items: string[], bullet: string = '•'): string {
return items.map(item => `${bullet} ${item}`).join('\n');
}
/**
* Truncate string with ellipsis
*/
export function truncate(str: string, maxLength: number): string {
if (str.length <= maxLength) return str;
return str.substring(0, maxLength - 3) + '...';
}

View File

@@ -0,0 +1,25 @@
{
"compilerOptions": {
"target": "ES2023",
"module": "commonjs",
"lib": ["ES2023"],
"types": ["node"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"moduleResolution": "node",
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}