@vgai/editor 0.4.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{axe-BJjvBeOZ.js → axe-N4cNyzfs.js} +1 -1
- package/dist/assets/{browserAll-Dn0pgD87.js → browserAll-BIBT0Z2H.js} +1 -1
- package/dist/assets/{create-runtime-BF9OD-U8.js → create-runtime-DxLhdk4U.js} +1 -1
- package/dist/assets/{game-u6c2Patu.js → game-4NbNRROm.js} +1 -1
- package/dist/assets/{game-umrHIqFB.js → game-B3UH5LjS.js} +1 -1
- package/dist/assets/{game-BR0GBNgF.js → game-ByxRLJi1.js} +1 -1
- package/dist/assets/{game-DeTGdgYt.js → game-CJ5BqOYG.js} +1 -1
- package/dist/assets/{game-DQxhCYsZ.js → game-CSJm1kwB.js} +1 -1
- package/dist/assets/{game-CLNm2g0y.js → game-D9Mo4L3Q.js} +1 -1
- package/dist/assets/{game-B346oj_f.js → game-DJmU9BQF.js} +1 -1
- package/dist/assets/{game-BCNN63uV.js → game-ZqGLgxRj.js} +1 -1
- package/dist/assets/{game-loop-t5W_oqxz.js → game-loop-9YP76JB1.js} +1 -1
- package/dist/assets/{game-source-DzE9taML.js → game-source-DLKG9pmj.js} +1 -1
- package/dist/assets/{index-DbQzGLxv.js → index-8A4JLNIp.js} +902 -892
- package/dist/assets/{index-CgcH1RpF.js → index-Brzr17q0.js} +1 -1
- package/dist/assets/{index-D3tcNB2T.js → index-CnKtvrru.js} +1 -1
- package/dist/assets/{index-Fc8Ezr20.js → index-D-WR11Jw.js} +1 -1
- package/dist/assets/index-DiUacMXR.css +1 -0
- package/dist/assets/{ingest-siblings-CW_xbvrF.js → ingest-siblings-Cmx39OBK.js} +1 -1
- package/dist/assets/{mount-manifest-DgUscyaD.js → mount-manifest-kzdZgkkc.js} +1 -1
- package/dist/assets/{react-18-C4tVCii_.js → react-18-DvWsdwa_.js} +1 -1
- package/dist/assets/webworkerAll-C9GR-KrR.js +1 -0
- package/dist/assets/{world-documents-BSaA5YIF.js → world-documents-DM_aIMAW.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/packaged.mjs +27125 -1075
- package/package.json +2 -2
- package/server/asset-history-snapshots.ts +189 -0
- package/server/asset-library-routes.ts +404 -53
- package/server/catalog-import-recipe.ts +97 -0
- package/server/catalog-reimport.ts +141 -0
- package/server/cloud-asset-catalog.ts +71 -5
- package/server/dev.ts +1 -1
- package/server/editor-server.ts +106 -51
- package/server/editor-sse.ts +5 -0
- package/server/generative-execution-context.ts +45 -0
- package/server/model-import-conversion.ts +345 -0
- package/server/packaged.ts +1 -1
- package/server/project-hmr-files.ts +3 -5
- package/server/project-output-writer.ts +440 -36
- package/server/project-tools.ts +529 -0
- package/server/server-utils.ts +14 -11
- package/src/EditorContext.tsx +4 -4
- package/src/action-registry.ts +12 -0
- package/src/asset-editor-context.tsx +9 -0
- package/src/asset-events.ts +3 -15
- package/src/asset-selection.ts +3 -0
- package/src/asset-workflow/asset-capabilities.ts +40 -0
- package/src/asset-workflow/asset-import-jobs.ts +108 -0
- package/src/asset-workflow/asset-library-query.ts +8 -1
- package/src/asset-workflow/asset-workflow-quality.ts +2 -0
- package/src/asset-workflow/import-contract.ts +2 -0
- package/src/asset-workflow/model-inspection.ts +92 -1
- package/src/asset-workflow/preview-resource-lifetime.ts +44 -0
- package/src/asset-workflow/project-asset-commands.ts +1 -0
- package/src/asset-workflow/project-asset-health.ts +3 -6
- package/src/asset-workflow/project-asset-operations.ts +119 -4
- package/src/command-listener.ts +37 -9
- package/src/components/ApplicationMenus.tsx +13 -15
- package/src/components/AssetBrowser.tsx +301 -26
- package/src/components/AssetImportDetails.tsx +207 -92
- package/src/components/AssetImportJobStatus.tsx +55 -0
- package/src/components/CenterDocuments.tsx +28 -1
- package/src/components/CommandPalette.tsx +2 -2
- package/src/components/DefaultEditorLayout.tsx +4 -0
- package/src/components/InspectorToolSection.tsx +12 -10
- package/src/components/OnlineAssetBrowser.tsx +197 -11
- package/src/components/ToolHost.tsx +8 -8
- package/src/components/ViewportOverlay.tsx +13 -26
- package/src/components/ViewportPanel.tsx +19 -14
- package/src/components/ViewportShadingMenu.tsx +103 -0
- package/src/components/WorkspaceDock.tsx +103 -17
- package/src/components/asset-documents.tsx +38 -11
- package/src/components/asset-editor-persistence.ts +300 -5
- package/src/components/asset-selection-section.tsx +83 -9
- package/src/components/asset-viewers/AudioViewer.tsx +4 -7
- package/src/components/asset-viewers/ModelViewer.tsx +168 -5
- package/src/components/asset-viewers/OnlineAssetDetail.tsx +52 -12
- package/src/components/asset-viewers/SourceAssetViewer.tsx +7 -26
- package/src/components/asset-workflow.css +30 -3
- package/src/components/primitives/editor-icons.ts +1 -0
- package/src/components/project-tool-documents.tsx +298 -0
- package/src/components/status-contributions.tsx +38 -0
- package/src/components/tool-documents.tsx +20 -17
- package/src/components/tool-schema-form.tsx +258 -0
- package/src/components/workspace-dock.css +16 -0
- package/src/editor-api.ts +159 -22
- package/src/editor-store.ts +6 -5
- package/src/editor-viewport.ts +12 -7
- package/src/game-viewport-state.ts +28 -0
- package/src/model-thumbnail.ts +97 -9
- package/src/play-mode.ts +30 -3
- package/src/project-tool-discovery.ts +5 -5
- package/src/{project-operations.ts → project-tools.ts} +39 -26
- package/src/scene-sync.ts +0 -67
- package/src/storage/handle-storage.ts +5 -1
- package/src/storage/http-storage.ts +7 -2
- package/src/storage/mem-storage.ts +7 -1
- package/src/storage/types.ts +2 -0
- package/src/theme-css.ts +17 -7
- package/src/tool-loader.ts +152 -241
- package/src/viewport-shading-boundary.ts +15 -0
- package/src/widgets/index.ts +2 -1
- package/src/workspace-dock-controller.ts +8 -0
- package/src/workspace-document-registry.ts +4 -4
- package/src/workspace-state-persistence.ts +36 -10
- package/template/.claude/skills/editor/SKILL.md +13 -3
- package/template/AGENTS.md +100 -83
- package/template/package.json +16 -0
- package/template/public/scenes/default.vscn.json +1 -2
- package/template/scripts/playtest.ts +7 -37
- package/template/server/rooms/game-room.ts +2 -2
- package/template/src/data/README.md +3 -2
- package/template/src/data/tuning.data.json +1 -3
- package/template/src/data/tuning.schema.json +0 -12
- package/template/src/data/tuning.schema.ts +2 -9
- package/template/src/data/tuning.ts +1 -1
- package/template/src/scripts/registry.ts +0 -3
- package/template/src/tools/README.md +151 -16
- package/template/src/tools/autoplay.tool.ts +31 -0
- package/template/src/tools/autoplay.ts +15 -0
- package/template/src/tools/tuning.document.tsx +107 -0
- package/template/src/tools/tuning.tool.ts +24 -0
- package/template/src/tools/tuning.ts +9 -0
- package/template/src/ui/Button.stories.tsx +1 -1
- package/template/tests/acceptance/example.spec.ts +4 -59
- package/template/vgai.game.json +1 -1
- package/dist/assets/index-bAGhEnnO.css +0 -1
- package/dist/assets/webworkerAll-BA4aq7xq.js +0 -1
- package/dist/project-scripts.bundle.js +0 -3917
- package/server/project-operations.ts +0 -343
- package/src/components/project-operation-documents.tsx +0 -226
- package/template/src/operations/README.md +0 -88
- package/template/src/scripts/components/data-spinner.ts +0 -86
- package/template/src/scripts/components/spin-lap-math.ts +0 -27
- package/template/src/tools/example.tool.tsx +0 -97
- package/template/src/tools/spin.tool.tsx +0 -101
- package/template/tests/logic/example.test.ts +0 -31
|
@@ -1,343 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Project-operation discovery and execution.
|
|
3
|
-
*
|
|
4
|
-
* Project modules stay ordinary TS/JS. This server-side boundary is the only
|
|
5
|
-
* automatic loader: it recursively scans src/operations for operation modules, asks the
|
|
6
|
-
* owning Vite server to load each module in Node, validates the exported
|
|
7
|
-
* OperationDefinition, and dispatches through the existing SDK registry.
|
|
8
|
-
* Nothing here imports an operation into the editor browser or a game build.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import type { Dirent } from 'node:fs';
|
|
12
|
-
import { readdir, readFile } from 'node:fs/promises';
|
|
13
|
-
import { createRequire } from 'node:module';
|
|
14
|
-
import { dirname, relative, resolve, sep } from 'node:path';
|
|
15
|
-
import { z } from 'zod';
|
|
16
|
-
import type { OperationDefinition, OperationOutcome } from '../../vgai-sdk/src/registry';
|
|
17
|
-
import { OperationRegistry } from '../../vgai-sdk/src/registry';
|
|
18
|
-
import { createProjectOutputWriter } from './project-output-writer';
|
|
19
|
-
|
|
20
|
-
export type ProjectModuleLoader = (absoluteModuleId: string) => Promise<unknown>;
|
|
21
|
-
|
|
22
|
-
export interface ProjectOperationErrorSummary {
|
|
23
|
-
code: string;
|
|
24
|
-
summary: string;
|
|
25
|
-
dataSchema?: unknown;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export interface ProjectOperationCatalogEntry {
|
|
29
|
-
name: string;
|
|
30
|
-
summary: string;
|
|
31
|
-
description: string;
|
|
32
|
-
sourcePath: string;
|
|
33
|
-
inputSchema: unknown;
|
|
34
|
-
resultSchema: unknown;
|
|
35
|
-
errors: ProjectOperationErrorSummary[];
|
|
36
|
-
requires: Record<string, boolean | undefined>;
|
|
37
|
-
host: 'node' | 'editor-browser' | 'runtime-page';
|
|
38
|
-
mutates: boolean;
|
|
39
|
-
supportsDryRun: boolean;
|
|
40
|
-
longRunning: boolean;
|
|
41
|
-
permission: { risk: 'read' | 'write' | 'destructive'; summary: string };
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export interface ProjectOperationLoadError {
|
|
45
|
-
sourcePath: string;
|
|
46
|
-
message: string;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export interface ProjectOperationCatalog {
|
|
50
|
-
operations: ProjectOperationCatalogEntry[];
|
|
51
|
-
loadErrors: ProjectOperationLoadError[];
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
interface LoadedCatalog extends ProjectOperationCatalog {
|
|
55
|
-
definitions: Map<string, OperationDefinition>;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function projectPath(projectRoot: string, absolutePath: string): string {
|
|
59
|
-
return relative(projectRoot, absolutePath).split(sep).join('/');
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
async function findOperationFiles(dir: string): Promise<string[]> {
|
|
63
|
-
const found: string[] = [];
|
|
64
|
-
let entries: Dirent[];
|
|
65
|
-
try {
|
|
66
|
-
entries = await readdir(dir, { withFileTypes: true });
|
|
67
|
-
} catch {
|
|
68
|
-
return found;
|
|
69
|
-
}
|
|
70
|
-
for (const entry of entries) {
|
|
71
|
-
if (entry.name.startsWith('.')) continue;
|
|
72
|
-
const absolute = resolve(dir, entry.name);
|
|
73
|
-
if (entry.isDirectory()) {
|
|
74
|
-
found.push(...(await findOperationFiles(absolute)));
|
|
75
|
-
} else if (entry.name.endsWith('.operation.ts') || entry.name.endsWith('.operation.js')) {
|
|
76
|
-
found.push(absolute);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
return found;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
interface OperationFile {
|
|
83
|
-
absolutePath: string;
|
|
84
|
-
sourcePath: string;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
interface PackageJsonShape {
|
|
88
|
-
name?: string;
|
|
89
|
-
dependencies?: Record<string, string>;
|
|
90
|
-
devDependencies?: Record<string, string>;
|
|
91
|
-
vgai?: { operations?: string[] };
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
async function readPackageJson(path: string): Promise<PackageJsonShape | null> {
|
|
95
|
-
try {
|
|
96
|
-
return JSON.parse(await readFile(path, 'utf8')) as PackageJsonShape;
|
|
97
|
-
} catch {
|
|
98
|
-
return null;
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function resolveDependencyPackageJson(
|
|
103
|
-
requireFromProject: ReturnType<typeof createRequire>,
|
|
104
|
-
dependencyName: string,
|
|
105
|
-
): string | null {
|
|
106
|
-
try {
|
|
107
|
-
return requireFromProject.resolve(`${dependencyName}/package.json`);
|
|
108
|
-
} catch {
|
|
109
|
-
return null;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
async function advertisedDependencyOperations(
|
|
114
|
-
requireFromProject: ReturnType<typeof createRequire>,
|
|
115
|
-
dependencyName: string,
|
|
116
|
-
): Promise<OperationFile[]> {
|
|
117
|
-
const packageJsonPath = resolveDependencyPackageJson(requireFromProject, dependencyName);
|
|
118
|
-
if (!packageJsonPath) return [];
|
|
119
|
-
const dependencyPackage = await readPackageJson(packageJsonPath);
|
|
120
|
-
const specs = dependencyPackage?.vgai?.operations;
|
|
121
|
-
if (!Array.isArray(specs)) return [];
|
|
122
|
-
const packageRoot = dirname(packageJsonPath);
|
|
123
|
-
return specs.flatMap((spec): OperationFile[] => {
|
|
124
|
-
if (typeof spec !== 'string' || !spec.startsWith('./')) return [];
|
|
125
|
-
const absolutePath = resolve(packageRoot, spec);
|
|
126
|
-
if (!absolutePath.startsWith(`${packageRoot}${sep}`)) return [];
|
|
127
|
-
return [{ absolutePath, sourcePath: `${dependencyName}:${spec.slice(2)}` }];
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
async function dependencyOperationFiles(projectRoot: string): Promise<OperationFile[]> {
|
|
132
|
-
const projectPackage = await readPackageJson(resolve(projectRoot, 'package.json'));
|
|
133
|
-
if (!projectPackage) return [];
|
|
134
|
-
const dependencyNames = [
|
|
135
|
-
...Object.keys(projectPackage.dependencies ?? {}),
|
|
136
|
-
...Object.keys(projectPackage.devDependencies ?? {}),
|
|
137
|
-
].sort();
|
|
138
|
-
const requireFromProject = createRequire(resolve(projectRoot, 'package.json'));
|
|
139
|
-
return (
|
|
140
|
-
await Promise.all(
|
|
141
|
-
dependencyNames.map((name) => advertisedDependencyOperations(requireFromProject, name)),
|
|
142
|
-
)
|
|
143
|
-
).flat();
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function operationShape(value: unknown): value is OperationDefinition {
|
|
147
|
-
if (value === null || typeof value !== 'object') return false;
|
|
148
|
-
const candidate = value as Partial<OperationDefinition>;
|
|
149
|
-
return (
|
|
150
|
-
typeof candidate.name === 'string' &&
|
|
151
|
-
typeof candidate.summary === 'string' &&
|
|
152
|
-
typeof candidate.description === 'string' &&
|
|
153
|
-
candidate.input !== null &&
|
|
154
|
-
typeof candidate.input === 'object' &&
|
|
155
|
-
typeof candidate.input.safeParse === 'function' &&
|
|
156
|
-
candidate.result !== null &&
|
|
157
|
-
typeof candidate.result === 'object' &&
|
|
158
|
-
typeof candidate.result.safeParse === 'function' &&
|
|
159
|
-
Array.isArray(candidate.errors) &&
|
|
160
|
-
candidate.requires !== null &&
|
|
161
|
-
typeof candidate.requires === 'object' &&
|
|
162
|
-
(candidate.host === 'node' ||
|
|
163
|
-
candidate.host === 'editor-browser' ||
|
|
164
|
-
candidate.host === 'runtime-page') &&
|
|
165
|
-
typeof candidate.mutates === 'boolean' &&
|
|
166
|
-
typeof candidate.supportsDryRun === 'boolean' &&
|
|
167
|
-
candidate.permission !== null &&
|
|
168
|
-
typeof candidate.permission === 'object' &&
|
|
169
|
-
typeof candidate.impl === 'function'
|
|
170
|
-
);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
function jsonSchema(schema: z.ZodType): unknown {
|
|
174
|
-
return z.toJSONSchema(schema);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function toCatalogEntry(
|
|
178
|
-
definition: OperationDefinition,
|
|
179
|
-
sourcePath: string,
|
|
180
|
-
): ProjectOperationCatalogEntry {
|
|
181
|
-
return {
|
|
182
|
-
name: definition.name,
|
|
183
|
-
summary: definition.summary,
|
|
184
|
-
description: definition.description,
|
|
185
|
-
sourcePath,
|
|
186
|
-
inputSchema: jsonSchema(definition.input),
|
|
187
|
-
resultSchema: jsonSchema(definition.result),
|
|
188
|
-
errors: definition.errors.map((error) => ({
|
|
189
|
-
code: error.code,
|
|
190
|
-
summary: error.summary,
|
|
191
|
-
...(error.data ? { dataSchema: jsonSchema(error.data) } : {}),
|
|
192
|
-
})),
|
|
193
|
-
requires: { ...definition.requires },
|
|
194
|
-
host: definition.host,
|
|
195
|
-
mutates: definition.mutates,
|
|
196
|
-
supportsDryRun: definition.supportsDryRun,
|
|
197
|
-
longRunning: definition.longRunning === true,
|
|
198
|
-
permission: { ...definition.permission },
|
|
199
|
-
};
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
async function loadCatalog(
|
|
203
|
-
projectRoot: string,
|
|
204
|
-
loadModule: ProjectModuleLoader | undefined,
|
|
205
|
-
): Promise<LoadedCatalog> {
|
|
206
|
-
const definitions = new Map<string, OperationDefinition>();
|
|
207
|
-
const operations: ProjectOperationCatalogEntry[] = [];
|
|
208
|
-
const loadErrors: ProjectOperationLoadError[] = [];
|
|
209
|
-
const projectFiles = (await findOperationFiles(resolve(projectRoot, 'src', 'operations')))
|
|
210
|
-
.sort()
|
|
211
|
-
.map((absolutePath) => ({ absolutePath, sourcePath: projectPath(projectRoot, absolutePath) }));
|
|
212
|
-
const files = [...projectFiles, ...(await dependencyOperationFiles(projectRoot))].sort((a, b) =>
|
|
213
|
-
a.sourcePath.localeCompare(b.sourcePath),
|
|
214
|
-
);
|
|
215
|
-
|
|
216
|
-
if (files.length > 0 && !loadModule) {
|
|
217
|
-
loadErrors.push({
|
|
218
|
-
sourcePath: 'src/operations',
|
|
219
|
-
message:
|
|
220
|
-
'This editor host cannot load project TypeScript modules. Use the Vite-backed dev or packaged editor.',
|
|
221
|
-
});
|
|
222
|
-
return { operations, loadErrors, definitions };
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
const registry = new OperationRegistry();
|
|
226
|
-
for (const { absolutePath, sourcePath } of files) {
|
|
227
|
-
try {
|
|
228
|
-
const imported = (await loadModule?.(absolutePath)) as Record<string, unknown> | undefined;
|
|
229
|
-
const definition = imported?.['operation'];
|
|
230
|
-
if (!operationShape(definition)) {
|
|
231
|
-
throw new Error(
|
|
232
|
-
'does not export a complete `operation` definition; export `operation = defineOperation({...})` from @vgai/sdk/registry',
|
|
233
|
-
);
|
|
234
|
-
}
|
|
235
|
-
if (!definition.name.startsWith('project.')) {
|
|
236
|
-
throw new Error(
|
|
237
|
-
`declares ${JSON.stringify(definition.name)}; project-local operation names must start with "project."`,
|
|
238
|
-
);
|
|
239
|
-
}
|
|
240
|
-
if (definition.mutates && definition.permission.risk === 'read') {
|
|
241
|
-
throw new Error(
|
|
242
|
-
`declares mutates:true with read risk; mutating operations must declare write or destructive risk`,
|
|
243
|
-
);
|
|
244
|
-
}
|
|
245
|
-
registry.register(definition);
|
|
246
|
-
const entry = toCatalogEntry(definition, sourcePath);
|
|
247
|
-
definitions.set(definition.name, definition);
|
|
248
|
-
operations.push(entry);
|
|
249
|
-
} catch (error) {
|
|
250
|
-
loadErrors.push({
|
|
251
|
-
sourcePath,
|
|
252
|
-
message: error instanceof Error ? error.message : String(error),
|
|
253
|
-
});
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
operations.sort((a, b) => a.name.localeCompare(b.name));
|
|
258
|
-
return { operations, loadErrors, definitions };
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
export async function discoverProjectOperations(
|
|
262
|
-
projectRoot: string,
|
|
263
|
-
loadModule: ProjectModuleLoader | undefined,
|
|
264
|
-
): Promise<ProjectOperationCatalog> {
|
|
265
|
-
const { definitions: _definitions, ...catalog } = await loadCatalog(projectRoot, loadModule);
|
|
266
|
-
return catalog;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
export type ProjectOperationExecution =
|
|
270
|
-
| { status: 200; body: OperationOutcome; changedProject: boolean }
|
|
271
|
-
| {
|
|
272
|
-
status: 404 | 409;
|
|
273
|
-
body: { ok: false; error: { code: string; message: string } };
|
|
274
|
-
changedProject: false;
|
|
275
|
-
};
|
|
276
|
-
|
|
277
|
-
export async function executeProjectOperation(options: {
|
|
278
|
-
projectRoot: string;
|
|
279
|
-
loadModule: ProjectModuleLoader | undefined;
|
|
280
|
-
name: string;
|
|
281
|
-
input: unknown;
|
|
282
|
-
confirmed: boolean;
|
|
283
|
-
signal?: AbortSignal;
|
|
284
|
-
}): Promise<ProjectOperationExecution> {
|
|
285
|
-
const loaded = await loadCatalog(options.projectRoot, options.loadModule);
|
|
286
|
-
const definition = loaded.definitions.get(options.name);
|
|
287
|
-
if (!definition) {
|
|
288
|
-
return {
|
|
289
|
-
status: 404,
|
|
290
|
-
changedProject: false,
|
|
291
|
-
body: {
|
|
292
|
-
ok: false,
|
|
293
|
-
error: {
|
|
294
|
-
code: 'PROJECT_OPERATION_NOT_FOUND',
|
|
295
|
-
message: `No loaded project operation is named ${JSON.stringify(options.name)}.`,
|
|
296
|
-
},
|
|
297
|
-
},
|
|
298
|
-
};
|
|
299
|
-
}
|
|
300
|
-
if (definition.host !== 'node') {
|
|
301
|
-
return {
|
|
302
|
-
status: 409,
|
|
303
|
-
changedProject: false,
|
|
304
|
-
body: {
|
|
305
|
-
ok: false,
|
|
306
|
-
error: {
|
|
307
|
-
code: 'PROJECT_OPERATION_HOST_UNAVAILABLE',
|
|
308
|
-
message:
|
|
309
|
-
`Project operation ${JSON.stringify(options.name)} declares host ${JSON.stringify(definition.host)}. ` +
|
|
310
|
-
'Slice 1 executes Node-hosted project operations; call reusable browser/runtime functions directly.',
|
|
311
|
-
},
|
|
312
|
-
},
|
|
313
|
-
};
|
|
314
|
-
}
|
|
315
|
-
if ((definition.mutates || definition.permission.risk !== 'read') && !options.confirmed) {
|
|
316
|
-
return {
|
|
317
|
-
status: 409,
|
|
318
|
-
changedProject: false,
|
|
319
|
-
body: {
|
|
320
|
-
ok: false,
|
|
321
|
-
error: {
|
|
322
|
-
code: 'PROJECT_OPERATION_CONFIRMATION_REQUIRED',
|
|
323
|
-
message:
|
|
324
|
-
`${definition.permission.risk} operation ${JSON.stringify(options.name)} requires explicit confirmation: ` +
|
|
325
|
-
definition.permission.summary,
|
|
326
|
-
},
|
|
327
|
-
},
|
|
328
|
-
};
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
const registry = new OperationRegistry();
|
|
332
|
-
registry.register(definition);
|
|
333
|
-
const outcome = await registry.dispatch(options.name, options.input, {
|
|
334
|
-
projectRoot: options.projectRoot,
|
|
335
|
-
projectOutputs: createProjectOutputWriter(options.projectRoot),
|
|
336
|
-
...(options.signal ? { signal: options.signal } : {}),
|
|
337
|
-
});
|
|
338
|
-
return {
|
|
339
|
-
status: 200,
|
|
340
|
-
body: outcome,
|
|
341
|
-
changedProject: definition.mutates && outcome.ok,
|
|
342
|
-
};
|
|
343
|
-
}
|
|
@@ -1,226 +0,0 @@
|
|
|
1
|
-
import { useMemo, useState, useSyncExternalStore } from 'react';
|
|
2
|
-
import type { EditorAction } from '../action-registry';
|
|
3
|
-
import { Button, bg, border, danger, fontSize, radius, text } from '../design-system';
|
|
4
|
-
import {
|
|
5
|
-
getProjectOperations,
|
|
6
|
-
projectOperationsVersion,
|
|
7
|
-
runProjectOperation,
|
|
8
|
-
subscribeProjectOperations,
|
|
9
|
-
} from '../project-operations';
|
|
10
|
-
import {
|
|
11
|
-
openWorkspaceDocument,
|
|
12
|
-
type WorkspaceDocumentContentProps,
|
|
13
|
-
} from '../workspace-document-registry';
|
|
14
|
-
|
|
15
|
-
const CATALOG_DOCUMENT_ID = 'project-operations';
|
|
16
|
-
const OPERATION_DOCUMENT_PREFIX = 'project-operation:';
|
|
17
|
-
|
|
18
|
-
export interface ProjectOperationDocumentStore {
|
|
19
|
-
setActiveViewportTab(tab: 'scene' | 'game'): void;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function operationDocumentId(name: string): string {
|
|
23
|
-
return `${OPERATION_DOCUMENT_PREFIX}${name}`;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function openProjectOperationsDocument(store: ProjectOperationDocumentStore): void {
|
|
27
|
-
openWorkspaceDocument({
|
|
28
|
-
id: CATALOG_DOCUMENT_ID,
|
|
29
|
-
title: 'Project Commands',
|
|
30
|
-
kind: 'operation',
|
|
31
|
-
Content: ProjectOperationDocumentContent,
|
|
32
|
-
closeable: true,
|
|
33
|
-
onActivate: () => store.setActiveViewportTab('scene'),
|
|
34
|
-
});
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function openProjectOperationDocument(
|
|
38
|
-
store: ProjectOperationDocumentStore | null,
|
|
39
|
-
name: string,
|
|
40
|
-
): boolean {
|
|
41
|
-
const operation = getProjectOperations().operations.find((entry) => entry.name === name);
|
|
42
|
-
if (!operation) return false;
|
|
43
|
-
openWorkspaceDocument({
|
|
44
|
-
id: operationDocumentId(name),
|
|
45
|
-
title: operation.summary || operation.name,
|
|
46
|
-
kind: 'operation',
|
|
47
|
-
provenance: { sourcePath: operation.sourcePath, origin: operation.name },
|
|
48
|
-
Content: ProjectOperationDocumentContent,
|
|
49
|
-
closeable: true,
|
|
50
|
-
...(store ? { onActivate: () => store.setActiveViewportTab('scene') } : {}),
|
|
51
|
-
});
|
|
52
|
-
return true;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export function buildProjectOperationActions(store: ProjectOperationDocumentStore): EditorAction[] {
|
|
56
|
-
return [
|
|
57
|
-
{
|
|
58
|
-
id: 'project-operations:open',
|
|
59
|
-
label: 'Open Project Commands',
|
|
60
|
-
category: 'action',
|
|
61
|
-
execute: () => openProjectOperationsDocument(store),
|
|
62
|
-
},
|
|
63
|
-
...getProjectOperations().operations.map((operation) => ({
|
|
64
|
-
id: `project-operation:${operation.name}`,
|
|
65
|
-
label: `Run: ${operation.summary || operation.name}`,
|
|
66
|
-
category: 'action' as const,
|
|
67
|
-
execute: () => void openProjectOperationDocument(store, operation.name),
|
|
68
|
-
})),
|
|
69
|
-
];
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function JsonBlock({ value }: { value: unknown }) {
|
|
73
|
-
return <pre style={jsonStyle}>{JSON.stringify(value, null, 2)}</pre>;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export function ProjectOperationDocumentContent({ documentId }: WorkspaceDocumentContentProps) {
|
|
77
|
-
useSyncExternalStore(subscribeProjectOperations, projectOperationsVersion);
|
|
78
|
-
const snapshot = getProjectOperations();
|
|
79
|
-
const operationName = documentId.startsWith(OPERATION_DOCUMENT_PREFIX)
|
|
80
|
-
? documentId.slice(OPERATION_DOCUMENT_PREFIX.length)
|
|
81
|
-
: null;
|
|
82
|
-
const operation = operationName
|
|
83
|
-
? snapshot.operations.find((entry) => entry.name === operationName)
|
|
84
|
-
: null;
|
|
85
|
-
const [inputText, setInputText] = useState('{}');
|
|
86
|
-
const [result, setResult] = useState<unknown>(null);
|
|
87
|
-
const [running, setRunning] = useState(false);
|
|
88
|
-
const [inputError, setInputError] = useState<string | null>(null);
|
|
89
|
-
const parsedSchema = useMemo(() => operation?.inputSchema ?? {}, [operation]);
|
|
90
|
-
|
|
91
|
-
if (!operationName) {
|
|
92
|
-
return (
|
|
93
|
-
<div style={rootStyle} data-testid="project-operations-catalog">
|
|
94
|
-
<h2 style={headingStyle}>Project Commands</h2>
|
|
95
|
-
<p style={mutedStyle}>
|
|
96
|
-
Auto-discovered from nested <code>src/operations/*.operation.ts</code> files. Commands are
|
|
97
|
-
ordinary project functions with a validated tooling boundary; custom interfaces live in{' '}
|
|
98
|
-
<code>src/tools/</code>.
|
|
99
|
-
</p>
|
|
100
|
-
{snapshot.operations.map((entry) => (
|
|
101
|
-
<Button
|
|
102
|
-
key={entry.name}
|
|
103
|
-
type="button"
|
|
104
|
-
className="vgai-operation-card"
|
|
105
|
-
onClick={() => openProjectOperationDocument(null, entry.name)}
|
|
106
|
-
data-testid={`project-command-card:${entry.name}`}
|
|
107
|
-
>
|
|
108
|
-
<div style={{ color: text[1] }}>{entry.summary}</div>
|
|
109
|
-
<code style={mutedStyle}>{entry.name}</code>
|
|
110
|
-
<div style={mutedStyle}>
|
|
111
|
-
{entry.host} · {entry.permission.risk} · {entry.sourcePath}
|
|
112
|
-
</div>
|
|
113
|
-
</Button>
|
|
114
|
-
))}
|
|
115
|
-
{snapshot.operations.length === 0 && snapshot.loadErrors.length === 0 && (
|
|
116
|
-
<div style={mutedStyle}>No project commands found.</div>
|
|
117
|
-
)}
|
|
118
|
-
{snapshot.loadErrors.map((error) => (
|
|
119
|
-
<div key={`${error.sourcePath}:${error.message}`} style={errorStyle}>
|
|
120
|
-
{error.sourcePath}: {error.message}
|
|
121
|
-
</div>
|
|
122
|
-
))}
|
|
123
|
-
</div>
|
|
124
|
-
);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
if (!operation) {
|
|
128
|
-
return <div style={rootStyle}>This operation is no longer available.</div>;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
const runnable = operation.host === 'node';
|
|
132
|
-
const execute = async () => {
|
|
133
|
-
let input: unknown;
|
|
134
|
-
try {
|
|
135
|
-
input = JSON.parse(inputText);
|
|
136
|
-
setInputError(null);
|
|
137
|
-
} catch (error) {
|
|
138
|
-
setInputError(error instanceof Error ? error.message : String(error));
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
const confirm =
|
|
142
|
-
operation.permission.risk === 'read' ||
|
|
143
|
-
window.confirm(`${operation.permission.summary}\n\nRun ${operation.name}?`);
|
|
144
|
-
if (!confirm) return;
|
|
145
|
-
setRunning(true);
|
|
146
|
-
try {
|
|
147
|
-
setResult(await runProjectOperation(operation.name, input, { confirm: true }));
|
|
148
|
-
} catch (error) {
|
|
149
|
-
setResult({ ok: false, error: { code: 'REQUEST_FAILED', message: String(error) } });
|
|
150
|
-
} finally {
|
|
151
|
-
setRunning(false);
|
|
152
|
-
}
|
|
153
|
-
};
|
|
154
|
-
|
|
155
|
-
return (
|
|
156
|
-
<div style={rootStyle} data-testid={`project-operation:${operation.name}`}>
|
|
157
|
-
<h2 style={headingStyle}>{operation.summary}</h2>
|
|
158
|
-
<code style={{ color: text[2] }}>{operation.name}</code>
|
|
159
|
-
<p style={{ color: text[2] }}>{operation.description}</p>
|
|
160
|
-
<div style={mutedStyle}>
|
|
161
|
-
{operation.host} · {operation.permission.risk} · {operation.sourcePath}
|
|
162
|
-
</div>
|
|
163
|
-
{!runnable && (
|
|
164
|
-
<div style={errorStyle}>
|
|
165
|
-
This command runner supports Node-hosted operations. Import the underlying function
|
|
166
|
-
directly in browser or runtime code.
|
|
167
|
-
</div>
|
|
168
|
-
)}
|
|
169
|
-
<label style={labelStyle} htmlFor={`operation-input:${operation.name}`}>
|
|
170
|
-
Input JSON
|
|
171
|
-
</label>
|
|
172
|
-
<textarea
|
|
173
|
-
id={`operation-input:${operation.name}`}
|
|
174
|
-
value={inputText}
|
|
175
|
-
onChange={(event) => setInputText(event.target.value)}
|
|
176
|
-
rows={10}
|
|
177
|
-
style={textareaStyle}
|
|
178
|
-
/>
|
|
179
|
-
{inputError && <div style={errorStyle}>{inputError}</div>}
|
|
180
|
-
<Button type="button" disabled={!runnable || running} onClick={() => void execute()}>
|
|
181
|
-
{running ? 'Running…' : 'Run command'}
|
|
182
|
-
</Button>
|
|
183
|
-
<details>
|
|
184
|
-
<summary style={labelStyle}>Input schema</summary>
|
|
185
|
-
<JsonBlock value={parsedSchema} />
|
|
186
|
-
</details>
|
|
187
|
-
{result !== null && (
|
|
188
|
-
<section>
|
|
189
|
-
<div style={labelStyle}>Result</div>
|
|
190
|
-
<JsonBlock value={result} />
|
|
191
|
-
</section>
|
|
192
|
-
)}
|
|
193
|
-
</div>
|
|
194
|
-
);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
const rootStyle: React.CSSProperties = {
|
|
198
|
-
padding: 16,
|
|
199
|
-
overflow: 'auto',
|
|
200
|
-
pointerEvents: 'auto',
|
|
201
|
-
color: text[1],
|
|
202
|
-
fontSize: fontSize.base,
|
|
203
|
-
};
|
|
204
|
-
const headingStyle: React.CSSProperties = { margin: '0 0 8px', fontSize: 18 };
|
|
205
|
-
const mutedStyle: React.CSSProperties = { color: text[2], fontSize: fontSize.sm };
|
|
206
|
-
const labelStyle: React.CSSProperties = { display: 'block', margin: '14px 0 6px', color: text[1] };
|
|
207
|
-
const errorStyle: React.CSSProperties = { marginTop: 10, color: danger, whiteSpace: 'pre-wrap' };
|
|
208
|
-
const textareaStyle: React.CSSProperties = {
|
|
209
|
-
width: '100%',
|
|
210
|
-
boxSizing: 'border-box',
|
|
211
|
-
padding: 10,
|
|
212
|
-
resize: 'vertical',
|
|
213
|
-
background: bg.inset,
|
|
214
|
-
color: text[1],
|
|
215
|
-
border: `1px solid ${border[2]}`,
|
|
216
|
-
borderRadius: radius.sm,
|
|
217
|
-
fontFamily: 'monospace',
|
|
218
|
-
};
|
|
219
|
-
const jsonStyle: React.CSSProperties = {
|
|
220
|
-
overflow: 'auto',
|
|
221
|
-
padding: 10,
|
|
222
|
-
background: bg.inset,
|
|
223
|
-
border: `1px solid ${border[1]}`,
|
|
224
|
-
borderRadius: radius.sm,
|
|
225
|
-
color: text[2],
|
|
226
|
-
};
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
# src/operations — discoverable project scripts
|
|
2
|
-
|
|
3
|
-
Put tooling-facing operations in `*.operation.ts` files here. They remain
|
|
4
|
-
ordinary TypeScript modules. Keep reusable implementation in a normal project
|
|
5
|
-
module, then wrap it with one thin `operation` definition.
|
|
6
|
-
|
|
7
|
-
```ts
|
|
8
|
-
// src/automation/inspect-something.ts
|
|
9
|
-
export async function inspectSomething(input: { path: string }) {
|
|
10
|
-
return { path: input.path };
|
|
11
|
-
}
|
|
12
|
-
```
|
|
13
|
-
|
|
14
|
-
```ts
|
|
15
|
-
// src/operations/inspect-something.operation.ts
|
|
16
|
-
import { defineOperation } from '@vgai/sdk/registry';
|
|
17
|
-
import { z } from 'zod';
|
|
18
|
-
import { inspectSomething } from '../automation/inspect-something';
|
|
19
|
-
|
|
20
|
-
export const operation = defineOperation({
|
|
21
|
-
name: 'project.inspectSomething',
|
|
22
|
-
summary: 'Inspect something owned by this project.',
|
|
23
|
-
description: 'A project-local operation. The implementation remains directly importable.',
|
|
24
|
-
input: z.object({ path: z.string() }),
|
|
25
|
-
result: z.object({ path: z.string() }),
|
|
26
|
-
errors: [],
|
|
27
|
-
requires: { project: true },
|
|
28
|
-
host: 'node',
|
|
29
|
-
mutates: false,
|
|
30
|
-
supportsDryRun: false,
|
|
31
|
-
permission: { risk: 'read', summary: 'Reads project-local information.' },
|
|
32
|
-
impl: inspectSomething,
|
|
33
|
-
});
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
The editor discovers these files without a registry. List them for agents with
|
|
37
|
-
`npm run vgai -- run --list --json`; inspect one with
|
|
38
|
-
`npm run vgai -- run project.inspectSomething --describe`; run it with
|
|
39
|
-
`npm run vgai -- run project.inspectSomething --input input.json` while the
|
|
40
|
-
project editor is running. The editor also provides a directly clickable
|
|
41
|
-
Project Commands catalog and a default JSON/schema UI.
|
|
42
|
-
|
|
43
|
-
For a frequent workflow, an ordinary package script may be a shortcut:
|
|
44
|
-
|
|
45
|
-
```json
|
|
46
|
-
{
|
|
47
|
-
"scripts": {
|
|
48
|
-
"inspect-project": "npm run vgai -- run project.inspectSomething"
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
That script is convenience, not registration; deleting it does not remove the
|
|
54
|
-
capability from the editor or catalog.
|
|
55
|
-
|
|
56
|
-
For a custom UI, add an ordinary `src/tools/*.tool.tsx` project tool and call
|
|
57
|
-
the same operation through the public editor SDK:
|
|
58
|
-
|
|
59
|
-
```ts
|
|
60
|
-
import { EditorClient } from '@vgai/editor-sdk';
|
|
61
|
-
|
|
62
|
-
const editor = new EditorClient({ url: window.location.origin });
|
|
63
|
-
const result = await editor.runProjectOperation('project.buildCharacter', input, {
|
|
64
|
-
confirm: true,
|
|
65
|
-
});
|
|
66
|
-
if (result.ok) await editor.openAsset('/characters/generated.glb', 'model');
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
Keep React/editor imports out of the operation module. If the operation writes
|
|
70
|
-
ordinary files under `public/`, a successful declared mutation refreshes the
|
|
71
|
-
project's Assets navigator automatically. The generator is a tool/command; its
|
|
72
|
-
persisted files are assets. It does not become an Asset Library tile.
|
|
73
|
-
|
|
74
|
-
Game/runtime code should import the underlying function directly rather than
|
|
75
|
-
calling through the tooling registry.
|
|
76
|
-
|
|
77
|
-
Installed packages can advertise operations from their own `package.json`
|
|
78
|
-
using `{"vgai":{"operations":["./path/to/file.operation.js"]}}`. The editor
|
|
79
|
-
loads direct dependencies only and reports the package-qualified source path.
|
|
80
|
-
This is how a library can provide an optional author-time bake/import command
|
|
81
|
-
without adding a domain switch to the editor or copying wrappers into every
|
|
82
|
-
project.
|
|
83
|
-
|
|
84
|
-
Node operations that generate assets receive `ctx.projectOutputs`. Hand it a
|
|
85
|
-
complete batch of ordinary files under `public/`; the host validates all paths,
|
|
86
|
-
stages the batch, commits it atomically, and returns a JSON-safe output
|
|
87
|
-
manifest. Use `{ dryRun: true }` to validate without writing. Successful writes
|
|
88
|
-
refresh the Assets navigator automatically.
|