pinokiod 7.2.18 → 7.3.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/Dockerfile +2 -0
- package/kernel/api/index.js +13 -179
- package/kernel/api/process/index.js +44 -99
- package/kernel/bin/conda-pins.js +53 -0
- package/kernel/bin/conda.js +35 -6
- package/kernel/bin/huggingface.js +1 -1
- package/kernel/bin/index.js +15 -2
- package/kernel/environment.js +11 -205
- package/kernel/git.js +13 -0
- package/kernel/index.js +1 -64
- package/kernel/plugin.js +58 -6
- package/kernel/prototype.js +0 -4
- package/kernel/shell.js +2 -23
- package/kernel/util.js +0 -60
- package/package.json +1 -1
- package/server/index.js +171 -229
- package/server/lib/content_validation.js +33 -47
- package/server/public/common.js +29 -103
- package/server/public/create-launcher.js +31 -4
- package/server/public/electron.css +6 -0
- package/server/public/style.css +0 -337
- package/server/public/task-launcher.css +3 -11
- package/server/public/task-launcher.js +32 -5
- package/server/public/universal-launcher.js +26 -3
- package/server/socket.js +11 -22
- package/server/views/app.ejs +30 -167
- package/server/views/d.ejs +35 -33
- package/server/views/editor.ejs +4 -25
- package/server/views/partials/main_sidebar.ejs +0 -1
- package/server/views/partials/menu.ejs +1 -1
- package/server/views/pre.ejs +1 -1
- package/server/views/shell.ejs +3 -11
- package/server/views/task_launch.ejs +10 -10
- package/server/views/terminal.ejs +5 -34
- package/spec/INSTRUCTION_SYNC.md +5 -5
- package/kernel/agent_instructions.js +0 -166
- package/kernel/api/shell_run_template.js +0 -273
- package/kernel/api/uri/index.js +0 -51
- package/kernel/plugin_sources.js +0 -289
- package/kernel/watch/context.js +0 -42
- package/kernel/watch/drivers/fs.js +0 -71
- package/kernel/watch/drivers/poll.js +0 -33
- package/kernel/watch/index.js +0 -185
- package/server/features/index.js +0 -13
- package/server/features/notes/index.js +0 -41
- package/server/features/notes/parser.js +0 -174
- package/server/features/notes/public/notes.css +0 -955
- package/server/features/notes/public/notes.js +0 -1149
- package/server/features/notes/registry_import.js +0 -412
- package/server/features/notes/routes.js +0 -156
- package/server/features/notes/service.js +0 -326
- package/server/features/notes/watcher.js +0 -74
- package/server/lib/workspace_catalog.js +0 -151
- package/server/lib/workspace_runtime.js +0 -390
- package/server/public/tasker.css +0 -336
- package/server/public/tasker.js +0 -407
- package/server/routes/workspaces.js +0 -44
- package/server/views/partials/workspace_row.ejs +0 -61
- package/server/views/tasker.ejs +0 -40
- package/server/views/workspaces.ejs +0 -813
- package/system/plugin/antigravity/antigravity.png +0 -0
- package/system/plugin/antigravity/pinokio.js +0 -35
- package/system/plugin/claude/claude.png +0 -0
- package/system/plugin/claude/pinokio.js +0 -61
- package/system/plugin/claude-auto/claude.png +0 -0
- package/system/plugin/claude-auto/pinokio.js +0 -72
- package/system/plugin/claude-desktop/icon.jpeg +0 -0
- package/system/plugin/claude-desktop/pinokio.js +0 -37
- package/system/plugin/codex/openai.webp +0 -0
- package/system/plugin/codex/pinokio.js +0 -56
- package/system/plugin/codex-auto/openai.webp +0 -0
- package/system/plugin/codex-auto/pinokio.js +0 -63
- package/system/plugin/codex-desktop/icon.png +0 -0
- package/system/plugin/codex-desktop/pinokio.js +0 -37
- package/system/plugin/crush/crush.png +0 -0
- package/system/plugin/crush/pinokio.js +0 -29
- package/system/plugin/cursor/cursor.jpeg +0 -0
- package/system/plugin/cursor/pinokio.js +0 -37
- package/system/plugin/gemini/gemini.jpeg +0 -0
- package/system/plugin/gemini/pinokio.js +0 -38
- package/system/plugin/gemini-auto/gemini.jpeg +0 -0
- package/system/plugin/gemini-auto/pinokio.js +0 -41
- package/system/plugin/qwen/pinokio.js +0 -48
- package/system/plugin/qwen/qwen.png +0 -0
- package/system/plugin/vscode/pinokio.js +0 -34
- package/system/plugin/vscode/vscode.png +0 -0
- package/system/plugin/windsurf/pinokio.js +0 -37
- package/system/plugin/windsurf/windsurf.png +0 -0
- package/test/plugin-sources.test.js +0 -45
|
@@ -1,326 +0,0 @@
|
|
|
1
|
-
const fs = require("fs")
|
|
2
|
-
const path = require("path")
|
|
3
|
-
const crypto = require("crypto")
|
|
4
|
-
const {
|
|
5
|
-
RESULT_RELATIVE_DIR,
|
|
6
|
-
NOTE_FILENAME,
|
|
7
|
-
METADATA_FILENAME,
|
|
8
|
-
buildExcerpt,
|
|
9
|
-
describeMediaRefs,
|
|
10
|
-
extractTitle,
|
|
11
|
-
parseNoteMetadata
|
|
12
|
-
} = require("./parser")
|
|
13
|
-
|
|
14
|
-
const MAX_MARKDOWN_BYTES = 5 * 1024 * 1024
|
|
15
|
-
|
|
16
|
-
function createHash(value) {
|
|
17
|
-
return crypto.createHash("sha256").update(String(value)).digest("hex").slice(0, 24)
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function clonePlainObject(value) {
|
|
21
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
22
|
-
return null
|
|
23
|
-
}
|
|
24
|
-
try {
|
|
25
|
-
return JSON.parse(JSON.stringify(value))
|
|
26
|
-
} catch (_) {
|
|
27
|
-
return null
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function normalizeRelativePath(value, fallback) {
|
|
32
|
-
const raw = String(value || fallback || "").trim().replace(/\\/g, "/")
|
|
33
|
-
if (raw.startsWith("/") || /^[a-zA-Z]:/.test(raw)) {
|
|
34
|
-
return fallback
|
|
35
|
-
}
|
|
36
|
-
const normalized = path.posix.normalize(raw)
|
|
37
|
-
if (!normalized || normalized === "." || normalized === ".." || normalized.startsWith("../") || normalized.includes("/../")) {
|
|
38
|
-
return fallback
|
|
39
|
-
}
|
|
40
|
-
return normalized
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function isInside(candidate, parent) {
|
|
44
|
-
const relative = path.relative(path.resolve(parent), path.resolve(candidate))
|
|
45
|
-
return relative === "" || (!!relative && !relative.startsWith("..") && !path.isAbsolute(relative))
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function normalizeNoteConfig(config = {}) {
|
|
49
|
-
const params = config && typeof config === "object" ? config : {}
|
|
50
|
-
return {
|
|
51
|
-
path: normalizeRelativePath(params.path, RESULT_RELATIVE_DIR),
|
|
52
|
-
content: normalizeRelativePath(params.content, ""),
|
|
53
|
-
ready: normalizeRelativePath(params.ready, METADATA_FILENAME),
|
|
54
|
-
description: typeof params.description === "string" ? params.description.trim() : "",
|
|
55
|
-
publish: clonePlainObject(params.publish)
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
async function findMetadataFiles(rootDir, filename) {
|
|
60
|
-
const root = path.resolve(rootDir)
|
|
61
|
-
const stats = await fs.promises.stat(root).catch(() => null)
|
|
62
|
-
if (!stats || !stats.isDirectory()) {
|
|
63
|
-
return []
|
|
64
|
-
}
|
|
65
|
-
const target = String(filename || METADATA_FILENAME)
|
|
66
|
-
const results = []
|
|
67
|
-
const visit = async (dir) => {
|
|
68
|
-
const entries = await fs.promises.readdir(dir, { withFileTypes: true }).catch(() => null)
|
|
69
|
-
if (!entries) return
|
|
70
|
-
for (const entry of entries) {
|
|
71
|
-
const fullPath = path.resolve(dir, entry.name)
|
|
72
|
-
if (entry.isDirectory()) {
|
|
73
|
-
await visit(fullPath)
|
|
74
|
-
} else if (entry.isFile() && entry.name === target) {
|
|
75
|
-
results.push(fullPath)
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
await visit(root)
|
|
80
|
-
return results
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function createNoteService({ kernel, taskWorkspaceLinks } = {}) {
|
|
84
|
-
if (!kernel) {
|
|
85
|
-
throw new Error("kernel is required")
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const resultsByBundle = new Map()
|
|
89
|
-
|
|
90
|
-
async function readMarkdown(notePath, stats) {
|
|
91
|
-
const noteStats = stats || await fs.promises.stat(notePath)
|
|
92
|
-
if (noteStats.size > MAX_MARKDOWN_BYTES) {
|
|
93
|
-
throw new Error(`note markdown is too large (${noteStats.size} bytes)`)
|
|
94
|
-
}
|
|
95
|
-
return fs.promises.readFile(notePath, "utf8")
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
async function readNoteMetadata(metadataPath) {
|
|
99
|
-
if (path.extname(metadataPath).toLowerCase() !== ".json") {
|
|
100
|
-
return {}
|
|
101
|
-
}
|
|
102
|
-
const raw = await fs.promises.readFile(metadataPath, "utf8")
|
|
103
|
-
return parseNoteMetadata(raw)
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
async function inspectNoteBundle({ taskId, ref, cwd, note, bundlePath, metadataPath } = {}) {
|
|
107
|
-
if (typeof cwd !== "string" || !cwd.trim()) {
|
|
108
|
-
return null
|
|
109
|
-
}
|
|
110
|
-
const workspacePath = path.resolve(cwd.trim())
|
|
111
|
-
const noteConfig = normalizeNoteConfig(note)
|
|
112
|
-
const rootDir = path.resolve(workspacePath, noteConfig.path)
|
|
113
|
-
const resultDir = path.resolve(bundlePath || rootDir)
|
|
114
|
-
const readyPath = path.resolve(metadataPath || path.resolve(resultDir, noteConfig.ready))
|
|
115
|
-
const readyStats = await fs.promises.stat(readyPath).catch(() => null)
|
|
116
|
-
if (!readyStats || !readyStats.isFile()) {
|
|
117
|
-
resultsByBundle.delete(resultDir)
|
|
118
|
-
return null
|
|
119
|
-
}
|
|
120
|
-
let metadata = {}
|
|
121
|
-
try {
|
|
122
|
-
metadata = await readNoteMetadata(readyPath)
|
|
123
|
-
} catch (_) {
|
|
124
|
-
resultsByBundle.delete(resultDir)
|
|
125
|
-
return null
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const contentPath = normalizeRelativePath(metadata.content || noteConfig.content, NOTE_FILENAME)
|
|
129
|
-
const notePath = path.resolve(resultDir, contentPath)
|
|
130
|
-
const noteStats = await fs.promises.stat(notePath).catch(() => null)
|
|
131
|
-
if (!noteStats || !noteStats.isFile()) {
|
|
132
|
-
resultsByBundle.delete(resultDir)
|
|
133
|
-
return null
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
const markdown = await readMarkdown(notePath, noteStats)
|
|
137
|
-
const workspaceName = path.basename(workspacePath)
|
|
138
|
-
const bundleName = path.basename(resultDir)
|
|
139
|
-
const media = await describeMediaRefs(markdown, resultDir)
|
|
140
|
-
const updatedAtMs = Math.max(readyStats.mtimeMs || 0, noteStats.mtimeMs || 0)
|
|
141
|
-
const id = createHash(`${workspacePath}|${resultDir}|${notePath}|${readyPath}`)
|
|
142
|
-
const previous = resultsByBundle.get(resultDir) || null
|
|
143
|
-
const publish = noteConfig.publish || (previous && previous.publish) || null
|
|
144
|
-
const description = noteConfig.description || (previous && previous.description) || ""
|
|
145
|
-
const mediaRevision = media
|
|
146
|
-
.map((item) => `${item.ref}:${item.exists ? item.bytes : "missing"}:${item.mtimeMs || 0}`)
|
|
147
|
-
.join("|")
|
|
148
|
-
const revision = createHash(`${noteStats.size}|${noteStats.mtimeMs}|${readyStats.size}|${readyStats.mtimeMs}|${mediaRevision}`)
|
|
149
|
-
const result = {
|
|
150
|
-
id,
|
|
151
|
-
revision,
|
|
152
|
-
taskId,
|
|
153
|
-
ref,
|
|
154
|
-
cwd: workspacePath,
|
|
155
|
-
workspaceName,
|
|
156
|
-
bundleName,
|
|
157
|
-
title: metadata.title || extractTitle(markdown, bundleName || workspaceName),
|
|
158
|
-
markdown,
|
|
159
|
-
excerpt: buildExcerpt(markdown),
|
|
160
|
-
resultDir,
|
|
161
|
-
watchRoot: rootDir,
|
|
162
|
-
notePath,
|
|
163
|
-
contentPath: notePath,
|
|
164
|
-
readyPath,
|
|
165
|
-
metadataPath: readyPath,
|
|
166
|
-
metadata,
|
|
167
|
-
publish,
|
|
168
|
-
description,
|
|
169
|
-
noteBytes: noteStats.size,
|
|
170
|
-
media: media.map((item, index) => ({
|
|
171
|
-
index,
|
|
172
|
-
ref: item.ref,
|
|
173
|
-
path: item.path,
|
|
174
|
-
bytes: item.bytes,
|
|
175
|
-
mtimeMs: item.mtimeMs,
|
|
176
|
-
exists: item.exists,
|
|
177
|
-
ext: path.extname(item.ref || "").toLowerCase()
|
|
178
|
-
})),
|
|
179
|
-
mediaCount: media.length,
|
|
180
|
-
missingMediaCount: media.filter((item) => !item.exists).length,
|
|
181
|
-
mediaBytes: media.reduce((total, item) => total + (Number.isFinite(item.bytes) ? item.bytes : 0), 0),
|
|
182
|
-
updatedAt: new Date(updatedAtMs || Date.now()).toISOString()
|
|
183
|
-
}
|
|
184
|
-
resultsByBundle.set(resultDir, result)
|
|
185
|
-
return result
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
async function inspectWorkspace({ taskId, ref, cwd, note } = {}) {
|
|
189
|
-
if (typeof cwd !== "string" || !cwd.trim()) {
|
|
190
|
-
return null
|
|
191
|
-
}
|
|
192
|
-
const workspacePath = path.resolve(cwd.trim())
|
|
193
|
-
const noteConfig = normalizeNoteConfig(note)
|
|
194
|
-
const rootDir = path.resolve(workspacePath, noteConfig.path)
|
|
195
|
-
const metadataFiles = await findMetadataFiles(rootDir, noteConfig.ready)
|
|
196
|
-
const seen = new Set()
|
|
197
|
-
const results = []
|
|
198
|
-
for (const metadataPath of metadataFiles) {
|
|
199
|
-
const bundlePath = path.dirname(metadataPath)
|
|
200
|
-
const result = await inspectNoteBundle({
|
|
201
|
-
taskId,
|
|
202
|
-
ref,
|
|
203
|
-
cwd: workspacePath,
|
|
204
|
-
note: noteConfig,
|
|
205
|
-
bundlePath,
|
|
206
|
-
metadataPath
|
|
207
|
-
})
|
|
208
|
-
if (result) {
|
|
209
|
-
seen.add(result.resultDir)
|
|
210
|
-
results.push(result)
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
for (const [bundlePath, result] of Array.from(resultsByBundle.entries())) {
|
|
214
|
-
if (result && result.cwd === workspacePath && result.watchRoot === rootDir && !seen.has(bundlePath)) {
|
|
215
|
-
resultsByBundle.delete(bundlePath)
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
return results[0] || null
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
async function trackWorkspace({ taskId, ref, cwd } = {}) {
|
|
222
|
-
let resolvedCwd = cwd
|
|
223
|
-
if (!resolvedCwd && ref && taskWorkspaceLinks && typeof taskWorkspaceLinks.resolveWorkspaceRef === "function") {
|
|
224
|
-
resolvedCwd = taskWorkspaceLinks.resolveWorkspaceRef(ref)
|
|
225
|
-
}
|
|
226
|
-
if (!resolvedCwd) {
|
|
227
|
-
return null
|
|
228
|
-
}
|
|
229
|
-
return inspectWorkspace({ taskId, ref, cwd: resolvedCwd })
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
async function listPending(options = {}) {
|
|
233
|
-
const filterCwd = typeof options.cwd === "string" && options.cwd.trim()
|
|
234
|
-
? path.resolve(options.cwd.trim())
|
|
235
|
-
: ""
|
|
236
|
-
return Array.from(resultsByBundle.values())
|
|
237
|
-
.filter((result) => !filterCwd || result.cwd === filterCwd)
|
|
238
|
-
.sort((a, b) => String(b.updatedAt).localeCompare(String(a.updatedAt)))
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
async function getPendingById(id) {
|
|
242
|
-
const normalizedId = typeof id === "string" ? id.trim() : ""
|
|
243
|
-
if (!normalizedId) {
|
|
244
|
-
return null
|
|
245
|
-
}
|
|
246
|
-
return Array.from(resultsByBundle.values()).find((item) => item.id === normalizedId) || null
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
async function refreshPendingItem(item) {
|
|
250
|
-
if (!item || !item.cwd || !item.resultDir || !item.metadataPath) {
|
|
251
|
-
return null
|
|
252
|
-
}
|
|
253
|
-
return inspectNoteBundle({
|
|
254
|
-
taskId: item.taskId,
|
|
255
|
-
ref: item.ref,
|
|
256
|
-
cwd: item.cwd,
|
|
257
|
-
note: {
|
|
258
|
-
description: item.description,
|
|
259
|
-
publish: item.publish
|
|
260
|
-
},
|
|
261
|
-
bundlePath: item.resultDir,
|
|
262
|
-
metadataPath: item.metadataPath
|
|
263
|
-
})
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
async function savePendingById(id, options = {}) {
|
|
267
|
-
const item = await getPendingById(id)
|
|
268
|
-
if (!item) {
|
|
269
|
-
return null
|
|
270
|
-
}
|
|
271
|
-
const markdown = typeof options.markdown === "string" ? options.markdown : null
|
|
272
|
-
if (markdown === null) {
|
|
273
|
-
throw new Error("markdown is required")
|
|
274
|
-
}
|
|
275
|
-
if (Buffer.byteLength(markdown, "utf8") > MAX_MARKDOWN_BYTES) {
|
|
276
|
-
const error = new Error(`note markdown is too large; limit is ${MAX_MARKDOWN_BYTES} bytes`)
|
|
277
|
-
error.code = "NOTE_TOO_LARGE"
|
|
278
|
-
throw error
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
const current = await refreshPendingItem(item)
|
|
282
|
-
if (!current) {
|
|
283
|
-
return null
|
|
284
|
-
}
|
|
285
|
-
const expectedRevision = typeof options.revision === "string" ? options.revision.trim() : ""
|
|
286
|
-
if (expectedRevision && current.revision !== expectedRevision) {
|
|
287
|
-
const error = new Error("Note changed on disk. Reload it before saving.")
|
|
288
|
-
error.code = "NOTE_CONFLICT"
|
|
289
|
-
error.item = current
|
|
290
|
-
throw error
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
const notePath = path.resolve(current.notePath)
|
|
294
|
-
const resultDir = path.resolve(current.resultDir)
|
|
295
|
-
if (!isInside(notePath, resultDir)) {
|
|
296
|
-
const error = new Error("Note path is outside the note folder")
|
|
297
|
-
error.code = "NOTE_INVALID_PATH"
|
|
298
|
-
throw error
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
await fs.promises.writeFile(notePath, markdown, "utf8")
|
|
302
|
-
return refreshPendingItem(current)
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
async function start() {
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
async function stop() {
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
return {
|
|
312
|
-
RESULT_RELATIVE_DIR,
|
|
313
|
-
getPendingById,
|
|
314
|
-
inspectNoteBundle,
|
|
315
|
-
inspectWorkspace,
|
|
316
|
-
listPending,
|
|
317
|
-
savePendingById,
|
|
318
|
-
start,
|
|
319
|
-
stop,
|
|
320
|
-
trackWorkspace
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
module.exports = {
|
|
325
|
-
createNoteService
|
|
326
|
-
}
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
const path = require("path")
|
|
2
|
-
|
|
3
|
-
function isInside(candidate, parent) {
|
|
4
|
-
const relative = path.relative(parent, candidate)
|
|
5
|
-
return relative === "" || (!!relative && !relative.startsWith("..") && !path.isAbsolute(relative))
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
class NoteWatcher {
|
|
9
|
-
constructor(options = {}) {
|
|
10
|
-
this.notes = options.notes
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
async watch(ctx, params = {}) {
|
|
14
|
-
if (!this.notes || typeof this.notes.inspectWorkspace !== "function") {
|
|
15
|
-
throw new Error("note service is unavailable")
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const noteDir = ctx.resolve(params.path || ".pinokio/notes")
|
|
19
|
-
const noteConfig = {
|
|
20
|
-
path: params.path || ".pinokio/notes",
|
|
21
|
-
description: params.description,
|
|
22
|
-
publish: params.publish
|
|
23
|
-
}
|
|
24
|
-
let timer = null
|
|
25
|
-
let disposed = false
|
|
26
|
-
|
|
27
|
-
const inspect = async () => {
|
|
28
|
-
if (disposed) return
|
|
29
|
-
await this.notes.inspectWorkspace({ cwd: ctx.cwd, note: noteConfig })
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const scheduleInspect = () => {
|
|
33
|
-
if (disposed) return
|
|
34
|
-
if (timer) clearTimeout(timer)
|
|
35
|
-
timer = setTimeout(() => {
|
|
36
|
-
timer = null
|
|
37
|
-
inspect().catch((error) => {
|
|
38
|
-
console.warn("[notes] failed to inspect workspace", error && error.message ? error.message : error)
|
|
39
|
-
})
|
|
40
|
-
}, 250)
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
await inspect()
|
|
44
|
-
const stopPoll = ctx.poll(params.interval || 1500, inspect, {
|
|
45
|
-
immediate: false,
|
|
46
|
-
onError: (error) => {
|
|
47
|
-
console.warn("[notes] poll failed", error && error.message ? error.message : error)
|
|
48
|
-
}
|
|
49
|
-
})
|
|
50
|
-
const unsubscribe = await ctx.watch.fs(ctx.cwd, (events) => {
|
|
51
|
-
if (!Array.isArray(events)) return
|
|
52
|
-
if (events.some((event) => event && event.path && isInside(path.resolve(event.path), noteDir))) {
|
|
53
|
-
scheduleInspect()
|
|
54
|
-
}
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
return async () => {
|
|
58
|
-
if (timer) {
|
|
59
|
-
clearTimeout(timer)
|
|
60
|
-
timer = null
|
|
61
|
-
}
|
|
62
|
-
await this.notes.inspectWorkspace({ cwd: ctx.cwd, note: noteConfig }).catch(() => {})
|
|
63
|
-
disposed = true
|
|
64
|
-
if (typeof stopPoll === "function") {
|
|
65
|
-
await stopPoll()
|
|
66
|
-
}
|
|
67
|
-
if (typeof unsubscribe === "function") {
|
|
68
|
-
await unsubscribe()
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
module.exports = NoteWatcher
|
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
const fs = require("fs")
|
|
2
|
-
const path = require("path")
|
|
3
|
-
|
|
4
|
-
const SORT_MODES = new Set(["most_used", "last_opened", "az"])
|
|
5
|
-
|
|
6
|
-
function normalizeSortMode(sort) {
|
|
7
|
-
if (SORT_MODES.has(sort)) return sort
|
|
8
|
-
return "most_used"
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
function normalizePathKey(filepath) {
|
|
12
|
-
const resolved = path.resolve(filepath).replace(/[\\/]+$/, "")
|
|
13
|
-
return process.platform === "win32" ? resolved.toLowerCase() : resolved
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function toRoutePath(filepath) {
|
|
17
|
-
const resolved = path.resolve(filepath).replace(/\\/g, "/")
|
|
18
|
-
const encoded = resolved
|
|
19
|
-
.split("/")
|
|
20
|
-
.map((segment, index) => {
|
|
21
|
-
if (index === 0 && segment === "") return ""
|
|
22
|
-
return encodeURIComponent(segment)
|
|
23
|
-
})
|
|
24
|
-
.join("/")
|
|
25
|
-
return encoded.startsWith("/") ? `/d${encoded}` : `/d/${encoded}`
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function latestTimestamp(values) {
|
|
29
|
-
return values.reduce((latest, value) => {
|
|
30
|
-
if (!value) return latest
|
|
31
|
-
const timestamp = typeof value === "number" ? value : new Date(value).getTime()
|
|
32
|
-
return Number.isFinite(timestamp) && timestamp > latest ? timestamp : latest
|
|
33
|
-
}, 0)
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function sortWorkspaces(items, sort) {
|
|
37
|
-
const mode = normalizeSortMode(sort)
|
|
38
|
-
const sorted = [...items]
|
|
39
|
-
sorted.sort((a, b) => {
|
|
40
|
-
if (mode === "az") {
|
|
41
|
-
return a.name.localeCompare(b.name, undefined, { sensitivity: "base" })
|
|
42
|
-
}
|
|
43
|
-
if (mode === "last_opened") {
|
|
44
|
-
const delta = (b.lastOpenedAtMs || b.modifiedAtMs || 0) - (a.lastOpenedAtMs || a.modifiedAtMs || 0)
|
|
45
|
-
if (delta !== 0) return delta
|
|
46
|
-
return a.name.localeCompare(b.name, undefined, { sensitivity: "base" })
|
|
47
|
-
}
|
|
48
|
-
const usageDelta = (b.usageCount || 0) - (a.usageCount || 0)
|
|
49
|
-
if (usageDelta !== 0) return usageDelta
|
|
50
|
-
return a.name.localeCompare(b.name, undefined, { sensitivity: "base" })
|
|
51
|
-
})
|
|
52
|
-
return sorted
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function newestNote(notes) {
|
|
56
|
-
return [...notes].sort((a, b) => {
|
|
57
|
-
return (new Date(b.updatedAt || 0).getTime() || 0) - (new Date(a.updatedAt || 0).getTime() || 0)
|
|
58
|
-
})[0] || null
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function createWorkspaceCatalogService({ kernel, workspaceRuntime, notes }) {
|
|
62
|
-
async function list(options = {}) {
|
|
63
|
-
const sort = normalizeSortMode(options.sort)
|
|
64
|
-
const root = path.resolve(kernel.path("workspaces"))
|
|
65
|
-
const entries = await fs.promises.readdir(root, { withFileTypes: true }).catch(() => [])
|
|
66
|
-
const runtime = workspaceRuntime.list()
|
|
67
|
-
const liveByPath = new Map()
|
|
68
|
-
|
|
69
|
-
for (const group of runtime.workspaces || []) {
|
|
70
|
-
if (group.root !== "workspaces") continue
|
|
71
|
-
liveByPath.set(normalizePathKey(group.cwd), group)
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const noteByPath = new Map()
|
|
75
|
-
const pendingNotes = notes ? await notes.listPending({}).catch(() => []) : []
|
|
76
|
-
for (const note of pendingNotes) {
|
|
77
|
-
if (!note.cwd) continue
|
|
78
|
-
const key = normalizePathKey(note.cwd)
|
|
79
|
-
const list = noteByPath.get(key) || []
|
|
80
|
-
list.push(note)
|
|
81
|
-
noteByPath.set(key, list)
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const folders = entries.filter((entry) => entry.isDirectory())
|
|
85
|
-
const items = []
|
|
86
|
-
|
|
87
|
-
for (const entry of folders) {
|
|
88
|
-
const cwd = path.join(root, entry.name)
|
|
89
|
-
const stats = await fs.promises.stat(cwd).catch(() => null)
|
|
90
|
-
const key = normalizePathKey(cwd)
|
|
91
|
-
const live = liveByPath.get(key)
|
|
92
|
-
const shells = live?.shells || []
|
|
93
|
-
const scripts = live?.scripts || []
|
|
94
|
-
const workspaceNotes = noteByPath.get(key) || []
|
|
95
|
-
const note = newestNote(workspaceNotes)
|
|
96
|
-
const modifiedAtMs = stats?.mtimeMs || 0
|
|
97
|
-
const lastOpenedAtMs = latestTimestamp([
|
|
98
|
-
modifiedAtMs,
|
|
99
|
-
...shells.map((shell) => shell.start_time),
|
|
100
|
-
...workspaceNotes.map((item) => item.updatedAt),
|
|
101
|
-
])
|
|
102
|
-
const primaryShell = shells.length === 1 ? shells[0] : null
|
|
103
|
-
const primaryScript = scripts.length === 1 ? scripts[0] : null
|
|
104
|
-
const usageCount = shells.length + scripts.length
|
|
105
|
-
|
|
106
|
-
items.push({
|
|
107
|
-
name: entry.name,
|
|
108
|
-
cwd,
|
|
109
|
-
relpath: entry.name,
|
|
110
|
-
modifiedAt: modifiedAtMs ? new Date(modifiedAtMs).toISOString() : null,
|
|
111
|
-
modifiedAtMs,
|
|
112
|
-
lastOpenedAt: lastOpenedAtMs ? new Date(lastOpenedAtMs).toISOString() : null,
|
|
113
|
-
lastOpenedAtMs,
|
|
114
|
-
usageCount,
|
|
115
|
-
running: shells.length > 0 || scripts.length > 0,
|
|
116
|
-
counts: {
|
|
117
|
-
shells: shells.length,
|
|
118
|
-
scripts: scripts.length,
|
|
119
|
-
notes: workspaceNotes.length,
|
|
120
|
-
},
|
|
121
|
-
shells,
|
|
122
|
-
scripts,
|
|
123
|
-
note,
|
|
124
|
-
noteReady: Boolean(note),
|
|
125
|
-
primaryUrl: primaryScript?.url || primaryShell?.url || null,
|
|
126
|
-
launchUrl: toRoutePath(cwd),
|
|
127
|
-
})
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
const running = sortWorkspaces(items.filter((item) => item.running), sort)
|
|
131
|
-
const offline = sortWorkspaces(items.filter((item) => !item.running), sort)
|
|
132
|
-
|
|
133
|
-
return {
|
|
134
|
-
root,
|
|
135
|
-
sort,
|
|
136
|
-
running,
|
|
137
|
-
offline,
|
|
138
|
-
items: [...running, ...offline],
|
|
139
|
-
counts: {
|
|
140
|
-
total: items.length,
|
|
141
|
-
running: running.length,
|
|
142
|
-
offline: offline.length,
|
|
143
|
-
notes: items.filter((item) => item.noteReady).length,
|
|
144
|
-
},
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
return { list, normalizeSortMode }
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
module.exports = { createWorkspaceCatalogService, normalizeSortMode }
|