pinokiod 8.0.118 → 8.1.1
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.
- package/kernel/api/env/index.js +2 -2
- package/kernel/api/index.js +4 -3
- package/kernel/api/launcher_target.js +1 -0
- package/kernel/app_log_sessions.js +2 -1
- package/kernel/environment.js +2 -1
- package/kernel/nested_layout.js +140 -0
- package/kernel/util.js +1 -0
- package/package.json +1 -1
- package/server/autolaunch.js +1 -1
- package/server/index.js +13 -11
- package/server/lib/app_logs.js +2 -0
- package/server/lib/app_registry.js +1 -2
- package/server/lib/app_search.js +2 -0
- package/server/socket.js +1 -1
- package/server/views/partials/main_sidebar.ejs +2 -1
- package/test/main-sidebar.test.js +12 -0
- package/test/nested-launcher-parity.test.js +342 -0
package/kernel/api/env/index.js
CHANGED
|
@@ -15,7 +15,7 @@ class Env {
|
|
|
15
15
|
}
|
|
16
16
|
*/
|
|
17
17
|
// write to current app folder's ENVIRONMENT
|
|
18
|
-
let api_path = Util.api_path(req.parent.path, kernel)
|
|
18
|
+
let api_path = await kernel.api.launcher_path(Util.api_path(req.parent.path, kernel))
|
|
19
19
|
let env_path = path.resolve(api_path, "ENVIRONMENT")
|
|
20
20
|
await Util.update_env(env_path, req.params)
|
|
21
21
|
}
|
|
@@ -35,7 +35,7 @@ class Env {
|
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
*/
|
|
38
|
-
let api_path = Util.api_path(req.parent.path, kernel)
|
|
38
|
+
let api_path = await kernel.api.launcher_path(Util.api_path(req.parent.path, kernel))
|
|
39
39
|
let env_path = path.resolve(api_path, "ENVIRONMENT")
|
|
40
40
|
let env = await Environment.get2(req.parent.path, kernel)
|
|
41
41
|
// does the key exist?
|
package/kernel/api/index.js
CHANGED
|
@@ -131,7 +131,7 @@ class Api {
|
|
|
131
131
|
if (formData.icon_dirty) {
|
|
132
132
|
//
|
|
133
133
|
// write icon file
|
|
134
|
-
let icon_path =
|
|
134
|
+
let icon_path = path.resolve(launcher_path, formData.icon_path)
|
|
135
135
|
await fs.promises.writeFile(icon_path, formData.avatar)
|
|
136
136
|
meta.icon = formData.icon_path
|
|
137
137
|
dirty = true
|
|
@@ -232,7 +232,7 @@ class Api {
|
|
|
232
232
|
meta.ui = `/p/${api_name}`
|
|
233
233
|
meta.browse = `/p/${api_name}/dev`
|
|
234
234
|
} else {
|
|
235
|
-
|
|
235
|
+
Util.NestedLayout.setMetaIcon(meta, api_name, api_root_path, relpath, isWithinApiRoot)
|
|
236
236
|
meta.link = `/p/${api_name}/${relpath}/dev#n1`
|
|
237
237
|
meta.web_path = `/api/${api_name}/${relpath}`
|
|
238
238
|
meta.ui = `/p/${api_name}/${relpath}`
|
|
@@ -1791,6 +1791,7 @@ class Api {
|
|
|
1791
1791
|
})
|
|
1792
1792
|
}
|
|
1793
1793
|
async get_default(repo_path) {
|
|
1794
|
+
repo_path = await this.launcher_path(repo_path)
|
|
1794
1795
|
let launcher = await this.launcher({
|
|
1795
1796
|
path: repo_path
|
|
1796
1797
|
})
|
|
@@ -1882,7 +1883,7 @@ class Api {
|
|
|
1882
1883
|
if (chunks.length == 2) {
|
|
1883
1884
|
// the script is requesting a uri of the git repo
|
|
1884
1885
|
// look for pinokio.js
|
|
1885
|
-
let p = path.resolve(request.path, "pinokio.js")
|
|
1886
|
+
let p = path.resolve(await this.launcher_path(request.path), "pinokio.js")
|
|
1886
1887
|
let exists = await this.exists(p)
|
|
1887
1888
|
if (exists) {
|
|
1888
1889
|
await this.launch(request, p)
|
|
@@ -109,6 +109,7 @@ const loadMenu = async (api, repoPath) => {
|
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
module.exports = async (api, repoPath, preferred = []) => {
|
|
112
|
+
repoPath = await api.launcher_path(repoPath)
|
|
112
113
|
let defaultTarget = await api.get_default(repoPath)
|
|
113
114
|
if (defaultTarget) {
|
|
114
115
|
return {
|
|
@@ -3,6 +3,7 @@ const path = require('path')
|
|
|
3
3
|
const crypto = require('crypto')
|
|
4
4
|
const { AsyncLocalStorage } = require('async_hooks')
|
|
5
5
|
const Environment = require('./environment')
|
|
6
|
+
const NestedLayout = require('./nested_layout')
|
|
6
7
|
|
|
7
8
|
class AppLogSessions {
|
|
8
9
|
constructor({ kernel, now = () => new Date().toISOString(), randomHex = () => crypto.randomBytes(3).toString('hex') }) {
|
|
@@ -90,7 +91,7 @@ class AppLogSessions {
|
|
|
90
91
|
return {
|
|
91
92
|
appRoot,
|
|
92
93
|
scriptPath: absolute,
|
|
93
|
-
script: this.toPosix(parts.slice(1).join(path.sep))
|
|
94
|
+
script: this.toPosix(await NestedLayout.sessionScript(this, apiRoot, appRoot, absolute, parts.slice(1).join(path.sep)))
|
|
94
95
|
}
|
|
95
96
|
}
|
|
96
97
|
|
package/kernel/environment.js
CHANGED
|
@@ -660,7 +660,7 @@ const get = async (homedir, kernel) => {
|
|
|
660
660
|
for(let key in current_env) {
|
|
661
661
|
let val = current_env[key]
|
|
662
662
|
if (val.startsWith("./")) {
|
|
663
|
-
let full_path =
|
|
663
|
+
let full_path = await Util.NestedLayout.environmentPath(kernel, homedir, val, got_root)
|
|
664
664
|
current_env[key] = full_path
|
|
665
665
|
}
|
|
666
666
|
if (val.trim() === "") {
|
|
@@ -1013,6 +1013,7 @@ const init = async (options, kernel) => {
|
|
|
1013
1013
|
await fs.promises.appendFile(excludePath, appendContent)
|
|
1014
1014
|
}
|
|
1015
1015
|
}
|
|
1016
|
+
await Util.NestedLayout.ensureGitExclude(root, relpath, kernel)
|
|
1016
1017
|
return {
|
|
1017
1018
|
relpath,
|
|
1018
1019
|
root_path: root,
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
const fs = require('fs')
|
|
2
|
+
const path = require('path')
|
|
3
|
+
|
|
4
|
+
const samePath = (left, right) => path.resolve(left) === path.resolve(right)
|
|
5
|
+
|
|
6
|
+
const launcherPath = async (kernel, workspace) => {
|
|
7
|
+
if (!kernel.api || typeof kernel.api.launcher_path !== 'function') return workspace
|
|
8
|
+
return kernel.api.launcher_path(workspace)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const relativeRoot = async (kernel, workspace) => path.relative(workspace, await launcherPath(kernel, workspace))
|
|
12
|
+
|
|
13
|
+
const menuRoot = async (kernel, workspace, legacyRoot) => {
|
|
14
|
+
if (!legacyRoot) return ''
|
|
15
|
+
return samePath(await launcherPath(kernel, workspace), path.resolve(workspace, legacyRoot)) ? legacyRoot : ''
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const environmentPath = async (kernel, homedir, value, root) => {
|
|
19
|
+
const base = root.relpath ? await launcherPath(kernel, homedir) : homedir
|
|
20
|
+
return path.resolve(base, value)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const sessionScript = async (sessions, apiRoot, appRoot, scriptPath, fallback) => {
|
|
24
|
+
const workspace = path.resolve(apiRoot, path.relative(apiRoot, scriptPath).split(path.sep)[0])
|
|
25
|
+
if (samePath(appRoot, workspace)) return fallback
|
|
26
|
+
const root = await launcherPath(sessions.kernel, workspace)
|
|
27
|
+
return sessions.isPathWithin(root, scriptPath) ? path.relative(root, scriptPath) : fallback
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const logScript = async (kernel, scriptPath, relativeParts) => {
|
|
31
|
+
const fallback = relativeParts.slice(2)
|
|
32
|
+
if (relativeParts[2] !== 'pinokio') return fallback
|
|
33
|
+
const root = await launcherPath(kernel, kernel.path(...relativeParts.slice(0, 2)))
|
|
34
|
+
const relative = path.relative(root, scriptPath)
|
|
35
|
+
return relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative) ? fallback : relative.split(path.sep).filter(Boolean)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const appLogValues = async (registry, appRoot, scriptQuery, runtimeScripts) => {
|
|
39
|
+
const root = await launcherPath(registry.kernel || {}, appRoot)
|
|
40
|
+
if (samePath(root, appRoot)) return { appRoot, scriptQuery, runtimeScripts }
|
|
41
|
+
const strip = value => typeof value === 'string' ? value.replace(/^pinokio[\\/]/, '') : value
|
|
42
|
+
return { appRoot: root, scriptQuery: strip(scriptQuery), runtimeScripts: runtimeScripts.map(strip) }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const appScripts = async (registry, appRoot) => {
|
|
46
|
+
const root = await launcherPath(registry.kernel, appRoot)
|
|
47
|
+
const prefix = path.relative(appRoot, root).split(path.sep).join('/')
|
|
48
|
+
const candidates = files => files.map(file => path.posix.join(prefix, file))
|
|
49
|
+
return {
|
|
50
|
+
installScript: await registry.firstExistingScript(appRoot, candidates(['install.js', 'install.json'])),
|
|
51
|
+
startScript: await registry.firstExistingScript(appRoot, candidates(['start.js', 'start.json']))
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const addSearchCandidates = async (search, appRoot, candidates) => {
|
|
56
|
+
const root = await launcherPath(search.kernel, appRoot)
|
|
57
|
+
if (samePath(root, appRoot)) return
|
|
58
|
+
const prefix = path.relative(appRoot, root).split(path.sep).join('/')
|
|
59
|
+
const seen = new Set(candidates.map(candidate => candidate.relativePath))
|
|
60
|
+
for (const candidate of await search.collectAppSearchCandidates(root)) {
|
|
61
|
+
const relativePath = `${prefix}/${candidate.relativePath}`
|
|
62
|
+
if (!seen.has(relativePath)) candidates.push({ ...candidate, relativePath })
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const setMetaIcon = (meta, apiName, apiRoot, relpath, isWithinApiRoot) => {
|
|
67
|
+
const prefix = relpath.split(path.sep).join('/') === 'pinokio' && isWithinApiRoot(apiRoot) ? 'pinokio/' : ''
|
|
68
|
+
meta.icon = meta.icon ? `/asset/api/${apiName}/${prefix}${meta.icon}` : '/pinokio-black.png'
|
|
69
|
+
if (prefix && meta.iconpath) meta.iconpath = `${prefix}${meta.iconpath}`
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const preLauncher = async (kernel, launcher) => {
|
|
73
|
+
const root = await launcherPath(kernel, launcher.root)
|
|
74
|
+
if (samePath(root, launcher.root) || !launcher.script || !Array.isArray(launcher.script.pre)) return launcher
|
|
75
|
+
const prefix = path.relative(launcher.root, root).split(path.sep).join('/')
|
|
76
|
+
const pre = launcher.script.pre.map(item => {
|
|
77
|
+
if (!item || typeof item !== 'object') return item
|
|
78
|
+
const copy = { ...item }
|
|
79
|
+
if (typeof copy.icon === 'string' && copy.icon) copy.icon = `${prefix}/${copy.icon}`
|
|
80
|
+
if (typeof copy.href === 'string' && copy.href && !copy.href.startsWith('http')) copy.href = path.resolve(root, copy.href)
|
|
81
|
+
return copy
|
|
82
|
+
})
|
|
83
|
+
return { ...launcher, root, script: { ...launcher.script, pre } }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const addAiFiles = async (kernel, workspace, filenames, files, exists) => {
|
|
87
|
+
const root = await launcherPath(kernel, workspace)
|
|
88
|
+
if (samePath(root, workspace)) return files
|
|
89
|
+
for (const filename of filenames) {
|
|
90
|
+
const candidate = path.resolve(root, filename)
|
|
91
|
+
const relative = path.relative(workspace, candidate).split(path.sep).join('/')
|
|
92
|
+
if (!files.includes(relative) && await exists(candidate)) files.push(relative)
|
|
93
|
+
}
|
|
94
|
+
return files
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Keep in sync with the legacy list in Environment.init.
|
|
98
|
+
const excludeEntries = [
|
|
99
|
+
'ENVIRONMENT', '/.pinokio-temp', '/logs', '/cache', '/AGENTS.md',
|
|
100
|
+
'/CLAUDE.md', '/GEMINI.md', '/QWEN.md', '/.geminiignore',
|
|
101
|
+
'.clinerules', '.cursorrules', '.windsurfrules'
|
|
102
|
+
]
|
|
103
|
+
|
|
104
|
+
const ensureGitExclude = async (root, relpath, kernel) => {
|
|
105
|
+
if (!relpath || !samePath(await launcherPath(kernel, path.dirname(root)), root)) return
|
|
106
|
+
const workspace = path.dirname(root)
|
|
107
|
+
const gitDir = path.resolve(workspace, '.git')
|
|
108
|
+
try {
|
|
109
|
+
if (!(await fs.promises.stat(gitDir)).isDirectory()) return
|
|
110
|
+
const excludePath = path.resolve(gitDir, 'info', 'exclude')
|
|
111
|
+
let content = ''
|
|
112
|
+
try {
|
|
113
|
+
content = await fs.promises.readFile(excludePath, 'utf8')
|
|
114
|
+
} catch (error) {
|
|
115
|
+
if (error.code !== 'ENOENT') return
|
|
116
|
+
}
|
|
117
|
+
const existing = new Set(content.split(/\r?\n/).map(line => line.trim()).filter(Boolean))
|
|
118
|
+
const prefix = path.relative(workspace, root).split(path.sep).join('/')
|
|
119
|
+
const missing = excludeEntries.map(entry => `/${prefix}/${entry.replace(/^\//, '')}`).filter(entry => !existing.has(entry))
|
|
120
|
+
if (!missing.length) return
|
|
121
|
+
await fs.promises.mkdir(path.dirname(excludePath), { recursive: true })
|
|
122
|
+
await fs.promises.appendFile(excludePath, `${content && !content.endsWith('\n') ? '\n' : ''}${missing.join('\n')}\n`)
|
|
123
|
+
} catch (_) {
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
module.exports = {
|
|
128
|
+
addAiFiles,
|
|
129
|
+
addSearchCandidates,
|
|
130
|
+
appLogValues,
|
|
131
|
+
appScripts,
|
|
132
|
+
ensureGitExclude,
|
|
133
|
+
environmentPath,
|
|
134
|
+
logScript,
|
|
135
|
+
menuRoot,
|
|
136
|
+
preLauncher,
|
|
137
|
+
relativeRoot,
|
|
138
|
+
sessionScript,
|
|
139
|
+
setMetaIcon
|
|
140
|
+
}
|
package/kernel/util.js
CHANGED
package/package.json
CHANGED
package/server/autolaunch.js
CHANGED
|
@@ -406,7 +406,7 @@ class ServerAutolaunch {
|
|
|
406
406
|
if (!localScript) {
|
|
407
407
|
continue
|
|
408
408
|
}
|
|
409
|
-
const scriptPath = path.resolve(launcherRoot, localScript)
|
|
409
|
+
const scriptPath = path.resolve(app.launcher_root && menuitem.href.trim().startsWith(apiPrefix) ? appRoot : launcherRoot, localScript)
|
|
410
410
|
if (!this.server.is_subpath(appRoot, scriptPath)) {
|
|
411
411
|
continue
|
|
412
412
|
}
|
package/server/index.js
CHANGED
|
@@ -3861,7 +3861,7 @@ class Server {
|
|
|
3861
3861
|
config.shortcuts = config.shortcuts(this.kernel, this.kernel.info)
|
|
3862
3862
|
}
|
|
3863
3863
|
}
|
|
3864
|
-
await this.renderShortcuts(uri, item.name, config, pathComponents)
|
|
3864
|
+
await this.renderShortcuts(uri, item.name, config, pathComponents, await Util.NestedLayout.relativeRoot(this.kernel, this.kernel.path("api", item.name)))
|
|
3865
3865
|
items[i].shortcuts = config.shortcuts
|
|
3866
3866
|
}
|
|
3867
3867
|
}
|
|
@@ -4369,14 +4369,14 @@ class Server {
|
|
|
4369
4369
|
|
|
4370
4370
|
}
|
|
4371
4371
|
}
|
|
4372
|
-
async renderShortcuts(uri, name, config, pathComponents) {
|
|
4372
|
+
async renderShortcuts(uri, name, config, pathComponents, launcher_root = "") {
|
|
4373
4373
|
if (config.shortcuts) {
|
|
4374
4374
|
for(let i=0; i<config.shortcuts.length; i++) {
|
|
4375
4375
|
let shortcut = config.shortcuts[i]
|
|
4376
4376
|
if (shortcut.action) {
|
|
4377
4377
|
if (shortcut.action.method === "stop") {
|
|
4378
4378
|
if (shortcut.action.uri) {
|
|
4379
|
-
let absolute = path.resolve(__dirname, ...pathComponents, shortcut.action.uri)
|
|
4379
|
+
let absolute = path.resolve(__dirname, launcher_root, ...pathComponents, shortcut.action.uri)
|
|
4380
4380
|
let seed = path.resolve(__dirname)
|
|
4381
4381
|
let p = absolute.replace(seed, "")
|
|
4382
4382
|
let link = p.split(/[\/\\]/).filter((x) => { return x }).join("/")
|
|
@@ -4584,6 +4584,7 @@ class Server {
|
|
|
4584
4584
|
// }].concat(config.menu)
|
|
4585
4585
|
|
|
4586
4586
|
let launcher_root = req.launcher_root || ""
|
|
4587
|
+
let nested_root = await Util.NestedLayout.menuRoot(this.kernel, path.resolve(uri, name), launcher_root)
|
|
4587
4588
|
|
|
4588
4589
|
for(let i=0; i<config.menu.length; i++) {
|
|
4589
4590
|
let menuitem = config.menu[i]
|
|
@@ -4640,7 +4641,7 @@ class Server {
|
|
|
4640
4641
|
}
|
|
4641
4642
|
} else {
|
|
4642
4643
|
config.menu[i].run = rendered.run
|
|
4643
|
-
config.menu[i].cwd = path.resolve(this.kernel.homedir, "api", launcher_root, name)
|
|
4644
|
+
config.menu[i].cwd = nested_root ? path.resolve(this.kernel.homedir, "api", name, nested_root) : path.resolve(this.kernel.homedir, "api", launcher_root, name)
|
|
4644
4645
|
if (launcher_root) {
|
|
4645
4646
|
config.menu[i].href = "/api/" + name + "/" + launcher_root
|
|
4646
4647
|
} else {
|
|
@@ -4749,7 +4750,7 @@ class Server {
|
|
|
4749
4750
|
if (menuitem.action) {
|
|
4750
4751
|
if (menuitem.action.method === "stop") {
|
|
4751
4752
|
if (menuitem.action.uri) {
|
|
4752
|
-
let absolute = path.resolve(__dirname, ...pathComponents, menuitem.action.uri)
|
|
4753
|
+
let absolute = path.resolve(__dirname, nested_root, ...pathComponents, menuitem.action.uri)
|
|
4753
4754
|
let seed = path.resolve(__dirname)
|
|
4754
4755
|
let p = absolute.replace(seed, "")
|
|
4755
4756
|
let link = p.split(/[\/\\]/).filter((x) => { return x }).join("/")
|
|
@@ -4773,7 +4774,7 @@ class Server {
|
|
|
4773
4774
|
// check on/off: if on/off exists => assume that it's a script
|
|
4774
4775
|
// 1. check if the script is running
|
|
4775
4776
|
if (menuitem.when) {
|
|
4776
|
-
let scriptPath = path.resolve(uri, name, menuitem.when)
|
|
4777
|
+
let scriptPath = path.resolve(uri, name, nested_root, menuitem.when)
|
|
4777
4778
|
let filepath = scriptPath.replace(/\?.+/, "")
|
|
4778
4779
|
let check = this.kernel.status(filepath)
|
|
4779
4780
|
if (check) {
|
|
@@ -5227,7 +5228,7 @@ class Server {
|
|
|
5227
5228
|
if (!workspaceStats.isDirectory()) {
|
|
5228
5229
|
throw new Error('Workspace path is not a directory')
|
|
5229
5230
|
}
|
|
5230
|
-
const candidate = path.resolve(workspacePath, 'logs')
|
|
5231
|
+
const candidate = path.resolve(await this.kernel.api.launcher_path(workspacePath), 'logs')
|
|
5231
5232
|
await fs.promises.mkdir(candidate, { recursive: true })
|
|
5232
5233
|
return {
|
|
5233
5234
|
logsRoot: candidate,
|
|
@@ -14677,7 +14678,7 @@ class Server {
|
|
|
14677
14678
|
// }
|
|
14678
14679
|
// }))
|
|
14679
14680
|
this.app.post("/env", ex(async (req, res) => {
|
|
14680
|
-
let fullpath = path.resolve(this.kernel.homedir, req.body.filepath, "ENVIRONMENT")
|
|
14681
|
+
let fullpath = path.resolve(await this.kernel.api.launcher_path(path.resolve(this.kernel.homedir, req.body.filepath)), "ENVIRONMENT")
|
|
14681
14682
|
let updated = req.body.vals
|
|
14682
14683
|
let hosts = req.body.hosts
|
|
14683
14684
|
await Util.update_env(fullpath, updated)
|
|
@@ -14830,7 +14831,7 @@ class Server {
|
|
|
14830
14831
|
//})
|
|
14831
14832
|
}))
|
|
14832
14833
|
this.app.get("/pre/api/:name", ex(async (req, res) => {
|
|
14833
|
-
let launcher = await this.kernel.api.launcher(req.params.name)
|
|
14834
|
+
let launcher = await Util.NestedLayout.preLauncher(this.kernel, await this.kernel.api.launcher(req.params.name))
|
|
14834
14835
|
let config = launcher.script
|
|
14835
14836
|
if (config && Array.isArray(config.pre)) {
|
|
14836
14837
|
const items = config.pre.filter((item) => item && typeof item === "object")
|
|
@@ -14888,7 +14889,7 @@ class Server {
|
|
|
14888
14889
|
}
|
|
14889
14890
|
}))
|
|
14890
14891
|
this.app.get("/share/:name", ex(async (req, res) => {
|
|
14891
|
-
let filepath = path.resolve(this.kernel.
|
|
14892
|
+
let filepath = path.resolve(await this.kernel.api.launcher_path(req.params.name), "ENVIRONMENT")
|
|
14892
14893
|
//let filepath = path.resolve(this.kernel.homedir, req.params[0])
|
|
14893
14894
|
const config = await Util.parse_env(filepath)
|
|
14894
14895
|
const keys = [
|
|
@@ -15640,10 +15641,11 @@ class Server {
|
|
|
15640
15641
|
files.push(filename)
|
|
15641
15642
|
}
|
|
15642
15643
|
}
|
|
15644
|
+
files = await Util.NestedLayout.addAiFiles(this.kernel, this.kernel.path("api", req.params.name), filenames, files, this.exists.bind(this))
|
|
15643
15645
|
|
|
15644
15646
|
let items = files.map((item) => {
|
|
15645
15647
|
return {
|
|
15646
|
-
text: item,
|
|
15648
|
+
text: path.basename(item),
|
|
15647
15649
|
href: `/_api/${req.params.name}/${item}`
|
|
15648
15650
|
}
|
|
15649
15651
|
})
|
package/server/lib/app_logs.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const fs = require('fs')
|
|
2
2
|
const path = require('path')
|
|
3
|
+
const NestedLayout = require('../../kernel/nested_layout')
|
|
3
4
|
|
|
4
5
|
class AppLogService {
|
|
5
6
|
constructor({ registry }) {
|
|
@@ -38,6 +39,7 @@ class AppLogService {
|
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
async resolveAppLogFile(appRoot, scriptQuery = '', runtimeScripts = []) {
|
|
42
|
+
({ appRoot, scriptQuery, runtimeScripts } = await NestedLayout.appLogValues(this.registry, appRoot, scriptQuery, runtimeScripts))
|
|
41
43
|
const apiLogsRoot = path.resolve(appRoot, 'logs', 'api')
|
|
42
44
|
const candidates = []
|
|
43
45
|
const addCandidate = (value) => {
|
|
@@ -412,8 +412,7 @@ class AppRegistryService {
|
|
|
412
412
|
|
|
413
413
|
const runtime = this.collectAppRuntime(appRoot)
|
|
414
414
|
runtime.external_ready_urls = this.buildExternalReadyUrls(runtime.ready_url, options.source || null)
|
|
415
|
-
const installScript = await
|
|
416
|
-
const startScript = await this.firstExistingScript(appRoot, ['start.js', 'start.json'])
|
|
415
|
+
const { installScript, startScript } = await Util.NestedLayout.appScripts(this, appRoot)
|
|
417
416
|
let defaultTarget = null
|
|
418
417
|
try {
|
|
419
418
|
defaultTarget = await this.kernel.api.get_default(appRoot)
|
package/server/lib/app_search.js
CHANGED
|
@@ -2,6 +2,7 @@ const fs = require('fs')
|
|
|
2
2
|
const path = require('path')
|
|
3
3
|
const crypto = require('crypto')
|
|
4
4
|
const MiniSearch = require('minisearch')
|
|
5
|
+
const NestedLayout = require('../../kernel/nested_layout')
|
|
5
6
|
|
|
6
7
|
const APP_SEARCH_CACHE_TTL_MS = 15000
|
|
7
8
|
const APP_SEARCH_MAX_FILE_BYTES = 1024 * 1024
|
|
@@ -265,6 +266,7 @@ class AppSearchService {
|
|
|
265
266
|
for (const app of apps) {
|
|
266
267
|
const appRoot = this.kernel.path('api', app.name)
|
|
267
268
|
const candidates = await this.collectAppSearchCandidates(appRoot)
|
|
269
|
+
await NestedLayout.addSearchCandidates(this, appRoot, candidates)
|
|
268
270
|
const fingerprint = this.computeAppSearchFingerprint(app, candidates)
|
|
269
271
|
const previous = previousPerApp.get(app.name)
|
|
270
272
|
let appDocs
|
package/server/socket.js
CHANGED
|
@@ -729,8 +729,8 @@ class Socket {
|
|
|
729
729
|
if (relative === null) return null
|
|
730
730
|
let relative_parts = relative.split(path.sep).filter(Boolean)
|
|
731
731
|
if (relative_parts[0] === "api" && relative_parts[1]) {
|
|
732
|
-
let filepath_chunks = relative_parts.slice(2)
|
|
733
732
|
let cwd = this.parent.kernel.path(...relative_parts.slice(0, 2))
|
|
733
|
+
let filepath_chunks = await Util.NestedLayout.logScript(this.parent.kernel, p, relative_parts)
|
|
734
734
|
let root = await Environment.get_root({ path: cwd }, this.parent.kernel)
|
|
735
735
|
cwd = root.root
|
|
736
736
|
return path.resolve(cwd, "logs/api", ...filepath_chunks)
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
<aside class="main-sidebar" id="main-sidebar" aria-label="Primary navigation">
|
|
10
10
|
<nav class="main-sidebar-nav" aria-label="Pinokio navigation">
|
|
11
11
|
<div class='main-sidebar-section main-sidebar-section-actions' aria-label="Actions">
|
|
12
|
-
<a class='tab main-sidebar-action <%= sidebarSelected === '
|
|
12
|
+
<a class='tab main-sidebar-action <%= sidebarSelected === 'home' ? 'selected' : '' %>' id='home' href="/home"<% if (sidebarSelected === 'home') { %> aria-current="page"<% } %>><i class="fa-solid fa-layer-group main-sidebar-action-icon"></i><div class='caption'>My Apps</div></a>
|
|
13
|
+
<a class='tab main-sidebar-action <%= sidebarSelected === 'explore' ? 'selected' : '' %>' id='explore' href="/home?mode=explore"<% if (sidebarSelected === 'explore') { %> aria-current="page"<% } %>><i class="fa-solid fa-globe main-sidebar-action-icon"></i><div class='caption'>Explore</div></a>
|
|
13
14
|
<details class="universal-create-menu">
|
|
14
15
|
<summary class="tab main-sidebar-action" aria-label="Create options"><i class="fa-solid fa-plus main-sidebar-action-icon"></i><div class='caption'>Create</div></summary>
|
|
15
16
|
<div class="universal-create-dropdown">
|
|
@@ -5,6 +5,18 @@ const test = require('node:test')
|
|
|
5
5
|
|
|
6
6
|
const sidebarFile = path.resolve(__dirname, '..', 'server', 'views', 'partials', 'main_sidebar.ejs')
|
|
7
7
|
|
|
8
|
+
test('main sidebar exposes My Apps before Explore', async () => {
|
|
9
|
+
const source = await fs.readFile(sidebarFile, 'utf8')
|
|
10
|
+
|
|
11
|
+
const myAppsIndex = source.indexOf("<div class='caption'>My Apps</div>")
|
|
12
|
+
const exploreIndex = source.indexOf("<div class='caption'>Explore</div>")
|
|
13
|
+
|
|
14
|
+
assert.notEqual(myAppsIndex, -1)
|
|
15
|
+
assert.notEqual(exploreIndex, -1)
|
|
16
|
+
assert.ok(myAppsIndex < exploreIndex)
|
|
17
|
+
assert.match(source, /sidebarSelected === 'home'[\s\S]*id='home' href="\/home"[\s\S]*aria-current="page"[\s\S]*fa-layer-group[\s\S]*>My Apps</)
|
|
18
|
+
})
|
|
19
|
+
|
|
8
20
|
test('main sidebar moves Home Server under Configure', async () => {
|
|
9
21
|
const source = await fs.readFile(sidebarFile, 'utf8')
|
|
10
22
|
|
|
@@ -0,0 +1,342 @@
|
|
|
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: 'Absolute', action: { method: 'stop', uri: path.resolve('/abs', 'stop.js') } },
|
|
220
|
+
{ text: 'State', when: 'start.js', off: 'off' }
|
|
221
|
+
],
|
|
222
|
+
shortcuts: [
|
|
223
|
+
{ action: { method: 'stop', uri: 'start.js' } },
|
|
224
|
+
{ action: { method: 'stop', uri: path.resolve('/abs', 'stop.js') } }
|
|
225
|
+
]
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
await server.renderMenu({ launcher_root: 'pinokio', $source: {} }, kernel.path('api'), 'nested', config, [])
|
|
229
|
+
assert.equal(config.menu[0].href, '/api/nested/pinokio/start.js')
|
|
230
|
+
assert.equal(config.menu[1].cwd, launcher)
|
|
231
|
+
assert.equal(config.menu[2].action.uri, '~/api/nested/pinokio/start.js')
|
|
232
|
+
assert.equal(config.menu[3].action.uri, '~/api/nested/abs/stop.js')
|
|
233
|
+
assert(checked.includes(path.resolve(launcher, 'start.js')))
|
|
234
|
+
|
|
235
|
+
await server.renderShortcuts(kernel.path('api'), 'nested', config, [], 'pinokio')
|
|
236
|
+
assert.equal(config.shortcuts[0].action.uri, '~/api/nested/pinokio/start.js')
|
|
237
|
+
assert.equal(config.shortcuts[1].action.uri, '~/api/nested/abs/stop.js')
|
|
238
|
+
})
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
test('root launcher load fallback keeps its legacy menu paths', async () => {
|
|
242
|
+
await withHome(async home => {
|
|
243
|
+
const kernel = kernelFor(home)
|
|
244
|
+
const workspace = path.resolve(home, 'api', 'fallback')
|
|
245
|
+
await write(workspace, 'pinokio.js')
|
|
246
|
+
await write(workspace, 'pinokio/pinokio.js')
|
|
247
|
+
await write(workspace, 'pinokio/start.js')
|
|
248
|
+
await write(workspace, 'pinokio.json')
|
|
249
|
+
kernel.loaded.set(path.resolve(workspace, 'pinokio/pinokio.js'), { menu: [] })
|
|
250
|
+
kernel.loaded.set(path.resolve(workspace, 'pinokio.json'), { menu: [{ href: '/api/fallback/start.js' }] })
|
|
251
|
+
const launcher = await kernel.api.launcher('fallback')
|
|
252
|
+
assert.equal(launcher.launcher_root, 'pinokio')
|
|
253
|
+
assert.equal(await kernel.api.launcher_path(workspace), workspace)
|
|
254
|
+
|
|
255
|
+
const checked = []
|
|
256
|
+
kernel.status = target => {
|
|
257
|
+
checked.push(target)
|
|
258
|
+
return false
|
|
259
|
+
}
|
|
260
|
+
const server = Object.assign(Object.create(Server.prototype), { kernel })
|
|
261
|
+
const config = {
|
|
262
|
+
menu: [
|
|
263
|
+
{ text: 'Shell', run: 'echo ready' },
|
|
264
|
+
{ text: 'Stop', action: { method: 'stop', uri: 'start.js' } },
|
|
265
|
+
{ text: 'State', when: 'start.js', off: 'off' }
|
|
266
|
+
],
|
|
267
|
+
shortcuts: [{ action: { method: 'stop', uri: 'start.js' } }]
|
|
268
|
+
}
|
|
269
|
+
await server.renderMenu({ launcher_root: launcher.launcher_root, $source: {} }, kernel.path('api'), 'fallback', config, [])
|
|
270
|
+
await server.renderShortcuts(kernel.path('api'), 'fallback', config, [], path.relative(workspace, await kernel.api.launcher_path(workspace)))
|
|
271
|
+
|
|
272
|
+
assert.equal(config.menu[0].cwd, path.resolve(home, 'api', 'pinokio', 'fallback'))
|
|
273
|
+
assert.equal(config.menu[1].action.uri, '~/api/fallback/start.js')
|
|
274
|
+
assert(checked.includes(path.resolve(workspace, 'start.js')))
|
|
275
|
+
assert.equal(config.shortcuts[0].action.uri, '~/api/fallback/start.js')
|
|
276
|
+
|
|
277
|
+
const app = (await kernel.api.listApps()).find(item => item.id === 'fallback')
|
|
278
|
+
const candidates = await new ServerAutolaunch(server).buildCandidates(app)
|
|
279
|
+
assert.equal(app.launcher_root, '')
|
|
280
|
+
assert(candidates.menu.some(item => item.script === 'pinokio/start.js'))
|
|
281
|
+
})
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
test('nested logs use launcher-relative paths in storage, display, and reports', async () => {
|
|
285
|
+
await withHome(async home => {
|
|
286
|
+
const kernel = kernelFor(home)
|
|
287
|
+
const workspace = path.resolve(home, 'api', 'nested')
|
|
288
|
+
const launcher = path.resolve(workspace, 'pinokio')
|
|
289
|
+
const script = await write(launcher, 'start.js')
|
|
290
|
+
await write(launcher, 'pinokio.js')
|
|
291
|
+
|
|
292
|
+
const socket = Object.assign(Object.create(Socket.prototype), { parent: { kernel } })
|
|
293
|
+
const logDir = await socket.resolveLogDir(script)
|
|
294
|
+
assert.equal(logDir, path.resolve(launcher, 'logs', 'api', 'start.js'))
|
|
295
|
+
const logFile = await write(logDir, 'latest', 'nested live content\n')
|
|
296
|
+
const sessionLog = await write(logDir, '123', 'nested report content\n')
|
|
297
|
+
|
|
298
|
+
const sessions = new AppLogSessions({ kernel, randomHex: () => 'nested' })
|
|
299
|
+
const run = await sessions.startRun({ path: script })
|
|
300
|
+
assert.equal(run.appRoot, launcher)
|
|
301
|
+
assert.equal(run.script, 'start.js')
|
|
302
|
+
await sessions.recordLogFile({ scriptPath: run.scriptPath, logFile: sessionLog, run })
|
|
303
|
+
|
|
304
|
+
const registry = new AppRegistryService({ kernel })
|
|
305
|
+
const selected = await new AppLogService({ registry }).resolveAppLogFile(workspace, '', ['pinokio/start.js'])
|
|
306
|
+
assert.equal(selected.script, 'start.js')
|
|
307
|
+
assert.equal(selected.file, logFile)
|
|
308
|
+
|
|
309
|
+
const server = Object.assign(Object.create(Server.prototype), { kernel })
|
|
310
|
+
assert.equal((await server.resolveLogsRoot({ workspace: 'nested' })).logsRoot, path.resolve(launcher, 'logs'))
|
|
311
|
+
|
|
312
|
+
const report = await new AppLogReportService({ registry, kernel }).buildReport({ appId: 'nested', status: { path: workspace, title: 'Nested' } })
|
|
313
|
+
assert.equal(report.sections[0].script, 'start.js')
|
|
314
|
+
assert.match(report.markdown, /nested report content/)
|
|
315
|
+
})
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
test('nested environment initialization adds parent repository excludes without touching root-layout repositories', async () => {
|
|
319
|
+
await withHome(async home => {
|
|
320
|
+
const kernel = kernelFor(home)
|
|
321
|
+
const nested = path.resolve(home, 'api', 'nested')
|
|
322
|
+
const root = path.resolve(home, 'api', 'root')
|
|
323
|
+
await write(nested, 'pinokio/pinokio.js')
|
|
324
|
+
await fs.mkdir(path.resolve(nested, '.git'), { recursive: true })
|
|
325
|
+
await write(root, 'pinokio.js')
|
|
326
|
+
await fs.mkdir(path.resolve(root, 'pinokio'), { recursive: true })
|
|
327
|
+
await fs.mkdir(path.resolve(root, '.git', 'info'), { recursive: true })
|
|
328
|
+
await write(root, '.git/info/exclude', 'existing\n')
|
|
329
|
+
|
|
330
|
+
await Environment.init({ name: 'nested' }, kernel)
|
|
331
|
+
assert.match(await fs.readFile(path.resolve(nested, '.git', 'info', 'exclude'), 'utf8'), /^\/pinokio\/ENVIRONMENT$/m)
|
|
332
|
+
|
|
333
|
+
await Environment.init({ name: 'root' }, kernel)
|
|
334
|
+
assert.equal(await fs.readFile(path.resolve(root, '.git', 'info', 'exclude'), 'utf8'), 'existing\n')
|
|
335
|
+
|
|
336
|
+
const worktree = path.resolve(home, 'api', 'worktree')
|
|
337
|
+
await write(worktree, 'pinokio/pinokio.js')
|
|
338
|
+
await write(worktree, '.git', 'gitdir: /tmp/example\n')
|
|
339
|
+
await Environment.init({ name: 'worktree' }, kernel)
|
|
340
|
+
assert.equal(await fs.readFile(path.resolve(worktree, '.git'), 'utf8'), 'gitdir: /tmp/example\n')
|
|
341
|
+
})
|
|
342
|
+
})
|