pinokiod 7.3.1 → 7.3.4

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.
Files changed (122) hide show
  1. package/kernel/api/github/index.js +444 -0
  2. package/kernel/api/index.js +199 -11
  3. package/kernel/api/process/index.js +124 -44
  4. package/kernel/api/shell_run_template.js +273 -0
  5. package/kernel/api/uri/index.js +51 -0
  6. package/kernel/bin/git.js +9 -10
  7. package/kernel/bin/huggingface.js +1 -1
  8. package/kernel/bin/zip.js +9 -1
  9. package/kernel/connect/providers/github/README.md +5 -4
  10. package/kernel/environment.js +195 -92
  11. package/kernel/git.js +126 -19
  12. package/kernel/gitconfig_template +7 -0
  13. package/kernel/gpu/amd.js +72 -0
  14. package/kernel/gpu/apple.js +8 -0
  15. package/kernel/gpu/common.js +12 -0
  16. package/kernel/gpu/intel.js +47 -0
  17. package/kernel/gpu/nvidia.js +8 -0
  18. package/kernel/index.js +11 -1
  19. package/kernel/managed_skills.js +871 -0
  20. package/kernel/plugin.js +6 -58
  21. package/kernel/plugin_sources.js +316 -0
  22. package/kernel/resource_usage/gpu.js +349 -0
  23. package/kernel/resource_usage/index.js +322 -0
  24. package/kernel/resource_usage/macos_footprint.js +197 -0
  25. package/kernel/resource_usage/preferences.js +92 -0
  26. package/kernel/resource_usage/process_tree.js +303 -0
  27. package/kernel/scripts/git/create +4 -4
  28. package/kernel/scripts/git/fork +7 -8
  29. package/kernel/shell.js +23 -2
  30. package/kernel/shells.js +41 -0
  31. package/kernel/sysinfo.js +62 -9
  32. package/kernel/util.js +60 -0
  33. package/package.json +1 -1
  34. package/server/index.js +984 -156
  35. package/server/lib/app_log_report.js +543 -0
  36. package/server/lib/content_validation.js +55 -33
  37. package/server/lib/launcher_instruction_bootstrap.js +4 -96
  38. package/server/lib/terminal_session_helpers.js +0 -3
  39. package/server/public/common.js +77 -31
  40. package/server/public/create-launcher.js +4 -32
  41. package/server/public/logs.js +1428 -0
  42. package/server/public/nav.js +7 -0
  43. package/server/public/plugin-detail.js +93 -10
  44. package/server/public/privacy_filter_worker.js +391 -0
  45. package/server/public/style.css +1104 -154
  46. package/server/public/task-launcher.js +8 -29
  47. package/server/public/universal-launcher.css +8 -6
  48. package/server/public/universal-launcher.js +3 -27
  49. package/server/routes/apps.js +195 -1
  50. package/server/views/app.ejs +3041 -717
  51. package/server/views/autolaunch.ejs +917 -0
  52. package/server/views/bootstrap.ejs +7 -1
  53. package/server/views/d.ejs +408 -65
  54. package/server/views/editor.ejs +85 -19
  55. package/server/views/index.ejs +661 -111
  56. package/server/views/init/index.ejs +1 -1
  57. package/server/views/install.ejs +1 -1
  58. package/server/views/logs.ejs +164 -86
  59. package/server/views/net.ejs +7 -1
  60. package/server/views/partials/d_terminal_column.ejs +2 -2
  61. package/server/views/partials/d_terminal_options.ejs +0 -8
  62. package/server/views/partials/fs_status.ejs +47 -0
  63. package/server/views/partials/home_action_modal.ejs +86 -0
  64. package/server/views/partials/home_run_menu.ejs +87 -0
  65. package/server/views/partials/main_sidebar.ejs +2 -0
  66. package/server/views/partials/menu.ejs +1 -1
  67. package/server/views/plugin_detail.ejs +19 -4
  68. package/server/views/plugins.ejs +201 -3
  69. package/server/views/pre.ejs +1 -1
  70. package/server/views/pro.ejs +1 -1
  71. package/server/views/shell.ejs +40 -18
  72. package/server/views/skills.ejs +506 -0
  73. package/server/views/terminal.ejs +45 -19
  74. package/spec/INSTRUCTION_SYNC.md +20 -10
  75. package/system/plugin/antigravity-cli/antigravity.png +0 -0
  76. package/system/plugin/antigravity-cli/common.js +155 -0
  77. package/system/plugin/antigravity-cli/install.js +272 -0
  78. package/system/plugin/antigravity-cli/pinokio.js +13 -0
  79. package/system/plugin/antigravity-cli-auto/antigravity.png +0 -0
  80. package/system/plugin/antigravity-cli-auto/pinokio.js +13 -0
  81. package/system/plugin/claude/claude.png +0 -0
  82. package/system/plugin/claude/pinokio.js +47 -0
  83. package/system/plugin/claude-auto/claude.png +0 -0
  84. package/system/plugin/claude-auto/pinokio.js +58 -0
  85. package/system/plugin/claude-desktop/icon.jpeg +0 -0
  86. package/system/plugin/claude-desktop/pinokio.js +23 -0
  87. package/system/plugin/codex/openai.webp +0 -0
  88. package/system/plugin/codex/pinokio.js +42 -0
  89. package/system/plugin/codex-auto/openai.webp +0 -0
  90. package/system/plugin/codex-auto/pinokio.js +49 -0
  91. package/system/plugin/codex-desktop/icon.png +0 -0
  92. package/system/plugin/codex-desktop/pinokio.js +23 -0
  93. package/system/plugin/crush/crush.png +0 -0
  94. package/system/plugin/crush/pinokio.js +15 -0
  95. package/system/plugin/cursor/cursor.jpeg +0 -0
  96. package/system/plugin/cursor/pinokio.js +23 -0
  97. package/system/plugin/qwen/pinokio.js +34 -0
  98. package/system/plugin/qwen/qwen.png +0 -0
  99. package/system/plugin/vscode/pinokio.js +20 -0
  100. package/system/plugin/vscode/vscode.png +0 -0
  101. package/system/plugin/windsurf/pinokio.js +23 -0
  102. package/system/plugin/windsurf/windsurf.png +0 -0
  103. package/test/antigravity-cli-plugin.test.js +185 -0
  104. package/test/app-api.test.js +239 -0
  105. package/test/app-log-report.test.js +67 -0
  106. package/test/environment-cache-preflight.test.js +98 -0
  107. package/test/git-bin.test.js +59 -0
  108. package/test/git-defaults.test.js +150 -0
  109. package/test/github-api.test.js +158 -0
  110. package/test/github-connection.test.js +117 -0
  111. package/test/huggingface-bin.test.js +25 -0
  112. package/test/managed-skills.test.js +351 -0
  113. package/test/plugin-action-functions.test.js +337 -0
  114. package/test/plugin-dev-iframe.test.js +17 -0
  115. package/test/plugin-sources.test.js +203 -0
  116. package/test/privacy-filter-worker-heuristics.test.js +69 -0
  117. package/test/process-wait.test.js +169 -0
  118. package/test/script-api.test.js +97 -0
  119. package/test/shell-api.test.js +134 -0
  120. package/test/shell-run-template.test.js +209 -0
  121. package/test/storage-api.test.js +137 -0
  122. package/test/uri-api.test.js +100 -0
@@ -0,0 +1,543 @@
1
+ const fs = require('fs')
2
+ const os = require('os')
3
+ const path = require('path')
4
+
5
+ const DEFAULT_TAIL_LINES = 800
6
+ const MAX_SECTION_CHARS = 120000
7
+ const SENSITIVE_ENV_KEY = /(?:TOKEN|SECRET|PASSWORD|PASSWD|PASSPHRASE|API[_-]?KEY|APIKEY|CREDENTIAL|COOKIE|SESSION|AUTH|PRIVATE[_-]?KEY)/i
8
+
9
+ const escapeRegExp = (value) => String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
10
+
11
+ class AppLogReportService {
12
+ constructor({ registry, kernel = null }) {
13
+ if (!registry) {
14
+ throw new Error('AppLogReportService requires registry')
15
+ }
16
+ this.registry = registry
17
+ this.kernel = kernel
18
+ }
19
+
20
+ async buildReport({ appId, status, tail = DEFAULT_TAIL_LINES, redact = true }) {
21
+ const appRoot = status && status.path ? status.path : null
22
+ if (!appRoot) {
23
+ return null
24
+ }
25
+ const tailLines = this.registry.parseTailCount(tail, DEFAULT_TAIL_LINES)
26
+ const rawSections = await this.collectSections(appRoot, tailLines)
27
+ const sections = []
28
+ const totals = {}
29
+
30
+ for (const section of rawSections) {
31
+ if (redact) {
32
+ const redacted = this.redactText(section.text, { appRoot })
33
+ this.mergeCounts(totals, redacted.counts)
34
+ sections.push({
35
+ ...section,
36
+ text: redacted.text,
37
+ redactions: redacted.counts
38
+ })
39
+ } else {
40
+ sections.push({
41
+ ...section,
42
+ redactions: {}
43
+ })
44
+ }
45
+ }
46
+
47
+ const metadata = {
48
+ app_id: appId,
49
+ title: status.title || appId,
50
+ repo_url: this.sanitizeRemoteUrl(status.repo_url || this.readGitRemote(appRoot)),
51
+ generated_at: new Date().toISOString(),
52
+ pinokiod: this.readPinokioVersion(),
53
+ platform: os.platform(),
54
+ arch: os.arch(),
55
+ node: process.version,
56
+ tail_count: tailLines,
57
+ system_spec: this.buildSystemSpec(),
58
+ redaction_mode: redact ? 'server_deterministic' : 'none'
59
+ }
60
+
61
+ return {
62
+ ...metadata,
63
+ section_count: sections.length,
64
+ sections,
65
+ redactions: totals,
66
+ markdown: this.renderMarkdown(metadata, sections, totals)
67
+ }
68
+ }
69
+
70
+ async collectSections(appRoot, tailLines) {
71
+ const sections = []
72
+ const apiLogsRoot = path.resolve(appRoot, 'logs', 'api')
73
+
74
+ if (await this.registry.pathIsDirectory(apiLogsRoot)) {
75
+ const latestFiles = await this.findNamedFiles(apiLogsRoot, 'latest')
76
+ for (const file of latestFiles) {
77
+ const relativeDir = path.relative(apiLogsRoot, path.dirname(file))
78
+ const script = this.toPosix(relativeDir)
79
+ sections.push(await this.buildSection({
80
+ appRoot,
81
+ root: apiLogsRoot,
82
+ file,
83
+ source: 'api',
84
+ script,
85
+ tailLines
86
+ }))
87
+ }
88
+ }
89
+
90
+ return sections
91
+ .filter(Boolean)
92
+ .sort((a, b) => this.compareSections(a, b))
93
+ }
94
+
95
+ async findNamedFiles(root, filename) {
96
+ const out = []
97
+ const walk = async (dir) => {
98
+ let entries = []
99
+ try {
100
+ entries = await fs.promises.readdir(dir, { withFileTypes: true })
101
+ } catch (_) {
102
+ return
103
+ }
104
+ for (const entry of entries) {
105
+ const entryPath = path.resolve(dir, entry.name)
106
+ if (!this.registry.isPathWithin(root, entryPath)) {
107
+ continue
108
+ }
109
+ if (entry.isDirectory()) {
110
+ await walk(entryPath)
111
+ } else if (entry.name === filename) {
112
+ out.push(entryPath)
113
+ }
114
+ }
115
+ }
116
+ await walk(root)
117
+ return out
118
+ }
119
+
120
+ async buildSection({ appRoot, root, file, source, script, tailLines }) {
121
+ if (!this.registry.isPathWithin(root, file)) {
122
+ return null
123
+ }
124
+ let text = ''
125
+ let stats = null
126
+ try {
127
+ text = await fs.promises.readFile(file, 'utf8')
128
+ stats = await fs.promises.stat(file)
129
+ } catch (_) {
130
+ return null
131
+ }
132
+ const allLines = text.split(/\r?\n/)
133
+ if (allLines.length > 0 && allLines[allLines.length - 1] === '') {
134
+ allLines.pop()
135
+ }
136
+ let tail = allLines.slice(-tailLines).join('\n')
137
+ let truncatedByChars = false
138
+ if (tail.length > MAX_SECTION_CHARS) {
139
+ tail = tail.slice(-MAX_SECTION_CHARS)
140
+ truncatedByChars = true
141
+ }
142
+ return {
143
+ source,
144
+ script,
145
+ file: this.toPosix(path.relative(appRoot, file)),
146
+ line_count: allLines.length,
147
+ tail_count: tailLines,
148
+ size: stats ? stats.size : Buffer.byteLength(text),
149
+ modified: stats ? stats.mtime : null,
150
+ truncated: allLines.length > tailLines || truncatedByChars,
151
+ text: tail
152
+ }
153
+ }
154
+
155
+ redactText(input, context = {}) {
156
+ let text = typeof input === 'string' ? input : ''
157
+ const counts = {}
158
+ const replace = (name, pattern, replacement) => {
159
+ text = text.replace(pattern, (...args) => {
160
+ counts[name] = (counts[name] || 0) + 1
161
+ if (typeof replacement === 'function') {
162
+ return replacement(...args)
163
+ }
164
+ return String(replacement).replace(/\$(\d+)/g, (_, index) => {
165
+ const value = args[Number.parseInt(index, 10)]
166
+ return value === undefined ? '' : String(value)
167
+ })
168
+ })
169
+ }
170
+
171
+ replace('private_keys', /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g, '[REDACTED_PRIVATE_KEY]')
172
+ text = text.replace(/^(\s*(?:export\s+)?)([A-Za-z_][A-Za-z0-9_.-]*)(\s*=\s*).*$/gm, (match, prefix, key, separator) => {
173
+ if (!SENSITIVE_ENV_KEY.test(key)) {
174
+ return match
175
+ }
176
+ counts.env_secrets = (counts.env_secrets || 0) + 1
177
+ return `${prefix}${key}${separator}[REDACTED_SECRET]`
178
+ })
179
+ replace('auth_headers', /\b(Authorization|Proxy-Authorization)\s*:\s*([^\r\n]+)/gi, '$1: [REDACTED_AUTH]')
180
+ replace('url_credentials', /([a-z][a-z0-9+.-]*:\/\/)([^/\s:@]+):([^@\s/]+)@/gi, '$1[REDACTED_CREDENTIALS]@')
181
+ replace('sensitive_query_params', /([?&](?:access_token|refresh_token|token|secret|password|passwd|api[_-]?key|apikey|auth|session|cookie|key)=)([^&\s"'<>]+)/gi, '$1[REDACTED_SECRET]')
182
+ replace('secret_flags', /((?:--?|\/)(?:token|secret|password|passwd|passphrase|api[-_]?key|apikey|credential|cookie|session|auth)(?:=|\s+))("[^"]*"|'[^']*'|[^\s]+)/gi, '$1[REDACTED_SECRET]')
183
+ replace('openai_keys', /\bsk-(?:proj-|svcacct-)?[A-Za-z0-9_-]{20,}\b/g, '[REDACTED_OPENAI_KEY]')
184
+ replace('huggingface_tokens', /\bhf_[A-Za-z0-9]{20,}\b/g, '[REDACTED_HF_TOKEN]')
185
+ replace('github_tokens', /\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9_]{20,}\b/g, '[REDACTED_GITHUB_TOKEN]')
186
+ replace('github_pat_tokens', /\bgithub_pat_[A-Za-z0-9_]{20,}\b/g, '[REDACTED_GITHUB_TOKEN]')
187
+ replace('aws_access_keys', /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/g, '[REDACTED_AWS_ACCESS_KEY]')
188
+ replace('stripe_secret_keys', /\bsk_live_[A-Za-z0-9]{20,}\b/g, '[REDACTED_STRIPE_KEY]')
189
+ replace('jwt_tokens', /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g, '[REDACTED_JWT]')
190
+ replace('emails', /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi, '[REDACTED_EMAIL]')
191
+
192
+ const pathPatterns = this.buildPathPatterns(context)
193
+ for (const pattern of pathPatterns) {
194
+ replace('local_paths', pattern, (match, prefix) => `${prefix || ''}[REDACTED_LOCAL_PATH]`)
195
+ }
196
+
197
+ text = text.replace(/(^|\s)([A-Za-z0-9_./+=-]{48,})(?=\s|$)/g, (match, prefix, value) => {
198
+ if (!/[A-Za-z]/.test(value) || !/[0-9]/.test(value)) {
199
+ return match
200
+ }
201
+ counts.likely_secret_values = (counts.likely_secret_values || 0) + 1
202
+ return `${prefix}[REDACTED_SECRET]`
203
+ })
204
+
205
+ return { text, counts }
206
+ }
207
+
208
+ buildPathPatterns(context = {}) {
209
+ const patterns = [
210
+ /(^|[\s"'(=])\/Users\/[^/\s"')]+/g,
211
+ /(^|[\s"'(=])\/home\/[^/\s"')]+/g,
212
+ /(^|[\s"'(=])[A-Za-z]:\\Users\\[^\\\s"')]+/g
213
+ ]
214
+ const appRoot = context.appRoot ? String(context.appRoot) : ''
215
+ if (appRoot) {
216
+ patterns.push(new RegExp(`(^|[\\s"'(=])${escapeRegExp(appRoot)}`, 'g'))
217
+ }
218
+ return patterns
219
+ }
220
+
221
+ mergeCounts(target, source) {
222
+ for (const [key, value] of Object.entries(source || {})) {
223
+ target[key] = (target[key] || 0) + value
224
+ }
225
+ }
226
+
227
+ renderMarkdown(metadata, sections, redactions) {
228
+ const lines = [
229
+ '# Issue Report',
230
+ '',
231
+ `App: ${metadata.title} (${metadata.app_id})`,
232
+ metadata.repo_url ? `Repo: ${metadata.repo_url}` : null,
233
+ `Generated: ${metadata.generated_at}`,
234
+ `Pinokio: ${metadata.pinokiod || 'unknown'}`,
235
+ `Platform: ${metadata.platform} ${metadata.arch}`,
236
+ `Node: ${metadata.node}`,
237
+ '',
238
+ '## Summary',
239
+ '',
240
+ '',
241
+ '## System',
242
+ '',
243
+ '```json',
244
+ JSON.stringify(metadata.system_spec || {}, null, 2),
245
+ '```'
246
+ ].filter((line) => line !== null)
247
+
248
+ if (metadata.redaction_mode !== 'none') {
249
+ lines.push(
250
+ '',
251
+ '## Sanitization',
252
+ '',
253
+ this.renderRedactionSummary(redactions, metadata.redaction_mode)
254
+ )
255
+ }
256
+
257
+ lines.push('', '## Logs')
258
+
259
+ if (sections.length === 0) {
260
+ lines.push('', 'No app log files were found.')
261
+ }
262
+
263
+ for (const section of sections) {
264
+ lines.push(
265
+ '',
266
+ `### ${section.file}`,
267
+ '',
268
+ `Source: ${section.source}${section.script ? ` / ${section.script}` : ''}`,
269
+ `Lines: ${section.line_count} total, last ${Math.min(section.line_count, section.tail_count)} included${section.truncated ? ' (truncated)' : ''}`,
270
+ '',
271
+ '```text',
272
+ section.text || '',
273
+ '```'
274
+ )
275
+ }
276
+
277
+ return lines.join('\n')
278
+ }
279
+
280
+ renderRedactionSummary(redactions, redactionMode = 'server_deterministic') {
281
+ if (redactionMode === 'none') {
282
+ return ''
283
+ }
284
+ const entries = Object.entries(redactions || {}).filter(([, count]) => count > 0)
285
+ if (entries.length === 0) {
286
+ return 'Basic redaction did not find structured secrets.'
287
+ }
288
+ return entries
289
+ .sort(([a], [b]) => a.localeCompare(b))
290
+ .map(([name, count]) => `- ${name.replace(/_/g, ' ')}: ${count}`)
291
+ .join('\n')
292
+ }
293
+
294
+ readGitRemote(appRoot) {
295
+ if (!appRoot) {
296
+ return null
297
+ }
298
+ try {
299
+ const configPath = path.resolve(appRoot, '.git', 'config')
300
+ if (!this.registry.isPathWithin(appRoot, configPath)) {
301
+ return null
302
+ }
303
+ const raw = fs.readFileSync(configPath, 'utf8')
304
+ const originMatch = raw.match(/\[remote "origin"\][\s\S]*?\n\s*url\s*=\s*([^\r\n]+)/)
305
+ if (originMatch && originMatch[1]) {
306
+ return this.sanitizeRemoteUrl(originMatch[1])
307
+ }
308
+ const firstRemote = raw.match(/\[remote "[^"]+"\][\s\S]*?\n\s*url\s*=\s*([^\r\n]+)/)
309
+ return firstRemote && firstRemote[1] ? this.sanitizeRemoteUrl(firstRemote[1]) : null
310
+ } catch (_) {
311
+ return null
312
+ }
313
+ }
314
+
315
+ sanitizeRemoteUrl(value) {
316
+ const text = typeof value === 'string' ? value.trim() : ''
317
+ if (!text) {
318
+ return null
319
+ }
320
+ try {
321
+ const parsed = new URL(text)
322
+ if (parsed.username || parsed.password) {
323
+ parsed.username = ''
324
+ parsed.password = ''
325
+ return parsed.toString()
326
+ }
327
+ } catch (_) {
328
+ return text
329
+ }
330
+ return text
331
+ }
332
+
333
+ compareSections(a, b) {
334
+ const sourceOrder = { api: 0, shell: 1 }
335
+ const bySource = (sourceOrder[a.source] ?? 99) - (sourceOrder[b.source] ?? 99)
336
+ if (bySource !== 0) return bySource
337
+ return this.scriptRank(a.script) - this.scriptRank(b.script)
338
+ || String(a.script || '').localeCompare(String(b.script || ''))
339
+ || String(a.file || '').localeCompare(String(b.file || ''))
340
+ }
341
+
342
+ scriptRank(script) {
343
+ const value = String(script || '').toLowerCase()
344
+ const ordered = ['install', 'update', 'start', 'run', 'launch', 'serve', 'shell']
345
+ const idx = ordered.findIndex((name) => value === name || value.startsWith(`${name}.`) || value.includes(`/${name}.`))
346
+ return idx >= 0 ? idx : ordered.length
347
+ }
348
+
349
+ toPosix(value) {
350
+ return String(value || '').split(path.sep).filter(Boolean).join('/')
351
+ }
352
+
353
+ buildSystemSpec() {
354
+ const kernel = this.kernel || {}
355
+ const info = kernel.sysinfo && typeof kernel.sysinfo === 'object' ? kernel.sysinfo : {}
356
+ return this.compactObject({
357
+ pinokio: {
358
+ version: this.readPinokioVersion(),
359
+ node: process.version,
360
+ platform: kernel.platform || process.platform,
361
+ arch: kernel.arch || process.arch,
362
+ torch_backend: kernel.torch_backend || info.torch_backend || null
363
+ },
364
+ hardware: this.compactObject({
365
+ gpu: kernel.gpu || info.gpu || null,
366
+ gpu_model: kernel.gpu_model || info.gpu_model || null,
367
+ ram_gb: typeof kernel.ram === 'number' ? kernel.ram : info.ram,
368
+ vram_gb: typeof kernel.vram === 'number' ? kernel.vram : info.vram
369
+ }),
370
+ os: this.sanitizeOsInfo(info.osInfo),
371
+ system: this.sanitizeSystem(info.system),
372
+ cpu: this.sanitizeCpu(info.cpu),
373
+ memory: this.sanitizeMemory(info.mem),
374
+ gpus: this.sanitizeGpus(info.gpus),
375
+ graphics: this.sanitizeGraphics(info.graphics)
376
+ })
377
+ }
378
+
379
+ sanitizeOsInfo(value) {
380
+ return this.pickObject(value, [
381
+ 'platform',
382
+ 'distro',
383
+ 'release',
384
+ 'codename',
385
+ 'kernel',
386
+ 'arch',
387
+ 'build',
388
+ 'servicepack',
389
+ 'uefi'
390
+ ])
391
+ }
392
+
393
+ sanitizeSystem(value) {
394
+ return this.pickObject(value, [
395
+ 'manufacturer',
396
+ 'model',
397
+ 'version',
398
+ 'virtual'
399
+ ])
400
+ }
401
+
402
+ sanitizeCpu(value) {
403
+ return this.pickObject(value, [
404
+ 'manufacturer',
405
+ 'brand',
406
+ 'vendor',
407
+ 'family',
408
+ 'model',
409
+ 'stepping',
410
+ 'revision',
411
+ 'speed',
412
+ 'speedMin',
413
+ 'speedMax',
414
+ 'cores',
415
+ 'physicalCores',
416
+ 'processors',
417
+ 'performanceCores',
418
+ 'efficiencyCores',
419
+ 'virtualization',
420
+ 'cache'
421
+ ])
422
+ }
423
+
424
+ sanitizeMemory(value) {
425
+ return this.pickObject(value, [
426
+ 'total',
427
+ 'free',
428
+ 'used',
429
+ 'active',
430
+ 'available',
431
+ 'buffers',
432
+ 'cached',
433
+ 'slab',
434
+ 'buffcache',
435
+ 'swaptotal',
436
+ 'swapused',
437
+ 'swapfree'
438
+ ])
439
+ }
440
+
441
+ sanitizeGpus(value) {
442
+ if (!Array.isArray(value)) {
443
+ return []
444
+ }
445
+ return value.map((gpu) => this.pickObject(gpu, [
446
+ 'vendor',
447
+ 'model',
448
+ 'bus',
449
+ 'vram',
450
+ 'vramDynamic',
451
+ 'cores',
452
+ 'metalVersion',
453
+ 'cudaVersion',
454
+ 'driverVersion'
455
+ ])).filter((gpu) => Object.keys(gpu).length > 0)
456
+ }
457
+
458
+ sanitizeGraphics(value) {
459
+ if (!value || typeof value !== 'object') {
460
+ return null
461
+ }
462
+ return this.compactObject({
463
+ controllers: Array.isArray(value.controllers)
464
+ ? value.controllers.map((controller) => this.pickObject(controller, [
465
+ 'vendor',
466
+ 'model',
467
+ 'bus',
468
+ 'vram',
469
+ 'vramDynamic',
470
+ 'cores',
471
+ 'metalVersion',
472
+ 'cudaVersion',
473
+ 'driverVersion'
474
+ ])).filter((controller) => Object.keys(controller).length > 0)
475
+ : [],
476
+ displays: Array.isArray(value.displays)
477
+ ? value.displays.map((display) => this.pickObject(display, [
478
+ 'model',
479
+ 'main',
480
+ 'builtin',
481
+ 'connection',
482
+ 'currentResX',
483
+ 'currentResY',
484
+ 'resolutionX',
485
+ 'resolutionY',
486
+ 'pixelDepth',
487
+ 'currentRefreshRate'
488
+ ])).filter((display) => Object.keys(display).length > 0)
489
+ : []
490
+ })
491
+ }
492
+
493
+ pickObject(value, keys) {
494
+ if (!value || typeof value !== 'object') {
495
+ return null
496
+ }
497
+ const out = {}
498
+ for (const key of keys) {
499
+ if (Object.prototype.hasOwnProperty.call(value, key) && value[key] !== undefined && value[key] !== null && value[key] !== '') {
500
+ out[key] = value[key]
501
+ }
502
+ }
503
+ return this.compactObject(out)
504
+ }
505
+
506
+ compactObject(value) {
507
+ if (!value || typeof value !== 'object') {
508
+ return value
509
+ }
510
+ if (Array.isArray(value)) {
511
+ return value
512
+ .map((entry) => this.compactObject(entry))
513
+ .filter((entry) => {
514
+ if (entry === null || entry === undefined || entry === '') return false
515
+ if (Array.isArray(entry)) return entry.length > 0
516
+ if (typeof entry === 'object') return Object.keys(entry).length > 0
517
+ return true
518
+ })
519
+ }
520
+ const out = {}
521
+ for (const [key, entry] of Object.entries(value)) {
522
+ const compacted = this.compactObject(entry)
523
+ if (compacted === null || compacted === undefined || compacted === '') continue
524
+ if (Array.isArray(compacted) && compacted.length === 0) continue
525
+ if (typeof compacted === 'object' && !Array.isArray(compacted) && Object.keys(compacted).length === 0) continue
526
+ out[key] = compacted
527
+ }
528
+ return out
529
+ }
530
+
531
+ readPinokioVersion() {
532
+ try {
533
+ const pkg = require('../../package.json')
534
+ return pkg && pkg.version ? String(pkg.version) : null
535
+ } catch (_) {
536
+ return null
537
+ }
538
+ }
539
+ }
540
+
541
+ AppLogReportService.DEFAULT_TAIL_LINES = DEFAULT_TAIL_LINES
542
+
543
+ module.exports = AppLogReportService
@@ -1,6 +1,7 @@
1
1
  const fs = require("fs")
2
2
  const path = require("path")
3
3
  const clearModule = require("clear-module")
4
+ const PluginSources = require("../../kernel/plugin_sources")
4
5
 
5
6
  const {
6
7
  TASK_CONFIG_FILENAME,
@@ -117,23 +118,7 @@ function createContentValidationService({ kernel }) {
117
118
  }
118
119
 
119
120
  const normalizePluginPath = (value) => {
120
- let normalized = typeof value === "string" ? value.trim() : ""
121
- if (!normalized) {
122
- return ""
123
- }
124
- normalized = normalized.replace(/\\/g, "/")
125
- normalized = normalized.replace(/^\/run(?=\/)/, "")
126
- if (!normalized.startsWith("/")) {
127
- normalized = `/${normalized}`
128
- }
129
- normalized = normalized.replace(/\/{2,}/g, "/").replace(/\/+$/, "")
130
- if (!normalized) {
131
- return ""
132
- }
133
- if (!normalized.endsWith("/pinokio.js")) {
134
- normalized = `${normalized}/pinokio.js`
135
- }
136
- return normalized
121
+ return PluginSources.normalizePluginPath(value)
137
122
  }
138
123
 
139
124
  const normalizeBundledPluginSpec = (value) => {
@@ -162,16 +147,17 @@ function createContentValidationService({ kernel }) {
162
147
  absolutePath,
163
148
  dir: pluginDir,
164
149
  config,
165
- hasInstall: Array.isArray(config && config.install),
166
- hasUpdate: Array.isArray(config && config.update),
167
- hasUninstall: Array.isArray(config && config.uninstall),
150
+ hasInstall: PluginSources.isAction(config && config.install),
151
+ hasUpdate: PluginSources.isAction(config && config.update),
152
+ hasUninstall: PluginSources.isAction(config && config.uninstall),
153
+ hasInstalledCheck: PluginSources.isInstalledCheck(config && config.installed),
168
154
  image: null,
169
155
  }
170
156
 
171
157
  if (config && typeof config.icon === "string" && config.icon.trim()) {
172
- if (normalizedPath.startsWith("/plugin/")) {
173
- const relativeDir = path.posix.dirname(normalizedPath.slice(1))
174
- context.image = `/asset/${relativeDir}/${config.icon.trim()}`
158
+ const pluginImage = PluginSources.pluginAssetHrefForIcon(normalizedPath, config.icon)
159
+ if (pluginImage) {
160
+ context.image = pluginImage
175
161
  } else if (normalizedPath.startsWith("/api/")) {
176
162
  const segments = normalizedPath.replace(/^\/+/, "").split("/")
177
163
  const appName = segments[1] || ""
@@ -192,7 +178,10 @@ function createContentValidationService({ kernel }) {
192
178
  const fallbackTitle = normalizedPath
193
179
  ? path.basename(path.posix.dirname(normalizedPath))
194
180
  : "Plugin"
195
- if (!normalizedPath || (!normalizedPath.startsWith("/plugin/") && !normalizedPath.startsWith("/api/"))) {
181
+ const isSystemPlugin = PluginSources.isSystemPluginPath(normalizedPath)
182
+ const isHomePlugin = normalizedPath.startsWith("/plugin/")
183
+ const isApiPlugin = normalizedPath.startsWith("/api/")
184
+ if (!normalizedPath || (!isSystemPlugin && !isHomePlugin && !isApiPlugin)) {
196
185
  return buildInvalid({
197
186
  type: "plugin",
198
187
  subjectTitle: fallbackTitle,
@@ -205,8 +194,21 @@ function createContentValidationService({ kernel }) {
205
194
  detailUrl: "/plugins",
206
195
  })
207
196
  }
197
+ if (PluginSources.isLegacyPluginCodePath(normalizedPath)) {
198
+ return buildInvalid({
199
+ type: "plugin",
200
+ subjectTitle: fallbackTitle,
201
+ errors: [
202
+ buildError(
203
+ "This managed plugin path is no longer used.",
204
+ "Open the built-in Pinokio plugin instead."
205
+ ),
206
+ ],
207
+ detailUrl: "/plugins",
208
+ })
209
+ }
208
210
 
209
- const absolutePath = kernel.path(normalizedPath.replace(/^\/+/, ""))
211
+ const absolutePath = PluginSources.pluginPathToAbsolute(kernel, normalizedPath)
210
212
  const folderPath = path.dirname(absolutePath)
211
213
  const loaded = await loadJsFile(absolutePath)
212
214
  if (!loaded.exists) {
@@ -251,25 +253,42 @@ function createContentValidationService({ kernel }) {
251
253
  { file: absolutePath }
252
254
  ))
253
255
  } else {
254
- if (!Array.isArray(config.run)) {
256
+ if (!PluginSources.isAction(config.run)) {
255
257
  errors.push(buildError(
256
- "Plugins must define a top-level run array.",
257
- "Add `run: [...]` to pinokio.js.",
258
+ "Plugins must define a top-level run array or function.",
259
+ "Add `run: [...]` or `run: async (ctx) => [...]` to pinokio.js.",
258
260
  { file: absolutePath }
259
261
  ))
260
262
  }
261
- const topLevelFunctionKeys = Object.keys(config).filter((key) => typeof config[key] === "function")
263
+ const topLevelFunctionKeys = Object.keys(config).filter((key) => {
264
+ return typeof config[key] === "function" && !PluginSources.FUNCTION_KEYS.has(key)
265
+ })
262
266
  if (topLevelFunctionKeys.length > 0) {
263
267
  errors.push(buildError(
264
268
  `Top-level function fields are not supported: ${topLevelFunctionKeys.join(", ")}.`,
265
- "Move those functions out of pinokio.js or replace them with data.",
269
+ "Only action fields such as run, install, uninstall, update, and the installed status check may be functions.",
270
+ { file: absolutePath }
271
+ ))
272
+ }
273
+ for (const key of PluginSources.ACTION_KEYS) {
274
+ if (key in config && !PluginSources.isAction(config[key])) {
275
+ errors.push(buildError(
276
+ `Plugin action ${key} must be an array or function.`,
277
+ `Set ${key} to an array or async function returning an array.`,
278
+ { file: absolutePath }
279
+ ))
280
+ }
281
+ }
282
+ if ("installed" in config && !PluginSources.isInstalledCheck(config.installed)) {
283
+ errors.push(buildError(
284
+ "Plugin installed must be a function.",
285
+ "Set installed to a function returning true when the plugin-managed setup exists.",
266
286
  { file: absolutePath }
267
287
  ))
268
288
  }
269
289
  if (normalizedPath.startsWith("/plugin/")) {
270
- const isManagedDefaultPlugin = normalizedPath.startsWith("/plugin/code/")
271
290
  const declaredPath = typeof config.path === "string" ? config.path.trim() : ""
272
- if (!isManagedDefaultPlugin && declaredPath !== "plugin") {
291
+ if (declaredPath !== "plugin") {
273
292
  errors.push(buildError(
274
293
  'Standalone plugins must set `path: "plugin"`.',
275
294
  'Add `path: "plugin"` to pinokio.js.',
@@ -567,7 +586,7 @@ function createContentValidationService({ kernel }) {
567
586
  })
568
587
  }
569
588
 
570
- const validateRunPath = async (pathComponents) => {
589
+ const validateRunPath = async (pathComponents, options = {}) => {
571
590
  if (!Array.isArray(pathComponents) || pathComponents.length === 0) {
572
591
  return buildValid()
573
592
  }
@@ -575,6 +594,9 @@ function createContentValidationService({ kernel }) {
575
594
  if (normalized.length === 0) {
576
595
  return buildValid()
577
596
  }
597
+ if (options.system && normalized[0] === "plugin") {
598
+ return validatePluginByPath(`${PluginSources.SYSTEM_PLUGIN_RUN_PREFIX}/${normalized.slice(1).join("/")}`)
599
+ }
578
600
  if (normalized[0] === "plugin") {
579
601
  return validatePluginByPath(`/${normalized.join("/")}`)
580
602
  }