pinokiod 7.3.14 → 7.4.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.
Files changed (51) hide show
  1. package/kernel/api/index.js +111 -15
  2. package/kernel/api/script/index.js +10 -0
  3. package/kernel/autolaunch.js +111 -0
  4. package/kernel/environment.js +89 -1
  5. package/kernel/git.js +9 -19
  6. package/kernel/index.js +142 -43
  7. package/kernel/launch_requirements.js +1115 -0
  8. package/kernel/ready.js +231 -0
  9. package/kernel/script.js +16 -0
  10. package/kernel/shells.js +9 -1
  11. package/kernel/workspace_status.js +111 -45
  12. package/package.json +1 -1
  13. package/server/autolaunch.js +725 -0
  14. package/server/index.js +244 -411
  15. package/server/public/logs.js +15 -1
  16. package/server/public/style.css +99 -31
  17. package/server/views/app.ejs +263 -159
  18. package/server/views/autolaunch.ejs +363 -75
  19. package/server/views/index.ejs +550 -26
  20. package/server/views/logs.ejs +14 -12
  21. package/server/views/partials/app_autolaunch_dependency_events.ejs +99 -0
  22. package/server/views/partials/app_autolaunch_dependency_save.ejs +10 -0
  23. package/server/views/partials/app_autolaunch_dependency_styles.ejs +357 -0
  24. package/server/views/partials/app_autolaunch_modal_helpers.ejs +347 -0
  25. package/server/views/partials/autolaunch_dependency_helpers.ejs +204 -0
  26. package/server/views/partials/autolaunch_dependency_save.ejs +13 -0
  27. package/server/views/partials/autolaunch_dependency_styles.ejs +347 -0
  28. package/server/views/partials/home_action_modal.ejs +4 -1
  29. package/server/views/partials/launch_requirements_status_client.ejs +271 -0
  30. package/server/views/partials/launch_requirements_status_styles.ejs +171 -0
  31. package/server/views/partials/launch_settings_dependency_save_factory.ejs +45 -0
  32. package/server/views/partials/launch_settings_dependency_script_loader_factory.ejs +25 -0
  33. package/server/views/terminal.ejs +196 -2
  34. package/test/home-autolaunch-live-ui.test.js +455 -0
  35. package/test/launch-requirements-browser.test.js +579 -0
  36. package/test/launch-requirements-contract-coverage.test.js +627 -0
  37. package/test/launch-requirements-real-browser.js +625 -0
  38. package/test/launch-requirements-status-client.test.js +132 -0
  39. package/test/launch-requirements.test.js +1806 -0
  40. package/test/launch-settings-ui.test.js +370 -0
  41. package/test/ready-state.test.js +49 -0
  42. package/test/server-autolaunch.test.js +1052 -0
  43. package/test/startup-git-index-benchmark.js +409 -0
  44. package/test/startup-git-index-browser.js +320 -0
  45. package/test/startup-git-index-performance.test.js +380 -0
  46. package/test/startup-git-index-refactor.test.js +450 -0
  47. package/test/startup-git-index-route.test.js +588 -0
  48. package/test/universal-launcher.smoke.spec.js +10 -9
  49. package/test/workspace-gitignore-benchmark.js +815 -0
  50. package/test/workspace-gitignore-path-scoped.test.js +256 -0
  51. package/spec/INSTRUCTION_SYNC.md +0 -432
@@ -0,0 +1,450 @@
1
+ const assert = require('node:assert/strict')
2
+ const { execFileSync } = require('node:child_process')
3
+ const fs = require('node:fs')
4
+ const fsp = require('node:fs/promises')
5
+ const os = require('node:os')
6
+ const path = require('node:path')
7
+ const test = require('node:test')
8
+
9
+ const Api = require('../kernel/api')
10
+ const Git = require('../kernel/git')
11
+ const Shell = require('../kernel/shell')
12
+ const Shells = require('../kernel/shells')
13
+ const WorkspaceStatusManager = require('../kernel/workspace_status')
14
+
15
+ function run(cmd, args, cwd, options = {}) {
16
+ return execFileSync(cmd, args, {
17
+ cwd,
18
+ encoding: 'utf8',
19
+ env: {
20
+ ...process.env,
21
+ GIT_AUTHOR_NAME: 'Codex Test',
22
+ GIT_AUTHOR_EMAIL: 'codex-test@example.com',
23
+ GIT_COMMITTER_NAME: 'Codex Test',
24
+ GIT_COMMITTER_EMAIL: 'codex-test@example.com',
25
+ GIT_TERMINAL_PROMPT: '0',
26
+ ...options.env,
27
+ },
28
+ stdio: options.stdio || ['ignore', 'pipe', 'pipe'],
29
+ }).trim()
30
+ }
31
+
32
+ function git(args, cwd) {
33
+ return run('git', args, cwd)
34
+ }
35
+
36
+ function fileUrl(targetPath) {
37
+ return `file://${targetPath}`
38
+ }
39
+
40
+ function makeTempRoot(prefix) {
41
+ return fs.mkdtempSync(path.join(os.tmpdir(), prefix))
42
+ }
43
+
44
+ function makeLowercaseTmpRoot(name) {
45
+ const root = path.join('/tmp', `${name}-${process.pid}-${Date.now()}`)
46
+ fs.rmSync(root, { recursive: true, force: true })
47
+ fs.mkdirSync(root, { recursive: true })
48
+ return root
49
+ }
50
+
51
+ function createKernel(root) {
52
+ const kernel = {
53
+ homedir: root,
54
+ platform: 'test',
55
+ arch: 'test',
56
+ gpu_model: 'test-gpu',
57
+ ram: 1,
58
+ vram: 0,
59
+ envs: { ...process.env },
60
+ path: (...parts) => path.join(root, ...parts),
61
+ api: {
62
+ exists: async (targetPath) => {
63
+ try {
64
+ await fsp.access(targetPath)
65
+ return true
66
+ } catch (_) {
67
+ return false
68
+ }
69
+ },
70
+ },
71
+ exec: async (params) => {
72
+ const messages = Array.isArray(params.message) ? params.message : [params.message]
73
+ for (const message of messages) {
74
+ run('/bin/sh', ['-lc', message], params.path || root)
75
+ }
76
+ },
77
+ }
78
+ kernel.git = new Git(kernel)
79
+ return kernel
80
+ }
81
+
82
+ function initRepo(repoPath, { remote, file = 'tracked.txt', content = 'baseline\n', message = 'baseline' } = {}) {
83
+ fs.mkdirSync(repoPath, { recursive: true })
84
+ git(['init'], repoPath)
85
+ git(['config', 'user.name', 'Codex Test'], repoPath)
86
+ git(['config', 'user.email', 'codex-test@example.com'], repoPath)
87
+ fs.writeFileSync(path.join(repoPath, file), content)
88
+ git(['add', '.'], repoPath)
89
+ git(['commit', '-m', message], repoPath)
90
+ git(['branch', '-M', 'main'], repoPath)
91
+ if (remote) {
92
+ git(['remote', 'add', 'origin', remote], repoPath)
93
+ }
94
+ return git(['rev-parse', 'HEAD'], repoPath)
95
+ }
96
+
97
+ function pushWorkRepoToBare({ root, bareName, workName, file, firstContent, firstMessage, secondContent, secondMessage }) {
98
+ const bare = path.join(root, 'remotes', bareName)
99
+ const work = path.join(root, 'work', workName)
100
+ fs.mkdirSync(path.dirname(bare), { recursive: true })
101
+ fs.mkdirSync(path.dirname(work), { recursive: true })
102
+ git(['init', '--bare', bare], root)
103
+ fs.mkdirSync(work, { recursive: true })
104
+ git(['init'], work)
105
+ git(['config', 'user.name', 'Codex Test'], work)
106
+ git(['config', 'user.email', 'codex-test@example.com'], work)
107
+ fs.writeFileSync(path.join(work, file), firstContent)
108
+ git(['add', '.'], work)
109
+ git(['commit', '-m', firstMessage], work)
110
+ git(['branch', '-M', 'main'], work)
111
+ git(['remote', 'add', 'origin', fileUrl(bare)], work)
112
+ git(['push', '-u', 'origin', 'main'], work)
113
+ const firstHead = git(['rev-parse', 'HEAD'], work)
114
+
115
+ let secondHead = firstHead
116
+ if (secondContent != null) {
117
+ fs.writeFileSync(path.join(work, file), secondContent)
118
+ git(['commit', '-am', secondMessage || 'second'], work)
119
+ git(['push'], work)
120
+ secondHead = git(['rev-parse', 'HEAD'], work)
121
+ }
122
+
123
+ return {
124
+ bare,
125
+ work,
126
+ remote: fileUrl(bare),
127
+ firstHead,
128
+ secondHead,
129
+ }
130
+ }
131
+
132
+ function readCheckpointPayload(gitApi, saved) {
133
+ const payloadPath = gitApi.checkpointFilePath(saved.hash)
134
+ assert.ok(payloadPath, 'checkpoint hash should map to a payload path')
135
+ return JSON.parse(fs.readFileSync(payloadPath, 'utf8'))
136
+ }
137
+
138
+ test('recursive git repo scan preserves /info/api?git compatibility for top-level, nested, and missing remotes', async () => {
139
+ const root = makeTempRoot('pinokio-startup-info-api-')
140
+ try {
141
+ const apiRoot = path.join(root, 'api')
142
+ const workspace = path.join(apiRoot, 'fixture')
143
+ const nested = path.join(workspace, 'nested')
144
+ fs.mkdirSync(apiRoot, { recursive: true })
145
+
146
+ const topRemote = 'https://example.com/pinokio-startup-fixture.git'
147
+ const nestedRemote = 'https://example.com/pinokio-startup-fixture-nested.git'
148
+ initRepo(workspace, { remote: topRemote, content: 'top\n', message: 'top baseline' })
149
+ initRepo(nested, { remote: nestedRemote, file: 'nested.txt', content: 'nested\n', message: 'nested baseline' })
150
+
151
+ const kernel = createKernel(root)
152
+ await kernel.git.repos(apiRoot)
153
+
154
+ const topHit = kernel.git.find(topRemote)
155
+ assert.ok(topHit)
156
+ assert.equal(topHit.path, workspace)
157
+ const topRepos = (await kernel.git.repos(topHit.path)).filter((repo) => repo.main)
158
+ assert.equal(topRepos.length, 1)
159
+ assert.equal(topRepos[0].url, topRemote)
160
+
161
+ const nestedHit = kernel.git.find(nestedRemote)
162
+ assert.ok(nestedHit)
163
+ assert.equal(nestedHit.path, nested)
164
+ const nestedRepos = (await kernel.git.repos(nestedHit.path)).filter((repo) => repo.main)
165
+ assert.equal(nestedRepos.length, 1)
166
+ assert.equal(nestedRepos[0].url, nestedRemote)
167
+
168
+ assert.equal(kernel.git.find('https://example.com/missing.git'), undefined)
169
+ } finally {
170
+ fs.rmSync(root, { recursive: true, force: true })
171
+ }
172
+ })
173
+
174
+ test('workspace repo scans clear stale git index locks without a startup API-root scan', async () => {
175
+ const root = makeTempRoot('pinokio-startup-stale-lock-')
176
+ try {
177
+ const apiRoot = path.join(root, 'api')
178
+ const workspace = path.join(apiRoot, 'fixture')
179
+ const nested = path.join(workspace, 'nested')
180
+ fs.mkdirSync(apiRoot, { recursive: true })
181
+ initRepo(workspace, { remote: 'https://example.com/stale-lock-root.git', content: 'top\n' })
182
+ initRepo(nested, { remote: 'https://example.com/stale-lock-nested.git', file: 'nested.txt', content: 'nested\n' })
183
+
184
+ const topLock = path.join(workspace, '.git', 'index.lock')
185
+ const nestedLock = path.join(nested, '.git', 'index.lock')
186
+ fs.writeFileSync(topLock, '')
187
+ fs.writeFileSync(nestedLock, '')
188
+
189
+ const kernel = createKernel(root)
190
+ await kernel.git.repos(workspace)
191
+
192
+ assert.equal(fs.existsSync(topLock), false)
193
+ assert.equal(fs.existsSync(nestedLock), false)
194
+ } finally {
195
+ fs.rmSync(root, { recursive: true, force: true })
196
+ }
197
+ })
198
+
199
+ test('findRepoRootForPath resolves top-level and nested repo roots without prefilled git dirs', async () => {
200
+ const root = makeTempRoot('pinokio-startup-diff-root-')
201
+ try {
202
+ const apiRoot = path.join(root, 'api')
203
+ const workspace = path.join(apiRoot, 'fixture')
204
+ const nested = path.join(workspace, 'nested')
205
+ fs.mkdirSync(apiRoot, { recursive: true })
206
+ initRepo(workspace, { remote: 'https://example.com/root.git', content: 'top\n' })
207
+ initRepo(nested, { remote: 'https://example.com/nested.git', file: 'nested.txt', content: 'nested\n' })
208
+
209
+ const kernel = createKernel(root)
210
+ assert.equal(kernel.git.dirs.size, 0)
211
+
212
+ assert.equal(
213
+ await kernel.git.findRepoRootForPath(path.join(workspace, 'tracked.txt')),
214
+ workspace
215
+ )
216
+ assert.equal(
217
+ await kernel.git.findRepoRootForPath(path.join(nested, 'nested.txt')),
218
+ nested
219
+ )
220
+ } finally {
221
+ fs.rmSync(root, { recursive: true, force: true })
222
+ }
223
+ })
224
+
225
+ test('workspace status cache returns cached data, marks dirty, then refreshes the selected workspace', async () => {
226
+ const manager = new WorkspaceStatusManager({
227
+ enableWatchers: true,
228
+ fallbackIntervalMs: 60000,
229
+ })
230
+ let computeCount = 0
231
+ const computeStatus = async () => ({
232
+ totalChanges: computeCount++,
233
+ repos: [{ name: 'fixture', changeCount: computeCount }],
234
+ })
235
+
236
+ const first = await manager.getStatus('fixture', computeStatus)
237
+ assert.equal(first.totalChanges, 0)
238
+
239
+ const cached = await manager.getStatus('fixture', computeStatus)
240
+ assert.equal(cached, first)
241
+ assert.equal(computeCount, 1)
242
+
243
+ manager.markDirty('fixture')
244
+ const staleWhileRefreshing = await manager.getStatus('fixture', computeStatus)
245
+ assert.equal(staleWhileRefreshing, first)
246
+
247
+ const entry = manager.cache.get('fixture')
248
+ if (entry && entry.inflight) {
249
+ await entry.inflight
250
+ }
251
+
252
+ const refreshed = await manager.getStatus('fixture', computeStatus)
253
+ assert.equal(refreshed.totalChanges, 1)
254
+ assert.equal(computeCount, 2)
255
+ })
256
+
257
+ test('appendWorkspaceSnapshot writes the same checkpoint shape for main and nested repos', async () => {
258
+ const root = makeTempRoot('pinokio-startup-checkpoint-save-')
259
+ try {
260
+ const apiRoot = path.join(root, 'api')
261
+ const workspaceName = 'fixture'
262
+ const workspace = path.join(apiRoot, workspaceName)
263
+ const nested = path.join(workspace, 'nested')
264
+ fs.mkdirSync(apiRoot, { recursive: true })
265
+
266
+ const topRemote = 'https://example.com/checkpoint-fixture.git'
267
+ const nestedRemote = 'https://example.com/checkpoint-fixture-nested.git'
268
+ const topHead = initRepo(workspace, { remote: topRemote, content: 'top\n', message: 'top baseline' })
269
+ const nestedHead = initRepo(nested, { remote: nestedRemote, file: 'nested.txt', content: 'nested\n', message: 'nested baseline' })
270
+
271
+ const kernel = createKernel(root)
272
+ await kernel.git.loadCheckpoints()
273
+ const repos = await kernel.git.repos(workspace)
274
+ const saved = await kernel.git.appendWorkspaceSnapshot(workspaceName, repos)
275
+ assert.ok(saved)
276
+
277
+ const payload = readCheckpointPayload(kernel.git, saved)
278
+ assert.deepEqual(payload, {
279
+ version: 1,
280
+ root: 'https://example.com/checkpoint-fixture',
281
+ repos: [
282
+ {
283
+ path: '.',
284
+ repo: 'https://example.com/checkpoint-fixture',
285
+ commit: topHead,
286
+ },
287
+ {
288
+ path: 'nested',
289
+ repo: 'https://example.com/checkpoint-fixture-nested',
290
+ commit: nestedHead,
291
+ },
292
+ ],
293
+ })
294
+ } finally {
295
+ fs.rmSync(root, { recursive: true, force: true })
296
+ }
297
+ })
298
+
299
+ test('restoreNewReposForActiveSnapshot pins nested repos created after the workspace before-set', async () => {
300
+ const root = makeLowercaseTmpRoot('pinokio-startup-restore')
301
+ try {
302
+ const home = path.join(root, 'home')
303
+ const apiRoot = path.join(home, 'api')
304
+ fs.mkdirSync(apiRoot, { recursive: true })
305
+
306
+ const main = pushWorkRepoToBare({
307
+ root,
308
+ bareName: 'main.git',
309
+ workName: 'main-work',
310
+ file: 'pinokio.json',
311
+ firstContent: '{"title":"Restore Fixture","menu":[]}\n',
312
+ firstMessage: 'main baseline',
313
+ })
314
+ const nested = pushWorkRepoToBare({
315
+ root,
316
+ bareName: 'nested.git',
317
+ workName: 'nested-work',
318
+ file: 'nested.txt',
319
+ firstContent: 'nested pinned\n',
320
+ firstMessage: 'nested pinned',
321
+ secondContent: 'nested latest\n',
322
+ secondMessage: 'nested latest',
323
+ })
324
+
325
+ const workspaceName = 'restore-fixture'
326
+ const workspace = path.join(apiRoot, workspaceName)
327
+ git(['clone', main.remote, workspace], apiRoot)
328
+ const beforeDirs = new Set([workspace])
329
+ git(['clone', nested.remote, path.join(workspace, 'nested')], workspace)
330
+
331
+ const nestedHeadBefore = git(['-C', path.join(workspace, 'nested'), 'rev-parse', 'HEAD'], root)
332
+ assert.equal(nestedHeadBefore, nested.secondHead)
333
+
334
+ const kernel = createKernel(home)
335
+ await kernel.git.loadCheckpoints()
336
+ const snapshotId = 'restore-fixture'
337
+ const remoteKey = kernel.git.normalizeRemote(main.remote)
338
+ const saved = await kernel.git.writeCheckpointPayload(remoteKey, main.remote, {
339
+ id: snapshotId,
340
+ visibility: 'public',
341
+ checkpoint: {
342
+ root: main.remote,
343
+ repos: [
344
+ { path: '.', remote: main.remote, commit: main.firstHead },
345
+ { path: 'nested', remote: nested.remote, commit: nested.firstHead },
346
+ ],
347
+ },
348
+ })
349
+ assert.ok(saved)
350
+
351
+ kernel.git.activeSnapshot[workspaceName] = { id: snapshotId, remoteKey }
352
+ await kernel.git.restoreNewReposForActiveSnapshot(workspaceName, workspace, beforeDirs)
353
+
354
+ const nestedHeadAfter = git(['-C', path.join(workspace, 'nested'), 'rev-parse', 'HEAD'], root)
355
+ assert.equal(nestedHeadAfter, nested.firstHead)
356
+ } finally {
357
+ fs.rmSync(root, { recursive: true, force: true })
358
+ }
359
+ })
360
+
361
+ test('shell launch computes checkpoint restore before-set from the selected workspace, not global git dirs', async () => {
362
+ const root = makeTempRoot('pinokio-startup-shell-before-dirs-')
363
+ const originalStart = Shell.prototype.start
364
+ try {
365
+ const apiRoot = path.join(root, 'api')
366
+ const workspaceName = 'fixture'
367
+ const workspace = path.join(apiRoot, workspaceName)
368
+ const existingNested = path.join(workspace, 'existing-nested')
369
+ const unrelatedRepo = path.join(apiRoot, 'other-app')
370
+ fs.mkdirSync(existingNested, { recursive: true })
371
+ fs.mkdirSync(unrelatedRepo, { recursive: true })
372
+
373
+ const reposCalls = []
374
+ let restoreArgs = null
375
+ const kernel = {
376
+ homedir: root,
377
+ platform: 'darwin',
378
+ path: (...parts) => path.join(root, ...parts),
379
+ bin: {
380
+ envs: (env = {}) => env,
381
+ },
382
+ api: {
383
+ running: {},
384
+ resolvePath: (cwd, targetPath) => path.resolve(cwd, targetPath),
385
+ },
386
+ git: {
387
+ dirs: new Set([unrelatedRepo, workspace]),
388
+ repos: async (scanRoot) => {
389
+ reposCalls.push(scanRoot)
390
+ return [
391
+ { gitParentPath: workspace },
392
+ { gitParentPath: existingNested },
393
+ ]
394
+ },
395
+ restoreNewReposForActiveSnapshot: async (name, restoreRoot, beforeDirs) => {
396
+ restoreArgs = { name, restoreRoot, beforeDirs }
397
+ },
398
+ },
399
+ }
400
+ const shells = new Shells(kernel)
401
+ shells.ensureBracketedPasteSupport = async () => true
402
+ Shell.prototype.start = async () => ''
403
+
404
+ await shells.launch({
405
+ message: 'noop',
406
+ path: path.join(workspace, 'scripts'),
407
+ }, {}, () => {})
408
+
409
+ assert.deepEqual(reposCalls, [workspace])
410
+ assert.ok(restoreArgs)
411
+ assert.equal(restoreArgs.name, workspaceName)
412
+ assert.equal(restoreArgs.restoreRoot, workspace)
413
+ assert.deepEqual(
414
+ Array.from(restoreArgs.beforeDirs).sort(),
415
+ [workspace, existingNested].sort()
416
+ )
417
+ assert.equal(restoreArgs.beforeDirs.has(unrelatedRepo), false)
418
+ } finally {
419
+ Shell.prototype.start = originalStart
420
+ fs.rmSync(root, { recursive: true, force: true })
421
+ }
422
+ })
423
+
424
+ test('api.linkGit drops deleted top-level app remotes after an explicit refresh', async () => {
425
+ const root = makeTempRoot('pinokio-startup-linkgit-delete-')
426
+ try {
427
+ const apiRoot = path.join(root, 'api')
428
+ const workspace = path.join(apiRoot, 'delete-me')
429
+ fs.mkdirSync(apiRoot, { recursive: true })
430
+ const remote = 'https://example.com/delete-me.git'
431
+ initRepo(workspace, { remote, content: 'delete me\n' })
432
+
433
+ const api = new Api({
434
+ homedir: root,
435
+ path: (...parts) => path.join(root, ...parts),
436
+ })
437
+ api.userdir = apiRoot
438
+
439
+ await api.linkGit()
440
+ assert.deepEqual(api.gitPath, {
441
+ [remote]: workspace,
442
+ })
443
+
444
+ fs.rmSync(workspace, { recursive: true, force: true })
445
+ await api.linkGit()
446
+ assert.deepEqual(api.gitPath, {})
447
+ } finally {
448
+ fs.rmSync(root, { recursive: true, force: true })
449
+ }
450
+ })