Initial commit
This commit is contained in:
43
skills/scripts/eslint.config.mjs
Normal file
43
skills/scripts/eslint.config.mjs
Normal 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'
|
||||
}
|
||||
}
|
||||
];
|
||||
39
skills/scripts/package.json
Normal file
39
skills/scripts/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
134
skills/scripts/src/cli/cli.ts
Normal file
134
skills/scripts/src/cli/cli.ts
Normal 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();
|
||||
}
|
||||
404
skills/scripts/src/cli/commands/analytics.ts
Normal file
404
skills/scripts/src/cli/commands/analytics.ts
Normal 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)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
602
skills/scripts/src/cli/commands/animation.ts
Normal file
602
skills/scripts/src/cli/commands/animation.ts
Normal 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)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
695
skills/scripts/src/cli/commands/asset.ts
Normal file
695
skills/scripts/src/cli/commands/asset.ts
Normal 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)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
263
skills/scripts/src/cli/commands/chain.ts
Normal file
263
skills/scripts/src/cli/commands/chain.ts
Normal 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)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
743
skills/scripts/src/cli/commands/component.ts
Normal file
743
skills/scripts/src/cli/commands/component.ts
Normal 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)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
254
skills/scripts/src/cli/commands/console.ts
Normal file
254
skills/scripts/src/cli/commands/console.ts
Normal 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)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
698
skills/scripts/src/cli/commands/db.ts
Normal file
698
skills/scripts/src/cli/commands/db.ts
Normal 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)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
487
skills/scripts/src/cli/commands/editor.ts
Normal file
487
skills/scripts/src/cli/commands/editor.ts
Normal 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)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
285
skills/scripts/src/cli/commands/gameobject.ts
Normal file
285
skills/scripts/src/cli/commands/gameobject.ts
Normal 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)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
167
skills/scripts/src/cli/commands/hierarchy.ts
Normal file
167
skills/scripts/src/cli/commands/hierarchy.ts
Normal 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
628
skills/scripts/src/cli/commands/material.ts
Normal file
628
skills/scripts/src/cli/commands/material.ts
Normal 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)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
154
skills/scripts/src/cli/commands/menu.ts
Normal file
154
skills/scripts/src/cli/commands/menu.ts
Normal 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)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
810
skills/scripts/src/cli/commands/prefab.ts
Normal file
810
skills/scripts/src/cli/commands/prefab.ts
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
341
skills/scripts/src/cli/commands/prefs.ts
Normal file
341
skills/scripts/src/cli/commands/prefs.ts
Normal 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)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
444
skills/scripts/src/cli/commands/scene.ts
Normal file
444
skills/scripts/src/cli/commands/scene.ts
Normal 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)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
380
skills/scripts/src/cli/commands/snapshot.ts
Normal file
380
skills/scripts/src/cli/commands/snapshot.ts
Normal 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)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
441
skills/scripts/src/cli/commands/sync.ts
Normal file
441
skills/scripts/src/cli/commands/sync.ts
Normal 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)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
383
skills/scripts/src/cli/commands/transform-history.ts
Normal file
383
skills/scripts/src/cli/commands/transform-history.ts
Normal 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)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
436
skills/scripts/src/cli/commands/transform.ts
Normal file
436
skills/scripts/src/cli/commands/transform.ts
Normal 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)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
209
skills/scripts/src/cli/commands/wait.ts
Normal file
209
skills/scripts/src/cli/commands/wait.ts
Normal 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)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
256
skills/scripts/src/constants/index.ts
Normal file
256
skills/scripts/src/constants/index.ts
Normal 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;
|
||||
343
skills/scripts/src/unity/client.ts
Normal file
343
skills/scripts/src/unity/client.ts
Normal 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);
|
||||
}
|
||||
205
skills/scripts/src/unity/protocol.ts
Normal file
205
skills/scripts/src/unity/protocol.ts
Normal 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;
|
||||
}
|
||||
52
skills/scripts/src/utils/command-helpers.ts
Normal file
52
skills/scripts/src/utils/command-helpers.ts
Normal 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)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
152
skills/scripts/src/utils/config.ts
Normal file
152
skills/scripts/src/utils/config.ts
Normal 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;
|
||||
}
|
||||
144
skills/scripts/src/utils/logger.ts
Normal file
144
skills/scripts/src/utils/logger.ts
Normal 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;
|
||||
75
skills/scripts/src/utils/output-formatter.ts
Normal file
75
skills/scripts/src/utils/output-formatter.ts
Normal 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) + '...';
|
||||
}
|
||||
25
skills/scripts/tsconfig.json
Normal file
25
skills/scripts/tsconfig.json
Normal 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"]
|
||||
}
|
||||
Reference in New Issue
Block a user