@devframes/plugin-assets 0.7.15
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/LICENSE.md +21 -0
- package/README.md +13 -0
- package/bin.mjs +12 -0
- package/dist/cli.d.mts +5 -0
- package/dist/cli.mjs +8 -0
- package/dist/client/index.d.mts +6 -0
- package/dist/client/index.mjs +7 -0
- package/dist/index.d.mts +80 -0
- package/dist/index.mjs +2 -0
- package/dist/node/index.d.mts +96 -0
- package/dist/node/index.mjs +3 -0
- package/dist/node-qqFgpP7-.mjs +75 -0
- package/dist/rpc/index.d.mts +1093 -0
- package/dist/rpc/index.mjs +2 -0
- package/dist/rpc-lfFykVQ_.mjs +489 -0
- package/dist/spa/assets/index-E5-oQb8O.js +6 -0
- package/dist/spa/assets/index-PDxhd6Av.css +1 -0
- package/dist/spa/index.html +14 -0
- package/dist/src-DuJ-iEes.mjs +88 -0
- package/dist/types-DPDOYVFh.d.mts +29 -0
- package/dist/vite.d.mts +11 -0
- package/dist/vite.mjs +13 -0
- package/package.json +91 -0
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { _ as UPLOAD_CHANNEL, a as revealInFolder, c as readImageMeta, d as assetInfoSchema, f as list, g as capabilities, h as deleteAssets, i as writeFunctions, l as openInEditor, n as readFunctions, o as rename, r as serverFunctions, s as readText, t as alwaysFunctions, u as mkdir, v as upload } from "../rpc-lfFykVQ_.mjs";
|
|
2
|
+
export { UPLOAD_CHANNEL, alwaysFunctions, assetInfoSchema, capabilities, deleteAssets, list, mkdir, openInEditor, readFunctions, readImageMeta, readText, rename, revealInFolder, serverFunctions, upload, writeFunctions };
|
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
import { dirname, extname, join, resolve } from "pathe";
|
|
2
|
+
import { createWriteStream } from "node:fs";
|
|
3
|
+
import fsp from "node:fs/promises";
|
|
4
|
+
import { createDefineWrapperWithContext } from "devframe/rpc";
|
|
5
|
+
import * as v from "valibot";
|
|
6
|
+
import { defineDiagnostics } from "nostics";
|
|
7
|
+
import { glob } from "tinyglobby";
|
|
8
|
+
import { joinURL } from "ufo";
|
|
9
|
+
import { launchEditor } from "devframe/utils/launch-editor";
|
|
10
|
+
import { imageMeta } from "image-meta";
|
|
11
|
+
import { open } from "devframe/utils/open";
|
|
12
|
+
//#region src/diagnostics.ts
|
|
13
|
+
const diagnostics = defineDiagnostics({
|
|
14
|
+
docsBase: "https://devfra.me/errors",
|
|
15
|
+
codes: {
|
|
16
|
+
DP_ASSETS_0001: {
|
|
17
|
+
why: (p) => `"${p.path}" resolves outside the managed directory.`,
|
|
18
|
+
fix: "Use a path relative to the configured `dir`, without leading `/` or `..` segments."
|
|
19
|
+
},
|
|
20
|
+
DP_ASSETS_0002: {
|
|
21
|
+
why: (p) => `Cannot upload "${p.path}" because the "${p.extension}" extension is not allowed. Allowed extensions: ${p.allowed.join(", ")}.`,
|
|
22
|
+
fix: "Configure `uploadExtensions` on the assets devframe to allow this extension, or pass `'*'` to allow any."
|
|
23
|
+
},
|
|
24
|
+
DP_ASSETS_0003: {
|
|
25
|
+
why: (p) => `Cannot rename to "${p.path}" because a file already exists there.`,
|
|
26
|
+
fix: "Choose a different name."
|
|
27
|
+
},
|
|
28
|
+
DP_ASSETS_0004: {
|
|
29
|
+
why: (p) => `No asset found at "${p.path}".`,
|
|
30
|
+
fix: "Refresh the asset list — it may have been moved or deleted already."
|
|
31
|
+
},
|
|
32
|
+
DP_ASSETS_0005: {
|
|
33
|
+
why: (p) => `Cannot create folder "${p.path}" because a file already exists there.`,
|
|
34
|
+
fix: "Choose a different folder name."
|
|
35
|
+
},
|
|
36
|
+
DP_ASSETS_0006: {
|
|
37
|
+
why: (p) => `"${p.name}" is not a valid file name.`,
|
|
38
|
+
fix: "Names must be non-empty and cannot contain `/` or `\\`."
|
|
39
|
+
},
|
|
40
|
+
DP_ASSETS_0007: {
|
|
41
|
+
why: "The upload streaming channel is unavailable because this devframe was set up with `write: false`.",
|
|
42
|
+
fix: "This indicates an internal registration bug — `upload` should never be reachable without `write: true`. Please report it."
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
//#endregion
|
|
47
|
+
//#region src/node/paths.ts
|
|
48
|
+
/**
|
|
49
|
+
* Resolve a client-supplied, root-relative path against the managed
|
|
50
|
+
* directory, rejecting anything that would escape it (`..` traversal, a
|
|
51
|
+
* rogue absolute path, etc.). Every RPC handler that touches the
|
|
52
|
+
* filesystem goes through this — never trust a path from the wire.
|
|
53
|
+
*/
|
|
54
|
+
function resolveAssetPath(root, relativePath) {
|
|
55
|
+
const cleaned = relativePath.replace(/^[/\\]+/, "");
|
|
56
|
+
const normalizedRoot = resolve(root);
|
|
57
|
+
const absolute = resolve(normalizedRoot, cleaned);
|
|
58
|
+
if (absolute !== normalizedRoot && !absolute.startsWith(`${normalizedRoot}/`)) throw diagnostics.DP_ASSETS_0001({ path: relativePath });
|
|
59
|
+
return absolute;
|
|
60
|
+
}
|
|
61
|
+
//#endregion
|
|
62
|
+
//#region src/node/context.ts
|
|
63
|
+
const configs = /* @__PURE__ */ new WeakMap();
|
|
64
|
+
const contexts = /* @__PURE__ */ new WeakMap();
|
|
65
|
+
/**
|
|
66
|
+
* Record the managed directory and write policy for a context. Called from
|
|
67
|
+
* the devframe's `setup` before any RPC handler runs.
|
|
68
|
+
*/
|
|
69
|
+
function configureAssets(ctx, config) {
|
|
70
|
+
configs.set(ctx, config);
|
|
71
|
+
contexts.delete(ctx);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Per-`DevframeNodeContext` assets state. Each RPC function pulls its
|
|
75
|
+
* managed directory and path resolver from here instead of re-reading
|
|
76
|
+
* `configureAssets`'s config on every call.
|
|
77
|
+
*/
|
|
78
|
+
function getAssetsContext(ctx) {
|
|
79
|
+
const existing = contexts.get(ctx);
|
|
80
|
+
if (existing) return existing;
|
|
81
|
+
const config = configs.get(ctx);
|
|
82
|
+
const dir = config?.dir ?? ctx.cwd;
|
|
83
|
+
const built = {
|
|
84
|
+
dir,
|
|
85
|
+
write: config?.write ?? true,
|
|
86
|
+
uploadExtensions: config?.uploadExtensions ?? "*",
|
|
87
|
+
baseURL: config?.baseURL ?? "/",
|
|
88
|
+
uploadChannel: config?.uploadChannel,
|
|
89
|
+
resolvePath: (relativePath) => resolveAssetPath(dir, relativePath)
|
|
90
|
+
};
|
|
91
|
+
contexts.set(ctx, built);
|
|
92
|
+
return built;
|
|
93
|
+
}
|
|
94
|
+
//#endregion
|
|
95
|
+
//#region src/rpc/functions/upload.ts
|
|
96
|
+
const defineAssetsRpc$9 = createDefineWrapperWithContext();
|
|
97
|
+
/** Streaming channel name, namespaced like every other RPC name in this plugin. */
|
|
98
|
+
const UPLOAD_CHANNEL = "devframes:plugin:assets:upload";
|
|
99
|
+
function isExtensionAllowed(path, allowed) {
|
|
100
|
+
if (allowed === "*") return true;
|
|
101
|
+
const ext = extname(path).slice(1).toLowerCase();
|
|
102
|
+
return allowed.includes(ext);
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Allocates an upload and returns a streaming id. Actual bytes flow over
|
|
106
|
+
* the `upload` channel opened in `setupAssets` — see the client-side
|
|
107
|
+
* `useUpload` hook for the matching `rpc.streaming.upload()` call.
|
|
108
|
+
*/
|
|
109
|
+
const upload = defineAssetsRpc$9({
|
|
110
|
+
name: "devframes:plugin:assets:upload",
|
|
111
|
+
type: "action",
|
|
112
|
+
jsonSerializable: true,
|
|
113
|
+
args: [v.object({ path: v.string() })],
|
|
114
|
+
returns: v.object({ uploadId: v.string() }),
|
|
115
|
+
agent: {
|
|
116
|
+
title: "Upload an asset",
|
|
117
|
+
description: "Allocate an upload slot for a new file at the given path. The caller streams the bytes over the paired upload channel.",
|
|
118
|
+
safety: "action",
|
|
119
|
+
tags: ["assets"]
|
|
120
|
+
},
|
|
121
|
+
setup: (ctx) => {
|
|
122
|
+
const assets = getAssetsContext(ctx);
|
|
123
|
+
return { handler: (async ({ path }) => {
|
|
124
|
+
if (!isExtensionAllowed(path, assets.uploadExtensions)) throw diagnostics.DP_ASSETS_0002({
|
|
125
|
+
path,
|
|
126
|
+
extension: extname(path).slice(1).toLowerCase() || "(none)",
|
|
127
|
+
allowed: assets.uploadExtensions === "*" ? [] : assets.uploadExtensions
|
|
128
|
+
});
|
|
129
|
+
const absolute = assets.resolvePath(path);
|
|
130
|
+
await fsp.mkdir(dirname(absolute), { recursive: true });
|
|
131
|
+
const channel = assets.uploadChannel;
|
|
132
|
+
if (!channel) throw diagnostics.DP_ASSETS_0007();
|
|
133
|
+
const reader = channel.openInbound();
|
|
134
|
+
(async () => {
|
|
135
|
+
const file = createWriteStream(absolute);
|
|
136
|
+
try {
|
|
137
|
+
for await (const chunk of reader) file.write(chunk);
|
|
138
|
+
} catch {
|
|
139
|
+
file.close();
|
|
140
|
+
await fsp.unlink(absolute).catch(() => {});
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
file.close();
|
|
144
|
+
})();
|
|
145
|
+
return { uploadId: reader.id };
|
|
146
|
+
}) };
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
/**
|
|
150
|
+
* Lets the UI know upfront whether upload / rename / delete / mkdir are
|
|
151
|
+
* available, so it can hide or disable those affordances proactively
|
|
152
|
+
* instead of letting the user hit a "method not found" error — the same
|
|
153
|
+
* `canWrite`-gating idea the git plugin's `GitStatus.canWrite` follows.
|
|
154
|
+
*/
|
|
155
|
+
const capabilities = createDefineWrapperWithContext()({
|
|
156
|
+
name: "devframes:plugin:assets:capabilities",
|
|
157
|
+
type: "query",
|
|
158
|
+
snapshot: true,
|
|
159
|
+
jsonSerializable: true,
|
|
160
|
+
args: [],
|
|
161
|
+
returns: v.object({
|
|
162
|
+
write: v.boolean(),
|
|
163
|
+
uploadExtensions: v.union([v.array(v.string()), v.literal("*")])
|
|
164
|
+
}),
|
|
165
|
+
agent: {
|
|
166
|
+
title: "Read assets capabilities",
|
|
167
|
+
description: "Read whether upload, rename, delete, and folder creation are enabled, and which upload extensions are allowed.",
|
|
168
|
+
safety: "read",
|
|
169
|
+
tags: ["assets"]
|
|
170
|
+
},
|
|
171
|
+
setup: (ctx) => {
|
|
172
|
+
const assets = getAssetsContext(ctx);
|
|
173
|
+
return { handler: (async () => ({
|
|
174
|
+
write: assets.write,
|
|
175
|
+
uploadExtensions: assets.uploadExtensions
|
|
176
|
+
})) };
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
/** One request covers both single- and multi-select delete. */
|
|
180
|
+
const deleteAssets = createDefineWrapperWithContext()({
|
|
181
|
+
name: "devframes:plugin:assets:delete",
|
|
182
|
+
type: "action",
|
|
183
|
+
jsonSerializable: true,
|
|
184
|
+
args: [v.object({ paths: v.array(v.string()) })],
|
|
185
|
+
returns: v.object({ deleted: v.array(v.string()) }),
|
|
186
|
+
agent: {
|
|
187
|
+
title: "Delete assets",
|
|
188
|
+
description: "Delete one or more assets from the managed directory.",
|
|
189
|
+
safety: "destructive",
|
|
190
|
+
tags: ["assets"]
|
|
191
|
+
},
|
|
192
|
+
setup: (ctx) => {
|
|
193
|
+
const assets = getAssetsContext(ctx);
|
|
194
|
+
return { handler: (async ({ paths }) => {
|
|
195
|
+
const deleted = [];
|
|
196
|
+
for (const path of paths) {
|
|
197
|
+
const absolute = assets.resolvePath(path);
|
|
198
|
+
try {
|
|
199
|
+
await fsp.unlink(absolute);
|
|
200
|
+
deleted.push(path);
|
|
201
|
+
} catch (error) {
|
|
202
|
+
if (error.code !== "ENOENT") throw error;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return { deleted };
|
|
206
|
+
}) };
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
//#endregion
|
|
210
|
+
//#region src/node/scanner.ts
|
|
211
|
+
const reImage = /\.(?:png|jpe?g|jxl|gif|svg|webp|avif|ico|bmp|tiff?)$/i;
|
|
212
|
+
const reVideo = /\.(?:mp4|webm|ogv|mov|avi|flv|wmv|mpg|mpeg|mkv|3gp|3g2|ts|mts|m2ts|vob|ogm|ogx|rm|rmvb|asf|amv|divx|m4v|svi|viv|f4v|f4p|f4a|f4b)$/i;
|
|
213
|
+
const reAudio = /\.(?:mp3|wav|ogg|flac|aac|wma|alac|ape|ac3|dts|tta|opus|amr|aiff|au|mid|midi|ra|rm|wv|weba|dss|spx|vox|tak|dsf|dff|dsd|cda)$/i;
|
|
214
|
+
const reFont = /\.(?:woff2?|eot|ttf|otf|ttc|pfa|pfb|pfm|afm)$/i;
|
|
215
|
+
const reText = /\.(?:json[5c]?|te?xt|[mc]?[jt]sx?|md[cx]?|markdown|ya?ml|toml|csv|css|scss|less|styl|html?|xml|vue|svelte|astro|env|ini|conf|sh)$/i;
|
|
216
|
+
/** Classifies a path by extension, driving preview and icon choice. */
|
|
217
|
+
function guessAssetType(path) {
|
|
218
|
+
if (reImage.test(path)) return "image";
|
|
219
|
+
if (reVideo.test(path)) return "video";
|
|
220
|
+
if (reAudio.test(path)) return "audio";
|
|
221
|
+
if (reFont.test(path)) return "font";
|
|
222
|
+
if (reText.test(path)) return "text";
|
|
223
|
+
return "other";
|
|
224
|
+
}
|
|
225
|
+
function toPublicPath(baseURL, posixPath) {
|
|
226
|
+
return joinURL(baseURL, posixPath.split("/").map(encodeURIComponent).join("/"));
|
|
227
|
+
}
|
|
228
|
+
/** Builds an {@link AssetInfo} from an already-resolved `fs.Stats`. */
|
|
229
|
+
function statToAssetInfo(dir, baseURL, relPath, stat) {
|
|
230
|
+
const posixPath = relPath.replace(/\\/g, "/");
|
|
231
|
+
return {
|
|
232
|
+
path: posixPath,
|
|
233
|
+
type: guessAssetType(posixPath),
|
|
234
|
+
publicPath: toPublicPath(baseURL, posixPath),
|
|
235
|
+
size: stat.size,
|
|
236
|
+
mtime: stat.mtimeMs
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
/** Recursively lists every file under `dir`, sorted alphabetically by path. */
|
|
240
|
+
async function scanAssets(dir, baseURL) {
|
|
241
|
+
const files = await glob(["**/*"], {
|
|
242
|
+
cwd: dir,
|
|
243
|
+
onlyFiles: true,
|
|
244
|
+
dot: false
|
|
245
|
+
});
|
|
246
|
+
return (await Promise.all(files.map(async (relPath) => {
|
|
247
|
+
try {
|
|
248
|
+
return statToAssetInfo(dir, baseURL, relPath, await fsp.lstat(join(dir, relPath)));
|
|
249
|
+
} catch {
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
}))).filter((info) => info !== void 0).sort((a, b) => a.path.localeCompare(b.path));
|
|
253
|
+
}
|
|
254
|
+
//#endregion
|
|
255
|
+
//#region src/rpc/functions/list.ts
|
|
256
|
+
const defineAssetsRpc$6 = createDefineWrapperWithContext();
|
|
257
|
+
const assetInfoSchema = v.object({
|
|
258
|
+
path: v.string(),
|
|
259
|
+
type: v.picklist([
|
|
260
|
+
"image",
|
|
261
|
+
"font",
|
|
262
|
+
"video",
|
|
263
|
+
"audio",
|
|
264
|
+
"text",
|
|
265
|
+
"other"
|
|
266
|
+
]),
|
|
267
|
+
publicPath: v.string(),
|
|
268
|
+
size: v.number(),
|
|
269
|
+
mtime: v.number()
|
|
270
|
+
});
|
|
271
|
+
const list = defineAssetsRpc$6({
|
|
272
|
+
name: "devframes:plugin:assets:list",
|
|
273
|
+
type: "query",
|
|
274
|
+
snapshot: true,
|
|
275
|
+
jsonSerializable: true,
|
|
276
|
+
args: [],
|
|
277
|
+
returns: v.array(assetInfoSchema),
|
|
278
|
+
agent: {
|
|
279
|
+
title: "List managed assets",
|
|
280
|
+
description: "List every file under the managed directory with its type, size, and last-modified time.",
|
|
281
|
+
safety: "read",
|
|
282
|
+
tags: ["assets", "files"]
|
|
283
|
+
},
|
|
284
|
+
setup: (ctx) => {
|
|
285
|
+
const assets = getAssetsContext(ctx);
|
|
286
|
+
return { handler: (async () => scanAssets(assets.dir, assets.baseURL)) };
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
const mkdir = createDefineWrapperWithContext()({
|
|
290
|
+
name: "devframes:plugin:assets:mkdir",
|
|
291
|
+
type: "action",
|
|
292
|
+
jsonSerializable: true,
|
|
293
|
+
args: [v.object({ path: v.string() })],
|
|
294
|
+
returns: v.void(),
|
|
295
|
+
agent: {
|
|
296
|
+
title: "Create a folder",
|
|
297
|
+
description: "Create a folder under the managed directory, including any missing parent folders.",
|
|
298
|
+
safety: "action",
|
|
299
|
+
tags: ["assets"]
|
|
300
|
+
},
|
|
301
|
+
setup: (ctx) => {
|
|
302
|
+
const assets = getAssetsContext(ctx);
|
|
303
|
+
return { handler: (async ({ path }) => {
|
|
304
|
+
const absolute = assets.resolvePath(path);
|
|
305
|
+
const stat = await fsp.stat(absolute).catch(() => void 0);
|
|
306
|
+
if (stat && !stat.isDirectory()) throw diagnostics.DP_ASSETS_0005({ path });
|
|
307
|
+
await fsp.mkdir(absolute, { recursive: true });
|
|
308
|
+
}) };
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
/**
|
|
312
|
+
* Reuses devframe's `launchEditor` utility (the same one backing the core
|
|
313
|
+
* `devframe:open-in-editor` recipe) but resolves the path against the
|
|
314
|
+
* managed directory first, so the client only ever sends a root-relative
|
|
315
|
+
* path — never the server's absolute filesystem layout.
|
|
316
|
+
*/
|
|
317
|
+
const openInEditor = createDefineWrapperWithContext()({
|
|
318
|
+
name: "devframes:plugin:assets:open-in-editor",
|
|
319
|
+
type: "action",
|
|
320
|
+
jsonSerializable: true,
|
|
321
|
+
args: [v.string()],
|
|
322
|
+
returns: v.void(),
|
|
323
|
+
agent: {
|
|
324
|
+
title: "Open an asset in the editor",
|
|
325
|
+
description: "Open an asset in the user's configured editor.",
|
|
326
|
+
safety: "action",
|
|
327
|
+
tags: ["assets"]
|
|
328
|
+
},
|
|
329
|
+
setup: (ctx) => {
|
|
330
|
+
const assets = getAssetsContext(ctx);
|
|
331
|
+
return { handler: (async (path) => {
|
|
332
|
+
launchEditor(assets.resolvePath(path));
|
|
333
|
+
}) };
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
const readImageMeta = createDefineWrapperWithContext()({
|
|
337
|
+
name: "devframes:plugin:assets:read-image-meta",
|
|
338
|
+
type: "query",
|
|
339
|
+
jsonSerializable: true,
|
|
340
|
+
args: [v.string()],
|
|
341
|
+
returns: v.nullable(v.object({
|
|
342
|
+
width: v.optional(v.number()),
|
|
343
|
+
height: v.optional(v.number()),
|
|
344
|
+
orientation: v.optional(v.number())
|
|
345
|
+
})),
|
|
346
|
+
agent: {
|
|
347
|
+
title: "Read image dimensions",
|
|
348
|
+
description: "Read width, height, and orientation for an image asset.",
|
|
349
|
+
safety: "read",
|
|
350
|
+
tags: ["assets", "images"]
|
|
351
|
+
},
|
|
352
|
+
setup: (ctx) => {
|
|
353
|
+
const assets = getAssetsContext(ctx);
|
|
354
|
+
return { handler: (async (path) => {
|
|
355
|
+
try {
|
|
356
|
+
const meta = imageMeta(await fsp.readFile(assets.resolvePath(path)));
|
|
357
|
+
return {
|
|
358
|
+
width: meta.width,
|
|
359
|
+
height: meta.height,
|
|
360
|
+
orientation: meta.orientation
|
|
361
|
+
};
|
|
362
|
+
} catch {
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
}) };
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
//#endregion
|
|
369
|
+
//#region src/rpc/functions/read-text.ts
|
|
370
|
+
const defineAssetsRpc$2 = createDefineWrapperWithContext();
|
|
371
|
+
const DEFAULT_LIMIT = 5e3;
|
|
372
|
+
const readText = defineAssetsRpc$2({
|
|
373
|
+
name: "devframes:plugin:assets:read-text",
|
|
374
|
+
type: "query",
|
|
375
|
+
jsonSerializable: true,
|
|
376
|
+
args: [v.string(), v.optional(v.number())],
|
|
377
|
+
returns: v.nullable(v.string()),
|
|
378
|
+
agent: {
|
|
379
|
+
title: "Read a text asset",
|
|
380
|
+
description: "Read the (possibly truncated) text content of a text-type asset, for preview or editing.",
|
|
381
|
+
safety: "read",
|
|
382
|
+
tags: ["assets", "text"]
|
|
383
|
+
},
|
|
384
|
+
setup: (ctx) => {
|
|
385
|
+
const assets = getAssetsContext(ctx);
|
|
386
|
+
return { handler: (async (path, limit = DEFAULT_LIMIT) => {
|
|
387
|
+
try {
|
|
388
|
+
return (await fsp.readFile(assets.resolvePath(path), "utf-8")).slice(0, limit);
|
|
389
|
+
} catch {
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
392
|
+
}) };
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
const rename = createDefineWrapperWithContext()({
|
|
396
|
+
name: "devframes:plugin:assets:rename",
|
|
397
|
+
type: "action",
|
|
398
|
+
jsonSerializable: true,
|
|
399
|
+
args: [v.object({
|
|
400
|
+
path: v.string(),
|
|
401
|
+
newName: v.string()
|
|
402
|
+
})],
|
|
403
|
+
returns: assetInfoSchema,
|
|
404
|
+
agent: {
|
|
405
|
+
title: "Rename an asset",
|
|
406
|
+
description: "Rename an asset, keeping it in the same folder.",
|
|
407
|
+
safety: "destructive",
|
|
408
|
+
tags: ["assets"]
|
|
409
|
+
},
|
|
410
|
+
setup: (ctx) => {
|
|
411
|
+
const assets = getAssetsContext(ctx);
|
|
412
|
+
return { handler: (async ({ path, newName }) => {
|
|
413
|
+
const trimmed = newName.trim();
|
|
414
|
+
if (!trimmed || /[/\\]/.test(trimmed)) throw diagnostics.DP_ASSETS_0006({ name: newName });
|
|
415
|
+
const folder = dirname(path);
|
|
416
|
+
const nextName = `${trimmed}${extname(path)}`;
|
|
417
|
+
const nextRelPath = folder === "." ? nextName : `${folder}/${nextName}`;
|
|
418
|
+
const from = assets.resolvePath(path);
|
|
419
|
+
const to = assets.resolvePath(nextRelPath);
|
|
420
|
+
if (from === to) {
|
|
421
|
+
const stat = await fsp.lstat(from);
|
|
422
|
+
return statToAssetInfo(assets.dir, assets.baseURL, path, stat);
|
|
423
|
+
}
|
|
424
|
+
if (await fsp.access(to).then(() => true).catch(() => false)) throw diagnostics.DP_ASSETS_0003({ path: nextRelPath });
|
|
425
|
+
await fsp.mkdir(dirname(to), { recursive: true });
|
|
426
|
+
try {
|
|
427
|
+
await fsp.rename(from, to);
|
|
428
|
+
} catch (error) {
|
|
429
|
+
if (error.code === "ENOENT") throw diagnostics.DP_ASSETS_0004({ path });
|
|
430
|
+
throw error;
|
|
431
|
+
}
|
|
432
|
+
const stat = await fsp.lstat(to);
|
|
433
|
+
return statToAssetInfo(assets.dir, assets.baseURL, nextRelPath, stat);
|
|
434
|
+
}) };
|
|
435
|
+
}
|
|
436
|
+
});
|
|
437
|
+
/**
|
|
438
|
+
* Reuses devframe's `open` utility (the same one backing the core
|
|
439
|
+
* `devframe:open-in-finder` recipe), opening the asset's containing folder
|
|
440
|
+
* so it's revealed in the OS file manager rather than launched with its
|
|
441
|
+
* default app (which `download` already covers).
|
|
442
|
+
*/
|
|
443
|
+
const revealInFolder = createDefineWrapperWithContext()({
|
|
444
|
+
name: "devframes:plugin:assets:reveal-in-folder",
|
|
445
|
+
type: "action",
|
|
446
|
+
jsonSerializable: true,
|
|
447
|
+
args: [v.string()],
|
|
448
|
+
returns: v.void(),
|
|
449
|
+
agent: {
|
|
450
|
+
title: "Reveal an asset in the file manager",
|
|
451
|
+
description: "Open the OS file manager at the asset's containing folder.",
|
|
452
|
+
safety: "action",
|
|
453
|
+
tags: ["assets"]
|
|
454
|
+
},
|
|
455
|
+
setup: (ctx) => {
|
|
456
|
+
const assets = getAssetsContext(ctx);
|
|
457
|
+
return { handler: (async (path) => {
|
|
458
|
+
await open(dirname(assets.resolvePath(path)));
|
|
459
|
+
}) };
|
|
460
|
+
}
|
|
461
|
+
});
|
|
462
|
+
//#endregion
|
|
463
|
+
//#region src/rpc/index.ts
|
|
464
|
+
/** Read-only RPC — always registered. */
|
|
465
|
+
const readFunctions = [
|
|
466
|
+
list,
|
|
467
|
+
readImageMeta,
|
|
468
|
+
readText,
|
|
469
|
+
capabilities
|
|
470
|
+
];
|
|
471
|
+
/**
|
|
472
|
+
* Informational actions — launch external apps, never touch the managed
|
|
473
|
+
* directory's contents. Always registered regardless of `write`.
|
|
474
|
+
*/
|
|
475
|
+
const alwaysFunctions = [openInEditor, revealInFolder];
|
|
476
|
+
/** Mutating RPC — registered only when write actions are enabled. */
|
|
477
|
+
const writeFunctions = [
|
|
478
|
+
upload,
|
|
479
|
+
rename,
|
|
480
|
+
deleteAssets,
|
|
481
|
+
mkdir
|
|
482
|
+
];
|
|
483
|
+
const serverFunctions = [
|
|
484
|
+
...readFunctions,
|
|
485
|
+
...alwaysFunctions,
|
|
486
|
+
...writeFunctions
|
|
487
|
+
];
|
|
488
|
+
//#endregion
|
|
489
|
+
export { UPLOAD_CHANNEL as _, revealInFolder as a, getAssetsContext as b, readImageMeta as c, assetInfoSchema as d, list as f, capabilities as g, deleteAssets as h, writeFunctions as i, openInEditor as l, scanAssets as m, readFunctions as n, rename as o, guessAssetType as p, serverFunctions as r, readText as s, alwaysFunctions as t, mkdir as u, upload as v, resolveAssetPath as x, configureAssets as y };
|