pinokiod 7.3.1 → 7.3.3

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 +98 -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 +97 -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,23 @@
1
+ module.exports = {
2
+ title: "Codex Desktop",
3
+ link: "https://openai.com/codex",
4
+ icon: "icon.png",
5
+ description: "Codex Desktop",
6
+ launch_type: "desktop",
7
+ run: [{
8
+ method: "uri.open",
9
+ params: {
10
+ uri: "codex://new",
11
+ params: {
12
+ prompt: "{{args.prompt || ''}}",
13
+ path: "{{args.cwd || ''}}"
14
+ }
15
+ }
16
+ }, {
17
+ method: "process.wait",
18
+ params: {
19
+ title: "Launched",
20
+ description: "Click the stop button to stop watching file changes"
21
+ }
22
+ }]
23
+ }
Binary file
@@ -0,0 +1,15 @@
1
+ module.exports = {
2
+ title: "Crush",
3
+ icon: "crush.png",
4
+ link: "https://github.com/charmbracelet/crush",
5
+ run: [{
6
+ id: "run",
7
+ method: "shell.run",
8
+ params: {
9
+ message: "npx -y @charmland/crush@latest",
10
+ path: "{{args.cwd}}",
11
+ buffer: 1024,
12
+ input: true
13
+ }
14
+ }]
15
+ }
Binary file
@@ -0,0 +1,23 @@
1
+ module.exports = {
2
+ title: "Cursor",
3
+ link: "https://cursor.com",
4
+ icon: "cursor.jpeg",
5
+ description: "The AI Code Editor",
6
+ launch_type: "desktop",
7
+ run: [{
8
+ when: "{{which('cursor')}}",
9
+ method: "exec",
10
+ params: {
11
+ message: "cursor .",
12
+ path: "{{args.cwd}}"
13
+ }
14
+ }, {
15
+ when: "{{!which('cursor')}}",
16
+ method: "notify",
17
+ params: {
18
+ html: "Cursor is not installed. Click to visit the Cursor homepage to download",
19
+ href: "https://cursor.com",
20
+ target: "_blank"
21
+ }
22
+ }]
23
+ }
@@ -0,0 +1,34 @@
1
+ module.exports = {
2
+ title: "Qwen Code",
3
+ link: "https://github.com/QwenLM/qwen-code",
4
+ icon: "qwen.png",
5
+ env: [{
6
+ key: "OPENAI_API_KEY",
7
+ default: "OPENAI_API_KEY"
8
+ }, {
9
+ key: "OPENAI_BASE_URL",
10
+ description: "use the OpenAI API compatible api endpoint",
11
+ default: "http://localhost:1234/v1"
12
+ }, {
13
+ key: "OPENAI_MODEL",
14
+ description: "the openai compatible model",
15
+ default: "mradermacher/Bootes-Qwen3_Coder-Reasoning-i1-GGUF"
16
+ }],
17
+ run: [{
18
+ id: "run",
19
+ method: "shell.run",
20
+ params: {
21
+ message: {
22
+ _: [
23
+ "npx",
24
+ "-y",
25
+ "@qwen-code/qwen-code@latest"
26
+ ],
27
+ "prompt-interactive": "{{args.prompt || undefined}}"
28
+ },
29
+ path: "{{args.cwd}}",
30
+ buffer: 1024,
31
+ input: true
32
+ }
33
+ }]
34
+ }
Binary file
@@ -0,0 +1,20 @@
1
+ module.exports = {
2
+ title: "VS Code",
3
+ link: "https://code.visualstudio.com/",
4
+ icon: "vscode.png",
5
+ description: "The AI Code Editor",
6
+ run: [{
7
+ when: "{{which('code')}}",
8
+ method: "exec",
9
+ params: {
10
+ message: "code .",
11
+ path: "{{args.cwd}}",
12
+ }
13
+ }, {
14
+ method: "process.wait",
15
+ params: {
16
+ title: "Launched",
17
+ description: "Click the stop button to stop watching file changes"
18
+ }
19
+ }]
20
+ }
Binary file
@@ -0,0 +1,23 @@
1
+ module.exports = {
2
+ title: "Windsurf",
3
+ link: "https://windsurf.com/",
4
+ icon: "windsurf.png",
5
+ description: "The AI Code Editor",
6
+ launch_type: "desktop",
7
+ run: [{
8
+ method: "uri.open",
9
+ params: {
10
+ uri: "windsurf://cascade/newChat",
11
+ params: {
12
+ prompt: "{{args.prompt || ''}}",
13
+ folder: "{{args.cwd || ''}}"
14
+ }
15
+ }
16
+ }, {
17
+ method: "process.wait",
18
+ params: {
19
+ title: "Launched",
20
+ description: "Click the stop button to stop watching file changes"
21
+ }
22
+ }]
23
+ }
@@ -0,0 +1,185 @@
1
+ const assert = require("node:assert/strict")
2
+ const fsp = require("node:fs/promises")
3
+ const os = require("node:os")
4
+ const path = require("node:path")
5
+ const test = require("node:test")
6
+
7
+ const antigravityCli = require("../system/plugin/antigravity-cli/pinokio")
8
+ const antigravityCliAuto = require("../system/plugin/antigravity-cli-auto/pinokio")
9
+ const antigravityCommon = require("../system/plugin/antigravity-cli/common")
10
+
11
+ function createKernel(root, platform = "darwin") {
12
+ return {
13
+ platform,
14
+ path: (...parts) => path.join(root, ...parts),
15
+ }
16
+ }
17
+
18
+ test("Antigravity CLI plugins expose managed lifecycle actions", () => {
19
+ for (const plugin of [antigravityCli, antigravityCliAuto]) {
20
+ assert.equal(typeof plugin.install, "function")
21
+ assert.equal(typeof plugin.update, "function")
22
+ assert.equal(typeof plugin.uninstall, "function")
23
+ assert.equal(typeof plugin.installed, "function")
24
+ assert.equal(typeof plugin.run, "function")
25
+ }
26
+ })
27
+
28
+ test("Antigravity CLI install and update install agy into Pinokio bin", () => {
29
+ const root = path.join(os.tmpdir(), "pinokio-antigravity-test")
30
+ const kernel = createKernel(root)
31
+
32
+ const install = antigravityCli.install(kernel, {})[0]
33
+ const update = antigravityCli.update(kernel, {})[0]
34
+
35
+ for (const step of [install, update]) {
36
+ assert.equal(step.method, "shell.run")
37
+ assert.equal(step.params.path, root)
38
+ assert.equal(step.params.conda.skip, true)
39
+ assert.equal(step.params.message._[0], process.execPath)
40
+ assert.equal(step.params.message._[1], antigravityCommon.installerPath())
41
+ assert.deepEqual(step.params.message._.slice(2), [
42
+ "--install-dir",
43
+ path.join(root, "bin"),
44
+ "--managed-dir",
45
+ path.join(root, "bin", "antigravity-cli"),
46
+ ])
47
+ assert.doesNotMatch(step.params.message._.join(" "), / -e /)
48
+ assert.doesNotMatch(step.params.message._.join(" "), /api\.github\.com\/repos\/google-antigravity\/antigravity-cli\/releases\/latest/)
49
+ assert.equal(Object.prototype.hasOwnProperty.call(step.params, "input"), false)
50
+ }
51
+ })
52
+
53
+ test("Antigravity CLI installer script downloads and verifies official release assets", () => {
54
+ const source = require("node:fs").readFileSync(antigravityCommon.installerPath(), "utf8")
55
+
56
+ assert.match(source, /api\.github\.com\/repos\/google-antigravity\/antigravity-cli\/releases\/latest/)
57
+ assert.match(source, /assetNameForPlatform/)
58
+ assert.match(source, /agy_cli_mac_.*\.tar\.gz/)
59
+ assert.match(source, /agy_cli_linux_.*\.tar\.gz/)
60
+ assert.match(source, /agy_cli_windows_.*\.zip/)
61
+ assert.match(source, /asset\.digest/)
62
+ assert.match(source, /install\.json/)
63
+ assert.doesNotMatch(source, /profile|bashrc|zshrc/)
64
+ assert.doesNotMatch(source, /antigravity-cli-auto-updater/)
65
+ })
66
+
67
+ test("Antigravity CLI uninstall removes only the managed binary and staging directory", () => {
68
+ const root = path.join(os.tmpdir(), "pinokio-antigravity-test")
69
+ const kernel = createKernel(root)
70
+ const step = antigravityCli.uninstall(kernel, {})[0]
71
+
72
+ assert.equal(step.method, "shell.run")
73
+ assert.equal(step.params.path, path.join(root, "bin"))
74
+ assert.match(step.params.message, new RegExp(`rm -f '${path.join(root, "bin", "agy")}'`.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")))
75
+ assert.match(step.params.message, new RegExp(`rm -rf '${path.join(root, "bin", "antigravity-cli")}'`.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")))
76
+ assert.doesNotMatch(step.params.message, new RegExp(`rm -rf '${path.join(root, "bin")}'`.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")))
77
+ assert.equal(Object.prototype.hasOwnProperty.call(step.params, "input"), false)
78
+ })
79
+
80
+ test("Antigravity CLI run uses the Pinokio-managed binary path", async () => {
81
+ const root = await fsp.mkdtemp(path.join(os.tmpdir(), "pinokio-antigravity-run-"))
82
+ try {
83
+ const kernel = createKernel(root)
84
+ const bin = antigravityCommon.binaryPath(kernel, "darwin")
85
+ await fsp.mkdir(path.dirname(bin), { recursive: true })
86
+ await fsp.writeFile(bin, "")
87
+
88
+ const steps = antigravityCli.run(kernel, {}, {
89
+ args: { cwd: "/tmp/workspace", prompt: "build this" },
90
+ input: {},
91
+ })
92
+
93
+ assert.equal(steps.length, 1)
94
+ assert.equal(steps[0].method, "shell.run")
95
+ assert.deepEqual(steps[0].params.message, {
96
+ _: [bin, "--prompt-interactive", "build this"]
97
+ })
98
+ assert.equal(steps[0].params.path, "/tmp/workspace")
99
+ assert.equal(steps[0].params.conda.skip, true)
100
+ } finally {
101
+ await fsp.rm(root, { recursive: true, force: true })
102
+ }
103
+ })
104
+
105
+ test("Antigravity CLI installed checks the Pinokio-managed agy binary", async () => {
106
+ const root = await fsp.mkdtemp(path.join(os.tmpdir(), "pinokio-antigravity-installed-"))
107
+ try {
108
+ const kernel = createKernel(root)
109
+ const bin = antigravityCommon.binaryPath(kernel, "darwin")
110
+
111
+ assert.equal(antigravityCli.installed(kernel, {}), false)
112
+
113
+ await fsp.mkdir(path.dirname(bin), { recursive: true })
114
+ await fsp.writeFile(bin, "")
115
+
116
+ assert.equal(antigravityCli.installed(kernel, {}), true)
117
+ } finally {
118
+ await fsp.rm(root, { recursive: true, force: true })
119
+ }
120
+ })
121
+
122
+ test("Antigravity CLI Auto adds the current permission-skip flag", async () => {
123
+ const root = await fsp.mkdtemp(path.join(os.tmpdir(), "pinokio-antigravity-auto-"))
124
+ try {
125
+ const kernel = createKernel(root)
126
+ const bin = antigravityCommon.binaryPath(kernel, "darwin")
127
+ await fsp.mkdir(path.dirname(bin), { recursive: true })
128
+ await fsp.writeFile(bin, "")
129
+
130
+ const steps = antigravityCliAuto.run(kernel, {}, {
131
+ args: { cwd: "/tmp/workspace" },
132
+ input: {},
133
+ })
134
+
135
+ assert.deepEqual(steps[0].params.message, {
136
+ _: [bin, "--dangerously-skip-permissions"]
137
+ })
138
+ } finally {
139
+ await fsp.rm(root, { recursive: true, force: true })
140
+ }
141
+ })
142
+
143
+ test("Antigravity CLI run asks for Pinokio install when managed binary is missing", async () => {
144
+ const root = await fsp.mkdtemp(path.join(os.tmpdir(), "pinokio-antigravity-missing-"))
145
+ try {
146
+ const kernel = createKernel(root)
147
+ const steps = antigravityCli.run(kernel, {}, {
148
+ args: { cwd: "/tmp/workspace" },
149
+ input: {},
150
+ })
151
+
152
+ assert.equal(steps[0].method, "notify")
153
+ assert.match(steps[0].params.html, /Open the plugin page/)
154
+ assert.equal(steps[0].params.href, "/plugin?path=%2Fpinokio%2Frun%2Fplugin%2Fantigravity-cli%2Fpinokio.js&next=install")
155
+ assert.equal(steps[0].params.target, "_parent")
156
+ assert.equal(steps[0].params.type, "warning")
157
+ } finally {
158
+ await fsp.rm(root, { recursive: true, force: true })
159
+ }
160
+ })
161
+
162
+ test("Antigravity CLI uses kernel.platform for Windows binary paths", async () => {
163
+ const root = await fsp.mkdtemp(path.join(os.tmpdir(), "pinokio-antigravity-win-"))
164
+ try {
165
+ const kernel = createKernel(root, "win32")
166
+ const bin = antigravityCommon.binaryPath(kernel, "win32")
167
+ await fsp.mkdir(path.dirname(bin), { recursive: true })
168
+ await fsp.writeFile(bin, "")
169
+
170
+ assert.equal(bin, path.join(root, "bin", "agy.exe"))
171
+ assert.equal(antigravityCli.installed(kernel, {}), true)
172
+
173
+ const uninstall = antigravityCli.uninstall(kernel, {})[0]
174
+ assert.equal(uninstall.params.shell, "powershell")
175
+ assert.match(uninstall.params.message, /agy\.exe/)
176
+
177
+ const run = antigravityCli.run(kernel, {}, {
178
+ args: { cwd: "C:\\workspace" },
179
+ input: {},
180
+ })
181
+ assert.deepEqual(run[0].params.message, { _: [bin] })
182
+ } finally {
183
+ await fsp.rm(root, { recursive: true, force: true })
184
+ }
185
+ })
@@ -0,0 +1,239 @@
1
+ const assert = require('node:assert/strict')
2
+ const fs = require('node:fs/promises')
3
+ const os = require('node:os')
4
+ const path = require('node:path')
5
+ const test = require('node:test')
6
+
7
+ const AppAPI = require('../kernel/api/app')
8
+
9
+ function createKernel(root, launcher = {}) {
10
+ const commands = []
11
+ return {
12
+ platform: 'test',
13
+ appLauncher: launcher,
14
+ bin: {
15
+ install2: async () => {
16
+ commands.push({ type: 'install2' })
17
+ },
18
+ sh: async (params) => {
19
+ commands.push({ type: 'sh', params })
20
+ }
21
+ },
22
+ api: {
23
+ userdir: path.join(root, 'api'),
24
+ init: async () => {
25
+ commands.push({ type: 'api.init' })
26
+ }
27
+ },
28
+ path: (...parts) => path.join(root, ...parts),
29
+ commands
30
+ }
31
+ }
32
+
33
+ test('app APIs forward search/info/refresh/launch requests to the launcher service', async () => {
34
+ const calls = []
35
+ const launcher = {
36
+ search: async (params) => {
37
+ calls.push({ method: 'search', params })
38
+ return ['search-result']
39
+ },
40
+ info: async (params) => {
41
+ calls.push({ method: 'info', params })
42
+ return { id: params.id }
43
+ },
44
+ refresh: async (params) => {
45
+ calls.push({ method: 'refresh', params })
46
+ return { refreshed: true }
47
+ },
48
+ launch: async (params) => {
49
+ calls.push({ method: 'launch', params })
50
+ return { launched: true }
51
+ }
52
+ }
53
+ const api = new AppAPI()
54
+ const kernel = createKernel('/tmp/pinokio-app-api', launcher)
55
+
56
+ assert.deepEqual(await api.search({ params: { query: 'Code', limit: 3, refresh: true } }, () => {}, kernel), ['search-result'])
57
+ assert.deepEqual(await api.info({ params: { id: 'com.example.Code', refresh: true } }, () => {}, kernel), { id: 'com.example.Code' })
58
+ assert.deepEqual(await api.refresh({ params: { force: true } }, () => {}, kernel), { refreshed: true })
59
+ assert.deepEqual(await api.launch({ params: { id: 'com.example.Code', args: ['--new-window'] } }, () => {}, kernel), { launched: true })
60
+
61
+ assert.deepEqual(calls, [
62
+ { method: 'search', params: { query: 'Code', limit: 3, refresh: true } },
63
+ { method: 'info', params: { id: 'com.example.Code', refresh: true } },
64
+ { method: 'refresh', params: { force: true } },
65
+ { method: 'launch', params: { id: 'com.example.Code', app: undefined, args: ['--new-window'], refresh: undefined, install: undefined } }
66
+ ])
67
+ })
68
+
69
+ test('app.launch delegates missing app install fallback without driving native UI', async () => {
70
+ const api = new AppAPI()
71
+ let fallbackArgs
72
+ api.handleInstallFlow = async (args) => {
73
+ fallbackArgs = args
74
+ return { installed: true }
75
+ }
76
+ const launcher = {
77
+ launch: async () => {
78
+ const error = new Error('not found')
79
+ error.code = 'APP_NOT_FOUND'
80
+ throw error
81
+ }
82
+ }
83
+ const kernel = createKernel('/tmp/pinokio-app-api', launcher)
84
+ const req = {
85
+ params: {
86
+ app: 'Native Tool',
87
+ install: 'https://example.test/tool.dmg'
88
+ }
89
+ }
90
+
91
+ assert.deepEqual(await api.launch(req, () => {}, kernel), { installed: true })
92
+ assert.equal(fallbackArgs.req, req)
93
+ assert.equal(fallbackArgs.kernel, kernel)
94
+ assert.equal(fallbackArgs.launcher, launcher)
95
+ assert.equal(fallbackArgs.params, req.params)
96
+ })
97
+
98
+ test('app.download validates names and builds git clone commands', async () => {
99
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-app-api-'))
100
+ try {
101
+ await fs.mkdir(path.join(root, 'api'), { recursive: true })
102
+ const api = new AppAPI()
103
+ const kernel = createKernel(root, {
104
+ refresh: async (params) => {
105
+ kernel.commands.push({ type: 'launcher.refresh', params })
106
+ }
107
+ })
108
+
109
+ await assert.rejects(
110
+ api.download({ params: {} }, () => {}, kernel),
111
+ /app\.download requires params\.uri/
112
+ )
113
+
114
+ assert.deepEqual(
115
+ await api.download({
116
+ params: {
117
+ uri: 'https://github.com/example/tool.git',
118
+ name: '../bad'
119
+ }
120
+ }, () => {}, kernel),
121
+ {
122
+ ok: false,
123
+ code: 'INVALID_NAME',
124
+ error: 'invalid name',
125
+ name: '../bad',
126
+ uri: 'https://github.com/example/tool.git'
127
+ }
128
+ )
129
+
130
+ await fs.mkdir(path.join(root, 'api', 'existing'), { recursive: true })
131
+ assert.deepEqual(
132
+ await api.download({
133
+ params: {
134
+ uri: 'https://github.com/example/existing.git',
135
+ name: 'existing'
136
+ }
137
+ }, () => {}, kernel),
138
+ {
139
+ ok: false,
140
+ code: 'APP_EXISTS',
141
+ error: 'already exists',
142
+ name: 'existing',
143
+ path: path.join(root, 'api', 'existing'),
144
+ uri: 'https://github.com/example/existing.git'
145
+ }
146
+ )
147
+
148
+ const result = await api.download({
149
+ params: {
150
+ uri: 'https://github.com/example/fresh.git',
151
+ name: 'fresh',
152
+ branch: 'dev'
153
+ }
154
+ }, () => {}, kernel)
155
+
156
+ assert.deepEqual(result, {
157
+ ok: true,
158
+ name: 'fresh',
159
+ path: path.join(root, 'api', 'fresh'),
160
+ uri: 'https://github.com/example/fresh.git',
161
+ branch: 'dev'
162
+ })
163
+ assert.equal(kernel.commands[0].type, 'install2')
164
+ assert.equal(kernel.commands[1].type, 'sh')
165
+ assert.equal(kernel.commands[1].params.path, path.join(root, 'api'))
166
+ assert.equal(kernel.commands[1].params.message, 'git clone --branch "dev" "https://github.com/example/fresh.git" "fresh"')
167
+ assert.deepEqual(kernel.commands.slice(2), [
168
+ { type: 'api.init' },
169
+ { type: 'launcher.refresh', params: { force: true } }
170
+ ])
171
+ } finally {
172
+ await fs.rm(root, { recursive: true, force: true })
173
+ }
174
+ })
175
+
176
+ test('waitForAppPresence handles present, missing, and install-detected apps', async () => {
177
+ const api = new AppAPI()
178
+ const modalEvents = []
179
+ api.htmlModal = {
180
+ open: async (req) => {
181
+ modalEvents.push({ action: 'open', params: req.params })
182
+ },
183
+ update: async (req) => {
184
+ modalEvents.push({ action: 'update', params: req.params })
185
+ },
186
+ close: async (req) => {
187
+ modalEvents.push({ action: 'close', params: req.params })
188
+ }
189
+ }
190
+
191
+ const byId = await api.waitForAppPresence({
192
+ params: { id: 'com.example.Present' }
193
+ }, () => {}, createKernel('/tmp/pinokio-app-api', {
194
+ info: async () => ({ id: 'com.example.Present', name: 'Present' }),
195
+ findMatch: async () => null
196
+ }))
197
+ assert.deepEqual(byId, { id: 'com.example.Present', name: 'Present' })
198
+
199
+ const byName = await api.waitForAppPresence({
200
+ params: { app: 'Present by Name' }
201
+ }, () => {}, createKernel('/tmp/pinokio-app-api', {
202
+ findMatch: async () => ({ entry: { id: 'present-by-name', name: 'Present by Name' } })
203
+ }))
204
+ assert.deepEqual(byName, { id: 'present-by-name', name: 'Present by Name' })
205
+
206
+ await assert.rejects(
207
+ api.waitForAppPresence({
208
+ params: { app: 'Missing' }
209
+ }, () => {}, createKernel('/tmp/pinokio-app-api', {
210
+ findMatch: async () => null
211
+ })),
212
+ (error) => error && error.code === 'APP_NOT_FOUND'
213
+ )
214
+
215
+ let matchAttempts = 0
216
+ const installed = await api.waitForAppPresence({
217
+ parent: { path: '/pinokio/api/demo/start.js' },
218
+ params: {
219
+ app: 'Later',
220
+ install: 'https://example.test/later.dmg',
221
+ installPollIntervalMs: 1,
222
+ installTimeoutMs: 50
223
+ }
224
+ }, () => {}, createKernel('/tmp/pinokio-app-api', {
225
+ refresh: async () => {},
226
+ findMatch: async () => {
227
+ matchAttempts += 1
228
+ if (matchAttempts >= 2) {
229
+ return { entry: { id: 'later', name: 'Later' } }
230
+ }
231
+ return null
232
+ }
233
+ }))
234
+
235
+ assert.deepEqual(installed, { id: 'later', name: 'Later' })
236
+ assert.ok(modalEvents.some((event) => event.action === 'open'))
237
+ assert.ok(modalEvents.some((event) => event.action === 'update'))
238
+ assert.ok(modalEvents.some((event) => event.action === 'close'))
239
+ })
@@ -0,0 +1,67 @@
1
+ const assert = require('node:assert/strict')
2
+ const fs = require('node:fs/promises')
3
+ const os = require('node:os')
4
+ const path = require('node:path')
5
+ const test = require('node:test')
6
+
7
+ const AppLogReportService = require('../server/lib/app_log_report')
8
+
9
+ const createRegistry = () => ({
10
+ parseTailCount: (value, fallback) => Number.parseInt(value, 10) || fallback,
11
+ async pathIsDirectory(targetPath) {
12
+ try {
13
+ return (await fs.stat(targetPath)).isDirectory()
14
+ } catch (_) {
15
+ return false
16
+ }
17
+ },
18
+ isPathWithin(parentPath, childPath) {
19
+ const relative = path.relative(parentPath, childPath)
20
+ return !relative || (!relative.startsWith('..') && !path.isAbsolute(relative))
21
+ }
22
+ })
23
+
24
+ test('app log report includes every logs/api/**/latest file and ignores shell logs', async () => {
25
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-app-log-report-'))
26
+ try {
27
+ const appRoot = path.join(root, 'demo')
28
+ await fs.mkdir(path.join(appRoot, 'logs', 'api', 'install.js'), { recursive: true })
29
+ await fs.mkdir(path.join(appRoot, 'logs', 'api', 'nested', 'start.js'), { recursive: true })
30
+ await fs.mkdir(path.join(appRoot, 'logs', 'shell'), { recursive: true })
31
+
32
+ await fs.writeFile(path.join(appRoot, 'logs', 'api', 'install.js', 'latest'), 'install log\n')
33
+ await fs.writeFile(path.join(appRoot, 'logs', 'api', 'nested', 'start.js', 'latest'), 'nested start log\n')
34
+ await fs.writeFile(path.join(appRoot, 'logs', 'shell', 'latest'), 'shell log should not be included\n')
35
+ await fs.mkdir(path.join(appRoot, '.git'), { recursive: true })
36
+ await fs.writeFile(path.join(appRoot, '.git', 'config'), [
37
+ '[remote "origin"]',
38
+ ' url = https://token:secret@github.com/example/demo.git',
39
+ ''
40
+ ].join('\n'))
41
+
42
+ const service = new AppLogReportService({ registry: createRegistry() })
43
+ const report = await service.buildReport({
44
+ appId: 'demo',
45
+ status: {
46
+ path: appRoot,
47
+ title: 'Demo'
48
+ },
49
+ redact: false
50
+ })
51
+
52
+ assert.deepEqual(
53
+ report.sections.map((section) => section.file).sort(),
54
+ [
55
+ 'logs/api/install.js/latest',
56
+ 'logs/api/nested/start.js/latest'
57
+ ]
58
+ )
59
+ assert.equal(report.repo_url, 'https://github.com/example/demo.git')
60
+ assert.equal(report.markdown.includes('token:secret'), false)
61
+ assert.equal(report.markdown.includes('Repo: https://github.com/example/demo.git'), true)
62
+ assert.equal(report.markdown.includes('## Sanitization'), false)
63
+ assert.equal(report.markdown.includes('shell log should not be included'), false)
64
+ } finally {
65
+ await fs.rm(root, { recursive: true, force: true })
66
+ }
67
+ })