pinokiod 7.0.3 → 7.0.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pinokiod",
3
- "version": "7.0.3",
3
+ "version": "7.0.4",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -40,7 +40,7 @@ Failure handling:
40
40
 
41
41
  Use direct `pterm` commands for control-plane operations:
42
42
 
43
- `pterm search`, `pterm status`, `pterm run`, `pterm logs`, `pterm which`, `pterm stars`, `pterm star` / `pterm unstar`, `pterm registry search`, `pterm download`
43
+ `pterm search`, `pterm status`, `pterm run`, `pterm logs`, `pterm upload`, `pterm which`, `pterm stars`, `pterm star` / `pterm unstar`, `pterm registry search`, `pterm download`
44
44
 
45
45
  Do not run update commands from this skill.
46
46
  Once a Pinokio-managed app is selected, treat `pterm` and the launcher-managed interfaces as the source of truth for lifecycle and execution. Do not switch to repo-local CLIs or bundled app binaries unless the user explicitly asks for CLI mode.
@@ -76,6 +76,8 @@ Follow these sections in order:
76
76
  - relevant apps with `ready=true`
77
77
  - otherwise relevant apps with `running=true`
78
78
  - otherwise relevant offline apps
79
+ - This runtime priority applies across all relevant candidates, not just the same app on different machines.
80
+ - If multiple different apps can satisfy the request, prefer the already-ready one over launching another offline app.
79
81
  - Within the selected runtime tier, rank by user preference:
80
82
  - exact `app_id`/title match (for explicit app requests)
81
83
  - `starred=true`
@@ -86,7 +88,7 @@ Follow these sections in order:
86
88
  - higher `score`
87
89
  - If the top candidate is not clearly better than alternatives, ask user once with top 3 candidates.
88
90
  - If a suitable installed app is found, select it and continue to Run App.
89
- - Federated search may return apps from other Pinokio machines on the LAN:
91
+ - Search results may include apps from other reachable Pinokio machines:
90
92
  - remote results use `app_id` in the form `<app_id>@<source.host>`
91
93
  - `source.local=false` means the result is from another machine
92
94
  - treat remote results as separate apps; do not merge them with the local app of the same name
@@ -106,33 +108,39 @@ Follow these sections in order:
106
108
 
107
109
  ### 3. Run App
108
110
 
109
- - Once you have a selected local app, use `pterm status`.
111
+ - Once you have a selected app, use `pterm status`.
110
112
  - Poll every 2s.
111
113
  - Use status fields from pterm output:
112
114
  - `path`: absolute app path to use with `pterm run`
113
115
  - `running`: script is running
114
116
  - `ready`: app is reachable/ready
115
117
  - `ready_url`: default base URL for API calls when available
116
- - `external_ready_urls`: optional ordered non-loopback app URLs for caller-side access; use them only when `ready_url` fails due to loopback restrictions
118
+ - `external_ready_urls`: optional ordered non-loopback app URLs for caller-side access; use them only when `ready_url` is missing or unusable due to loopback restrictions
117
119
  - `state`: `offline | starting | online`
118
- - `source`: machine identity for federated results
120
+ - `source`: machine identity for results from other reachable Pinokio machines
119
121
  - Use `--probe` only for readiness confirmation before first API call (or when status is uncertain).
120
122
  - Use `--timeout=<ms>` only when you need a non-default probe timeout.
121
123
  - Treat `offline` as expected before first run.
122
124
  - If `app_id` contains `@<host>` or `source.local=false`, the app is remote:
123
125
  - treat `path` and `ready_url` as source-local fields, not caller-usable local paths/URLs
124
126
  - use `external_ready_urls` in order for caller-side API access when available
125
- - do not assume `pterm run <path>` can launch a remote app from the current machine
127
+ - use `pterm run <app_id>` for remote launch; do not use a remote machine's `path` value as a local path
128
+ - for remote path-based tasks:
129
+ - this applies only when the task expects filesystem paths such as `/path/to/file`
130
+ - do not pass local paths from this machine to the remote app
131
+ - first run `pterm upload <app_id> <file...>`
132
+ - then use the returned remote `path` values
126
133
  - If app is offline or not ready, run it:
127
- - Run `pterm run <app_path>`.
134
+ - For remote apps, run `pterm run <app_id>`.
135
+ - Otherwise run `pterm run <app_path>`.
128
136
  - If the launcher has no explicit default item or the launch action depends on current menu state, infer one or more ordered selectors from the launcher's current menu and pass them via repeated `--default`.
129
137
  - Prefer stable launcher selectors such as `run.js?mode=Default`, then broader fallbacks like `run.js`, then installation fallback like `install.js`.
130
138
  - Continue polling with `pterm status <app_id>`.
131
139
  - Default startup timeout: 180s.
132
140
  - Success criteria:
133
141
  - `state=online` and `ready=true`
134
- - use `ready_url` by default
135
- - if `ready_url` fails because the client cannot access loopback and `external_ready_urls` exists, try those URLs in order
142
+ - use `ready_url` by default when it exists and is caller-usable
143
+ - if `ready_url` is missing, or it fails because the client cannot access loopback, and `external_ready_urls` exists, try those URLs in order
136
144
  - missing `external_ready_urls` is normal; it usually means network sharing is off
137
145
  - Failure criteria:
138
146
  - timeout before success
@@ -1,8 +1,15 @@
1
1
  const express = require('express')
2
+ const fs = require('fs')
3
+ const path = require('path')
4
+ const crypto = require('crypto')
2
5
  const axios = require('axios')
6
+ const multer = require('multer')
7
+ const FormData = require('form-data')
8
+ const sanitize = require('sanitize-filename')
3
9
 
4
10
  const DEFAULT_PEER_PORT = 42000
5
11
  const DEFAULT_PEER_TIMEOUT_MS = 2500
12
+ const DEFAULT_PEER_UPLOAD_TIMEOUT_MS = 30000
6
13
  const IPV4_HOST_PATTERN = /^(?:\d{1,3}\.){3}\d{1,3}$/
7
14
 
8
15
  const isQualifiedHost = (value = '') => {
@@ -55,6 +62,7 @@ module.exports = function registerAppRoutes(app, { registry, preferences, appSea
55
62
  }
56
63
 
57
64
  const router = express.Router()
65
+ const upload = multer()
58
66
 
59
67
  const asyncHandler = (fn) => (req, res, next) => {
60
68
  Promise.resolve(fn(req, res, next)).catch(next)
@@ -189,6 +197,60 @@ module.exports = function registerAppRoutes(app, { registry, preferences, appSea
189
197
  }
190
198
  return Object.keys(info).filter((host) => host && host !== localHost)
191
199
  }
200
+ const uniqueUploadPath = async (directory, originalName) => {
201
+ const parsed = path.parse(originalName)
202
+ const baseName = parsed.name || 'upload'
203
+ const ext = parsed.ext || ''
204
+ let counter = 0
205
+ while (true) {
206
+ const candidateName = counter === 0
207
+ ? `${baseName}${ext}`
208
+ : `${baseName}-${counter}${ext}`
209
+ const candidatePath = path.join(directory, candidateName)
210
+ try {
211
+ await fs.promises.access(candidatePath)
212
+ counter += 1
213
+ } catch (_) {
214
+ return {
215
+ name: candidateName,
216
+ path: candidatePath
217
+ }
218
+ }
219
+ }
220
+ }
221
+ const storeAppUploads = async (appPath, files = []) => {
222
+ const token = crypto.randomBytes(16).toString('hex')
223
+ const uploadDir = path.join(appPath, '.pinokio-temp', 'uploads', token)
224
+ await fs.promises.mkdir(uploadDir, { recursive: true })
225
+ const stored = []
226
+ for (const file of files) {
227
+ if (!file || !file.buffer) {
228
+ continue
229
+ }
230
+ const originalName = path.basename(typeof file.originalname === 'string' && file.originalname.trim()
231
+ ? file.originalname.trim()
232
+ : 'upload')
233
+ const safeName = sanitize(originalName) || `upload-${Date.now()}`
234
+ const target = await uniqueUploadPath(uploadDir, safeName)
235
+ await fs.promises.writeFile(target.path, file.buffer)
236
+ stored.push({
237
+ name: originalName,
238
+ path: target.path,
239
+ size: typeof file.size === 'number' ? file.size : file.buffer.length,
240
+ mimeType: typeof file.mimetype === 'string' ? file.mimetype : ''
241
+ })
242
+ }
243
+ return {
244
+ token,
245
+ files: stored
246
+ }
247
+ }
248
+ const decorateUploadResult = (uploadResult, source, appId) => {
249
+ const next = uploadResult && typeof uploadResult === 'object' ? { ...uploadResult } : {}
250
+ next.app_id = qualifyAppId(appId || next.app_id || '', source.host)
251
+ next.source = source
252
+ return next
253
+ }
192
254
  const mergeSearchApps = (localApps, remoteApps, query = '') => {
193
255
  const merged = []
194
256
  const seen = new Set()
@@ -393,6 +455,72 @@ module.exports = function registerAppRoutes(app, { registry, preferences, appSea
393
455
  })
394
456
  }))
395
457
 
458
+ router.post('/apps/:app_id/upload', upload.any(), asyncHandler(async (req, res) => {
459
+ const parsedAppId = parseQualifiedAppId(req.params.app_id)
460
+ const requestedAppId = parsedAppId.app_id || req.params.app_id
461
+ const remoteHost = parsedAppId.qualified ? parsedAppId.host : null
462
+ const files = Array.isArray(req.files) ? req.files : []
463
+ if (files.length === 0) {
464
+ res.status(400).json({ error: 'No files provided' })
465
+ return
466
+ }
467
+ if (remoteHost && remoteHost !== currentPeerHost()) {
468
+ try {
469
+ const form = new FormData()
470
+ for (const file of files) {
471
+ if (!file || !file.buffer) {
472
+ continue
473
+ }
474
+ form.append('files', file.buffer, {
475
+ filename: path.basename(file.originalname || 'upload'),
476
+ contentType: file.mimetype || 'application/octet-stream',
477
+ knownLength: typeof file.size === 'number' ? file.size : file.buffer.length
478
+ })
479
+ }
480
+ const response = await axios.post(`http://${remoteHost}:${peerPort()}/apps/${encodeURIComponent(requestedAppId)}/upload`, form, {
481
+ timeout: DEFAULT_PEER_UPLOAD_TIMEOUT_MS,
482
+ maxBodyLength: Infinity,
483
+ maxContentLength: Infinity,
484
+ headers: {
485
+ ...peerRequestHeaders(req),
486
+ ...form.getHeaders()
487
+ }
488
+ })
489
+ res.json(decorateUploadResult(response.data, buildSource(remoteHost, false), requestedAppId))
490
+ return
491
+ } catch (error) {
492
+ if (error && error.response) {
493
+ res.status(error.response.status).json(error.response.data)
494
+ return
495
+ }
496
+ res.status(502).json({
497
+ error: 'Peer upload unavailable',
498
+ app_id: qualifyAppId(requestedAppId, remoteHost),
499
+ source: buildSource(remoteHost, false)
500
+ })
501
+ return
502
+ }
503
+ }
504
+ const appId = registry.normalizeAppId(requestedAppId)
505
+ if (!appId) {
506
+ res.status(400).json({ error: 'Invalid app_id' })
507
+ return
508
+ }
509
+ const status = await registry.buildAppStatus(appId, {
510
+ source: req.$source || null
511
+ })
512
+ if (!status || !status.path) {
513
+ res.status(404).json({ error: 'App not found', app_id: appId })
514
+ return
515
+ }
516
+ const payload = await storeAppUploads(status.path, files)
517
+ if (!Array.isArray(payload.files) || payload.files.length === 0) {
518
+ res.status(400).json({ error: 'No valid files provided', app_id: appId })
519
+ return
520
+ }
521
+ res.json(decorateUploadResult(payload, buildSource(currentPeerHost(), true), appId))
522
+ }))
523
+
396
524
  router.get('/apps/status/:app_id', asyncHandler(async (req, res) => {
397
525
  const parsedAppId = parseQualifiedAppId(req.params.app_id)
398
526
  const requestedAppId = parsedAppId.app_id || req.params.app_id