pinokiod 7.2.18 → 7.3.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 (89) hide show
  1. package/Dockerfile +2 -0
  2. package/kernel/api/index.js +13 -179
  3. package/kernel/api/process/index.js +44 -99
  4. package/kernel/bin/conda-python.js +30 -0
  5. package/kernel/bin/conda.js +22 -3
  6. package/kernel/bin/huggingface.js +1 -1
  7. package/kernel/bin/index.js +11 -1
  8. package/kernel/environment.js +11 -205
  9. package/kernel/git.js +13 -0
  10. package/kernel/index.js +1 -64
  11. package/kernel/plugin.js +58 -6
  12. package/kernel/prototype.js +0 -4
  13. package/kernel/shell.js +2 -23
  14. package/kernel/util.js +0 -60
  15. package/package.json +1 -1
  16. package/server/index.js +171 -229
  17. package/server/lib/content_validation.js +33 -47
  18. package/server/public/common.js +29 -103
  19. package/server/public/create-launcher.js +31 -4
  20. package/server/public/electron.css +6 -0
  21. package/server/public/style.css +0 -337
  22. package/server/public/task-launcher.css +3 -11
  23. package/server/public/task-launcher.js +32 -5
  24. package/server/public/universal-launcher.js +26 -3
  25. package/server/socket.js +11 -22
  26. package/server/views/app.ejs +30 -167
  27. package/server/views/d.ejs +35 -33
  28. package/server/views/editor.ejs +4 -25
  29. package/server/views/partials/main_sidebar.ejs +0 -1
  30. package/server/views/partials/menu.ejs +1 -1
  31. package/server/views/pre.ejs +1 -1
  32. package/server/views/shell.ejs +3 -11
  33. package/server/views/task_launch.ejs +10 -10
  34. package/server/views/terminal.ejs +5 -34
  35. package/spec/INSTRUCTION_SYNC.md +5 -5
  36. package/kernel/agent_instructions.js +0 -166
  37. package/kernel/api/shell_run_template.js +0 -273
  38. package/kernel/api/uri/index.js +0 -51
  39. package/kernel/plugin_sources.js +0 -289
  40. package/kernel/watch/context.js +0 -42
  41. package/kernel/watch/drivers/fs.js +0 -71
  42. package/kernel/watch/drivers/poll.js +0 -33
  43. package/kernel/watch/index.js +0 -185
  44. package/server/features/index.js +0 -13
  45. package/server/features/notes/index.js +0 -41
  46. package/server/features/notes/parser.js +0 -174
  47. package/server/features/notes/public/notes.css +0 -955
  48. package/server/features/notes/public/notes.js +0 -1149
  49. package/server/features/notes/registry_import.js +0 -412
  50. package/server/features/notes/routes.js +0 -156
  51. package/server/features/notes/service.js +0 -326
  52. package/server/features/notes/watcher.js +0 -74
  53. package/server/lib/workspace_catalog.js +0 -151
  54. package/server/lib/workspace_runtime.js +0 -390
  55. package/server/public/tasker.css +0 -336
  56. package/server/public/tasker.js +0 -407
  57. package/server/routes/workspaces.js +0 -44
  58. package/server/views/partials/workspace_row.ejs +0 -61
  59. package/server/views/tasker.ejs +0 -40
  60. package/server/views/workspaces.ejs +0 -813
  61. package/system/plugin/antigravity/antigravity.png +0 -0
  62. package/system/plugin/antigravity/pinokio.js +0 -35
  63. package/system/plugin/claude/claude.png +0 -0
  64. package/system/plugin/claude/pinokio.js +0 -61
  65. package/system/plugin/claude-auto/claude.png +0 -0
  66. package/system/plugin/claude-auto/pinokio.js +0 -72
  67. package/system/plugin/claude-desktop/icon.jpeg +0 -0
  68. package/system/plugin/claude-desktop/pinokio.js +0 -37
  69. package/system/plugin/codex/openai.webp +0 -0
  70. package/system/plugin/codex/pinokio.js +0 -56
  71. package/system/plugin/codex-auto/openai.webp +0 -0
  72. package/system/plugin/codex-auto/pinokio.js +0 -63
  73. package/system/plugin/codex-desktop/icon.png +0 -0
  74. package/system/plugin/codex-desktop/pinokio.js +0 -37
  75. package/system/plugin/crush/crush.png +0 -0
  76. package/system/plugin/crush/pinokio.js +0 -29
  77. package/system/plugin/cursor/cursor.jpeg +0 -0
  78. package/system/plugin/cursor/pinokio.js +0 -37
  79. package/system/plugin/gemini/gemini.jpeg +0 -0
  80. package/system/plugin/gemini/pinokio.js +0 -38
  81. package/system/plugin/gemini-auto/gemini.jpeg +0 -0
  82. package/system/plugin/gemini-auto/pinokio.js +0 -41
  83. package/system/plugin/qwen/pinokio.js +0 -48
  84. package/system/plugin/qwen/qwen.png +0 -0
  85. package/system/plugin/vscode/pinokio.js +0 -34
  86. package/system/plugin/vscode/vscode.png +0 -0
  87. package/system/plugin/windsurf/pinokio.js +0 -37
  88. package/system/plugin/windsurf/windsurf.png +0 -0
  89. package/test/plugin-sources.test.js +0 -45
@@ -1,412 +0,0 @@
1
- const express = require("express")
2
- const fs = require("fs")
3
- const path = require("path")
4
- const axios = require("axios")
5
- const FormData = require("form-data")
6
- const mime = require("mime-types")
7
- const {
8
- describeMediaRefs,
9
- extractTitleAndBody,
10
- normalizeTitle
11
- } = require("./parser")
12
-
13
- const DEFAULT_MAX_FILES = 10
14
- const DEFAULT_MAX_FILE_BYTES = 25 * 1024 * 1024
15
-
16
- const asyncHandler = (fn) => (req, res, next) => {
17
- Promise.resolve(fn(req, res, next)).catch(next)
18
- }
19
-
20
- const escapeHtml = (value) => String(value || "")
21
- .replace(/&/g, "&")
22
- .replace(/</g, "&lt;")
23
- .replace(/>/g, "&gt;")
24
- .replace(/"/g, "&quot;")
25
-
26
- function renderMessage(res, status, title, message) {
27
- res.status(status).send(`<!doctype html>
28
- <html>
29
- <head>
30
- <meta charset="utf-8">
31
- <title>${escapeHtml(title)}</title>
32
- <style>
33
- body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; margin: 40px; color: #111827; }
34
- .box { max-width: 680px; border: 1px solid #d1d5db; border-radius: 8px; padding: 18px; }
35
- h1 { margin: 0 0 8px; font-size: 22px; }
36
- p { margin: 0; color: #4b5563; line-height: 1.5; }
37
- </style>
38
- </head>
39
- <body>
40
- <div class="box">
41
- <h1>${escapeHtml(title)}</h1>
42
- <p>${escapeHtml(message)}</p>
43
- </div>
44
- </body>
45
- </html>`)
46
- }
47
-
48
- function renderImportLauncher(res, { authorizeUrl, autoOpen }) {
49
- res.status(200).send(`<!doctype html>
50
- <html>
51
- <head>
52
- <meta charset="utf-8">
53
- <title>Import draft</title>
54
- <style>
55
- body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; margin: 40px; color: #111827; background: #f8fafc; }
56
- .box { max-width: 680px; border: 1px solid #d1d5db; border-radius: 8px; padding: 18px; background: white; box-shadow: 0 16px 42px rgba(15, 23, 42, 0.08); }
57
- h1 { margin: 0 0 8px; font-size: 22px; }
58
- p { margin: 0 0 14px; color: #4b5563; line-height: 1.5; }
59
- button, a.button { display: inline-flex; align-items: center; justify-content: center; min-height: 34px; border: 1px solid #111827; border-radius: 6px; background: #111827; color: white; padding: 7px 12px; font-weight: 700; text-decoration: none; cursor: pointer; }
60
- .muted { color: #6b7280; font-size: 13px; }
61
- </style>
62
- </head>
63
- <body>
64
- <div class="box">
65
- <h1>Import draft</h1>
66
- <p id="status">${autoOpen ? "Opening the registry authorization page in your browser..." : "Click Open registry to authorize the import."}</p>
67
- <button id="open" type="button">Open registry</button>
68
- <div class="muted" style="margin-top:12px;">The registry will return to Pinokio after authorization.</div>
69
- </div>
70
- <script>
71
- window.__PINOKIO_DRAFT_IMPORT_VERSION = "metadata-b64";
72
- const authorizeUrl = ${JSON.stringify(authorizeUrl)};
73
- const autoOpen = ${JSON.stringify(Boolean(autoOpen))};
74
- const statusEl = document.getElementById("status");
75
- const openButton = document.getElementById("open");
76
-
77
- function setStatus(message) {
78
- statusEl.textContent = message;
79
- }
80
-
81
- async function openRegistry() {
82
- setStatus("Opening registry in your browser...");
83
- const response = await fetch("/pinokio/open", {
84
- method: "POST",
85
- headers: { "Content-Type": "application/json" },
86
- body: JSON.stringify({
87
- url: authorizeUrl,
88
- surface: "browser"
89
- })
90
- });
91
- if (!response.ok) {
92
- throw new Error("Unable to open registry.");
93
- }
94
- setStatus("Authorize the import in your browser.");
95
- }
96
-
97
- openButton.addEventListener("click", () => {
98
- openRegistry().catch((error) => {
99
- setStatus(error && error.message ? error.message : "Unable to open registry.");
100
- });
101
- });
102
- if (autoOpen) {
103
- window.setTimeout(() => {
104
- openRegistry().catch((error) => {
105
- setStatus(error && error.message ? error.message : "Unable to open registry.");
106
- });
107
- }, 100);
108
- }
109
- </script>
110
- </body>
111
- </html>`)
112
- }
113
-
114
- function normalizeRegistryBase(raw, fallback) {
115
- const value = String(raw || fallback || "").trim()
116
- if (!value) return ""
117
- try {
118
- const url = new URL(value)
119
- if (url.protocol !== "https:" && url.protocol !== "http:") return ""
120
- url.hash = ""
121
- url.search = ""
122
- return url.toString().replace(/\/$/, "")
123
- } catch (_) {
124
- return ""
125
- }
126
- }
127
-
128
- function requestOrigin(req) {
129
- const host = req.get("host") || "localhost:42000"
130
- return `${req.protocol || "http"}://${host}`
131
- }
132
-
133
- function buildDraftImportReturnUrl(req, item, bundle) {
134
- const returnUrl = new URL("/registry/draft-import/callback", requestOrigin(req))
135
- returnUrl.searchParams.set("draft", item.id)
136
- if (bundle.appSlug) returnUrl.searchParams.set("app", bundle.appSlug)
137
- return returnUrl.toString()
138
- }
139
-
140
- function buildAuthorizeUrl(req, registryBase, item, bundle) {
141
- const authorizeUrl = new URL("/draft-import/authorize", registryBase)
142
- authorizeUrl.searchParams.set("handoff", "callback")
143
- authorizeUrl.searchParams.set("return", buildDraftImportReturnUrl(req, item, bundle))
144
- if (bundle.appSlug) authorizeUrl.searchParams.set("app", bundle.appSlug)
145
- return authorizeUrl
146
- }
147
-
148
- async function findNoteById(notes, id) {
149
- const normalized = String(id || "").trim()
150
- if (!normalized) return null
151
- const items = await notes.listPending({})
152
- return (items || []).find((item) => item && item.id === normalized) || null
153
- }
154
-
155
- function isRegistryPostPublish(publish) {
156
- if (!publish || typeof publish !== "object") return false
157
- const target = String(publish.target || "").trim().toLowerCase()
158
- const type = String(publish.type || "post").trim().toLowerCase()
159
- return target === "registry" && type === "post"
160
- }
161
-
162
- function normalizeParent(parent) {
163
- if (!parent || typeof parent !== "object" || Array.isArray(parent)) return null
164
- const type = String(parent.type || "").trim().toLowerCase()
165
- const url = String(parent.url || parent.repoUrl || "").trim()
166
- if (type !== "app" || !url) return null
167
- return { type: "app", url }
168
- }
169
-
170
- async function buildDraftBundle(item, query = {}) {
171
- const markdown = await fs.promises.readFile(item.notePath, "utf8")
172
- const resultDir = path.dirname(item.notePath)
173
- const titleFallback = item.title || (item.workspaceName ? `Note for ${item.workspaceName}` : "Note")
174
- const extracted = extractTitleAndBody(markdown, titleFallback)
175
- const metadataTitle = item.metadata && typeof item.metadata.title === "string"
176
- ? normalizeTitle(item.metadata.title)
177
- : ""
178
- const title = metadataTitle || extracted.title
179
- const body = metadataTitle && extracted.title && normalizeTitle(extracted.title) === metadataTitle
180
- ? extracted.body
181
- : (!metadataTitle ? extracted.body : String(markdown || "").trim())
182
- const publish = item.publish && typeof item.publish === "object" ? item.publish : null
183
- const media = await describeMediaRefs(markdown, resultDir, { mediaOnly: false })
184
- return {
185
- title,
186
- body,
187
- publish,
188
- parent: normalizeParent(publish && publish.parent),
189
- appSlug: String(query.app || "").trim(),
190
- media
191
- }
192
- }
193
-
194
- function preflightBundle(bundle, options = {}) {
195
- const maxFiles = Number(options.maxFiles || DEFAULT_MAX_FILES)
196
- const maxFileBytes = Number(options.maxFileBytes || DEFAULT_MAX_FILE_BYTES)
197
- if (!bundle.title) {
198
- return "Note title is missing."
199
- }
200
- if (!isRegistryPostPublish(bundle.publish)) {
201
- return "This note is not configured for registry publishing."
202
- }
203
- if (bundle.media.length > maxFiles) {
204
- return `Note has ${bundle.media.length} media files. The registry limit is ${maxFiles}.`
205
- }
206
- const missing = bundle.media.filter((item) => !item.exists)
207
- if (missing.length > 0) {
208
- return `Note references missing media: ${missing.map((item) => item.ref).join(", ")}`
209
- }
210
- const oversized = bundle.media.find((item) => item.bytes > maxFileBytes)
211
- if (oversized) {
212
- return `Media file is too large: ${oversized.ref}. The per-file limit is ${Math.round(maxFileBytes / 1024 / 1024)} MB.`
213
- }
214
- return ""
215
- }
216
-
217
- async function uploadBundle(registryBase, token, bundle) {
218
- const form = new FormData()
219
- const metadata = JSON.stringify({
220
- title: bundle.title,
221
- body: bundle.body,
222
- app: bundle.appSlug || "",
223
- parent: bundle.parent || null,
224
- media: bundle.media.map((item) => ({ path: item.ref }))
225
- })
226
- form.append("metadata_b64", Buffer.from(metadata, "utf8").toString("base64"))
227
- for (const item of bundle.media) {
228
- form.append("files", fs.createReadStream(item.path), {
229
- filename: path.basename(item.ref),
230
- contentType: mime.lookup(item.path) || "application/octet-stream",
231
- knownLength: item.bytes
232
- })
233
- }
234
- const endpoint = `${registryBase}/registry-bridge/draft-imports`
235
- const headers = {
236
- Authorization: `Bearer ${token}`,
237
- ...form.getHeaders()
238
- }
239
- const contentLength = await new Promise((resolve) => {
240
- form.getLength((error, length) => resolve(error ? null : length))
241
- })
242
- if (Number.isFinite(contentLength)) {
243
- headers["Content-Length"] = contentLength
244
- }
245
- console.log("[draft-import] request", {
246
- endpoint,
247
- media: bundle.media.length,
248
- contentLength: Number.isFinite(contentLength) ? contentLength : null
249
- })
250
- const response = await axios.post(endpoint, form, {
251
- timeout: 180000,
252
- maxContentLength: Infinity,
253
- maxBodyLength: Infinity,
254
- validateStatus: () => true,
255
- headers
256
- })
257
- if (response.status < 200 || response.status >= 300) {
258
- const error = new Error(
259
- response.data && response.data.error
260
- ? String(response.data.error)
261
- : `Registry upload failed with status ${response.status}.`
262
- )
263
- error.status = response.status
264
- error.registryEndpoint = endpoint
265
- error.responseData = response.data
266
- throw error
267
- }
268
- return response.data || {}
269
- }
270
-
271
- async function uploadDraftFromRequest(notes, query, token, registryBase, options = {}) {
272
- const item = await findNoteById(notes, query.draft)
273
- if (!item) {
274
- const error = new Error("The local note is no longer available.")
275
- error.status = 404
276
- throw error
277
- }
278
- const bundle = await buildDraftBundle(item, query)
279
- const problem = preflightBundle(bundle, options)
280
- if (problem) {
281
- const error = new Error(problem)
282
- error.status = 400
283
- throw error
284
- }
285
- return uploadBundle(registryBase, token, bundle)
286
- }
287
-
288
- function registerDraftImportRoutes(app, options = {}) {
289
- const notes = options.notes
290
- if (!notes) {
291
- throw new Error("notes is required")
292
- }
293
- const defaultRegistryUrl = options.defaultRegistryUrl || "https://beta.pinokio.co"
294
- const router = express.Router()
295
-
296
- router.get("/registry/draft-import/authorize-url", asyncHandler(async (req, res) => {
297
- const item = await findNoteById(notes, req.query.draft)
298
- if (!item) {
299
- return res.status(404).json({ error: "The local note is no longer available." })
300
- }
301
- const bundle = await buildDraftBundle(item, req.query)
302
- const problem = preflightBundle(bundle, options)
303
- if (problem) {
304
- return res.status(400).json({ error: problem })
305
- }
306
- const registryBase = normalizeRegistryBase(req.query.registry, defaultRegistryUrl)
307
- if (!registryBase) {
308
- return res.status(400).json({ error: "The registry URL is invalid." })
309
- }
310
- const authorizeUrl = buildAuthorizeUrl(req, registryBase, item, bundle)
311
- res.setHeader("Cache-Control", "no-store")
312
- return res.json({
313
- draftId: item.id,
314
- authorizeUrl: authorizeUrl.toString()
315
- })
316
- }))
317
-
318
- router.get("/registry/draft-import/start", asyncHandler(async (req, res) => {
319
- const item = await findNoteById(notes, req.query.draft)
320
- if (!item) {
321
- return renderMessage(res, 404, "Note not found", "The local note is no longer available.")
322
- }
323
- const bundle = await buildDraftBundle(item, req.query)
324
- const problem = preflightBundle(bundle, options)
325
- if (problem) {
326
- return renderMessage(res, 400, "Note is not ready", problem)
327
- }
328
- const registryBase = normalizeRegistryBase(req.query.registry, defaultRegistryUrl)
329
- if (!registryBase) {
330
- return renderMessage(res, 400, "Registry unavailable", "The registry URL is invalid.")
331
- }
332
- const authorizeUrl = buildAuthorizeUrl(req, registryBase, item, bundle)
333
- res.setHeader("Cache-Control", "no-store")
334
- res.setHeader("Cross-Origin-Opener-Policy", "unsafe-none")
335
- return renderImportLauncher(res, {
336
- authorizeUrl: authorizeUrl.toString(),
337
- autoOpen: req.query.auto === "1"
338
- })
339
- }))
340
-
341
- router.post("/registry/draft-import/complete", asyncHandler(async (req, res) => {
342
- const token = String(req.body && req.body.token || "").trim()
343
- if (!token) {
344
- return res.status(400).json({ error: "Missing registry token." })
345
- }
346
- const registryBase = normalizeRegistryBase(req.body && req.body.registry, defaultRegistryUrl)
347
- if (!registryBase) {
348
- return res.status(400).json({ error: "The registry URL is invalid." })
349
- }
350
- try {
351
- console.log("[draft-import] uploading", {
352
- draft: req.body && req.body.draft,
353
- registry: registryBase,
354
- app: req.body && req.body.app ? String(req.body.app) : ""
355
- })
356
- const result = await uploadDraftFromRequest(
357
- notes,
358
- { draft: req.body && req.body.draft, app: req.body && req.body.app },
359
- token,
360
- registryBase,
361
- options
362
- )
363
- if (result && result.editUrl) {
364
- return res.json({ ok: true, editUrl: String(result.editUrl) })
365
- }
366
- return res.json({ ok: true, editUrl: registryBase })
367
- } catch (error) {
368
- const response = error && error.response
369
- const status = response && response.status ? response.status : (error && error.status ? error.status : 500)
370
- const endpoint = error && error.registryEndpoint ? error.registryEndpoint : `${registryBase}/registry-bridge/draft-imports`
371
- const responseData = response && response.data ? response.data : (error && error.responseData ? error.responseData : null)
372
- console.warn("[draft-import] upload failed", {
373
- status,
374
- endpoint,
375
- error: error && error.message ? error.message : "Upload failed.",
376
- response: typeof responseData === "string" ? responseData.slice(0, 500) : responseData
377
- })
378
- const message = response && response.data && response.data.error
379
- ? response.data.error
380
- : (error && error.message ? error.message : "Upload failed.")
381
- return res.status(status).json({ error: message, status, endpoint })
382
- }
383
- }))
384
-
385
- router.get("/registry/draft-import/callback", asyncHandler(async (req, res) => {
386
- const token = String(req.query.token || "").trim()
387
- if (!token) {
388
- return renderMessage(res, 400, "Missing token", "The registry did not return an import token.")
389
- }
390
- const registryBase = normalizeRegistryBase(req.query.registry, defaultRegistryUrl)
391
- if (!registryBase) {
392
- return renderMessage(res, 400, "Registry unavailable", "The registry URL is invalid.")
393
- }
394
- try {
395
- const result = await uploadDraftFromRequest(notes, req.query, token, registryBase, options)
396
- if (result && result.editUrl) {
397
- return res.redirect(String(result.editUrl))
398
- }
399
- return renderMessage(res, 200, "Draft imported", "The registry accepted the draft.")
400
- } catch (error) {
401
- const response = error && error.response
402
- const message = response && response.data && response.data.error
403
- ? response.data.error
404
- : (error && error.message ? error.message : "Upload failed.")
405
- return renderMessage(res, response && response.status ? response.status : 500, "Import failed", message)
406
- }
407
- }))
408
-
409
- app.use(router)
410
- }
411
-
412
- module.exports = registerDraftImportRoutes
@@ -1,156 +0,0 @@
1
- const express = require("express")
2
- const path = require("path")
3
- const registerDraftImportRoutes = require("./registry_import")
4
-
5
- function asyncHandler(fn) {
6
- return (req, res, next) => {
7
- Promise.resolve(fn(req, res, next)).catch(next)
8
- }
9
- }
10
-
11
- function isInside(candidate, parent) {
12
- const relative = path.relative(parent, candidate)
13
- return relative === "" || (!!relative && !relative.startsWith("..") && !path.isAbsolute(relative))
14
- }
15
-
16
- function parsePublishConfig(value) {
17
- if (typeof value !== "string" || !value.trim()) {
18
- return null
19
- }
20
- let parsed
21
- try {
22
- parsed = JSON.parse(value)
23
- } catch (_) {
24
- return null
25
- }
26
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
27
- return null
28
- }
29
- const target = String(parsed.target || "").trim().toLowerCase()
30
- const type = String(parsed.type || "post").trim().toLowerCase()
31
- if (target !== "registry" || type !== "post") {
32
- return null
33
- }
34
- const publish = { target: "registry", type: "post" }
35
- const parent = parsed.parent && typeof parsed.parent === "object" && !Array.isArray(parsed.parent)
36
- ? parsed.parent
37
- : null
38
- const parentType = parent ? String(parent.type || "").trim().toLowerCase() : ""
39
- const parentUrl = parent ? String(parent.url || parent.repoUrl || "").trim() : ""
40
- if (parentType === "app" && parentUrl) {
41
- publish.parent = { type: "app", url: parentUrl }
42
- }
43
- return publish
44
- }
45
-
46
- function registerNoteRoutes(app, options = {}) {
47
- const notes = options.notes
48
- if (!notes) {
49
- throw new Error("notes is required")
50
- }
51
- const kernel = options.kernel
52
-
53
- const router = express.Router()
54
- router.get("/notes", asyncHandler(async (req, res) => {
55
- const cwd = typeof req.query.cwd === "string" ? req.query.cwd : ""
56
- if (cwd && typeof notes.inspectWorkspace === "function") {
57
- const resolvedCwd = path.resolve(cwd)
58
- const home = kernel && kernel.homedir ? path.resolve(kernel.homedir) : ""
59
- if (!home || isInside(resolvedCwd, home)) {
60
- const publish = parsePublishConfig(req.query.publish)
61
- const note = publish ? { publish } : undefined
62
- await notes.inspectWorkspace({ cwd: resolvedCwd, note }).catch(() => null)
63
- }
64
- }
65
- const items = await notes.listPending({ cwd })
66
- res.json({
67
- ok: true,
68
- items
69
- })
70
- }))
71
-
72
- router.get("/notes/:id/media/:index", asyncHandler(async (req, res) => {
73
- const item = typeof notes.getPendingById === "function"
74
- ? await notes.getPendingById(req.params.id)
75
- : null
76
- if (!item) {
77
- res.status(404).send("Note not found")
78
- return
79
- }
80
- const index = Number(req.params.index)
81
- const media = Array.isArray(item.media) && Number.isInteger(index)
82
- ? item.media[index]
83
- : null
84
- if (!media || !media.exists || !media.path) {
85
- res.status(404).send("Media not found")
86
- return
87
- }
88
- const filePath = path.resolve(media.path)
89
- const basePath = path.resolve(item.resultDir)
90
- const relative = path.relative(basePath, filePath)
91
- if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
92
- res.status(403).send("Media path is outside the note")
93
- return
94
- }
95
- res.setHeader("Cache-Control", "no-store")
96
- res.sendFile(filePath)
97
- }))
98
-
99
- router.put("/notes/:id", express.text({ type: "*/*", limit: "6mb" }), asyncHandler(async (req, res) => {
100
- if (typeof notes.savePendingById !== "function") {
101
- res.status(501).json({ ok: false, error: "Note editing is unavailable." })
102
- return
103
- }
104
- const markdown = typeof req.body === "string"
105
- ? req.body
106
- : (req.body && typeof req.body.markdown === "string"
107
- ? req.body.markdown
108
- : null)
109
- if (markdown === null) {
110
- res.status(400).json({ ok: false, error: "markdown is required" })
111
- return
112
- }
113
- const revision = typeof req.get("x-pinokio-note-revision") === "string"
114
- ? req.get("x-pinokio-note-revision")
115
- : (req.body && typeof req.body.revision === "string"
116
- ? req.body.revision
117
- : "")
118
- try {
119
- const item = await notes.savePendingById(req.params.id, { markdown, revision })
120
- if (!item) {
121
- res.status(404).json({ ok: false, error: "Note not found" })
122
- return
123
- }
124
- res.json({ ok: true, item })
125
- } catch (error) {
126
- if (error && error.code === "NOTE_CONFLICT") {
127
- res.status(409).json({
128
- ok: false,
129
- error: error.message,
130
- item: error.item || null
131
- })
132
- return
133
- }
134
- if (error && (error.code === "NOTE_TOO_LARGE" || error.code === "NOTE_INVALID_PATH")) {
135
- res.status(400).json({ ok: false, error: error.message })
136
- return
137
- }
138
- throw error
139
- }
140
- }))
141
-
142
- router.get("/notes.js", (req, res) => {
143
- res.setHeader("Cache-Control", "no-store")
144
- res.sendFile(path.resolve(__dirname, "public", "notes.js"))
145
- })
146
-
147
- router.get("/notes.css", (req, res) => {
148
- res.setHeader("Cache-Control", "no-store")
149
- res.sendFile(path.resolve(__dirname, "public", "notes.css"))
150
- })
151
-
152
- app.use(router)
153
- registerDraftImportRoutes(app, options)
154
- }
155
-
156
- module.exports = registerNoteRoutes