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.
@@ -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
- subscribers.forEach((subscriber) => {
354
- if (subscriber.readyState === WebSocket.OPEN) {
355
- subscriber.send(frame)
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() {