pterm 0.0.23 → 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/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
+ }