pinokiod 7.0.5 → 7.0.8
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 +1 -1
- package/prototype/system/SKILL_PINOKIO.md +114 -38
- package/server/index.js +186 -0
- package/server/routes/apps.js +391 -8
package/package.json
CHANGED
|
@@ -50,8 +50,8 @@ Once a Pinokio-managed app is selected, treat `pterm` and the launcher-managed i
|
|
|
50
50
|
Follow these sections in order:
|
|
51
51
|
1. Use Search App first.
|
|
52
52
|
2. Only use Registry Fallback if Search App found no suitable installed app and the user approved it.
|
|
53
|
-
3. Then use
|
|
54
|
-
4. Then use
|
|
53
|
+
3. Then use Launch App.
|
|
54
|
+
4. Then use Using Apps if the app exposes an automatable API.
|
|
55
55
|
5. Only use Parallel Mode when the user explicitly asks to use multiple apps or multiple machines in parallel.
|
|
56
56
|
|
|
57
57
|
### 1. Search App
|
|
@@ -88,9 +88,10 @@ Follow these sections in order:
|
|
|
88
88
|
- more recent `last_launch_at` (if available)
|
|
89
89
|
- higher `score`
|
|
90
90
|
- If the top candidate is not clearly better than alternatives, ask user once with top 3 candidates.
|
|
91
|
-
- If a suitable installed app is found, select it and continue to
|
|
91
|
+
- If a suitable installed app is found, select it and continue to Launch App.
|
|
92
92
|
- Search results may include apps from other reachable Pinokio machines:
|
|
93
|
-
-
|
|
93
|
+
- prefer the canonical `ref` field when it exists
|
|
94
|
+
- `ref` uses the form `pinokio://<host>:<port>/<scope>/<id>`
|
|
94
95
|
- `source.local=false` means the result is from another machine
|
|
95
96
|
- treat remote results as separate apps; do not merge them with the local app of the same name
|
|
96
97
|
|
|
@@ -107,12 +108,13 @@ Follow these sections in order:
|
|
|
107
108
|
- then run the downloaded app with `pterm run <local_app_path_or_name>`
|
|
108
109
|
- Do not use `pterm run <url>` for the registry flow.
|
|
109
110
|
|
|
110
|
-
### 3.
|
|
111
|
+
### 3. Launch App
|
|
111
112
|
|
|
112
113
|
- Once you have a selected app, use `pterm status`.
|
|
113
114
|
- Poll every 2s.
|
|
114
115
|
- Use status fields from pterm output:
|
|
115
116
|
- `path`: absolute app path to use with `pterm run`
|
|
117
|
+
- `ref`: canonical Pinokio resource reference in the form `pinokio://<host>:<port>/<scope>/<id>`
|
|
116
118
|
- `running`: script is running
|
|
117
119
|
- `ready`: app is reachable/ready
|
|
118
120
|
- `ready_url`: default base URL for API calls when available
|
|
@@ -122,21 +124,21 @@ Follow these sections in order:
|
|
|
122
124
|
- Use `--probe` only for readiness confirmation before first API call (or when status is uncertain).
|
|
123
125
|
- Use `--timeout=<ms>` only when you need a non-default probe timeout.
|
|
124
126
|
- Treat `offline` as expected before first run.
|
|
125
|
-
- If `
|
|
127
|
+
- If `ref` points to another machine or `source.local=false`, the app is remote:
|
|
126
128
|
- treat `path` and `ready_url` as source-local fields, not caller-usable local paths/URLs
|
|
127
129
|
- use `external_ready_urls` in order for caller-side API access when available
|
|
128
|
-
- use `pterm run <
|
|
130
|
+
- use `pterm run <ref>` for remote launch; do not use a remote machine's `path` value as a local path
|
|
129
131
|
- for remote path-based tasks:
|
|
130
132
|
- this applies only when the task expects filesystem paths such as `/path/to/file`
|
|
131
133
|
- do not pass local paths from this machine to the remote app
|
|
132
|
-
- first run `pterm upload <
|
|
134
|
+
- first run `pterm upload <ref> <file...>`
|
|
133
135
|
- then use the returned remote `path` values
|
|
134
136
|
- If app is offline or not ready, run it:
|
|
135
|
-
-
|
|
137
|
+
- If `ref` exists, run `pterm run <ref>`.
|
|
136
138
|
- Otherwise run `pterm run <app_path>`.
|
|
137
139
|
- 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`.
|
|
138
140
|
- Prefer stable launcher selectors such as `run.js?mode=Default`, then broader fallbacks like `run.js`, then installation fallback like `install.js`.
|
|
139
|
-
- Continue polling with `pterm status <app_id>`.
|
|
141
|
+
- Continue polling with `pterm status <ref>` when `ref` exists, otherwise `pterm status <app_id>`.
|
|
140
142
|
- Default startup timeout: 180s.
|
|
141
143
|
- Success criteria:
|
|
142
144
|
- `state=online` and `ready=true`
|
|
@@ -147,30 +149,92 @@ Follow these sections in order:
|
|
|
147
149
|
- timeout before success
|
|
148
150
|
- app drops back to `offline` during startup after a run attempt
|
|
149
151
|
- `pterm run` terminates and status never reaches ready
|
|
150
|
-
- on failure, fetch `pterm logs <
|
|
152
|
+
- on failure, fetch `pterm logs <ref> --tail 200` when `ref` exists, otherwise `pterm logs <app_id> --tail 200`, and return:
|
|
151
153
|
- raw log tail
|
|
152
154
|
- short diagnosis
|
|
155
|
+
- After successful task completion:
|
|
156
|
+
- do not stop or shut down the app unless the user explicitly asks
|
|
157
|
+
- prefer leaving a successfully running app online for reuse
|
|
153
158
|
|
|
154
|
-
### 4.
|
|
155
|
-
-
|
|
156
|
-
-
|
|
157
|
-
-
|
|
158
|
-
-
|
|
159
|
-
-
|
|
160
|
-
|
|
161
|
-
-
|
|
162
|
-
|
|
163
|
-
-
|
|
164
|
-
-
|
|
159
|
+
### 4. Using Apps
|
|
160
|
+
- Create or reuse one app-specific skill folder for the selected app:
|
|
161
|
+
- local default: `<current_working_directory>/pinokio_agent/skills/<scope>/<app_id>/`
|
|
162
|
+
- fallback: `<PINOKIO_HOME>/agents/skills/<scope>/<app_id>/`
|
|
163
|
+
- App-specific skill folder structure:
|
|
164
|
+
- `SKILL.md`: short instructions for how to use this app
|
|
165
|
+
- include frontmatter with only:
|
|
166
|
+
- `name`: short stable app-specific skill name using lowercase letters, digits, and hyphens only; derive it from a normalized app identity such as `<scope>-<app_id>` and keep it under 64 characters
|
|
167
|
+
- `description`: one clear sentence describing what this app-specific skill does and when it should be used
|
|
168
|
+
- optional `clients/`: reusable local client files
|
|
169
|
+
- optional `references/`: saved API artifacts such as OpenAPI specs, Gradio config, or concise notes
|
|
170
|
+
- outputs: `<app_skill_folder>/output/<target_host>/...`
|
|
171
|
+
|
|
172
|
+
- Reuse an existing app-specific skill when possible:
|
|
173
|
+
- if `<app_skill_folder>/SKILL.md` exists and still describes the app's current API correctly, read it first and follow it
|
|
174
|
+
- if the folder already contains a reusable client for the needed operation and it still works against the current app API, reuse that client
|
|
175
|
+
- if the folder has no `SKILL.md`, or the saved instructions or saved client no longer match the current API, rediscover the app interface and rewrite the app-specific skill folder
|
|
176
|
+
|
|
177
|
+
- If rediscovery is needed, choose exactly one usage mode:
|
|
178
|
+
- Mode A: use the app directly
|
|
179
|
+
- Mode B: reuse or generate a reusable client
|
|
180
|
+
- Use Mode A only if all of these are true:
|
|
181
|
+
- the running app already exposes a documented HTTP API you can call directly
|
|
182
|
+
- the task is simple enough to complete with one or a few direct requests
|
|
183
|
+
- saving a client file would not make later work meaningfully easier
|
|
184
|
+
- Standard callable API examples:
|
|
185
|
+
- OpenAPI / Swagger endpoints
|
|
186
|
+
- FastAPI docs
|
|
187
|
+
- Gradio API
|
|
188
|
+
- other documented standard HTTP interfaces
|
|
189
|
+
- Otherwise use Mode B.
|
|
190
|
+
|
|
191
|
+
- Shared rules for both modes:
|
|
192
|
+
- prefer documented/public APIs exposed by the running launcher
|
|
193
|
+
- choose a base URL that the current machine can actually reach:
|
|
194
|
+
- use `ready_url` when it exists and the current machine can reach it
|
|
195
|
+
- otherwise use `external_ready_urls` in order
|
|
196
|
+
- if the task needs remote filesystem paths, first run `pterm upload <ref> <file...>` and use the returned remote paths for that target only
|
|
197
|
+
- never reuse a remote uploaded path from one target on another target
|
|
198
|
+
- keep `<app_skill_folder>/SKILL.md` concise and operational
|
|
199
|
+
- put bulky raw artifacts in `references/` instead of bloating `SKILL.md`
|
|
200
|
+
|
|
201
|
+
- Mode A: use the app directly
|
|
202
|
+
- execute the needed requests directly from the current machine
|
|
203
|
+
- update `<app_skill_folder>/SKILL.md` to record:
|
|
204
|
+
- what callable API surface exists
|
|
205
|
+
- how to choose the base URL
|
|
206
|
+
- required request inputs and outputs
|
|
207
|
+
- whether remote upload is needed for path-based tasks
|
|
208
|
+
- do not create a reusable client in this mode unless the workflow later becomes repetitive or multi-step enough to justify Mode B
|
|
209
|
+
|
|
210
|
+
- Mode B: reuse or generate a reusable client
|
|
211
|
+
- if no matching client exists under `<app_skill_folder>/clients/` for the needed operation, generate one
|
|
212
|
+
- if a client exists but the contract no longer matches, regenerate it only for:
|
|
213
|
+
- 404/405 endpoint mismatch
|
|
214
|
+
- 400/422 payload/schema mismatch
|
|
215
|
+
- auth/header mismatch
|
|
165
216
|
- inspect docs/code to infer endpoint + payload
|
|
166
|
-
- generate minimal HTTP client
|
|
167
|
-
-
|
|
168
|
-
-
|
|
169
|
-
-
|
|
170
|
-
|
|
171
|
-
-
|
|
172
|
-
-
|
|
173
|
-
-
|
|
217
|
+
- generate a minimal cross-platform HTTP client in `py` or `js`
|
|
218
|
+
- do not use Bash, PowerShell, or other machine-specific shell scripts for reusable clients unless the user explicitly asks for a machine-local one-off script
|
|
219
|
+
- generated clients run on the current machine; do not copy or write them onto the remote machine
|
|
220
|
+
- organize clients by app and operation, not by host
|
|
221
|
+
- example: if local Cropper and remote Cropper use the same endpoint and payload shape for `trim`, reuse one client such as `clients/trim.py`
|
|
222
|
+
- do not create a second client file only because the target host changed
|
|
223
|
+
- pass per-run values into the client at execution time:
|
|
224
|
+
- a base URL that the current machine can actually reach
|
|
225
|
+
- uploaded remote file paths when needed
|
|
226
|
+
- per-run auth headers/cookies if required
|
|
227
|
+
- never hardcode per-run values into the saved client:
|
|
228
|
+
- `ref`
|
|
229
|
+
- base URL / host / port
|
|
230
|
+
- uploaded temp file paths
|
|
231
|
+
- per-run auth tokens or cookies
|
|
232
|
+
- update `<app_skill_folder>/SKILL.md` to record:
|
|
233
|
+
- which client file to use for each operation
|
|
234
|
+
- required runtime arguments
|
|
235
|
+
- expected outputs
|
|
236
|
+
- when the client should be regenerated
|
|
237
|
+
|
|
174
238
|
- Do not execute the app's internal Python/Node/bundled CLI as a fallback when `pterm` has already selected a launcher-managed app.
|
|
175
239
|
- If no automatable API exists after the app is running, report that clearly instead of bypassing the launcher with an internal CLI.
|
|
176
240
|
|
|
@@ -182,7 +246,7 @@ Follow these sections in order:
|
|
|
182
246
|
- compare multiple relevant apps side by side
|
|
183
247
|
- generate multiple outputs concurrently
|
|
184
248
|
- Do not use this mode by default.
|
|
185
|
-
- Keep each selected app as a separate target
|
|
249
|
+
- Keep each selected app as a separate target. Prefer `ref` as the target identifier when it exists.
|
|
186
250
|
- Selection rules:
|
|
187
251
|
- if the user asks for all relevant apps, use all relevant search results that can perform the task
|
|
188
252
|
- if the user asks for a specific count, use the top N relevant search results after normal search ranking
|
|
@@ -191,9 +255,15 @@ Follow these sections in order:
|
|
|
191
255
|
- prefer `ready` apps first
|
|
192
256
|
- then `running` apps
|
|
193
257
|
- then offline apps if more targets are still needed
|
|
258
|
+
- If subagents are available, prefer one subagent per selected target.
|
|
259
|
+
- the main agent should do the search, choose the targets, and aggregate the final results
|
|
260
|
+
- each subagent should own exactly one target `ref` when it exists, otherwise one `app_id`
|
|
261
|
+
- each subagent should run status/run/logs/upload only for its own target
|
|
194
262
|
- Run and monitor each selected target independently.
|
|
195
|
-
- Keep outputs labeled by target `app_id`.
|
|
196
|
-
- For remote path-based tasks, run `pterm upload <
|
|
263
|
+
- Keep outputs labeled by target `ref` when it exists, otherwise `app_id`.
|
|
264
|
+
- For remote path-based tasks, run `pterm upload <ref> <file...>` separately for each remote target when `ref` exists, otherwise fall back to `app_id`.
|
|
265
|
+
- Do not reuse one target's uploaded remote file path for another target.
|
|
266
|
+
- If subagents are unavailable, keep the same per-target separation and run the targets sequentially.
|
|
197
267
|
|
|
198
268
|
## Behavior Rules
|
|
199
269
|
|
|
@@ -202,8 +272,8 @@ Follow these sections in order:
|
|
|
202
272
|
- Do not rewrite launcher files unless user explicitly asked.
|
|
203
273
|
- When a task needs a local executable such as `python`, prefer resolving it with `pterm which <command>` before falling back to generic shell discovery.
|
|
204
274
|
- Prefer returning full logs over brittle deterministic error parsing.
|
|
205
|
-
- REST endpoints may be used for diagnostics only when pterm is unavailable; do not claim full install/launch lifecycle completion without compatible pterm commands.
|
|
206
|
-
- Do not keep searching after app selection; move to
|
|
275
|
+
- Pinokio control-plane REST endpoints may be used for diagnostics only when `pterm` is unavailable; do not claim full install/launch lifecycle completion without compatible `pterm` commands.
|
|
276
|
+
- Do not keep searching after app selection; move to Launch App.
|
|
207
277
|
- Do not assume `external_ready_urls` exists; localhost-only apps are normal.
|
|
208
278
|
- Do not conflate loopback access failure, sandbox denial, or missing permission with "Pinokio is not running" or "`pterm` is not installed."
|
|
209
279
|
- On `pterm` permission failure, prefer asking for permission over asking the user to manually run commands.
|
|
@@ -213,8 +283,14 @@ Follow these sections in order:
|
|
|
213
283
|
|
|
214
284
|
User: "Launch FaceFusion"
|
|
215
285
|
|
|
216
|
-
1. Use Search App and then
|
|
286
|
+
1. Use Search App and then Launch App as usual.
|
|
217
287
|
2. If launcher menu has no explicit default item, infer ordered selectors from the current launcher menu.
|
|
218
288
|
3. Run:
|
|
219
|
-
|
|
220
|
-
|
|
289
|
+
- if `ref` exists:
|
|
290
|
+
`pterm run <ref> --default 'run.js?mode=Default' --default run.js --default install.js`
|
|
291
|
+
- otherwise:
|
|
292
|
+
`pterm run <app_path> --default 'run.js?mode=Default' --default run.js --default install.js`
|
|
293
|
+
4. Poll:
|
|
294
|
+
- `pterm status <ref>` when `ref` exists
|
|
295
|
+
- otherwise `pterm status <app_id>`
|
|
296
|
+
until ready.
|
package/server/index.js
CHANGED
|
@@ -46,6 +46,7 @@ const NOTIFICATION_SOUND_EXTENSIONS = new Set(['.aac', '.flac', '.m4a', '.mp3',
|
|
|
46
46
|
const LOG_STREAM_INITIAL_BYTES = 512 * 1024
|
|
47
47
|
const LOG_STREAM_KEEPALIVE_MS = 25000
|
|
48
48
|
const DEFAULT_REGISTRY_URL = 'https://beta.pinokio.co'
|
|
49
|
+
const LOOPBACK_HOSTS = new Set(['127.0.0.1', 'localhost', '::1', '[::1]'])
|
|
49
50
|
|
|
50
51
|
const ex = fn => (req, res, next) => {
|
|
51
52
|
Promise.resolve(fn(req, res, next)).catch(next);
|
|
@@ -8592,6 +8593,191 @@ class Server {
|
|
|
8592
8593
|
Util.openURL(req.body.url)
|
|
8593
8594
|
res.json({ success: true })
|
|
8594
8595
|
}))
|
|
8596
|
+
this.app.post("/pinokio/open", ex(async (req, res) => {
|
|
8597
|
+
const url = typeof req.body.url === 'string' ? req.body.url.trim() : ''
|
|
8598
|
+
if (!url) {
|
|
8599
|
+
res.status(400).json({ error: 'Missing url' })
|
|
8600
|
+
return
|
|
8601
|
+
}
|
|
8602
|
+
const normalizeSurface = (value) => {
|
|
8603
|
+
return String(value || '').trim().toLowerCase() === 'popup' ? 'popup' : 'browser'
|
|
8604
|
+
}
|
|
8605
|
+
const normalizePreset = (value) => {
|
|
8606
|
+
const normalized = String(value || '').trim().toLowerCase()
|
|
8607
|
+
if (normalized === 'center-small' || normalized === 'center-medium' || normalized === 'center-large' || normalized === 'fullscreen') {
|
|
8608
|
+
return normalized
|
|
8609
|
+
}
|
|
8610
|
+
return 'center-medium'
|
|
8611
|
+
}
|
|
8612
|
+
const defaultPeerPort = () => {
|
|
8613
|
+
const rawPort = Number.parseInt(String(this.kernel?.peer?.default_port || this.kernel?.server_port || this.port || DEFAULT_PORT), 10)
|
|
8614
|
+
return Number.isFinite(rawPort) && rawPort > 0 ? rawPort : DEFAULT_PORT
|
|
8615
|
+
}
|
|
8616
|
+
const currentPeerHost = () => {
|
|
8617
|
+
return this.kernel && this.kernel.peer && this.kernel.peer.host
|
|
8618
|
+
? String(this.kernel.peer.host).trim()
|
|
8619
|
+
: ''
|
|
8620
|
+
}
|
|
8621
|
+
const currentPeerName = () => {
|
|
8622
|
+
return this.kernel && this.kernel.peer && this.kernel.peer.name
|
|
8623
|
+
? String(this.kernel.peer.name).trim()
|
|
8624
|
+
: ''
|
|
8625
|
+
}
|
|
8626
|
+
const isLocalPeerToken = (value) => {
|
|
8627
|
+
const normalized = String(value || '').trim().toLowerCase()
|
|
8628
|
+
return !!normalized && (LOOPBACK_HOSTS.has(normalized) || normalized === currentPeerHost().toLowerCase() || normalized === currentPeerName().toLowerCase())
|
|
8629
|
+
}
|
|
8630
|
+
const resolvePeerTarget = (rawValue) => {
|
|
8631
|
+
const localHost = currentPeerHost() || '127.0.0.1'
|
|
8632
|
+
const localName = currentPeerName() || localHost
|
|
8633
|
+
const fallbackPort = defaultPeerPort()
|
|
8634
|
+
if (rawValue === undefined || rawValue === null || String(rawValue).trim() === '') {
|
|
8635
|
+
return {
|
|
8636
|
+
local: true,
|
|
8637
|
+
host: localHost,
|
|
8638
|
+
port: fallbackPort,
|
|
8639
|
+
name: localName
|
|
8640
|
+
}
|
|
8641
|
+
}
|
|
8642
|
+
const trimmed = String(rawValue).trim()
|
|
8643
|
+
let hostOrName = trimmed
|
|
8644
|
+
let port = fallbackPort
|
|
8645
|
+
const separatorIndex = trimmed.lastIndexOf(':')
|
|
8646
|
+
if (separatorIndex > 0 && separatorIndex < trimmed.length - 1) {
|
|
8647
|
+
const possiblePort = trimmed.slice(separatorIndex + 1).trim()
|
|
8648
|
+
if (/^\d+$/.test(possiblePort)) {
|
|
8649
|
+
hostOrName = trimmed.slice(0, separatorIndex).trim()
|
|
8650
|
+
const explicitPort = Number.parseInt(possiblePort, 10)
|
|
8651
|
+
if (Number.isFinite(explicitPort) && explicitPort > 0) {
|
|
8652
|
+
port = explicitPort
|
|
8653
|
+
}
|
|
8654
|
+
}
|
|
8655
|
+
}
|
|
8656
|
+
if (!hostOrName) {
|
|
8657
|
+
return {
|
|
8658
|
+
error: 'Invalid peer'
|
|
8659
|
+
}
|
|
8660
|
+
}
|
|
8661
|
+
if (isLocalPeerToken(hostOrName)) {
|
|
8662
|
+
return {
|
|
8663
|
+
local: true,
|
|
8664
|
+
host: localHost,
|
|
8665
|
+
port,
|
|
8666
|
+
name: localName
|
|
8667
|
+
}
|
|
8668
|
+
}
|
|
8669
|
+
if (this.kernel && this.kernel.peer && this.kernel.peer.info && typeof this.kernel.peer.info === 'object') {
|
|
8670
|
+
for (const [host, info] of Object.entries(this.kernel.peer.info)) {
|
|
8671
|
+
const peerName = info && info.name ? String(info.name).trim() : ''
|
|
8672
|
+
if (host === hostOrName || (peerName && peerName === hostOrName)) {
|
|
8673
|
+
return {
|
|
8674
|
+
local: host === localHost,
|
|
8675
|
+
host,
|
|
8676
|
+
port,
|
|
8677
|
+
name: peerName || host
|
|
8678
|
+
}
|
|
8679
|
+
}
|
|
8680
|
+
}
|
|
8681
|
+
}
|
|
8682
|
+
if (LOOPBACK_HOSTS.has(hostOrName.toLowerCase())) {
|
|
8683
|
+
return {
|
|
8684
|
+
local: true,
|
|
8685
|
+
host: localHost,
|
|
8686
|
+
port,
|
|
8687
|
+
name: localName
|
|
8688
|
+
}
|
|
8689
|
+
}
|
|
8690
|
+
if (/^(?:\d{1,3}\.){3}\d{1,3}$/.test(hostOrName)) {
|
|
8691
|
+
return {
|
|
8692
|
+
local: false,
|
|
8693
|
+
host: hostOrName,
|
|
8694
|
+
port,
|
|
8695
|
+
name: hostOrName
|
|
8696
|
+
}
|
|
8697
|
+
}
|
|
8698
|
+
return {
|
|
8699
|
+
error: `Unknown peer: ${trimmed}`
|
|
8700
|
+
}
|
|
8701
|
+
}
|
|
8702
|
+
const requestedSurface = normalizeSurface(req.body.surface)
|
|
8703
|
+
const requestedPreset = normalizePreset(req.body.preset)
|
|
8704
|
+
const peerTarget = resolvePeerTarget(req.body.peer)
|
|
8705
|
+
if (peerTarget.error) {
|
|
8706
|
+
res.status(400).json({ error: peerTarget.error })
|
|
8707
|
+
return
|
|
8708
|
+
}
|
|
8709
|
+
if (!peerTarget.local) {
|
|
8710
|
+
try {
|
|
8711
|
+
const response = await axios.post(`http://${peerTarget.host}:${peerTarget.port}/pinokio/open`, {
|
|
8712
|
+
url,
|
|
8713
|
+
surface: requestedSurface,
|
|
8714
|
+
preset: requestedPreset
|
|
8715
|
+
}, {
|
|
8716
|
+
timeout: 5000,
|
|
8717
|
+
headers: {
|
|
8718
|
+
'x-pinokio-peer': '1'
|
|
8719
|
+
}
|
|
8720
|
+
})
|
|
8721
|
+
res.json({
|
|
8722
|
+
...(response.data && typeof response.data === 'object' ? response.data : { success: true }),
|
|
8723
|
+
peer: {
|
|
8724
|
+
host: peerTarget.host,
|
|
8725
|
+
port: peerTarget.port,
|
|
8726
|
+
name: peerTarget.name || peerTarget.host,
|
|
8727
|
+
local: false
|
|
8728
|
+
}
|
|
8729
|
+
})
|
|
8730
|
+
return
|
|
8731
|
+
} catch (error) {
|
|
8732
|
+
const remoteMessage = error && error.response && error.response.data && error.response.data.error
|
|
8733
|
+
? error.response.data.error
|
|
8734
|
+
: (error && error.message ? error.message : 'Peer open failed')
|
|
8735
|
+
res.status(502).json({
|
|
8736
|
+
error: remoteMessage,
|
|
8737
|
+
peer: {
|
|
8738
|
+
host: peerTarget.host,
|
|
8739
|
+
port: peerTarget.port,
|
|
8740
|
+
name: peerTarget.name || peerTarget.host,
|
|
8741
|
+
local: false
|
|
8742
|
+
}
|
|
8743
|
+
})
|
|
8744
|
+
return
|
|
8745
|
+
}
|
|
8746
|
+
}
|
|
8747
|
+
let result
|
|
8748
|
+
if (this.browser && typeof this.browser.open === 'function') {
|
|
8749
|
+
result = await Promise.resolve(this.browser.open({
|
|
8750
|
+
url,
|
|
8751
|
+
surface: requestedSurface,
|
|
8752
|
+
preset: requestedPreset
|
|
8753
|
+
}))
|
|
8754
|
+
}
|
|
8755
|
+
if (!result || result.ok === false) {
|
|
8756
|
+
Util.openURL(url)
|
|
8757
|
+
result = {
|
|
8758
|
+
ok: true,
|
|
8759
|
+
surface_used: 'browser'
|
|
8760
|
+
}
|
|
8761
|
+
}
|
|
8762
|
+
const surfaceUsed = typeof result.surface_used === 'string' && result.surface_used
|
|
8763
|
+
? result.surface_used
|
|
8764
|
+
: 'browser'
|
|
8765
|
+
res.json({
|
|
8766
|
+
success: true,
|
|
8767
|
+
url,
|
|
8768
|
+
requested_surface: requestedSurface,
|
|
8769
|
+
surface_used: surfaceUsed,
|
|
8770
|
+
preset_used: surfaceUsed === 'popup'
|
|
8771
|
+
? (typeof result.preset_used === 'string' && result.preset_used ? result.preset_used : requestedPreset)
|
|
8772
|
+
: null,
|
|
8773
|
+
peer: {
|
|
8774
|
+
host: peerTarget.host,
|
|
8775
|
+
port: peerTarget.port,
|
|
8776
|
+
name: peerTarget.name || peerTarget.host,
|
|
8777
|
+
local: true
|
|
8778
|
+
}
|
|
8779
|
+
})
|
|
8780
|
+
}))
|
|
8595
8781
|
this.app.post("/openfs", ex(async (req, res) => {
|
|
8596
8782
|
//Util.openfs(req.body.path, req.body.mode)
|
|
8597
8783
|
if (req.body.name) {
|
package/server/routes/apps.js
CHANGED
|
@@ -11,6 +11,7 @@ const DEFAULT_PEER_PORT = 42000
|
|
|
11
11
|
const DEFAULT_PEER_TIMEOUT_MS = 2500
|
|
12
12
|
const DEFAULT_PEER_UPLOAD_TIMEOUT_MS = 30000
|
|
13
13
|
const IPV4_HOST_PATTERN = /^(?:\d{1,3}\.){3}\d{1,3}$/
|
|
14
|
+
const PINOKIO_REF_PROTOCOL = 'pinokio:'
|
|
14
15
|
|
|
15
16
|
const isQualifiedHost = (value = '') => {
|
|
16
17
|
return IPV4_HOST_PATTERN.test(String(value || '').trim())
|
|
@@ -56,6 +57,84 @@ const parseQualifiedAppId = (value = '') => {
|
|
|
56
57
|
}
|
|
57
58
|
}
|
|
58
59
|
|
|
60
|
+
const isLoopbackHost = (value = '') => {
|
|
61
|
+
const normalized = String(value || '').trim().toLowerCase()
|
|
62
|
+
return normalized === '127.0.0.1' || normalized === 'localhost' || normalized === '::1' || normalized === '[::1]'
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const parsePinokioRef = (value = '') => {
|
|
66
|
+
if (typeof value !== 'string') {
|
|
67
|
+
return {
|
|
68
|
+
valid: false,
|
|
69
|
+
error: 'Invalid ref'
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const trimmed = value.trim()
|
|
73
|
+
if (!trimmed) {
|
|
74
|
+
return {
|
|
75
|
+
valid: false,
|
|
76
|
+
error: 'Missing ref'
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
let parsed
|
|
80
|
+
try {
|
|
81
|
+
parsed = new URL(trimmed)
|
|
82
|
+
} catch (_) {
|
|
83
|
+
return {
|
|
84
|
+
valid: false,
|
|
85
|
+
error: 'Invalid ref'
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (parsed.protocol !== PINOKIO_REF_PROTOCOL) {
|
|
89
|
+
return {
|
|
90
|
+
valid: false,
|
|
91
|
+
error: 'Invalid ref protocol'
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const host = typeof parsed.hostname === 'string' ? parsed.hostname.trim() : ''
|
|
95
|
+
const port = Number.parseInt(String(parsed.port || ''), 10)
|
|
96
|
+
const pathSegments = String(parsed.pathname || '')
|
|
97
|
+
.split('/')
|
|
98
|
+
.filter(Boolean)
|
|
99
|
+
.map((segment) => {
|
|
100
|
+
try {
|
|
101
|
+
return decodeURIComponent(segment)
|
|
102
|
+
} catch (_) {
|
|
103
|
+
return segment
|
|
104
|
+
}
|
|
105
|
+
})
|
|
106
|
+
const scope = pathSegments.length > 0 ? pathSegments[0] : ''
|
|
107
|
+
const id = pathSegments.length > 1 ? pathSegments.slice(1).join('/') : ''
|
|
108
|
+
if (!host || !Number.isFinite(port) || port <= 0 || !scope || !id) {
|
|
109
|
+
return {
|
|
110
|
+
valid: false,
|
|
111
|
+
error: 'Invalid ref'
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
valid: true,
|
|
116
|
+
ref: trimmed,
|
|
117
|
+
host,
|
|
118
|
+
port,
|
|
119
|
+
scope,
|
|
120
|
+
id
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const buildPinokioRef = ({ host, port, scope, id }) => {
|
|
125
|
+
const normalizedHost = typeof host === 'string' ? host.trim() : ''
|
|
126
|
+
const normalizedScope = typeof scope === 'string' ? scope.trim() : ''
|
|
127
|
+
const normalizedId = typeof id === 'string' ? id.trim() : ''
|
|
128
|
+
const normalizedPort = Number.parseInt(String(port || ''), 10)
|
|
129
|
+
if (!normalizedHost || !normalizedScope || !normalizedId || !Number.isFinite(normalizedPort) || normalizedPort <= 0) {
|
|
130
|
+
return null
|
|
131
|
+
}
|
|
132
|
+
const encodedPath = [normalizedScope, ...normalizedId.split('/').filter(Boolean)]
|
|
133
|
+
.map((segment) => encodeURIComponent(segment))
|
|
134
|
+
.join('/')
|
|
135
|
+
return `pinokio://${normalizedHost}:${normalizedPort}/${encodedPath}`
|
|
136
|
+
}
|
|
137
|
+
|
|
59
138
|
module.exports = function registerAppRoutes(app, { registry, preferences, appSearch, appLogs, getTheme }) {
|
|
60
139
|
if (!app || !registry || !preferences || !appSearch || !appLogs) {
|
|
61
140
|
throw new Error('App routes require app, registry, preferences, appSearch, and appLogs')
|
|
@@ -120,6 +199,43 @@ module.exports = function registerAppRoutes(app, { registry, preferences, appSea
|
|
|
120
199
|
}
|
|
121
200
|
return `${normalizedAppId}@${host}`
|
|
122
201
|
}
|
|
202
|
+
const canonicalRefHost = (source, overrideHost = '') => {
|
|
203
|
+
const hostOverride = typeof overrideHost === 'string' ? overrideHost.trim() : ''
|
|
204
|
+
if (hostOverride) {
|
|
205
|
+
return hostOverride
|
|
206
|
+
}
|
|
207
|
+
if (source && typeof source.host === 'string' && source.host.trim()) {
|
|
208
|
+
return source.host.trim()
|
|
209
|
+
}
|
|
210
|
+
return currentPeerHost() || '127.0.0.1'
|
|
211
|
+
}
|
|
212
|
+
const isLocalPinokioRef = (parsedRef) => {
|
|
213
|
+
if (!parsedRef || !parsedRef.valid) {
|
|
214
|
+
return false
|
|
215
|
+
}
|
|
216
|
+
if (parsedRef.port !== peerPort()) {
|
|
217
|
+
return false
|
|
218
|
+
}
|
|
219
|
+
const localHost = currentPeerHost()
|
|
220
|
+
return isLoopbackHost(parsedRef.host) || (localHost && parsedRef.host === localHost)
|
|
221
|
+
}
|
|
222
|
+
const attachApiRef = (payload, source, appId, options = {}) => {
|
|
223
|
+
const next = payload && typeof payload === 'object' ? { ...payload } : {}
|
|
224
|
+
const normalizedAppId = typeof appId === 'string' ? appId.trim() : ''
|
|
225
|
+
if (!normalizedAppId) {
|
|
226
|
+
return next
|
|
227
|
+
}
|
|
228
|
+
const ref = buildPinokioRef({
|
|
229
|
+
host: canonicalRefHost(source, options.host),
|
|
230
|
+
port: Number.parseInt(String(options.port || peerPort()), 10) || peerPort(),
|
|
231
|
+
scope: 'api',
|
|
232
|
+
id: normalizedAppId
|
|
233
|
+
})
|
|
234
|
+
if (ref) {
|
|
235
|
+
next.ref = ref
|
|
236
|
+
}
|
|
237
|
+
return next
|
|
238
|
+
}
|
|
123
239
|
const neutralizeRemoteSearchPreferences = (appResult) => {
|
|
124
240
|
const next = appResult && typeof appResult === 'object' ? { ...appResult } : {}
|
|
125
241
|
next.starred = false
|
|
@@ -153,25 +269,31 @@ module.exports = function registerAppRoutes(app, { registry, preferences, appSea
|
|
|
153
269
|
const parsed = Date.parse(value)
|
|
154
270
|
return Number.isFinite(parsed) ? parsed : 0
|
|
155
271
|
}
|
|
156
|
-
const decorateSearchResult = (appResult, source) => {
|
|
272
|
+
const decorateSearchResult = (appResult, source, options = {}) => {
|
|
157
273
|
const next = source && !source.local
|
|
158
274
|
? neutralizeRemoteSearchPreferences(appResult)
|
|
159
275
|
: (appResult && typeof appResult === 'object' ? { ...appResult } : {})
|
|
160
|
-
|
|
276
|
+
const resourceId = typeof next.app_id === 'string' && next.app_id.trim()
|
|
277
|
+
? next.app_id.trim()
|
|
278
|
+
: (typeof next.name === 'string' ? next.name.trim() : '')
|
|
279
|
+
next.app_id = qualifyAppId(resourceId, source.host)
|
|
161
280
|
next.source = source
|
|
162
281
|
if (!source.local) {
|
|
163
282
|
next.ready_url = null
|
|
164
283
|
}
|
|
165
|
-
return next
|
|
284
|
+
return attachApiRef(next, source, resourceId, options)
|
|
166
285
|
}
|
|
167
|
-
const decorateStatusResult = (statusResult, source) => {
|
|
286
|
+
const decorateStatusResult = (statusResult, source, options = {}) => {
|
|
168
287
|
const next = statusResult && typeof statusResult === 'object' ? { ...statusResult } : {}
|
|
169
|
-
|
|
288
|
+
const resourceId = typeof next.app_id === 'string' && next.app_id.trim()
|
|
289
|
+
? next.app_id.trim()
|
|
290
|
+
: (typeof next.name === 'string' ? next.name.trim() : '')
|
|
291
|
+
next.app_id = qualifyAppId(resourceId, source.host)
|
|
170
292
|
next.source = source
|
|
171
293
|
if (!source.local) {
|
|
172
294
|
next.ready_url = null
|
|
173
295
|
}
|
|
174
|
-
return next
|
|
296
|
+
return attachApiRef(next, source, resourceId, options)
|
|
175
297
|
}
|
|
176
298
|
const peerRequestHeaders = (req) => {
|
|
177
299
|
const headers = {
|
|
@@ -245,11 +367,11 @@ module.exports = function registerAppRoutes(app, { registry, preferences, appSea
|
|
|
245
367
|
files: stored
|
|
246
368
|
}
|
|
247
369
|
}
|
|
248
|
-
const decorateUploadResult = (uploadResult, source, appId) => {
|
|
370
|
+
const decorateUploadResult = (uploadResult, source, appId, options = {}) => {
|
|
249
371
|
const next = uploadResult && typeof uploadResult === 'object' ? { ...uploadResult } : {}
|
|
250
372
|
next.app_id = qualifyAppId(appId || next.app_id || '', source.host)
|
|
251
373
|
next.source = source
|
|
252
|
-
return next
|
|
374
|
+
return attachApiRef(next, source, appId || next.app_id || '', options)
|
|
253
375
|
}
|
|
254
376
|
const mergeSearchApps = (localApps, remoteApps, query = '') => {
|
|
255
377
|
const merged = []
|
|
@@ -400,6 +522,255 @@ module.exports = function registerAppRoutes(app, { registry, preferences, appSea
|
|
|
400
522
|
res.json({ apps })
|
|
401
523
|
}))
|
|
402
524
|
|
|
525
|
+
router.get('/pinokio/resource/status', asyncHandler(async (req, res) => {
|
|
526
|
+
const parsedRef = parsePinokioRef(typeof req.query.ref === 'string' ? req.query.ref : '')
|
|
527
|
+
if (!parsedRef.valid) {
|
|
528
|
+
res.status(400).json({ error: parsedRef.error || 'Invalid ref' })
|
|
529
|
+
return
|
|
530
|
+
}
|
|
531
|
+
if (parsedRef.scope !== 'api') {
|
|
532
|
+
res.status(400).json({ error: `Unsupported ref scope: ${parsedRef.scope}` })
|
|
533
|
+
return
|
|
534
|
+
}
|
|
535
|
+
const canonicalRef = buildPinokioRef(parsedRef)
|
|
536
|
+
if (!isLocalPinokioRef(parsedRef)) {
|
|
537
|
+
try {
|
|
538
|
+
const timeout = Number.parseInt(String(req.query.timeout || ''), 10)
|
|
539
|
+
const params = { ref: canonicalRef }
|
|
540
|
+
if (typeof req.query.probe !== 'undefined') {
|
|
541
|
+
params.probe = req.query.probe
|
|
542
|
+
}
|
|
543
|
+
if (Number.isFinite(timeout)) {
|
|
544
|
+
params.timeout = String(timeout)
|
|
545
|
+
}
|
|
546
|
+
const response = await axios.get(`http://${parsedRef.host}:${parsedRef.port}/pinokio/resource/status`, {
|
|
547
|
+
timeout: DEFAULT_PEER_TIMEOUT_MS,
|
|
548
|
+
headers: peerRequestHeaders(req),
|
|
549
|
+
params
|
|
550
|
+
})
|
|
551
|
+
const payload = decorateStatusResult(response.data, buildSource(parsedRef.host, false), {
|
|
552
|
+
host: parsedRef.host,
|
|
553
|
+
port: parsedRef.port
|
|
554
|
+
})
|
|
555
|
+
payload.app_id = parsedRef.id
|
|
556
|
+
payload.ref = canonicalRef
|
|
557
|
+
res.json(payload)
|
|
558
|
+
return
|
|
559
|
+
} catch (error) {
|
|
560
|
+
if (error && error.response) {
|
|
561
|
+
res.status(error.response.status).json(error.response.data)
|
|
562
|
+
return
|
|
563
|
+
}
|
|
564
|
+
res.status(502).json({
|
|
565
|
+
error: 'Peer resource status unavailable',
|
|
566
|
+
ref: canonicalRef,
|
|
567
|
+
source: buildSource(parsedRef.host, false)
|
|
568
|
+
})
|
|
569
|
+
return
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
const appId = registry.normalizeAppId(parsedRef.id)
|
|
573
|
+
if (!appId) {
|
|
574
|
+
res.status(400).json({ error: 'Invalid app_id', ref: canonicalRef })
|
|
575
|
+
return
|
|
576
|
+
}
|
|
577
|
+
const probe = registry.parseBooleanQuery(req.query.probe, false)
|
|
578
|
+
const timeout = Number.parseInt(String(req.query.timeout || ''), 10)
|
|
579
|
+
const status = await registry.buildAppStatus(appId, {
|
|
580
|
+
probe,
|
|
581
|
+
timeout: Number.isFinite(timeout) ? timeout : 1500,
|
|
582
|
+
source: req.$source || null
|
|
583
|
+
})
|
|
584
|
+
if (!status) {
|
|
585
|
+
res.status(404).json({ error: 'App not found', ref: canonicalRef })
|
|
586
|
+
return
|
|
587
|
+
}
|
|
588
|
+
status.preference = await preferences.getPreference(appId)
|
|
589
|
+
const payload = decorateStatusResult(status, buildSource(currentPeerHost(), true), {
|
|
590
|
+
host: parsedRef.host,
|
|
591
|
+
port: parsedRef.port
|
|
592
|
+
})
|
|
593
|
+
payload.app_id = appId
|
|
594
|
+
payload.ref = canonicalRef
|
|
595
|
+
res.json(payload)
|
|
596
|
+
}))
|
|
597
|
+
|
|
598
|
+
router.get('/pinokio/resource/logs', asyncHandler(async (req, res) => {
|
|
599
|
+
const parsedRef = parsePinokioRef(typeof req.query.ref === 'string' ? req.query.ref : '')
|
|
600
|
+
if (!parsedRef.valid) {
|
|
601
|
+
res.status(400).json({ error: parsedRef.error || 'Invalid ref' })
|
|
602
|
+
return
|
|
603
|
+
}
|
|
604
|
+
if (parsedRef.scope !== 'api') {
|
|
605
|
+
res.status(400).json({ error: `Unsupported ref scope: ${parsedRef.scope}` })
|
|
606
|
+
return
|
|
607
|
+
}
|
|
608
|
+
const canonicalRef = buildPinokioRef(parsedRef)
|
|
609
|
+
if (!isLocalPinokioRef(parsedRef)) {
|
|
610
|
+
try {
|
|
611
|
+
const params = { ref: canonicalRef }
|
|
612
|
+
if (typeof req.query.script === 'string' && req.query.script.trim()) {
|
|
613
|
+
params.script = req.query.script
|
|
614
|
+
}
|
|
615
|
+
const tail = registry.parseTailCount(req.query.tail, 200)
|
|
616
|
+
if (Number.isFinite(tail) && tail > 0) {
|
|
617
|
+
params.tail = String(tail)
|
|
618
|
+
}
|
|
619
|
+
const response = await axios.get(`http://${parsedRef.host}:${parsedRef.port}/pinokio/resource/logs`, {
|
|
620
|
+
timeout: DEFAULT_PEER_TIMEOUT_MS,
|
|
621
|
+
headers: peerRequestHeaders(req),
|
|
622
|
+
params
|
|
623
|
+
})
|
|
624
|
+
const payload = response && response.data && typeof response.data === 'object'
|
|
625
|
+
? { ...response.data }
|
|
626
|
+
: {}
|
|
627
|
+
payload.app_id = parsedRef.id
|
|
628
|
+
payload.ref = canonicalRef
|
|
629
|
+
payload.source = buildSource(parsedRef.host, false)
|
|
630
|
+
res.json(payload)
|
|
631
|
+
return
|
|
632
|
+
} catch (error) {
|
|
633
|
+
if (error && error.response) {
|
|
634
|
+
res.status(error.response.status).json(error.response.data)
|
|
635
|
+
return
|
|
636
|
+
}
|
|
637
|
+
res.status(502).json({
|
|
638
|
+
error: 'Peer resource logs unavailable',
|
|
639
|
+
ref: canonicalRef,
|
|
640
|
+
source: buildSource(parsedRef.host, false)
|
|
641
|
+
})
|
|
642
|
+
return
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
const appId = registry.normalizeAppId(parsedRef.id)
|
|
646
|
+
if (!appId) {
|
|
647
|
+
res.status(400).json({ error: 'Invalid app_id', ref: canonicalRef })
|
|
648
|
+
return
|
|
649
|
+
}
|
|
650
|
+
const status = await registry.buildAppStatus(appId, {
|
|
651
|
+
source: req.$source || null
|
|
652
|
+
})
|
|
653
|
+
if (!status) {
|
|
654
|
+
res.status(404).json({ error: 'App not found', ref: canonicalRef })
|
|
655
|
+
return
|
|
656
|
+
}
|
|
657
|
+
const tail = registry.parseTailCount(req.query.tail, 200)
|
|
658
|
+
const scriptQuery = typeof req.query.script === 'string' ? req.query.script : ''
|
|
659
|
+
const resolvedLog = await appLogs.resolveAppLogFile(status.path, scriptQuery, status.running_scripts)
|
|
660
|
+
if (resolvedLog && resolvedLog.error === 'INVALID_SCRIPT') {
|
|
661
|
+
res.status(400).json({ error: 'Invalid script path', ref: canonicalRef })
|
|
662
|
+
return
|
|
663
|
+
}
|
|
664
|
+
if (!resolvedLog || !resolvedLog.file) {
|
|
665
|
+
res.status(404).json({
|
|
666
|
+
error: 'No log file found',
|
|
667
|
+
ref: canonicalRef,
|
|
668
|
+
script: scriptQuery || null
|
|
669
|
+
})
|
|
670
|
+
return
|
|
671
|
+
}
|
|
672
|
+
const logData = await appLogs.readLogTail(resolvedLog.file, tail)
|
|
673
|
+
res.json({
|
|
674
|
+
app_id: appId,
|
|
675
|
+
ref: canonicalRef,
|
|
676
|
+
source: buildSource(currentPeerHost(), true),
|
|
677
|
+
script: resolvedLog.script,
|
|
678
|
+
file: registry.toPosixRelative(status.path, resolvedLog.file),
|
|
679
|
+
...logData
|
|
680
|
+
})
|
|
681
|
+
}))
|
|
682
|
+
|
|
683
|
+
router.post('/pinokio/resource/upload', upload.any(), asyncHandler(async (req, res) => {
|
|
684
|
+
const rawRef = typeof req.query.ref === 'string' && req.query.ref.trim()
|
|
685
|
+
? req.query.ref
|
|
686
|
+
: (req.body && typeof req.body.ref === 'string' ? req.body.ref : '')
|
|
687
|
+
const parsedRef = parsePinokioRef(rawRef)
|
|
688
|
+
if (!parsedRef.valid) {
|
|
689
|
+
res.status(400).json({ error: parsedRef.error || 'Invalid ref' })
|
|
690
|
+
return
|
|
691
|
+
}
|
|
692
|
+
if (parsedRef.scope !== 'api') {
|
|
693
|
+
res.status(400).json({ error: `Unsupported ref scope: ${parsedRef.scope}` })
|
|
694
|
+
return
|
|
695
|
+
}
|
|
696
|
+
const canonicalRef = buildPinokioRef(parsedRef)
|
|
697
|
+
const files = Array.isArray(req.files) ? req.files : []
|
|
698
|
+
if (files.length === 0) {
|
|
699
|
+
res.status(400).json({ error: 'No files provided', ref: canonicalRef })
|
|
700
|
+
return
|
|
701
|
+
}
|
|
702
|
+
if (!isLocalPinokioRef(parsedRef)) {
|
|
703
|
+
try {
|
|
704
|
+
const form = new FormData()
|
|
705
|
+
for (const file of files) {
|
|
706
|
+
if (!file || !file.buffer) {
|
|
707
|
+
continue
|
|
708
|
+
}
|
|
709
|
+
form.append('files', file.buffer, {
|
|
710
|
+
filename: path.basename(file.originalname || 'upload'),
|
|
711
|
+
contentType: file.mimetype || 'application/octet-stream',
|
|
712
|
+
knownLength: typeof file.size === 'number' ? file.size : file.buffer.length
|
|
713
|
+
})
|
|
714
|
+
}
|
|
715
|
+
const response = await axios.post(`http://${parsedRef.host}:${parsedRef.port}/pinokio/resource/upload`, form, {
|
|
716
|
+
timeout: DEFAULT_PEER_UPLOAD_TIMEOUT_MS,
|
|
717
|
+
maxBodyLength: Infinity,
|
|
718
|
+
maxContentLength: Infinity,
|
|
719
|
+
headers: {
|
|
720
|
+
...peerRequestHeaders(req),
|
|
721
|
+
...form.getHeaders()
|
|
722
|
+
},
|
|
723
|
+
params: {
|
|
724
|
+
ref: canonicalRef
|
|
725
|
+
}
|
|
726
|
+
})
|
|
727
|
+
const payload = response && response.data && typeof response.data === 'object'
|
|
728
|
+
? { ...response.data }
|
|
729
|
+
: {}
|
|
730
|
+
payload.app_id = parsedRef.id
|
|
731
|
+
payload.ref = canonicalRef
|
|
732
|
+
payload.source = buildSource(parsedRef.host, false)
|
|
733
|
+
res.json(payload)
|
|
734
|
+
return
|
|
735
|
+
} catch (error) {
|
|
736
|
+
if (error && error.response) {
|
|
737
|
+
res.status(error.response.status).json(error.response.data)
|
|
738
|
+
return
|
|
739
|
+
}
|
|
740
|
+
res.status(502).json({
|
|
741
|
+
error: 'Peer resource upload unavailable',
|
|
742
|
+
ref: canonicalRef,
|
|
743
|
+
source: buildSource(parsedRef.host, false)
|
|
744
|
+
})
|
|
745
|
+
return
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
const appId = registry.normalizeAppId(parsedRef.id)
|
|
749
|
+
if (!appId) {
|
|
750
|
+
res.status(400).json({ error: 'Invalid app_id', ref: canonicalRef })
|
|
751
|
+
return
|
|
752
|
+
}
|
|
753
|
+
const status = await registry.buildAppStatus(appId, {
|
|
754
|
+
source: req.$source || null
|
|
755
|
+
})
|
|
756
|
+
if (!status || !status.path) {
|
|
757
|
+
res.status(404).json({ error: 'App not found', ref: canonicalRef })
|
|
758
|
+
return
|
|
759
|
+
}
|
|
760
|
+
const payload = await storeAppUploads(status.path, files)
|
|
761
|
+
if (!Array.isArray(payload.files) || payload.files.length === 0) {
|
|
762
|
+
res.status(400).json({ error: 'No valid files provided', ref: canonicalRef })
|
|
763
|
+
return
|
|
764
|
+
}
|
|
765
|
+
const decorated = decorateUploadResult(payload, buildSource(currentPeerHost(), true), appId, {
|
|
766
|
+
host: parsedRef.host,
|
|
767
|
+
port: parsedRef.port
|
|
768
|
+
})
|
|
769
|
+
decorated.app_id = appId
|
|
770
|
+
decorated.ref = canonicalRef
|
|
771
|
+
res.json(decorated)
|
|
772
|
+
}))
|
|
773
|
+
|
|
403
774
|
router.get('/apps/search', asyncHandler(async (req, res) => {
|
|
404
775
|
const q = typeof req.query.q === 'string' ? req.query.q : ''
|
|
405
776
|
const mode = typeof req.query.mode === 'string' ? req.query.mode : ''
|
|
@@ -600,6 +971,12 @@ module.exports = function registerAppRoutes(app, { registry, preferences, appSea
|
|
|
600
971
|
: {}
|
|
601
972
|
payload.app_id = qualifyAppId(requestedAppId, remoteHost)
|
|
602
973
|
payload.source = buildSource(remoteHost, false)
|
|
974
|
+
payload.ref = buildPinokioRef({
|
|
975
|
+
host: remoteHost,
|
|
976
|
+
port: peerPort(),
|
|
977
|
+
scope: 'api',
|
|
978
|
+
id: requestedAppId
|
|
979
|
+
})
|
|
603
980
|
res.json(payload)
|
|
604
981
|
return
|
|
605
982
|
} catch (error) {
|
|
@@ -645,6 +1022,12 @@ module.exports = function registerAppRoutes(app, { registry, preferences, appSea
|
|
|
645
1022
|
const logData = await appLogs.readLogTail(resolvedLog.file, tail)
|
|
646
1023
|
res.json({
|
|
647
1024
|
app_id: appId,
|
|
1025
|
+
ref: buildPinokioRef({
|
|
1026
|
+
host: currentPeerHost() || '127.0.0.1',
|
|
1027
|
+
port: peerPort(),
|
|
1028
|
+
scope: 'api',
|
|
1029
|
+
id: appId
|
|
1030
|
+
}),
|
|
648
1031
|
script: resolvedLog.script,
|
|
649
1032
|
source: resolvedLog.source,
|
|
650
1033
|
file: registry.toPosixRelative(status.path, resolvedLog.file),
|