pinokiod 7.2.7 → 7.2.9
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/api/index.js +28 -0
- package/kernel/api/shell_run_template.js +273 -0
- package/kernel/index.js +2 -0
- package/kernel/shell.js +21 -2
- package/kernel/watch/context.js +42 -0
- package/kernel/watch/drivers/fs.js +71 -0
- package/kernel/watch/drivers/poll.js +33 -0
- package/kernel/watch/index.js +158 -0
- package/package.json +1 -1
- package/server/features/drafts/index.js +41 -0
- package/server/features/drafts/parser.js +169 -0
- package/server/features/drafts/public/drafts.js +1546 -0
- package/server/features/drafts/registry_import.js +427 -0
- package/server/features/drafts/routes.js +68 -0
- package/server/features/drafts/service.js +261 -0
- package/server/features/drafts/watcher.js +76 -0
- package/server/features/index.js +13 -0
- package/server/index.js +56 -7
- package/server/lib/workspace_catalog.js +151 -0
- package/server/lib/workspace_runtime.js +390 -0
- package/server/public/common.js +8 -0
- package/server/routes/workspaces.js +44 -0
- package/server/socket.js +22 -11
- package/server/views/app.ejs +159 -1
- package/server/views/partials/main_sidebar.ejs +1 -0
- package/server/views/partials/workspace_row.ejs +61 -0
- package/server/views/terminal.ejs +8 -0
- package/server/views/terminals.ejs +1 -0
- package/server/views/workspaces.ejs +812 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
const path = require("path")
|
|
2
|
+
const WatchContext = require("./context")
|
|
3
|
+
|
|
4
|
+
class WatchManager {
|
|
5
|
+
constructor(kernel) {
|
|
6
|
+
this.kernel = kernel
|
|
7
|
+
this.handlers = new Map()
|
|
8
|
+
this.sessions = new Map()
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
registerHandler(name, handler) {
|
|
12
|
+
const normalized = typeof name === "string" ? name.trim() : ""
|
|
13
|
+
if (!normalized) {
|
|
14
|
+
throw new Error("watch handler name is required")
|
|
15
|
+
}
|
|
16
|
+
this.handlers.set(normalized, handler)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
hasHandler(script, handlerName) {
|
|
20
|
+
const watches = script && Array.isArray(script.watch) ? script.watch : []
|
|
21
|
+
return watches.some((watch) => watch && watch.handler === handlerName)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
renderDeclaration(raw, memory) {
|
|
25
|
+
let rendered = raw
|
|
26
|
+
let pass = 0
|
|
27
|
+
while (true) {
|
|
28
|
+
rendered = this.kernel.template.render(rendered, memory)
|
|
29
|
+
if (this.kernel.template.istemplate(rendered)) {
|
|
30
|
+
pass += 1
|
|
31
|
+
if (pass >= 4) break
|
|
32
|
+
} else {
|
|
33
|
+
break
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return this.kernel.template.flatten(rendered)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
buildMemory({ request, script, cwd, dirname, input, args }) {
|
|
40
|
+
return {
|
|
41
|
+
script: this.kernel.script,
|
|
42
|
+
input,
|
|
43
|
+
args,
|
|
44
|
+
cwd,
|
|
45
|
+
dirname,
|
|
46
|
+
uri: request.uri,
|
|
47
|
+
self: script,
|
|
48
|
+
kernel: this.kernel,
|
|
49
|
+
...this.kernel.vars
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async resolveExternalHandler(ctx, uri) {
|
|
54
|
+
const modpath = ctx.resolveModule(uri)
|
|
55
|
+
const loaded = await this.kernel.loader.load(modpath)
|
|
56
|
+
let handler = loaded && loaded.resolved
|
|
57
|
+
if (typeof handler === "function") {
|
|
58
|
+
handler = new handler()
|
|
59
|
+
}
|
|
60
|
+
return handler
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async startForScript(options = {}) {
|
|
64
|
+
const script = options.script
|
|
65
|
+
const declarations = script && Array.isArray(script.watch) ? script.watch : []
|
|
66
|
+
if (declarations.length === 0) {
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const id = options.id
|
|
71
|
+
if (!id) {
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
await this.stop(id)
|
|
75
|
+
|
|
76
|
+
const input = options.input || {}
|
|
77
|
+
const args = options.args || input
|
|
78
|
+
const cwd = path.resolve(options.cwd)
|
|
79
|
+
const dirname = path.resolve(options.dirname || options.cwd)
|
|
80
|
+
const memory = this.buildMemory({
|
|
81
|
+
request: options.request || {},
|
|
82
|
+
script,
|
|
83
|
+
cwd,
|
|
84
|
+
dirname,
|
|
85
|
+
input,
|
|
86
|
+
args
|
|
87
|
+
})
|
|
88
|
+
const disposers = []
|
|
89
|
+
|
|
90
|
+
for (const rawDeclaration of declarations) {
|
|
91
|
+
if (!rawDeclaration || typeof rawDeclaration !== "object") {
|
|
92
|
+
continue
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
const declaration = this.renderDeclaration(rawDeclaration, memory)
|
|
96
|
+
const ctx = new WatchContext({
|
|
97
|
+
kernel: this.kernel,
|
|
98
|
+
manager: this,
|
|
99
|
+
id,
|
|
100
|
+
cwd,
|
|
101
|
+
dirname,
|
|
102
|
+
request: options.request,
|
|
103
|
+
script,
|
|
104
|
+
declaration,
|
|
105
|
+
input,
|
|
106
|
+
args
|
|
107
|
+
})
|
|
108
|
+
const methodName = typeof declaration.method === "string" ? declaration.method.trim() : ""
|
|
109
|
+
let handler = null
|
|
110
|
+
if (declaration.handler) {
|
|
111
|
+
handler = this.handlers.get(String(declaration.handler).trim())
|
|
112
|
+
} else if (declaration.uri) {
|
|
113
|
+
handler = await this.resolveExternalHandler(ctx, declaration.uri)
|
|
114
|
+
}
|
|
115
|
+
if (!handler || !methodName || typeof handler[methodName] !== "function") {
|
|
116
|
+
console.warn("[watch] handler not found", declaration)
|
|
117
|
+
continue
|
|
118
|
+
}
|
|
119
|
+
const cleanup = await handler[methodName](ctx, declaration.params || {})
|
|
120
|
+
if (cleanup) {
|
|
121
|
+
disposers.push(cleanup)
|
|
122
|
+
}
|
|
123
|
+
} catch (error) {
|
|
124
|
+
console.warn("[watch] failed to start", error && error.message ? error.message : error)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (disposers.length > 0) {
|
|
129
|
+
this.sessions.set(id, disposers)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async stop(id) {
|
|
134
|
+
const normalized = typeof id === "string" ? id : ""
|
|
135
|
+
if (!normalized || !this.sessions.has(normalized)) {
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
const disposers = this.sessions.get(normalized) || []
|
|
139
|
+
this.sessions.delete(normalized)
|
|
140
|
+
for (const disposer of disposers.reverse()) {
|
|
141
|
+
try {
|
|
142
|
+
if (typeof disposer === "function") {
|
|
143
|
+
await disposer()
|
|
144
|
+
} else if (disposer && typeof disposer.stop === "function") {
|
|
145
|
+
await disposer.stop()
|
|
146
|
+
} else if (disposer && typeof disposer.dispose === "function") {
|
|
147
|
+
await disposer.dispose()
|
|
148
|
+
} else if (disposer && typeof disposer.unsubscribe === "function") {
|
|
149
|
+
await disposer.unsubscribe()
|
|
150
|
+
}
|
|
151
|
+
} catch (error) {
|
|
152
|
+
console.warn("[watch] cleanup failed", error && error.message ? error.message : error)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
module.exports = WatchManager
|
package/package.json
CHANGED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const { createDraftService } = require("./service")
|
|
2
|
+
const registerDraftRoutes = require("./routes")
|
|
3
|
+
const DraftWatcher = require("./watcher")
|
|
4
|
+
|
|
5
|
+
function createDraftFeature(options = {}) {
|
|
6
|
+
const { app, kernel } = options
|
|
7
|
+
if (!app) {
|
|
8
|
+
throw new Error("app is required")
|
|
9
|
+
}
|
|
10
|
+
if (!kernel) {
|
|
11
|
+
throw new Error("kernel is required")
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const service = createDraftService({
|
|
15
|
+
kernel,
|
|
16
|
+
taskWorkspaceLinks: options.taskWorkspaceLinks
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
registerDraftRoutes(app, {
|
|
20
|
+
...options,
|
|
21
|
+
drafts: service
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
if (kernel.watch && typeof kernel.watch.registerHandler === "function") {
|
|
25
|
+
kernel.watch.registerHandler("draft", new DraftWatcher({ drafts: service }))
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
service,
|
|
30
|
+
async start() {
|
|
31
|
+
await service.start()
|
|
32
|
+
},
|
|
33
|
+
async stop() {
|
|
34
|
+
await service.stop()
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
module.exports = {
|
|
40
|
+
createDraftFeature
|
|
41
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
const fs = require("fs")
|
|
2
|
+
const path = require("path")
|
|
3
|
+
|
|
4
|
+
const RESULT_RELATIVE_DIR = path.join(".pinokio", "draft")
|
|
5
|
+
const POST_FILENAME = "post.md"
|
|
6
|
+
const METADATA_FILENAME = "pinokio.json"
|
|
7
|
+
const DEFAULT_READY_FILENAME = METADATA_FILENAME
|
|
8
|
+
const PREVIEW_CHARS = 1200
|
|
9
|
+
const MEDIA_EXTENSIONS = new Set([
|
|
10
|
+
".apng",
|
|
11
|
+
".avif",
|
|
12
|
+
".gif",
|
|
13
|
+
".jpeg",
|
|
14
|
+
".jpg",
|
|
15
|
+
".m4a",
|
|
16
|
+
".mp3",
|
|
17
|
+
".mp4",
|
|
18
|
+
".ogg",
|
|
19
|
+
".png",
|
|
20
|
+
".svg",
|
|
21
|
+
".wav",
|
|
22
|
+
".webm",
|
|
23
|
+
".webp"
|
|
24
|
+
])
|
|
25
|
+
|
|
26
|
+
function normalizeWhitespace(value) {
|
|
27
|
+
return String(value || "").replace(/\s+/g, " ").trim()
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function extractTitle(markdown, workspaceName) {
|
|
31
|
+
const lines = String(markdown || "").split(/\r?\n/)
|
|
32
|
+
for (const line of lines) {
|
|
33
|
+
const match = line.match(/^#\s+(.+?)\s*#*\s*$/)
|
|
34
|
+
if (match && match[1]) {
|
|
35
|
+
return normalizeWhitespace(match[1]).slice(0, 140) || "Draft"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return workspaceName ? `Draft for ${workspaceName}` : "Draft"
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function normalizeTitle(value) {
|
|
42
|
+
return normalizeWhitespace(value).slice(0, 160)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function parseDraftMetadata(raw) {
|
|
46
|
+
const parsed = JSON.parse(String(raw || ""))
|
|
47
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
48
|
+
return {}
|
|
49
|
+
}
|
|
50
|
+
const metadata = { ...parsed }
|
|
51
|
+
if (typeof metadata.title === "string") {
|
|
52
|
+
metadata.title = normalizeTitle(metadata.title)
|
|
53
|
+
} else {
|
|
54
|
+
delete metadata.title
|
|
55
|
+
}
|
|
56
|
+
return metadata
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function extractTitleAndBody(markdown, fallbackTitle) {
|
|
60
|
+
const lines = String(markdown || "").split(/\r?\n/)
|
|
61
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
62
|
+
const match = lines[i].match(/^#\s+(.+?)\s*#*\s*$/)
|
|
63
|
+
if (!match || !match[1]) continue
|
|
64
|
+
const title = normalizeWhitespace(match[1]).slice(0, 160)
|
|
65
|
+
const bodyLines = [...lines.slice(0, i), ...lines.slice(i + 1)]
|
|
66
|
+
while (bodyLines.length && !bodyLines[0].trim()) bodyLines.shift()
|
|
67
|
+
return { title: title || fallbackTitle, body: bodyLines.join("\n").trim() }
|
|
68
|
+
}
|
|
69
|
+
return { title: fallbackTitle, body: String(markdown || "").trim() }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function buildExcerpt(markdown) {
|
|
73
|
+
const stripped = String(markdown || "")
|
|
74
|
+
.replace(/```[\s\S]*?```/g, " ")
|
|
75
|
+
.replace(/!\[[^\]]*]\([^)]+\)/g, " ")
|
|
76
|
+
.replace(/\[[^\]]+]\([^)]+\)/g, (match) => {
|
|
77
|
+
const label = match.match(/^\[([^\]]+)]/)
|
|
78
|
+
return label && label[1] ? ` ${label[1]} ` : " "
|
|
79
|
+
})
|
|
80
|
+
.replace(/^#+\s+/gm, "")
|
|
81
|
+
.replace(/[*_`>#-]/g, " ")
|
|
82
|
+
return normalizeWhitespace(stripped).slice(0, PREVIEW_CHARS)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function isExternalRef(value) {
|
|
86
|
+
return /^(?:[a-z][a-z0-9+.-]*:|\/\/|#)/i.test(value)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function normalizeMarkdownRef(value) {
|
|
90
|
+
const raw = String(value || "").trim().replace(/^<|>$/g, "")
|
|
91
|
+
if (!raw || raw.includes("\0") || isExternalRef(raw) || path.isAbsolute(raw)) {
|
|
92
|
+
return ""
|
|
93
|
+
}
|
|
94
|
+
const withoutHash = raw.split("#")[0]
|
|
95
|
+
const withoutQuery = withoutHash.split("?")[0]
|
|
96
|
+
if (!withoutQuery) {
|
|
97
|
+
return ""
|
|
98
|
+
}
|
|
99
|
+
let decoded = withoutQuery
|
|
100
|
+
try {
|
|
101
|
+
decoded = decodeURIComponent(withoutQuery)
|
|
102
|
+
} catch (_) {
|
|
103
|
+
}
|
|
104
|
+
const normalized = path.posix.normalize(decoded.replace(/\\/g, "/"))
|
|
105
|
+
if (!normalized || normalized === "." || normalized === ".." || normalized.startsWith("../") || normalized.includes("/../")) {
|
|
106
|
+
return ""
|
|
107
|
+
}
|
|
108
|
+
return normalized
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function collectMarkdownRefs(markdown) {
|
|
112
|
+
const refs = []
|
|
113
|
+
const seen = new Set()
|
|
114
|
+
const patterns = [
|
|
115
|
+
/!\[[^\]]*]\(([^)\s]+)(?:\s+["'][^"']*["'])?\)/g,
|
|
116
|
+
/\[(?:video|audio|media|image|screenshot|file|asset)[^\]]*]\(([^)\s]+)(?:\s+["'][^"']*["'])?\)/gi,
|
|
117
|
+
/\[[^\]]+]\(([^)\s]+)(?:\s+["'][^"']*["'])?\)/g
|
|
118
|
+
]
|
|
119
|
+
for (const pattern of patterns) {
|
|
120
|
+
let match = null
|
|
121
|
+
while ((match = pattern.exec(markdown))) {
|
|
122
|
+
const ref = normalizeMarkdownRef(match[1])
|
|
123
|
+
if (!ref || seen.has(ref)) continue
|
|
124
|
+
seen.add(ref)
|
|
125
|
+
refs.push(ref)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return refs
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function describeMediaRefs(markdown, baseDir, options = {}) {
|
|
132
|
+
const refs = collectMarkdownRefs(markdown)
|
|
133
|
+
const media = []
|
|
134
|
+
for (const ref of refs) {
|
|
135
|
+
const ext = path.extname(ref).toLowerCase()
|
|
136
|
+
if (options.mediaOnly !== false && !MEDIA_EXTENSIONS.has(ext)) {
|
|
137
|
+
continue
|
|
138
|
+
}
|
|
139
|
+
const filePath = path.resolve(baseDir, ref)
|
|
140
|
+
const relative = path.relative(baseDir, filePath)
|
|
141
|
+
if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
142
|
+
continue
|
|
143
|
+
}
|
|
144
|
+
const stats = await fs.promises.stat(filePath).catch(() => null)
|
|
145
|
+
media.push({
|
|
146
|
+
ref,
|
|
147
|
+
path: filePath,
|
|
148
|
+
bytes: stats && stats.isFile() ? stats.size : 0,
|
|
149
|
+
mtimeMs: stats && stats.isFile() ? stats.mtimeMs : 0,
|
|
150
|
+
exists: Boolean(stats && stats.isFile())
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
return media
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
module.exports = {
|
|
157
|
+
METADATA_FILENAME,
|
|
158
|
+
DEFAULT_READY_FILENAME,
|
|
159
|
+
RESULT_RELATIVE_DIR,
|
|
160
|
+
POST_FILENAME,
|
|
161
|
+
buildExcerpt,
|
|
162
|
+
collectMarkdownRefs,
|
|
163
|
+
describeMediaRefs,
|
|
164
|
+
extractTitle,
|
|
165
|
+
extractTitleAndBody,
|
|
166
|
+
normalizeMarkdownRef,
|
|
167
|
+
normalizeTitle,
|
|
168
|
+
parseDraftMetadata
|
|
169
|
+
}
|