dsh-artcraft-plugin-lightcraft 0.2.11 → 0.2.12
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/package.json +1 -1
- package/src/kit/preview.js +115 -3
package/package.json
CHANGED
package/src/kit/preview.js
CHANGED
|
@@ -11,10 +11,13 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { createHash } from 'node:crypto'
|
|
14
|
-
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs'
|
|
14
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs'
|
|
15
15
|
import { homedir } from 'node:os'
|
|
16
16
|
import { basename, dirname, join } from 'node:path'
|
|
17
17
|
import { runCraft } from './exec.js'
|
|
18
|
+
import { CraftMcpClient } from './mcp.js'
|
|
19
|
+
import { discoverCraft } from './discover.js'
|
|
20
|
+
import { PROFILES_BY_ID } from './profiles.mjs'
|
|
18
21
|
|
|
19
22
|
|
|
20
23
|
/** Where the previews live when no configuration overrides it. */
|
|
@@ -58,9 +61,66 @@ const RECIPES = {
|
|
|
58
61
|
lightcraft: { argv: (file, out) => ['render', file, '-o', out] },
|
|
59
62
|
}
|
|
60
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Crafts whose page only exists through their MCP server.
|
|
66
|
+
*
|
|
67
|
+
* PdfCraft has no CLI render verb and FilmCraft renders a sequence frame, not a
|
|
68
|
+
* file; both answer over MCP, so the preview borrows their own server.
|
|
69
|
+
*/
|
|
70
|
+
const MCP_RENDER = { pdfcraft: 'pdfcraft', filmcraft: 'filmcraft' }
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Crafts that can export a PDF but cannot rasterise one. Their preview exports
|
|
74
|
+
* a temporary PDF with the craft's own CLI and renders it through PdfCraft —
|
|
75
|
+
* the alternative is no preview at all for the three most document-shaped apps.
|
|
76
|
+
*/
|
|
77
|
+
const PDF_BRIDGE = {
|
|
78
|
+
gridcraft: (file, pdf) => ['convert', file, pdf],
|
|
79
|
+
designcraft: (file, pdf) => ['run', '--in', file, '--export', pdf],
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Open one craft's MCP server, run a callback, always close it. */
|
|
83
|
+
async function withMcp (craftId , run ) {
|
|
84
|
+
// `PROFILES_BY_ID` is a Map, not a record.
|
|
85
|
+
const profile = (PROFILES_BY_ID ).get(craftId)
|
|
86
|
+
if (profile === undefined) return undefined
|
|
87
|
+
const binary = discoverCraft(profile)
|
|
88
|
+
if (binary === undefined) return undefined
|
|
89
|
+
const client = new CraftMcpClient(profile, binary)
|
|
90
|
+
try {
|
|
91
|
+
await client.start(60_000)
|
|
92
|
+
return await run(client)
|
|
93
|
+
} catch {
|
|
94
|
+
return undefined
|
|
95
|
+
} finally {
|
|
96
|
+
client.close()
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Call an MCP tool that answers with an image, and copy it to the step file. */
|
|
101
|
+
async function imageFromCall(
|
|
102
|
+
client ,
|
|
103
|
+
tool ,
|
|
104
|
+
args ,
|
|
105
|
+
out ,
|
|
106
|
+
) {
|
|
107
|
+
const text = await client.callTool(tool, args, join(dirname(out), '.mcp-images'))
|
|
108
|
+
const match = /\[image\]\s+(.+)/.exec(text)
|
|
109
|
+
if (match === null) return undefined
|
|
110
|
+
try {
|
|
111
|
+
copyFileSync(match[1].trim(), out)
|
|
112
|
+
} catch {
|
|
113
|
+
return undefined
|
|
114
|
+
}
|
|
115
|
+
const hash = imageHash(out)
|
|
116
|
+
return hash === undefined ? undefined : { path: out, hash }
|
|
117
|
+
}
|
|
118
|
+
|
|
61
119
|
/** Whether a craft can render a preview at all. */
|
|
62
120
|
export function canPreview(profile ) {
|
|
63
121
|
return RECIPES[profile.id] !== undefined
|
|
122
|
+
|| MCP_RENDER[profile.id] !== undefined
|
|
123
|
+
|| PDF_BRIDGE[profile.id] !== undefined
|
|
64
124
|
}
|
|
65
125
|
|
|
66
126
|
/** One preview step, as documented in docs/PREVIEW.md. */
|
|
@@ -152,11 +212,41 @@ export async function renderPreview(
|
|
|
152
212
|
binary ,
|
|
153
213
|
options ,
|
|
154
214
|
) {
|
|
215
|
+
if (!existsSync(options.file)) return undefined
|
|
216
|
+
mkdirSync(options.dir, { recursive: true })
|
|
217
|
+
|
|
218
|
+
// PdfCraft and FilmCraft render through their own MCP server: PdfCraft has no
|
|
219
|
+
// CLI render verb, and a FilmCraft frame is a timeline position, not a file.
|
|
220
|
+
if (MCP_RENDER[profile.id] !== undefined) {
|
|
221
|
+
if (profile.id === 'pdfcraft') return await renderPdf(options.file, options)
|
|
222
|
+
return await withMcp('filmcraft', async (client) => {
|
|
223
|
+
await client.callTool('command_run', { id: 'file.open', params: { path: options.file } })
|
|
224
|
+
return await imageFromCall(client, 'render_frame', { max_side: 1400 }, options.out)
|
|
225
|
+
})
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// GridCraft and DesignCraft export a PDF but cannot rasterise one; PdfCraft
|
|
229
|
+
// renders the page, so the preview has a picture instead of a step with none.
|
|
230
|
+
const bridge = PDF_BRIDGE[profile.id]
|
|
231
|
+
if (bridge !== undefined) {
|
|
232
|
+
const pdf = join(options.dir, `${profile.id}-preview.pdf`)
|
|
233
|
+
try {
|
|
234
|
+
const exported = await runCraft({
|
|
235
|
+
profile, binary,
|
|
236
|
+
argv: bridge(options.file, pdf),
|
|
237
|
+
cwd: process.cwd(),
|
|
238
|
+
timeoutMs: options.timeoutMs ?? 120_000,
|
|
239
|
+
})
|
|
240
|
+
if (!exported.ok || !existsSync(pdf)) return undefined
|
|
241
|
+
} catch {
|
|
242
|
+
return undefined
|
|
243
|
+
}
|
|
244
|
+
return await renderPdf(pdf, options)
|
|
245
|
+
}
|
|
246
|
+
|
|
155
247
|
const recipe = RECIPES[profile.id]
|
|
156
248
|
if (recipe === undefined) return undefined
|
|
157
|
-
if (!existsSync(options.file)) return undefined
|
|
158
249
|
try {
|
|
159
|
-
mkdirSync(options.dir, { recursive: true })
|
|
160
250
|
const argv = recipe.argv(options.file, options.out, options.scale ?? 1.5, options.page ?? 0)
|
|
161
251
|
const result = await runCraft({ profile, binary, argv, cwd: process.cwd(), timeoutMs: options.timeoutMs ?? 60_000 })
|
|
162
252
|
if (!result.ok || !existsSync(options.out)) return undefined
|
|
@@ -167,6 +257,28 @@ export async function renderPreview(
|
|
|
167
257
|
}
|
|
168
258
|
}
|
|
169
259
|
|
|
260
|
+
/** Render one page of a PDF through PdfCraft's MCP server. */
|
|
261
|
+
async function renderPdf(
|
|
262
|
+
file ,
|
|
263
|
+
options ,
|
|
264
|
+
) {
|
|
265
|
+
return await withMcp('pdfcraft', async (client) => {
|
|
266
|
+
const opened = await client.callTool('doc_open', { path: file })
|
|
267
|
+
// PdfCraft answers `{doc: 1, pages: 2, …}`; `id` is only a fallback for a
|
|
268
|
+
// server that renames the field.
|
|
269
|
+
let docId
|
|
270
|
+
try {
|
|
271
|
+
const parsed = JSON.parse(opened)
|
|
272
|
+
docId = parsed.doc ?? parsed.id
|
|
273
|
+
} catch {
|
|
274
|
+
const match = /"(?:doc|id)"\s*:\s*(\d+)/.exec(opened)
|
|
275
|
+
docId = match === null ? undefined : Number(match[1])
|
|
276
|
+
}
|
|
277
|
+
if (typeof docId !== 'number') return undefined
|
|
278
|
+
return await imageFromCall(client, 'page_render', { doc: docId, page: (options.page ?? 0) + 1, dpi: 110 }, options.out)
|
|
279
|
+
})
|
|
280
|
+
}
|
|
281
|
+
|
|
170
282
|
/**
|
|
171
283
|
* The next step number for a craft, across processes.
|
|
172
284
|
*
|