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,261 @@
|
|
|
1
|
+
const fs = require("fs")
|
|
2
|
+
const path = require("path")
|
|
3
|
+
const crypto = require("crypto")
|
|
4
|
+
const {
|
|
5
|
+
RESULT_RELATIVE_DIR,
|
|
6
|
+
POST_FILENAME,
|
|
7
|
+
DEFAULT_READY_FILENAME,
|
|
8
|
+
buildExcerpt,
|
|
9
|
+
describeMediaRefs,
|
|
10
|
+
extractTitle,
|
|
11
|
+
parseDraftMetadata
|
|
12
|
+
} = require("./parser")
|
|
13
|
+
|
|
14
|
+
const STATE_FILENAME = "drafts.json"
|
|
15
|
+
const MAX_PREVIEW_BYTES = 256 * 1024
|
|
16
|
+
|
|
17
|
+
function createHash(value) {
|
|
18
|
+
return crypto.createHash("sha256").update(String(value)).digest("hex").slice(0, 24)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function dismissalKey(id, revision) {
|
|
22
|
+
const normalizedId = typeof id === "string" ? id.trim() : ""
|
|
23
|
+
const normalizedRevision = typeof revision === "string" ? revision.trim() : ""
|
|
24
|
+
return normalizedRevision ? `${normalizedId}:${normalizedRevision}` : normalizedId
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function clonePlainObject(value) {
|
|
28
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
29
|
+
return null
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
return JSON.parse(JSON.stringify(value))
|
|
33
|
+
} catch (_) {
|
|
34
|
+
return null
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function normalizeRelativePath(value, fallback) {
|
|
39
|
+
const raw = String(value || fallback || "").trim().replace(/\\/g, "/")
|
|
40
|
+
if (raw.startsWith("/") || /^[a-zA-Z]:/.test(raw)) {
|
|
41
|
+
return fallback
|
|
42
|
+
}
|
|
43
|
+
const normalized = path.posix.normalize(raw)
|
|
44
|
+
if (!normalized || normalized === "." || normalized === ".." || normalized.startsWith("../") || normalized.includes("/../")) {
|
|
45
|
+
return fallback
|
|
46
|
+
}
|
|
47
|
+
return normalized
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function normalizeDraftConfig(config = {}) {
|
|
51
|
+
const params = config && typeof config === "object" ? config : {}
|
|
52
|
+
return {
|
|
53
|
+
path: normalizeRelativePath(params.path, RESULT_RELATIVE_DIR),
|
|
54
|
+
content: normalizeRelativePath(params.content || params.post, POST_FILENAME),
|
|
55
|
+
ready: normalizeRelativePath(params.ready, DEFAULT_READY_FILENAME),
|
|
56
|
+
description: typeof params.description === "string" ? params.description.trim() : "",
|
|
57
|
+
publish: clonePlainObject(params.publish)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function createDraftService({ kernel, taskWorkspaceLinks } = {}) {
|
|
62
|
+
if (!kernel) {
|
|
63
|
+
throw new Error("kernel is required")
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const statePath = () => path.resolve(kernel.path("tasks"), STATE_FILENAME)
|
|
67
|
+
const resultsByWorkspace = new Map()
|
|
68
|
+
const dismissedIds = new Set()
|
|
69
|
+
let started = false
|
|
70
|
+
let stateLoaded = false
|
|
71
|
+
|
|
72
|
+
async function ensureStateLoaded() {
|
|
73
|
+
if (stateLoaded) return
|
|
74
|
+
stateLoaded = true
|
|
75
|
+
try {
|
|
76
|
+
const raw = await fs.promises.readFile(statePath(), "utf8")
|
|
77
|
+
const parsed = JSON.parse(raw)
|
|
78
|
+
if (parsed && Array.isArray(parsed.dismissed)) {
|
|
79
|
+
parsed.dismissed.forEach((id) => {
|
|
80
|
+
if (typeof id === "string" && id.trim()) {
|
|
81
|
+
dismissedIds.add(id.trim())
|
|
82
|
+
}
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
} catch (_) {
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function saveState() {
|
|
90
|
+
await fs.promises.mkdir(path.dirname(statePath()), { recursive: true })
|
|
91
|
+
const payload = {
|
|
92
|
+
version: 1,
|
|
93
|
+
dismissed: Array.from(dismissedIds).slice(-500)
|
|
94
|
+
}
|
|
95
|
+
await fs.promises.writeFile(statePath(), JSON.stringify(payload, null, 2))
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function readMarkdownPreview(postPath) {
|
|
99
|
+
const handle = await fs.promises.open(postPath, "r")
|
|
100
|
+
try {
|
|
101
|
+
const buffer = Buffer.alloc(MAX_PREVIEW_BYTES)
|
|
102
|
+
const read = await handle.read(buffer, 0, MAX_PREVIEW_BYTES, 0)
|
|
103
|
+
return buffer.slice(0, read.bytesRead).toString("utf8")
|
|
104
|
+
} finally {
|
|
105
|
+
await handle.close().catch(() => {})
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function readDraftMetadata(metadataPath) {
|
|
110
|
+
if (path.extname(metadataPath).toLowerCase() !== ".json") {
|
|
111
|
+
return {}
|
|
112
|
+
}
|
|
113
|
+
const raw = await fs.promises.readFile(metadataPath, "utf8")
|
|
114
|
+
return parseDraftMetadata(raw)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function inspectWorkspace({ taskId, ref, cwd, draft } = {}) {
|
|
118
|
+
await ensureStateLoaded()
|
|
119
|
+
if (typeof cwd !== "string" || !cwd.trim()) {
|
|
120
|
+
return null
|
|
121
|
+
}
|
|
122
|
+
const workspacePath = path.resolve(cwd.trim())
|
|
123
|
+
const draftConfig = normalizeDraftConfig(draft)
|
|
124
|
+
const resultDir = path.resolve(workspacePath, draftConfig.path)
|
|
125
|
+
const readyPath = path.resolve(resultDir, draftConfig.ready)
|
|
126
|
+
const postPath = path.resolve(resultDir, draftConfig.content)
|
|
127
|
+
const readyStats = await fs.promises.stat(readyPath).catch(() => null)
|
|
128
|
+
const postStats = await fs.promises.stat(postPath).catch(() => null)
|
|
129
|
+
if (!readyStats || !readyStats.isFile() || !postStats || !postStats.isFile()) {
|
|
130
|
+
resultsByWorkspace.delete(workspacePath)
|
|
131
|
+
return null
|
|
132
|
+
}
|
|
133
|
+
let metadata = {}
|
|
134
|
+
try {
|
|
135
|
+
metadata = await readDraftMetadata(readyPath)
|
|
136
|
+
} catch (_) {
|
|
137
|
+
resultsByWorkspace.delete(workspacePath)
|
|
138
|
+
return null
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const markdown = await readMarkdownPreview(postPath)
|
|
142
|
+
const workspaceName = path.basename(workspacePath)
|
|
143
|
+
const media = await describeMediaRefs(markdown, resultDir)
|
|
144
|
+
const updatedAtMs = Math.max(readyStats.mtimeMs || 0, postStats.mtimeMs || 0)
|
|
145
|
+
const id = createHash(`${workspacePath}|${resultDir}|${postPath}|${readyPath}`)
|
|
146
|
+
const mediaRevision = media
|
|
147
|
+
.map((item) => `${item.ref}:${item.exists ? item.bytes : "missing"}:${item.mtimeMs || 0}`)
|
|
148
|
+
.join("|")
|
|
149
|
+
const revision = createHash(`${postStats.size}|${postStats.mtimeMs}|${readyStats.size}|${readyStats.mtimeMs}|${mediaRevision}`)
|
|
150
|
+
const result = {
|
|
151
|
+
id,
|
|
152
|
+
revision,
|
|
153
|
+
taskId,
|
|
154
|
+
ref,
|
|
155
|
+
cwd: workspacePath,
|
|
156
|
+
workspaceName,
|
|
157
|
+
title: metadata.title || extractTitle(markdown, workspaceName),
|
|
158
|
+
markdown,
|
|
159
|
+
excerpt: buildExcerpt(markdown),
|
|
160
|
+
resultDir,
|
|
161
|
+
postPath,
|
|
162
|
+
contentPath: postPath,
|
|
163
|
+
readyPath,
|
|
164
|
+
metadataPath: readyPath,
|
|
165
|
+
metadata,
|
|
166
|
+
publish: draftConfig.publish,
|
|
167
|
+
description: draftConfig.description,
|
|
168
|
+
postBytes: postStats.size,
|
|
169
|
+
media: media.map((item, index) => ({
|
|
170
|
+
index,
|
|
171
|
+
ref: item.ref,
|
|
172
|
+
path: item.path,
|
|
173
|
+
bytes: item.bytes,
|
|
174
|
+
mtimeMs: item.mtimeMs,
|
|
175
|
+
exists: item.exists,
|
|
176
|
+
ext: path.extname(item.ref || "").toLowerCase()
|
|
177
|
+
})),
|
|
178
|
+
mediaCount: media.length,
|
|
179
|
+
missingMediaCount: media.filter((item) => !item.exists).length,
|
|
180
|
+
mediaBytes: media.reduce((total, item) => total + (Number.isFinite(item.bytes) ? item.bytes : 0), 0),
|
|
181
|
+
updatedAt: new Date(updatedAtMs || Date.now()).toISOString()
|
|
182
|
+
}
|
|
183
|
+
resultsByWorkspace.set(workspacePath, result)
|
|
184
|
+
return result
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function trackWorkspace({ taskId, ref, cwd } = {}) {
|
|
188
|
+
let resolvedCwd = cwd
|
|
189
|
+
if (!resolvedCwd && ref && taskWorkspaceLinks && typeof taskWorkspaceLinks.resolveWorkspaceRef === "function") {
|
|
190
|
+
resolvedCwd = taskWorkspaceLinks.resolveWorkspaceRef(ref)
|
|
191
|
+
}
|
|
192
|
+
if (!resolvedCwd) {
|
|
193
|
+
return null
|
|
194
|
+
}
|
|
195
|
+
return inspectWorkspace({ taskId, ref, cwd: resolvedCwd })
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function listPending(options = {}) {
|
|
199
|
+
await ensureStateLoaded()
|
|
200
|
+
const filterCwd = typeof options.cwd === "string" && options.cwd.trim()
|
|
201
|
+
? path.resolve(options.cwd.trim())
|
|
202
|
+
: ""
|
|
203
|
+
return Array.from(resultsByWorkspace.values())
|
|
204
|
+
.filter((result) => !dismissedIds.has(dismissalKey(result.id, result.revision)) && !dismissedIds.has(result.id))
|
|
205
|
+
.filter((result) => !filterCwd || result.cwd === filterCwd)
|
|
206
|
+
.sort((a, b) => String(b.updatedAt).localeCompare(String(a.updatedAt)))
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function getPendingById(id) {
|
|
210
|
+
await ensureStateLoaded()
|
|
211
|
+
const normalizedId = typeof id === "string" ? id.trim() : ""
|
|
212
|
+
if (!normalizedId) {
|
|
213
|
+
return null
|
|
214
|
+
}
|
|
215
|
+
const result = Array.from(resultsByWorkspace.values()).find((item) => item.id === normalizedId) || null
|
|
216
|
+
if (!result || dismissedIds.has(dismissalKey(result.id, result.revision)) || dismissedIds.has(result.id)) {
|
|
217
|
+
return null
|
|
218
|
+
}
|
|
219
|
+
return result
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async function dismiss(id, revision) {
|
|
223
|
+
await ensureStateLoaded()
|
|
224
|
+
const normalizedId = typeof id === "string" ? id.trim() : ""
|
|
225
|
+
if (!normalizedId) {
|
|
226
|
+
return false
|
|
227
|
+
}
|
|
228
|
+
dismissedIds.add(dismissalKey(normalizedId, revision))
|
|
229
|
+
await saveState()
|
|
230
|
+
return true
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async function refreshLinkedWorkspaces() {
|
|
234
|
+
await ensureStateLoaded()
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async function start() {
|
|
238
|
+
if (started) return
|
|
239
|
+
started = true
|
|
240
|
+
await ensureStateLoaded()
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async function stop() {
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return {
|
|
247
|
+
RESULT_RELATIVE_DIR,
|
|
248
|
+
dismiss,
|
|
249
|
+
getPendingById,
|
|
250
|
+
inspectWorkspace,
|
|
251
|
+
listPending,
|
|
252
|
+
refreshLinkedWorkspaces,
|
|
253
|
+
start,
|
|
254
|
+
stop,
|
|
255
|
+
trackWorkspace
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
module.exports = {
|
|
260
|
+
createDraftService
|
|
261
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
const path = require("path")
|
|
2
|
+
|
|
3
|
+
function isInside(candidate, parent) {
|
|
4
|
+
const relative = path.relative(parent, candidate)
|
|
5
|
+
return relative === "" || (!!relative && !relative.startsWith("..") && !path.isAbsolute(relative))
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
class DraftWatcher {
|
|
9
|
+
constructor(options = {}) {
|
|
10
|
+
this.drafts = options.drafts
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async ready(ctx, params = {}) {
|
|
14
|
+
if (!this.drafts || typeof this.drafts.inspectWorkspace !== "function") {
|
|
15
|
+
throw new Error("draft service is unavailable")
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const draftDir = ctx.resolve(params.path || ".pinokio/draft")
|
|
19
|
+
const draftConfig = {
|
|
20
|
+
path: params.path || ".pinokio/draft",
|
|
21
|
+
content: params.content,
|
|
22
|
+
ready: params.ready,
|
|
23
|
+
description: params.description,
|
|
24
|
+
publish: params.publish
|
|
25
|
+
}
|
|
26
|
+
let timer = null
|
|
27
|
+
let disposed = false
|
|
28
|
+
|
|
29
|
+
const inspect = async () => {
|
|
30
|
+
if (disposed) return
|
|
31
|
+
await this.drafts.inspectWorkspace({ cwd: ctx.cwd, draft: draftConfig })
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const scheduleInspect = () => {
|
|
35
|
+
if (disposed) return
|
|
36
|
+
if (timer) clearTimeout(timer)
|
|
37
|
+
timer = setTimeout(() => {
|
|
38
|
+
timer = null
|
|
39
|
+
inspect().catch((error) => {
|
|
40
|
+
console.warn("[drafts] failed to inspect workspace", error && error.message ? error.message : error)
|
|
41
|
+
})
|
|
42
|
+
}, 250)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
await inspect()
|
|
46
|
+
const stopPoll = ctx.poll(params.interval || 1500, inspect, {
|
|
47
|
+
immediate: false,
|
|
48
|
+
onError: (error) => {
|
|
49
|
+
console.warn("[drafts] poll failed", error && error.message ? error.message : error)
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
const unsubscribe = await ctx.watch.fs(ctx.cwd, (events) => {
|
|
53
|
+
if (!Array.isArray(events)) return
|
|
54
|
+
if (events.some((event) => event && event.path && isInside(path.resolve(event.path), draftDir))) {
|
|
55
|
+
scheduleInspect()
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
return async () => {
|
|
60
|
+
if (timer) {
|
|
61
|
+
clearTimeout(timer)
|
|
62
|
+
timer = null
|
|
63
|
+
}
|
|
64
|
+
await this.drafts.inspectWorkspace({ cwd: ctx.cwd, draft: draftConfig }).catch(() => {})
|
|
65
|
+
disposed = true
|
|
66
|
+
if (typeof stopPoll === "function") {
|
|
67
|
+
await stopPoll()
|
|
68
|
+
}
|
|
69
|
+
if (typeof unsubscribe === "function") {
|
|
70
|
+
await unsubscribe()
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
module.exports = DraftWatcher
|
package/server/index.js
CHANGED
|
@@ -28,6 +28,8 @@ const system = require('systeminformation')
|
|
|
28
28
|
const serveIndex = require('./serveIndex')
|
|
29
29
|
const registerFileRoutes = require('./routes/files')
|
|
30
30
|
const registerAppRoutes = require('./routes/apps')
|
|
31
|
+
const registerWorkspacesRoutes = require('./routes/workspaces')
|
|
32
|
+
const { mountFeatures } = require('./features')
|
|
31
33
|
const Git = require("../kernel/git")
|
|
32
34
|
const TerminalApi = require('../kernel/api/terminal')
|
|
33
35
|
|
|
@@ -78,6 +80,8 @@ const { createDesktopEventRouter } = require("./lib/desktop_event_router")
|
|
|
78
80
|
const { createInjectRouter, resolveInjectList } = require("./lib/inject_router")
|
|
79
81
|
const { createTaskPackageService } = require("./lib/task_packages")
|
|
80
82
|
const { createTaskWorkspaceLinkService } = require("./lib/task_workspace_links")
|
|
83
|
+
const { createWorkspaceRuntimeService } = require("./lib/workspace_runtime")
|
|
84
|
+
const { createWorkspaceCatalogService } = require("./lib/workspace_catalog")
|
|
81
85
|
const { createContentValidationService } = require("./lib/content_validation")
|
|
82
86
|
const { buildSecureRouterDebugSnapshot, createSecureRouterDebugStore } = require("./lib/secure_router_debug")
|
|
83
87
|
const AppRegistryService = require("./lib/app_registry")
|
|
@@ -2865,11 +2869,19 @@ class Server {
|
|
|
2865
2869
|
const protectionPreference = protectionAppId && this.appPreferences && typeof this.appPreferences.getPreference === "function"
|
|
2866
2870
|
? await this.appPreferences.getPreference(protectionAppId)
|
|
2867
2871
|
: null
|
|
2872
|
+
const draftWatchEnabled = this.kernel.watch && typeof this.kernel.watch.hasHandler === "function"
|
|
2873
|
+
? this.kernel.watch.hasHandler(resolved, "draft")
|
|
2874
|
+
: false
|
|
2875
|
+
const draftWatchCwd = draftWatchEnabled
|
|
2876
|
+
? (req.query.cwd || path.dirname(filepath))
|
|
2877
|
+
: ""
|
|
2868
2878
|
const result = {
|
|
2869
2879
|
portal: this.portal,
|
|
2870
2880
|
projectName: (pathComponents.length > 0 ? pathComponents[0] : ''),
|
|
2871
2881
|
protection_app_id: protectionAppId,
|
|
2872
2882
|
protection_enabled: protectionPreference ? protectionPreference.protection_enabled !== false : false,
|
|
2883
|
+
draft_watch_enabled: draftWatchEnabled,
|
|
2884
|
+
draft_watch_cwd: draftWatchCwd,
|
|
2873
2885
|
kill_message,
|
|
2874
2886
|
callback,
|
|
2875
2887
|
callback_target,
|
|
@@ -8403,6 +8415,32 @@ class Server {
|
|
|
8403
8415
|
const taskWorkspaceLinks = createTaskWorkspaceLinkService({
|
|
8404
8416
|
kernel: this.kernel
|
|
8405
8417
|
})
|
|
8418
|
+
const workspaceRuntime = createWorkspaceRuntimeService({
|
|
8419
|
+
kernel: this.kernel
|
|
8420
|
+
})
|
|
8421
|
+
this.workspaceRuntime = workspaceRuntime
|
|
8422
|
+
const features = await mountFeatures({
|
|
8423
|
+
app: this.app,
|
|
8424
|
+
kernel: this.kernel,
|
|
8425
|
+
taskWorkspaceLinks,
|
|
8426
|
+
defaultRegistryUrl: DEFAULT_REGISTRY_URL
|
|
8427
|
+
})
|
|
8428
|
+
this.features = features
|
|
8429
|
+
const drafts = features.drafts.service
|
|
8430
|
+
this.drafts = drafts
|
|
8431
|
+
const workspaceCatalog = createWorkspaceCatalogService({
|
|
8432
|
+
kernel: this.kernel,
|
|
8433
|
+
workspaceRuntime,
|
|
8434
|
+
drafts
|
|
8435
|
+
})
|
|
8436
|
+
registerWorkspacesRoutes(this.app, {
|
|
8437
|
+
workspaceCatalog,
|
|
8438
|
+
composePeerAccessPayload: () => this.composePeerAccessPayload(),
|
|
8439
|
+
getTheme: () => this.theme,
|
|
8440
|
+
getPeers: () => this.getPeers(),
|
|
8441
|
+
getCurrentHost: () => this.kernel.peer.host,
|
|
8442
|
+
getPortal: () => this.portal
|
|
8443
|
+
})
|
|
8406
8444
|
const TASK_INPUT_NAME_PATTERN = /^[a-zA-Z0-9_][a-zA-Z0-9_.-]*$/
|
|
8407
8445
|
const suggestTaskFolderName = async (rootDir, preferredName) => {
|
|
8408
8446
|
const normalizedRoot = path.resolve(rootDir)
|
|
@@ -10073,7 +10111,8 @@ class Server {
|
|
|
10073
10111
|
}
|
|
10074
10112
|
}
|
|
10075
10113
|
|
|
10076
|
-
const
|
|
10114
|
+
const filledInputValues = filterFilledTaskInputValues(inputValues)
|
|
10115
|
+
const prompt = taskPackages.applyTemplateValues(task.template, filledInputValues).trim()
|
|
10077
10116
|
if (!prompt) {
|
|
10078
10117
|
await renderTaskLaunchPage(req, res, task, {
|
|
10079
10118
|
selectedTool,
|
|
@@ -10194,6 +10233,9 @@ class Server {
|
|
|
10194
10233
|
params.set("cwd", targetPath)
|
|
10195
10234
|
params.set("chrome", "full")
|
|
10196
10235
|
params.set("prompt", prompt)
|
|
10236
|
+
if (filledInputValues.url) {
|
|
10237
|
+
params.set("url", filledInputValues.url)
|
|
10238
|
+
}
|
|
10197
10239
|
res.redirect(`${pluginHref}?${params.toString()}`)
|
|
10198
10240
|
})
|
|
10199
10241
|
this.app.post("/task/start", handleTaskStartRequest)
|
|
@@ -10500,7 +10542,8 @@ class Server {
|
|
|
10500
10542
|
return
|
|
10501
10543
|
}
|
|
10502
10544
|
|
|
10503
|
-
const
|
|
10545
|
+
const filledInputValues = filterFilledTaskInputValues(inputValues)
|
|
10546
|
+
const prompt = taskPackages.applyTemplateValues(task.template, filledInputValues).trim()
|
|
10504
10547
|
if (!prompt) {
|
|
10505
10548
|
res.status(400).json({
|
|
10506
10549
|
ok: false,
|
|
@@ -10600,6 +10643,9 @@ class Server {
|
|
|
10600
10643
|
params.set("chrome", "full")
|
|
10601
10644
|
params.set("session", createUniversalLauncherSessionId())
|
|
10602
10645
|
params.set("prompt", prompt)
|
|
10646
|
+
if (filledInputValues.url) {
|
|
10647
|
+
params.set("url", filledInputValues.url)
|
|
10648
|
+
}
|
|
10603
10649
|
|
|
10604
10650
|
res.json({
|
|
10605
10651
|
ok: true,
|
|
@@ -11851,11 +11897,14 @@ class Server {
|
|
|
11851
11897
|
// but allow it when the request originates from the local machine
|
|
11852
11898
|
if (payload.audience === 'device' && typeof payload.device_id === 'string' && payload.device_id) {
|
|
11853
11899
|
try {
|
|
11854
|
-
|
|
11855
|
-
|
|
11856
|
-
|
|
11857
|
-
|
|
11858
|
-
|
|
11900
|
+
const remoteAddress = req.socket && req.socket.remoteAddress ? req.socket.remoteAddress : req.ip
|
|
11901
|
+
const isLocalRequest = this.socket && typeof this.socket.isLocalAddress === 'function'
|
|
11902
|
+
? this.socket.isLocalAddress(remoteAddress)
|
|
11903
|
+
: false
|
|
11904
|
+
const isLocalDevice = this.socket && typeof this.socket.isLocalDevice === 'function'
|
|
11905
|
+
? this.socket.isLocalDevice(payload.device_id)
|
|
11906
|
+
: false
|
|
11907
|
+
payload.host = !!(isLocalRequest || isLocalDevice)
|
|
11859
11908
|
} catch (_) {
|
|
11860
11909
|
payload.host = false
|
|
11861
11910
|
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
const fs = require("fs")
|
|
2
|
+
const path = require("path")
|
|
3
|
+
|
|
4
|
+
const SORT_MODES = new Set(["most_used", "last_opened", "az"])
|
|
5
|
+
|
|
6
|
+
function normalizeSortMode(sort) {
|
|
7
|
+
if (SORT_MODES.has(sort)) return sort
|
|
8
|
+
return "most_used"
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function normalizePathKey(filepath) {
|
|
12
|
+
const resolved = path.resolve(filepath).replace(/[\\/]+$/, "")
|
|
13
|
+
return process.platform === "win32" ? resolved.toLowerCase() : resolved
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function toRoutePath(filepath) {
|
|
17
|
+
const resolved = path.resolve(filepath).replace(/\\/g, "/")
|
|
18
|
+
const encoded = resolved
|
|
19
|
+
.split("/")
|
|
20
|
+
.map((segment, index) => {
|
|
21
|
+
if (index === 0 && segment === "") return ""
|
|
22
|
+
return encodeURIComponent(segment)
|
|
23
|
+
})
|
|
24
|
+
.join("/")
|
|
25
|
+
return encoded.startsWith("/") ? `/d${encoded}` : `/d/${encoded}`
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function latestTimestamp(values) {
|
|
29
|
+
return values.reduce((latest, value) => {
|
|
30
|
+
if (!value) return latest
|
|
31
|
+
const timestamp = typeof value === "number" ? value : new Date(value).getTime()
|
|
32
|
+
return Number.isFinite(timestamp) && timestamp > latest ? timestamp : latest
|
|
33
|
+
}, 0)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function sortWorkspaces(items, sort) {
|
|
37
|
+
const mode = normalizeSortMode(sort)
|
|
38
|
+
const sorted = [...items]
|
|
39
|
+
sorted.sort((a, b) => {
|
|
40
|
+
if (mode === "az") {
|
|
41
|
+
return a.name.localeCompare(b.name, undefined, { sensitivity: "base" })
|
|
42
|
+
}
|
|
43
|
+
if (mode === "last_opened") {
|
|
44
|
+
const delta = (b.lastOpenedAtMs || b.modifiedAtMs || 0) - (a.lastOpenedAtMs || a.modifiedAtMs || 0)
|
|
45
|
+
if (delta !== 0) return delta
|
|
46
|
+
return a.name.localeCompare(b.name, undefined, { sensitivity: "base" })
|
|
47
|
+
}
|
|
48
|
+
const usageDelta = (b.usageCount || 0) - (a.usageCount || 0)
|
|
49
|
+
if (usageDelta !== 0) return usageDelta
|
|
50
|
+
return a.name.localeCompare(b.name, undefined, { sensitivity: "base" })
|
|
51
|
+
})
|
|
52
|
+
return sorted
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function newestDraft(drafts) {
|
|
56
|
+
return [...drafts].sort((a, b) => {
|
|
57
|
+
return (new Date(b.updatedAt || 0).getTime() || 0) - (new Date(a.updatedAt || 0).getTime() || 0)
|
|
58
|
+
})[0] || null
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function createWorkspaceCatalogService({ kernel, workspaceRuntime, drafts }) {
|
|
62
|
+
async function list(options = {}) {
|
|
63
|
+
const sort = normalizeSortMode(options.sort)
|
|
64
|
+
const root = path.resolve(kernel.path("workspaces"))
|
|
65
|
+
const entries = await fs.promises.readdir(root, { withFileTypes: true }).catch(() => [])
|
|
66
|
+
const runtime = workspaceRuntime.list()
|
|
67
|
+
const liveByPath = new Map()
|
|
68
|
+
|
|
69
|
+
for (const group of runtime.workspaces || []) {
|
|
70
|
+
if (group.root !== "workspaces") continue
|
|
71
|
+
liveByPath.set(normalizePathKey(group.cwd), group)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const draftByPath = new Map()
|
|
75
|
+
const pendingDrafts = drafts ? await drafts.listPending({}).catch(() => []) : []
|
|
76
|
+
for (const draft of pendingDrafts) {
|
|
77
|
+
if (!draft.cwd) continue
|
|
78
|
+
const key = normalizePathKey(draft.cwd)
|
|
79
|
+
const list = draftByPath.get(key) || []
|
|
80
|
+
list.push(draft)
|
|
81
|
+
draftByPath.set(key, list)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const folders = entries.filter((entry) => entry.isDirectory())
|
|
85
|
+
const items = []
|
|
86
|
+
|
|
87
|
+
for (const entry of folders) {
|
|
88
|
+
const cwd = path.join(root, entry.name)
|
|
89
|
+
const stats = await fs.promises.stat(cwd).catch(() => null)
|
|
90
|
+
const key = normalizePathKey(cwd)
|
|
91
|
+
const live = liveByPath.get(key)
|
|
92
|
+
const shells = live?.shells || []
|
|
93
|
+
const scripts = live?.scripts || []
|
|
94
|
+
const workspaceDrafts = draftByPath.get(key) || []
|
|
95
|
+
const draft = newestDraft(workspaceDrafts)
|
|
96
|
+
const modifiedAtMs = stats?.mtimeMs || 0
|
|
97
|
+
const lastOpenedAtMs = latestTimestamp([
|
|
98
|
+
modifiedAtMs,
|
|
99
|
+
...shells.map((shell) => shell.start_time),
|
|
100
|
+
...workspaceDrafts.map((item) => item.updatedAt),
|
|
101
|
+
])
|
|
102
|
+
const primaryShell = shells.length === 1 ? shells[0] : null
|
|
103
|
+
const primaryScript = scripts.length === 1 ? scripts[0] : null
|
|
104
|
+
const usageCount = shells.length + scripts.length
|
|
105
|
+
|
|
106
|
+
items.push({
|
|
107
|
+
name: entry.name,
|
|
108
|
+
cwd,
|
|
109
|
+
relpath: entry.name,
|
|
110
|
+
modifiedAt: modifiedAtMs ? new Date(modifiedAtMs).toISOString() : null,
|
|
111
|
+
modifiedAtMs,
|
|
112
|
+
lastOpenedAt: lastOpenedAtMs ? new Date(lastOpenedAtMs).toISOString() : null,
|
|
113
|
+
lastOpenedAtMs,
|
|
114
|
+
usageCount,
|
|
115
|
+
running: shells.length > 0 || scripts.length > 0,
|
|
116
|
+
counts: {
|
|
117
|
+
shells: shells.length,
|
|
118
|
+
scripts: scripts.length,
|
|
119
|
+
drafts: workspaceDrafts.length,
|
|
120
|
+
},
|
|
121
|
+
shells,
|
|
122
|
+
scripts,
|
|
123
|
+
draft,
|
|
124
|
+
draftReady: Boolean(draft),
|
|
125
|
+
primaryUrl: primaryScript?.url || primaryShell?.url || null,
|
|
126
|
+
launchUrl: toRoutePath(cwd),
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const running = sortWorkspaces(items.filter((item) => item.running), sort)
|
|
131
|
+
const offline = sortWorkspaces(items.filter((item) => !item.running), sort)
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
root,
|
|
135
|
+
sort,
|
|
136
|
+
running,
|
|
137
|
+
offline,
|
|
138
|
+
items: [...running, ...offline],
|
|
139
|
+
counts: {
|
|
140
|
+
total: items.length,
|
|
141
|
+
running: running.length,
|
|
142
|
+
offline: offline.length,
|
|
143
|
+
drafts: items.filter((item) => item.draftReady).length,
|
|
144
|
+
},
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return { list, normalizeSortMode }
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
module.exports = { createWorkspaceCatalogService, normalizeSortMode }
|