pinokiod 7.0.4 → 7.0.6
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 +31 -7
- package/server/routes/apps.js +430 -9
package/package.json
CHANGED
|
@@ -52,6 +52,7 @@ Follow these sections in order:
|
|
|
52
52
|
2. Only use Registry Fallback if Search App found no suitable installed app and the user approved it.
|
|
53
53
|
3. Then use Run App.
|
|
54
54
|
4. Then use API Call Strategy if the app exposes an automatable API.
|
|
55
|
+
5. Only use Parallel Mode when the user explicitly asks to use multiple apps or multiple machines in parallel.
|
|
55
56
|
|
|
56
57
|
### 1. Search App
|
|
57
58
|
|
|
@@ -89,7 +90,8 @@ Follow these sections in order:
|
|
|
89
90
|
- If the top candidate is not clearly better than alternatives, ask user once with top 3 candidates.
|
|
90
91
|
- If a suitable installed app is found, select it and continue to Run App.
|
|
91
92
|
- Search results may include apps from other reachable Pinokio machines:
|
|
92
|
-
-
|
|
93
|
+
- prefer the canonical `ref` field when it exists
|
|
94
|
+
- `ref` uses the form `pinokio://<host>:<port>/<scope>/<id>`
|
|
93
95
|
- `source.local=false` means the result is from another machine
|
|
94
96
|
- treat remote results as separate apps; do not merge them with the local app of the same name
|
|
95
97
|
|
|
@@ -112,6 +114,7 @@ Follow these sections in order:
|
|
|
112
114
|
- Poll every 2s.
|
|
113
115
|
- Use status fields from pterm output:
|
|
114
116
|
- `path`: absolute app path to use with `pterm run`
|
|
117
|
+
- `ref`: canonical Pinokio resource reference in the form `pinokio://<host>:<port>/<scope>/<id>`
|
|
115
118
|
- `running`: script is running
|
|
116
119
|
- `ready`: app is reachable/ready
|
|
117
120
|
- `ready_url`: default base URL for API calls when available
|
|
@@ -121,21 +124,21 @@ Follow these sections in order:
|
|
|
121
124
|
- Use `--probe` only for readiness confirmation before first API call (or when status is uncertain).
|
|
122
125
|
- Use `--timeout=<ms>` only when you need a non-default probe timeout.
|
|
123
126
|
- Treat `offline` as expected before first run.
|
|
124
|
-
- If `
|
|
127
|
+
- If `ref` points to another machine or `source.local=false`, the app is remote:
|
|
125
128
|
- treat `path` and `ready_url` as source-local fields, not caller-usable local paths/URLs
|
|
126
129
|
- use `external_ready_urls` in order for caller-side API access when available
|
|
127
|
-
- use `pterm run <
|
|
130
|
+
- use `pterm run <ref>` for remote launch; do not use a remote machine's `path` value as a local path
|
|
128
131
|
- for remote path-based tasks:
|
|
129
132
|
- this applies only when the task expects filesystem paths such as `/path/to/file`
|
|
130
133
|
- do not pass local paths from this machine to the remote app
|
|
131
|
-
- first run `pterm upload <
|
|
134
|
+
- first run `pterm upload <ref> <file...>`
|
|
132
135
|
- then use the returned remote `path` values
|
|
133
136
|
- If app is offline or not ready, run it:
|
|
134
|
-
-
|
|
137
|
+
- If `ref` exists, run `pterm run <ref>`.
|
|
135
138
|
- Otherwise run `pterm run <app_path>`.
|
|
136
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`.
|
|
137
140
|
- Prefer stable launcher selectors such as `run.js?mode=Default`, then broader fallbacks like `run.js`, then installation fallback like `install.js`.
|
|
138
|
-
- Continue polling with `pterm status <app_id>`.
|
|
141
|
+
- Continue polling with `pterm status <ref>` when `ref` exists, otherwise `pterm status <app_id>`.
|
|
139
142
|
- Default startup timeout: 180s.
|
|
140
143
|
- Success criteria:
|
|
141
144
|
- `state=online` and `ready=true`
|
|
@@ -146,7 +149,7 @@ Follow these sections in order:
|
|
|
146
149
|
- timeout before success
|
|
147
150
|
- app drops back to `offline` during startup after a run attempt
|
|
148
151
|
- `pterm run` terminates and status never reaches ready
|
|
149
|
-
- 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:
|
|
150
153
|
- raw log tail
|
|
151
154
|
- short diagnosis
|
|
152
155
|
|
|
@@ -173,6 +176,27 @@ Follow these sections in order:
|
|
|
173
176
|
- Do not execute the app's internal Python/Node/bundled CLI as a fallback when `pterm` has already selected a launcher-managed app.
|
|
174
177
|
- If no automatable API exists after the app is running, report that clearly instead of bypassing the launcher with an internal CLI.
|
|
175
178
|
|
|
179
|
+
### 5. Parallel Mode (explicit only)
|
|
180
|
+
|
|
181
|
+
- Use this section only when the user explicitly asks to:
|
|
182
|
+
- run on multiple machines
|
|
183
|
+
- use multiple apps in parallel
|
|
184
|
+
- compare multiple relevant apps side by side
|
|
185
|
+
- generate multiple outputs concurrently
|
|
186
|
+
- Do not use this mode by default.
|
|
187
|
+
- Keep each selected app as a separate target. Prefer `ref` as the target identifier when it exists.
|
|
188
|
+
- Selection rules:
|
|
189
|
+
- if the user asks for all relevant apps, use all relevant search results that can perform the task
|
|
190
|
+
- if the user asks for a specific count, use the top N relevant search results after normal search ranking
|
|
191
|
+
- if the user asks for parallel use but does not specify how many apps or machines to use, ask once
|
|
192
|
+
- Ranking still applies in this mode:
|
|
193
|
+
- prefer `ready` apps first
|
|
194
|
+
- then `running` apps
|
|
195
|
+
- then offline apps if more targets are still needed
|
|
196
|
+
- Run and monitor each selected target independently.
|
|
197
|
+
- Keep outputs labeled by target `ref` when it exists, otherwise `app_id`.
|
|
198
|
+
- For remote path-based tasks, run `pterm upload <ref> <file...>` separately for each remote target when `ref` exists, otherwise fall back to `app_id`.
|
|
199
|
+
|
|
176
200
|
## Behavior Rules
|
|
177
201
|
|
|
178
202
|
- Do not add app-specific hardcoding when user gave only capability (for example "tts").
|
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 : ''
|
|
@@ -577,7 +948,51 @@ module.exports = function registerAppRoutes(app, { registry, preferences, appSea
|
|
|
577
948
|
}))
|
|
578
949
|
|
|
579
950
|
router.get('/apps/logs/:app_id', asyncHandler(async (req, res) => {
|
|
580
|
-
const
|
|
951
|
+
const parsedAppId = parseQualifiedAppId(req.params.app_id)
|
|
952
|
+
const requestedAppId = parsedAppId.app_id || req.params.app_id
|
|
953
|
+
const remoteHost = parsedAppId.qualified ? parsedAppId.host : null
|
|
954
|
+
if (remoteHost && remoteHost !== currentPeerHost()) {
|
|
955
|
+
try {
|
|
956
|
+
const params = {}
|
|
957
|
+
if (typeof req.query.script === 'string' && req.query.script.trim()) {
|
|
958
|
+
params.script = req.query.script
|
|
959
|
+
}
|
|
960
|
+
const tail = registry.parseTailCount(req.query.tail, 200)
|
|
961
|
+
if (Number.isFinite(tail) && tail > 0) {
|
|
962
|
+
params.tail = String(tail)
|
|
963
|
+
}
|
|
964
|
+
const response = await axios.get(`http://${remoteHost}:${peerPort()}/apps/logs/${encodeURIComponent(requestedAppId)}`, {
|
|
965
|
+
timeout: DEFAULT_PEER_TIMEOUT_MS,
|
|
966
|
+
headers: peerRequestHeaders(req),
|
|
967
|
+
params
|
|
968
|
+
})
|
|
969
|
+
const payload = response && response.data && typeof response.data === 'object'
|
|
970
|
+
? { ...response.data }
|
|
971
|
+
: {}
|
|
972
|
+
payload.app_id = qualifyAppId(requestedAppId, remoteHost)
|
|
973
|
+
payload.source = buildSource(remoteHost, false)
|
|
974
|
+
payload.ref = buildPinokioRef({
|
|
975
|
+
host: remoteHost,
|
|
976
|
+
port: peerPort(),
|
|
977
|
+
scope: 'api',
|
|
978
|
+
id: requestedAppId
|
|
979
|
+
})
|
|
980
|
+
res.json(payload)
|
|
981
|
+
return
|
|
982
|
+
} catch (error) {
|
|
983
|
+
if (error && error.response) {
|
|
984
|
+
res.status(error.response.status).json(error.response.data)
|
|
985
|
+
return
|
|
986
|
+
}
|
|
987
|
+
res.status(502).json({
|
|
988
|
+
error: 'Peer logs unavailable',
|
|
989
|
+
app_id: qualifyAppId(requestedAppId, remoteHost),
|
|
990
|
+
source: buildSource(remoteHost, false)
|
|
991
|
+
})
|
|
992
|
+
return
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
const appId = registry.normalizeAppId(requestedAppId)
|
|
581
996
|
if (!appId) {
|
|
582
997
|
res.status(400).json({ error: 'Invalid app_id' })
|
|
583
998
|
return
|
|
@@ -607,6 +1022,12 @@ module.exports = function registerAppRoutes(app, { registry, preferences, appSea
|
|
|
607
1022
|
const logData = await appLogs.readLogTail(resolvedLog.file, tail)
|
|
608
1023
|
res.json({
|
|
609
1024
|
app_id: appId,
|
|
1025
|
+
ref: buildPinokioRef({
|
|
1026
|
+
host: currentPeerHost() || '127.0.0.1',
|
|
1027
|
+
port: peerPort(),
|
|
1028
|
+
scope: 'api',
|
|
1029
|
+
id: appId
|
|
1030
|
+
}),
|
|
610
1031
|
script: resolvedLog.script,
|
|
611
1032
|
source: resolvedLog.source,
|
|
612
1033
|
file: registry.toPosixRelative(status.path, resolvedLog.file),
|