pinokiod 8.1.0 → 8.2.0

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.
@@ -0,0 +1,338 @@
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 Api = require('../kernel/api')
8
+ const Env = require('../kernel/api/env')
9
+ const Environment = require('../kernel/environment')
10
+ const NestedLayout = require('../kernel/nested_layout')
11
+ const getLauncherTarget = require('../kernel/api/launcher_target')
12
+ const AppLogSessions = require('../kernel/app_log_sessions')
13
+ const Server = require('../server')
14
+ const ServerAutolaunch = require('../server/autolaunch')
15
+ const Socket = require('../server/socket')
16
+ const AppLogService = require('../server/lib/app_logs')
17
+ const AppLogReportService = require('../server/lib/app_log_report')
18
+ const AppRegistryService = require('../server/lib/app_registry')
19
+ const AppSearchService = require('../server/lib/app_search')
20
+
21
+ const exists = target => fs.access(target).then(() => true, () => false)
22
+
23
+ const kernelFor = home => {
24
+ const loaded = new Map()
25
+ const kernel = {
26
+ homedir: home,
27
+ path: (...parts) => path.resolve(home, ...parts),
28
+ exists,
29
+ status: () => false,
30
+ template: { render: value => value },
31
+ memory: { local: {} },
32
+ loader: { load: async target => ({ resolved: loaded.get(path.resolve(target)) || null }) }
33
+ }
34
+ kernel.api = new Api(kernel)
35
+ kernel.api.userdir = path.resolve(home, 'api')
36
+ kernel.api.exists = exists
37
+ kernel.api.running = {}
38
+ kernel.loaded = loaded
39
+ return kernel
40
+ }
41
+
42
+ const withHome = async callback => {
43
+ const home = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-nested-parity-'))
44
+ try {
45
+ await callback(home)
46
+ } finally {
47
+ await fs.rm(home, { recursive: true, force: true })
48
+ }
49
+ }
50
+
51
+ const write = async (root, relative, content = '') => {
52
+ const target = path.resolve(root, relative)
53
+ await fs.mkdir(path.dirname(target), { recursive: true })
54
+ await fs.writeFile(target, content)
55
+ return target
56
+ }
57
+
58
+ test('root launcher precedence and environment writes remain unchanged', async () => {
59
+ await withHome(async home => {
60
+ const kernel = kernelFor(home)
61
+ const env = new Env()
62
+ const layouts = [
63
+ ['classic', ['pinokio.js']],
64
+ ['root-with-folder', ['pinokio.js', 'pinokio/note.txt']],
65
+ ['both', ['pinokio.js', 'pinokio/pinokio.js']],
66
+ ['empty-folder', ['pinokio/note.txt']],
67
+ ['no-manifest', []]
68
+ ]
69
+
70
+ for (const [name, files] of layouts) {
71
+ const workspace = path.resolve(home, 'api', name)
72
+ await fs.mkdir(workspace, { recursive: true })
73
+ for (const file of files) await write(workspace, file)
74
+ await write(workspace, 'ENVIRONMENT')
75
+ assert.equal(await kernel.api.launcher_path(workspace), workspace, name)
76
+ if (files.includes('pinokio.js')) kernel.loaded.set(path.resolve(workspace, 'pinokio.js'), { icon: 'icon.png' })
77
+ const meta = await kernel.api.meta(name)
78
+ if (files.includes('pinokio.js')) {
79
+ assert.equal(meta.icon, `/asset/api/${name}/icon.png`, name)
80
+ assert.equal(meta.iconpath, 'icon.png', name)
81
+ }
82
+ await env.set({ parent: { path: path.resolve(workspace, 'start.js') }, params: { VALUE: name } }, null, kernel)
83
+ assert.match(await fs.readFile(path.resolve(workspace, 'ENVIRONMENT'), 'utf8'), new RegExp(`VALUE=${name}`), name)
84
+ }
85
+
86
+ const pinokioEntry = path.resolve(home, 'api', 'pinokio-entry')
87
+ await fs.mkdir(pinokioEntry, { recursive: true })
88
+ await write(pinokioEntry, 'pinokio')
89
+ await write(pinokioEntry, 'ENVIRONMENT')
90
+ assert.equal(await kernel.api.launcher_path(pinokioEntry), pinokioEntry)
91
+ await env.set({ parent: { path: path.resolve(pinokioEntry, 'start.js') }, params: { VALUE: 'file' } }, null, kernel)
92
+ assert.match(await fs.readFile(path.resolve(pinokioEntry, 'ENVIRONMENT'), 'utf8'), /VALUE=file/)
93
+ })
94
+ })
95
+
96
+ test('nested launcher environment, metadata, targets, registry, and search use pinokio/', async () => {
97
+ await withHome(async home => {
98
+ const kernel = kernelFor(home)
99
+ const workspace = path.resolve(home, 'api', 'nested')
100
+ const launcher = path.resolve(workspace, 'pinokio')
101
+ const launcherConfig = { title: 'Nested', icon: 'icon.png', menu: [
102
+ { text: 'Start', href: 'start.js', default: true },
103
+ { text: 'Nested child', href: 'pinokio/tool.js' },
104
+ { text: 'Absolute nested', href: '/api/nested/pinokio/absolute.js' },
105
+ { text: 'Absolute workspace', href: '/api/nested/workspace.js' }
106
+ ] }
107
+ await write(workspace, 'README.md', 'workspace documentation')
108
+ await write(launcher, 'README.md', 'launcher documentation')
109
+ await write(launcher, 'pinokio.js', 'nested launcher token')
110
+ await write(launcher, 'install.js', 'nested install token')
111
+ await write(launcher, 'start.js', 'nested start token')
112
+ await write(launcher, 'pinokio/tool.js', 'nested child token')
113
+ await write(launcher, 'absolute.js', 'absolute nested token')
114
+ await write(workspace, 'workspace.js', 'absolute workspace token')
115
+ await write(launcher, 'ENVIRONMENT', 'DATA=./data\nVALUE=old\n')
116
+ kernel.loaded.set(path.resolve(launcher, 'pinokio.js'), launcherConfig)
117
+
118
+ assert.equal(await kernel.api.launcher_path(workspace), launcher)
119
+ assert.equal((await Environment.get(workspace, kernel)).DATA, path.resolve(launcher, 'data'))
120
+
121
+ await new Env().set({ parent: { path: path.resolve(launcher, 'start.js') }, params: { VALUE: 'nested' } }, null, kernel)
122
+ assert.match(await fs.readFile(path.resolve(launcher, 'ENVIRONMENT'), 'utf8'), /VALUE=nested/)
123
+
124
+ const meta = await kernel.api.meta('nested')
125
+ assert.equal(meta.icon, '/asset/api/nested/pinokio/icon.png')
126
+ assert.equal(meta.iconpath, 'pinokio/icon.png')
127
+ assert.equal(await kernel.api.get_default(workspace), path.resolve(launcher, 'start.js'))
128
+ assert.equal((await getLauncherTarget(kernel.api, workspace, ['start.js'])).uri, path.resolve(launcher, 'start.js'))
129
+
130
+ const registry = new AppRegistryService({ kernel })
131
+ const status = await registry.buildAppStatus('nested')
132
+ assert.equal(status.install_script, 'pinokio/install.js')
133
+ assert.equal(status.start_script, 'pinokio/start.js')
134
+ assert.equal(status.default_script, 'pinokio/start.js')
135
+
136
+ const searchRegistry = {
137
+ listInfoApps: async () => [{ name: 'nested', title: 'Nested', description: '', icon: '' }],
138
+ isPathWithin: registry.isPathWithin.bind(registry)
139
+ }
140
+ const state = await new AppSearchService({ kernel, registry: searchRegistry }).ensureSearchState(true)
141
+ const files = state.docs.map(doc => doc.file)
142
+ assert(files.includes('README.md'))
143
+ assert(files.includes('pinokio/pinokio.js'))
144
+ assert(files.includes('pinokio/install.js'))
145
+ assert(files.includes('pinokio/start.js'))
146
+ assert.equal(files.filter(file => file === 'pinokio/README.md').length, 1)
147
+
148
+ const server = Object.assign(Object.create(Server.prototype), { kernel })
149
+ const app = (await kernel.api.listApps()).find(item => item.id === 'nested')
150
+ const candidates = await new ServerAutolaunch(server).buildCandidates(app)
151
+ assert(candidates.menu.some(item => item.script === 'pinokio/start.js'))
152
+ assert(candidates.menu.some(item => item.script === 'pinokio/pinokio/tool.js'))
153
+ assert(candidates.menu.some(item => item.script === 'pinokio/absolute.js'))
154
+ assert(candidates.menu.some(item => item.script === 'workspace.js'))
155
+ assert(candidates.other.some(item => item.script === 'pinokio/install.js'))
156
+
157
+ await kernel.api.updateMeta({ icon_dirty: true, icon_path: 'uploaded.png', avatar: Buffer.from('icon') }, 'nested')
158
+ assert.equal(await fs.readFile(path.resolve(launcher, 'uploaded.png'), 'utf8'), 'icon')
159
+ assert.equal(await exists(path.resolve(workspace, 'uploaded.png')), false)
160
+ })
161
+ })
162
+
163
+ test('nested preflight assets and AI documents use pinokio/ without changing root launchers', async () => {
164
+ await withHome(async home => {
165
+ const kernel = kernelFor(home)
166
+ const nested = path.resolve(home, 'api', 'nested')
167
+ const nestedRoot = path.resolve(nested, 'pinokio')
168
+ await write(nestedRoot, 'pinokio.js')
169
+ await write(nestedRoot, 'CLAUDE.md')
170
+ await write(nested, 'CLAUDE.md')
171
+
172
+ const launcher = {
173
+ root: nested,
174
+ script: { pre: [{ icon: 'icon.png', href: 'setup.js' }, { href: 'https://example.com' }] }
175
+ }
176
+ const adapted = await NestedLayout.preLauncher(kernel, launcher)
177
+ assert.equal(adapted.root, nestedRoot)
178
+ assert.equal(adapted.script.pre[0].icon, 'pinokio/icon.png')
179
+ assert.equal(adapted.script.pre[0].href, path.resolve(nestedRoot, 'setup.js'))
180
+ assert.equal(adapted.script.pre[1].href, 'https://example.com')
181
+ assert.equal(launcher.script.pre[0].icon, 'icon.png')
182
+
183
+ const files = await NestedLayout.addAiFiles(kernel, nested, ['AGENTS.md', 'CLAUDE.md'], ['CLAUDE.md'], exists)
184
+ assert.deepEqual(files, ['CLAUDE.md', 'pinokio/CLAUDE.md'])
185
+
186
+ const root = path.resolve(home, 'api', 'root')
187
+ await write(root, 'pinokio.js')
188
+ await write(root, 'pinokio/pinokio.js')
189
+ const rootLauncher = { root, script: { pre: [{ icon: 'icon.png', href: 'setup.js' }] } }
190
+ const rootFiles = []
191
+ assert.equal(await NestedLayout.preLauncher(kernel, rootLauncher), rootLauncher)
192
+ assert.equal(await NestedLayout.addAiFiles(kernel, root, ['CLAUDE.md'], rootFiles, exists), rootFiles)
193
+ assert.deepEqual(rootFiles, [])
194
+
195
+ const iconpath = { invalid: true }
196
+ const rootMeta = { icon: iconpath, iconpath }
197
+ NestedLayout.setMetaIcon(rootMeta, 'root', root, '', () => true)
198
+ assert.strictEqual(rootMeta.iconpath, iconpath)
199
+ })
200
+ })
201
+
202
+ test('nested menus and shortcuts resolve relative paths from pinokio/', async () => {
203
+ await withHome(async home => {
204
+ const kernel = kernelFor(home)
205
+ const workspace = path.resolve(home, 'api', 'nested')
206
+ const launcher = path.resolve(workspace, 'pinokio')
207
+ await write(launcher, 'pinokio.js')
208
+ const checked = []
209
+ kernel.status = target => {
210
+ checked.push(target)
211
+ return false
212
+ }
213
+ const server = Object.assign(Object.create(Server.prototype), { kernel })
214
+ const config = {
215
+ menu: [
216
+ { text: 'Start', href: 'start.js' },
217
+ { text: 'Shell', run: 'echo ready' },
218
+ { text: 'Stop', action: { method: 'stop', uri: 'start.js' } },
219
+ { text: 'State', when: 'start.js', off: 'off' }
220
+ ],
221
+ shortcuts: [
222
+ { action: { method: 'stop', uri: 'start.js' } }
223
+ ]
224
+ }
225
+
226
+ await server.renderMenu({ launcher_root: 'pinokio', $source: {} }, kernel.path('api'), 'nested', config, [])
227
+ assert.equal(config.menu[0].href, '/api/nested/pinokio/start.js')
228
+ assert.equal(config.menu[1].cwd, launcher)
229
+ assert.equal(config.menu[2].action.uri, '~/api/nested/pinokio/start.js')
230
+ assert(checked.includes(path.resolve(launcher, 'start.js')))
231
+
232
+ await server.renderShortcuts(kernel.path('api'), 'nested', config, [], 'pinokio')
233
+ assert.equal(config.shortcuts[0].action.uri, '~/api/nested/pinokio/start.js')
234
+ })
235
+ })
236
+
237
+ test('root launcher load fallback keeps its legacy menu paths', async () => {
238
+ await withHome(async home => {
239
+ const kernel = kernelFor(home)
240
+ const workspace = path.resolve(home, 'api', 'fallback')
241
+ await write(workspace, 'pinokio.js')
242
+ await write(workspace, 'pinokio/pinokio.js')
243
+ await write(workspace, 'pinokio/start.js')
244
+ await write(workspace, 'pinokio.json')
245
+ kernel.loaded.set(path.resolve(workspace, 'pinokio/pinokio.js'), { menu: [] })
246
+ kernel.loaded.set(path.resolve(workspace, 'pinokio.json'), { menu: [{ href: '/api/fallback/start.js' }] })
247
+ const launcher = await kernel.api.launcher('fallback')
248
+ assert.equal(launcher.launcher_root, 'pinokio')
249
+ assert.equal(await kernel.api.launcher_path(workspace), workspace)
250
+
251
+ const checked = []
252
+ kernel.status = target => {
253
+ checked.push(target)
254
+ return false
255
+ }
256
+ const server = Object.assign(Object.create(Server.prototype), { kernel })
257
+ const config = {
258
+ menu: [
259
+ { text: 'Shell', run: 'echo ready' },
260
+ { text: 'Stop', action: { method: 'stop', uri: 'start.js' } },
261
+ { text: 'State', when: 'start.js', off: 'off' }
262
+ ],
263
+ shortcuts: [{ action: { method: 'stop', uri: 'start.js' } }]
264
+ }
265
+ await server.renderMenu({ launcher_root: launcher.launcher_root, $source: {} }, kernel.path('api'), 'fallback', config, [])
266
+ await server.renderShortcuts(kernel.path('api'), 'fallback', config, [], path.relative(workspace, await kernel.api.launcher_path(workspace)))
267
+
268
+ assert.equal(config.menu[0].cwd, path.resolve(home, 'api', 'pinokio', 'fallback'))
269
+ assert.equal(config.menu[1].action.uri, '~/api/fallback/start.js')
270
+ assert(checked.includes(path.resolve(workspace, 'start.js')))
271
+ assert.equal(config.shortcuts[0].action.uri, '~/api/fallback/start.js')
272
+
273
+ const app = (await kernel.api.listApps()).find(item => item.id === 'fallback')
274
+ const candidates = await new ServerAutolaunch(server).buildCandidates(app)
275
+ assert.equal(app.launcher_root, '')
276
+ assert(candidates.menu.some(item => item.script === 'pinokio/start.js'))
277
+ })
278
+ })
279
+
280
+ test('nested logs use launcher-relative paths in storage, display, and reports', async () => {
281
+ await withHome(async home => {
282
+ const kernel = kernelFor(home)
283
+ const workspace = path.resolve(home, 'api', 'nested')
284
+ const launcher = path.resolve(workspace, 'pinokio')
285
+ const script = await write(launcher, 'start.js')
286
+ await write(launcher, 'pinokio.js')
287
+
288
+ const socket = Object.assign(Object.create(Socket.prototype), { parent: { kernel } })
289
+ const logDir = await socket.resolveLogDir(script)
290
+ assert.equal(logDir, path.resolve(launcher, 'logs', 'api', 'start.js'))
291
+ const logFile = await write(logDir, 'latest', 'nested live content\n')
292
+ const sessionLog = await write(logDir, '123', 'nested report content\n')
293
+
294
+ const sessions = new AppLogSessions({ kernel, randomHex: () => 'nested' })
295
+ const run = await sessions.startRun({ path: script })
296
+ assert.equal(run.appRoot, launcher)
297
+ assert.equal(run.script, 'start.js')
298
+ await sessions.recordLogFile({ scriptPath: run.scriptPath, logFile: sessionLog, run })
299
+
300
+ const registry = new AppRegistryService({ kernel })
301
+ const selected = await new AppLogService({ registry }).resolveAppLogFile(workspace, '', ['pinokio/start.js'])
302
+ assert.equal(selected.script, 'start.js')
303
+ assert.equal(selected.file, logFile)
304
+
305
+ const server = Object.assign(Object.create(Server.prototype), { kernel })
306
+ assert.equal((await server.resolveLogsRoot({ workspace: 'nested' })).logsRoot, path.resolve(launcher, 'logs'))
307
+
308
+ const report = await new AppLogReportService({ registry, kernel }).buildReport({ appId: 'nested', status: { path: workspace, title: 'Nested' } })
309
+ assert.equal(report.sections[0].script, 'start.js')
310
+ assert.match(report.markdown, /nested report content/)
311
+ })
312
+ })
313
+
314
+ test('nested environment initialization adds parent repository excludes without touching root-layout repositories', async () => {
315
+ await withHome(async home => {
316
+ const kernel = kernelFor(home)
317
+ const nested = path.resolve(home, 'api', 'nested')
318
+ const root = path.resolve(home, 'api', 'root')
319
+ await write(nested, 'pinokio/pinokio.js')
320
+ await fs.mkdir(path.resolve(nested, '.git'), { recursive: true })
321
+ await write(root, 'pinokio.js')
322
+ await fs.mkdir(path.resolve(root, 'pinokio'), { recursive: true })
323
+ await fs.mkdir(path.resolve(root, '.git', 'info'), { recursive: true })
324
+ await write(root, '.git/info/exclude', 'existing\n')
325
+
326
+ await Environment.init({ name: 'nested' }, kernel)
327
+ assert.match(await fs.readFile(path.resolve(nested, '.git', 'info', 'exclude'), 'utf8'), /^\/pinokio\/ENVIRONMENT$/m)
328
+
329
+ await Environment.init({ name: 'root' }, kernel)
330
+ assert.equal(await fs.readFile(path.resolve(root, '.git', 'info', 'exclude'), 'utf8'), 'existing\n')
331
+
332
+ const worktree = path.resolve(home, 'api', 'worktree')
333
+ await write(worktree, 'pinokio/pinokio.js')
334
+ await write(worktree, '.git', 'gitdir: /tmp/example\n')
335
+ await Environment.init({ name: 'worktree' }, kernel)
336
+ assert.equal(await fs.readFile(path.resolve(worktree, '.git'), 'utf8'), 'gitdir: /tmp/example\n')
337
+ })
338
+ })