pinokiod 7.0.5 → 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 +12 -10
- package/server/routes/apps.js +391 -8
package/package.json
CHANGED
|
@@ -90,7 +90,8 @@ Follow these sections in order:
|
|
|
90
90
|
- If the top candidate is not clearly better than alternatives, ask user once with top 3 candidates.
|
|
91
91
|
- If a suitable installed app is found, select it and continue to Run 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
|
|
|
@@ -113,6 +114,7 @@ Follow these sections in order:
|
|
|
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,7 +149,7 @@ 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
|
|
153
155
|
|
|
@@ -182,7 +184,7 @@ Follow these sections in order:
|
|
|
182
184
|
- compare multiple relevant apps side by side
|
|
183
185
|
- generate multiple outputs concurrently
|
|
184
186
|
- Do not use this mode by default.
|
|
185
|
-
- Keep each selected app as a separate target
|
|
187
|
+
- Keep each selected app as a separate target. Prefer `ref` as the target identifier when it exists.
|
|
186
188
|
- Selection rules:
|
|
187
189
|
- if the user asks for all relevant apps, use all relevant search results that can perform the task
|
|
188
190
|
- if the user asks for a specific count, use the top N relevant search results after normal search ranking
|
|
@@ -192,8 +194,8 @@ Follow these sections in order:
|
|
|
192
194
|
- then `running` apps
|
|
193
195
|
- then offline apps if more targets are still needed
|
|
194
196
|
- Run and monitor each selected target independently.
|
|
195
|
-
- Keep outputs labeled by target `app_id`.
|
|
196
|
-
- For remote path-based tasks, run `pterm upload <
|
|
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`.
|
|
197
199
|
|
|
198
200
|
## Behavior Rules
|
|
199
201
|
|
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),
|