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,1052 @@
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 Environment = require("../kernel/environment")
8
+ const ServerAutolaunch = require("../server/autolaunch")
9
+ const Util = require("../kernel/util")
10
+
11
+ const pathExists = async (target) => {
12
+ try {
13
+ await fs.stat(target)
14
+ return true
15
+ } catch (_) {
16
+ return false
17
+ }
18
+ }
19
+
20
+ const isSubpath = (parent, child) => {
21
+ const relative = path.relative(path.resolve(parent), path.resolve(child))
22
+ return !relative || (!relative.startsWith("..") && !path.isAbsolute(relative))
23
+ }
24
+
25
+ test("autolaunch route preserves configured script when startup is disabled", async () => {
26
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), "pinokio-server-autolaunch-"))
27
+ try {
28
+ const apiRoot = path.resolve(root, "api")
29
+ const appRoot = path.resolve(apiRoot, "target")
30
+ const envPath = path.resolve(appRoot, "ENVIRONMENT")
31
+ await fs.mkdir(appRoot, { recursive: true })
32
+ await fs.writeFile(path.resolve(appRoot, "start.js"), "module.exports = { run: [] }\n", "utf8")
33
+ await fs.writeFile(envPath, "PINOKIO_SCRIPT_AUTOLAUNCH=start.js\n", "utf8")
34
+
35
+ const routes = {}
36
+ const kernel = {
37
+ homedir: root,
38
+ exists: pathExists,
39
+ path: (name, ...chunks) => path.resolve(root, name, ...chunks),
40
+ api: {
41
+ userdir: apiRoot,
42
+ listApps: async () => [{ id: "target", title: "Target" }]
43
+ }
44
+ }
45
+ const server = {
46
+ kernel,
47
+ exists: pathExists,
48
+ is_subpath: isSubpath,
49
+ app: {
50
+ get: () => {},
51
+ post: (route, handler) => {
52
+ routes[route] = handler
53
+ }
54
+ }
55
+ }
56
+ new ServerAutolaunch(server).registerRoutes()
57
+
58
+ let statusCode = 200
59
+ let payload = null
60
+ await new Promise((resolve, reject) => {
61
+ routes["/autolaunch"](
62
+ { body: { app: "target", script: "start.js", enabled: false } },
63
+ {
64
+ status(code) {
65
+ statusCode = code
66
+ return this
67
+ },
68
+ json(body) {
69
+ payload = body
70
+ resolve()
71
+ }
72
+ },
73
+ (error) => {
74
+ if (error) reject(error)
75
+ }
76
+ )
77
+ })
78
+
79
+ const env = await Util.parse_env(envPath)
80
+ assert.equal(statusCode, 200)
81
+ assert.equal(payload.ok, true)
82
+ assert.equal(payload.app.autolaunch, "start.js")
83
+ assert.equal(payload.app.autolaunch_startup, "")
84
+ assert.equal(payload.app.autolaunch_enabled, false)
85
+ assert.equal(env.PINOKIO_SCRIPT_AUTOLAUNCH, "start.js")
86
+ assert.equal(env[Environment.SCRIPT_AUTOLAUNCH_ENABLED_KEY], "false")
87
+ } finally {
88
+ await fs.rm(root, { recursive: true, force: true })
89
+ }
90
+ })
91
+
92
+ test("autolaunch route rejects script save without explicit startup enabled state", async () => {
93
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), "pinokio-server-autolaunch-"))
94
+ try {
95
+ const apiRoot = path.resolve(root, "api")
96
+ const appRoot = path.resolve(apiRoot, "target")
97
+ const envPath = path.resolve(appRoot, "ENVIRONMENT")
98
+ await fs.mkdir(appRoot, { recursive: true })
99
+ await fs.writeFile(path.resolve(appRoot, "start.js"), "module.exports = { run: [] }\n", "utf8")
100
+ await fs.writeFile(envPath, [
101
+ "PINOKIO_SCRIPT_AUTOLAUNCH=start.js",
102
+ "PINOKIO_SCRIPT_AUTOLAUNCH_ENABLED=false",
103
+ ""
104
+ ].join("\n"), "utf8")
105
+
106
+ const routes = {}
107
+ const kernel = {
108
+ homedir: root,
109
+ exists: pathExists,
110
+ path: (name, ...chunks) => path.resolve(root, name, ...chunks),
111
+ api: {
112
+ userdir: apiRoot,
113
+ listApps: async () => [{ id: "target", title: "Target" }]
114
+ }
115
+ }
116
+ const server = {
117
+ kernel,
118
+ exists: pathExists,
119
+ is_subpath: isSubpath,
120
+ app: {
121
+ get: () => {},
122
+ post: (route, handler) => {
123
+ routes[route] = handler
124
+ }
125
+ }
126
+ }
127
+ new ServerAutolaunch(server).registerRoutes()
128
+
129
+ let statusCode = 200
130
+ let payload = null
131
+ await new Promise((resolve, reject) => {
132
+ routes["/autolaunch"](
133
+ { body: { app: "target", script: "start.js" } },
134
+ {
135
+ status(code) {
136
+ statusCode = code
137
+ return this
138
+ },
139
+ json(body) {
140
+ payload = body
141
+ resolve()
142
+ }
143
+ },
144
+ (error) => {
145
+ if (error) reject(error)
146
+ }
147
+ )
148
+ })
149
+
150
+ const env = await Util.parse_env(envPath)
151
+ assert.equal(statusCode, 400)
152
+ assert.equal(payload.ok, false)
153
+ assert.match(payload.error, /enabled/i)
154
+ assert.equal(env.PINOKIO_SCRIPT_AUTOLAUNCH, "start.js")
155
+ assert.equal(env[Environment.SCRIPT_AUTOLAUNCH_ENABLED_KEY], "false")
156
+ } finally {
157
+ await fs.rm(root, { recursive: true, force: true })
158
+ }
159
+ })
160
+
161
+ test("autolaunch route rejects clearing launch script while requirements exist", async () => {
162
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), "pinokio-server-autolaunch-"))
163
+ try {
164
+ const apiRoot = path.resolve(root, "api")
165
+ const appRoot = path.resolve(apiRoot, "target")
166
+ const envPath = path.resolve(appRoot, "ENVIRONMENT")
167
+ await fs.mkdir(appRoot, { recursive: true })
168
+ await fs.writeFile(path.resolve(appRoot, "start.js"), "module.exports = { run: [] }\n", "utf8")
169
+ await fs.writeFile(envPath, [
170
+ "PINOKIO_SCRIPT_AUTOLAUNCH=start.js",
171
+ "PINOKIO_SCRIPT_REQUIRES=helper",
172
+ ""
173
+ ].join("\n"), "utf8")
174
+
175
+ const routes = {}
176
+ const kernel = {
177
+ homedir: root,
178
+ exists: pathExists,
179
+ path: (name, ...chunks) => path.resolve(root, name, ...chunks),
180
+ api: {
181
+ userdir: apiRoot,
182
+ listApps: async () => [{ id: "target", title: "Target" }, { id: "helper", title: "Helper" }]
183
+ }
184
+ }
185
+ const server = {
186
+ kernel,
187
+ exists: pathExists,
188
+ is_subpath: isSubpath,
189
+ app: {
190
+ get: () => {},
191
+ post: (route, handler) => {
192
+ routes[route] = handler
193
+ }
194
+ }
195
+ }
196
+ new ServerAutolaunch(server).registerRoutes()
197
+
198
+ let statusCode = 200
199
+ let payload = null
200
+ await new Promise((resolve, reject) => {
201
+ routes["/autolaunch"](
202
+ { body: { app: "target", clear_script: true } },
203
+ {
204
+ status(code) {
205
+ statusCode = code
206
+ return this
207
+ },
208
+ json(body) {
209
+ payload = body
210
+ resolve()
211
+ }
212
+ },
213
+ (error) => {
214
+ if (error) reject(error)
215
+ }
216
+ )
217
+ })
218
+
219
+ const env = await Util.parse_env(envPath)
220
+ assert.equal(statusCode, 400)
221
+ assert.equal(payload.ok, false)
222
+ assert.match(payload.error, /Remove requirements before clearing/)
223
+ assert.equal(env.PINOKIO_SCRIPT_AUTOLAUNCH, "start.js")
224
+ assert.equal(env[Environment.SCRIPT_REQUIREMENTS_KEY], "helper")
225
+ } finally {
226
+ await fs.rm(root, { recursive: true, force: true })
227
+ }
228
+ })
229
+
230
+ test("autolaunch route rejects empty script without explicit clear action", async () => {
231
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), "pinokio-server-autolaunch-"))
232
+ try {
233
+ const apiRoot = path.resolve(root, "api")
234
+ const appRoot = path.resolve(apiRoot, "target")
235
+ const envPath = path.resolve(appRoot, "ENVIRONMENT")
236
+ await fs.mkdir(appRoot, { recursive: true })
237
+ await fs.writeFile(path.resolve(appRoot, "start.js"), "module.exports = { run: [] }\n", "utf8")
238
+ await fs.writeFile(envPath, [
239
+ "PINOKIO_SCRIPT_AUTOLAUNCH=start.js",
240
+ "PINOKIO_SCRIPT_AUTOLAUNCH_ENABLED=false",
241
+ ""
242
+ ].join("\n"), "utf8")
243
+
244
+ const routes = {}
245
+ const kernel = {
246
+ homedir: root,
247
+ exists: pathExists,
248
+ path: (name, ...chunks) => path.resolve(root, name, ...chunks),
249
+ api: {
250
+ userdir: apiRoot,
251
+ listApps: async () => [{ id: "target", title: "Target" }]
252
+ }
253
+ }
254
+ const server = {
255
+ kernel,
256
+ exists: pathExists,
257
+ is_subpath: isSubpath,
258
+ app: {
259
+ get: () => {},
260
+ post: (route, handler) => {
261
+ routes[route] = handler
262
+ }
263
+ }
264
+ }
265
+ new ServerAutolaunch(server).registerRoutes()
266
+
267
+ let statusCode = 200
268
+ let payload = null
269
+ await new Promise((resolve, reject) => {
270
+ routes["/autolaunch"](
271
+ { body: { app: "target", script: "", enabled: false } },
272
+ {
273
+ status(code) {
274
+ statusCode = code
275
+ return this
276
+ },
277
+ json(body) {
278
+ payload = body
279
+ resolve()
280
+ }
281
+ },
282
+ (error) => {
283
+ if (error) reject(error)
284
+ }
285
+ )
286
+ })
287
+
288
+ const env = await Util.parse_env(envPath)
289
+ assert.equal(statusCode, 400)
290
+ assert.equal(payload.ok, false)
291
+ assert.match(payload.error, /clear_script/i)
292
+ assert.equal(env.PINOKIO_SCRIPT_AUTOLAUNCH, "start.js")
293
+ assert.equal(env[Environment.SCRIPT_AUTOLAUNCH_ENABLED_KEY], "false")
294
+ } finally {
295
+ await fs.rm(root, { recursive: true, force: true })
296
+ }
297
+ })
298
+
299
+ test("home startup display graph skips cancelled startup roots", async () => {
300
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), "pinokio-server-autolaunch-"))
301
+ try {
302
+ const apiRoot = path.resolve(root, "api")
303
+ const targetRoot = path.resolve(apiRoot, "target")
304
+ const helperRoot = path.resolve(apiRoot, "helper")
305
+ await fs.mkdir(targetRoot, { recursive: true })
306
+ await fs.mkdir(helperRoot, { recursive: true })
307
+ await fs.writeFile(path.resolve(targetRoot, "ENVIRONMENT"), [
308
+ "PINOKIO_SCRIPT_AUTOLAUNCH=start.js",
309
+ "PINOKIO_SCRIPT_AUTOLAUNCH_ENABLED=true",
310
+ "PINOKIO_SCRIPT_REQUIRES=helper",
311
+ ""
312
+ ].join("\n"), "utf8")
313
+ await fs.writeFile(path.resolve(helperRoot, "ENVIRONMENT"), [
314
+ "PINOKIO_SCRIPT_AUTOLAUNCH=start.js",
315
+ "PINOKIO_SCRIPT_AUTOLAUNCH_ENABLED=false",
316
+ ""
317
+ ].join("\n"), "utf8")
318
+
319
+ const kernel = {
320
+ homedir: root,
321
+ launch_complete: false,
322
+ exists: pathExists,
323
+ path: (name, ...chunks) => path.resolve(root, name, ...chunks),
324
+ launchRequirements: {
325
+ isCancelled: (appId) => appId === "target"
326
+ },
327
+ api: {
328
+ userdir: apiRoot,
329
+ listApps: async () => [
330
+ { id: "target", title: "Target" },
331
+ { id: "helper", title: "Helper" }
332
+ ]
333
+ }
334
+ }
335
+ const server = {
336
+ kernel,
337
+ exists: pathExists,
338
+ is_subpath: isSubpath,
339
+ app: { get: () => {}, post: () => {} }
340
+ }
341
+ const graph = await new ServerAutolaunch(server).buildHomeStartupDisplayGraph()
342
+
343
+ assert.equal(graph.has("target"), false)
344
+ assert.equal(graph.has("helper"), false)
345
+ } finally {
346
+ await fs.rm(root, { recursive: true, force: true })
347
+ }
348
+ })
349
+
350
+ test("autolaunch dependencies route rejects invalid dependency ids atomically", async () => {
351
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), "pinokio-server-autolaunch-"))
352
+ try {
353
+ const apiRoot = path.resolve(root, "api")
354
+ const targetRoot = path.resolve(apiRoot, "target")
355
+ const helperRoot = path.resolve(apiRoot, "helper")
356
+ const existingRoot = path.resolve(apiRoot, "existing")
357
+ const targetEnv = path.resolve(targetRoot, "ENVIRONMENT")
358
+ await fs.mkdir(targetRoot, { recursive: true })
359
+ await fs.mkdir(helperRoot, { recursive: true })
360
+ await fs.mkdir(existingRoot, { recursive: true })
361
+ await fs.writeFile(path.resolve(targetRoot, "start.js"), "module.exports = { run: [] }\n", "utf8")
362
+ await fs.writeFile(path.resolve(helperRoot, "start.js"), "module.exports = { run: [] }\n", "utf8")
363
+ await fs.writeFile(path.resolve(existingRoot, "start.js"), "module.exports = { run: [] }\n", "utf8")
364
+ await fs.writeFile(targetEnv, [
365
+ "PINOKIO_SCRIPT_AUTOLAUNCH=start.js",
366
+ "PINOKIO_SCRIPT_REQUIRES=existing",
367
+ ""
368
+ ].join("\n"), "utf8")
369
+ await fs.writeFile(path.resolve(helperRoot, "ENVIRONMENT"), "PINOKIO_SCRIPT_AUTOLAUNCH=start.js\n", "utf8")
370
+ await fs.writeFile(path.resolve(existingRoot, "ENVIRONMENT"), "PINOKIO_SCRIPT_AUTOLAUNCH=start.js\n", "utf8")
371
+
372
+ const routes = {}
373
+ const kernel = {
374
+ homedir: root,
375
+ exists: pathExists,
376
+ path: (name, ...chunks) => path.resolve(root, name, ...chunks),
377
+ api: {
378
+ userdir: apiRoot,
379
+ listApps: async () => [
380
+ { id: "target", title: "Target" },
381
+ { id: "helper", title: "Helper" },
382
+ { id: "existing", title: "Existing" }
383
+ ]
384
+ }
385
+ }
386
+ const server = {
387
+ kernel,
388
+ exists: pathExists,
389
+ is_subpath: isSubpath,
390
+ app: {
391
+ get: () => {},
392
+ post: (route, handler) => {
393
+ routes[route] = handler
394
+ }
395
+ }
396
+ }
397
+ new ServerAutolaunch(server).registerRoutes()
398
+
399
+ let statusCode = 200
400
+ let payload = null
401
+ await new Promise((resolve, reject) => {
402
+ routes["/autolaunch/dependencies"](
403
+ { body: { app: "target", dependencies: ["helper", "missing"] } },
404
+ {
405
+ status(code) {
406
+ statusCode = code
407
+ return this
408
+ },
409
+ json(body) {
410
+ payload = body
411
+ resolve()
412
+ }
413
+ },
414
+ (error) => {
415
+ if (error) reject(error)
416
+ }
417
+ )
418
+ })
419
+
420
+ const env = await Util.parse_env(targetEnv)
421
+ assert.equal(statusCode, 400)
422
+ assert.equal(payload.ok, false)
423
+ assert.match(payload.error, /missing|invalid|installed/i)
424
+ assert.equal(env[Environment.SCRIPT_REQUIREMENTS_KEY], "existing")
425
+ } finally {
426
+ await fs.rm(root, { recursive: true, force: true })
427
+ }
428
+ })
429
+
430
+ test("autolaunch route clears launch script when requirements are empty", async () => {
431
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), "pinokio-server-autolaunch-"))
432
+ try {
433
+ const apiRoot = path.resolve(root, "api")
434
+ const appRoot = path.resolve(apiRoot, "target")
435
+ const envPath = path.resolve(appRoot, "ENVIRONMENT")
436
+ await fs.mkdir(appRoot, { recursive: true })
437
+ await fs.writeFile(path.resolve(appRoot, "start.js"), "module.exports = { run: [] }\n", "utf8")
438
+ await fs.writeFile(envPath, [
439
+ "PINOKIO_SCRIPT_AUTOLAUNCH=start.js",
440
+ "PINOKIO_SCRIPT_REQUIRES=",
441
+ ""
442
+ ].join("\n"), "utf8")
443
+
444
+ const routes = {}
445
+ const kernel = {
446
+ homedir: root,
447
+ exists: pathExists,
448
+ path: (name, ...chunks) => path.resolve(root, name, ...chunks),
449
+ api: {
450
+ userdir: apiRoot,
451
+ listApps: async () => [{ id: "target", title: "Target" }]
452
+ }
453
+ }
454
+ const server = {
455
+ kernel,
456
+ exists: pathExists,
457
+ is_subpath: isSubpath,
458
+ app: {
459
+ get: () => {},
460
+ post: (route, handler) => {
461
+ routes[route] = handler
462
+ }
463
+ }
464
+ }
465
+ new ServerAutolaunch(server).registerRoutes()
466
+
467
+ let statusCode = 200
468
+ let payload = null
469
+ await new Promise((resolve, reject) => {
470
+ routes["/autolaunch"](
471
+ { body: { app: "target", clear_script: true } },
472
+ {
473
+ status(code) {
474
+ statusCode = code
475
+ return this
476
+ },
477
+ json(body) {
478
+ payload = body
479
+ resolve()
480
+ }
481
+ },
482
+ (error) => {
483
+ if (error) reject(error)
484
+ }
485
+ )
486
+ })
487
+
488
+ const env = await Util.parse_env(envPath)
489
+ assert.equal(statusCode, 200)
490
+ assert.equal(payload.ok, true)
491
+ assert.equal(payload.app.autolaunch, "")
492
+ assert.equal(payload.app.autolaunch_enabled, false)
493
+ assert.equal(env.PINOKIO_SCRIPT_AUTOLAUNCH, "")
494
+ assert.equal(env[Environment.SCRIPT_AUTOLAUNCH_ENABLED_KEY], "")
495
+ assert.equal(env[Environment.SCRIPT_REQUIREMENTS_KEY], "")
496
+ } finally {
497
+ await fs.rm(root, { recursive: true, force: true })
498
+ }
499
+ })
500
+
501
+ test("disable all startup launch preserves configured scripts", async () => {
502
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), "pinokio-server-autolaunch-"))
503
+ try {
504
+ const apiRoot = path.resolve(root, "api")
505
+ const targetRoot = path.resolve(apiRoot, "target")
506
+ const helperRoot = path.resolve(apiRoot, "helper")
507
+ const targetEnv = path.resolve(targetRoot, "ENVIRONMENT")
508
+ const helperEnv = path.resolve(helperRoot, "ENVIRONMENT")
509
+ await fs.mkdir(targetRoot, { recursive: true })
510
+ await fs.mkdir(helperRoot, { recursive: true })
511
+ await fs.writeFile(path.resolve(targetRoot, "start.js"), "module.exports = { run: [] }\n", "utf8")
512
+ await fs.writeFile(path.resolve(helperRoot, "start.js"), "module.exports = { run: [] }\n", "utf8")
513
+ await fs.writeFile(targetEnv, "PINOKIO_SCRIPT_AUTOLAUNCH=start.js\n", "utf8")
514
+ await fs.writeFile(helperEnv, [
515
+ "PINOKIO_SCRIPT_AUTOLAUNCH=start.js",
516
+ "PINOKIO_SCRIPT_AUTOLAUNCH_ENABLED=false",
517
+ ""
518
+ ].join("\n"), "utf8")
519
+
520
+ const routes = {}
521
+ const kernel = {
522
+ homedir: root,
523
+ exists: pathExists,
524
+ path: (name, ...chunks) => path.resolve(root, name, ...chunks),
525
+ api: {
526
+ userdir: apiRoot,
527
+ listApps: async () => [{ id: "target", title: "Target" }, { id: "helper", title: "Helper" }]
528
+ }
529
+ }
530
+ const server = {
531
+ kernel,
532
+ exists: pathExists,
533
+ is_subpath: isSubpath,
534
+ app: {
535
+ get: () => {},
536
+ post: (route, handler) => {
537
+ routes[route] = handler
538
+ }
539
+ }
540
+ }
541
+ new ServerAutolaunch(server).registerRoutes()
542
+
543
+ let payload = null
544
+ await new Promise((resolve, reject) => {
545
+ routes["/autolaunch/disable-all"](
546
+ {},
547
+ {
548
+ json(body) {
549
+ payload = body
550
+ resolve()
551
+ }
552
+ },
553
+ (error) => {
554
+ if (error) reject(error)
555
+ }
556
+ )
557
+ })
558
+
559
+ const target = await Util.parse_env(targetEnv)
560
+ const helper = await Util.parse_env(helperEnv)
561
+ assert.equal(payload.ok, true)
562
+ assert.equal(payload.disabled, 1)
563
+ assert.equal(target.PINOKIO_SCRIPT_AUTOLAUNCH, "start.js")
564
+ assert.equal(target[Environment.SCRIPT_AUTOLAUNCH_ENABLED_KEY], "false")
565
+ assert.equal(helper.PINOKIO_SCRIPT_AUTOLAUNCH, "start.js")
566
+ assert.equal(helper[Environment.SCRIPT_AUTOLAUNCH_ENABLED_KEY], "false")
567
+ const targetState = payload.apps.find((app) => app.id === "target")
568
+ assert.equal(targetState.autolaunch, "start.js")
569
+ assert.equal(targetState.autolaunch_enabled, false)
570
+ } finally {
571
+ await fs.rm(root, { recursive: true, force: true })
572
+ }
573
+ })
574
+
575
+ test("autolaunch dependencies route rejects requirements without owning launch script", async () => {
576
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), "pinokio-server-autolaunch-"))
577
+ try {
578
+ const apiRoot = path.resolve(root, "api")
579
+ const appRoot = path.resolve(apiRoot, "target")
580
+ const helperRoot = path.resolve(apiRoot, "helper")
581
+ const envPath = path.resolve(appRoot, "ENVIRONMENT")
582
+ await fs.mkdir(appRoot, { recursive: true })
583
+ await fs.mkdir(helperRoot, { recursive: true })
584
+ await fs.writeFile(path.resolve(appRoot, "start.js"), "module.exports = { run: [] }\n", "utf8")
585
+ await fs.writeFile(path.resolve(helperRoot, "start.js"), "module.exports = { run: [] }\n", "utf8")
586
+ await fs.writeFile(envPath, "PINOKIO_SCRIPT_AUTOLAUNCH=\n", "utf8")
587
+
588
+ const routes = {}
589
+ const kernel = {
590
+ homedir: root,
591
+ exists: pathExists,
592
+ path: (name, ...chunks) => path.resolve(root, name, ...chunks),
593
+ api: {
594
+ userdir: apiRoot,
595
+ listApps: async () => [{ id: "target", title: "Target" }, { id: "helper", title: "Helper" }]
596
+ }
597
+ }
598
+ const server = {
599
+ kernel,
600
+ exists: pathExists,
601
+ is_subpath: isSubpath,
602
+ app: {
603
+ get: () => {},
604
+ post: (route, handler) => {
605
+ routes[route] = handler
606
+ }
607
+ }
608
+ }
609
+ new ServerAutolaunch(server).registerRoutes()
610
+
611
+ let statusCode = 200
612
+ let payload = null
613
+ await new Promise((resolve, reject) => {
614
+ routes["/autolaunch/dependencies"](
615
+ { body: { app: "target", dependencies: ["helper"] } },
616
+ {
617
+ status(code) {
618
+ statusCode = code
619
+ return this
620
+ },
621
+ json(body) {
622
+ payload = body
623
+ resolve()
624
+ }
625
+ },
626
+ (error) => {
627
+ if (error) reject(error)
628
+ }
629
+ )
630
+ })
631
+
632
+ const env = await Util.parse_env(envPath)
633
+ assert.equal(statusCode, 400)
634
+ assert.equal(payload.ok, false)
635
+ assert.match(payload.error, /Choose this app's launch script/)
636
+ assert.equal(env[Environment.SCRIPT_REQUIREMENTS_KEY], undefined)
637
+ } finally {
638
+ await fs.rm(root, { recursive: true, force: true })
639
+ }
640
+ })
641
+
642
+ test("autolaunch dependencies route rejects requirements whose app has no launch script", async () => {
643
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), "pinokio-server-autolaunch-"))
644
+ try {
645
+ const apiRoot = path.resolve(root, "api")
646
+ const appRoot = path.resolve(apiRoot, "target")
647
+ const helperRoot = path.resolve(apiRoot, "helper")
648
+ const envPath = path.resolve(appRoot, "ENVIRONMENT")
649
+ await fs.mkdir(appRoot, { recursive: true })
650
+ await fs.mkdir(helperRoot, { recursive: true })
651
+ await fs.writeFile(path.resolve(appRoot, "start.js"), "module.exports = { run: [] }\n", "utf8")
652
+ await fs.writeFile(path.resolve(helperRoot, "start.js"), "module.exports = { run: [] }\n", "utf8")
653
+ await fs.writeFile(envPath, "PINOKIO_SCRIPT_AUTOLAUNCH=start.js\n", "utf8")
654
+ await fs.writeFile(path.resolve(helperRoot, "ENVIRONMENT"), "PINOKIO_SCRIPT_AUTOLAUNCH=\n", "utf8")
655
+
656
+ const routes = {}
657
+ const kernel = {
658
+ homedir: root,
659
+ exists: pathExists,
660
+ path: (name, ...chunks) => path.resolve(root, name, ...chunks),
661
+ api: {
662
+ userdir: apiRoot,
663
+ listApps: async () => [{ id: "target", title: "Target" }, { id: "helper", title: "Helper" }]
664
+ }
665
+ }
666
+ const server = {
667
+ kernel,
668
+ exists: pathExists,
669
+ is_subpath: isSubpath,
670
+ app: {
671
+ get: () => {},
672
+ post: (route, handler) => {
673
+ routes[route] = handler
674
+ }
675
+ }
676
+ }
677
+ new ServerAutolaunch(server).registerRoutes()
678
+
679
+ let statusCode = 200
680
+ let payload = null
681
+ await new Promise((resolve, reject) => {
682
+ routes["/autolaunch/dependencies"](
683
+ { body: { app: "target", dependencies: ["helper"] } },
684
+ {
685
+ status(code) {
686
+ statusCode = code
687
+ return this
688
+ },
689
+ json(body) {
690
+ payload = body
691
+ resolve()
692
+ }
693
+ },
694
+ (error) => {
695
+ if (error) reject(error)
696
+ }
697
+ )
698
+ })
699
+
700
+ const env = await Util.parse_env(envPath)
701
+ assert.equal(statusCode, 400)
702
+ assert.equal(payload.ok, false)
703
+ assert.match(payload.error, /Choose Helper's launch script/)
704
+ assert.equal(env[Environment.SCRIPT_REQUIREMENTS_KEY], undefined)
705
+ } finally {
706
+ await fs.rm(root, { recursive: true, force: true })
707
+ }
708
+ })
709
+
710
+ test("autolaunch candidate state does not migrate startup script to a second launch key", async () => {
711
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), "pinokio-server-autolaunch-"))
712
+ try {
713
+ const apiRoot = path.resolve(root, "api")
714
+ const appRoot = path.resolve(apiRoot, "target")
715
+ const envPath = path.resolve(appRoot, "ENVIRONMENT")
716
+ await fs.mkdir(appRoot, { recursive: true })
717
+ await fs.writeFile(path.resolve(appRoot, "start.js"), "module.exports = { run: [] }\n", "utf8")
718
+ await fs.writeFile(envPath, "PINOKIO_SCRIPT_AUTOLAUNCH=start.js\n", "utf8")
719
+
720
+ const kernel = {
721
+ homedir: root,
722
+ exists: pathExists,
723
+ path: (name, ...chunks) => path.resolve(root, name, ...chunks),
724
+ api: {
725
+ userdir: apiRoot,
726
+ listApps: async () => [{ id: "target", title: "Target" }]
727
+ }
728
+ }
729
+ const server = {
730
+ kernel,
731
+ exists: pathExists,
732
+ is_subpath: isSubpath,
733
+ app: {
734
+ get: () => {},
735
+ post: () => {}
736
+ }
737
+ }
738
+ const autolaunch = new ServerAutolaunch(server)
739
+ const state = await autolaunch.buildAppState({ id: "target", title: "Target" })
740
+ const env = await Util.parse_env(envPath)
741
+
742
+ assert.equal(state.autolaunch, "start.js")
743
+ assert.equal(state.autolaunch_startup, "start.js")
744
+ assert.equal(state.autolaunch_enabled, true)
745
+ const forbiddenKey = "PINOKIO" + "_SCRIPT" + "_LAUNCH"
746
+ assert.equal(Object.prototype.hasOwnProperty.call(env, forbiddenKey), false)
747
+ } finally {
748
+ await fs.rm(root, { recursive: true, force: true })
749
+ }
750
+ })
751
+
752
+ test("home startup state renders configured requirement waiting before runtime status", async () => {
753
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), "pinokio-server-autolaunch-"))
754
+ try {
755
+ const apiRoot = path.resolve(root, "api")
756
+ const targetRoot = path.resolve(apiRoot, "target")
757
+ const helperRoot = path.resolve(apiRoot, "helper")
758
+ await fs.mkdir(targetRoot, { recursive: true })
759
+ await fs.mkdir(helperRoot, { recursive: true })
760
+ await fs.writeFile(path.resolve(targetRoot, "start.js"), "module.exports = { run: [] }\n", "utf8")
761
+ await fs.writeFile(path.resolve(helperRoot, "start.js"), "module.exports = { run: [] }\n", "utf8")
762
+ await fs.writeFile(path.resolve(targetRoot, "ENVIRONMENT"), [
763
+ "PINOKIO_SCRIPT_AUTOLAUNCH=start.js",
764
+ "PINOKIO_SCRIPT_REQUIRES=helper",
765
+ ""
766
+ ].join("\n"), "utf8")
767
+ await fs.writeFile(path.resolve(helperRoot, "ENVIRONMENT"), [
768
+ "PINOKIO_SCRIPT_AUTOLAUNCH=start.js",
769
+ ""
770
+ ].join("\n"), "utf8")
771
+
772
+ const kernel = {
773
+ homedir: root,
774
+ launch_complete: false,
775
+ autolaunch_status: { running: true, apps: {} },
776
+ exists: pathExists,
777
+ path: (name, ...chunks) => path.resolve(root, name, ...chunks),
778
+ api: {
779
+ userdir: apiRoot,
780
+ listApps: async () => [{ id: "target", title: "Target" }, { id: "helper", title: "Helper" }],
781
+ launcher: async (id) => ({ script: { title: id === "helper" ? "Helper" : "Target" } })
782
+ }
783
+ }
784
+ const server = {
785
+ kernel,
786
+ exists: pathExists,
787
+ is_subpath: isSubpath,
788
+ app: {
789
+ get: () => {},
790
+ post: () => {}
791
+ }
792
+ }
793
+ const autolaunch = new ServerAutolaunch(server)
794
+ const item = {
795
+ name: "Target",
796
+ uri: "target",
797
+ filepath: targetRoot,
798
+ path: "target"
799
+ }
800
+
801
+ const applied = await autolaunch.applyHomeStartingState(item, 0)
802
+
803
+ assert.equal(applied, true)
804
+ assert.equal(item.running, true)
805
+ assert.equal(item.autolaunch_starting, true)
806
+ assert.equal(item.autolaunch_waiting, true)
807
+ assert.equal(item.autolaunch_script, "start.js")
808
+ assert.equal(item.autolaunch_status_label, "Waiting for Helper")
809
+ } finally {
810
+ await fs.rm(root, { recursive: true, force: true })
811
+ }
812
+ })
813
+
814
+ test("home startup state renders recursive requirement-only rows before runtime status", async () => {
815
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), "pinokio-server-autolaunch-"))
816
+ try {
817
+ const apiRoot = path.resolve(root, "api")
818
+ const rootAppRoot = path.resolve(apiRoot, "root-app")
819
+ const middleAppRoot = path.resolve(apiRoot, "middle-app")
820
+ const leafAppRoot = path.resolve(apiRoot, "leaf-app")
821
+ await fs.mkdir(rootAppRoot, { recursive: true })
822
+ await fs.mkdir(middleAppRoot, { recursive: true })
823
+ await fs.mkdir(leafAppRoot, { recursive: true })
824
+ await fs.writeFile(path.resolve(rootAppRoot, "root.custom.js"), "module.exports = { run: [] }\n", "utf8")
825
+ await fs.writeFile(path.resolve(middleAppRoot, "middle.custom.js"), "module.exports = { run: [] }\n", "utf8")
826
+ await fs.writeFile(path.resolve(leafAppRoot, "leaf.custom.js"), "module.exports = { run: [] }\n", "utf8")
827
+ await fs.writeFile(path.resolve(rootAppRoot, "ENVIRONMENT"), [
828
+ "PINOKIO_SCRIPT_AUTOLAUNCH=root.custom.js",
829
+ "PINOKIO_SCRIPT_AUTOLAUNCH_ENABLED=true",
830
+ "PINOKIO_SCRIPT_REQUIRES=middle-app",
831
+ ""
832
+ ].join("\n"), "utf8")
833
+ await fs.writeFile(path.resolve(middleAppRoot, "ENVIRONMENT"), [
834
+ "PINOKIO_SCRIPT_AUTOLAUNCH=middle.custom.js",
835
+ "PINOKIO_SCRIPT_AUTOLAUNCH_ENABLED=false",
836
+ "PINOKIO_SCRIPT_REQUIRES=leaf-app",
837
+ ""
838
+ ].join("\n"), "utf8")
839
+ await fs.writeFile(path.resolve(leafAppRoot, "ENVIRONMENT"), [
840
+ "PINOKIO_SCRIPT_AUTOLAUNCH=leaf.custom.js",
841
+ "PINOKIO_SCRIPT_AUTOLAUNCH_ENABLED=false",
842
+ ""
843
+ ].join("\n"), "utf8")
844
+
845
+ const titles = {
846
+ "root-app": "Root App",
847
+ "middle-app": "Middle App",
848
+ "leaf-app": "Leaf App"
849
+ }
850
+ const kernel = {
851
+ homedir: root,
852
+ launch_complete: false,
853
+ autolaunch_status: { running: true, apps: {} },
854
+ exists: pathExists,
855
+ getScriptProgress: () => null,
856
+ path: (name, ...chunks) => path.resolve(root, name, ...chunks),
857
+ api: {
858
+ userdir: apiRoot,
859
+ listApps: async () => [
860
+ { id: "root-app", title: titles["root-app"] },
861
+ { id: "middle-app", title: titles["middle-app"] },
862
+ { id: "leaf-app", title: titles["leaf-app"] }
863
+ ],
864
+ launcher: async (id) => ({ script: { title: titles[id] || id } })
865
+ }
866
+ }
867
+ const server = {
868
+ kernel,
869
+ exists: pathExists,
870
+ is_subpath: isSubpath,
871
+ app: {
872
+ get: () => {},
873
+ post: () => {}
874
+ }
875
+ }
876
+ const autolaunch = new ServerAutolaunch(server)
877
+ const items = [
878
+ { name: "Root App", uri: "root-app", filepath: rootAppRoot, path: "root-app" },
879
+ { name: "Middle App", uri: "middle-app", filepath: middleAppRoot, path: "middle-app" },
880
+ { name: "Leaf App", uri: "leaf-app", filepath: leafAppRoot, path: "leaf-app" }
881
+ ]
882
+
883
+ const applied = []
884
+ for (const [index, item] of items.entries()) {
885
+ applied.push(await autolaunch.applyHomeStartingState(item, index))
886
+ }
887
+
888
+ assert.deepEqual(applied, [true, true, true])
889
+ assert.equal(items[0].autolaunch_status_label, "Waiting for Middle App")
890
+ assert.equal(items[1].autolaunch_status_label, "Waiting for Leaf App")
891
+ assert.equal(items[2].autolaunch_status_label, "Starting leaf.custom.js")
892
+ assert.equal(items[0].autolaunch_script, "root.custom.js")
893
+ assert.equal(items[1].autolaunch_script, "middle.custom.js")
894
+ assert.equal(items[2].autolaunch_script, "leaf.custom.js")
895
+ } finally {
896
+ await fs.rm(root, { recursive: true, force: true })
897
+ }
898
+ })
899
+
900
+ test("home startup state renders requirement-only startup rows from runtime status", async () => {
901
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), "pinokio-server-autolaunch-"))
902
+ try {
903
+ const apiRoot = path.resolve(root, "api")
904
+ const helperRoot = path.resolve(apiRoot, "helper")
905
+ await fs.mkdir(helperRoot, { recursive: true })
906
+ await fs.writeFile(path.resolve(helperRoot, "start.js"), "module.exports = { run: [] }\n", "utf8")
907
+ await fs.writeFile(path.resolve(helperRoot, "ENVIRONMENT"), [
908
+ "PINOKIO_SCRIPT_AUTOLAUNCH=start.js",
909
+ ""
910
+ ].join("\n"), "utf8")
911
+
912
+ const launchPath = path.resolve(helperRoot, "start.js")
913
+ const kernel = {
914
+ homedir: root,
915
+ launch_complete: false,
916
+ autolaunch_status: {
917
+ running: true,
918
+ apps: {
919
+ helper: {
920
+ id: "helper",
921
+ title: "Helper",
922
+ script: "start.js",
923
+ launch_path: launchPath,
924
+ state: "pending",
925
+ dependencies: [],
926
+ waiting_for: [],
927
+ startup_root: false
928
+ }
929
+ }
930
+ },
931
+ exists: pathExists,
932
+ getScriptProgress: () => null,
933
+ path: (name, ...chunks) => path.resolve(root, name, ...chunks),
934
+ api: {
935
+ userdir: apiRoot,
936
+ listApps: async () => [{ id: "helper", title: "Helper" }],
937
+ launcher: async () => ({ script: { title: "Helper" } })
938
+ }
939
+ }
940
+ const server = {
941
+ kernel,
942
+ exists: pathExists,
943
+ is_subpath: isSubpath,
944
+ app: {
945
+ get: () => {},
946
+ post: () => {}
947
+ }
948
+ }
949
+ const autolaunch = new ServerAutolaunch(server)
950
+ const item = {
951
+ name: "helper",
952
+ uri: "helper",
953
+ filepath: helperRoot,
954
+ path: "helper"
955
+ }
956
+
957
+ const applied = await autolaunch.applyHomeStartingState(item, 0)
958
+
959
+ assert.equal(applied, true)
960
+ assert.equal(item.running, true)
961
+ assert.equal(item.autolaunch_starting, true)
962
+ assert.equal(item.autolaunch_script, "start.js")
963
+ assert.equal(item.autolaunch_status_label, "Starting start.js")
964
+ } finally {
965
+ await fs.rm(root, { recursive: true, force: true })
966
+ }
967
+ })
968
+
969
+ test("home startup state renders active runtime status after launch_complete flips", async () => {
970
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), "pinokio-server-autolaunch-"))
971
+ try {
972
+ const apiRoot = path.resolve(root, "api")
973
+ const targetRoot = path.resolve(apiRoot, "target")
974
+ const helperRoot = path.resolve(apiRoot, "helper")
975
+ await fs.mkdir(targetRoot, { recursive: true })
976
+ await fs.mkdir(helperRoot, { recursive: true })
977
+ await fs.writeFile(path.resolve(targetRoot, "start.js"), "module.exports = { run: [] }\n", "utf8")
978
+ await fs.writeFile(path.resolve(helperRoot, "start.js"), "module.exports = { run: [] }\n", "utf8")
979
+ await fs.writeFile(path.resolve(targetRoot, "ENVIRONMENT"), [
980
+ "PINOKIO_SCRIPT_AUTOLAUNCH=start.js",
981
+ "PINOKIO_SCRIPT_REQUIRES=helper",
982
+ ""
983
+ ].join("\n"), "utf8")
984
+ await fs.writeFile(path.resolve(helperRoot, "ENVIRONMENT"), [
985
+ "PINOKIO_SCRIPT_AUTOLAUNCH=start.js",
986
+ ""
987
+ ].join("\n"), "utf8")
988
+
989
+ const kernel = {
990
+ homedir: root,
991
+ launch_complete: true,
992
+ autolaunch_status: {
993
+ running: false,
994
+ apps: {
995
+ target: {
996
+ id: "target",
997
+ title: "Target",
998
+ script: "start.js",
999
+ launch_path: path.resolve(targetRoot, "start.js"),
1000
+ state: "waiting",
1001
+ dependencies: ["helper"],
1002
+ waiting_for: ["helper"],
1003
+ startup_root: true
1004
+ },
1005
+ helper: {
1006
+ id: "helper",
1007
+ title: "Helper",
1008
+ script: "start.js",
1009
+ launch_path: path.resolve(helperRoot, "start.js"),
1010
+ state: "starting",
1011
+ dependencies: [],
1012
+ waiting_for: [],
1013
+ startup_root: false
1014
+ }
1015
+ }
1016
+ },
1017
+ exists: pathExists,
1018
+ getScriptProgress: () => null,
1019
+ path: (name, ...chunks) => path.resolve(root, name, ...chunks),
1020
+ api: {
1021
+ userdir: apiRoot,
1022
+ listApps: async () => [{ id: "target", title: "Target" }, { id: "helper", title: "Helper" }],
1023
+ launcher: async (id) => ({ script: { title: id === "helper" ? "Helper" : "Target" } })
1024
+ }
1025
+ }
1026
+ const server = {
1027
+ kernel,
1028
+ exists: pathExists,
1029
+ is_subpath: isSubpath,
1030
+ app: {
1031
+ get: () => {},
1032
+ post: () => {}
1033
+ }
1034
+ }
1035
+ const autolaunch = new ServerAutolaunch(server)
1036
+ const item = {
1037
+ name: "target",
1038
+ uri: "target",
1039
+ filepath: targetRoot,
1040
+ path: "target"
1041
+ }
1042
+
1043
+ const applied = await autolaunch.applyHomeStartingState(item, 0)
1044
+
1045
+ assert.equal(applied, true)
1046
+ assert.equal(item.running, true)
1047
+ assert.equal(item.autolaunch_waiting, true)
1048
+ assert.equal(item.autolaunch_status_label, "Waiting for Helper")
1049
+ } finally {
1050
+ await fs.rm(root, { recursive: true, force: true })
1051
+ }
1052
+ })