-
Notifications
You must be signed in to change notification settings - Fork 0
feat: engine plugin system for game-engine agnostic orchestration #4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,70 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { spawnSync } from 'child_process'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { EnginePlugin } from './engine-plugin'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import OrchestratorLogger from '../orchestrator/services/core/orchestrator-logger'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Load an EnginePlugin from an external CLI executable. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Protocol: the executable receives `{"command":"get-engine-config"}` on stdin | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * and must print a JSON response on stdout: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * { "name": "godot", "cacheFolders": [".godot/imported"], "preStopCommand": "..." } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * The executable can be any language (Go, Python, Rust, shell, etc.). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export function loadEngineFromCli(executablePath: string): EnginePlugin { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const request = JSON.stringify({ command: 'get-engine-config' }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const result = spawnSync(executablePath, ['get-engine-config'], { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| input: request, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| encoding: 'utf-8', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| timeout: 30_000, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| shell: process.platform === 'win32', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (result.error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error(`Failed to spawn engine plugin executable '${executablePath}': ${result.error.message}`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (result.status !== 0) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const stderr = result.stderr?.trim() || ''; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| `Engine plugin executable '${executablePath}' exited with code ${result.status}${stderr ? ': ' + stderr : ''}`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const stdout = result.stdout?.trim() || ''; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Find the last JSON object in stdout (skip any non-JSON preamble) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const lines = stdout.split('\n'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let config: any; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (let i = lines.length - 1; i >= 0; i--) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const parsed = JSON.parse(lines[i].trim()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (typeof parsed === 'object' && parsed !== null && parsed.name) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| config = parsed; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| break; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Not JSON, skip | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+39
to
+51
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Line-by-line JSON parsing will reject valid multi-line plugin responses. Current parsing only attempts Proposed fix- const lines = stdout.split('\n');
- let config: any;
- for (let i = lines.length - 1; i >= 0; i--) {
- try {
- const parsed = JSON.parse(lines[i].trim());
- if (typeof parsed === 'object' && parsed !== null && parsed.name) {
- config = parsed;
- break;
- }
- } catch {
- // Not JSON, skip
- }
- }
+ const lines = stdout.split('\n');
+ let config: any;
+ for (let i = lines.length - 1; i >= 0; i--) {
+ const candidate = lines.slice(i).join('\n').trim();
+ if (!candidate) continue;
+ try {
+ const parsed = JSON.parse(candidate);
+ if (typeof parsed === 'object' && parsed !== null && parsed.name) {
+ config = parsed;
+ break;
+ }
+ } catch {
+ // Not valid JSON from this boundary, continue scanning upward
+ }
+ }📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!config) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error(`Engine plugin executable '${executablePath}' did not return valid JSON config. Output: ${stdout}`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!config.name || !Array.isArray(config.cacheFolders)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| `Engine plugin config from '${executablePath}' missing required fields (name, cacheFolders). Got: ${JSON.stringify(config)}`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| OrchestratorLogger.log(`Loaded engine plugin '${config.name}' from CLI: ${executablePath}`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| name: config.name, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cacheFolders: config.cacheFolders, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| preStopCommand: config.preStopCommand || undefined, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| import { spawnSync } from 'child_process'; | ||
| import { EnginePlugin } from './engine-plugin'; | ||
| import OrchestratorLogger from '../orchestrator/services/core/orchestrator-logger'; | ||
|
|
||
| /** | ||
| * Load an EnginePlugin from a Docker image. | ||
| * | ||
| * The container is run with `docker run --rm <image> get-engine-config` | ||
| * and must print a JSON config on stdout: | ||
| * | ||
| * { "name": "godot", "cacheFolders": [".godot/imported"], "preStopCommand": "..." } | ||
| * | ||
| * This allows community engine plugins to be distributed as Docker images. | ||
| */ | ||
| export function loadEngineFromDocker(image: string): EnginePlugin { | ||
| const result = spawnSync('docker', ['run', '--rm', image, 'get-engine-config'], { | ||
| encoding: 'utf-8', | ||
| timeout: 120_000, | ||
| shell: process.platform === 'win32', | ||
| }); | ||
|
|
||
| if (result.error) { | ||
| throw new Error(`Failed to run engine plugin Docker image '${image}': ${result.error.message}`); | ||
| } | ||
|
|
||
| if (result.status !== 0) { | ||
| const stderr = result.stderr?.trim() || ''; | ||
| throw new Error( | ||
| `Engine plugin Docker image '${image}' exited with code ${result.status}${stderr ? ': ' + stderr : ''}`, | ||
| ); | ||
| } | ||
|
|
||
| const stdout = result.stdout?.trim() || ''; | ||
|
|
||
| // Find the last JSON object in stdout | ||
| const lines = stdout.split('\n'); | ||
| let config: any; | ||
| for (let i = lines.length - 1; i >= 0; i--) { | ||
| try { | ||
| const parsed = JSON.parse(lines[i].trim()); | ||
| if (typeof parsed === 'object' && parsed !== null && parsed.name) { | ||
| config = parsed; | ||
| break; | ||
| } | ||
| } catch { | ||
| // Not JSON, skip | ||
| } | ||
| } | ||
|
Comment on lines
+35
to
+48
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Line-oriented parsing rejects valid Docker plugin output. At Lines 35-48, the loader only 🤖 Prompt for AI Agents |
||
|
|
||
| if (!config) { | ||
| throw new Error(`Engine plugin Docker image '${image}' did not return valid JSON config. Output: ${stdout}`); | ||
| } | ||
|
|
||
| if (!config.name || !Array.isArray(config.cacheFolders)) { | ||
| throw new Error( | ||
| `Engine plugin config from Docker image '${image}' missing required fields (name, cacheFolders). Got: ${JSON.stringify(config)}`, | ||
| ); | ||
| } | ||
|
|
||
| OrchestratorLogger.log(`Loaded engine plugin '${config.name}' from Docker: ${image}`); | ||
|
|
||
| return { | ||
| name: config.name, | ||
| cacheFolders: config.cacheFolders, | ||
| preStopCommand: config.preStopCommand || undefined, | ||
| }; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| /** | ||
| * Engine plugin interface — allows the orchestrator to support | ||
| * different game engines (Unity, Godot, Unreal, etc.) without | ||
| * hardcoding engine-specific behavior. | ||
| * | ||
| * Each engine provides: | ||
| * - Cache folder names (e.g. 'Library' for Unity, '.godot/imported' for Godot) | ||
| * - Optional container preStop command (e.g. license cleanup for Unity) | ||
| */ | ||
| export interface EnginePlugin { | ||
| /** Engine identifier: 'unity', 'godot', 'unreal', etc. */ | ||
| readonly name: string; | ||
|
|
||
| /** | ||
| * Folders to cache between builds, relative to projectPath. | ||
| * Examples: ['Library'] for Unity, ['.godot/imported', '.godot/shader_cache'] for Godot. | ||
| */ | ||
| readonly cacheFolders: string[]; | ||
|
|
||
| /** | ||
| * Shell command for container preStop hook (e.g. license cleanup). | ||
| * Undefined means no preStop hook. | ||
| */ | ||
| readonly preStopCommand?: string; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| export { EnginePlugin } from './engine-plugin'; | ||
| export { UnityPlugin } from './unity-plugin'; | ||
| export { loadEngineFromModule } from './module-engine-loader'; | ||
| export { loadEngineFromCli } from './cli-engine-loader'; | ||
| export { loadEngineFromDocker } from './docker-engine-loader'; | ||
|
|
||
| import { EnginePlugin } from './engine-plugin'; | ||
| import { UnityPlugin } from './unity-plugin'; | ||
| import { loadEngineFromModule } from './module-engine-loader'; | ||
| import { loadEngineFromCli } from './cli-engine-loader'; | ||
| import { loadEngineFromDocker } from './docker-engine-loader'; | ||
| import OrchestratorLogger from '../orchestrator/services/core/orchestrator-logger'; | ||
|
|
||
| /** Current engine plugin — defaults to Unity. */ | ||
| let currentEngine: EnginePlugin = UnityPlugin; | ||
|
|
||
| /** Get the current engine plugin. */ | ||
| export function getEngine(): EnginePlugin { | ||
| return currentEngine; | ||
| } | ||
|
|
||
| /** Replace the engine plugin (e.g. for Godot, Unreal, or testing). */ | ||
| export function setEngine(engine: EnginePlugin): void { | ||
| currentEngine = engine; | ||
| } | ||
|
|
||
| /** | ||
| * Initialize the engine from a plugin source string. | ||
| * | ||
| * Source formats: | ||
| * - `docker:<image>` — load from a Docker image | ||
| * - `cli:<path>` — load from an external CLI executable | ||
| * - `module:<id>` — load from an npm package or local JS/TS file | ||
| * - `<id>` (no prefix) — treated as a module (npm package or local path) | ||
| * | ||
| * When engine is 'unity' (or unset) with no enginePlugin, the built-in | ||
| * UnityPlugin is used — no loading needed. | ||
| */ | ||
| export function initEngine(engine: string, enginePlugin?: string): void { | ||
| if (!enginePlugin) { | ||
| if (engine === 'unity') { | ||
| currentEngine = UnityPlugin; | ||
| return; | ||
| } | ||
| throw new Error( | ||
| `Engine '${engine}' requires an enginePlugin source. ` + | ||
| `Use one of: module:<npm-package>, cli:<executable-path>, docker:<image>`, | ||
| ); | ||
| } | ||
|
|
||
| OrchestratorLogger.log(`Loading engine plugin for '${engine}' from: ${enginePlugin}`); | ||
|
|
||
| let loaded: EnginePlugin; | ||
|
|
||
| if (enginePlugin.startsWith('docker:')) { | ||
| loaded = loadEngineFromDocker(enginePlugin.slice('docker:'.length)); | ||
| } else if (enginePlugin.startsWith('cli:')) { | ||
| loaded = loadEngineFromCli(enginePlugin.slice('cli:'.length)); | ||
| } else if (enginePlugin.startsWith('module:')) { | ||
| loaded = loadEngineFromModule(enginePlugin.slice('module:'.length)); | ||
| } else { | ||
| // No prefix — default to module loader (npm package or local path) | ||
| loaded = loadEngineFromModule(enginePlugin); | ||
| } | ||
|
|
||
| currentEngine = loaded; | ||
| OrchestratorLogger.log(`Engine plugin '${loaded.name}' initialized successfully`); | ||
|
Comment on lines
+39
to
+67
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Validate the loaded plugin before mutating global state.
🤖 Prompt for AI Agents |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| import { EnginePlugin } from './engine-plugin'; | ||
| import OrchestratorLogger from '../orchestrator/services/core/orchestrator-logger'; | ||
|
|
||
| /** | ||
| * Load an EnginePlugin from a TypeScript/JavaScript module. | ||
| * | ||
| * Accepts: | ||
| * - npm package name: `@game-ci/godot-engine` | ||
| * - Local file path: `./my-engine-plugin.js` or `/absolute/path/plugin.js` | ||
| * | ||
|
Comment on lines
+5
to
+10
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: git ls-files src/model/engine/ | head -20Repository: game-ci/orchestrator Length of output: 276 🏁 Script executed: cat -n src/model/engine/module-engine-loader.tsRepository: game-ci/orchestrator Length of output: 1992 🏁 Script executed: # Check if there's a tsconfig or jest config that might enable TS loading
fd -t f "tsconfig\|jest\|loader" . --max-depth 3Repository: game-ci/orchestrator Length of output: 46 🏁 Script executed: # Look for existing require() patterns and how paths are handled elsewhere
rg "require\(" src/model/ -B 2 -A 2 | head -40Repository: game-ci/orchestrator Length of output: 3982 🏁 Script executed: # Check for any path resolution utilities or similar patterns
rg "process\.cwd\|path\.resolve" src/ | head -20Repository: game-ci/orchestrator Length of output: 46 🏁 Script executed: rg "loadEngineFromModule" src/ --type ts --type jsRepository: game-ci/orchestrator Length of output: 636 🏁 Script executed: # Check the test file for this module
fd "module-engine-loader" src/ -t fRepository: game-ci/orchestrator Length of output: 104 🏁 Script executed: # Look for build configuration and runtime context
cat package.json | head -60Repository: game-ci/orchestrator Length of output: 1737 🏁 Script executed: # Check if TypeScript is compiled as part of build
rg "tsc\|esbuild\|webpack" . --type json --type js --type ts | head -20Repository: game-ci/orchestrator Length of output: 46 🏁 Script executed: cat -n src/model/engine/index.tsRepository: game-ci/orchestrator Length of output: 3095 🏁 Script executed: # Check if there are any tests related to engine loading
fd -t f "\.test\|\.spec" src/ | xargs grep -l "loadEngineFromModule\|module-engine-loader" 2>/dev/nullRepository: game-ci/orchestrator Length of output: 48 🏁 Script executed: # Check if there's any dynamic require or loader configuration
cat tsconfig.json | head -40Repository: game-ci/orchestrator Length of output: 631 🏁 Script executed: # Check if there's any documentation about the expected behavior
fd -t f "README\|CONTRIBUTING\|docs" . --max-depth 2 | head -10Repository: game-ci/orchestrator Length of output: 46 🏁 Script executed: # Look for any examples or tests that show how enginePlugin paths are used
rg "module:\|enginePlugin" src/ --type ts -B 2 -A 2 | head -40Repository: game-ci/orchestrator Length of output: 46 🏁 Script executed: # Check if there are any built-in plugins or examples
fd "plugin" src/ --type ts | head -10Repository: game-ci/orchestrator Length of output: 233 🏁 Script executed: # Look for any example plugins or documentation about the API
rg "EnginePlugin\|enginePlugin" src/ -A 3 | head -60Repository: game-ci/orchestrator Length of output: 46 🏁 Script executed: # Check if the documented TypeScript support makes sense given the compilation step
# Look at what actually happens when require() is called
python3 << 'PYTHON_EOF'
# Simulate require() resolution in Node.js CommonJS
# When require('./my-engine-plugin.js') is called from dist/model/engine/module-engine-loader.js
# Node resolves it relative to dist/model/engine/
import os
import json
# The module file path after compilation
module_file = "dist/model/engine/module-engine-loader.js"
module_dir = os.path.dirname(module_file)
# Test path: './my-engine-plugin.js' as documented
test_path = "./my-engine-plugin.js"
# How Node.js resolves this
resolved = os.path.normpath(os.path.join(module_dir, test_path))
print(f"Module directory: {module_dir}")
print(f"Documented path: {test_path}")
print(f"Node.js would resolve to: {resolved}")
print(f"Expected by user (cwd-based): ./my-engine-plugin.js")
print(f"Mismatch: {resolved != './my-engine-plugin.js'}")
# For absolute paths, require() works fine
abs_path = "/absolute/path/plugin.js"
print(f"\nAbsolute path {abs_path} resolves to itself: ✓")
# For npm packages
npm_pkg = "@game-ci/godot-engine"
print(f"npm package {npm_pkg} resolves via node_modules: ✓")
PYTHON_EOFRepository: game-ci/orchestrator Length of output: 394 🏁 Script executed: # Check if .ts files are mentioned in any actual implementation or tests
rg "\.ts" src/model/engine/ --type tsRepository: game-ci/orchestrator Length of output: 46 Relative file paths and TypeScript support are not actually implemented despite being documented. The docstring (lines 5-10) advertises support for local file paths like
Absolute paths and npm packages work correctly via 🛠️ Fix for relative file path resolution+import path from 'node:path';
import { EnginePlugin } from './engine-plugin';
import OrchestratorLogger from '../orchestrator/services/core/orchestrator-logger';
export function loadEngineFromModule(moduleId: string): EnginePlugin {
let loaded: any;
try {
- loaded = require(moduleId);
+ const request =
+ moduleId.startsWith('.') || path.isAbsolute(moduleId)
+ ? path.resolve(process.cwd(), moduleId)
+ : moduleId;
+ loaded = require(request);
} catch (error: any) {
throw new Error(`Failed to load engine plugin module '${moduleId}': ${error.message}`);
}🤖 Prompt for AI Agents |
||
| * The module must export an EnginePlugin-compatible object as default or named `plugin`: | ||
| * | ||
| * // ES module | ||
| * export default { name: 'godot', cacheFolders: ['.godot/imported'] }; | ||
| * | ||
| * // CommonJS | ||
| * module.exports = { name: 'godot', cacheFolders: ['.godot/imported'] }; | ||
| */ | ||
| export function loadEngineFromModule(moduleId: string): EnginePlugin { | ||
| let loaded: any; | ||
| try { | ||
| loaded = require(moduleId); | ||
| } catch (error: any) { | ||
| throw new Error(`Failed to load engine plugin module '${moduleId}': ${error.message}`); | ||
| } | ||
|
|
||
| // Support default export, named `plugin` export, or the module itself | ||
| const config = loaded.default || loaded.plugin || loaded; | ||
|
|
||
| if (!config || typeof config !== 'object') { | ||
| throw new Error(`Engine plugin module '${moduleId}' did not export a valid object`); | ||
| } | ||
|
|
||
| if (!config.name || !Array.isArray(config.cacheFolders)) { | ||
| throw new Error( | ||
| `Engine plugin from module '${moduleId}' missing required fields (name, cacheFolders). Got: ${JSON.stringify(config)}`, | ||
| ); | ||
| } | ||
|
|
||
| OrchestratorLogger.log(`Loaded engine plugin '${config.name}' from module: ${moduleId}`); | ||
|
|
||
| return { | ||
| name: config.name, | ||
| cacheFolders: config.cacheFolders, | ||
| preStopCommand: config.preStopCommand || undefined, | ||
| }; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| import { EnginePlugin } from './engine-plugin'; | ||
|
|
||
| /** | ||
| * Unity engine plugin — built-in. | ||
| * | ||
| * This is a plugin like any other, it just ships with the orchestrator. | ||
| * unity-builder owns everything else (image resolution, versioning, build | ||
| * scripts, licensing activation flow). This plugin only provides the | ||
| * minimal config the orchestrator needs to handle caching and container | ||
| * lifecycle generically. | ||
| */ | ||
| export const UnityPlugin: EnginePlugin = { | ||
| name: 'unity', | ||
| cacheFolders: ['Library'], | ||
| preStopCommand: | ||
| 'cd /data/builder/action/steps && chmod +x /steps/return_license.sh 2>/dev/null || true; /steps/return_license.sh 2>/dev/null || true', | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Protocol mismatch: loader sends both stdin JSON and CLI args.
The contract says plugin receives
{"command":"get-engine-config"}on stdin, but Line 18 also forces['get-engine-config']argv. Plugins that validate argv strictly can fail despite correct stdin handling.Proposed fix
📝 Committable suggestion
🤖 Prompt for AI Agents