pinokiod 3.170.0 → 3.181.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/kernel/favicon.js +91 -34
- package/kernel/peer.js +73 -0
- package/kernel/util.js +13 -2
- package/package.json +1 -1
- package/server/index.js +249 -26
- package/server/public/common.js +244 -0
- package/server/public/files-app/app.css +73 -1
- package/server/public/files-app/app.js +255 -2
- package/server/public/layout.js +115 -1
- package/server/public/nav.js +227 -64
- package/server/public/style.css +27 -3
- package/server/public/tab-idle-notifier.js +3 -0
- package/server/routes/files.js +96 -0
- package/server/socket.js +71 -4
- package/server/views/app.ejs +603 -53
- package/server/views/connect.ejs +9 -0
- package/server/views/file_browser.ejs +9 -2
- package/server/views/index.ejs +11 -2
- package/server/views/init/index.ejs +9 -2
- package/server/views/layout.ejs +7 -5
- package/server/views/net.ejs +9 -0
- package/server/views/network.ejs +9 -0
- package/server/views/review.ejs +4 -3
- package/server/views/screenshots.ejs +9 -0
- package/server/views/settings.ejs +9 -0
- package/server/views/terminals.ejs +12 -3
- package/server/views/tools.ejs +10 -1
package/server/routes/files.js
CHANGED
|
@@ -280,5 +280,101 @@ module.exports = function registerFileRoutes(app, { kernel, getTheme, exists })
|
|
|
280
280
|
});
|
|
281
281
|
}));
|
|
282
282
|
|
|
283
|
+
router.post('/api/files/delete', asyncHandler(async (req, res) => {
|
|
284
|
+
const { workspace, path: relativePath, root: rootParam } = req.body || {};
|
|
285
|
+
if (typeof relativePath !== 'string') {
|
|
286
|
+
throw createHttpError(400, 'Path must be provided');
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const resolved = await resolveWorkspacePath(workspace, relativePath, rootParam);
|
|
290
|
+
const { absolutePath, relativePosix, workspaceSlug } = resolved;
|
|
291
|
+
|
|
292
|
+
if (!relativePosix) {
|
|
293
|
+
throw createHttpError(400, 'Cannot delete workspace root');
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const stats = await fs.promises.stat(absolutePath).catch(() => null);
|
|
297
|
+
if (!stats) {
|
|
298
|
+
throw createHttpError(404, 'Path not found');
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
let removedType;
|
|
302
|
+
if (stats.isDirectory()) {
|
|
303
|
+
await fs.promises.rm(absolutePath, { recursive: true, force: true });
|
|
304
|
+
removedType = 'directory';
|
|
305
|
+
} else if (stats.isFile()) {
|
|
306
|
+
await fs.promises.unlink(absolutePath);
|
|
307
|
+
removedType = 'file';
|
|
308
|
+
} else {
|
|
309
|
+
throw createHttpError(400, 'Unsupported file type');
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
res.json({
|
|
313
|
+
workspace: workspaceSlug,
|
|
314
|
+
path: relativePosix,
|
|
315
|
+
type: removedType,
|
|
316
|
+
success: true,
|
|
317
|
+
});
|
|
318
|
+
}));
|
|
319
|
+
|
|
320
|
+
router.post('/api/files/rename', asyncHandler(async (req, res) => {
|
|
321
|
+
const { workspace, path: relativePath, name: newName, root: rootParam } = req.body || {};
|
|
322
|
+
if (typeof relativePath !== 'string' || relativePath.length === 0) {
|
|
323
|
+
throw createHttpError(400, 'Path must be provided');
|
|
324
|
+
}
|
|
325
|
+
if (typeof newName !== 'string' || newName.trim().length === 0) {
|
|
326
|
+
throw createHttpError(400, 'New name must be provided');
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const sanitizedName = newName.trim();
|
|
330
|
+
if (sanitizedName.includes('/') || sanitizedName.includes('\\')) {
|
|
331
|
+
throw createHttpError(400, 'Name cannot contain path separators');
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const resolved = await resolveWorkspacePath(workspace, relativePath, rootParam);
|
|
335
|
+
const { absolutePath, relativePosix, workspaceSlug, workspaceRoot } = resolved;
|
|
336
|
+
|
|
337
|
+
if (!relativePosix) {
|
|
338
|
+
throw createHttpError(400, 'Cannot rename workspace root');
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const sourceStats = await fs.promises.stat(absolutePath).catch(() => null);
|
|
342
|
+
if (!sourceStats) {
|
|
343
|
+
throw createHttpError(404, 'Source path not found');
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const parentSegments = resolved.relativeSegments.slice(0, -1);
|
|
347
|
+
const sourceName = resolved.relativeSegments[resolved.relativeSegments.length - 1];
|
|
348
|
+
if (sourceName === sanitizedName) {
|
|
349
|
+
res.json({
|
|
350
|
+
workspace: workspaceSlug,
|
|
351
|
+
path: relativePosix,
|
|
352
|
+
target: relativePosix,
|
|
353
|
+
success: true,
|
|
354
|
+
unchanged: true,
|
|
355
|
+
});
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const targetAbsolute = path.resolve(workspaceRoot, ...parentSegments, sanitizedName);
|
|
360
|
+
const relativeTargetSegments = sanitizeSegments([...parentSegments, sanitizedName].join('/'));
|
|
361
|
+
const relativeTarget = relativeTargetSegments.join('/');
|
|
362
|
+
const collision = await fs.promises.stat(targetAbsolute).catch(() => null);
|
|
363
|
+
if (collision) {
|
|
364
|
+
throw createHttpError(409, 'A file or folder with that name already exists');
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
await fs.promises.rename(absolutePath, targetAbsolute);
|
|
368
|
+
|
|
369
|
+
const targetStats = await fs.promises.stat(targetAbsolute);
|
|
370
|
+
res.json({
|
|
371
|
+
workspace: workspaceSlug,
|
|
372
|
+
path: relativePosix,
|
|
373
|
+
target: relativeTarget,
|
|
374
|
+
type: targetStats.isDirectory() ? 'directory' : targetStats.isFile() ? 'file' : 'other',
|
|
375
|
+
success: true,
|
|
376
|
+
});
|
|
377
|
+
}));
|
|
378
|
+
|
|
283
379
|
app.use(router);
|
|
284
380
|
};
|
package/server/socket.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const querystring = require("querystring");
|
|
2
2
|
const WebSocket = require('ws');
|
|
3
3
|
const path = require('path')
|
|
4
|
+
const os = require('os')
|
|
4
5
|
const Util = require("../kernel/util")
|
|
5
6
|
const Environment = require("../kernel/environment")
|
|
6
7
|
const NOTIFICATION_CHANNEL = 'kernel.notifications'
|
|
@@ -15,6 +16,20 @@ class Socket {
|
|
|
15
16
|
this.server = parent.server
|
|
16
17
|
// this.kernel = parent.kernel
|
|
17
18
|
const wss = new WebSocket.Server({ server: this.parent.server })
|
|
19
|
+
this.localDeviceIds = new Set()
|
|
20
|
+
this.localAddresses = new Set()
|
|
21
|
+
try {
|
|
22
|
+
const ifaces = os.networkInterfaces() || {}
|
|
23
|
+
Object.values(ifaces).forEach((arr) => {
|
|
24
|
+
(arr || []).forEach((info) => {
|
|
25
|
+
if (info && info.address) {
|
|
26
|
+
this.localAddresses.add(info.address)
|
|
27
|
+
}
|
|
28
|
+
})
|
|
29
|
+
})
|
|
30
|
+
this.localAddresses.add('127.0.0.1')
|
|
31
|
+
this.localAddresses.add('::1')
|
|
32
|
+
} catch (_) {}
|
|
18
33
|
this.subscriptions = new Map(); // Initialize a Map to store the WebSocket connections interested in each event
|
|
19
34
|
this.notificationChannel = NOTIFICATION_CHANNEL
|
|
20
35
|
this.notificationBridgeDispose = null
|
|
@@ -36,6 +51,12 @@ class Socket {
|
|
|
36
51
|
this.subscriptions.delete(eventName);
|
|
37
52
|
}
|
|
38
53
|
});
|
|
54
|
+
// Cleanup device tracking
|
|
55
|
+
try {
|
|
56
|
+
if (ws._isLocalClient && ws._deviceId) {
|
|
57
|
+
this.localDeviceIds.delete(ws._deviceId)
|
|
58
|
+
}
|
|
59
|
+
} catch (_) {}
|
|
39
60
|
this.checkNotificationBridge();
|
|
40
61
|
});
|
|
41
62
|
ws.on('message', async (message, isBinary) => {
|
|
@@ -171,6 +192,25 @@ class Socket {
|
|
|
171
192
|
this.parent.kernel.api.process(req)
|
|
172
193
|
}
|
|
173
194
|
} else {
|
|
195
|
+
if (req.method === this.notificationChannel) {
|
|
196
|
+
if (typeof req.device_id === 'string' && req.device_id.trim()) {
|
|
197
|
+
ws._deviceId = req.device_id.trim()
|
|
198
|
+
}
|
|
199
|
+
// Mark local client sockets by IP matching any local address
|
|
200
|
+
try {
|
|
201
|
+
const ip = ws._ip || ''
|
|
202
|
+
const isLocal = (addr) => {
|
|
203
|
+
if (!addr || typeof addr !== 'string') return false
|
|
204
|
+
if (this.localAddresses.has(addr)) return true
|
|
205
|
+
const v = addr.trim().toLowerCase()
|
|
206
|
+
return v.startsWith('::ffff:127.') || v.startsWith('127.')
|
|
207
|
+
}
|
|
208
|
+
ws._isLocalClient = isLocal(ip)
|
|
209
|
+
if (ws._isLocalClient && ws._deviceId) {
|
|
210
|
+
this.localDeviceIds.add(ws._deviceId)
|
|
211
|
+
}
|
|
212
|
+
} catch (_) {}
|
|
213
|
+
}
|
|
174
214
|
this.subscribe(ws, req.method)
|
|
175
215
|
if (req.mode !== "listen") {
|
|
176
216
|
this.parent.kernel.api.process(req)
|
|
@@ -350,11 +390,38 @@ class Socket {
|
|
|
350
390
|
data: payload,
|
|
351
391
|
}
|
|
352
392
|
const frame = JSON.stringify(envelope)
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
393
|
+
const targetId = (payload && typeof payload.device_id === 'string' && payload.device_id.trim()) ? payload.device_id.trim() : null
|
|
394
|
+
const audience = (payload && typeof payload.audience === 'string' && payload.audience.trim()) ? payload.audience.trim() : null
|
|
395
|
+
if (audience === 'device' && targetId) {
|
|
396
|
+
let delivered = false
|
|
397
|
+
subscribers.forEach((subscriber) => {
|
|
398
|
+
if (subscriber.readyState !== WebSocket.OPEN) {
|
|
399
|
+
return
|
|
400
|
+
}
|
|
401
|
+
if (subscriber._deviceId && subscriber._deviceId === targetId) {
|
|
402
|
+
try { subscriber.send(frame); delivered = true } catch (_) {}
|
|
403
|
+
}
|
|
404
|
+
})
|
|
405
|
+
if (!delivered) {
|
|
406
|
+
// Fallback: broadcast if no matching device subscriber is available
|
|
407
|
+
subscribers.forEach((subscriber) => {
|
|
408
|
+
if (subscriber.readyState === WebSocket.OPEN) {
|
|
409
|
+
try { subscriber.send(frame) } catch (_) {}
|
|
410
|
+
}
|
|
411
|
+
})
|
|
356
412
|
}
|
|
357
|
-
}
|
|
413
|
+
} else {
|
|
414
|
+
subscribers.forEach((subscriber) => {
|
|
415
|
+
if (subscriber.readyState === WebSocket.OPEN) {
|
|
416
|
+
try { subscriber.send(frame) } catch (_) {}
|
|
417
|
+
}
|
|
418
|
+
})
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
isLocalDevice(deviceId) {
|
|
423
|
+
if (!deviceId || typeof deviceId !== 'string') return false
|
|
424
|
+
return this.localDeviceIds.has(deviceId)
|
|
358
425
|
}
|
|
359
426
|
|
|
360
427
|
ensureNotificationBridge() {
|