pinokiod 7.3.15 → 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 (48) 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/views/app.ejs +256 -152
  16. package/server/views/autolaunch.ejs +363 -75
  17. package/server/views/index.ejs +550 -26
  18. package/server/views/partials/app_autolaunch_dependency_events.ejs +99 -0
  19. package/server/views/partials/app_autolaunch_dependency_save.ejs +10 -0
  20. package/server/views/partials/app_autolaunch_dependency_styles.ejs +357 -0
  21. package/server/views/partials/app_autolaunch_modal_helpers.ejs +347 -0
  22. package/server/views/partials/autolaunch_dependency_helpers.ejs +204 -0
  23. package/server/views/partials/autolaunch_dependency_save.ejs +13 -0
  24. package/server/views/partials/autolaunch_dependency_styles.ejs +347 -0
  25. package/server/views/partials/home_action_modal.ejs +4 -1
  26. package/server/views/partials/launch_requirements_status_client.ejs +271 -0
  27. package/server/views/partials/launch_requirements_status_styles.ejs +171 -0
  28. package/server/views/partials/launch_settings_dependency_save_factory.ejs +45 -0
  29. package/server/views/partials/launch_settings_dependency_script_loader_factory.ejs +25 -0
  30. package/server/views/terminal.ejs +196 -2
  31. package/test/home-autolaunch-live-ui.test.js +455 -0
  32. package/test/launch-requirements-browser.test.js +579 -0
  33. package/test/launch-requirements-contract-coverage.test.js +627 -0
  34. package/test/launch-requirements-real-browser.js +625 -0
  35. package/test/launch-requirements-status-client.test.js +132 -0
  36. package/test/launch-requirements.test.js +1806 -0
  37. package/test/launch-settings-ui.test.js +370 -0
  38. package/test/ready-state.test.js +49 -0
  39. package/test/server-autolaunch.test.js +1052 -0
  40. package/test/startup-git-index-benchmark.js +409 -0
  41. package/test/startup-git-index-browser.js +320 -0
  42. package/test/startup-git-index-performance.test.js +380 -0
  43. package/test/startup-git-index-refactor.test.js +450 -0
  44. package/test/startup-git-index-route.test.js +588 -0
  45. package/test/universal-launcher.smoke.spec.js +10 -9
  46. package/test/workspace-gitignore-benchmark.js +815 -0
  47. package/test/workspace-gitignore-path-scoped.test.js +256 -0
  48. package/spec/INSTRUCTION_SYNC.md +0 -432
package/server/index.js CHANGED
@@ -28,6 +28,7 @@ const system = require('systeminformation')
28
28
  const serveIndex = require('./serveIndex')
29
29
  const registerFileRoutes = require('./routes/files')
30
30
  const registerAppRoutes = require('./routes/apps')
31
+ const ServerAutolaunch = require('./autolaunch')
31
32
  const Git = require("../kernel/git")
32
33
  const TerminalApi = require('../kernel/api/terminal')
33
34
  const PluginSources = require("../kernel/plugin_sources")
@@ -192,28 +193,9 @@ class Server {
192
193
  /(^|\/)\.pytest_cache\//,
193
194
  /(^|\/)\.git\//
194
195
  ]
195
- this.apiGitRefreshTimer = null
196
196
  this.workspaceStatus = new WorkspaceStatusManager({
197
197
  enableWatchers: process.env.PINOKIO_DISABLE_WATCH === '1' ? false : true,
198
198
  fallbackIntervalMs: 60000,
199
- onEvent: (workspaceName, events) => {
200
- if (!this.kernel.homedir) return
201
- if (!events || events.length === 0) return
202
- const apiRoot = this.kernel.path('api')
203
- const topLevel = events.some(({ path: evtPath, type }) => {
204
- if (!evtPath) return false
205
- const rel = path.relative(apiRoot, evtPath)
206
- if (!rel || rel.startsWith('..') || path.isAbsolute(rel)) return false
207
- const parts = rel.split(path.sep)
208
- return parts.length === 1 && (type === 'create' || type === 'delete')
209
- })
210
- if (topLevel) {
211
- if (this.apiGitRefreshTimer) clearTimeout(this.apiGitRefreshTimer)
212
- this.apiGitRefreshTimer = setTimeout(() => {
213
- this.kernel.git.repos(apiRoot).catch(() => {})
214
- }, 1000)
215
- }
216
- },
217
199
  })
218
200
  this.appRegistry = new AppRegistryService({ kernel: this.kernel })
219
201
  this.appPreferences = new AppPreferencesService({ kernel: this.kernel })
@@ -228,6 +210,7 @@ class Server {
228
210
  })
229
211
  this.desktopEventRouter = createDesktopEventRouter({ kernel: this.kernel })
230
212
  this.injectRouter = createInjectRouter({ kernel: this.kernel })
213
+ this.autolaunch = new ServerAutolaunch(this)
231
214
 
232
215
  // sometimes the C:\Windows\System32 is not in PATH, need to add
233
216
  let platform = os.platform()
@@ -395,6 +378,73 @@ class Server {
395
378
  console.error('Failed to emit fatal notification:', err)
396
379
  }
397
380
  }
381
+ async refreshTopLevelAppInventory() {
382
+ try {
383
+ if (this.kernel && this.kernel.api && typeof this.kernel.api.linkGit === "function") {
384
+ await this.kernel.api.linkGit()
385
+ }
386
+ } catch (error) {
387
+ console.warn("App inventory refresh error:", error && error.message ? error.message : error)
388
+ }
389
+ }
390
+ async findApiRepoByGitUrl(gitUrl) {
391
+ if (!gitUrl || typeof gitUrl !== "string") return null
392
+ const candidates = [gitUrl]
393
+ if (gitUrl.endsWith(".git")) {
394
+ candidates.push(gitUrl.slice(0, -4))
395
+ } else {
396
+ candidates.push(`${gitUrl}.git`)
397
+ }
398
+ const mapping = this.kernel && this.kernel.git ? this.kernel.git.mapping : null
399
+ const removeStaleMapping = (candidate) => {
400
+ if (!mapping) return
401
+ delete mapping[candidate]
402
+ if (candidate.endsWith(".git")) {
403
+ delete mapping[candidate.slice(0, -4)]
404
+ } else {
405
+ delete mapping[`${candidate}.git`]
406
+ }
407
+ }
408
+ const isUsableRepoPath = async (repoPath) => {
409
+ if (!repoPath || typeof repoPath !== "string") return false
410
+ if (this.kernel.git && typeof this.kernel.git.hasGitMetadata === "function") {
411
+ return this.kernel.git.hasGitMetadata(repoPath)
412
+ }
413
+ try {
414
+ const stat = await fs.promises.stat(repoPath)
415
+ return stat.isDirectory()
416
+ } catch (_) {
417
+ return false
418
+ }
419
+ }
420
+
421
+ for (const candidate of candidates) {
422
+ const found = this.kernel.git.find(candidate)
423
+ if (found && await isUsableRepoPath(found.path)) return found
424
+ if (found) removeStaleMapping(candidate)
425
+ }
426
+
427
+ if (this.kernel.api && this.kernel.api.gitPath) {
428
+ for (const candidate of candidates) {
429
+ const repoPath = this.kernel.api.gitPath[candidate]
430
+ if (repoPath && await isUsableRepoPath(repoPath)) {
431
+ const found = { path: repoPath }
432
+ try {
433
+ found.head = await this.kernel.git.getHead(repoPath)
434
+ } catch (_) {}
435
+ this.kernel.git.mapping[candidate] = found
436
+ return found
437
+ }
438
+ }
439
+ }
440
+
441
+ await this.kernel.git.repos(this.kernel.path("api"))
442
+ for (const candidate of candidates) {
443
+ const found = this.kernel.git.find(candidate)
444
+ if (found) return found
445
+ }
446
+ return null
447
+ }
398
448
  stop() {
399
449
  if (this.resourceUsage && typeof this.resourceUsage.stop === 'function') {
400
450
  this.resourceUsage.stop()
@@ -759,6 +809,11 @@ class Server {
759
809
  shortcuts: x.shortcuts,
760
810
  index: x.index,
761
811
  running_scripts: x.running_scripts,
812
+ autolaunch_starting: x.autolaunch_starting ? true : false,
813
+ autolaunch_waiting: x.autolaunch_waiting ? true : false,
814
+ autolaunch_blocked: x.autolaunch_blocked ? true : false,
815
+ autolaunch_status_label: typeof x.autolaunch_status_label === "string" ? x.autolaunch_status_label : "",
816
+ autolaunch_script: typeof x.autolaunch_script === "string" ? x.autolaunch_script : "",
762
817
  //icon: (x.isDirectory() ? "fa-solid fa-folder" : "fa-regular fa-file"),
763
818
  name,
764
819
  uri,
@@ -1170,296 +1225,6 @@ class Server {
1170
1225
  list: this.getPeers(),
1171
1226
  }
1172
1227
  }
1173
- normalizeAutolaunchAppId(value) {
1174
- if (typeof value !== "string") {
1175
- return ""
1176
- }
1177
- const id = value.trim()
1178
- if (!id || id === "." || id === ".." || id.includes("\0") || /[\\/]/.test(id)) {
1179
- return ""
1180
- }
1181
- return id
1182
- }
1183
- normalizeAutolaunchScriptPath(value) {
1184
- if (typeof value !== "string") {
1185
- return ""
1186
- }
1187
- let script = value.trim().replace(/\\/g, "/")
1188
- if (!script || script.includes("\0")) {
1189
- return ""
1190
- }
1191
- script = script.split("#")[0].split("?")[0].trim()
1192
- if (!script || /^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(script) || script.startsWith("//")) {
1193
- return ""
1194
- }
1195
- if (script.startsWith("/")) {
1196
- return ""
1197
- }
1198
- const normalized = path.posix.normalize(script).replace(/^\.\/+/, "")
1199
- if (!normalized || normalized === "." || normalized === ".." || normalized.startsWith("../") || path.posix.isAbsolute(normalized)) {
1200
- return ""
1201
- }
1202
- return normalized
1203
- }
1204
- isAutolaunchScriptFilename(filename) {
1205
- const ext = path.extname(filename || "").toLowerCase()
1206
- if (![".js", ".json", ".mjs", ".cjs"].includes(ext)) {
1207
- return false
1208
- }
1209
- const base = path.basename(filename || "").toLowerCase()
1210
- return !["package.json", "pinokio.js", "pinokio.json", "pinokio_meta.json"].includes(base)
1211
- }
1212
- stripAutolaunchLabel(value) {
1213
- if (typeof value !== "string") {
1214
- return ""
1215
- }
1216
- return value
1217
- .replace(/<[^>]*>/g, " ")
1218
- .replace(/\s+/g, " ")
1219
- .trim()
1220
- }
1221
- async getAutolaunchAppById(appId) {
1222
- const id = this.normalizeAutolaunchAppId(appId)
1223
- if (!id) {
1224
- return null
1225
- }
1226
- const apps = await this.kernel.api.listApps()
1227
- return apps.find((app) => app && app.id === id) || null
1228
- }
1229
- async getAutolaunchEnvInfo(app) {
1230
- const appRoot = path.resolve(this.kernel.api.userdir, app.id)
1231
- const gotRoot = await Environment.get_root({ path: appRoot }, this.kernel)
1232
- const envRoot = gotRoot && gotRoot.root ? gotRoot.root : appRoot
1233
- const envPath = path.resolve(envRoot, "ENVIRONMENT")
1234
- const env = await Util.parse_env(envPath)
1235
- const value = typeof env.PINOKIO_SCRIPT_AUTOLAUNCH === "string"
1236
- ? env.PINOKIO_SCRIPT_AUTOLAUNCH.trim()
1237
- : ""
1238
- const exists = await this.exists(envPath)
1239
- return {
1240
- appRoot,
1241
- envRoot,
1242
- envPath,
1243
- envRelpath: gotRoot && gotRoot.relpath ? gotRoot.relpath : "",
1244
- exists,
1245
- value,
1246
- enabled: value.length > 0
1247
- }
1248
- }
1249
- async buildAutolaunchAppState(app) {
1250
- const envInfo = await this.getAutolaunchEnvInfo(app)
1251
- return {
1252
- id: app.id,
1253
- name: app.name,
1254
- title: app.title || app.name || app.id,
1255
- description: app.description || "",
1256
- icon: app.icon || "/pinokio-black.png",
1257
- workspace_path: app.workspace_path,
1258
- launcher_path: app.launcher_path,
1259
- launcher_root: app.launcher_root || "",
1260
- env_path: envInfo.envPath,
1261
- autolaunch: envInfo.value,
1262
- autolaunch_enabled: envInfo.enabled
1263
- }
1264
- }
1265
- async buildAutolaunchAppsState() {
1266
- const apps = await this.kernel.api.listApps()
1267
- const states = []
1268
- for (const app of apps) {
1269
- states.push(await this.buildAutolaunchAppState(app))
1270
- }
1271
- return states
1272
- }
1273
- async resolveAutolaunchScript(appRoot, script) {
1274
- const normalized = this.normalizeAutolaunchScriptPath(script)
1275
- if (!normalized || !this.isAutolaunchScriptFilename(normalized)) {
1276
- return null
1277
- }
1278
- const scriptPath = path.resolve(appRoot, normalized)
1279
- if (!this.is_subpath(appRoot, scriptPath)) {
1280
- return null
1281
- }
1282
- let stat
1283
- try {
1284
- stat = await fs.promises.stat(scriptPath)
1285
- } catch (_) {
1286
- return null
1287
- }
1288
- if (!stat || !stat.isFile()) {
1289
- return null
1290
- }
1291
- return {
1292
- script: normalized,
1293
- path: scriptPath
1294
- }
1295
- }
1296
- flattenAutolaunchMenu(menu, trail = []) {
1297
- const items = []
1298
- if (!Array.isArray(menu)) {
1299
- return items
1300
- }
1301
- for (const menuitem of menu) {
1302
- if (!menuitem || typeof menuitem !== "object") {
1303
- continue
1304
- }
1305
- const label = this.stripAutolaunchLabel(menuitem.text || menuitem.name || menuitem.html || "")
1306
- const nextTrail = label ? trail.concat(label) : trail
1307
- if (Array.isArray(menuitem.menu)) {
1308
- items.push(...this.flattenAutolaunchMenu(menuitem.menu, nextTrail))
1309
- } else {
1310
- items.push({ item: menuitem, group: trail.join(" / ") })
1311
- }
1312
- }
1313
- return items
1314
- }
1315
- async addAutolaunchCandidate(candidates, seen, candidate, appRoot) {
1316
- const resolved = await this.resolveAutolaunchScript(appRoot, candidate.script)
1317
- if (!resolved || seen.has(resolved.script)) {
1318
- return null
1319
- }
1320
- seen.add(resolved.script)
1321
- const label = this.stripAutolaunchLabel(candidate.label || "") || resolved.script
1322
- const menuDefault = !!candidate.menu_default
1323
- const item = {
1324
- script: resolved.script,
1325
- label,
1326
- group: candidate.group || "",
1327
- icon: candidate.icon || "",
1328
- source: candidate.source || "local",
1329
- menu_default: menuDefault,
1330
- has_params: !!candidate.has_params
1331
- }
1332
- candidates.push(item)
1333
- return item
1334
- }
1335
- async collectAutolaunchScriptFiles(root, appRoot) {
1336
- const results = []
1337
- const ignoredDirs = new Set([
1338
- ".git",
1339
- ".venv",
1340
- "__pycache__",
1341
- "app",
1342
- "cache",
1343
- "data",
1344
- "env",
1345
- "logs",
1346
- "models",
1347
- "node_modules",
1348
- "output",
1349
- "outputs",
1350
- "venv"
1351
- ])
1352
- const maxResults = 500
1353
- const walk = async (dir, depth) => {
1354
- if (depth > 4 || results.length >= maxResults) {
1355
- return
1356
- }
1357
- let entries
1358
- try {
1359
- entries = await fs.promises.readdir(dir, { withFileTypes: true })
1360
- } catch (_) {
1361
- return
1362
- }
1363
- for (const entry of entries) {
1364
- if (!entry || !entry.name || entry.name.includes("\0")) {
1365
- continue
1366
- }
1367
- const fullPath = path.resolve(dir, entry.name)
1368
- if (entry.isDirectory()) {
1369
- if (!ignoredDirs.has(entry.name)) {
1370
- await walk(fullPath, depth + 1)
1371
- }
1372
- continue
1373
- }
1374
- if (!entry.isFile() || !this.isAutolaunchScriptFilename(entry.name)) {
1375
- continue
1376
- }
1377
- if (!this.is_subpath(appRoot, fullPath)) {
1378
- continue
1379
- }
1380
- const rel = path.relative(appRoot, fullPath).split(path.sep).join("/")
1381
- results.push(rel)
1382
- }
1383
- }
1384
- await walk(root, 0)
1385
- results.sort((a, b) => a.localeCompare(b))
1386
- return results
1387
- }
1388
- async buildAutolaunchCandidates(app) {
1389
- const envInfo = await this.getAutolaunchEnvInfo(app)
1390
- const appRoot = envInfo.appRoot
1391
- const launcher = await this.kernel.api.launcher(app.id)
1392
- const launcherRoot = launcher && launcher.launcher_root
1393
- ? path.resolve(appRoot, launcher.launcher_root)
1394
- : appRoot
1395
- const menuCandidates = []
1396
- const otherCandidates = []
1397
- const seen = new Set()
1398
-
1399
- try {
1400
- let config = await this.kernel.api.meta(app.id)
1401
- config = await this.processMenu(app.id, safeStructuredClone(config || {}))
1402
- const flat = this.flattenAutolaunchMenu(config && config.menu ? config.menu : [])
1403
- for (const entry of flat) {
1404
- const menuitem = entry.item
1405
- if (!menuitem || typeof menuitem.href !== "string") {
1406
- continue
1407
- }
1408
- let href = menuitem.href.trim()
1409
- const apiPrefix = `/api/${app.id}/`
1410
- if (href.startsWith(apiPrefix)) {
1411
- href = href.slice(apiPrefix.length)
1412
- } else if (href.startsWith("/")) {
1413
- continue
1414
- }
1415
- const localScript = this.normalizeAutolaunchScriptPath(href)
1416
- if (!localScript) {
1417
- continue
1418
- }
1419
- const scriptPath = path.resolve(launcherRoot, localScript)
1420
- if (!this.is_subpath(appRoot, scriptPath)) {
1421
- continue
1422
- }
1423
- const script = path.relative(appRoot, scriptPath).split(path.sep).join("/")
1424
- await this.addAutolaunchCandidate(menuCandidates, seen, {
1425
- script,
1426
- label: menuitem.text || menuitem.name || script,
1427
- group: entry.group,
1428
- icon: typeof menuitem.icon === "string" ? menuitem.icon : "",
1429
- source: "menu",
1430
- menu_default: !!menuitem.default,
1431
- has_params: !!(menuitem.params && typeof menuitem.params === "object")
1432
- }, appRoot)
1433
- }
1434
- } catch (error) {
1435
- console.warn("[autolaunch] failed to resolve menu candidates", app.id, error && error.message ? error.message : error)
1436
- }
1437
-
1438
- if (envInfo.value) {
1439
- await this.addAutolaunchCandidate(menuCandidates, seen, {
1440
- script: envInfo.value,
1441
- label: envInfo.value,
1442
- source: "current"
1443
- }, appRoot)
1444
- }
1445
-
1446
- const localScripts = await this.collectAutolaunchScriptFiles(launcherRoot, appRoot)
1447
- for (const script of localScripts) {
1448
- await this.addAutolaunchCandidate(otherCandidates, seen, {
1449
- script,
1450
- label: script,
1451
- source: "local"
1452
- }, appRoot)
1453
- }
1454
-
1455
- return {
1456
- app: await this.buildAutolaunchAppState(app),
1457
- launcher_root: path.relative(appRoot, launcherRoot).split(path.sep).join("/"),
1458
- menu: menuCandidates,
1459
- other: otherCandidates,
1460
- current: envInfo.value
1461
- }
1462
- }
1463
1228
  async renderInvalidContentPage(req, res, invalid, options = {}) {
1464
1229
  const type = invalid && typeof invalid.type === "string" ? invalid.type : "app"
1465
1230
  const sidebarSelected = options.sidebarSelected || (type === "plugin" ? "plugins" : type === "task" ? "tasks" : "home")
@@ -1718,7 +1483,7 @@ class Server {
1718
1483
  let autolaunchAppState = null
1719
1484
  try {
1720
1485
  const appRoot = this.kernel.path("api", name)
1721
- autolaunchAppState = await this.buildAutolaunchAppState({
1486
+ autolaunchAppState = await this.autolaunch.buildAppState({
1722
1487
  id: name,
1723
1488
  name,
1724
1489
  title: config && config.title ? config.title : name,
@@ -1766,6 +1531,12 @@ class Server {
1766
1531
  config,
1767
1532
  protection_enabled: protectionPreference ? protectionPreference.protection_enabled !== false : false,
1768
1533
  autolaunch_app: autolaunchAppState,
1534
+ launch_requirements_status_enabled: !!(
1535
+ autolaunchAppState &&
1536
+ autolaunchAppState.autolaunch &&
1537
+ Array.isArray(autolaunchAppState.autolaunch_depends) &&
1538
+ autolaunchAppState.autolaunch_depends.length > 0
1539
+ ),
1769
1540
  // sidebar_url: "/pinokio/sidebar/" + name,
1770
1541
  home: req.originalUrl,
1771
1542
  run_tab,
@@ -2014,7 +1785,7 @@ class Server {
2014
1785
  let check = !!relative && !relative.startsWith('..') && !path.isAbsolute(relative);
2015
1786
  return check
2016
1787
  }
2017
- async getRepoHeadStatus(repoRelPath) {
1788
+ async getRepoHeadStatus(repoRelPath, options = {}) {
2018
1789
  const repoParam = repoRelPath || ""
2019
1790
  const dir = repoParam ? this.kernel.path("api", repoParam) : this.kernel.path("api")
2020
1791
 
@@ -2027,19 +1798,21 @@ class Server {
2027
1798
  return { changes: [], git_commit_url: null }
2028
1799
  }
2029
1800
 
2030
- let gitIgnoreEngine = null
2031
1801
  let workspaceName = null
1802
+ let workspaceRoot = null
2032
1803
  let workspaceRelBase = ''
1804
+ let gitIgnoreContext = null
2033
1805
  try {
2034
1806
  if (this.workspaceStatus && this.kernel && typeof this.kernel.path === 'function') {
2035
1807
  if (repoParam && repoParam.length > 0) {
2036
1808
  const parts = repoParam.split('/')
2037
1809
  workspaceName = parts[0]
2038
- const workspaceRoot = this.kernel.path("api", workspaceName)
2039
- await this.workspaceStatus.ensureGitIgnoreEngine(workspaceName, workspaceRoot)
2040
- gitIgnoreEngine = this.workspaceStatus.gitIgnoreEngines && this.workspaceStatus.gitIgnoreEngines.get
2041
- ? this.workspaceStatus.gitIgnoreEngines.get(workspaceName) || null
2042
- : null
1810
+ workspaceRoot = this.kernel.path("api", workspaceName)
1811
+ gitIgnoreContext = options && options.gitIgnoreContext
1812
+ ? options.gitIgnoreContext
1813
+ : (typeof this.workspaceStatus.createPathScopedGitIgnoreContext === 'function'
1814
+ ? this.workspaceStatus.createPathScopedGitIgnoreContext(workspaceRoot)
1815
+ : null)
2043
1816
  if (parts.length > 1) {
2044
1817
  workspaceRelBase = parts.slice(1).join('/')
2045
1818
  }
@@ -2116,16 +1889,16 @@ class Server {
2116
1889
  if (!filepath) {
2117
1890
  continue
2118
1891
  }
2119
- if (gitIgnoreEngine && workspaceName) {
1892
+ if (!shouldIncludePath(filepath)) {
1893
+ continue
1894
+ }
1895
+ if (gitIgnoreContext && workspaceName && this.workspaceStatus && typeof this.workspaceStatus.isPathScopedGitIgnored === 'function') {
2120
1896
  const workspaceRelative = workspaceRelBase ? `${workspaceRelBase}/${filepath}` : filepath
2121
1897
  const normalizedWorkspaceRel = normalizePath(workspaceRelative)
2122
- if (gitIgnoreEngine.ignores(normalizedWorkspaceRel)) {
1898
+ if (await this.workspaceStatus.isPathScopedGitIgnored(gitIgnoreContext, normalizedWorkspaceRel)) {
2123
1899
  continue
2124
1900
  }
2125
1901
  }
2126
- if (!shouldIncludePath(filepath)) {
2127
- continue
2128
- }
2129
1902
  const normalizedFile = normalizePath(filepath)
2130
1903
  const absolutePath = path.join(dir, filepath)
2131
1904
  let stats
@@ -2774,12 +2547,15 @@ class Server {
2774
2547
  async computeWorkspaceGitStatus(workspaceName) {
2775
2548
  const workspacePath = this.kernel.path("api", workspaceName)
2776
2549
  const repos = await this.kernel.git.repos(workspacePath)
2550
+ const gitIgnoreContext = this.workspaceStatus && typeof this.workspaceStatus.createPathScopedGitIgnoreContext === 'function'
2551
+ ? this.workspaceStatus.createPathScopedGitIgnoreContext(workspacePath)
2552
+ : null
2777
2553
 
2778
2554
  const statuses = []
2779
2555
  for (const repo of repos) {
2780
2556
  const repoParam = repo.gitParentRelPath || workspaceName
2781
2557
  try {
2782
- const { changes, git_commit_url, git_history_url, git_fork_url, git_push_url } = await this.getRepoHeadStatus(repoParam)
2558
+ const { changes, git_commit_url, git_history_url, git_fork_url, git_push_url } = await this.getRepoHeadStatus(repoParam, { gitIgnoreContext })
2783
2559
  const historyUrl = git_history_url || (repoParam ? `/info/git/HEAD/${repoParam}` : `/info/git/HEAD/${workspaceName}`)
2784
2560
  statuses.push({
2785
2561
  name: repo.name,
@@ -3320,6 +3096,7 @@ class Server {
3320
3096
  const result = {
3321
3097
  portal: this.portal,
3322
3098
  projectName: (pathComponents.length > 0 ? pathComponents[0] : ''),
3099
+ name: (pathComponents.length > 0 ? pathComponents[0] : ''),
3323
3100
  protection_app_id: protectionAppId,
3324
3101
  protection_enabled: protectionPreference ? protectionPreference.protection_enabled !== false : false,
3325
3102
  active_process_wait: activeProcessWait ? {
@@ -3371,6 +3148,9 @@ class Server {
3371
3148
  taskSaveWorkspacesRoot: this.kernel.path("workspaces"),
3372
3149
  script_id: (req.base ? `${full_filepath}?cwd=${req.query.cwd}` : null),
3373
3150
  script_path: (req.base ? full_filepath : null),
3151
+ launch_requirements_status_enabled: this.kernel && typeof this.kernel.hasLaunchRequirementConfig === "function"
3152
+ ? await this.kernel.hasLaunchRequirementConfig(filepath)
3153
+ : false,
3374
3154
  }
3375
3155
 
3376
3156
  res.render(template, result)
@@ -3596,6 +3376,9 @@ class Server {
3596
3376
 
3597
3377
 
3598
3378
  let index = 0
3379
+ const homeStartupDisplayGraph = !this.kernel.launch_complete
3380
+ ? await this.autolaunch.buildHomeStartupDisplayGraph()
3381
+ : null
3599
3382
  for(let i=0; i<items.length; i++) {
3600
3383
  let item = items[i]
3601
3384
  let launcher = await this.kernel.api.launcher(item.name)
@@ -3793,7 +3576,14 @@ class Server {
3793
3576
  if (this.is_subpath(api_path, key)) {
3794
3577
  // scripts inside api folder
3795
3578
  if (this.is_subpath(p, key)) {
3796
- items[i].running_scripts.push({ path: path.relative(this.kernel.homedir, key), name: path.relative(p, key) })
3579
+ const progress = this.kernel && typeof this.kernel.getScriptProgress === "function"
3580
+ ? this.kernel.getScriptProgress(key)
3581
+ : null
3582
+ items[i].running_scripts.push({
3583
+ path: path.relative(this.kernel.homedir, key),
3584
+ name: path.relative(p, key),
3585
+ ...(progress || {})
3586
+ })
3797
3587
  }
3798
3588
  } else {
3799
3589
  // other global scripts
@@ -3810,6 +3600,14 @@ class Server {
3810
3600
  }
3811
3601
  }
3812
3602
  items[i].terminal_online_count = userTerminalShellMatches.length + pluginTerminalRunCount
3603
+ if (items[i].running && !this.kernel.launch_complete) {
3604
+ const status = this.kernel.autolaunch_status && this.kernel.autolaunch_status.apps
3605
+ ? this.kernel.autolaunch_status.apps[items[i].name]
3606
+ : null
3607
+ if (status && !["ready", "blocked"].includes(status.state || "")) {
3608
+ await this.autolaunch.applyHomeStartingState(items[i], items[i].index, homeStartupDisplayGraph)
3609
+ }
3610
+ }
3813
3611
  if (!items[i].running && shellMatches && shellMatches.length > 0) {
3814
3612
  running.push(items[i])
3815
3613
  items[i].running = true
@@ -3818,9 +3616,14 @@ class Server {
3818
3616
  index++
3819
3617
  }
3820
3618
  if (!items[i].running) {
3821
- items[i].index = index
3822
- index++;
3823
- notRunning.push(items[i])
3619
+ if (await this.autolaunch.applyHomeStartingState(items[i], index, homeStartupDisplayGraph)) {
3620
+ running.push(items[i])
3621
+ index++
3622
+ } else {
3623
+ items[i].index = index
3624
+ index++;
3625
+ notRunning.push(items[i])
3626
+ }
3824
3627
  }
3825
3628
  }
3826
3629
  }
@@ -6346,7 +6149,6 @@ class Server {
6346
6149
  let str = await Environment.ENV("system", this.kernel.homedir, this.kernel)
6347
6150
  await fs.promises.writeFile(path.resolve(this.kernel.homedir, "ENVIRONMENT"), str)
6348
6151
  }
6349
- this.workspaceStatus.ensureWatcher('api', this.kernel.path('api')).catch(() => {})
6350
6152
  }
6351
6153
 
6352
6154
 
@@ -6613,88 +6415,64 @@ class Server {
6613
6415
  }
6614
6416
  })
6615
6417
  */
6616
- this.app.get("/autolaunch/candidates", ex(async (req, res) => {
6617
- const app = await this.getAutolaunchAppById(req.query.app)
6618
- if (!app) {
6619
- res.status(404).json({ ok: false, error: "App not found." })
6620
- return
6621
- }
6622
- const state = await this.buildAutolaunchCandidates(app)
6623
- res.json({ ok: true, ...state })
6418
+ this.autolaunch.registerRoutes()
6419
+ this.app.get("/pinokio/launch-requirements/:app", ex(async (req, res) => {
6420
+ const appId = this.kernel && typeof this.kernel.normalizeAppId === "function"
6421
+ ? this.kernel.normalizeAppId(req.params.app)
6422
+ : ""
6423
+ const status = this.kernel && typeof this.kernel.getLaunchRequirementsStatus === "function"
6424
+ ? this.kernel.getLaunchRequirementsStatus(appId || req.params.app)
6425
+ : null
6426
+ res.json({
6427
+ ok: true,
6428
+ status
6429
+ })
6624
6430
  }))
6625
- this.app.post("/autolaunch", ex(async (req, res) => {
6626
- const app = await this.getAutolaunchAppById(req.body && req.body.app)
6627
- if (!app) {
6628
- res.status(404).json({ ok: false, error: "App not found." })
6431
+ this.app.post("/pinokio/launch-requirements/:app/cancel", ex(async (req, res) => {
6432
+ const appId = this.kernel && typeof this.kernel.normalizeAppId === "function"
6433
+ ? this.kernel.normalizeAppId(req.params.app)
6434
+ : ""
6435
+ if (!appId) {
6436
+ res.status(400).json({ ok: false, error: "Invalid app." })
6629
6437
  return
6630
6438
  }
6631
- const requestedScript = typeof req.body.script === "string" ? req.body.script.trim() : ""
6632
- if (!requestedScript) {
6633
- const envInfo = await this.getAutolaunchEnvInfo(app)
6634
- if (envInfo.exists) {
6635
- await Util.update_env(envInfo.envPath, {
6636
- PINOKIO_SCRIPT_AUTOLAUNCH: ""
6439
+ const status = this.kernel && typeof this.kernel.getLaunchRequirementsStatus === "function"
6440
+ ? this.kernel.getLaunchRequirementsStatus(appId)
6441
+ : null
6442
+ let script = status && typeof status.script === "string" ? status.script.trim() : ""
6443
+ if (!script) {
6444
+ const appRoot = this.kernel.path("api", appId)
6445
+ const env = await Environment.get(appRoot, this.kernel).catch(() => ({}))
6446
+ script = Environment.getScriptAutolaunch(env)
6447
+ }
6448
+ const launchPath = script ? path.resolve(this.kernel.path("api", appId), script) : ""
6449
+ const cancelled = !!(launchPath && this.kernel.launchRequirements && typeof this.kernel.launchRequirements.cancel === "function" && this.kernel.launchRequirements.cancel(launchPath, { force: true }))
6450
+ const running = this.kernel && this.kernel.api && this.kernel.api.running ? this.kernel.api.running : {}
6451
+ const runningKeys = launchPath
6452
+ ? Object.keys(running).filter((key) => {
6453
+ const value = String(key || "")
6454
+ return value === launchPath || value.startsWith(`${launchPath}?`) || value.startsWith(`${launchPath}&`)
6637
6455
  })
6456
+ : []
6457
+ for (const key of runningKeys) {
6458
+ if (this.kernel && this.kernel.api && typeof this.kernel.api.stop === "function") {
6459
+ await this.kernel.api.stop({ params: { id: key } })
6638
6460
  }
6639
- res.json({
6640
- ok: true,
6641
- app: await this.buildAutolaunchAppState(app)
6642
- })
6643
- return
6644
6461
  }
6645
-
6646
- const envInfo = await this.getAutolaunchEnvInfo(app)
6647
- const resolved = await this.resolveAutolaunchScript(envInfo.appRoot, requestedScript)
6648
- if (!resolved) {
6649
- res.status(400).json({
6650
- ok: false,
6651
- error: "Select an existing local script inside the app."
6462
+ if ((cancelled || runningKeys.length > 0) && this.kernel && this.kernel.api && typeof this.kernel.api.ondata === "function") {
6463
+ this.kernel.api.ondata({
6464
+ id: launchPath,
6465
+ type: "launch.requirements.control",
6466
+ data: {
6467
+ action: "cancelled",
6468
+ app_id: appId
6469
+ }
6652
6470
  })
6653
- return
6654
6471
  }
6655
- const initialized = await Environment.init({ name: app.id }, this.kernel)
6656
- await Util.update_env(initialized.env_path, {
6657
- PINOKIO_SCRIPT_AUTOLAUNCH: resolved.script
6658
- })
6659
6472
  res.json({
6660
6473
  ok: true,
6661
- app: await this.buildAutolaunchAppState(app)
6662
- })
6663
- }))
6664
- this.app.post("/autolaunch/disable-all", ex(async (req, res) => {
6665
- const apps = await this.kernel.api.listApps()
6666
- let disabled = 0
6667
- for (const app of apps) {
6668
- const envInfo = await this.getAutolaunchEnvInfo(app)
6669
- if (envInfo.exists && envInfo.value) {
6670
- await Util.update_env(envInfo.envPath, {
6671
- PINOKIO_SCRIPT_AUTOLAUNCH: ""
6672
- })
6673
- disabled++
6674
- }
6675
- }
6676
- res.json({
6677
- ok: true,
6678
- disabled,
6679
- apps: await this.buildAutolaunchAppsState()
6680
- })
6681
- }))
6682
- this.app.get("/autolaunch", ex(async (req, res) => {
6683
- const peerAccess = await this.composePeerAccessPayload()
6684
- const list = this.getPeers()
6685
- const apps = await this.buildAutolaunchAppsState()
6686
- const appsJson = JSON.stringify(apps).replace(/</g, "\\u003c")
6687
- res.render("autolaunch", {
6688
- current_host: this.kernel.peer.host,
6689
- ...peerAccess,
6690
- apps,
6691
- appsJson,
6692
- enabledCount: apps.filter((app) => app.autolaunch_enabled).length,
6693
- portal: this.portal,
6694
- logo: this.logo,
6695
- theme: this.theme,
6696
- agent: req.agent,
6697
- list,
6474
+ cancelled,
6475
+ stopped: runningKeys.length > 0
6698
6476
  })
6699
6477
  }))
6700
6478
  this.app.get("/tools", ex(async (req, res) => {
@@ -7230,6 +7008,7 @@ class Server {
7230
7008
  res.json({ ok: false, error: err && err.message ? err.message : 'Clone failed' })
7231
7009
  return
7232
7010
  }
7011
+ await this.refreshTopLevelAppInventory()
7233
7012
  res.json({ ok: true, redirect: `/p/${encodeURIComponent(folder)}` })
7234
7013
  return
7235
7014
  }
@@ -7258,6 +7037,7 @@ class Server {
7258
7037
  if (this.kernel.git && this.kernel.git.activeSnapshot) {
7259
7038
  this.kernel.git.activeSnapshot[folder] = { id: snapshot.id, remoteKey }
7260
7039
  }
7040
+ await this.refreshTopLevelAppInventory()
7261
7041
  res.json({ ok: true, redirect: `/p/${encodeURIComponent(folder)}` })
7262
7042
  }))
7263
7043
  this.app.post("/checkpoints/delete", ex(async (req, res) => {
@@ -10929,6 +10709,9 @@ class Server {
10929
10709
  tool: selectedTool,
10930
10710
  uploadToken: ""
10931
10711
  })
10712
+ if (payload && payload.ok) {
10713
+ await this.refreshTopLevelAppInventory()
10714
+ }
10932
10715
  res.redirect(payload.url)
10933
10716
  } catch (error) {
10934
10717
  await renderTaskLaunchPage(req, res, task, {
@@ -11217,6 +11000,7 @@ class Server {
11217
11000
  return
11218
11001
  }
11219
11002
 
11003
+ await this.refreshTopLevelAppInventory()
11220
11004
  res.json({
11221
11005
  ok: true,
11222
11006
  folder: folderName,
@@ -11532,6 +11316,7 @@ class Server {
11532
11316
  })
11533
11317
 
11534
11318
  if (intent === "create_app") {
11319
+ await this.refreshTopLevelAppInventory()
11535
11320
  res.json({
11536
11321
  ok: true,
11537
11322
  url: `/initialize/${encodeURIComponent(requestedFolderName)}`
@@ -11581,6 +11366,10 @@ class Server {
11581
11366
  name: typeof body.name === "string" ? body.name : "",
11582
11367
  id: typeof body.id === "string" ? body.id : ""
11583
11368
  })
11369
+ const intent = typeof body.intent === "string" ? body.intent.trim().toLowerCase() : ""
11370
+ if (intent === "create_app") {
11371
+ await this.refreshTopLevelAppInventory()
11372
+ }
11584
11373
  res.json({
11585
11374
  ok: true,
11586
11375
  ...finalized
@@ -11602,6 +11391,9 @@ class Server {
11602
11391
  tool: body.tool,
11603
11392
  uploadToken: body.uploadToken
11604
11393
  })
11394
+ if (payload && payload.ok) {
11395
+ await this.refreshTopLevelAppInventory()
11396
+ }
11605
11397
  res.json(payload)
11606
11398
  } catch (error) {
11607
11399
  const status = Number.isInteger(error && error.status) ? error.status : 500
@@ -14614,16 +14406,10 @@ class Server {
14614
14406
  }))
14615
14407
  this.app.get("/gitdiff/:ref/*", ex(async (req, res) => {
14616
14408
  let fullpath = this.kernel.path("api", req.params[0])
14617
- let dir
14618
- let dirs = Array.from(this.kernel.git.dirs)
14619
- dirs.sort((x, y) => {
14620
- return y.length - x.length
14621
- })
14622
- for(let d of dirs) {
14623
- if (fullpath.startsWith(d)) {
14624
- dir = d
14625
- break
14626
- }
14409
+ let dir = await this.kernel.git.findRepoRootForPath(fullpath)
14410
+ if (!dir) {
14411
+ res.status(404).json({ error: "Repository not found" })
14412
+ return
14627
14413
  }
14628
14414
  let filepath = path.relative(dir, fullpath)
14629
14415
  let binary = false;
@@ -15686,7 +15472,7 @@ class Server {
15686
15472
  }))
15687
15473
  this.app.get("/info/api", ex(async (req,res) => {
15688
15474
  // api related info
15689
- let repo = this.kernel.git.find(req.query.git)
15475
+ let repo = await this.findApiRepoByGitUrl(req.query.git)
15690
15476
  if (repo) {
15691
15477
  let repos = await this.kernel.git.repos(repo.path)
15692
15478
  repos = repos.filter((r) => {
@@ -15876,6 +15662,7 @@ class Server {
15876
15662
  resolve()
15877
15663
  }, 2000)
15878
15664
  })
15665
+ await this.refreshTopLevelAppInventory()
15879
15666
  res.json({ success: true })
15880
15667
  }
15881
15668
  } catch(err) {
@@ -15974,6 +15761,43 @@ class Server {
15974
15761
  delete info.memory
15975
15762
  res.json(info)
15976
15763
  }))
15764
+ this.app.get("/pinokio/home_status", ex((req, res) => {
15765
+ const runningMap = this.kernel && this.kernel.api && this.kernel.api.running
15766
+ ? this.kernel.api.running
15767
+ : {}
15768
+ const apiRoot = this.kernel.path("api")
15769
+ const runningApps = new Set()
15770
+ const runningScripts = []
15771
+ for (const key of Object.keys(runningMap)) {
15772
+ if (typeof key !== "string" || !path.isAbsolute(key) || !this.is_subpath(apiRoot, key)) {
15773
+ continue
15774
+ }
15775
+ const relativePath = path.relative(apiRoot, key)
15776
+ if (!relativePath || relativePath.startsWith("..") || path.isAbsolute(relativePath)) {
15777
+ continue
15778
+ }
15779
+ const app = relativePath.split(path.sep)[0]
15780
+ if (!app) {
15781
+ continue
15782
+ }
15783
+ runningApps.add(app)
15784
+ runningScripts.push({
15785
+ id: key,
15786
+ app,
15787
+ path: relativePath,
15788
+ home_path: path.relative(this.kernel.homedir, key),
15789
+ script_path: path.relative(path.resolve(apiRoot, app), key),
15790
+ ...((this.kernel && typeof this.kernel.getScriptProgress === "function" && this.kernel.getScriptProgress(key)) || {})
15791
+ })
15792
+ }
15793
+ res.json({
15794
+ launch_complete: this.kernel.launch_complete,
15795
+ running_apps: Array.from(runningApps),
15796
+ running_scripts: runningScripts,
15797
+ autolaunch: this.kernel.autolaunch_status || { apps: {} },
15798
+ startup: this.getStartupStatus()
15799
+ })
15800
+ }))
15977
15801
  this.app.get("/pinokio/home", ex((req, res) => {
15978
15802
  res.json({
15979
15803
  path: this.kernel.homedir ? path.resolve(this.kernel.homedir) : null
@@ -16123,6 +15947,7 @@ class Server {
16123
15947
  */
16124
15948
 
16125
15949
  let formData = req.body
15950
+ let appInventoryChanged = false
16126
15951
  for(let key in req.files) {
16127
15952
  let file = req.files[key]
16128
15953
  formData[file.fieldname] = file.buffer
@@ -16137,6 +15962,7 @@ class Server {
16137
15962
  let new_path = this.kernel.path("api", formData.new_path)
16138
15963
 
16139
15964
  await fs.promises.cp(old_path, new_path, { recursive: true })
15965
+ appInventoryChanged = true
16140
15966
 
16141
15967
  // 2. edit meta in the new_path
16142
15968
  await this.kernel.api.updateMeta(formData, formData.new_path)
@@ -16149,6 +15975,7 @@ class Server {
16149
15975
  let old_path = this.kernel.path("api", formData.old_path)
16150
15976
  let new_path = this.kernel.path("api", formData.new_path)
16151
15977
  await fs.promises.rename(old_path, new_path)
15978
+ appInventoryChanged = true
16152
15979
  }
16153
15980
 
16154
15981
  // 2. edit meta in the new_path
@@ -16165,15 +15992,20 @@ class Server {
16165
15992
  let old_path = this.kernel.path("api", formData.old_path)
16166
15993
  let new_path = this.kernel.path("api", formData.new_path)
16167
15994
  await fs.promises.cp(old_path, new_path, { recursive: true })
15995
+ appInventoryChanged = true
16168
15996
  } else if (formData.move) {
16169
15997
  // 2. move only
16170
15998
  let old_path = this.kernel.path("api", formData.old_path)
16171
15999
  let new_path = this.kernel.path("api", formData.new_path)
16172
16000
  await fs.promises.rename(old_path, new_path)
16001
+ appInventoryChanged = true
16173
16002
  } else {
16174
16003
  // nothing
16175
16004
  }
16176
16005
  }
16006
+ if (appInventoryChanged) {
16007
+ await this.refreshTopLevelAppInventory()
16008
+ }
16177
16009
  res.json({
16178
16010
  success: true,
16179
16011
  reload: formData.new_path,
@@ -16403,6 +16235,7 @@ class Server {
16403
16235
  path: this.kernel.api.userdir
16404
16236
  }, (stream) => {
16405
16237
  })
16238
+ await this.refreshTopLevelAppInventory()
16406
16239
  res.json({
16407
16240
  result: "success"
16408
16241
  })