pinokiod 7.2.7 → 7.2.9

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.
@@ -0,0 +1,390 @@
1
+ const path = require("path");
2
+
3
+ function createWorkspaceRuntimeService({ kernel }) {
4
+ if (!kernel) {
5
+ throw new Error("kernel is required");
6
+ }
7
+
8
+ const normalizePathKey = (value) => {
9
+ if (typeof value !== "string" || !value.trim()) {
10
+ return "";
11
+ }
12
+ const resolved = path.resolve(value.trim());
13
+ return process.platform === "win32" ? resolved.toLowerCase() : resolved;
14
+ };
15
+
16
+ const decodeMaybe = (value) => {
17
+ const raw = typeof value === "string" ? value.trim() : "";
18
+ if (!raw) {
19
+ return "";
20
+ }
21
+ try {
22
+ return decodeURIComponent(raw).trim();
23
+ } catch (_) {
24
+ return raw;
25
+ }
26
+ };
27
+
28
+ const resolveCandidatePath = (value) => {
29
+ const decoded = decodeMaybe(value);
30
+ if (!decoded || decoded.includes("\0")) {
31
+ return "";
32
+ }
33
+ if (decoded.startsWith("~/")) {
34
+ return path.resolve(kernel.homedir, decoded.slice(2));
35
+ }
36
+ if (path.isAbsolute(decoded)) {
37
+ return path.resolve(decoded);
38
+ }
39
+ return "";
40
+ };
41
+
42
+ const knownRoots = () => {
43
+ const roots = [];
44
+ const addRoot = (type, label) => {
45
+ if (!kernel || typeof kernel.path !== "function") {
46
+ return;
47
+ }
48
+ const rootPath = kernel.path(type);
49
+ if (typeof rootPath === "string" && rootPath.trim()) {
50
+ roots.push({
51
+ type,
52
+ label,
53
+ path: path.resolve(rootPath)
54
+ });
55
+ }
56
+ };
57
+ addRoot("workspaces", "Workspace");
58
+ addRoot("api", "App");
59
+ addRoot("plugin", "Plugin");
60
+ return roots;
61
+ };
62
+
63
+ const isPathWithin = (candidate, root) => {
64
+ if (!candidate || !root) {
65
+ return false;
66
+ }
67
+ const relative = path.relative(root, candidate);
68
+ return Boolean(relative && !relative.startsWith("..") && !path.isAbsolute(relative));
69
+ };
70
+
71
+ const resolveWorkspaceForPath = (candidatePath) => {
72
+ const candidate = resolveCandidatePath(candidatePath);
73
+ if (!candidate) {
74
+ return null;
75
+ }
76
+ for (const root of knownRoots()) {
77
+ if (!isPathWithin(candidate, root.path)) {
78
+ continue;
79
+ }
80
+ const relative = path.relative(root.path, candidate);
81
+ const segments = relative.split(path.sep).filter(Boolean);
82
+ const name = segments[0] || "";
83
+ if (!name) {
84
+ continue;
85
+ }
86
+ const cwd = path.resolve(root.path, name);
87
+ return {
88
+ key: normalizePathKey(cwd),
89
+ cwd,
90
+ name,
91
+ root: root.type,
92
+ rootLabel: root.label
93
+ };
94
+ }
95
+ return null;
96
+ };
97
+
98
+ const parseParamsFromText = (value) => {
99
+ const raw = typeof value === "string" ? value : "";
100
+ const index = raw.indexOf("?");
101
+ if (index < 0) {
102
+ return null;
103
+ }
104
+ try {
105
+ return new URLSearchParams(raw.slice(index + 1).replace(/&amp;/g, "&"));
106
+ } catch (_) {
107
+ return null;
108
+ }
109
+ };
110
+
111
+ const firstWorkspaceFromCandidates = (candidates) => {
112
+ for (const candidate of candidates) {
113
+ const workspace = resolveWorkspaceForPath(candidate);
114
+ if (workspace) {
115
+ return workspace;
116
+ }
117
+ }
118
+ return null;
119
+ };
120
+
121
+ const getShellCandidates = (shell) => {
122
+ const candidates = [];
123
+ if (!shell || typeof shell !== "object") {
124
+ return candidates;
125
+ }
126
+ const push = (value) => {
127
+ if (typeof value === "string" && value.trim()) {
128
+ candidates.push(value);
129
+ }
130
+ };
131
+ push(shell.path);
132
+ push(shell.group);
133
+ if (shell.params && typeof shell.params === "object") {
134
+ push(shell.params.cwd);
135
+ push(shell.params.path);
136
+ if (shell.params.$parent && typeof shell.params.$parent === "object") {
137
+ push(shell.params.$parent.cwd);
138
+ push(shell.params.$parent.path);
139
+ }
140
+ }
141
+ for (const text of [shell.id, shell.group]) {
142
+ const params = parseParamsFromText(text);
143
+ if (!params) {
144
+ continue;
145
+ }
146
+ push(params.get("cwd"));
147
+ push(params.get("path"));
148
+ }
149
+ return candidates;
150
+ };
151
+
152
+ const getScriptCandidates = (id) => {
153
+ const candidates = [];
154
+ const raw = typeof id === "string" ? id : "";
155
+ if (!raw) {
156
+ return candidates;
157
+ }
158
+ const params = parseParamsFromText(raw);
159
+ if (params) {
160
+ candidates.push(params.get("cwd"));
161
+ candidates.push(params.get("path"));
162
+ }
163
+ const pathPart = raw.split("?")[0];
164
+ candidates.push(pathPart);
165
+ return candidates;
166
+ };
167
+
168
+ const parseTerminalIdFromText = (value) => {
169
+ const params = parseParamsFromText(value);
170
+ if (!params) {
171
+ return "";
172
+ }
173
+ const terminalId = params.get("terminal_id");
174
+ return typeof terminalId === "string" ? terminalId.trim() : "";
175
+ };
176
+
177
+ const buildShellUrl = (shell) => {
178
+ const raw = shell && typeof shell.id === "string" ? shell.id.trim() : "";
179
+ if (!raw) {
180
+ return "";
181
+ }
182
+ const index = raw.indexOf("?");
183
+ const base = index >= 0 ? raw.slice(0, index) : raw;
184
+ const query = index >= 0 ? raw.slice(index + 1) : "";
185
+ const params = new URLSearchParams(query);
186
+ if (!params.has("path") && shell && typeof shell.path === "string" && shell.path.trim()) {
187
+ params.set("path", shell.path.trim());
188
+ }
189
+ if (!params.has("terminal_id") && shell && typeof shell.terminal_id === "string" && shell.terminal_id.trim()) {
190
+ params.set("terminal_id", shell.terminal_id.trim());
191
+ }
192
+ if (!params.has("input")) {
193
+ params.set("input", "1");
194
+ }
195
+ const queryString = params.toString();
196
+ let route = "";
197
+ if (base.startsWith("/shell/")) {
198
+ const shellId = base.slice("/shell/".length);
199
+ route = shellId.includes("/")
200
+ ? `/shell/${encodeURIComponent(shellId)}`
201
+ : base;
202
+ } else if (base.startsWith("shell/")) {
203
+ route = `/shell/${encodeURIComponent(base.slice("shell/".length))}`;
204
+ } else {
205
+ route = `/shell/${encodeURIComponent(base)}`;
206
+ }
207
+ return queryString ? `${route}?${queryString}` : route;
208
+ };
209
+
210
+ const buildRunUrl = (id) => {
211
+ const raw = typeof id === "string" ? id.trim() : "";
212
+ if (!raw) {
213
+ return "";
214
+ }
215
+ const index = raw.indexOf("?");
216
+ const scriptPath = resolveCandidatePath(index >= 0 ? raw.slice(0, index) : raw);
217
+ const query = index >= 0 ? raw.slice(index + 1) : "";
218
+ if (!scriptPath) {
219
+ return "";
220
+ }
221
+ const roots = [
222
+ { name: "api", path: kernel.path("api") },
223
+ { name: "plugin", path: kernel.path("plugin") },
224
+ { name: "scripts", path: kernel.path("scripts") }
225
+ ].filter((root) => typeof root.path === "string" && root.path.trim());
226
+ for (const root of roots) {
227
+ const rootPath = path.resolve(root.path);
228
+ const relative = path.relative(rootPath, scriptPath);
229
+ if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
230
+ continue;
231
+ }
232
+ const params = new URLSearchParams(query);
233
+ if (!params.has("chrome")) {
234
+ params.set("chrome", "full");
235
+ }
236
+ const route = relative.split(path.sep).map(encodeURIComponent).join("/");
237
+ const queryString = params.toString();
238
+ return `/run/${root.name}/${route}${queryString ? `?${queryString}` : ""}`;
239
+ }
240
+ return "";
241
+ };
242
+
243
+ const shellTitle = (shell) => {
244
+ if (shell && shell.params && typeof shell.params.$title === "string" && shell.params.$title.trim()) {
245
+ return shell.params.$title.trim();
246
+ }
247
+ if (shell && typeof shell.cmd === "string" && shell.cmd.trim()) {
248
+ return shell.cmd.trim().slice(0, 120);
249
+ }
250
+ return "Terminal";
251
+ };
252
+
253
+ const scriptTitle = (id) => {
254
+ const raw = typeof id === "string" ? id.trim() : "";
255
+ if (!raw) {
256
+ return "Script";
257
+ }
258
+ const pathPart = raw.split("?")[0];
259
+ if (pathPart && path.isAbsolute(pathPart)) {
260
+ return path.basename(pathPart) || "Script";
261
+ }
262
+ return raw.slice(0, 120);
263
+ };
264
+
265
+ const createGroup = (workspace) => ({
266
+ cwd: workspace.cwd,
267
+ name: workspace.name,
268
+ root: workspace.root,
269
+ rootLabel: workspace.rootLabel,
270
+ running: true,
271
+ shells: [],
272
+ scripts: []
273
+ });
274
+
275
+ const list = () => {
276
+ const groups = new Map();
277
+ const unscoped = {
278
+ shells: [],
279
+ scripts: []
280
+ };
281
+ const getGroup = (workspace) => {
282
+ if (!workspace || !workspace.key) {
283
+ return null;
284
+ }
285
+ if (!groups.has(workspace.key)) {
286
+ groups.set(workspace.key, createGroup(workspace));
287
+ }
288
+ return groups.get(workspace.key);
289
+ };
290
+
291
+ const shells = kernel.shell && Array.isArray(kernel.shell.shells)
292
+ ? kernel.shell.shells
293
+ : [];
294
+ for (const shell of shells) {
295
+ if (!shell || shell.done === true || !shell.ptyProcess) {
296
+ continue;
297
+ }
298
+ const item = {
299
+ id: typeof shell.id === "string" ? shell.id : "",
300
+ group: typeof shell.group === "string" ? shell.group : "",
301
+ title: shellTitle(shell),
302
+ cwd: typeof shell.path === "string" ? shell.path : "",
303
+ state: shell.state || null,
304
+ start_time: Number.isFinite(shell.start_time) ? shell.start_time : null,
305
+ terminal_id: shell.terminal_id || parseTerminalIdFromText(shell.id) || parseTerminalIdFromText(shell.group) || null,
306
+ url: buildRunUrl(shell.group) || buildShellUrl(shell)
307
+ };
308
+ const workspace = firstWorkspaceFromCandidates(getShellCandidates(shell));
309
+ const group = getGroup(workspace);
310
+ if (group) {
311
+ group.shells.push(item);
312
+ } else {
313
+ unscoped.shells.push(item);
314
+ }
315
+ }
316
+
317
+ const running = kernel.api && kernel.api.running && typeof kernel.api.running === "object"
318
+ ? kernel.api.running
319
+ : {};
320
+ for (const id of Object.keys(running)) {
321
+ if (typeof id !== "string" || !id || id.startsWith("shell/")) {
322
+ continue;
323
+ }
324
+ const item = {
325
+ id,
326
+ title: scriptTitle(id),
327
+ path: id.split("?")[0],
328
+ cwd: "",
329
+ url: buildRunUrl(id)
330
+ };
331
+ const workspace = firstWorkspaceFromCandidates(getScriptCandidates(id));
332
+ if (workspace) {
333
+ item.cwd = workspace.cwd;
334
+ }
335
+ const group = getGroup(workspace);
336
+ if (group) {
337
+ group.scripts.push(item);
338
+ } else {
339
+ unscoped.scripts.push(item);
340
+ }
341
+ }
342
+
343
+ const workspaces = Array.from(groups.values())
344
+ .map((workspace) => ({
345
+ ...workspace,
346
+ counts: {
347
+ shells: workspace.shells.length,
348
+ scripts: workspace.scripts.length
349
+ }
350
+ }))
351
+ .sort((a, b) => {
352
+ const totalA = a.counts.shells + a.counts.scripts;
353
+ const totalB = b.counts.shells + b.counts.scripts;
354
+ if (totalA !== totalB) {
355
+ return totalB - totalA;
356
+ }
357
+ return String(a.name || "").localeCompare(String(b.name || ""));
358
+ });
359
+
360
+ return {
361
+ workspaces,
362
+ unscoped,
363
+ counts: {
364
+ workspaces: workspaces.length,
365
+ shells: workspaces.reduce((total, workspace) => total + workspace.counts.shells, 0) + unscoped.shells.length,
366
+ scripts: workspaces.reduce((total, workspace) => total + workspace.counts.scripts, 0) + unscoped.scripts.length
367
+ }
368
+ };
369
+ };
370
+
371
+ const summary = () => {
372
+ const runtime = list();
373
+ return {
374
+ runningWorkspaces: runtime.counts.workspaces,
375
+ runningShells: runtime.counts.shells,
376
+ runningScripts: runtime.counts.scripts,
377
+ unscopedShells: runtime.unscoped.shells.length,
378
+ unscopedScripts: runtime.unscoped.scripts.length
379
+ };
380
+ };
381
+
382
+ return {
383
+ list,
384
+ summary
385
+ };
386
+ }
387
+
388
+ module.exports = {
389
+ createWorkspaceRuntimeService
390
+ };
@@ -2328,6 +2328,14 @@ if (typeof hotkeys === 'function') {
2328
2328
  playNextSound();
2329
2329
  };
2330
2330
 
2331
+ window.PinokioPlayNotificationSound = (sound) => {
2332
+ if (sound === false || isFalseyString(sound)) {
2333
+ return false;
2334
+ }
2335
+ enqueueSound(typeof sound === 'string' && sound ? sound : '/chime.mp3');
2336
+ return true;
2337
+ };
2338
+
2331
2339
  const handlePacket = (packet) => {
2332
2340
  if (!packet || packet.id !== CHANNEL_ID || packet.type !== 'notification') {
2333
2341
  return;
@@ -0,0 +1,44 @@
1
+ const express = require("express")
2
+
3
+ function registerWorkspacesRoutes(app, options = {}) {
4
+ const {
5
+ workspaceCatalog,
6
+ composePeerAccessPayload,
7
+ getTheme,
8
+ getPeers,
9
+ getCurrentHost,
10
+ getPortal,
11
+ } = options
12
+
13
+ if (!workspaceCatalog) {
14
+ throw new Error("workspaceCatalog is required")
15
+ }
16
+
17
+ const router = express.Router()
18
+
19
+ router.get("/workspaces", async (req, res, next) => {
20
+ try {
21
+ const catalog = await workspaceCatalog.list({ sort: req.query.sort })
22
+ res.render("workspaces", {
23
+ title: "Workspaces",
24
+ sidebarSelected: "workspaces",
25
+ workspaceCatalog: catalog,
26
+ theme: getTheme ? getTheme(req) : null,
27
+ peers: getPeers ? getPeers() : [],
28
+ currentHost: getCurrentHost ? getCurrentHost(req) : null,
29
+ portal: getPortal ? getPortal(req) : null,
30
+ peerAccess: composePeerAccessPayload ? composePeerAccessPayload(req) : null,
31
+ })
32
+ } catch (err) {
33
+ next(err)
34
+ }
35
+ })
36
+
37
+ router.get("/activity", (req, res) => {
38
+ res.redirect("/workspaces")
39
+ })
40
+
41
+ app.use(router)
42
+ }
43
+
44
+ module.exports = registerWorkspacesRoutes
package/server/socket.js CHANGED
@@ -284,7 +284,7 @@ class Socket {
284
284
  } else {
285
285
  let buf = this.buffer[id]
286
286
  let sh = this.active_shell[id]
287
- this.subscribe(ws, id, buf, sh)
287
+ this.subscribe(ws, id, buf, sh, req)
288
288
  if (req.mode !== "listen") {
289
289
  // Run only if currently not running
290
290
  if (!this.parent.kernel.api.running[id]) {
@@ -308,7 +308,7 @@ class Socket {
308
308
  if (req.id) {
309
309
  let buf = this.buffer[req.id]
310
310
  let sh = this.active_shell[req.id]
311
- this.subscribe(ws, req.id, buf, sh)
311
+ this.subscribe(ws, req.id, buf, sh, req)
312
312
  if (req.mode === "listen") {
313
313
  return
314
314
  }
@@ -345,13 +345,7 @@ class Socket {
345
345
  // Mark local client sockets by IP matching any local address
346
346
  try {
347
347
  const ip = ws._ip || ''
348
- const isLocal = (addr) => {
349
- if (!addr || typeof addr !== 'string') return false
350
- if (this.localAddresses.has(addr)) return true
351
- const v = addr.trim().toLowerCase()
352
- return v.startsWith('::ffff:127.') || v.startsWith('127.')
353
- }
354
- ws._isLocalClient = isLocal(ip)
348
+ ws._isLocalClient = this.isLocalAddress(ip)
355
349
  if (ws._isLocalClient && ws._deviceId) {
356
350
  this.localDeviceIds.add(ws._deviceId)
357
351
  }
@@ -431,18 +425,22 @@ class Socket {
431
425
  this.old_buffer = structuredClone(this.buffer)
432
426
  }, 5000)
433
427
  }
434
- subscribe(ws, id, buf, sh) {
428
+ subscribe(ws, id, buf, sh, req = {}) {
435
429
  let resolvedShellId = sh || null
436
430
  let resolvedState = buf
437
431
  let hasState = typeof resolvedState === "string" ? resolvedState.length > 0 : Boolean(resolvedState)
432
+ let resolvedShell = null
438
433
  if ((!resolvedShellId || !hasState) && this.parent && this.parent.kernel && this.parent.kernel.shell && Array.isArray(this.parent.kernel.shell.shells)) {
439
- const groupedShell = this.parent.kernel.shell.shells.find((candidate) => {
434
+ const directShell = this.parent.kernel.shell.get(id)
435
+ const liveDirectShell = directShell && directShell.done !== true ? directShell : null
436
+ const groupedShell = liveDirectShell || this.parent.kernel.shell.shells.find((candidate) => {
440
437
  return candidate
441
438
  && candidate.done !== true
442
439
  && typeof candidate.group === "string"
443
440
  && candidate.group === id
444
441
  })
445
442
  if (groupedShell) {
443
+ resolvedShell = groupedShell
446
444
  if (!resolvedShellId) {
447
445
  resolvedShellId = groupedShell.id
448
446
  }
@@ -455,6 +453,12 @@ class Socket {
455
453
  }
456
454
  }
457
455
  }
456
+ if (!resolvedShell && resolvedShellId && this.parent && this.parent.kernel && this.parent.kernel.shell) {
457
+ resolvedShell = this.parent.kernel.shell.get(resolvedShellId)
458
+ }
459
+ if (resolvedShell && req && req.input) {
460
+ resolvedShell.input = true
461
+ }
458
462
  if (this.parent.kernel.api.running[id] || resolvedShellId || hasState) {
459
463
  ws.send(JSON.stringify({
460
464
  type: "connect",
@@ -652,6 +656,13 @@ class Socket {
652
656
  return this.localDeviceIds.has(deviceId)
653
657
  }
654
658
 
659
+ isLocalAddress(addr) {
660
+ if (!addr || typeof addr !== 'string') return false
661
+ if (this.localAddresses.has(addr)) return true
662
+ const v = addr.trim().toLowerCase()
663
+ return v === 'localhost' || v === '::1' || v.startsWith('::ffff:127.') || v.startsWith('127.')
664
+ }
665
+
655
666
  ensureNotificationBridge() {
656
667
  if (this.notificationBridgeDispose) {
657
668
  return