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
@@ -0,0 +1,231 @@
1
+ const path = require('path')
2
+
3
+ class ReadyState {
4
+ constructor(kernel) {
5
+ this.kernel = kernel
6
+ this.status = {
7
+ apps: {}
8
+ }
9
+ }
10
+ normalizeAppId(value) {
11
+ if (typeof value !== "string") {
12
+ return ""
13
+ }
14
+ const id = value.trim()
15
+ if (!id || id === "." || id === ".." || id.includes("\0") || /[\\/]/.test(id)) {
16
+ return ""
17
+ }
18
+ return id
19
+ }
20
+ getAppIdForLaunchPath(launchPath) {
21
+ if (!launchPath || typeof launchPath !== "string" || !path.isAbsolute(launchPath)) {
22
+ return ""
23
+ }
24
+ const apiRoot = this.kernel.path("api")
25
+ const relative = path.relative(apiRoot, launchPath)
26
+ if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
27
+ return ""
28
+ }
29
+ return this.normalizeAppId(relative.split(path.sep)[0])
30
+ }
31
+ getAppRelativeLaunchScript(appId, launchPath) {
32
+ const id = this.normalizeAppId(appId)
33
+ if (!id || !launchPath || typeof launchPath !== "string" || !path.isAbsolute(launchPath)) {
34
+ return ""
35
+ }
36
+ const appRoot = path.resolve(this.kernel.path("api"), id)
37
+ const relative = path.relative(appRoot, launchPath)
38
+ if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
39
+ return ""
40
+ }
41
+ return relative.split(path.sep).join("/")
42
+ }
43
+ resolveScriptPath(uri, cwd) {
44
+ if (typeof uri !== "string" || !uri.trim() || !this.kernel.api) {
45
+ return ""
46
+ }
47
+ try {
48
+ return this.kernel.api.filePath(uri.trim(), cwd)
49
+ } catch (e) {
50
+ return ""
51
+ }
52
+ }
53
+ isScriptReady(scriptPath) {
54
+ if (!scriptPath || typeof scriptPath !== "string") {
55
+ return false
56
+ }
57
+ const resolved = path.resolve(scriptPath)
58
+ const appId = this.getAppIdForLaunchPath(resolved)
59
+ if (!appId) {
60
+ return false
61
+ }
62
+ const status = this.status && this.status.apps
63
+ ? this.status.apps[appId]
64
+ : null
65
+ if (!status) {
66
+ return false
67
+ }
68
+ const script = this.getAppRelativeLaunchScript(appId, resolved)
69
+ const scriptStatus = status.scripts ? status.scripts[script] : null
70
+ if (scriptStatus) {
71
+ return scriptStatus.state === "ready"
72
+ }
73
+ return status.state === "ready" && (!status.script || status.script === script)
74
+ }
75
+ ready(requestOrUri) {
76
+ if (typeof requestOrUri === "string") {
77
+ return this.isScriptReady(this.resolveScriptPath(requestOrUri))
78
+ }
79
+ const params = requestOrUri && requestOrUri.params
80
+ const parent = requestOrUri && requestOrUri.parent && typeof requestOrUri.parent === "object"
81
+ ? requestOrUri.parent
82
+ : null
83
+ let uri = ""
84
+ if (typeof params === "string") {
85
+ uri = params
86
+ } else if (params && typeof params === "object") {
87
+ uri = params.uri || params.path || params.script || ""
88
+ } else if (parent && parent.path) {
89
+ uri = parent.path
90
+ }
91
+ return this.isScriptReady(this.resolveScriptPath(uri, requestOrUri && requestOrUri.cwd))
92
+ }
93
+ getScriptProgress(scriptPath) {
94
+ if (!scriptPath || typeof scriptPath !== "string") {
95
+ return null
96
+ }
97
+ const resolved = path.resolve(scriptPath)
98
+ const rpc = this.kernel.memory && this.kernel.memory.rpc
99
+ ? this.kernel.memory.rpc[resolved]
100
+ : null
101
+ if (!rpc || typeof rpc !== "object") {
102
+ return null
103
+ }
104
+ const current = Number(rpc.current)
105
+ const total = Number(rpc.total)
106
+ if (!Number.isInteger(current) || !Number.isInteger(total) || total <= 0) {
107
+ return null
108
+ }
109
+ return {
110
+ step_current: Math.max(1, current + 1),
111
+ step_total: total
112
+ }
113
+ }
114
+ stripProgress(record = {}) {
115
+ const next = { ...record }
116
+ delete next.step_current
117
+ delete next.step_total
118
+ return next
119
+ }
120
+ clearScriptProgress(launchPath) {
121
+ if (!launchPath || typeof launchPath !== "string") {
122
+ return
123
+ }
124
+ const resolved = path.resolve(launchPath)
125
+ const rpc = this.kernel.memory && this.kernel.memory.rpc
126
+ ? this.kernel.memory.rpc[resolved]
127
+ : null
128
+ if (rpc && typeof rpc === "object") {
129
+ delete rpc.current
130
+ delete rpc.total
131
+ }
132
+ }
133
+ writeScriptProgress(launchPath, current, total) {
134
+ if (!launchPath || typeof launchPath !== "string") {
135
+ return
136
+ }
137
+ if (!this.kernel.memory) {
138
+ this.kernel.memory = {}
139
+ }
140
+ if (!this.kernel.memory.rpc) {
141
+ this.kernel.memory.rpc = {}
142
+ }
143
+ const resolved = path.resolve(launchPath)
144
+ this.kernel.memory.rpc[resolved] = {
145
+ ...(this.kernel.memory.rpc[resolved] || {}),
146
+ current,
147
+ total
148
+ }
149
+ }
150
+ updateLaunchStatus(launchPath, state, details) {
151
+ const appId = this.getAppIdForLaunchPath(launchPath)
152
+ if (!appId) {
153
+ return
154
+ }
155
+ const script = this.getAppRelativeLaunchScript(appId, launchPath)
156
+ const current = this.status.apps[appId] || { id: appId, scripts: {} }
157
+ const scripts = { ...(current.scripts || {}) }
158
+ const hasProgress = Object.prototype.hasOwnProperty.call(details || {}, "step_current")
159
+ || Object.prototype.hasOwnProperty.call(details || {}, "step_total")
160
+ if (script) {
161
+ scripts[script] = {
162
+ ...(hasProgress ? (scripts[script] || { script }) : this.stripProgress(scripts[script] || { script })),
163
+ script,
164
+ state,
165
+ ...details
166
+ }
167
+ }
168
+ this.status.apps[appId] = {
169
+ ...(hasProgress ? current : this.stripProgress(current)),
170
+ id: appId,
171
+ state,
172
+ script: script || current.script,
173
+ ...details,
174
+ scripts
175
+ }
176
+ }
177
+ markStarted(launchPath) {
178
+ this.clearScriptProgress(launchPath)
179
+ this.updateLaunchStatus(launchPath, "starting", {
180
+ started_at: Date.now()
181
+ })
182
+ }
183
+ markProgress(launchPath, current, total) {
184
+ const stepCurrent = Number(current)
185
+ const stepTotal = Number(total)
186
+ if (!Number.isInteger(stepCurrent) || !Number.isInteger(stepTotal) || stepTotal <= 0) {
187
+ return null
188
+ }
189
+ const progress = {
190
+ step_current: Math.max(1, stepCurrent + 1),
191
+ step_total: stepTotal
192
+ }
193
+ this.writeScriptProgress(launchPath, stepCurrent, stepTotal)
194
+ this.updateLaunchStatus(launchPath, "starting", progress)
195
+ return progress
196
+ }
197
+ markReady(launchPath) {
198
+ this.clearScriptProgress(launchPath)
199
+ this.updateLaunchStatus(launchPath, "ready", {
200
+ ready_at: Date.now()
201
+ })
202
+ }
203
+ markFailed(launchPath, error) {
204
+ this.clearScriptProgress(launchPath)
205
+ this.updateLaunchStatus(launchPath, "failed", {
206
+ error: error && error.message ? error.message : String(error || "Launch failed"),
207
+ failed_at: Date.now()
208
+ })
209
+ }
210
+ markStopped(launchPath) {
211
+ this.clearScriptProgress(launchPath)
212
+ this.updateLaunchStatus(launchPath, "stopped", {
213
+ stopped_at: Date.now()
214
+ })
215
+ }
216
+ isAppReady(appId) {
217
+ const dependencyId = this.normalizeAppId(appId)
218
+ const status = this.status && this.status.apps
219
+ ? this.status.apps[dependencyId]
220
+ : null
221
+ if (!status) {
222
+ return false
223
+ }
224
+ if (status.scripts && Object.values(status.scripts).some((script) => script && script.state === "ready")) {
225
+ return true
226
+ }
227
+ return status.state === "ready"
228
+ }
229
+ }
230
+
231
+ module.exports = ReadyState
package/kernel/script.js CHANGED
@@ -39,6 +39,22 @@ class Script {
39
39
  return false
40
40
  }
41
41
  }
42
+ // script.ready('https://github.com/cocktailpeanutlabs/comfyui.git/start.json')
43
+ // script.ready('~/api/comfyui.git/start.json')
44
+ // script.ready(cwd, 'start.json')
45
+ ready(...chunks) {
46
+ let id
47
+ try {
48
+ id = this.resolve(...chunks)
49
+ } catch (e) {
50
+ return false
51
+ }
52
+ if (id) {
53
+ return this.kernel.isScriptReady(id)
54
+ } else {
55
+ return false
56
+ }
57
+ }
42
58
  // script.local('https://github.com/cocktailpeanutlabs/comfyui.git/start.json')
43
59
  // script.local('~/api/comfyui.git/start.json')
44
60
  // script.local(cwd, 'start.json')
package/kernel/shells.js CHANGED
@@ -235,7 +235,15 @@ class Shells {
235
235
  if (segments.length > 0) {
236
236
  workspaceName = segments[0]
237
237
  workspaceRoot = this.kernel.path("api", workspaceName)
238
- beforeDirs = new Set(this.kernel.git.dirs)
238
+ beforeDirs = new Set()
239
+ try {
240
+ const beforeRepos = await this.kernel.git.repos(workspaceRoot)
241
+ for (const repo of beforeRepos) {
242
+ if (repo && repo.gitParentPath) {
243
+ beforeDirs.add(repo.gitParentPath)
244
+ }
245
+ }
246
+ } catch (_) {}
239
247
  }
240
248
  }
241
249
  }
@@ -11,6 +11,7 @@ class WorkspaceStatusManager {
11
11
  this.watchers = new Map()
12
12
  this.onEvent = typeof options.onEvent === 'function' ? options.onEvent : null
13
13
  this.gitIgnoreEngines = new Map()
14
+ this.gitIgnoreScanSkipDirs = new Set(['.git', 'node_modules', 'venv', '.venv'])
14
15
  this.defaultIgnores = options.ignores && Array.isArray(options.ignores) ? options.ignores : [
15
16
  '**/.git/**',
16
17
  '**/node_modules/**',
@@ -50,7 +51,7 @@ class WorkspaceStatusManager {
50
51
  }
51
52
  for (const entry of entries) {
52
53
  if (entry.isDirectory()) {
53
- if (entry.name === '.git' || entry.name === 'node_modules' || entry.name === 'venv' || entry.name === '.venv') {
54
+ if (this.gitIgnoreScanSkipDirs.has(entry.name)) {
54
55
  continue
55
56
  }
56
57
  await walk(path.join(dir, entry.name))
@@ -73,34 +74,119 @@ class WorkspaceStatusManager {
73
74
  } catch (_) {
74
75
  continue
75
76
  }
76
- const relDir = path.relative(workspaceRoot, path.dirname(gitignorePath))
77
- const prefix = relDir && relDir !== '.' ? relDir.split(path.sep).join('/') + '/' : ''
78
- const lines = content.split(/\r?\n/)
79
- for (let line of lines) {
80
- if (!line) continue
81
- line = line.trim()
82
- if (!line || line.startsWith('#')) continue
83
- let negated = false
84
- if (line.startsWith('!')) {
85
- negated = true
86
- line = line.slice(1)
87
- }
88
- if (!line) continue
89
- line = line.replace(/^\/+/, '')
90
- if (!line) continue
91
- const pattern = prefix + line
92
- if (!pattern) continue
93
- if (negated) {
94
- ig.add('!' + pattern)
95
- } else {
96
- ig.add(pattern)
97
- }
98
- }
77
+ this.addGitIgnoreContent(ig, workspaceRoot, gitignorePath, content)
99
78
  }
100
79
 
101
80
  this.gitIgnoreEngines.set(workspaceName, ig)
102
81
  }
103
82
 
83
+ normalizeGitIgnorePath(value) {
84
+ return String(value || '').replace(/\\/g, '/').replace(/\/+/g, '/')
85
+ }
86
+
87
+ addGitIgnoreContent(engine, workspaceRoot, gitignorePath, content) {
88
+ const relDir = path.relative(workspaceRoot, path.dirname(gitignorePath))
89
+ const prefix = relDir && relDir !== '.' ? this.normalizeGitIgnorePath(relDir) + '/' : ''
90
+ const lines = String(content || '').split(/\r?\n/)
91
+ for (let line of lines) {
92
+ if (!line) continue
93
+ line = line.trim()
94
+ if (!line || line.startsWith('#')) continue
95
+ let negated = false
96
+ if (line.startsWith('!')) {
97
+ negated = true
98
+ line = line.slice(1)
99
+ }
100
+ if (!line) continue
101
+ line = line.replace(/^\/+/, '')
102
+ if (!line) continue
103
+ const pattern = prefix + line
104
+ if (!pattern) continue
105
+ engine.add((negated ? '!' : '') + pattern)
106
+ }
107
+ }
108
+
109
+ createPathScopedGitIgnoreContext(workspaceRoot) {
110
+ return {
111
+ workspaceRoot,
112
+ contentCache: new Map(),
113
+ engineCache: new Map(),
114
+ }
115
+ }
116
+
117
+ async readPathScopedGitIgnore(context, gitignorePath) {
118
+ if (context.contentCache.has(gitignorePath)) {
119
+ return context.contentCache.get(gitignorePath)
120
+ }
121
+ try {
122
+ const content = await fs.promises.readFile(gitignorePath, 'utf8')
123
+ context.contentCache.set(gitignorePath, content)
124
+ return content
125
+ } catch (_) {
126
+ context.contentCache.set(gitignorePath, null)
127
+ return null
128
+ }
129
+ }
130
+
131
+ pathScopedGitIgnoreDirs(workspaceRelativePath) {
132
+ const normalized = this.normalizeGitIgnorePath(workspaceRelativePath)
133
+ const parts = normalized.split('/').filter(Boolean)
134
+ const dirs = []
135
+ for (let depth = 0; depth < parts.length; depth++) {
136
+ dirs.push(depth === 0 ? '' : parts.slice(0, depth).join('/'))
137
+ if (this.gitIgnoreScanSkipDirs.has(parts[depth])) {
138
+ break
139
+ }
140
+ }
141
+ return dirs
142
+ }
143
+
144
+ async pathScopedGitIgnoreEngine(context, workspaceRelativePath) {
145
+ if (!context || !context.workspaceRoot) {
146
+ return null
147
+ }
148
+ const dirs = this.pathScopedGitIgnoreDirs(workspaceRelativePath)
149
+ const cacheKey = dirs.join('\0')
150
+ if (context.engineCache.has(cacheKey)) {
151
+ return context.engineCache.get(cacheKey)
152
+ }
153
+ const engine = ignore()
154
+ for (const relDir of dirs) {
155
+ const gitignorePath = path.join(context.workspaceRoot, relDir, '.gitignore')
156
+ const content = await this.readPathScopedGitIgnore(context, gitignorePath)
157
+ if (content) {
158
+ this.addGitIgnoreContent(engine, context.workspaceRoot, gitignorePath, content)
159
+ }
160
+ }
161
+ context.engineCache.set(cacheKey, engine)
162
+ return engine
163
+ }
164
+
165
+ async isPathScopedGitIgnored(context, workspaceRelativePath) {
166
+ const normalized = this.normalizeGitIgnorePath(workspaceRelativePath)
167
+ if (!normalized) {
168
+ return false
169
+ }
170
+ const engine = await this.pathScopedGitIgnoreEngine(context, normalized)
171
+ return !!(engine && engine.ignores(normalized))
172
+ }
173
+
174
+ async filterPathScopedGitIgnored(contextOrWorkspaceRoot, records) {
175
+ const context = typeof contextOrWorkspaceRoot === 'string'
176
+ ? this.createPathScopedGitIgnoreContext(contextOrWorkspaceRoot)
177
+ : contextOrWorkspaceRoot
178
+ const ignored = new Set()
179
+ for (const record of records || []) {
180
+ if (!record || !record.key || !record.workspaceRelative) {
181
+ continue
182
+ }
183
+ if (await this.isPathScopedGitIgnored(context, record.workspaceRelative)) {
184
+ ignored.add(record.key)
185
+ }
186
+ }
187
+ return ignored
188
+ }
189
+
104
190
  markDirty(workspaceName) {
105
191
  let entry = this.cache.get(workspaceName)
106
192
  if (!entry) {
@@ -119,7 +205,6 @@ class WorkspaceStatusManager {
119
205
  return
120
206
  }
121
207
  try {
122
- await this.ensureGitIgnoreEngine(workspaceName, workspaceRoot)
123
208
  const subscription = await ParcelWatcher.subscribe(
124
209
  workspaceRoot,
125
210
  (error, events) => {
@@ -130,29 +215,10 @@ class WorkspaceStatusManager {
130
215
  if (!events || events.length === 0) {
131
216
  return
132
217
  }
133
- let relevantEvents = events
134
- const engine = this.gitIgnoreEngines.get(workspaceName)
135
- if (engine && workspaceRoot) {
136
- const root = workspaceRoot
137
- relevantEvents = events.filter((evt) => {
138
- if (!evt || !evt.path) {
139
- return true
140
- }
141
- let rel = path.relative(root, evt.path)
142
- if (!rel || rel.startsWith('..') || path.isAbsolute(rel)) {
143
- return true
144
- }
145
- const normalized = rel.split(path.sep).join('/')
146
- return !engine.ignores(normalized)
147
- })
148
- }
149
- if (relevantEvents.length === 0) {
150
- return
151
- }
152
218
  this.markDirty(workspaceName)
153
219
  if (this.onEvent) {
154
220
  try {
155
- this.onEvent(workspaceName, relevantEvents)
221
+ this.onEvent(workspaceName, events)
156
222
  } catch (err) {
157
223
  console.warn('workspace watcher callback error', workspaceName, err && err.message ? err.message : err)
158
224
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pinokiod",
3
- "version": "7.3.15",
3
+ "version": "7.4.0",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {