diff --git a/docs/tutorialkit.dev/package.json b/docs/tutorialkit.dev/package.json
index 7cefd8a60..8a029083d 100644
--- a/docs/tutorialkit.dev/package.json
+++ b/docs/tutorialkit.dev/package.json
@@ -12,7 +12,7 @@
   },
   "dependencies": {
     "@tutorialkit/react": "workspace:*",
-    "@webcontainer/api": "1.2.4",
+    "@webcontainer/api": "1.5.1",
     "classnames": "^2.5.1",
     "react": "^18.3.1",
     "react-dom": "^18.3.1"
diff --git a/docs/tutorialkit.dev/src/content/docs/guides/overriding-components.mdx b/docs/tutorialkit.dev/src/content/docs/guides/overriding-components.mdx
index e6431ea71..c7751a2ba 100644
--- a/docs/tutorialkit.dev/src/content/docs/guides/overriding-components.mdx
+++ b/docs/tutorialkit.dev/src/content/docs/guides/overriding-components.mdx
@@ -46,6 +46,7 @@ When overriding `TopBar` you can place TutorialKit's default components using fo
 
 - `logo`: Logo of the application
 - `open-in-stackblitz-link`: Link for opening current lesson in StackBlitz
+- `download-button`: Button for downloading current lesson as `.zip` file
 - `theme-switch`:  Switch for changing the theme
 - `login-button`: For StackBlitz Enterprise user, the login button
 
@@ -61,6 +62,8 @@ When overriding `TopBar` you can place TutorialKit's default components using fo
 
   <LanguageSelect />
 
+  <slot name="download-button" />
+
   <slot name="open-in-stackblitz-link" />
 
   <slot name="login-button" />
diff --git a/docs/tutorialkit.dev/src/content/docs/reference/configuration.mdx b/docs/tutorialkit.dev/src/content/docs/reference/configuration.mdx
index 31707ca94..0c99cdb9d 100644
--- a/docs/tutorialkit.dev/src/content/docs/reference/configuration.mdx
+++ b/docs/tutorialkit.dev/src/content/docs/reference/configuration.mdx
@@ -412,6 +412,20 @@ type TemplateType = "html" | "node" | "angular-cli" | "create-react-app" | "java
 
 ```
 
+### `downloadAsZip`
+Display a button for downloading the current lesson as `.zip` file. Defaults to `false`.
+The default filename is constructed by concatenating folder names of part,  chapter and lesson.
+<PropertyTable inherited type="DownloadAsZip" />
+
+The `DownloadAsZip` type has the following shape:
+
+```ts
+type DownloadAsZip =
+    | boolean
+    | { filename?: string }
+
+```
+
 ##### `meta`
 
 Configures `<meta>` tags for Open Graph protocole and Twitter.
diff --git a/e2e/package.json b/e2e/package.json
index d21b1326c..23f8fcd1b 100644
--- a/e2e/package.json
+++ b/e2e/package.json
@@ -34,6 +34,7 @@
     "playwright": "^1.46.0",
     "react": "^18.3.1",
     "react-dom": "^18.3.1",
-    "unocss": "^0.59.4"
+    "unocss": "^0.59.4",
+    "unzipper": "^0.12.3"
   }
 }
diff --git a/e2e/src/components/TopBar.astro b/e2e/src/components/TopBar.astro
index 099610741..93b00d6e9 100644
--- a/e2e/src/components/TopBar.astro
+++ b/e2e/src/components/TopBar.astro
@@ -7,6 +7,10 @@
 
   <div class="mr-2 color-tk-text-primary">Custom Top Bar Mounted</div>
 
+  <div class="mr-2">
+    <slot name="download-button" />
+  </div>
+
   <div class="mr-2">
     <slot name="open-in-stackblitz-link" />
   </div>
diff --git a/e2e/src/content/tutorial/meta.md b/e2e/src/content/tutorial/meta.md
index 29eef72cd..da65467f9 100644
--- a/e2e/src/content/tutorial/meta.md
+++ b/e2e/src/content/tutorial/meta.md
@@ -2,4 +2,5 @@
 type: tutorial
 mainCommand: ''
 prepareCommands: []
+downloadAsZip: true
 ---
diff --git a/e2e/test/topbar.override-components.test.ts b/e2e/test/topbar.override-components.test.ts
index cb34ba1cf..62bf8d68e 100644
--- a/e2e/test/topbar.override-components.test.ts
+++ b/e2e/test/topbar.override-components.test.ts
@@ -7,6 +7,7 @@ test('developer can override TopBar', async ({ page }) => {
   await expect(nav.getByText('Custom Top Bar Mounted')).toBeVisible();
 
   // default elements should also be visible
+  await expect(nav.getByRole('button', { name: 'Download lesson as zip-file' })).toBeVisible();
   await expect(nav.getByRole('button', { name: 'Open in StackBlitz' })).toBeVisible();
   await expect(nav.getByRole('button', { name: 'Toggle Theme' })).toBeVisible();
 });
diff --git a/e2e/test/topbar.test.ts b/e2e/test/topbar.test.ts
new file mode 100644
index 000000000..07e207923
--- /dev/null
+++ b/e2e/test/topbar.test.ts
@@ -0,0 +1,78 @@
+/// <reference types="node" />
+import { readdirSync, readFileSync, rmSync } from 'node:fs';
+import type { Readable } from 'node:stream';
+import { test, expect } from '@playwright/test';
+import * as unzipper from 'unzipper';
+import { theme } from '../../packages/theme/src/theme';
+
+test('user can change theme', async ({ page }) => {
+  await page.goto('/');
+
+  const heading = page.getByRole('heading', { level: 1 });
+  const html = page.locator('html');
+
+  // default light theme
+  await expect(html).toHaveAttribute('data-theme', 'light');
+  await expect(heading).toHaveCSS('color', hexToRGB(theme.colors.gray[800]));
+
+  await page.getByRole('navigation').getByRole('button', { name: 'Toggle Theme' }).click();
+
+  await expect(html).toHaveAttribute('data-theme', 'dark');
+  await expect(heading).toHaveCSS('color', hexToRGB(theme.colors.gray[200]));
+});
+
+test('user can download project as zip', async ({ page }) => {
+  await page.goto('/', { waitUntil: 'networkidle' });
+
+  const downloadPromise = page.waitForEvent('download');
+  await page.getByRole('navigation').getByRole('button', { name: 'Download lesson as zip-file' }).click();
+
+  const download = await downloadPromise;
+  expect(download.suggestedFilename()).toBe('tests-file-tree-allow-edits-disabled.zip');
+
+  const stream = await download.createReadStream();
+  const files = await unzip(stream);
+
+  expect(files).toMatchObject({
+    './tutorial/file-on-template.js': "export default 'This file is present on template';\n",
+    './tutorial/first-level/file.js': "export default 'File in first level';\n",
+    './tutorial/first-level/second-level/file.js': "export default 'File in second level';\n",
+  });
+
+  expect(files['./tutorial/index.mjs']).toMatch("import http from 'node:http'");
+});
+
+function hexToRGB(hex: string) {
+  return `rgb(${parseInt(hex.slice(1, 3), 16)}, ${parseInt(hex.slice(3, 5), 16)}, ${parseInt(hex.slice(5, 7), 16)})`;
+}
+
+async function unzip(stream: Readable) {
+  await stream.pipe(unzipper.Extract({ path: './downloads' })).promise();
+
+  const files = readDirectoryContents('./downloads');
+  rmSync('./downloads', { recursive: true });
+
+  return files.reduce(
+    (all, current) => ({
+      ...all,
+      [current.name.replace('/downloads', '')]: current.content,
+    }),
+    {},
+  );
+}
+
+function readDirectoryContents(directory: string) {
+  const files: { name: string; content: string }[] = [];
+
+  for (const entry of readdirSync(directory, { withFileTypes: true })) {
+    const name = `${directory}/${entry.name}`;
+
+    if (entry.isFile()) {
+      files.push({ name, content: readFileSync(name, 'utf-8') });
+    } else if (entry.isDirectory()) {
+      files.push(...readDirectoryContents(name));
+    }
+  }
+
+  return files;
+}
diff --git a/packages/astro/package.json b/packages/astro/package.json
index 6ee7c7400..ad6064437 100644
--- a/packages/astro/package.json
+++ b/packages/astro/package.json
@@ -45,7 +45,7 @@
     "@tutorialkit/types": "workspace:*",
     "@types/react": "^18.3.3",
     "@unocss/reset": "^0.62.2",
-    "@webcontainer/api": "1.2.4",
+    "@webcontainer/api": "1.5.1",
     "astro": "^4.15.0",
     "astro-expressive-code": "^0.35.3",
     "chokidar": "3.6.0",
diff --git a/packages/astro/src/default/components/DownloadButton.tsx b/packages/astro/src/default/components/DownloadButton.tsx
new file mode 100644
index 000000000..a0b2f3904
--- /dev/null
+++ b/packages/astro/src/default/components/DownloadButton.tsx
@@ -0,0 +1,44 @@
+import { tutorialStore, webcontainer as webcontainerPromise } from './webcontainer.js';
+
+export function DownloadButton() {
+  return (
+    <button
+      title="Download lesson as zip-file"
+      className="flex items-center text-2xl text-tk-elements-topBar-iconButton-iconColor hover:text-tk-elements-topBar-iconButton-iconColorHover transition-theme bg-tk-elements-topBar-iconButton-backgroundColor hover:bg-tk-elements-topBar-iconButton-backgroundColorHover p-1 rounded-md"
+      onClick={onClick}
+    >
+      <div className="i-ph-download-simple" />
+    </button>
+  );
+}
+
+async function onClick() {
+  const lesson = tutorialStore.lesson;
+
+  if (!lesson) {
+    throw new Error('Missing lesson');
+  }
+
+  const webcontainer = await webcontainerPromise;
+  const data = await webcontainer.export('/home/tutorial', { format: 'zip', excludes: ['node_modules'] });
+
+  let filename =
+    typeof lesson.data.downloadAsZip === 'object'
+      ? lesson.data.downloadAsZip.filename
+      : [lesson.part?.id, lesson.chapter?.id, lesson.id].filter(Boolean).join('-');
+
+  if (!filename.endsWith('.zip')) {
+    filename += '.zip';
+  }
+
+  const link = document.createElement('a');
+  link.style.display = 'none';
+  link.download = filename;
+  link.href = URL.createObjectURL(new Blob([data], { type: 'application/zip' }));
+
+  document.body.appendChild(link);
+  link.click();
+
+  document.body.removeChild(link);
+  URL.revokeObjectURL(link.href);
+}
diff --git a/packages/astro/src/default/components/TopBar.astro b/packages/astro/src/default/components/TopBar.astro
index 861441723..0cfb1bcdc 100644
--- a/packages/astro/src/default/components/TopBar.astro
+++ b/packages/astro/src/default/components/TopBar.astro
@@ -4,6 +4,10 @@
   <div class="flex flex-1">
     <slot name="logo" />
   </div>
+
+  <div class="mr-2">
+    <slot name="download-button" />
+  </div>
   <div class="mr-2">
     <slot name="open-in-stackblitz-link" />
   </div>
diff --git a/packages/astro/src/default/components/TopBarWrapper.astro b/packages/astro/src/default/components/TopBarWrapper.astro
index 1d3b89101..9e7c7aab0 100644
--- a/packages/astro/src/default/components/TopBarWrapper.astro
+++ b/packages/astro/src/default/components/TopBarWrapper.astro
@@ -3,6 +3,7 @@ import { TopBar } from 'tutorialkit:override-components';
 import type { Lesson } from '@tutorialkit/types';
 import { ThemeSwitch } from './ThemeSwitch';
 import { LoginButton } from './LoginButton';
+import { DownloadButton } from './DownloadButton';
 import { OpenInStackblitzLink } from './OpenInStackblitzLink';
 import Logo from './Logo.astro';
 import { useAuth } from './setup';
@@ -10,14 +11,17 @@ import { useAuth } from './setup';
 interface Props {
   logoLink: string;
   openInStackBlitz: Lesson['data']['openInStackBlitz'];
+  downloadAsZip: Lesson['data']['downloadAsZip'];
 }
 
-const { logoLink, openInStackBlitz } = Astro.props;
+const { logoLink, openInStackBlitz, downloadAsZip } = Astro.props;
 ---
 
 <TopBar>
   <Logo slot="logo" logoLink={logoLink ?? '/'} />
 
+  {downloadAsZip && <DownloadButton client:load transition:persist slot="download-button" />}
+
   {openInStackBlitz && <OpenInStackblitzLink client:load transition:persist slot="open-in-stackblitz-link" />}
 
   <ThemeSwitch client:load transition:persist slot="theme-switch" />
diff --git a/packages/astro/src/default/pages/[...slug].astro b/packages/astro/src/default/pages/[...slug].astro
index 1a88bfefb..18b86ff22 100644
--- a/packages/astro/src/default/pages/[...slug].astro
+++ b/packages/astro/src/default/pages/[...slug].astro
@@ -24,9 +24,16 @@ meta.description ??= 'A TutorialKit interactive lesson';
 
 <Layout title={title} meta={meta}>
   <PageLoadingIndicator />
+
   <div id="previews-container" style="display: none;"></div>
+
   <main class="max-w-full flex flex-col h-full overflow-hidden" data-swap-root>
-    <TopBarWrapper logoLink={logoLink ?? '/'} openInStackBlitz={lesson.data.openInStackBlitz} />
+    <TopBarWrapper
+      logoLink={logoLink ?? '/'}
+      openInStackBlitz={lesson.data.openInStackBlitz}
+      downloadAsZip={lesson.data.downloadAsZip}
+    />
+
     <MainContainer lesson={lesson} navList={navList} />
   </main>
 </Layout>
diff --git a/packages/astro/src/default/utils/__snapshots__/multiple-parts.json b/packages/astro/src/default/utils/__snapshots__/multiple-parts.json
index 42fa3bb17..35cce10f0 100644
--- a/packages/astro/src/default/utils/__snapshots__/multiple-parts.json
+++ b/packages/astro/src/default/utils/__snapshots__/multiple-parts.json
@@ -70,7 +70,8 @@
         "i18n": {
           "mocked": "default localization"
         },
-        "openInStackBlitz": true
+        "openInStackBlitz": true,
+        "downloadAsZip": false
       },
       "id": "1-first",
       "filepath": "1-part/1-chapter/1-first/content.md",
@@ -106,7 +107,8 @@
         "i18n": {
           "mocked": "default localization"
         },
-        "openInStackBlitz": true
+        "openInStackBlitz": true,
+        "downloadAsZip": false
       },
       "id": "1-second",
       "filepath": "2-part/2-chapter/1-second/content.md",
@@ -146,7 +148,8 @@
         "i18n": {
           "mocked": "default localization"
         },
-        "openInStackBlitz": true
+        "openInStackBlitz": true,
+        "downloadAsZip": false
       },
       "id": "1-third",
       "filepath": "3-part/3-chapter/1-third/content.md",
diff --git a/packages/astro/src/default/utils/__snapshots__/single-lesson-no-part.json b/packages/astro/src/default/utils/__snapshots__/single-lesson-no-part.json
index 8455d92cc..12488d1a1 100644
--- a/packages/astro/src/default/utils/__snapshots__/single-lesson-no-part.json
+++ b/packages/astro/src/default/utils/__snapshots__/single-lesson-no-part.json
@@ -9,7 +9,8 @@
         "i18n": {
           "mocked": "default localization"
         },
-        "openInStackBlitz": true
+        "openInStackBlitz": true,
+        "downloadAsZip": false
       },
       "id": "1-lesson",
       "filepath": "1-lesson/content.md",
diff --git a/packages/astro/src/default/utils/__snapshots__/single-part-and-lesson-no-chapter.json b/packages/astro/src/default/utils/__snapshots__/single-part-and-lesson-no-chapter.json
index 447e765b3..0fc4b6e12 100644
--- a/packages/astro/src/default/utils/__snapshots__/single-part-and-lesson-no-chapter.json
+++ b/packages/astro/src/default/utils/__snapshots__/single-part-and-lesson-no-chapter.json
@@ -20,7 +20,8 @@
         "i18n": {
           "mocked": "default localization"
         },
-        "openInStackBlitz": true
+        "openInStackBlitz": true,
+        "downloadAsZip": false
       },
       "id": "1-lesson",
       "filepath": "1-part/1-lesson/content.md",
diff --git a/packages/astro/src/default/utils/__snapshots__/single-part-chapter-and-lesson.json b/packages/astro/src/default/utils/__snapshots__/single-part-chapter-and-lesson.json
index bd70e0a18..6b6d4c788 100644
--- a/packages/astro/src/default/utils/__snapshots__/single-part-chapter-and-lesson.json
+++ b/packages/astro/src/default/utils/__snapshots__/single-part-chapter-and-lesson.json
@@ -30,7 +30,8 @@
         "i18n": {
           "mocked": "default localization"
         },
-        "openInStackBlitz": true
+        "openInStackBlitz": true,
+        "downloadAsZip": false
       },
       "id": "1-lesson",
       "filepath": "1-part/1-chapter/1-lesson/content.md",
diff --git a/packages/astro/src/default/utils/__snapshots__/single-part-chapter-and-multiple-lessons.json b/packages/astro/src/default/utils/__snapshots__/single-part-chapter-and-multiple-lessons.json
index 68fc92ca5..73d1c265a 100644
--- a/packages/astro/src/default/utils/__snapshots__/single-part-chapter-and-multiple-lessons.json
+++ b/packages/astro/src/default/utils/__snapshots__/single-part-chapter-and-multiple-lessons.json
@@ -30,7 +30,8 @@
         "i18n": {
           "mocked": "default localization"
         },
-        "openInStackBlitz": true
+        "openInStackBlitz": true,
+        "downloadAsZip": false
       },
       "id": "1-first",
       "filepath": "1-part/1-chapter/1-first/content.md",
@@ -66,7 +67,8 @@
         "i18n": {
           "mocked": "default localization"
         },
-        "openInStackBlitz": true
+        "openInStackBlitz": true,
+        "downloadAsZip": false
       },
       "id": "2-second",
       "filepath": "1-part/1-chapter/2-second/content.md",
@@ -106,7 +108,8 @@
         "i18n": {
           "mocked": "default localization"
         },
-        "openInStackBlitz": true
+        "openInStackBlitz": true,
+        "downloadAsZip": false
       },
       "id": "3-third",
       "filepath": "1-part/1-chapter/3-third/content.md",
diff --git a/packages/astro/src/default/utils/__snapshots__/single-part-multiple-chapters.json b/packages/astro/src/default/utils/__snapshots__/single-part-multiple-chapters.json
index 3504e10f6..b3848743b 100644
--- a/packages/astro/src/default/utils/__snapshots__/single-part-multiple-chapters.json
+++ b/packages/astro/src/default/utils/__snapshots__/single-part-multiple-chapters.json
@@ -48,7 +48,8 @@
         "i18n": {
           "mocked": "default localization"
         },
-        "openInStackBlitz": true
+        "openInStackBlitz": true,
+        "downloadAsZip": false
       },
       "id": "1-first",
       "filepath": "1-part/1-chapter/1-first/content.md",
@@ -84,7 +85,8 @@
         "i18n": {
           "mocked": "default localization"
         },
-        "openInStackBlitz": true
+        "openInStackBlitz": true,
+        "downloadAsZip": false
       },
       "id": "1-second",
       "filepath": "1-part/2-chapter/1-second/content.md",
@@ -124,7 +126,8 @@
         "i18n": {
           "mocked": "default localization"
         },
-        "openInStackBlitz": true
+        "openInStackBlitz": true,
+        "downloadAsZip": false
       },
       "id": "1-third",
       "filepath": "1-part/3-chapter/1-third/content.md",
diff --git a/packages/astro/src/default/utils/content.ts b/packages/astro/src/default/utils/content.ts
index 7567b40c8..9bd2eb1a0 100644
--- a/packages/astro/src/default/utils/content.ts
+++ b/packages/astro/src/default/utils/content.ts
@@ -97,6 +97,7 @@ export async function getTutorial(): Promise<Tutorial> {
         'meta',
         'editPageLink',
         'openInStackBlitz',
+        'downloadAsZip',
         'filesystem',
       ]),
     };
@@ -160,6 +161,7 @@ async function parseCollection(collection: CollectionEntryTutorial[]) {
       tutorialMetaData.template ??= 'default';
       tutorialMetaData.i18n = Object.assign({ ...DEFAULT_LOCALIZATION }, tutorialMetaData.i18n);
       tutorialMetaData.openInStackBlitz ??= true;
+      tutorialMetaData.downloadAsZip ??= false;
 
       tutorial.logoLink = data.logoLink;
     } else if (type === 'part') {
diff --git a/packages/cli/overwrites/src/content/tutorial/meta.md b/packages/cli/overwrites/src/content/tutorial/meta.md
index 86a50e02e..984d22f0f 100644
--- a/packages/cli/overwrites/src/content/tutorial/meta.md
+++ b/packages/cli/overwrites/src/content/tutorial/meta.md
@@ -3,4 +3,5 @@ type: tutorial
 mainCommand: ['npm run dev', 'Starting http server']
 prepareCommands:
   - ['npm install', 'Installing dependencies']
+downloadAsZip: true
 ---
diff --git a/packages/cli/tests/__snapshots__/create-tutorial.test.ts.snap b/packages/cli/tests/__snapshots__/create-tutorial.test.ts.snap
index dd6190482..5560ad7dd 100644
--- a/packages/cli/tests/__snapshots__/create-tutorial.test.ts.snap
+++ b/packages/cli/tests/__snapshots__/create-tutorial.test.ts.snap
@@ -235,6 +235,7 @@ exports[`create and eject a project 1`] = `
   "public/logo.svg",
   "src",
   "src/components",
+  "src/components/DownloadButton.tsx",
   "src/components/HeadTags.astro",
   "src/components/LoginButton.tsx",
   "src/components/Logo.astro",
diff --git a/packages/react/package.json b/packages/react/package.json
index e69a7087e..0d840a590 100644
--- a/packages/react/package.json
+++ b/packages/react/package.json
@@ -85,7 +85,7 @@
     "@replit/codemirror-lang-svelte": "^6.0.0",
     "@tutorialkit/runtime": "workspace:*",
     "@tutorialkit/theme": "workspace:*",
-    "@webcontainer/api": "1.2.4",
+    "@webcontainer/api": "1.5.1",
     "@xterm/addon-fit": "^0.10.0",
     "@xterm/addon-web-links": "^0.11.0",
     "@xterm/xterm": "^5.5.0",
diff --git a/packages/runtime/package.json b/packages/runtime/package.json
index 431081e95..17037585b 100644
--- a/packages/runtime/package.json
+++ b/packages/runtime/package.json
@@ -34,7 +34,7 @@
   },
   "dependencies": {
     "@tutorialkit/types": "workspace:*",
-    "@webcontainer/api": "1.2.4",
+    "@webcontainer/api": "1.5.1",
     "nanostores": "^0.10.3",
     "picomatch": "^4.0.2"
   },
diff --git a/packages/template/src/content/tutorial/1-basics/2-foo/meta.md b/packages/template/src/content/tutorial/1-basics/2-foo/meta.md
index df136d4ab..a0966799b 100644
--- a/packages/template/src/content/tutorial/1-basics/2-foo/meta.md
+++ b/packages/template/src/content/tutorial/1-basics/2-foo/meta.md
@@ -2,4 +2,5 @@
 type: chapter
 title: The second chapter in part 1
 openInStackBlitz: true
+downloadAsZip: true
 ---
diff --git a/packages/template/src/content/tutorial/2-advanced/1-unicorn/meta.md b/packages/template/src/content/tutorial/2-advanced/1-unicorn/meta.md
index 84d84f723..4e76caaf6 100644
--- a/packages/template/src/content/tutorial/2-advanced/1-unicorn/meta.md
+++ b/packages/template/src/content/tutorial/2-advanced/1-unicorn/meta.md
@@ -2,4 +2,5 @@
 type: chapter
 title: The first chatper in part 2
 openInStackBlitz: false
+downloadAsZip: false
 ---
diff --git a/packages/template/src/content/tutorial/meta.md b/packages/template/src/content/tutorial/meta.md
index 96727ddf1..c73ad5872 100644
--- a/packages/template/src/content/tutorial/meta.md
+++ b/packages/template/src/content/tutorial/meta.md
@@ -12,4 +12,6 @@ i18n:
 openInStackBlitz:
   projectTitle: Example Title
   projectDescription: Example Description
+downloadAsZip:
+  filename: custom-lesson-name-without-extension
 ---
diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json
index 8057b322b..30a7d4f80 100644
--- a/packages/test-utils/package.json
+++ b/packages/test-utils/package.json
@@ -5,7 +5,7 @@
   "type": "module",
   "private": true,
   "devDependencies": {
-    "@webcontainer/api": "1.2.4",
+    "@webcontainer/api": "1.5.1",
     "typescript": "^5.4.5",
     "vitest": "^2.1.1"
   }
diff --git a/packages/types/src/schemas/common.ts b/packages/types/src/schemas/common.ts
index 4f3972a40..c669bb92f 100644
--- a/packages/types/src/schemas/common.ts
+++ b/packages/types/src/schemas/common.ts
@@ -292,6 +292,17 @@ export const webcontainerSchema = commandsSchema.extend({
     ])
     .optional()
     .describe('Display a link for opening current lesson in StackBlitz.'),
+  downloadAsZip: z
+    .union([
+      // `false` for disabling the button
+      z.boolean(),
+
+      z.strictObject({
+        filename: z.string(),
+      }),
+    ])
+    .optional()
+    .describe('Display a button for downloading the current lesson as `.zip` file.'),
 });
 
 export const baseSchema = webcontainerSchema.extend({
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 46bfbeabb..3aadc0202 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -25,7 +25,7 @@ importers:
         version: 5.3.0
       commitlint:
         specifier: ^19.3.0
-        version: 19.3.0(@types/node@22.4.2)(typescript@5.5.3)
+        version: 19.3.0(@types/node@22.9.0)(typescript@5.5.3)
       conventional-changelog:
         specifier: ^6.0.0
         version: 6.0.0
@@ -80,7 +80,7 @@ importers:
         version: 18.3.3
       astro:
         specifier: ^4.15.0
-        version: 4.15.0(@types/node@22.4.2)(typescript@5.5.3)
+        version: 4.15.0(@types/node@22.9.0)(typescript@5.5.3)
       prettier-plugin-astro:
         specifier: ^0.14.1
         version: 0.14.1
@@ -94,8 +94,8 @@ importers:
         specifier: workspace:*
         version: link:../../packages/react
       '@webcontainer/api':
-        specifier: 1.2.4
-        version: 1.2.4
+        specifier: 1.5.1
+        version: 1.5.1
       classnames:
         specifier: ^2.5.1
         version: 2.5.1
@@ -132,7 +132,7 @@ importers:
         version: 18.3.0
       astro:
         specifier: ^4.15.0
-        version: 4.15.0(@types/node@22.4.2)(sass@1.77.6)(typescript@5.5.3)
+        version: 4.15.0(@types/node@22.9.0)(sass@1.77.6)(typescript@5.5.3)
       sass:
         specifier: ^1.77.6
         version: 1.77.6
@@ -180,7 +180,7 @@ importers:
         version: link:../packages/types
       '@types/node':
         specifier: ^22.2.0
-        version: 22.4.2
+        version: 22.9.0
       '@types/react':
         specifier: ^18.3.3
         version: 18.3.3
@@ -195,7 +195,7 @@ importers:
         version: 0.62.2
       astro:
         specifier: ^4.15.0
-        version: 4.15.0(@types/node@22.4.2)(typescript@5.5.3)
+        version: 4.15.0(@types/node@22.9.0)(typescript@5.5.3)
       fast-glob:
         specifier: ^3.3.2
         version: 3.3.2
@@ -211,6 +211,9 @@ importers:
       unocss:
         specifier: ^0.59.4
         version: 0.59.4(postcss@8.4.41)(vite@5.4.2)
+      unzipper:
+        specifier: ^0.12.3
+        version: 0.12.3
 
   extensions/vscode:
     dependencies:
@@ -283,7 +286,7 @@ importers:
         version: 3.1.0
       vitest:
         specifier: ^2.1.1
-        version: 2.1.1(@types/node@22.4.2)
+        version: 2.1.1(@types/node@22.9.0)
 
   packages/astro:
     dependencies:
@@ -324,11 +327,11 @@ importers:
         specifier: ^0.62.2
         version: 0.62.3
       '@webcontainer/api':
-        specifier: 1.2.4
-        version: 1.2.4
+        specifier: 1.5.1
+        version: 1.5.1
       astro:
         specifier: ^4.15.0
-        version: 4.15.0(@types/node@22.4.2)(typescript@5.5.3)
+        version: 4.15.0(@types/node@22.9.0)(typescript@5.5.3)
       astro-expressive-code:
         specifier: ^0.35.3
         version: 0.35.3(astro@4.15.0)
@@ -398,7 +401,7 @@ importers:
         version: 0.8.4(vite@5.4.2)
       vitest:
         specifier: ^2.1.1
-        version: 2.1.1(@types/node@22.4.2)
+        version: 2.1.1(@types/node@22.9.0)
 
   packages/cli:
     dependencies:
@@ -561,8 +564,8 @@ importers:
         specifier: workspace:*
         version: link:../theme
       '@webcontainer/api':
-        specifier: 1.2.4
-        version: 1.2.4
+        specifier: 1.5.1
+        version: 1.5.1
       '@xterm/addon-fit':
         specifier: ^0.10.0
         version: 0.10.0(@xterm/xterm@5.5.0)
@@ -614,7 +617,7 @@ importers:
         version: 5.5.3
       vitest:
         specifier: ^2.1.1
-        version: 2.1.1(@types/node@22.4.2)
+        version: 2.1.1(@types/node@22.9.0)
 
   packages/runtime:
     dependencies:
@@ -622,8 +625,8 @@ importers:
         specifier: workspace:*
         version: link:../types
       '@webcontainer/api':
-        specifier: 1.2.4
-        version: 1.2.4
+        specifier: 1.5.1
+        version: 1.5.1
       nanostores:
         specifier: ^0.10.3
         version: 0.10.3
@@ -639,13 +642,13 @@ importers:
         version: 5.5.3
       vite:
         specifier: ^5.3.1
-        version: 5.3.4(@types/node@22.4.2)
+        version: 5.3.4(@types/node@22.9.0)
       vite-tsconfig-paths:
         specifier: ^4.3.2
         version: 4.3.2(typescript@5.5.3)(vite@5.3.4)
       vitest:
         specifier: ^2.1.1
-        version: 2.1.1(@types/node@22.4.2)
+        version: 2.1.1(@types/node@22.9.0)
 
   packages/template:
     dependencies:
@@ -693,14 +696,14 @@ importers:
   packages/test-utils:
     devDependencies:
       '@webcontainer/api':
-        specifier: 1.2.4
-        version: 1.2.4
+        specifier: 1.5.1
+        version: 1.5.1
       typescript:
         specifier: ^5.4.5
         version: 5.5.3
       vitest:
         specifier: ^2.1.1
-        version: 2.1.1(@types/node@22.4.2)
+        version: 2.1.1(@types/node@22.9.0)
 
   packages/theme:
     dependencies:
@@ -738,7 +741,7 @@ importers:
         version: 5.5.3
       vitest:
         specifier: ^2.1.1
-        version: 2.1.1(@types/node@22.4.2)
+        version: 2.1.1(@types/node@22.9.0)
 
 packages:
 
@@ -877,7 +880,7 @@ packages:
       '@astrojs/markdown-remark': 5.1.0
       '@mdx-js/mdx': 3.0.1
       acorn: 8.12.0
-      astro: 4.15.0(@types/node@22.4.2)(typescript@5.5.3)
+      astro: 4.15.0(@types/node@22.9.0)(typescript@5.5.3)
       es-module-lexer: 1.5.3
       estree-util-visit: 2.0.0
       github-slugger: 2.0.0
@@ -936,7 +939,7 @@ packages:
       '@pagefind/default-ui': 1.1.0
       '@types/hast': 3.0.4
       '@types/mdast': 4.0.4
-      astro: 4.15.0(@types/node@22.4.2)(sass@1.77.6)(typescript@5.5.3)
+      astro: 4.15.0(@types/node@22.9.0)(sass@1.77.6)(typescript@5.5.3)
       astro-expressive-code: 0.35.3(astro@4.15.0)
       bcp-47: 2.1.0
       hast-util-from-html: 2.0.1
@@ -1663,14 +1666,14 @@ packages:
       style-mod: 4.1.2
       w3c-keyname: 2.2.8
 
-  /@commitlint/cli@19.3.0(@types/node@22.4.2)(typescript@5.5.3):
+  /@commitlint/cli@19.3.0(@types/node@22.9.0)(typescript@5.5.3):
     resolution: {integrity: sha512-LgYWOwuDR7BSTQ9OLZ12m7F/qhNY+NpAyPBgo4YNMkACE7lGuUnuQq1yi9hz1KA4+3VqpOYl8H1rY/LYK43v7g==}
     engines: {node: '>=v18'}
     hasBin: true
     dependencies:
       '@commitlint/format': 19.3.0
       '@commitlint/lint': 19.2.2
-      '@commitlint/load': 19.2.0(@types/node@22.4.2)(typescript@5.5.3)
+      '@commitlint/load': 19.2.0(@types/node@22.9.0)(typescript@5.5.3)
       '@commitlint/read': 19.2.1
       '@commitlint/types': 19.0.3
       execa: 8.0.1
@@ -1739,7 +1742,7 @@ packages:
       '@commitlint/types': 19.0.3
     dev: true
 
-  /@commitlint/load@19.2.0(@types/node@22.4.2)(typescript@5.5.3):
+  /@commitlint/load@19.2.0(@types/node@22.9.0)(typescript@5.5.3):
     resolution: {integrity: sha512-XvxxLJTKqZojCxaBQ7u92qQLFMMZc4+p9qrIq/9kJDy8DOrEa7P1yx7Tjdc2u2JxIalqT4KOGraVgCE7eCYJyQ==}
     engines: {node: '>=v18'}
     dependencies:
@@ -1749,7 +1752,7 @@ packages:
       '@commitlint/types': 19.0.3
       chalk: 5.3.0
       cosmiconfig: 9.0.0(typescript@5.5.3)
-      cosmiconfig-typescript-loader: 5.0.0(@types/node@22.4.2)(cosmiconfig@9.0.0)(typescript@5.5.3)
+      cosmiconfig-typescript-loader: 5.0.0(@types/node@22.9.0)(cosmiconfig@9.0.0)(typescript@5.5.3)
       lodash.isplainobject: 4.0.6
       lodash.merge: 4.6.2
       lodash.uniq: 4.5.0
@@ -3701,7 +3704,7 @@ packages:
   /@types/conventional-commits-parser@5.0.0:
     resolution: {integrity: sha512-loB369iXNmAZglwWATL+WRe+CRMmmBPtpolYzIebFaX4YA3x+BEfLqhUAV9WanycKI3TG1IMr5bMJDajDKLlUQ==}
     dependencies:
-      '@types/node': 22.4.2
+      '@types/node': 22.9.0
     dev: true
 
   /@types/cookie@0.6.0:
@@ -3731,7 +3734,7 @@ packages:
     resolution: {integrity: sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==}
     dependencies:
       '@types/jsonfile': 6.1.4
-      '@types/node': 20.14.11
+      '@types/node': 22.9.0
     dev: true
 
   /@types/gtag.js@0.0.20:
@@ -3750,7 +3753,7 @@ packages:
   /@types/jsonfile@6.1.4:
     resolution: {integrity: sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==}
     dependencies:
-      '@types/node': 22.4.2
+      '@types/node': 22.9.0
     dev: true
 
   /@types/mdast@4.0.4:
@@ -3788,6 +3791,12 @@ packages:
     resolution: {integrity: sha512-nAvM3Ey230/XzxtyDcJ+VjvlzpzoHwLsF7JaDRfoI0ytO0mVheerNmM45CtA0yOILXwXXxOrcUWH3wltX+7PSw==}
     dependencies:
       undici-types: 6.19.8
+    dev: true
+
+  /@types/node@22.9.0:
+    resolution: {integrity: sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ==}
+    dependencies:
+      undici-types: 6.19.8
 
   /@types/normalize-package-data@2.4.4:
     resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==}
@@ -3814,7 +3823,7 @@ packages:
   /@types/sax@1.2.7:
     resolution: {integrity: sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==}
     dependencies:
-      '@types/node': 22.4.2
+      '@types/node': 22.9.0
     dev: true
 
   /@types/semver@7.5.8:
@@ -4018,7 +4027,7 @@ packages:
       '@unocss/core': 0.59.4
       '@unocss/reset': 0.59.4
       '@unocss/vite': 0.59.4(vite@5.4.2)
-      vite: 5.4.2(@types/node@22.4.2)(sass@1.77.6)
+      vite: 5.4.2(@types/node@22.9.0)(sass@1.77.6)
     transitivePeerDependencies:
       - rollup
 
@@ -4217,7 +4226,7 @@ packages:
       chokidar: 3.6.0
       fast-glob: 3.3.2
       magic-string: 0.30.11
-      vite: 5.4.2(@types/node@22.4.2)(sass@1.77.6)
+      vite: 5.4.2(@types/node@22.9.0)(sass@1.77.6)
     transitivePeerDependencies:
       - rollup
 
@@ -4232,7 +4241,7 @@ packages:
       '@babel/plugin-transform-react-jsx-source': 7.24.7(@babel/core@7.24.7)
       '@types/babel__core': 7.20.5
       react-refresh: 0.14.2
-      vite: 5.4.2(@types/node@22.4.2)
+      vite: 5.4.2(@types/node@22.9.0)
     transitivePeerDependencies:
       - supports-color
 
@@ -4259,7 +4268,7 @@ packages:
       '@vitest/spy': 2.1.1
       estree-walker: 3.0.3
       magic-string: 0.30.11
-      vite: 5.4.2(@types/node@22.4.2)
+      vite: 5.4.2(@types/node@22.9.0)
 
   /@vitest/pretty-format@2.1.1:
     resolution: {integrity: sha512-SjxPFOtuINDUW8/UkElJYQSFtnWX7tMksSGW0vfjxMneFqxVr8YJ979QpMbDW7g+BIiq88RAGDjf7en6rvLPPQ==}
@@ -4425,8 +4434,8 @@ packages:
     resolution: {integrity: sha512-KYSIHVmslkaCDyw013pphY+d7x1qV8IZupYfeIfzNA+nsaWHbn5uPuQRvdRFsa9zFzGeudPuoGoZ1Op4jrJXIQ==}
     dev: true
 
-  /@webcontainer/api@1.2.4:
-    resolution: {integrity: sha512-vV42eKuat5QGz7agFJupT5sZj0CHOj/gg6J3/HanvgOVETt7gupzR+iuVNHwudS3yuW+x78Ai7T6fwvV7uBThQ==}
+  /@webcontainer/api@1.5.1:
+    resolution: {integrity: sha512-+ELk+TbTOUx0LawAUdB+nnxaofg/FxUXo/Ac/+CzHSP3SOc3ebBAW3fLo4UZfvJdUW+ygWZOiQMthPLQXvKZEg==}
 
   /@xterm/addon-fit@0.10.0(@xterm/xterm@5.5.0):
     resolution: {integrity: sha512-UFYkDm4HUahf2lnEyHvio51TNGiLK66mqP2JoATy7hRZeXaGMRDr00JiSF7m63vR5WKATF605yEggJKsw0JpMQ==}
@@ -4610,7 +4619,7 @@ packages:
     peerDependencies:
       astro: ^4.0.0-beta || ^3.3.0
     dependencies:
-      astro: 4.15.0(@types/node@22.4.2)(typescript@5.5.3)
+      astro: 4.15.0(@types/node@22.9.0)(typescript@5.5.3)
       rehype-expressive-code: 0.35.3
 
   /astro@4.15.0(@types/node@20.14.11)(typescript@5.5.3):
@@ -4699,7 +4708,7 @@ packages:
       - typescript
     dev: true
 
-  /astro@4.15.0(@types/node@22.4.2)(sass@1.77.6)(typescript@5.5.3):
+  /astro@4.15.0(@types/node@22.9.0)(sass@1.77.6)(typescript@5.5.3):
     resolution: {integrity: sha512-bL2ol1+j1Xf/7Q8DQSWP1BfkBd6RkkgVsmp9TCzYklqPSeInpAYGGsAgi+SY7Sf40Vk9o+ku6Zl1zav4MLN4uA==}
     engines: {node: ^18.17.1 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0'}
     hasBin: true
@@ -4761,7 +4770,7 @@ packages:
       tsconfck: 3.1.1(typescript@5.5.3)
       unist-util-visit: 5.0.0
       vfile: 6.0.3
-      vite: 5.4.2(@types/node@22.4.2)(sass@1.77.6)
+      vite: 5.4.2(@types/node@22.9.0)(sass@1.77.6)
       vitefu: 0.2.5(vite@5.4.2)
       which-pm: 3.0.0
       xxhash-wasm: 1.0.2
@@ -4785,7 +4794,7 @@ packages:
       - typescript
     dev: true
 
-  /astro@4.15.0(@types/node@22.4.2)(typescript@5.5.3):
+  /astro@4.15.0(@types/node@22.9.0)(typescript@5.5.3):
     resolution: {integrity: sha512-bL2ol1+j1Xf/7Q8DQSWP1BfkBd6RkkgVsmp9TCzYklqPSeInpAYGGsAgi+SY7Sf40Vk9o+ku6Zl1zav4MLN4uA==}
     engines: {node: ^18.17.1 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0'}
     hasBin: true
@@ -4847,7 +4856,7 @@ packages:
       tsconfck: 3.1.1(typescript@5.5.3)
       unist-util-visit: 5.0.0
       vfile: 6.0.3
-      vite: 5.4.2(@types/node@22.4.2)
+      vite: 5.4.2(@types/node@22.9.0)
       vitefu: 0.2.5(vite@5.4.2)
       which-pm: 3.0.0
       xxhash-wasm: 1.0.2
@@ -4962,6 +4971,10 @@ packages:
       readable-stream: 3.6.2
     dev: true
 
+  /bluebird@3.7.2:
+    resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==}
+    dev: true
+
   /boolbase@1.0.0:
     resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
 
@@ -5200,6 +5213,7 @@ packages:
   /color-convert@2.0.1:
     resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
     engines: {node: '>=7.0.0'}
+    requiresBuild: true
     dependencies:
       color-name: 1.1.4
 
@@ -5231,12 +5245,12 @@ packages:
   /comma-separated-tokens@2.0.3:
     resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==}
 
-  /commitlint@19.3.0(@types/node@22.4.2)(typescript@5.5.3):
+  /commitlint@19.3.0(@types/node@22.9.0)(typescript@5.5.3):
     resolution: {integrity: sha512-B8eUVQCjz+1ZAjR3LC3+vzKg7c4/qN4QhSxkjp0u0v7Pi79t9CsnGAluvveKmFh56e885zgToPL5ax+l8BHTPg==}
     engines: {node: '>=v18'}
     hasBin: true
     dependencies:
-      '@commitlint/cli': 19.3.0(@types/node@22.4.2)(typescript@5.5.3)
+      '@commitlint/cli': 19.3.0(@types/node@22.9.0)(typescript@5.5.3)
       '@commitlint/types': 19.0.3
     transitivePeerDependencies:
       - '@types/node'
@@ -5425,7 +5439,11 @@ packages:
       browserslist: 4.23.3
     dev: true
 
-  /cosmiconfig-typescript-loader@5.0.0(@types/node@22.4.2)(cosmiconfig@9.0.0)(typescript@5.5.3):
+  /core-util-is@1.0.3:
+    resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
+    dev: true
+
+  /cosmiconfig-typescript-loader@5.0.0(@types/node@22.9.0)(cosmiconfig@9.0.0)(typescript@5.5.3):
     resolution: {integrity: sha512-+8cK7jRAReYkMwMiG+bxhcNKiHJDM6bR9FD/nGBXOWdMLuYawjF5cGrtLilJ+LGd3ZjCXnJjR5DkfWPoIVlqJA==}
     engines: {node: '>=v16'}
     peerDependencies:
@@ -5433,7 +5451,7 @@ packages:
       cosmiconfig: '>=8.2'
       typescript: '>=4'
     dependencies:
-      '@types/node': 22.4.2
+      '@types/node': 22.9.0
       cosmiconfig: 9.0.0(typescript@5.5.3)
       jiti: 1.21.6
       typescript: 5.5.3
@@ -5645,6 +5663,12 @@ packages:
     resolution: {integrity: sha512-20TuZZHCEZ2O71q9/+8BwKwZ0QtD9D8ObhrihJPr+vLLYlSuAU3/zL4cSlgbfeoGHTjCSJBa7NGcrF9/Bx/WJQ==}
     engines: {node: '>=4'}
 
+  /duplexer2@0.1.4:
+    resolution: {integrity: sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==}
+    dependencies:
+      readable-stream: 2.3.8
+    dev: true
+
   /duplexer@0.1.2:
     resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==}
 
@@ -6948,6 +6972,10 @@ packages:
     dependencies:
       is-inside-container: 1.0.0
 
+  /isarray@1.0.0:
+    resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
+    dev: true
+
   /isexe@2.0.0:
     resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
 
@@ -7869,6 +7897,10 @@ packages:
   /node-fetch-native@1.6.4:
     resolution: {integrity: sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ==}
 
+  /node-int64@0.4.0:
+    resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==}
+    dev: true
+
   /node-releases@2.0.14:
     resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==}
 
@@ -8323,6 +8355,10 @@ packages:
     resolution: {integrity: sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==}
     engines: {node: '>=6'}
 
+  /process-nextick-args@2.0.1:
+    resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
+    dev: true
+
   /prompts@2.4.2:
     resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==}
     engines: {node: '>= 6'}
@@ -8481,6 +8517,18 @@ packages:
       unicorn-magic: 0.1.0
     dev: true
 
+  /readable-stream@2.3.8:
+    resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==}
+    dependencies:
+      core-util-is: 1.0.3
+      inherits: 2.0.4
+      isarray: 1.0.0
+      process-nextick-args: 2.0.1
+      safe-buffer: 5.1.2
+      string_decoder: 1.1.1
+      util-deprecate: 1.0.2
+    dev: true
+
   /readable-stream@3.6.2:
     resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
     engines: {node: '>= 6'}
@@ -8822,6 +8870,10 @@ packages:
     resolution: {integrity: sha512-AUNrbEUHeKY8XsYr/DYpl+qk5+aM+DChopnWOPEzn8YKzOhv4l2zH6LzZms3tOZP3wwdOyc0RmTciyi46HLIuA==}
     dev: true
 
+  /safe-buffer@5.1.2:
+    resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==}
+    dev: true
+
   /safe-buffer@5.2.1:
     resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
     dev: true
@@ -9058,7 +9110,7 @@ packages:
       astro: '>=4.0.0'
     dependencies:
       '@astrojs/starlight': 0.23.4(astro@4.15.0)
-      astro: 4.15.0(@types/node@22.4.2)(sass@1.77.6)(typescript@5.5.3)
+      astro: 4.15.0(@types/node@22.9.0)(sass@1.77.6)(typescript@5.5.3)
       github-slugger: 2.0.0
       hast-util-from-html: 2.0.1
       hast-util-has-property: 3.0.0
@@ -9114,6 +9166,12 @@ packages:
       get-east-asian-width: 1.2.0
       strip-ansi: 7.1.0
 
+  /string_decoder@1.1.1:
+    resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==}
+    dependencies:
+      safe-buffer: 5.1.2
+    dev: true
+
   /string_decoder@1.3.0:
     resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
     dependencies:
@@ -9387,6 +9445,7 @@ packages:
 
   /tslib@2.6.3:
     resolution: {integrity: sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==}
+    requiresBuild: true
 
   /tunnel-agent@0.6.0:
     resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==}
@@ -9648,12 +9707,22 @@ packages:
       '@unocss/transformer-directives': 0.59.4
       '@unocss/transformer-variant-group': 0.59.4
       '@unocss/vite': 0.59.4(vite@5.4.2)
-      vite: 5.4.2(@types/node@22.4.2)(sass@1.77.6)
+      vite: 5.4.2(@types/node@22.9.0)(sass@1.77.6)
     transitivePeerDependencies:
       - postcss
       - rollup
       - supports-color
 
+  /unzipper@0.12.3:
+    resolution: {integrity: sha512-PZ8hTS+AqcGxsaQntl3IRBw65QrBI6lxzqDEL7IAo/XCEqRTKGfOX56Vea5TH9SZczRVxuzk1re04z/YjuYCJA==}
+    dependencies:
+      bluebird: 3.7.2
+      duplexer2: 0.1.4
+      fs-extra: 11.2.0
+      graceful-fs: 4.2.11
+      node-int64: 0.4.0
+    dev: true
+
   /update-browserslist-db@1.0.16(browserslist@4.23.1):
     resolution: {integrity: sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==}
     hasBin: true
@@ -9800,6 +9869,27 @@ packages:
       - sugarss
       - supports-color
       - terser
+    dev: true
+
+  /vite-node@2.1.1(@types/node@22.9.0):
+    resolution: {integrity: sha512-N/mGckI1suG/5wQI35XeR9rsMsPqKXzq1CdUndzVstBj/HvyxxGctwnK6WX43NGt5L3Z5tcRf83g4TITKJhPrA==}
+    engines: {node: ^18.0.0 || >=20.0.0}
+    hasBin: true
+    dependencies:
+      cac: 6.7.14
+      debug: 4.3.6
+      pathe: 1.1.2
+      vite: 5.4.2(@types/node@22.9.0)
+    transitivePeerDependencies:
+      - '@types/node'
+      - less
+      - lightningcss
+      - sass
+      - sass-embedded
+      - stylus
+      - sugarss
+      - supports-color
+      - terser
 
   /vite-plugin-inspect@0.8.4(vite@5.4.2):
     resolution: {integrity: sha512-G0N3rjfw+AiiwnGw50KlObIHYWfulVwaCBUBLh2xTW9G1eM9ocE5olXkEYUbwyTmX+azM8duubi+9w5awdCz+g==}
@@ -9820,7 +9910,7 @@ packages:
       perfect-debounce: 1.0.0
       picocolors: 1.0.1
       sirv: 2.0.4
-      vite: 5.4.2(@types/node@22.4.2)
+      vite: 5.4.2(@types/node@22.9.0)
     transitivePeerDependencies:
       - rollup
       - supports-color
@@ -9837,13 +9927,13 @@ packages:
       debug: 4.3.5
       globrex: 0.1.2
       tsconfck: 3.1.0(typescript@5.5.3)
-      vite: 5.3.4(@types/node@22.4.2)
+      vite: 5.3.4(@types/node@22.9.0)
     transitivePeerDependencies:
       - supports-color
       - typescript
     dev: true
 
-  /vite@5.3.4(@types/node@22.4.2):
+  /vite@5.3.4(@types/node@22.9.0):
     resolution: {integrity: sha512-Cw+7zL3ZG9/NZBB8C+8QbQZmR54GwqIz+WMI4b3JgdYJvX+ny9AjJXqkGQlDXSXRP9rP0B4tbciRMOVEKulVOA==}
     engines: {node: ^18.0.0 || >=20.0.0}
     hasBin: true
@@ -9871,7 +9961,7 @@ packages:
       terser:
         optional: true
     dependencies:
-      '@types/node': 22.4.2
+      '@types/node': 22.9.0
       esbuild: 0.21.5
       postcss: 8.4.39
       rollup: 4.18.1
@@ -9955,8 +10045,9 @@ packages:
       rollup: 4.21.1
     optionalDependencies:
       fsevents: 2.3.3
+    dev: true
 
-  /vite@5.4.2(@types/node@22.4.2)(sass@1.77.6):
+  /vite@5.4.2(@types/node@22.9.0):
     resolution: {integrity: sha512-dDrQTRHp5C1fTFzcSaMxjk6vdpKvT+2/mIdE07Gw2ykehT49O0z/VHS3zZ8iV/Gh8BJJKHWOe5RjaNrW5xf/GA==}
     engines: {node: ^18.0.0 || >=20.0.0}
     hasBin: true
@@ -9987,7 +10078,45 @@ packages:
       terser:
         optional: true
     dependencies:
-      '@types/node': 22.4.2
+      '@types/node': 22.9.0
+      esbuild: 0.21.5
+      postcss: 8.4.41
+      rollup: 4.21.1
+    optionalDependencies:
+      fsevents: 2.3.3
+
+  /vite@5.4.2(@types/node@22.9.0)(sass@1.77.6):
+    resolution: {integrity: sha512-dDrQTRHp5C1fTFzcSaMxjk6vdpKvT+2/mIdE07Gw2ykehT49O0z/VHS3zZ8iV/Gh8BJJKHWOe5RjaNrW5xf/GA==}
+    engines: {node: ^18.0.0 || >=20.0.0}
+    hasBin: true
+    peerDependencies:
+      '@types/node': ^18.0.0 || >=20.0.0
+      less: '*'
+      lightningcss: ^1.21.0
+      sass: '*'
+      sass-embedded: '*'
+      stylus: '*'
+      sugarss: '*'
+      terser: ^5.4.0
+    peerDependenciesMeta:
+      '@types/node':
+        optional: true
+      less:
+        optional: true
+      lightningcss:
+        optional: true
+      sass:
+        optional: true
+      sass-embedded:
+        optional: true
+      stylus:
+        optional: true
+      sugarss:
+        optional: true
+      terser:
+        optional: true
+    dependencies:
+      '@types/node': 22.9.0
       esbuild: 0.21.5
       postcss: 8.4.41
       rollup: 4.21.1
@@ -10003,7 +10132,7 @@ packages:
       vite:
         optional: true
     dependencies:
-      vite: 5.4.2(@types/node@22.4.2)
+      vite: 5.4.2(@types/node@22.9.0)
 
   /vitest@2.1.1(@types/node@20.14.11):
     resolution: {integrity: sha512-97We7/VC0e9X5zBVkvt7SGQMGrRtn3KtySFQG5fpaMlS+l62eeXRQO633AYhSTC3z7IMebnPPNjGXVGNRFlxBA==}
@@ -10117,6 +10246,63 @@ packages:
       - sugarss
       - supports-color
       - terser
+    dev: true
+
+  /vitest@2.1.1(@types/node@22.9.0):
+    resolution: {integrity: sha512-97We7/VC0e9X5zBVkvt7SGQMGrRtn3KtySFQG5fpaMlS+l62eeXRQO633AYhSTC3z7IMebnPPNjGXVGNRFlxBA==}
+    engines: {node: ^18.0.0 || >=20.0.0}
+    hasBin: true
+    peerDependencies:
+      '@edge-runtime/vm': '*'
+      '@types/node': ^18.0.0 || >=20.0.0
+      '@vitest/browser': 2.1.1
+      '@vitest/ui': 2.1.1
+      happy-dom: '*'
+      jsdom: '*'
+    peerDependenciesMeta:
+      '@edge-runtime/vm':
+        optional: true
+      '@types/node':
+        optional: true
+      '@vitest/browser':
+        optional: true
+      '@vitest/ui':
+        optional: true
+      happy-dom:
+        optional: true
+      jsdom:
+        optional: true
+    dependencies:
+      '@types/node': 22.9.0
+      '@vitest/expect': 2.1.1
+      '@vitest/mocker': 2.1.1(@vitest/spy@2.1.1)(vite@5.4.2)
+      '@vitest/pretty-format': 2.1.1
+      '@vitest/runner': 2.1.1
+      '@vitest/snapshot': 2.1.1
+      '@vitest/spy': 2.1.1
+      '@vitest/utils': 2.1.1
+      chai: 5.1.1
+      debug: 4.3.6
+      magic-string: 0.30.11
+      pathe: 1.1.2
+      std-env: 3.7.0
+      tinybench: 2.9.0
+      tinyexec: 0.3.0
+      tinypool: 1.0.1
+      tinyrainbow: 1.2.0
+      vite: 5.4.2(@types/node@22.9.0)
+      vite-node: 2.1.1(@types/node@22.9.0)
+      why-is-node-running: 2.3.0
+    transitivePeerDependencies:
+      - less
+      - lightningcss
+      - msw
+      - sass
+      - sass-embedded
+      - stylus
+      - sugarss
+      - supports-color
+      - terser
 
   /volar-service-css@0.0.59(@volar/language-service@2.4.0-alpha.16):
     resolution: {integrity: sha512-gLNjJnECbalPvQB7qeJjhkDN8sR5M3ItbVYjnyio61aHaWptIiXm/HfDahcQ2ApwmvWidkMWWegjGq5L0BENDA==}