tide-commander 0.52.0
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/LICENSE +21 -0
- package/README.md +364 -0
- package/dist/assets/characters/Textures/colormap.png +0 -0
- package/dist/assets/characters/character-female-a.glb +0 -0
- package/dist/assets/characters/character-female-b.glb +0 -0
- package/dist/assets/characters/character-female-c.glb +0 -0
- package/dist/assets/characters/character-female-d.glb +0 -0
- package/dist/assets/characters/character-female-e.glb +0 -0
- package/dist/assets/characters/character-female-f.glb +0 -0
- package/dist/assets/characters/character-male-a-processed.gltf +11862 -0
- package/dist/assets/characters/character-male-a.glb +0 -0
- package/dist/assets/characters/character-male-b.glb +0 -0
- package/dist/assets/characters/character-male-c.glb +0 -0
- package/dist/assets/characters/character-male-d.glb +0 -0
- package/dist/assets/characters/character-male-e.glb +0 -0
- package/dist/assets/characters/character-male-f.glb +0 -0
- package/dist/assets/icons/icon-192.png +0 -0
- package/dist/assets/icons/icon-512.png +0 -0
- package/dist/assets/landing-Cc0MDBAK.css +1 -0
- package/dist/assets/main-BIpLsrUu.css +1 -0
- package/dist/assets/main-DMTRw3br.js +276 -0
- package/dist/assets/textures/concrete_floor_worn_001_diff_1k.jpg +0 -0
- package/dist/assets/textures/logo-blanco.png +0 -0
- package/dist/assets/vendor-react-uS-d4TUT.js +17 -0
- package/dist/assets/vendor-three-4iQNXcoo.js +3828 -0
- package/dist/assets/web-BZdi2lG9.js +1 -0
- package/dist/assets/web-yHsOO1Qb.js +1 -0
- package/dist/index.html +38 -0
- package/dist/manifest.json +39 -0
- package/dist/src/packages/landing/index.html +463 -0
- package/dist/src/packages/server/app.js +87 -0
- package/dist/src/packages/server/auth/index.js +121 -0
- package/dist/src/packages/server/claude/backend.js +578 -0
- package/dist/src/packages/server/claude/index.js +8 -0
- package/dist/src/packages/server/claude/runner/internal-events.js +22 -0
- package/dist/src/packages/server/claude/runner/process-lifecycle.js +208 -0
- package/dist/src/packages/server/claude/runner/recovery-store.js +72 -0
- package/dist/src/packages/server/claude/runner/resource-monitor.js +51 -0
- package/dist/src/packages/server/claude/runner/restart-policy.js +69 -0
- package/dist/src/packages/server/claude/runner/stdout-pipeline.js +153 -0
- package/dist/src/packages/server/claude/runner/watchdog.js +114 -0
- package/dist/src/packages/server/claude/runner.js +310 -0
- package/dist/src/packages/server/claude/session-loader.js +898 -0
- package/dist/src/packages/server/claude/types.js +5 -0
- package/dist/src/packages/server/cli.js +113 -0
- package/dist/src/packages/server/codex/backend.js +119 -0
- package/dist/src/packages/server/codex/index.js +2 -0
- package/dist/src/packages/server/codex/json-event-parser.js +612 -0
- package/dist/src/packages/server/data/builtin-skills/bitbucket-pr.js +298 -0
- package/dist/src/packages/server/data/builtin-skills/full-notifications.js +49 -0
- package/dist/src/packages/server/data/builtin-skills/git-captain.js +304 -0
- package/dist/src/packages/server/data/builtin-skills/index.js +61 -0
- package/dist/src/packages/server/data/builtin-skills/pm2-logs.js +354 -0
- package/dist/src/packages/server/data/builtin-skills/send-message-to-agent.js +51 -0
- package/dist/src/packages/server/data/builtin-skills/server-logs.js +124 -0
- package/dist/src/packages/server/data/builtin-skills/streaming-exec.js +94 -0
- package/dist/src/packages/server/data/builtin-skills/types.js +4 -0
- package/dist/src/packages/server/data/builtin-skills.js +6 -0
- package/dist/src/packages/server/data/index.js +890 -0
- package/dist/src/packages/server/data/snapshots.js +371 -0
- package/dist/src/packages/server/index.js +96 -0
- package/dist/src/packages/server/prompts/tide-commander.js +13 -0
- package/dist/src/packages/server/routes/agents.js +406 -0
- package/dist/src/packages/server/routes/config.js +347 -0
- package/dist/src/packages/server/routes/custom-models.js +170 -0
- package/dist/src/packages/server/routes/exec.js +269 -0
- package/dist/src/packages/server/routes/files.js +995 -0
- package/dist/src/packages/server/routes/index.js +38 -0
- package/dist/src/packages/server/routes/notifications.js +81 -0
- package/dist/src/packages/server/routes/permissions.js +115 -0
- package/dist/src/packages/server/routes/snapshots.js +224 -0
- package/dist/src/packages/server/routes/stt.js +99 -0
- package/dist/src/packages/server/routes/tts.js +166 -0
- package/dist/src/packages/server/routes/voice-assistant.js +310 -0
- package/dist/src/packages/server/runtime/claude-runtime-provider.js +10 -0
- package/dist/src/packages/server/runtime/codex-runtime-provider.js +11 -0
- package/dist/src/packages/server/runtime/index.js +2 -0
- package/dist/src/packages/server/runtime/types.js +6 -0
- package/dist/src/packages/server/services/agent-lifecycle-service.js +82 -0
- package/dist/src/packages/server/services/agent-service.js +410 -0
- package/dist/src/packages/server/services/boss-message-service.js +430 -0
- package/dist/src/packages/server/services/boss-service.js +553 -0
- package/dist/src/packages/server/services/building-service.js +867 -0
- package/dist/src/packages/server/services/claude-service.js +5 -0
- package/dist/src/packages/server/services/custom-class-service.js +323 -0
- package/dist/src/packages/server/services/database-service.js +914 -0
- package/dist/src/packages/server/services/docker-service.js +865 -0
- package/dist/src/packages/server/services/fileTracker.js +242 -0
- package/dist/src/packages/server/services/index.js +21 -0
- package/dist/src/packages/server/services/permission-service.js +258 -0
- package/dist/src/packages/server/services/pm2-service.js +435 -0
- package/dist/src/packages/server/services/runtime-command-execution.js +168 -0
- package/dist/src/packages/server/services/runtime-events.js +357 -0
- package/dist/src/packages/server/services/runtime-service.js +308 -0
- package/dist/src/packages/server/services/runtime-status-sync.js +104 -0
- package/dist/src/packages/server/services/runtime-subagents.js +50 -0
- package/dist/src/packages/server/services/runtime-watchdog.js +74 -0
- package/dist/src/packages/server/services/secrets-service.js +206 -0
- package/dist/src/packages/server/services/skill-service.js +508 -0
- package/dist/src/packages/server/services/subordinate-context-service.js +223 -0
- package/dist/src/packages/server/services/supervisor-claude.js +132 -0
- package/dist/src/packages/server/services/supervisor-prompts.js +80 -0
- package/dist/src/packages/server/services/supervisor-service.js +659 -0
- package/dist/src/packages/server/services/work-plan-service.js +476 -0
- package/dist/src/packages/server/setup.js +86 -0
- package/dist/src/packages/server/utils/index.js +4 -0
- package/dist/src/packages/server/utils/logger.js +302 -0
- package/dist/src/packages/server/utils/string.js +39 -0
- package/dist/src/packages/server/utils/tool-formatting.js +139 -0
- package/dist/src/packages/server/utils/unicode.js +46 -0
- package/dist/src/packages/server/websocket/handler.js +290 -0
- package/dist/src/packages/server/websocket/handlers/agent-handler.js +515 -0
- package/dist/src/packages/server/websocket/handlers/boss-handler.js +116 -0
- package/dist/src/packages/server/websocket/handlers/boss-response-handler.js +250 -0
- package/dist/src/packages/server/websocket/handlers/building-handler.js +298 -0
- package/dist/src/packages/server/websocket/handlers/command-handler.js +217 -0
- package/dist/src/packages/server/websocket/handlers/custom-class-handler.js +68 -0
- package/dist/src/packages/server/websocket/handlers/database-handler.js +223 -0
- package/dist/src/packages/server/websocket/handlers/notification-handler.js +25 -0
- package/dist/src/packages/server/websocket/handlers/permission-handler.js +21 -0
- package/dist/src/packages/server/websocket/handlers/secrets-handler.js +61 -0
- package/dist/src/packages/server/websocket/handlers/skill-handler.js +148 -0
- package/dist/src/packages/server/websocket/handlers/supervisor-handler.js +44 -0
- package/dist/src/packages/server/websocket/handlers/sync-handler.js +19 -0
- package/dist/src/packages/server/websocket/handlers/types.js +4 -0
- package/dist/src/packages/server/websocket/listeners/boss-listeners.js +21 -0
- package/dist/src/packages/server/websocket/listeners/index.js +32 -0
- package/dist/src/packages/server/websocket/listeners/permission-listeners.js +19 -0
- package/dist/src/packages/server/websocket/listeners/runtime-listeners.js +196 -0
- package/dist/src/packages/server/websocket/listeners/skill-listeners.js +51 -0
- package/dist/src/packages/server/websocket/listeners/supervisor-listeners.js +37 -0
- package/dist/src/packages/shared/agent-types.js +54 -0
- package/dist/src/packages/shared/building-types.js +43 -0
- package/dist/src/packages/shared/common-types.js +1 -0
- package/dist/src/packages/shared/database-types.js +8 -0
- package/dist/src/packages/shared/types/snapshot.js +7 -0
- package/dist/src/packages/shared/types.js +12 -0
- package/dist/src/packages/shared/websocket-messages.js +1 -0
- package/dist/sw.js +37 -0
- package/package.json +90 -0
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config Export/Import Routes
|
|
3
|
+
* Handles exporting and importing Tide Commander configuration
|
|
4
|
+
*/
|
|
5
|
+
import { Router } from 'express';
|
|
6
|
+
import { createReadStream, existsSync, mkdirSync, readdirSync, statSync } from 'fs';
|
|
7
|
+
import { readFile, writeFile, mkdir, rm } from 'fs/promises';
|
|
8
|
+
import { join, dirname } from 'path';
|
|
9
|
+
import os from 'os';
|
|
10
|
+
import archiver from 'archiver';
|
|
11
|
+
import { Extract } from 'unzip-stream';
|
|
12
|
+
import { createLogger } from '../utils/index.js';
|
|
13
|
+
const log = createLogger('ConfigRoutes');
|
|
14
|
+
const router = Router();
|
|
15
|
+
// Config directories
|
|
16
|
+
const HOME_CONFIG_DIR = join(os.homedir(), '.tide-commander');
|
|
17
|
+
const DATA_CONFIG_DIR = join(os.homedir(), '.local', 'share', 'tide-commander');
|
|
18
|
+
const CONFIG_CATEGORIES = [
|
|
19
|
+
{
|
|
20
|
+
id: 'agents',
|
|
21
|
+
name: 'Agents',
|
|
22
|
+
description: 'Agent positions, names, and settings',
|
|
23
|
+
files: ['agents.json'],
|
|
24
|
+
sourceDir: 'data',
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
id: 'areas',
|
|
28
|
+
name: 'Areas',
|
|
29
|
+
description: 'Drawing areas and zones',
|
|
30
|
+
files: ['areas.json'],
|
|
31
|
+
sourceDir: 'data',
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
id: 'buildings',
|
|
35
|
+
name: 'Buildings',
|
|
36
|
+
description: 'Building configurations and PM2 settings',
|
|
37
|
+
files: ['buildings.json'],
|
|
38
|
+
sourceDir: 'data',
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
id: 'skills',
|
|
42
|
+
name: 'Skills',
|
|
43
|
+
description: 'Custom skills and their assignments',
|
|
44
|
+
files: ['skills.json'],
|
|
45
|
+
sourceDir: 'data',
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: 'custom-classes',
|
|
49
|
+
name: 'Custom Agent Classes',
|
|
50
|
+
description: 'Custom agent class definitions and instructions',
|
|
51
|
+
files: ['custom-agent-classes.json'],
|
|
52
|
+
sourceDir: 'data',
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
id: 'class-instructions',
|
|
56
|
+
name: 'Class Instructions',
|
|
57
|
+
description: 'Markdown instruction files for custom classes',
|
|
58
|
+
files: ['class-instructions/*'],
|
|
59
|
+
sourceDir: 'home',
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
id: 'prompts',
|
|
63
|
+
name: 'Agent Prompts',
|
|
64
|
+
description: 'Individual agent prompt files',
|
|
65
|
+
files: ['prompts/*'],
|
|
66
|
+
sourceDir: 'home',
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
id: 'custom-models',
|
|
70
|
+
name: 'Custom 3D Models',
|
|
71
|
+
description: 'GLB model files for custom agent classes',
|
|
72
|
+
files: ['custom-models/*'],
|
|
73
|
+
sourceDir: 'home',
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
id: 'hooks',
|
|
77
|
+
name: 'Hooks',
|
|
78
|
+
description: 'Hook scripts and settings',
|
|
79
|
+
files: ['hooks/*', 'hook-settings.json'],
|
|
80
|
+
sourceDir: 'home',
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
id: 'permissions',
|
|
84
|
+
name: 'Remembered Permissions',
|
|
85
|
+
description: 'Saved permission decisions',
|
|
86
|
+
files: ['remembered-permissions.json'],
|
|
87
|
+
sourceDir: 'home',
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
id: 'secrets',
|
|
91
|
+
name: 'Secrets',
|
|
92
|
+
description: 'Encrypted secrets (keys not included)',
|
|
93
|
+
files: ['secrets.json'],
|
|
94
|
+
sourceDir: 'data',
|
|
95
|
+
},
|
|
96
|
+
];
|
|
97
|
+
/**
|
|
98
|
+
* Get list of available config categories
|
|
99
|
+
*/
|
|
100
|
+
router.get('/categories', (_req, res) => {
|
|
101
|
+
const categories = CONFIG_CATEGORIES.map(cat => ({
|
|
102
|
+
id: cat.id,
|
|
103
|
+
name: cat.name,
|
|
104
|
+
description: cat.description,
|
|
105
|
+
}));
|
|
106
|
+
res.json(categories);
|
|
107
|
+
});
|
|
108
|
+
/**
|
|
109
|
+
* Helper to get files matching a pattern
|
|
110
|
+
*/
|
|
111
|
+
function getFilesForPattern(baseDir, pattern) {
|
|
112
|
+
const files = [];
|
|
113
|
+
if (pattern.endsWith('/*')) {
|
|
114
|
+
// Directory pattern - get all files in directory
|
|
115
|
+
const dirName = pattern.slice(0, -2);
|
|
116
|
+
const dirPath = join(baseDir, dirName);
|
|
117
|
+
if (existsSync(dirPath) && statSync(dirPath).isDirectory()) {
|
|
118
|
+
const entries = readdirSync(dirPath);
|
|
119
|
+
for (const entry of entries) {
|
|
120
|
+
const entryPath = join(dirPath, entry);
|
|
121
|
+
if (statSync(entryPath).isFile()) {
|
|
122
|
+
files.push(join(dirName, entry));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
// Single file pattern
|
|
129
|
+
const filePath = join(baseDir, pattern);
|
|
130
|
+
if (existsSync(filePath) && statSync(filePath).isFile()) {
|
|
131
|
+
files.push(pattern);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return files;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Export selected config categories as a ZIP file
|
|
138
|
+
* GET /api/config/export?categories=agents,buildings,skills
|
|
139
|
+
*/
|
|
140
|
+
router.get('/export', async (req, res) => {
|
|
141
|
+
try {
|
|
142
|
+
const categoriesParam = req.query.categories;
|
|
143
|
+
const selectedIds = categoriesParam ? categoriesParam.split(',') : CONFIG_CATEGORIES.map(c => c.id);
|
|
144
|
+
// Validate categories
|
|
145
|
+
const selectedCategories = CONFIG_CATEGORIES.filter(c => selectedIds.includes(c.id));
|
|
146
|
+
if (selectedCategories.length === 0) {
|
|
147
|
+
res.status(400).json({ error: 'No valid categories selected' });
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
log.log(`Exporting config categories: ${selectedCategories.map(c => c.id).join(', ')}`);
|
|
151
|
+
// Set response headers for ZIP download
|
|
152
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
153
|
+
const filename = `tide-commander-config-${timestamp}.zip`;
|
|
154
|
+
res.setHeader('Content-Type', 'application/zip');
|
|
155
|
+
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
|
|
156
|
+
// Create ZIP archive
|
|
157
|
+
const archive = archiver('zip', { zlib: { level: 9 } });
|
|
158
|
+
archive.on('error', (err) => {
|
|
159
|
+
log.error('Archive error:', err);
|
|
160
|
+
if (!res.headersSent) {
|
|
161
|
+
res.status(500).json({ error: 'Failed to create archive' });
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
// Pipe archive to response
|
|
165
|
+
archive.pipe(res);
|
|
166
|
+
// Add manifest file
|
|
167
|
+
const manifest = {
|
|
168
|
+
version: '1.0',
|
|
169
|
+
exportedAt: new Date().toISOString(),
|
|
170
|
+
categories: selectedCategories.map(c => c.id),
|
|
171
|
+
};
|
|
172
|
+
archive.append(JSON.stringify(manifest, null, 2), { name: 'manifest.json' });
|
|
173
|
+
// Add files for each selected category
|
|
174
|
+
for (const category of selectedCategories) {
|
|
175
|
+
const baseDir = category.sourceDir === 'home' ? HOME_CONFIG_DIR : DATA_CONFIG_DIR;
|
|
176
|
+
for (const pattern of category.files) {
|
|
177
|
+
const files = getFilesForPattern(baseDir, pattern);
|
|
178
|
+
for (const file of files) {
|
|
179
|
+
const filePath = join(baseDir, file);
|
|
180
|
+
const archivePath = join(category.sourceDir, file);
|
|
181
|
+
archive.file(filePath, { name: archivePath });
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
await archive.finalize();
|
|
186
|
+
log.log(`Config export completed: ${filename}`);
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
log.error('Export error:', error);
|
|
190
|
+
if (!res.headersSent) {
|
|
191
|
+
res.status(500).json({ error: error.message });
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
/**
|
|
196
|
+
* Preview what's in an uploaded config ZIP
|
|
197
|
+
* POST /api/config/preview (multipart/form-data with 'file' field)
|
|
198
|
+
*/
|
|
199
|
+
router.post('/preview', async (req, res) => {
|
|
200
|
+
try {
|
|
201
|
+
if (!req.body || !Buffer.isBuffer(req.body)) {
|
|
202
|
+
res.status(400).json({ error: 'No file uploaded' });
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
// Create temp directory for extraction
|
|
206
|
+
const tempDir = join(os.tmpdir(), `tide-config-preview-${Date.now()}`);
|
|
207
|
+
mkdirSync(tempDir, { recursive: true });
|
|
208
|
+
try {
|
|
209
|
+
// Write buffer to temp file and extract
|
|
210
|
+
const tempZip = join(tempDir, 'upload.zip');
|
|
211
|
+
await writeFile(tempZip, req.body);
|
|
212
|
+
// Extract ZIP
|
|
213
|
+
await new Promise((resolve, reject) => {
|
|
214
|
+
createReadStream(tempZip)
|
|
215
|
+
.pipe(Extract({ path: tempDir }))
|
|
216
|
+
.on('close', resolve)
|
|
217
|
+
.on('error', reject);
|
|
218
|
+
});
|
|
219
|
+
// Read manifest
|
|
220
|
+
const manifestPath = join(tempDir, 'manifest.json');
|
|
221
|
+
if (!existsSync(manifestPath)) {
|
|
222
|
+
res.status(400).json({ error: 'Invalid config file: missing manifest.json' });
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
const manifest = JSON.parse(await readFile(manifestPath, 'utf-8'));
|
|
226
|
+
// Scan for available categories
|
|
227
|
+
const availableCategories = [];
|
|
228
|
+
for (const category of CONFIG_CATEGORIES) {
|
|
229
|
+
if (manifest.categories?.includes(category.id)) {
|
|
230
|
+
const baseDir = join(tempDir, category.sourceDir);
|
|
231
|
+
let fileCount = 0;
|
|
232
|
+
for (const pattern of category.files) {
|
|
233
|
+
const files = getFilesForPattern(baseDir, pattern);
|
|
234
|
+
fileCount += files.length;
|
|
235
|
+
}
|
|
236
|
+
if (fileCount > 0) {
|
|
237
|
+
availableCategories.push({
|
|
238
|
+
id: category.id,
|
|
239
|
+
name: category.name,
|
|
240
|
+
description: category.description,
|
|
241
|
+
fileCount,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
res.json({
|
|
247
|
+
version: manifest.version,
|
|
248
|
+
exportedAt: manifest.exportedAt,
|
|
249
|
+
categories: availableCategories,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
finally {
|
|
253
|
+
// Cleanup temp directory
|
|
254
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
catch (error) {
|
|
258
|
+
log.error('Preview error:', error);
|
|
259
|
+
res.status(500).json({ error: error.message });
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
/**
|
|
263
|
+
* Import config from uploaded ZIP
|
|
264
|
+
* POST /api/config/import?categories=agents,buildings (multipart/form-data with 'file' field)
|
|
265
|
+
*/
|
|
266
|
+
router.post('/import', async (req, res) => {
|
|
267
|
+
try {
|
|
268
|
+
if (!req.body || !Buffer.isBuffer(req.body)) {
|
|
269
|
+
res.status(400).json({ error: 'No file uploaded' });
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
const categoriesParam = req.query.categories;
|
|
273
|
+
if (!categoriesParam) {
|
|
274
|
+
res.status(400).json({ error: 'No categories specified' });
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
const selectedIds = categoriesParam.split(',');
|
|
278
|
+
const selectedCategories = CONFIG_CATEGORIES.filter(c => selectedIds.includes(c.id));
|
|
279
|
+
if (selectedCategories.length === 0) {
|
|
280
|
+
res.status(400).json({ error: 'No valid categories selected' });
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
log.log(`Importing config categories: ${selectedCategories.map(c => c.id).join(', ')}`);
|
|
284
|
+
// Create temp directory for extraction
|
|
285
|
+
const tempDir = join(os.tmpdir(), `tide-config-import-${Date.now()}`);
|
|
286
|
+
mkdirSync(tempDir, { recursive: true });
|
|
287
|
+
try {
|
|
288
|
+
// Write buffer to temp file and extract
|
|
289
|
+
const tempZip = join(tempDir, 'upload.zip');
|
|
290
|
+
await writeFile(tempZip, req.body);
|
|
291
|
+
// Extract ZIP
|
|
292
|
+
await new Promise((resolve, reject) => {
|
|
293
|
+
createReadStream(tempZip)
|
|
294
|
+
.pipe(Extract({ path: tempDir }))
|
|
295
|
+
.on('close', resolve)
|
|
296
|
+
.on('error', reject);
|
|
297
|
+
});
|
|
298
|
+
// Verify manifest
|
|
299
|
+
const manifestPath = join(tempDir, 'manifest.json');
|
|
300
|
+
if (!existsSync(manifestPath)) {
|
|
301
|
+
res.status(400).json({ error: 'Invalid config file: missing manifest.json' });
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
const imported = [];
|
|
305
|
+
// Import each selected category
|
|
306
|
+
for (const category of selectedCategories) {
|
|
307
|
+
const sourceBaseDir = join(tempDir, category.sourceDir);
|
|
308
|
+
const targetBaseDir = category.sourceDir === 'home' ? HOME_CONFIG_DIR : DATA_CONFIG_DIR;
|
|
309
|
+
const importedFiles = [];
|
|
310
|
+
for (const pattern of category.files) {
|
|
311
|
+
const files = getFilesForPattern(sourceBaseDir, pattern);
|
|
312
|
+
for (const file of files) {
|
|
313
|
+
const sourcePath = join(sourceBaseDir, file);
|
|
314
|
+
const targetPath = join(targetBaseDir, file);
|
|
315
|
+
// Ensure target directory exists
|
|
316
|
+
const targetDir = dirname(targetPath);
|
|
317
|
+
if (!existsSync(targetDir)) {
|
|
318
|
+
await mkdir(targetDir, { recursive: true });
|
|
319
|
+
}
|
|
320
|
+
// Copy file
|
|
321
|
+
const content = await readFile(sourcePath);
|
|
322
|
+
await writeFile(targetPath, content);
|
|
323
|
+
importedFiles.push(file);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
if (importedFiles.length > 0) {
|
|
327
|
+
imported.push({ category: category.id, files: importedFiles });
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
log.log(`Config import completed: ${imported.map(i => `${i.category}(${i.files.length})`).join(', ')}`);
|
|
331
|
+
res.json({
|
|
332
|
+
success: true,
|
|
333
|
+
imported,
|
|
334
|
+
message: 'Config imported successfully. Restart Tide Commander to apply changes.',
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
finally {
|
|
338
|
+
// Cleanup temp directory
|
|
339
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
catch (error) {
|
|
343
|
+
log.error('Import error:', error);
|
|
344
|
+
res.status(500).json({ error: error.message });
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
export default router;
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom Models Routes
|
|
3
|
+
* REST API endpoints for custom 3D model operations
|
|
4
|
+
*/
|
|
5
|
+
import { Router } from 'express';
|
|
6
|
+
import * as fs from 'fs';
|
|
7
|
+
import { logger } from '../utils/logger.js';
|
|
8
|
+
import { saveCustomModel, hasCustomModel, getCustomModelPath, getCustomClass, updateCustomClass, } from '../services/custom-class-service.js';
|
|
9
|
+
const log = logger.files;
|
|
10
|
+
const router = Router();
|
|
11
|
+
// Maximum file size: 50MB
|
|
12
|
+
const MAX_FILE_SIZE = 50 * 1024 * 1024;
|
|
13
|
+
/**
|
|
14
|
+
* POST /api/custom-models/upload/:classId
|
|
15
|
+
* Upload a custom GLB model for a class
|
|
16
|
+
*
|
|
17
|
+
* Headers:
|
|
18
|
+
* Content-Type: application/octet-stream or model/gltf-binary
|
|
19
|
+
* X-Filename: optional original filename
|
|
20
|
+
*
|
|
21
|
+
* Body: Raw GLB file data
|
|
22
|
+
*
|
|
23
|
+
* Returns: { success: true, path: string, size: number }
|
|
24
|
+
*/
|
|
25
|
+
router.post('/upload/:classId', async (req, res) => {
|
|
26
|
+
try {
|
|
27
|
+
const { classId } = req.params;
|
|
28
|
+
if (!classId) {
|
|
29
|
+
res.status(400).json({ error: 'Missing classId parameter' });
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
// Verify the class exists
|
|
33
|
+
const customClass = getCustomClass(classId);
|
|
34
|
+
if (!customClass) {
|
|
35
|
+
res.status(404).json({ error: 'Custom class not found' });
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
// Collect body data
|
|
39
|
+
const chunks = [];
|
|
40
|
+
let totalSize = 0;
|
|
41
|
+
req.on('data', (chunk) => {
|
|
42
|
+
totalSize += chunk.length;
|
|
43
|
+
if (totalSize > MAX_FILE_SIZE) {
|
|
44
|
+
res.status(413).json({ error: 'File too large (max 50MB)' });
|
|
45
|
+
req.destroy();
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
chunks.push(chunk);
|
|
49
|
+
});
|
|
50
|
+
req.on('end', () => {
|
|
51
|
+
if (res.headersSent)
|
|
52
|
+
return; // Already responded with error
|
|
53
|
+
const buffer = Buffer.concat(chunks);
|
|
54
|
+
// Validate GLB magic number (glTF binary starts with 'glTF')
|
|
55
|
+
if (buffer.length < 4 || buffer.toString('ascii', 0, 4) !== 'glTF') {
|
|
56
|
+
res.status(400).json({ error: 'Invalid GLB file format' });
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
// Save the model
|
|
60
|
+
const modelPath = saveCustomModel(classId, buffer);
|
|
61
|
+
// Update the class with the custom model path
|
|
62
|
+
updateCustomClass(classId, {
|
|
63
|
+
customModelPath: modelPath,
|
|
64
|
+
model: undefined, // Clear built-in model selection
|
|
65
|
+
});
|
|
66
|
+
log.log(`Uploaded custom model for class ${classId} (${buffer.length} bytes)`);
|
|
67
|
+
res.json({
|
|
68
|
+
success: true,
|
|
69
|
+
path: modelPath,
|
|
70
|
+
size: buffer.length,
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
req.on('error', (err) => {
|
|
74
|
+
log.error('Model upload error:', err);
|
|
75
|
+
if (!res.headersSent) {
|
|
76
|
+
res.status(500).json({ error: 'Upload failed' });
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
log.error('Failed to upload custom model:', err);
|
|
82
|
+
res.status(500).json({ error: err.message });
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
/**
|
|
86
|
+
* GET /api/custom-models/:classId
|
|
87
|
+
* Serve a custom model file
|
|
88
|
+
*
|
|
89
|
+
* Returns: GLB file binary data
|
|
90
|
+
*/
|
|
91
|
+
router.get('/:classId', async (req, res) => {
|
|
92
|
+
try {
|
|
93
|
+
const { classId } = req.params;
|
|
94
|
+
if (!classId) {
|
|
95
|
+
res.status(400).json({ error: 'Missing classId parameter' });
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const modelPath = getCustomModelPath(classId);
|
|
99
|
+
if (!modelPath) {
|
|
100
|
+
res.status(404).json({ error: 'Custom model not found' });
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const stats = fs.statSync(modelPath);
|
|
104
|
+
res.setHeader('Content-Type', 'model/gltf-binary');
|
|
105
|
+
res.setHeader('Content-Length', stats.size);
|
|
106
|
+
res.setHeader('Cache-Control', 'public, max-age=3600'); // Cache for 1 hour
|
|
107
|
+
const stream = fs.createReadStream(modelPath);
|
|
108
|
+
stream.pipe(res);
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
log.error('Failed to serve custom model:', err);
|
|
112
|
+
res.status(500).json({ error: err.message });
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
/**
|
|
116
|
+
* DELETE /api/custom-models/:classId
|
|
117
|
+
* Delete a custom model and revert class to default model
|
|
118
|
+
*/
|
|
119
|
+
router.delete('/:classId', async (req, res) => {
|
|
120
|
+
try {
|
|
121
|
+
const { classId } = req.params;
|
|
122
|
+
if (!classId) {
|
|
123
|
+
res.status(400).json({ error: 'Missing classId parameter' });
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const customClass = getCustomClass(classId);
|
|
127
|
+
if (!customClass) {
|
|
128
|
+
res.status(404).json({ error: 'Custom class not found' });
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const modelPath = getCustomModelPath(classId);
|
|
132
|
+
if (modelPath && fs.existsSync(modelPath)) {
|
|
133
|
+
fs.unlinkSync(modelPath);
|
|
134
|
+
log.log(`Deleted custom model for class ${classId}`);
|
|
135
|
+
}
|
|
136
|
+
// Update class to use default model
|
|
137
|
+
updateCustomClass(classId, {
|
|
138
|
+
customModelPath: undefined,
|
|
139
|
+
model: 'character-male-a.glb', // Revert to default
|
|
140
|
+
animationMapping: undefined,
|
|
141
|
+
availableAnimations: undefined,
|
|
142
|
+
modelScale: undefined,
|
|
143
|
+
});
|
|
144
|
+
res.json({ success: true });
|
|
145
|
+
}
|
|
146
|
+
catch (err) {
|
|
147
|
+
log.error('Failed to delete custom model:', err);
|
|
148
|
+
res.status(500).json({ error: err.message });
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
/**
|
|
152
|
+
* GET /api/custom-models/:classId/exists
|
|
153
|
+
* Check if a custom model exists for a class
|
|
154
|
+
*/
|
|
155
|
+
router.get('/:classId/exists', async (req, res) => {
|
|
156
|
+
try {
|
|
157
|
+
const { classId } = req.params;
|
|
158
|
+
if (!classId) {
|
|
159
|
+
res.status(400).json({ error: 'Missing classId parameter' });
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
const exists = hasCustomModel(classId);
|
|
163
|
+
res.json({ exists, classId });
|
|
164
|
+
}
|
|
165
|
+
catch (err) {
|
|
166
|
+
log.error('Failed to check custom model:', err);
|
|
167
|
+
res.status(500).json({ error: err.message });
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
export default router;
|