wicker-study-mcp 2.0.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.
@@ -0,0 +1,60 @@
1
+ import { execFile } from 'node:child_process'
2
+ import { lstat, mkdir, mkdtemp, rename, rm, unlink, stat } from 'node:fs/promises'
3
+ import { homedir, tmpdir } from 'node:os'
4
+ import { basename, dirname, isAbsolute, join, resolve } from 'node:path'
5
+ import { promisify } from 'node:util'
6
+ import { CanvasCourseImportError, importCanvasCourse } from './canvas-course-import.mjs'
7
+
8
+ const execFileAsync = promisify(execFile)
9
+
10
+ function safeFilename(value, fallback = 'canvas-course') {
11
+ const name = String(value || '').replace(/[\\/:*?"<>|]/g, '-').replace(/\s+/g, ' ').trim().replace(/[. ]+$/g, '')
12
+ return name || fallback
13
+ }
14
+
15
+ async function assertNewOutput(path) {
16
+ try {
17
+ await lstat(path)
18
+ throw new CanvasCourseImportError('Choose a new ZIP path. Existing files are never overwritten.')
19
+ } catch (error) {
20
+ if (error?.code === 'ENOENT') return
21
+ throw error
22
+ }
23
+ }
24
+
25
+ export async function exportCanvasCourseZip({ courseUrl, accessToken, moduleIds, outputPath, maxResources, maxFileBytes, fetchImpl } = {}) {
26
+ if (process.platform !== 'darwin') throw new CanvasCourseImportError('Local Canvas ZIP export currently requires macOS.')
27
+ if (!outputPath || !String(outputPath).trim()) throw new CanvasCourseImportError('outputPath is required for the Canvas ZIP export.')
28
+ if (!isAbsolute(String(outputPath))) throw new CanvasCourseImportError('outputPath must be an absolute .zip path.')
29
+ const resolvedOutputPath = resolve(String(outputPath))
30
+ if (!resolvedOutputPath.toLowerCase().endsWith('.zip')) throw new CanvasCourseImportError('outputPath must be an absolute .zip path.')
31
+ await assertNewOutput(resolvedOutputPath)
32
+ await mkdir(dirname(resolvedOutputPath), { recursive: true })
33
+
34
+ const staging = await mkdtemp(join(tmpdir(), 'wicker-study-canvas-export-'))
35
+ const importRoot = join(staging, 'course')
36
+ const temporaryZip = `${resolvedOutputPath}.partial-${process.pid}-${Date.now()}`
37
+ try {
38
+ const imported = await importCanvasCourse({ courseUrl, accessToken, outputFolder: importRoot, moduleIds, maxResources, maxFileBytes, fetchImpl })
39
+ const folderName = safeFilename(`${imported.course.code || 'Canvas course'} ${imported.course.id}`)
40
+ const packagedFolder = join(staging, folderName)
41
+ await rename(importRoot, packagedFolder)
42
+ try {
43
+ await execFileAsync('/usr/bin/ditto', ['-c', '-k', '--sequesterRsrc', '--keepParent', packagedFolder, temporaryZip], { timeout: 10 * 60_000, maxBuffer: 1024 * 1024 })
44
+ } catch (error) {
45
+ throw new CanvasCourseImportError(`Could not create the Canvas ZIP: ${error.message}`)
46
+ }
47
+ await rename(temporaryZip, resolvedOutputPath)
48
+ const details = await stat(resolvedOutputPath)
49
+ return { zipPath: resolvedOutputPath, fileName: basename(resolvedOutputPath), bytes: details.size, imported }
50
+ } catch (error) {
51
+ await unlink(temporaryZip).catch(() => {})
52
+ throw error
53
+ } finally {
54
+ await rm(staging, { recursive: true, force: true }).catch(() => {})
55
+ }
56
+ }
57
+
58
+ export function defaultCanvasZipPath(courseId) {
59
+ return join(homedir(), 'Downloads', `${safeFilename(`Wicker Study Canvas ${courseId}`)}.zip`)
60
+ }