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.
- package/Dockerfile +2 -0
- package/kernel/api/index.js +13 -179
- package/kernel/api/process/index.js +44 -99
- package/kernel/bin/conda-python.js +30 -0
- package/kernel/bin/conda.js +22 -3
- package/kernel/bin/huggingface.js +1 -1
- package/kernel/bin/index.js +11 -1
- 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,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, "<")
|
|
23
|
-
.replace(/>/g, ">")
|
|
24
|
-
.replace(/"/g, """)
|
|
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
|