pinokiod 7.2.16 → 7.2.18
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/agent_instructions.js +166 -0
- package/kernel/api/index.js +137 -12
- package/kernel/bin/huggingface.js +1 -1
- package/kernel/environment.js +23 -9
- package/kernel/plugin_sources.js +57 -4
- package/kernel/prototype.js +4 -0
- package/kernel/shell.js +2 -0
- package/kernel/watch/index.js +31 -4
- package/package.json +1 -1
- package/server/features/index.js +4 -4
- package/server/features/{drafts → notes}/index.js +9 -9
- package/server/features/{drafts → notes}/parser.js +12 -7
- package/server/features/notes/public/notes.css +955 -0
- package/server/features/notes/public/notes.js +1149 -0
- package/server/features/{drafts → notes}/registry_import.js +59 -74
- package/server/features/notes/routes.js +156 -0
- package/server/features/notes/service.js +326 -0
- package/server/features/{drafts → notes}/watcher.js +14 -16
- package/server/index.js +61 -30
- package/server/lib/content_validation.js +19 -8
- package/server/lib/workspace_catalog.js +18 -18
- package/server/public/task-launcher.css +11 -3
- package/server/public/tasker.css +336 -0
- package/server/public/tasker.js +407 -0
- package/server/views/d.ejs +33 -2
- package/server/views/partials/menu.ejs +1 -1
- package/server/views/partials/workspace_row.ejs +11 -11
- package/server/views/pre.ejs +1 -1
- package/server/views/task_launch.ejs +10 -10
- package/server/views/tasker.ejs +40 -0
- package/server/views/terminal.ejs +15 -6
- package/server/views/terminals.ejs +0 -1
- package/server/views/workspaces.ejs +2 -1
- package/system/plugin/antigravity/pinokio.js +2 -4
- package/system/plugin/claude/pinokio.js +2 -4
- package/system/plugin/claude-auto/pinokio.js +2 -4
- package/system/plugin/claude-desktop/pinokio.js +2 -4
- package/system/plugin/codex/pinokio.js +2 -4
- package/system/plugin/codex-auto/pinokio.js +2 -4
- package/system/plugin/codex-desktop/pinokio.js +2 -4
- package/system/plugin/crush/pinokio.js +2 -4
- package/system/plugin/cursor/pinokio.js +2 -4
- package/system/plugin/gemini/pinokio.js +2 -4
- package/system/plugin/gemini-auto/pinokio.js +2 -4
- package/system/plugin/qwen/pinokio.js +2 -4
- package/system/plugin/vscode/pinokio.js +2 -4
- package/system/plugin/windsurf/pinokio.js +2 -4
- package/test/plugin-sources.test.js +45 -0
- package/server/features/drafts/public/drafts.js +0 -1569
- package/server/features/drafts/routes.js +0 -68
- package/server/features/drafts/service.js +0 -261
|
@@ -0,0 +1,326 @@
|
|
|
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
|
+
}
|
|
@@ -5,21 +5,19 @@ function isInside(candidate, parent) {
|
|
|
5
5
|
return relative === "" || (!!relative && !relative.startsWith("..") && !path.isAbsolute(relative))
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
class
|
|
8
|
+
class NoteWatcher {
|
|
9
9
|
constructor(options = {}) {
|
|
10
|
-
this.
|
|
10
|
+
this.notes = options.notes
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
async
|
|
14
|
-
if (!this.
|
|
15
|
-
throw new Error("
|
|
13
|
+
async watch(ctx, params = {}) {
|
|
14
|
+
if (!this.notes || typeof this.notes.inspectWorkspace !== "function") {
|
|
15
|
+
throw new Error("note service is unavailable")
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
path: params.path || ".pinokio/
|
|
21
|
-
content: params.content,
|
|
22
|
-
ready: params.ready,
|
|
18
|
+
const noteDir = ctx.resolve(params.path || ".pinokio/notes")
|
|
19
|
+
const noteConfig = {
|
|
20
|
+
path: params.path || ".pinokio/notes",
|
|
23
21
|
description: params.description,
|
|
24
22
|
publish: params.publish
|
|
25
23
|
}
|
|
@@ -28,7 +26,7 @@ class DraftWatcher {
|
|
|
28
26
|
|
|
29
27
|
const inspect = async () => {
|
|
30
28
|
if (disposed) return
|
|
31
|
-
await this.
|
|
29
|
+
await this.notes.inspectWorkspace({ cwd: ctx.cwd, note: noteConfig })
|
|
32
30
|
}
|
|
33
31
|
|
|
34
32
|
const scheduleInspect = () => {
|
|
@@ -37,7 +35,7 @@ class DraftWatcher {
|
|
|
37
35
|
timer = setTimeout(() => {
|
|
38
36
|
timer = null
|
|
39
37
|
inspect().catch((error) => {
|
|
40
|
-
console.warn("[
|
|
38
|
+
console.warn("[notes] failed to inspect workspace", error && error.message ? error.message : error)
|
|
41
39
|
})
|
|
42
40
|
}, 250)
|
|
43
41
|
}
|
|
@@ -46,12 +44,12 @@ class DraftWatcher {
|
|
|
46
44
|
const stopPoll = ctx.poll(params.interval || 1500, inspect, {
|
|
47
45
|
immediate: false,
|
|
48
46
|
onError: (error) => {
|
|
49
|
-
console.warn("[
|
|
47
|
+
console.warn("[notes] poll failed", error && error.message ? error.message : error)
|
|
50
48
|
}
|
|
51
49
|
})
|
|
52
50
|
const unsubscribe = await ctx.watch.fs(ctx.cwd, (events) => {
|
|
53
51
|
if (!Array.isArray(events)) return
|
|
54
|
-
if (events.some((event) => event && event.path && isInside(path.resolve(event.path),
|
|
52
|
+
if (events.some((event) => event && event.path && isInside(path.resolve(event.path), noteDir))) {
|
|
55
53
|
scheduleInspect()
|
|
56
54
|
}
|
|
57
55
|
})
|
|
@@ -61,7 +59,7 @@ class DraftWatcher {
|
|
|
61
59
|
clearTimeout(timer)
|
|
62
60
|
timer = null
|
|
63
61
|
}
|
|
64
|
-
await this.
|
|
62
|
+
await this.notes.inspectWorkspace({ cwd: ctx.cwd, note: noteConfig }).catch(() => {})
|
|
65
63
|
disposed = true
|
|
66
64
|
if (typeof stopPoll === "function") {
|
|
67
65
|
await stopPoll()
|
|
@@ -73,4 +71,4 @@ class DraftWatcher {
|
|
|
73
71
|
}
|
|
74
72
|
}
|
|
75
73
|
|
|
76
|
-
module.exports =
|
|
74
|
+
module.exports = NoteWatcher
|
package/server/index.js
CHANGED
|
@@ -2758,10 +2758,12 @@ class Server {
|
|
|
2758
2758
|
} else {
|
|
2759
2759
|
resolved = runner(this.kernel, this.kernel.info)
|
|
2760
2760
|
}
|
|
2761
|
-
|
|
2761
|
+
const action = resolved ? resolved[actionKey] : null
|
|
2762
|
+
runnable = typeof action === "function" || (Array.isArray(action) && action.length > 0)
|
|
2762
2763
|
} else {
|
|
2763
|
-
runnable = runner && Array.isArray(runner[actionKey]) && runner[actionKey].length > 0
|
|
2764
2764
|
resolved = runner
|
|
2765
|
+
const action = resolved ? resolved[actionKey] : null
|
|
2766
|
+
runnable = typeof action === "function" || (Array.isArray(action) && action.length > 0)
|
|
2765
2767
|
}
|
|
2766
2768
|
|
|
2767
2769
|
let template = "terminal"
|
|
@@ -2870,10 +2872,10 @@ class Server {
|
|
|
2870
2872
|
const protectionPreference = protectionAppId && this.appPreferences && typeof this.appPreferences.getPreference === "function"
|
|
2871
2873
|
? await this.appPreferences.getPreference(protectionAppId)
|
|
2872
2874
|
: null
|
|
2873
|
-
const
|
|
2874
|
-
? this.kernel.watch.hasHandler(resolved, "
|
|
2875
|
+
const noteWatchEnabled = this.kernel.watch && typeof this.kernel.watch.hasHandler === "function"
|
|
2876
|
+
? this.kernel.watch.hasHandler(resolved, "note")
|
|
2875
2877
|
: false
|
|
2876
|
-
const
|
|
2878
|
+
const noteWatchCwd = noteWatchEnabled
|
|
2877
2879
|
? (req.query.cwd || path.dirname(filepath))
|
|
2878
2880
|
: ""
|
|
2879
2881
|
const activeProcessWait = this.kernel.activeProcessWaits && this.kernel.activeProcessWaits[filepath]
|
|
@@ -2884,8 +2886,8 @@ class Server {
|
|
|
2884
2886
|
projectName: (pathComponents.length > 0 ? pathComponents[0] : ''),
|
|
2885
2887
|
protection_app_id: protectionAppId,
|
|
2886
2888
|
protection_enabled: protectionPreference ? protectionPreference.protection_enabled !== false : false,
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
+
note_watch_enabled: noteWatchEnabled,
|
|
2890
|
+
note_watch_cwd: noteWatchCwd,
|
|
2889
2891
|
active_process_wait: activeProcessWait ? {
|
|
2890
2892
|
title: activeProcessWait.title,
|
|
2891
2893
|
description: activeProcessWait.description,
|
|
@@ -5311,15 +5313,7 @@ class Server {
|
|
|
5311
5313
|
return normalized
|
|
5312
5314
|
}
|
|
5313
5315
|
isValidBundledPluginConfig(pluginConfig) {
|
|
5314
|
-
|
|
5315
|
-
return false
|
|
5316
|
-
}
|
|
5317
|
-
for (const key of Object.keys(pluginConfig)) {
|
|
5318
|
-
if (typeof pluginConfig[key] === "function") {
|
|
5319
|
-
return false
|
|
5320
|
-
}
|
|
5321
|
-
}
|
|
5322
|
-
return true
|
|
5316
|
+
return PluginSources.isValidPluginConfig(pluginConfig)
|
|
5323
5317
|
}
|
|
5324
5318
|
isPathInsideRootForBundledPlugin(candidatePath, rootPath) {
|
|
5325
5319
|
const relative = path.relative(rootPath, candidatePath)
|
|
@@ -6857,9 +6851,9 @@ class Server {
|
|
|
6857
6851
|
cwd: typeof pluginItem.ownerApp.cwd === "string" ? pluginItem.ownerApp.cwd : "",
|
|
6858
6852
|
}
|
|
6859
6853
|
: null,
|
|
6860
|
-
hasInstall:
|
|
6861
|
-
hasUninstall:
|
|
6862
|
-
hasUpdate:
|
|
6854
|
+
hasInstall: PluginSources.isAction(pluginItem?.install),
|
|
6855
|
+
hasUninstall: PluginSources.isAction(pluginItem?.uninstall),
|
|
6856
|
+
hasUpdate: PluginSources.isAction(pluginItem?.update),
|
|
6863
6857
|
category,
|
|
6864
6858
|
categoryTitle: category === "ide" ? "Desktop Plugin" : "Terminal Plugin",
|
|
6865
6859
|
categorySubtitle: category === "ide" ? "Launch externally" : "Launch in Pinokio",
|
|
@@ -8415,12 +8409,12 @@ class Server {
|
|
|
8415
8409
|
defaultRegistryUrl: DEFAULT_REGISTRY_URL
|
|
8416
8410
|
})
|
|
8417
8411
|
this.features = features
|
|
8418
|
-
const
|
|
8419
|
-
this.
|
|
8412
|
+
const notes = features.notes.service
|
|
8413
|
+
this.notes = notes
|
|
8420
8414
|
const workspaceCatalog = createWorkspaceCatalogService({
|
|
8421
8415
|
kernel: this.kernel,
|
|
8422
8416
|
workspaceRuntime,
|
|
8423
|
-
|
|
8417
|
+
notes
|
|
8424
8418
|
})
|
|
8425
8419
|
registerWorkspacesRoutes(this.app, {
|
|
8426
8420
|
workspaceCatalog,
|
|
@@ -9744,6 +9738,13 @@ class Server {
|
|
|
9744
9738
|
})
|
|
9745
9739
|
}))
|
|
9746
9740
|
|
|
9741
|
+
this.app.get("/tasker", ex(async (req, res) => {
|
|
9742
|
+
res.render("tasker", {
|
|
9743
|
+
theme: this.theme,
|
|
9744
|
+
agent: req.agent,
|
|
9745
|
+
})
|
|
9746
|
+
}))
|
|
9747
|
+
|
|
9747
9748
|
this.app.get("/tasks/new", ex(async (req, res) => {
|
|
9748
9749
|
const sourceWorkspace = normalizeTaskBuilderSourceWorkspace(req.query.sourceWorkspaceCwd)
|
|
9749
9750
|
const lockTargetSelection = req.query.lockTarget === "1" || req.query.lockTarget === "true"
|
|
@@ -13415,9 +13416,21 @@ class Server {
|
|
|
13415
13416
|
// }
|
|
13416
13417
|
// }))
|
|
13417
13418
|
this.app.post("/env", ex(async (req, res) => {
|
|
13418
|
-
|
|
13419
|
+
const requestFilepath = typeof req.body.filepath === "string" ? req.body.filepath : ""
|
|
13420
|
+
const requestRoot = path.resolve(this.kernel.homedir, requestFilepath)
|
|
13421
|
+
let fullpath = path.resolve(requestRoot, "ENVIRONMENT")
|
|
13422
|
+
if (!(await this.kernel.exists(fullpath))) {
|
|
13423
|
+
const normalizedFilepath = requestFilepath.replace(/\\/g, "/")
|
|
13424
|
+
const filepathParts = normalizedFilepath.split("/").filter(Boolean)
|
|
13425
|
+
if (filepathParts[0] === "api" && filepathParts[1] && filepathParts.length === 2) {
|
|
13426
|
+
const launcher = await this.kernel.api.launcher(filepathParts[1])
|
|
13427
|
+
if (launcher && launcher.launcher_root) {
|
|
13428
|
+
fullpath = path.resolve(launcher.root, launcher.launcher_root, "ENVIRONMENT")
|
|
13429
|
+
}
|
|
13430
|
+
}
|
|
13431
|
+
}
|
|
13419
13432
|
let updated = req.body.vals
|
|
13420
|
-
let hosts = req.body.hosts
|
|
13433
|
+
let hosts = req.body.hosts || {}
|
|
13421
13434
|
await Util.update_env(fullpath, updated)
|
|
13422
13435
|
const normalizedFilepath = typeof req.body.filepath === "string"
|
|
13423
13436
|
? req.body.filepath.replace(/\\/g, "/")
|
|
@@ -13570,14 +13583,15 @@ class Server {
|
|
|
13570
13583
|
this.app.get("/pre/api/:name", ex(async (req, res) => {
|
|
13571
13584
|
let launcher = await this.kernel.api.launcher(req.params.name)
|
|
13572
13585
|
let config = launcher.script
|
|
13573
|
-
if (config && config.pre) {
|
|
13574
|
-
config.pre.
|
|
13575
|
-
|
|
13586
|
+
if (config && Array.isArray(config.pre)) {
|
|
13587
|
+
const items = config.pre.filter((item) => item && typeof item === "object")
|
|
13588
|
+
items.forEach((item) => {
|
|
13589
|
+
if (typeof item.icon === "string" && item.icon) {
|
|
13576
13590
|
item.icon = `/api/${req.params.name}/${item.icon}?raw=true`
|
|
13577
13591
|
} else {
|
|
13578
13592
|
item.icon = "/pinokio-black.png"
|
|
13579
13593
|
}
|
|
13580
|
-
if (!item.href.startsWith("http")) {
|
|
13594
|
+
if (typeof item.href === "string" && item.href && !item.href.startsWith("http")) {
|
|
13581
13595
|
item.href = path.resolve(this.kernel.homedir, "api", req.params.name, item.href)
|
|
13582
13596
|
}
|
|
13583
13597
|
})
|
|
@@ -13588,7 +13602,7 @@ class Server {
|
|
|
13588
13602
|
theme: this.theme,
|
|
13589
13603
|
agent: req.agent,
|
|
13590
13604
|
name: req.params.name,
|
|
13591
|
-
items
|
|
13605
|
+
items,
|
|
13592
13606
|
env
|
|
13593
13607
|
})
|
|
13594
13608
|
} else {
|
|
@@ -14074,7 +14088,7 @@ class Server {
|
|
|
14074
14088
|
mode = "launch_type.desktop"
|
|
14075
14089
|
} else if (launchType === "terminal") {
|
|
14076
14090
|
mode = "launch_type.terminal"
|
|
14077
|
-
} else {
|
|
14091
|
+
} else if (Array.isArray(item.run)) {
|
|
14078
14092
|
for(let step of item.run) {
|
|
14079
14093
|
if (step.method === "exec") {
|
|
14080
14094
|
mode = "exec"
|
|
@@ -14089,6 +14103,8 @@ class Server {
|
|
|
14089
14103
|
break
|
|
14090
14104
|
}
|
|
14091
14105
|
}
|
|
14106
|
+
} else if (typeof item.run === "function") {
|
|
14107
|
+
mode = "shell"
|
|
14092
14108
|
}
|
|
14093
14109
|
if (mode === "launch_type.desktop" || mode === "exec" || mode === "launch") {
|
|
14094
14110
|
item.type = "Open"
|
|
@@ -14141,6 +14157,20 @@ class Server {
|
|
|
14141
14157
|
spec = await fs.promises.readFile(path.resolve(filepath, "SPEC.md"), "utf8")
|
|
14142
14158
|
} catch (e) {
|
|
14143
14159
|
}
|
|
14160
|
+
const registryEnabled = await this.isRegistryEnabled().catch(() => false)
|
|
14161
|
+
let registry_parent_url = ""
|
|
14162
|
+
if (registryEnabled) {
|
|
14163
|
+
try {
|
|
14164
|
+
registry_parent_url = await git.getConfig({
|
|
14165
|
+
fs,
|
|
14166
|
+
http,
|
|
14167
|
+
dir: filepath,
|
|
14168
|
+
path: "remote.origin.url"
|
|
14169
|
+
}) || ""
|
|
14170
|
+
} catch (_) {
|
|
14171
|
+
registry_parent_url = ""
|
|
14172
|
+
}
|
|
14173
|
+
}
|
|
14144
14174
|
res.render("d", {
|
|
14145
14175
|
filepath,
|
|
14146
14176
|
spec,
|
|
@@ -14151,6 +14181,7 @@ class Server {
|
|
|
14151
14181
|
install: this.install,
|
|
14152
14182
|
agent: req.agent,
|
|
14153
14183
|
theme: this.theme,
|
|
14184
|
+
registry_parent_url,
|
|
14154
14185
|
//dynamic: plugin_menu
|
|
14155
14186
|
dynamic,
|
|
14156
14187
|
})
|
|
@@ -147,9 +147,9 @@ function createContentValidationService({ kernel }) {
|
|
|
147
147
|
absolutePath,
|
|
148
148
|
dir: pluginDir,
|
|
149
149
|
config,
|
|
150
|
-
hasInstall:
|
|
151
|
-
hasUpdate:
|
|
152
|
-
hasUninstall:
|
|
150
|
+
hasInstall: PluginSources.isAction(config && config.install),
|
|
151
|
+
hasUpdate: PluginSources.isAction(config && config.update),
|
|
152
|
+
hasUninstall: PluginSources.isAction(config && config.uninstall),
|
|
153
153
|
image: null,
|
|
154
154
|
}
|
|
155
155
|
|
|
@@ -252,21 +252,32 @@ function createContentValidationService({ kernel }) {
|
|
|
252
252
|
{ file: absolutePath }
|
|
253
253
|
))
|
|
254
254
|
} else {
|
|
255
|
-
if (!
|
|
255
|
+
if (!PluginSources.isAction(config.run)) {
|
|
256
256
|
errors.push(buildError(
|
|
257
|
-
"Plugins must define a top-level run array.",
|
|
258
|
-
"Add `run: [...]` to pinokio.js.",
|
|
257
|
+
"Plugins must define a top-level run array or function.",
|
|
258
|
+
"Add `run: [...]` or `run: async (ctx) => [...]` to pinokio.js.",
|
|
259
259
|
{ file: absolutePath }
|
|
260
260
|
))
|
|
261
261
|
}
|
|
262
|
-
const topLevelFunctionKeys = Object.keys(config).filter((key) =>
|
|
262
|
+
const topLevelFunctionKeys = Object.keys(config).filter((key) => {
|
|
263
|
+
return typeof config[key] === "function" && !PluginSources.ACTION_KEYS.has(key)
|
|
264
|
+
})
|
|
263
265
|
if (topLevelFunctionKeys.length > 0) {
|
|
264
266
|
errors.push(buildError(
|
|
265
267
|
`Top-level function fields are not supported: ${topLevelFunctionKeys.join(", ")}.`,
|
|
266
|
-
"
|
|
268
|
+
"Only action fields such as run, install, uninstall, and update may be functions.",
|
|
267
269
|
{ file: absolutePath }
|
|
268
270
|
))
|
|
269
271
|
}
|
|
272
|
+
for (const key of PluginSources.ACTION_KEYS) {
|
|
273
|
+
if (key in config && !PluginSources.isAction(config[key])) {
|
|
274
|
+
errors.push(buildError(
|
|
275
|
+
`Plugin action ${key} must be an array or function.`,
|
|
276
|
+
`Set ${key} to an array or async function returning an array.`,
|
|
277
|
+
{ file: absolutePath }
|
|
278
|
+
))
|
|
279
|
+
}
|
|
280
|
+
}
|
|
270
281
|
if (normalizedPath.startsWith("/plugin/")) {
|
|
271
282
|
const declaredPath = typeof config.path === "string" ? config.path.trim() : ""
|
|
272
283
|
if (declaredPath !== "plugin") {
|