pterm 0.0.24 → 0.0.25
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/README.md +181 -15
- package/endpoint.js +163 -0
- package/index.js +70 -15
- package/package.json +1 -1
- package/rpc.js +26 -8
- package/script.js +114 -59
- package/target.js +437 -0
- package/util.js +146 -18
package/target.js
ADDED
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
const os = require('os')
|
|
2
|
+
const path = require('path')
|
|
3
|
+
const axios = require('axios')
|
|
4
|
+
const { DEFAULT_PORT, resolveHttpBaseUrl } = require('./endpoint')
|
|
5
|
+
|
|
6
|
+
const IPV4_HOST_PATTERN = /^(?:\d{1,3}\.){3}\d{1,3}$/
|
|
7
|
+
const PINOKIO_REF_PROTOCOL = 'pinokio:'
|
|
8
|
+
|
|
9
|
+
const isHttpUri = (value) => typeof value === 'string' && /^https?:\/\//i.test(value)
|
|
10
|
+
const isPinokioRef = (value) => typeof value === 'string' && value.trim().toLowerCase().startsWith('pinokio://')
|
|
11
|
+
const isLoopbackHost = (value = '') => {
|
|
12
|
+
const normalized = String(value || '').trim().toLowerCase()
|
|
13
|
+
return normalized === '127.0.0.1' || normalized === 'localhost' || normalized === '::1' || normalized === '[::1]'
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const parsePinokioRef = (value = '') => {
|
|
17
|
+
if (typeof value !== 'string') {
|
|
18
|
+
return {
|
|
19
|
+
valid: false,
|
|
20
|
+
error: 'Invalid ref'
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const trimmed = value.trim()
|
|
24
|
+
if (!trimmed) {
|
|
25
|
+
return {
|
|
26
|
+
valid: false,
|
|
27
|
+
error: 'Missing ref'
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
let parsed
|
|
31
|
+
try {
|
|
32
|
+
parsed = new URL(trimmed)
|
|
33
|
+
} catch (_) {
|
|
34
|
+
return {
|
|
35
|
+
valid: false,
|
|
36
|
+
error: 'Invalid ref'
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (parsed.protocol !== PINOKIO_REF_PROTOCOL) {
|
|
40
|
+
return {
|
|
41
|
+
valid: false,
|
|
42
|
+
error: 'Invalid ref protocol'
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const host = typeof parsed.hostname === 'string' ? parsed.hostname.trim() : ''
|
|
46
|
+
const port = Number.parseInt(String(parsed.port || ''), 10)
|
|
47
|
+
const pathSegments = String(parsed.pathname || '')
|
|
48
|
+
.split('/')
|
|
49
|
+
.filter(Boolean)
|
|
50
|
+
.map((segment) => {
|
|
51
|
+
try {
|
|
52
|
+
return decodeURIComponent(segment)
|
|
53
|
+
} catch (_) {
|
|
54
|
+
return segment
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
const scope = pathSegments.length > 0 ? pathSegments[0] : ''
|
|
58
|
+
const id = pathSegments.length > 1 ? pathSegments.slice(1).join('/') : ''
|
|
59
|
+
if (!host || !Number.isFinite(port) || port <= 0 || !scope || !id) {
|
|
60
|
+
return {
|
|
61
|
+
valid: false,
|
|
62
|
+
error: 'Invalid ref'
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
valid: true,
|
|
67
|
+
ref: trimmed,
|
|
68
|
+
host,
|
|
69
|
+
port,
|
|
70
|
+
scope,
|
|
71
|
+
id
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const buildPinokioRef = ({ host, port, scope, id }) => {
|
|
76
|
+
const normalizedHost = typeof host === 'string' ? host.trim() : ''
|
|
77
|
+
const normalizedScope = typeof scope === 'string' ? scope.trim() : ''
|
|
78
|
+
const normalizedId = typeof id === 'string' ? id.trim() : ''
|
|
79
|
+
const normalizedPort = Number.parseInt(String(port || ''), 10)
|
|
80
|
+
if (!normalizedHost || !normalizedScope || !normalizedId || !Number.isFinite(normalizedPort) || normalizedPort <= 0) {
|
|
81
|
+
return null
|
|
82
|
+
}
|
|
83
|
+
const encodedPath = [normalizedScope, ...normalizedId.split('/').filter(Boolean)]
|
|
84
|
+
.map((segment) => encodeURIComponent(segment))
|
|
85
|
+
.join('/')
|
|
86
|
+
return `pinokio://${normalizedHost}:${normalizedPort}/${encodedPath}`
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const parseQualifiedAppId = (value = '') => {
|
|
90
|
+
if (typeof value !== 'string') {
|
|
91
|
+
return {
|
|
92
|
+
appId: '',
|
|
93
|
+
host: null,
|
|
94
|
+
qualified: false
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const trimmed = value.trim()
|
|
98
|
+
const atIndex = trimmed.lastIndexOf('@')
|
|
99
|
+
if (atIndex <= 0 || atIndex >= trimmed.length - 1) {
|
|
100
|
+
return {
|
|
101
|
+
appId: trimmed,
|
|
102
|
+
host: null,
|
|
103
|
+
qualified: false
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const appId = trimmed.slice(0, atIndex).trim()
|
|
107
|
+
const host = trimmed.slice(atIndex + 1).trim()
|
|
108
|
+
if (!appId || !IPV4_HOST_PATTERN.test(host)) {
|
|
109
|
+
return {
|
|
110
|
+
appId: trimmed,
|
|
111
|
+
host: null,
|
|
112
|
+
qualified: false
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
appId,
|
|
117
|
+
host,
|
|
118
|
+
qualified: true
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const buildControlPlane = (host, port = DEFAULT_PORT) => ({
|
|
123
|
+
protocol: 'http',
|
|
124
|
+
host,
|
|
125
|
+
port
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
const buildPeerControlPlane = (host) => buildControlPlane(host, DEFAULT_PORT)
|
|
129
|
+
|
|
130
|
+
const isDirectScriptTarget = (value) => {
|
|
131
|
+
if (typeof value !== 'string') {
|
|
132
|
+
return false
|
|
133
|
+
}
|
|
134
|
+
return isHttpUri(value) || value.startsWith('~/') || path.isAbsolute(value)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const looksLikeRelativeScriptTarget = (value) => {
|
|
138
|
+
if (typeof value !== 'string') {
|
|
139
|
+
return false
|
|
140
|
+
}
|
|
141
|
+
const trimmed = value.trim()
|
|
142
|
+
if (!trimmed) {
|
|
143
|
+
return false
|
|
144
|
+
}
|
|
145
|
+
if (trimmed.startsWith('./') || trimmed.startsWith('../')) {
|
|
146
|
+
return true
|
|
147
|
+
}
|
|
148
|
+
const queryIndex = trimmed.indexOf('?')
|
|
149
|
+
const scriptPath = queryIndex >= 0 ? trimmed.slice(0, queryIndex) : trimmed
|
|
150
|
+
if (!scriptPath) {
|
|
151
|
+
return false
|
|
152
|
+
}
|
|
153
|
+
if (scriptPath.includes('/') || scriptPath.includes('\\')) {
|
|
154
|
+
return true
|
|
155
|
+
}
|
|
156
|
+
const extension = path.extname(scriptPath).toLowerCase()
|
|
157
|
+
return ['.js', '.json', '.mjs', '.cjs'].includes(extension)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const appendQuery = (targetPath, queryString) => {
|
|
161
|
+
if (!queryString) {
|
|
162
|
+
return targetPath
|
|
163
|
+
}
|
|
164
|
+
return `${targetPath}?${queryString}`
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const expandHomeTarget = async (target, controlPlane = null) => {
|
|
168
|
+
if (typeof target !== 'string' || !target.startsWith('~/')) {
|
|
169
|
+
return target
|
|
170
|
+
}
|
|
171
|
+
if (controlPlane) {
|
|
172
|
+
throw new Error("remote ~/ paths are not supported; use a relative script path with --ref or an absolute remote path")
|
|
173
|
+
}
|
|
174
|
+
const queryIndex = target.indexOf('?')
|
|
175
|
+
const scriptPath = queryIndex >= 0 ? target.slice(0, queryIndex) : target
|
|
176
|
+
const queryString = queryIndex >= 0 ? target.slice(queryIndex + 1) : ''
|
|
177
|
+
return appendQuery(path.resolve(os.homedir(), scriptPath.slice(2)), queryString)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const fetchApiResourceStatus = async (parsedRef, options = {}) => {
|
|
181
|
+
if (!parsedRef || !parsedRef.valid) {
|
|
182
|
+
throw new Error('Invalid ref')
|
|
183
|
+
}
|
|
184
|
+
if (parsedRef.scope !== 'api') {
|
|
185
|
+
throw new Error(`Unsupported ref scope: ${parsedRef.scope}`)
|
|
186
|
+
}
|
|
187
|
+
const controlPlane = buildControlPlane(parsedRef.host, parsedRef.port)
|
|
188
|
+
const baseUrl = await resolveHttpBaseUrl(controlPlane)
|
|
189
|
+
const params = {}
|
|
190
|
+
if (typeof options.probe !== 'undefined') {
|
|
191
|
+
params.probe = options.probe
|
|
192
|
+
}
|
|
193
|
+
if (typeof options.timeout !== 'undefined' && options.timeout !== null) {
|
|
194
|
+
params.timeout = String(options.timeout)
|
|
195
|
+
}
|
|
196
|
+
const response = await axios.get(`${baseUrl}/pinokio/resource/status`, {
|
|
197
|
+
params: {
|
|
198
|
+
ref: buildPinokioRef(parsedRef),
|
|
199
|
+
...params
|
|
200
|
+
}
|
|
201
|
+
})
|
|
202
|
+
const status = response && response.data ? response.data : {}
|
|
203
|
+
const appPath = typeof status.path === 'string' ? status.path.trim() : ''
|
|
204
|
+
if (!appPath) {
|
|
205
|
+
throw new Error(`resource path unavailable: ${parsedRef.ref || buildPinokioRef(parsedRef)}`)
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
appPath,
|
|
209
|
+
status,
|
|
210
|
+
controlPlane,
|
|
211
|
+
remote: !isLoopbackHost(parsedRef.host),
|
|
212
|
+
appId: parsedRef.id,
|
|
213
|
+
host: parsedRef.host,
|
|
214
|
+
port: parsedRef.port,
|
|
215
|
+
ref: buildPinokioRef(parsedRef)
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const resolveAppStatusContext = async (appRef) => {
|
|
220
|
+
if (isPinokioRef(appRef)) {
|
|
221
|
+
const parsedRef = parsePinokioRef(appRef)
|
|
222
|
+
if (!parsedRef.valid) {
|
|
223
|
+
throw new Error(parsedRef.error || 'Invalid ref')
|
|
224
|
+
}
|
|
225
|
+
return fetchApiResourceStatus(parsedRef)
|
|
226
|
+
}
|
|
227
|
+
const parsedAppId = parseQualifiedAppId(appRef)
|
|
228
|
+
const controlPlane = parsedAppId.qualified ? buildPeerControlPlane(parsedAppId.host) : null
|
|
229
|
+
const baseUrl = await resolveHttpBaseUrl(controlPlane)
|
|
230
|
+
const statusAppId = parsedAppId.qualified ? parsedAppId.appId : appRef
|
|
231
|
+
const response = await axios.get(`${baseUrl}/apps/status/${encodeURIComponent(statusAppId)}`)
|
|
232
|
+
const status = response && response.data ? response.data : {}
|
|
233
|
+
const appPath = typeof status.path === 'string' ? status.path.trim() : ''
|
|
234
|
+
if (!appPath) {
|
|
235
|
+
throw new Error(`app path unavailable: ${appRef}`)
|
|
236
|
+
}
|
|
237
|
+
return {
|
|
238
|
+
appPath,
|
|
239
|
+
status,
|
|
240
|
+
controlPlane,
|
|
241
|
+
remote: Boolean(parsedAppId.qualified),
|
|
242
|
+
appId: parsedAppId.qualified ? parsedAppId.appId : appRef,
|
|
243
|
+
host: parsedAppId.qualified ? parsedAppId.host : null
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const resolveAppControlTarget = async (rawUri) => {
|
|
248
|
+
if (isHttpUri(rawUri)) {
|
|
249
|
+
return {
|
|
250
|
+
uri: rawUri,
|
|
251
|
+
controlPlane: null,
|
|
252
|
+
remote: false
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
if (isPinokioRef(rawUri)) {
|
|
256
|
+
const parsedRef = parsePinokioRef(rawUri)
|
|
257
|
+
if (!parsedRef.valid) {
|
|
258
|
+
throw new Error(parsedRef.error || 'Invalid ref')
|
|
259
|
+
}
|
|
260
|
+
const context = await fetchApiResourceStatus(parsedRef)
|
|
261
|
+
return {
|
|
262
|
+
uri: context.appPath,
|
|
263
|
+
controlPlane: context.controlPlane,
|
|
264
|
+
remote: context.remote,
|
|
265
|
+
appId: context.appId,
|
|
266
|
+
host: context.host,
|
|
267
|
+
port: context.port,
|
|
268
|
+
ref: context.ref
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
const parsedAppId = parseQualifiedAppId(rawUri)
|
|
272
|
+
if (!parsedAppId.qualified) {
|
|
273
|
+
return {
|
|
274
|
+
uri: path.resolve(process.cwd(), rawUri),
|
|
275
|
+
controlPlane: null,
|
|
276
|
+
remote: false
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
const controlPlane = buildPeerControlPlane(parsedAppId.host)
|
|
280
|
+
const baseUrl = await resolveHttpBaseUrl(controlPlane)
|
|
281
|
+
const response = await axios.get(`${baseUrl}/apps/status/${encodeURIComponent(parsedAppId.appId)}`)
|
|
282
|
+
const remotePath = response && response.data && typeof response.data.path === 'string'
|
|
283
|
+
? response.data.path.trim()
|
|
284
|
+
: ''
|
|
285
|
+
if (!remotePath) {
|
|
286
|
+
throw new Error(`remote app path unavailable: ${parsedAppId.appId}@${parsedAppId.host}`)
|
|
287
|
+
}
|
|
288
|
+
return {
|
|
289
|
+
uri: remotePath,
|
|
290
|
+
controlPlane,
|
|
291
|
+
remote: true,
|
|
292
|
+
appId: parsedAppId.appId,
|
|
293
|
+
host: parsedAppId.host
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const resolveStartTarget = async (rawUri, appRef = '') => {
|
|
298
|
+
const target = typeof rawUri === 'string' ? rawUri.trim() : ''
|
|
299
|
+
if (!target) {
|
|
300
|
+
return {
|
|
301
|
+
uri: rawUri,
|
|
302
|
+
controlPlane: null,
|
|
303
|
+
remote: false
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
if (!appRef || !String(appRef).trim()) {
|
|
307
|
+
return {
|
|
308
|
+
uri: rawUri,
|
|
309
|
+
controlPlane: null,
|
|
310
|
+
remote: false
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
const context = await resolveAppStatusContext(String(appRef).trim())
|
|
314
|
+
if (isDirectScriptTarget(target)) {
|
|
315
|
+
const directUri = target.startsWith('~/')
|
|
316
|
+
? await expandHomeTarget(target, context.controlPlane)
|
|
317
|
+
: rawUri
|
|
318
|
+
return {
|
|
319
|
+
uri: directUri,
|
|
320
|
+
controlPlane: context.controlPlane,
|
|
321
|
+
remote: context.remote,
|
|
322
|
+
appId: context.appId,
|
|
323
|
+
host: context.host
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
const queryIndex = target.indexOf('?')
|
|
327
|
+
const scriptPath = queryIndex >= 0 ? target.slice(0, queryIndex) : target
|
|
328
|
+
const queryString = queryIndex >= 0 ? target.slice(queryIndex + 1) : ''
|
|
329
|
+
return {
|
|
330
|
+
uri: appendQuery(path.resolve(context.appPath, scriptPath), queryString),
|
|
331
|
+
controlPlane: context.controlPlane,
|
|
332
|
+
remote: context.remote,
|
|
333
|
+
appId: context.appId,
|
|
334
|
+
host: context.host
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const resolveStopControlTarget = async (rawUri) => {
|
|
339
|
+
const target = typeof rawUri === 'string' ? rawUri.trim() : ''
|
|
340
|
+
if (isPinokioRef(target)) {
|
|
341
|
+
const parsedRef = parsePinokioRef(target)
|
|
342
|
+
if (!parsedRef.valid) {
|
|
343
|
+
throw new Error(parsedRef.error || 'Invalid ref')
|
|
344
|
+
}
|
|
345
|
+
const context = await fetchApiResourceStatus(parsedRef)
|
|
346
|
+
const runningScripts = Array.isArray(context.status.running_scripts)
|
|
347
|
+
? context.status.running_scripts.filter((script) => typeof script === 'string' && script.trim())
|
|
348
|
+
: []
|
|
349
|
+
return {
|
|
350
|
+
uris: runningScripts.map((script) => path.resolve(context.appPath, script)),
|
|
351
|
+
controlPlane: context.controlPlane,
|
|
352
|
+
remote: context.remote,
|
|
353
|
+
appId: context.appId,
|
|
354
|
+
host: context.host,
|
|
355
|
+
port: context.port,
|
|
356
|
+
ref: context.ref
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
if (isDirectScriptTarget(target) || looksLikeRelativeScriptTarget(target)) {
|
|
360
|
+
if (isHttpUri(rawUri)) {
|
|
361
|
+
return {
|
|
362
|
+
uris: [rawUri],
|
|
363
|
+
controlPlane: null,
|
|
364
|
+
remote: false
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
const directUri = target.startsWith('~/')
|
|
368
|
+
? await expandHomeTarget(target)
|
|
369
|
+
: path.resolve(process.cwd(), target)
|
|
370
|
+
return {
|
|
371
|
+
uris: [directUri],
|
|
372
|
+
controlPlane: null,
|
|
373
|
+
remote: false
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
const parsedAppId = parseQualifiedAppId(target)
|
|
377
|
+
const controlPlane = parsedAppId.qualified ? buildPeerControlPlane(parsedAppId.host) : null
|
|
378
|
+
const baseUrl = await resolveHttpBaseUrl(controlPlane)
|
|
379
|
+
const statusAppId = parsedAppId.qualified ? parsedAppId.appId : target
|
|
380
|
+
const response = await axios.get(`${baseUrl}/apps/status/${encodeURIComponent(statusAppId)}`)
|
|
381
|
+
const status = response && response.data ? response.data : {}
|
|
382
|
+
const appPath = typeof status.path === 'string' ? status.path.trim() : ''
|
|
383
|
+
const runningScripts = Array.isArray(status.running_scripts)
|
|
384
|
+
? status.running_scripts.filter((script) => typeof script === 'string' && script.trim())
|
|
385
|
+
: []
|
|
386
|
+
return {
|
|
387
|
+
uris: appPath
|
|
388
|
+
? runningScripts.map((script) => path.resolve(appPath, script))
|
|
389
|
+
: [],
|
|
390
|
+
controlPlane,
|
|
391
|
+
remote: Boolean(parsedAppId.qualified),
|
|
392
|
+
appId: parsedAppId.qualified ? parsedAppId.appId : rawUri,
|
|
393
|
+
host: parsedAppId.qualified ? parsedAppId.host : null
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const resolveStopTarget = async (rawUri, appRef = '') => {
|
|
398
|
+
const target = typeof rawUri === 'string' ? rawUri.trim() : ''
|
|
399
|
+
if (!target || !appRef || !String(appRef).trim()) {
|
|
400
|
+
return resolveStopControlTarget(rawUri)
|
|
401
|
+
}
|
|
402
|
+
const context = await resolveAppStatusContext(String(appRef).trim())
|
|
403
|
+
if (isDirectScriptTarget(target)) {
|
|
404
|
+
const directUri = target.startsWith('~/')
|
|
405
|
+
? await expandHomeTarget(target, context.controlPlane)
|
|
406
|
+
: target
|
|
407
|
+
return {
|
|
408
|
+
uris: [directUri],
|
|
409
|
+
controlPlane: context.controlPlane,
|
|
410
|
+
remote: context.remote,
|
|
411
|
+
appId: context.appId,
|
|
412
|
+
host: context.host
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
const queryIndex = target.indexOf('?')
|
|
416
|
+
const scriptPath = queryIndex >= 0 ? target.slice(0, queryIndex) : target
|
|
417
|
+
const queryString = queryIndex >= 0 ? target.slice(queryIndex + 1) : ''
|
|
418
|
+
return {
|
|
419
|
+
uris: [appendQuery(path.resolve(context.appPath, scriptPath), queryString)],
|
|
420
|
+
controlPlane: context.controlPlane,
|
|
421
|
+
remote: context.remote,
|
|
422
|
+
appId: context.appId,
|
|
423
|
+
host: context.host
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
module.exports = {
|
|
428
|
+
isHttpUri,
|
|
429
|
+
isPinokioRef,
|
|
430
|
+
parsePinokioRef,
|
|
431
|
+
buildPinokioRef,
|
|
432
|
+
parseQualifiedAppId,
|
|
433
|
+
resolveAppControlTarget,
|
|
434
|
+
resolveStartTarget,
|
|
435
|
+
resolveStopTarget,
|
|
436
|
+
resolveStopControlTarget,
|
|
437
|
+
}
|