chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:22:17 +08:00
commit 4d7ba00a23
95 changed files with 6907 additions and 0 deletions
+42
View File
@@ -0,0 +1,42 @@
version: 2.1
jobs:
deploy:
docker:
- image: cimg/node:22.9
resource_class: medium
steps:
- add_ssh_keys:
fingerprints:
- "86:3b:c9:a6:d1:b9:a8:dc:0e:00:db:99:8d:19:c4:3e"
- run:
name: Add known hosts
command: |
mkdir -p ~/.ssh
echo $GH_HOST >> ~/.ssh/known_hosts
echo $HAVANA_HOST >> ~/.ssh/known_hosts
- run:
name: Install NPM
command: |
sudo apt-get update && sudo apt-get install -y rsync npm
- run:
name: Clone WebVM
command: |
git clone --branch $CIRCLE_BRANCH --single-branch git@github.com:leaningtech/webvm.git
- run:
name: Build WebVM
command: |
cd webvm/
npm install
npm run build
- run:
name: Deploy webvm
command: |
rsync -avz --chown circleci:runtimes webvm/build/ circleci@havana.leaningtech.com:webvm/
workflows:
deploy:
when:
equal: [ << pipeline.trigger_source >>, "api" ]
jobs:
- deploy
+245
View File
@@ -0,0 +1,245 @@
name: Deploy
# Define when the workflow should run
on:
# Allow manual triggering of the workflow from the Actions tab
workflow_dispatch:
# Allow inputs to be passed when manually triggering the workflow from the Actions tab
inputs:
DOCKERFILE_PATH:
type: string
description: 'Path to the Dockerfile'
required: true
default: 'dockerfiles/debian_mini'
IMAGE_SIZE:
type: string
description: 'Image size, 950M max'
required: true
default: '750M'
DEPLOY_TO_GITHUB_PAGES:
type: boolean
description: 'Deploy to Github pages'
required: true
default: true
GITHUB_RELEASE:
type: boolean
description: 'Upload GitHub release'
required: true
default: false
jobs:
guard_clause:
runs-on: ubuntu-latest
env:
GH_TOKEN: ${{ github.token }} # As required by the GitHub-CLI
permissions:
actions: 'write' # Required in order to terminate the workflow run.
steps:
- uses: actions/checkout@v4
# Guard clause that cancels the workflow in case of an invalid DOCKERFILE_PATH and/or incorrectly configured Github Pages.
# The main reason for choosing this workaround for aborting the workflow is the fact that it does not display the workflow as successful, which can set false expectations.
- name: DOCKERFILE_PATH.
shell: bash
run: |
# We check whether the Dockerfile_path is valid.
if [ ! -f ${{ github.event.inputs.DOCKERFILE_PATH }} ]; then
echo "::error title=Invalid Dockerfile path::No file found at ${{ github.event.inputs.DOCKERFILE_PATH }}"
echo "terminate=true" >> $GITHUB_ENV
fi
- name: Github Pages config guard clause
if: ${{ github.event.inputs.DEPLOY_TO_GITHUB_PAGES == 'true' }}
run: |
# We use the Github Rest api to get information regarding pages for the Github Repository and store it into a temporary file named "pages_response".
set +e
gh api \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
/repos/${{ github.repository_owner }}/$(basename ${{ github.repository }})/pages > pages_response
# We make sure Github Pages has been enabled for this repository.
if [ "$?" -ne 0 ]; then
echo "::error title=Potential pages configuration error.::Please make sure you have enabled Github pages for the ${{ github.repository }} repository. If already enabled then Github pages might be down"
echo "terminate=true" >> $GITHUB_ENV
fi
set -e
# We make sure the Github pages build & deployment source is set to "workflow" (Github Actions). Instead of a "legacy" (branch).
if [[ "$(jq --compact-output --raw-output .build_type pages_response)" != "workflow" ]]; then
echo "Undefined behaviour, Make sure the Github Pages source is correctly configured in the Github Pages settings."
echo "::error title=Pages configuration error.::Please make sure you have correctly picked \"Github Actions\" as the build and deployment source for the Github Pages."
echo "terminate=true" >> $GITHUB_ENV
fi
rm pages_response
- name: Terminate run if error occurred.
run: |
if [[ $terminate == "true" ]]; then
gh run cancel ${{ github.run_id }}
gh run watch ${{ github.run_id }}
fi
build:
needs: guard_clause # Dependency
runs-on: ubuntu-latest # Image to run the worker on.
env:
TAG: "ext2-webvm-base-image" # Tag of docker image.
IMAGE_SIZE: '${{ github.event.inputs.IMAGE_SIZE }}'
DEPLOY_DIR: /webvm_deploy/ # Path to directory where we host the final image from.
permissions: # Permissions to grant the GITHUB_TOKEN.
contents: write # Required permission to make a github release.
steps:
# Checks-out our repository under $GITHUB_WORKSPACE, so our job can access it
- uses: actions/checkout@v4
# Setting the IMAGE_NAME variable in GITHUB_ENV to <Dockerfile name>_<date>_<run_id>.ext2.
- name: Generate the image_name.
id: image_name_gen
run: |
echo "IMAGE_NAME=$(basename ${{ github.event.inputs.DOCKERFILE_PATH }})_$(date +%Y%m%d)_${{ github.run_id }}.ext2" >> $GITHUB_ENV
# Create directory to host the image from.
- run: sudo mkdir -p $DEPLOY_DIR
# Build the i386 Dockerfile image.
- run: docker build . --tag $TAG --file ${{ github.event.inputs.DOCKERFILE_PATH }} --platform=i386
# Run the docker image so that we can export the container.
# Run the Docker container with the Google Public DNS nameservers: 8.8.8.8, 8.8.4.4
- run: |
docker run --dns 8.8.8.8 --dns 8.8.4.4 -d $TAG
echo "CONTAINER_ID=$(sudo docker ps -aq)" >> $GITHUB_ENV
# We extract the CMD, we first need to figure whether the Dockerfile uses CMD or an Entrypoint.
- name: Extracting CMD / Entrypoint and args
shell: bash
run: |
cmd=$(sudo docker inspect --format='{{json .Config.Cmd}}' $CONTAINER_ID)
entrypoint=$(sudo docker inspect --format='{{json .Config.Entrypoint}}' $CONTAINER_ID)
if [[ $entrypoint != "null" && $cmd != "null" ]]; then
echo "CMD=$( sudo docker inspect $CONTAINER_ID | jq --compact-output '.[0].Config.Entrypoint' )" >> $GITHUB_ENV
echo "ARGS=$( sudo docker inspect $CONTAINER_ID | jq --compact-output '.[0].Config.Cmd' )" >> $GITHUB_ENV
elif [[ $cmd != "null" ]]; then
echo "CMD=$( sudo docker inspect $CONTAINER_ID | jq --compact-output '.[0].Config.Cmd[:1]' )" >> $GITHUB_ENV
echo "ARGS=$( sudo docker inspect $CONTAINER_ID | jq --compact-output '.[0].Config.Cmd[1:]' )" >> $GITHUB_ENV
else
echo "CMD=$( sudo docker inspect $CONTAINER_ID | jq --compact-output '.[0].Config.Entrypoint[:1]' )" >> $GITHUB_ENV
echo "ARGS=$( sudo docker inspect $CONTAINER_ID | jq --compact-output '.[0].Config.Entrypoint[1:]' )" >> $GITHUB_ENV
fi
# We extract the ENV, CMD/Entrypoint and cwd from the Docker container with docker inspect.
- name: Extracting env, args and cwd.
shell: bash
run: |
echo "ENV=$( sudo docker inspect $CONTAINER_ID | jq --compact-output '.[0].Config.Env' )" >> $GITHUB_ENV
echo "CWD=$( sudo docker inspect $CONTAINER_ID | jq --compact-output '.[0].Config.WorkingDir' )" >> $GITHUB_ENV
# We create and mount the base ext2 image to extract the Docker container's filesystem its contents into.
- name: Create ext2 image.
run: |
# Preallocate space for the ext2 image
sudo fallocate -l $IMAGE_SIZE ${IMAGE_NAME}
# Format to ext2 linux kernel revision 0
sudo mkfs.ext2 -r 0 ${IMAGE_NAME}
# Mount the ext2 image to modify it
sudo mount -o loop -t ext2 ${IMAGE_NAME} /mnt/
# We opt for 'docker cp --archive' over 'docker save' since our focus is solely on the end product rather than individual layers and metadata.
# However, it's important to note that despite being specified in the documentation, the '--archive' flag does not currently preserve uid/gid information when copying files from the container to the host machine.
# Another compelling reason to use 'docker cp' is that it preserves resolv.conf.
- name: Export and unpack container filesystem contents into mounted ext2 image.
run: |
sudo docker cp -a ${CONTAINER_ID}:/ /mnt/
sudo umount /mnt/
# Result is an ext2 image for webvm.
# The .txt suffix enabled HTTP compression for free
- name: Generate image split chunks and .meta file
run: |
sudo split ${{ env.IMAGE_NAME }} ${{ env.DEPLOY_DIR }}/${{ env.IMAGE_NAME }}.c -a 6 -b 128k -x --additional-suffix=.txt
sudo bash -c "stat -c%s ${{ env.IMAGE_NAME }} > ${{ env.DEPLOY_DIR }}/${{ env.IMAGE_NAME }}.meta"
# This step updates the default config_github_terminal.js file by performing the following actions:
# 1. Replaces all occurrences of IMAGE_URL with the URL to the image.
# 2. Replace CMD with the Dockerfile entry command.
# 3. Replace args with the Dockerfile CMD / Entrypoint args.
# 4. Replace ENV with the container's environment values.
# 5. Replace CWD with the container's current working directory.
- name: Adjust config_github_terminal.js
run: |
sed -i 's#IMAGE_URL#"${{ env.IMAGE_NAME }}"#g' config_github_terminal.js
sed -i 's#CMD#${{ env.CMD }}#g' config_github_terminal.js
sed -i 's#ARGS#${{ env.ARGS }}#g' config_github_terminal.js
sed -i 's#ENV#${{ env.ENV }}#g' config_github_terminal.js
sed -i 's#CWD#${{ env.CWD }}#g' config_github_terminal.js
- name: Build NPM package
run: |
npm install
WEBVM_MODE=github npm run build
# Move required files for gh-pages deployment to the deployment directory $DEPLOY_DIR.
- name: Copy build
run: |
rm build/alpine.html
sudo mv build/* $DEPLOY_DIR/
# We generate index.list files for our httpfs to function properly.
- name: make index.list
shell: bash
run: |
find $DEPLOY_DIR -type d | while read -r dir;
do
index_list="$dir/index.list";
sudo rm -f "$index_list";
sudo ls "$dir" | sudo tee "$index_list" > /dev/null;
sudo chmod +rw "$index_list";
sudo echo "created $index_list";
done
# Create a gh-pages artifact in order to deploy to gh-pages.
- name: Upload GitHub Pages artifact
uses: actions/upload-pages-artifact@v3
with:
# Path of the directory containing the static assets for our gh pages deployment.
path: ${{ env.DEPLOY_DIR }} # optional, default is _site/
- name: github release # To upload our final ext2 image as a github release.
if: ${{ github.event.inputs.GITHUB_RELEASE == 'true' }}
uses: softprops/action-gh-release@v2
with:
target_commitish: ${{ github.sha }} # Last commit on the GITHUB_REF branch or tag
tag_name: ext2_image
fail_on_unmatched_files: 'true' # Fail in case of no matches with the file(s) glob(s).
files: | # Assets to upload as release.
${{ env.IMAGE_NAME }}
deploy_to_github_pages: # Job that deploys the github-pages artifact to github-pages.
if: ${{ github.event.inputs.DEPLOY_TO_GITHUB_PAGES == 'true' }}
needs: build
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
# Grant GITHUB_TOKEN the permissions required to make a Pages deployment
permissions:
pages: write # to deploy to Pages
id-token: write # to verify the deployment originates from an appropriate source
runs-on: ubuntu-latest
steps:
# Deployment to github pages
- name: Deploy GitHub Pages site
id: deployment
uses: actions/deploy-pages@v4
+9
View File
@@ -0,0 +1,9 @@
/node_modules
/.svelte-kit
pnpm-lock.yaml
build/
custom-disk-images/*
!custom-disk-images/.gitkeep
nginx_access.log
nginx_error.log
nginx_main_error.log
+1
View File
@@ -0,0 +1 @@
engine-strict=true
+202
View File
@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+248
View File
@@ -0,0 +1,248 @@
# WebVM
[![Discord server](https://img.shields.io/discord/988743885121548329?color=%235865F2&logo=discord&logoColor=%23fff)](https://discord.gg/yWRr2YnD9c)
[![Issues](https://img.shields.io/github/issues/leaningtech/webvm)](https://github.com/leaningtech/webvm/issues)
This repository hosts the source code for [https://webvm.io](https://webvm.io), a Linux virtual machine that runs in your browser.
Try out the new Alpine / Xorg / i3 graphical environment: [https://webvm.io/alpine.html](https://webvm.io/alpine.html)
<img src="/assets/welcome_to_WebVM_alpine_2024.png" width="90%">
## What is WebVM?
WebVM is a server-less virtual environment running fully client-side in HTML5/WebAssembly. It's designed to be Linux ABI-compatible and runs an unmodified Debian distribution including many native development toolchains.
WebVM is powered by the **CheerpX** virtualization engine, which provides:
- x86-to-WebAssembly JIT compiler
- Virtual block-based file system
- Linux syscall emulator
- Safe, sandboxed client-side execution
## Table of Contents
- [Networking](#networking)
- [Development & Customization](#development--customization)
- [Deploy to GitHub Pages](#deploy-to-github-pages)
- [Local Serving & Image Configuration](#local-serving--image-configuration)
- [Claude AI Integration](#claude-ai-integration)
- [Community & Support](#community--support)
- [Learn More](#learn-more)
- [License](#license)
> [!NOTE]
> Visit [https://webvm.io](https://webvm.io) to get started immediately in your browser. No setup required.
>
> For local setup, custom image builds, and fork/deploy guidance, see [Development & Customization](#development--customization).
## Networking
WebVM supports **Tailscale** integration. So your browser VM can reach your private network and, with an exit node, the public internet too.
> [!NOTE]
> Some low-level networking operations (especially ICMP used by `ping`) are not currently available in this environment. For connectivity checks, use `curl` or `wget`.
### Local network
1. Open the "Networking" panel from the sidebar
2. Click "Connect to Tailscale"
3. Log in (create a free account at [tailscale.com](https://tailscale.com) if needed)
4. Click "Connect" when prompted
WebVM now has access to all machines in your Tailscale network!
### Internet Usage Tips
> [!TIP]
> On slower connections there may be a short delay before initialisation. Connection status is shown as a colored dot on the button: orange = local network, green = global/internet. The button text shows your Tailscale IP address once connected.
To access the public internet from WebVM, set up an **Exit Node** on another device in your Tailscale network:
1. Follow the [Tailscale Exit Node quickstart](https://tailscale.com/kb/1408/quick-guide-exit-nodes?tab=linux) (sections: "Advertise a device as an exit node")
2. WebVM automatically uses the exit node once advertised
### Using an Auth Key
As an alternative to interactive login, add your Tailscale auth key to the URL fragment:
```
https://webvm.io/#authKey=<your-ephemeral-key>
```
This is equivalent to Tailscale's `--login-server` option.
> [!TIP]
> If you also need a custom control server, add `controlUrl` in the same URL fragment and separate values with `&`, for example: `#authKey=...&controlUrl=...`.
### Self-Hosting Tailscale with Headscale
We also support [headscale](https://headscale.net/stable/), a selfhosted open source implementation of the Tailscale control server.
Because Headscale does not add CORS headers by default, you will need a proxy in front of it. See the [Headscale reverse proxy setup docs](https://headscale.net/stable/ref/integration/reverse-proxy/#nginx) for an example.
Once ready, add the following line to your `location /` block in your nginx config file.
```nginx
if ($http_origin = "https://yourdomain.com") {
add_header 'Access-Control-Allow-Origin' "$http_origin";
add_header 'Access-Control-Allow-Credentials' 'true' always;
}
```
Then access WebVM with:
```
https://yourdomain.com/#controlUrl=<your-headscale-url>
```
## Development & Customization
> [!NOTE]
> Users have root privileges by default. `sudo` is not installed though this can easily be added to the Dockerfile if needed.
### Deploy to GitHub Pages
This is a simple, beginner-friendly workflow. For local hosting and customization, see below [Local Serving & Image Configuration](#local-serving--image-configuration).
Fork the WebVM repository to deploy your own version to GitHub Pages:
<img src="/assets/fork_deploy_instructions.gif" alt="deploy_instructions_gif" width="90%">
1. **Fork the repository**
2. **Enable GitHub Pages** _via forked repository_ in Settings → Pages using "GitHub Actions" as source
3. **Run the `Deploy` workflow** from Actions
4. After completion, open the URL shown under the `deploy_to_github_pages` job
<img src="/assets/result.png" width="70%">
The same `Deploy` workflow also builds custom `.ext2` disk images from a Dockerfile. You can point it at `dockerfiles/debian_mini` or another Dockerfile, then either publish the result as a GitHub Release asset or deploy the Pages build from your fork.
> [!NOTE]
> `dockerfiles/debian_large` is too large of an image for GitHub pages.
### Local Serving & Image Configuration
#### 1. Clone the repository
```sh
git clone https://github.com/leaningtech/webvm.git
cd webvm
```
#### 2. Put your image in `custom-disk-images/`
This repository includes a persistent `custom-disk-images/` directory for local `.ext2` files.
To use the official Debian mini image, download it from Releases:
[debian_mini_20230519_5022088024.ext2](https://github.com/leaningtech/webvm/releases/download/ext2_image/debian_mini_20230519_5022088024.ext2)
You can also copy in an image you built yourself.
#### 3. Point WebVM to your local image
Edit `config_public_terminal.js`:
```js
export const diskImageUrl =
"/custom-disk-images/debian_mini_20230519_5022088024.ext2";
export const diskImageType = "bytes";
```
#### 4. Install dependencies and build
```sh
npm install
npm run build
```
#### 5. Start Nginx and open WebVM
```sh
nginx -p . -c nginx.conf
```
Then open http://127.0.0.1:8081 and enjoy your local WebVM!
For the full Alpine desktop environment, see [leaningtech/alpine-image](https://github.com/leaningtech/alpine-image).
For more details, see [CheerpX Custom Images documentation](https://cheerpx.io/docs/guides/custom-images).
> [!TIP]
> For Python3 REPL, the `Deploy` workflow takes into account the `CMD` specified in the Dockerfile.
>
> To build a REPL you can simply change `CMD [ "/bin/bash" ]` to `CMD [ "/usr/bin/python3" ]` and deploy.
## Claude AI Integration
To access Claude AI, you need an API key. Follow these steps to get started:
#### 1. Create an account
- Visit [Anthropic Console](https://console.anthropic.com/login) and sign up with your e-mail. You'll receive a sign in link to the Anthropic Console.
#### 2. Get your API key
- Once logged in, navigate to **Get API keys**.
- Purchase the amount of credits you need. After completing the purchase, you'll be able to generate the key through the API console.
#### 3. Log in with your API key
- Navigate to your WebVM and hover over the robot icon. This will show the Claude AI Integration tab. For added convenience, you can click the pin button in the top right corner to keep the tab in place.
- You'll see a prompt where you can insert your Claude API key.
#### 4. Start using Claude AI
- Once your API key is entered, you can begin interacting with Claude AI by asking questions such as:
**"Solve the CTF challenge at `/home/user/chall1.bin.` Note that the binary reads from stdin."**
<img src="/assets/webvm_claude_ctf.gif" alt="deploy_instructions_gif" width="90%">
**Important:** Your API key is private and should never be shared. We do not have access to your key, it is only stored locally in your browser.
## Community & Support
**Report issues:** Use [GitHub Issues](https://github.com/leaningtech/webvm/issues) to report bugs or request features.
**Chat with us:** Join our [Discord community](https://discord.gg/yTNZgySKGa) to discuss WebVM, share ideas, and get help.
## Learn More
**Articles & Resources:**
- [WebVM: Server-less x86 virtual machines in the browser](https://leaningtech.com/webvm-server-less-x86-virtual-machines-in-the-browser/)
- [WebVM: Linux Virtualization in WebAssembly with Full Networking via Tailscale](https://leaningtech.com/webvm-virtual-machine-with-networking-via-tailscale/)
- [Mini.WebVM: Your own Linux box from Dockerfile, virtualized in the browser via WebAssembly](https://leaningtech.com/mini-webvm-your-linux-box-from-dockerfile-via-wasm/)
- [Crafting the Impossible: X86 Virtualization in the Browser with WebAssembly](https://www.youtube.com/watch?v=VqrbVycTXmw) — Talk at JsNation 2022
**Example Deployment:**
- [Mini.WebVM Reference](https://mini.webvm.io) — A running example deployed to GitHub Pages
**Technology Behind WebVM:**
This project is powered by:
- **[CheerpX](https://cheerpx.io/)** — x86-to-WebAssembly JIT compiler | by [Leaning Technologies](https://leaningtech.com/)
- **[xterm.js](https://xtermjs.org/)** — Web-based terminal emulator
- **[Tailscale](https://tailscale.com/)** — VPN networking layer
- **[lwIP](https://savannah.nongnu.org/projects/lwip/)** — TCP/IP stack, compiled for the Web via [Cheerp](https://github.com/leaningtech/cheerp-meta/)
**Versioning:**
WebVM uses the [CheerpX](https://www.npmjs.com/package/@leaningtech/cheerpx) NPM package, which is updated on every release.
Every build is immutable. If a specific version works well for you today, it will keep working forever.
## License
WebVM is released under the Apache License, Version 2.0.
You are welcome to use, modify, and redistribute the contents of this repository.
The public CheerpX deployment is provided **as-is** and is **free to use** for technological exploration, testing and use by individuals. Any other use by organizations, including non-profit, academia and the public sector, requires a license. Downloading a CheerpX build for the purpose of hosting it elsewhere is not permitted without a commercial license.
Read more [here](https://cheerpx.io/docs/licensing) about our licensing practices.
If you want to build a product on top of CheerpX/WebVM, please see our other licensing options: [CheerpX licensing](https://cheerpx.io/licensing) or get in touch: sales@leaningtech.com
+7
View File
@@ -0,0 +1,7 @@
# WeHub 来源说明
- 原始项目:`leaningtech/webvm`
- 原始仓库:https://github.com/leaningtech/webvm
- 导入方式:上游默认分支的最新快照
- 原作者、版权和许可证信息以原始仓库及本仓库 LICENSE 为准
- 本文件仅用于记录来源,不代表 WeHub 是原项目作者
Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 79.37 89.83"><defs><style>.cls-1{fill:#4b647f;}.cls-2{fill:#6386a5;}.cls-3{fill:#e2e2e2;}.cls-4{fill:#fff;}</style></defs><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><g id="code_html5"><polygon class="cls-1" points="79.37 0 39.69 0 39.69 0 0 0 7.44 80.69 39.69 89.83 39.69 89.83 39.69 89.83 39.69 89.83 39.69 89.83 71.92 80.69 79.37 0"/><polygon class="cls-2" points="39.69 6.57 39.69 82.99 39.69 82.99 65.8 75.59 72.16 6.57 39.69 6.57"/><path class="cls-3" d="M39.52,50.42a7.7,7.7,0,1,1,0-15.4h.17V19.66c-1.16,0-2.31,0-3.46,0a1,1,0,0,0-1.15.95c-.29,1.68-.55,3.36-.79,5.05a.61.61,0,0,1-.45.57c-.72.27-1.42.6-2.13.89a.48.48,0,0,1-.39,0q-1.84-1.41-3.66-2.83c-.88-.7-1.28-.71-2.2.05a34.62,34.62,0,0,0-4.54,4.6c-.57.7-.55,1.05,0,1.76l2.92,3.75a.42.42,0,0,1,0,.52c-.33.73-.65,1.45-.94,2.2a.45.45,0,0,1-.4.33c-1.57.23-3.13.49-4.7.69a1.59,1.59,0,0,0-1.4.89v7.22a1.45,1.45,0,0,0,1.35.9c1.56.2,3.11.44,4.66.67a.53.53,0,0,1,.49.4c.28.77.6,1.53.94,2.27a.56.56,0,0,1-.05.66c-1,1.23-1.93,2.48-2.88,3.73A1.26,1.26,0,0,0,21,56.57,45.34,45.34,0,0,0,25.7,61.3a1.08,1.08,0,0,0,1.67.1c1.34-1,2.67-2,4-3a.48.48,0,0,1,.37,0c.76.3,1.5.63,2.25.92a.39.39,0,0,1,.29.36c.24,1.61.51,3.22.73,4.84a1.42,1.42,0,0,0,.88,1.32h3.79V50.41Z"/><path class="cls-4" d="M61.39,38.31l-4.88-.73a.5.5,0,0,1-.43-.37q-.45-1.17-1-2.31a.48.48,0,0,1,.05-.59c1-1.24,1.94-2.5,2.9-3.76A1.23,1.23,0,0,0,58,28.9a49.11,49.11,0,0,0-4.68-4.78A1.1,1.1,0,0,0,51.67,24l-4,3a.54.54,0,0,1-.41.06c-.75-.29-1.47-.62-2.22-.91a.4.4,0,0,1-.31-.38c-.25-1.7-.52-3.4-.78-5.09a1.08,1.08,0,0,0-1.19-1.05c-1,0-2,0-3.06,0V35a7.69,7.69,0,0,1,0,15.38V65.79h3.5A1.45,1.45,0,0,0,44,64.54c.24-1.62.5-3.23.73-4.84a.5.5,0,0,1,.35-.45c.74-.29,1.46-.62,2.2-.92a.51.51,0,0,1,.42.05c1.2.92,2.4,1.86,3.6,2.8.9.72,1.32.74,2.23,0a35.07,35.07,0,0,0,4.57-4.63c.56-.69.54-1.05,0-1.76l-2.9-3.72a.49.49,0,0,1-.06-.59c.34-.72.63-1.46,1-2.18a.5.5,0,0,1,.32-.27c1.68-.27,3.37-.53,5.06-.78A1.13,1.13,0,0,0,62.57,46c0-2.11,0-4.23,0-6.34A1.18,1.18,0,0,0,61.39,38.31Z"/></g></g></g></svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 127.14 96.36"><defs><style>.cls-1{fill:#5865f2;}</style></defs><g id="图层_2" data-name="图层 2"><g id="Discord_Logos" data-name="Discord Logos"><g id="Discord_Logo_-_Large_-_White" data-name="Discord Logo - Large - White"><path class="cls-1" d="M107.7,8.07A105.15,105.15,0,0,0,81.47,0a72.06,72.06,0,0,0-3.36,6.83A97.68,97.68,0,0,0,49,6.83,72.37,72.37,0,0,0,45.64,0,105.89,105.89,0,0,0,19.39,8.09C2.79,32.65-1.71,56.6.54,80.21h0A105.73,105.73,0,0,0,32.71,96.36,77.7,77.7,0,0,0,39.6,85.25a68.42,68.42,0,0,1-10.85-5.18c.91-.66,1.8-1.34,2.66-2a75.57,75.57,0,0,0,64.32,0c.87.71,1.76,1.39,2.66,2a68.68,68.68,0,0,1-10.87,5.19,77,77,0,0,0,6.89,11.1A105.25,105.25,0,0,0,126.6,80.22h0C129.24,52.84,122.09,29.11,107.7,8.07ZM42.45,65.69C36.18,65.69,31,60,31,53s5-12.74,11.43-12.74S54,46,53.89,53,48.84,65.69,42.45,65.69Zm42.24,0C78.41,65.69,73.25,60,73.25,53s5-12.74,11.44-12.74S96.23,46,96.12,53,91.08,65.69,84.69,65.69Z"/></g></g></g></svg>

After

Width:  |  Height:  |  Size: 988 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

+1
View File
@@ -0,0 +1 @@
<svg width="98" height="96" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 960 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 309 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

+11
View File
@@ -0,0 +1,11 @@
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<ellipse cx="2.89214" cy="11.7148" rx="2.89214" ry="2.88514" fill="white"></ellipse>
<ellipse cx="11.5685" cy="11.7148" rx="2.89214" ry="2.88514" fill="white"></ellipse>
<ellipse opacity="0.2" cx="2.89214" cy="20.3703" rx="2.89214" ry="2.88514" fill="white"></ellipse>
<ellipse opacity="0.2" cx="20.245" cy="20.3703" rx="2.89214" ry="2.88514" fill="white"></ellipse>
<ellipse cx="11.5685" cy="20.3703" rx="2.89214" ry="2.88514" fill="white"></ellipse>
<ellipse cx="20.245" cy="11.7148" rx="2.89214" ry="2.88514" fill="white"></ellipse>
<ellipse opacity="0.2" cx="2.89214" cy="3.0594" rx="2.89214" ry="2.88514" fill="white"></ellipse>
<ellipse opacity="0.2" cx="11.5685" cy="3.0594" rx="2.89214" ry="2.88514" fill="white"></ellipse>
<ellipse opacity="0.2" cx="20.245" cy="3.0594" rx="2.89214" ry="2.88514" fill="white"></ellipse>
</svg>

After

Width:  |  Height:  |  Size: 1007 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 514 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 473 KiB

+23
View File
@@ -0,0 +1,23 @@
// The root filesystem location
export const diskImageUrl = IMAGE_URL;
// The root filesystem backend type
export const diskImageType = "github";
// Print an introduction message about the technology
export const printIntro = true;
// Is a graphical display needed
export const needsDisplay = false;
// Executable full path (Required)
export const cmd = CMD; // Default: "/bin/bash";
// Arguments, as an array (Required)
export const args = ARGS; // Default: ["--login"];
// Optional extra parameters
export const opts = {
// Environment variables
env: ENV, // Default: ["HOME=/home/user", "TERM=xterm", "USER=user", "SHELL=/bin/bash", "EDITOR=vim", "LANG=en_US.UTF-8", "LC_ALL=C"],
// Current working directory
cwd: CWD, // Default: "/home/user",
// User id
uid: 1000,
// Group id
gid: 1000
};
+19
View File
@@ -0,0 +1,19 @@
// The root filesystem location
export const diskImageUrl = "wss://disks.webvm.io/alpine_20251007.ext2";
// The root filesystem backend type
export const diskImageType = "cloud";
// Print an introduction message about the technology
export const printIntro = false;
// Is a graphical display needed
export const needsDisplay = true;
// Executable full path (Required)
export const cmd = "/sbin/init";
// Arguments, as an array (Required)
export const args = [];
// Optional extra parameters
export const opts = {
// User id
uid: 0,
// Group id
gid: 0
};
+23
View File
@@ -0,0 +1,23 @@
// The root OS image location, change to local filepath if serving locally
export const diskImageUrl = "wss://disks.webvm.io/debian_buster_large_permis_fixed_01-06-2026.ext2";
// The root filesystem backend type use "cloud" for serving remotely or "bytes" for serving locally
export const diskImageType = "cloud";
// Print an introduction message about the technology
export const printIntro = true;
// Is a graphical display needed
export const needsDisplay = false;
// Executable full path (Required)
export const cmd = "/bin/bash";
// Arguments, as an array (Required)
export const args = ["--login"];
// Optional extra parameters
export const opts = {
// Environment variables
env: ["HOME=/home/user", "TERM=xterm", "USER=user", "SHELL=/bin/bash", "EDITOR=vim", "LANG=en_US.UTF-8", "LC_ALL=C"],
// Current working directory
cwd: "/home/user",
// User id
uid: 1000,
// Group id
gid: 1000
};
View File
+1
View File
@@ -0,0 +1 @@
.dockerignore
+29
View File
@@ -0,0 +1,29 @@
FROM --platform=linux/386 docker.io/i386/debian:buster
ARG DEBIAN_FRONTEND=noninteractive
# Point APT to the archived mirrors (Buster is EOL)
RUN echo "deb [trusted=yes] http://archive.debian.org/debian buster main contrib non-free" \
> /etc/apt/sources.list && \
echo "deb [trusted=yes] http://archive.debian.org/debian-security buster/updates main" \
>> /etc/apt/sources.list
RUN apt-get update && apt-get -y upgrade && \
apt-get install -y apt-utils beef bsdgames bsdmainutils ca-certificates \
cowsay cpio cron curl dmidecode dmsetup g++ gcc gdbm-l10n git \
hexedit ifupdown init logrotate lsb-base lshw lua5.3 luajit lynx make \
nano netbase nodejs openssl procps python3 python3-cryptography \
python3-jinja2 python3-numpy python3-pandas python3-pip python3-scipy \
python3-six python3-yaml readline-common rsyslog ruby sensible-utils \
ssh systemd systemd-sysv tasksel tasksel-data udev vim wget whiptail \
xxd iptables isc-dhcp-client isc-dhcp-common kmod less netcat-openbsd
# Make a user, then copy over the /example directory
RUN useradd -m user && echo "user:password" | chpasswd
COPY --chown=user:user ./examples /home/user/examples
RUN chmod -R +x /home/user/examples/lua
# We set WORKDIR, as this gets extracted by Webvm to be used as the cwd. This is optional.
WORKDIR /home/user/
# We set env, as this gets extracted by Webvm. This is optional.
ENV HOME="/home/user" TERM="xterm" USER="user" SHELL="/bin/bash" EDITOR="vim" LANG="C.UTF-8"
RUN echo 'root:password' | chpasswd
CMD [ "/bin/bash" ]
+25
View File
@@ -0,0 +1,25 @@
FROM --platform=linux/386 docker.io/i386/debian:buster
ARG DEBIAN_FRONTEND=noninteractive
# Point APT to the archived mirrors (Buster is EOL)
RUN echo "deb [trusted=yes] http://archive.debian.org/debian buster main contrib non-free" \
> /etc/apt/sources.list && \
echo "deb [trusted=yes] http://archive.debian.org/debian-security buster/updates main" \
>> /etc/apt/sources.list
RUN apt-get clean && apt-get update && apt-get -y upgrade
RUN apt-get -y install apt-utils gcc \
python3 vim unzip ruby nodejs \
fakeroot dbus base whiptail hexedit \
patch wamerican ucf manpages \
file luajit make lua5.3 dialog curl \
less cowsay netcat-openbsd
RUN useradd -m user && echo "user:password" | chpasswd
COPY --chown=user:user ./examples /home/user/examples
RUN chmod -R +x /home/user/examples/lua
# We set WORKDIR, as this gets extracted by Webvm to be used as the cwd. This is optional.
WORKDIR /home/user/
# We set env, as this gets extracted by Webvm. This is optional.
ENV HOME="/home/user" TERM="xterm" USER="user" SHELL="/bin/bash" EDITOR="vim" LANG="C.UTF-8"
RUN echo 'root:password' | chpasswd
CMD [ "/bin/bash" ]
Binary file not shown.

After

Width:  |  Height:  |  Size: 359 KiB

Binary file not shown.
+5
View File
@@ -0,0 +1,5 @@
Welcome to WebVM: A complete desktop environment running in the browser
WebVM is powered by CheerpX: a x86-to-WebAssembly virtualization engine and Just-in-Time compiler
For more info: https://cheerpx.io
+3
View File
@@ -0,0 +1,3 @@
ArchitectureOverview.png
WebAssemblyTools.pdf
Welcome.txt
+13
View File
@@ -0,0 +1,13 @@
SRCS = $(wildcard *.c)
PROGS = $(patsubst %.c,%,$(SRCS))
all: $(PROGS)
%: %.c
$(CC) $(CFLAGS) -o $@ $<
clean:
rm -f $(PROGS)
.PHONY: all clean
+12
View File
@@ -0,0 +1,12 @@
#include <stdio.h>
// Most of the C compilers support a third parameter to main which
// store all envorinment variables
int main(int argc, char *argv[], char * envp[])
{
int i;
for (i = 0; envp[i] != NULL; i++)
printf("\n%s", envp[i]);
getchar();
return 0;
}
+6
View File
@@ -0,0 +1,6 @@
#include <stdio.h>
int main()
{
printf("Hello, World!\n");
}
+7
View File
@@ -0,0 +1,7 @@
#include <unistd.h>
int main()
{
link("env", "env3");
return 0;
}
+10
View File
@@ -0,0 +1,10 @@
#include <fcntl.h>
#include <stdio.h>
#include <errno.h>
int main()
{
int ret = openat(AT_FDCWD, "/dev/tty", 0x88102, 0);
printf("return value is %d and errno is %d\n", ret, errno);
}
+17
View File
@@ -0,0 +1,17 @@
#include <sys/wait.h>
#include <unistd.h>
#include <errno.h>
#include <stdio.h>
int main()
{
int status;
pid_t p = getpid();
// waitpid takes a children's pid, not the current process one
// if the pid is not a children of the current process, it returns -ECHILD
pid_t res = waitpid(1001, &status, WNOHANG);
printf("res is %d, p is %d and errno is %d\n", res, p, errno);
}
+20
View File
@@ -0,0 +1,20 @@
#!/usr/bin/env luajit
cfizz,cbuzz=0,0
for i=1,20 do
cfizz=cfizz+1
cbuzz=cbuzz+1
io.write(i .. ": ")
if cfizz~=3 and cbuzz~=5 then
io.write(i)
else
if cfizz==3 then
io.write("Fizz")
cfizz=0
end
if cbuzz==5 then
io.write("Buzz")
cbuzz=0
end
end
io.write("\n")
end
+13
View File
@@ -0,0 +1,13 @@
#!/usr/bin/env luajit
fruits = {"banana","orange","apple","grapes"}
for k,v in ipairs(fruits) do
print(k,v)
end
table.sort(fruits)
print("sorted table")
for k,v in ipairs(fruits) do
print(k,v)
end
+20
View File
@@ -0,0 +1,20 @@
#!/usr/bin/env luajit
A = { ["John"] = true, ["Bob"] = true, ["Mary"] = true, ["Elena"] = true }
B = { ["Jim"] = true, ["Mary"] = true, ["John"] = true, ["Bob"] = true }
A_B = {}
for a in pairs(A) do
if not B[a] then A_B[a] = true end
end
B_A = {}
for b in pairs(B) do
if not A[b] then B_A[b] = true end
end
for a_b in pairs(A_B) do
print( a_b )
end
for b_a in pairs(B_A) do
print( b_a )
end
+6
View File
@@ -0,0 +1,6 @@
console.log("process.uptime = ", global.process.uptime());
console.log("process.title = ", global.process.title);
console.log("process version = ", global.process.version);
console.log("process.platform = ", global.process.platform);
console.log("process.cwd = ", global.process.cwd());
console.log("process.uptime = ", global.process.uptime());
+161
View File
@@ -0,0 +1,161 @@
const PI = Math.PI;
const SOLAR_MASS = 4 * PI * PI;
const DAYS_PER_YEAR = 365.24;
function Body(x, y, z, vx, vy, vz, mass) {
this.x = x;
this.y = y;
this.z = z;
this.vx = vx;
this.vy = vy;
this.vz = vz;
this.mass = mass;
}
function Jupiter() {
return new Body(
4.84143144246472090e+00,
-1.16032004402742839e+00,
-1.03622044471123109e-01,
1.66007664274403694e-03 * DAYS_PER_YEAR,
7.69901118419740425e-03 * DAYS_PER_YEAR,
-6.90460016972063023e-05 * DAYS_PER_YEAR,
9.54791938424326609e-04 * SOLAR_MASS
);
}
function Saturn() {
return new Body(
8.34336671824457987e+00,
4.12479856412430479e+00,
-4.03523417114321381e-01,
-2.76742510726862411e-03 * DAYS_PER_YEAR,
4.99852801234917238e-03 * DAYS_PER_YEAR,
2.30417297573763929e-05 * DAYS_PER_YEAR,
2.85885980666130812e-04 * SOLAR_MASS
);
}
function Uranus() {
return new Body(
1.28943695621391310e+01,
-1.51111514016986312e+01,
-2.23307578892655734e-01,
2.96460137564761618e-03 * DAYS_PER_YEAR,
2.37847173959480950e-03 * DAYS_PER_YEAR,
-2.96589568540237556e-05 * DAYS_PER_YEAR,
4.36624404335156298e-05 * SOLAR_MASS
);
}
function Neptune() {
return new Body(
1.53796971148509165e+01,
-2.59193146099879641e+01,
1.79258772950371181e-01,
2.68067772490389322e-03 * DAYS_PER_YEAR,
1.62824170038242295e-03 * DAYS_PER_YEAR,
-9.51592254519715870e-05 * DAYS_PER_YEAR,
5.15138902046611451e-05 * SOLAR_MASS
);
}
function Sun() {
return new Body(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, SOLAR_MASS);
}
const bodies = Array(Sun(), Jupiter(), Saturn(), Uranus(), Neptune());
function offsetMomentum() {
let px = 0;
let py = 0;
let pz = 0;
const size = bodies.length;
for (let i = 0; i < size; i++) {
const body = bodies[i];
const mass = body.mass;
px += body.vx * mass;
py += body.vy * mass;
pz += body.vz * mass;
}
const body = bodies[0];
body.vx = -px / SOLAR_MASS;
body.vy = -py / SOLAR_MASS;
body.vz = -pz / SOLAR_MASS;
}
function advance(dt) {
const size = bodies.length;
for (let i = 0; i < size; i++) {
const bodyi = bodies[i];
let vxi = bodyi.vx;
let vyi = bodyi.vy;
let vzi = bodyi.vz;
for (let j = i + 1; j < size; j++) {
const bodyj = bodies[j];
const dx = bodyi.x - bodyj.x;
const dy = bodyi.y - bodyj.y;
const dz = bodyi.z - bodyj.z;
const d2 = dx * dx + dy * dy + dz * dz;
const mag = dt / (d2 * Math.sqrt(d2));
const massj = bodyj.mass;
vxi -= dx * massj * mag;
vyi -= dy * massj * mag;
vzi -= dz * massj * mag;
const massi = bodyi.mass;
bodyj.vx += dx * massi * mag;
bodyj.vy += dy * massi * mag;
bodyj.vz += dz * massi * mag;
}
bodyi.vx = vxi;
bodyi.vy = vyi;
bodyi.vz = vzi;
}
for (let i = 0; i < size; i++) {
const body = bodies[i];
body.x += dt * body.vx;
body.y += dt * body.vy;
body.z += dt * body.vz;
}
}
function energy() {
let e = 0;
const size = bodies.length;
for (let i = 0; i < size; i++) {
const bodyi = bodies[i];
e += 0.5 * bodyi.mass * ( bodyi.vx * bodyi.vx + bodyi.vy * bodyi.vy + bodyi.vz * bodyi.vz );
for (let j = i + 1; j < size; j++) {
const bodyj = bodies[j];
const dx = bodyi.x - bodyj.x;
const dy = bodyi.y - bodyj.y;
const dz = bodyi.z - bodyj.z;
const distance = Math.sqrt(dx * dx + dy * dy + dz * dz);
e -= (bodyi.mass * bodyj.mass) / distance;
}
}
return e;
}
const n = +50000000;
offsetMomentum();
console.log(energy().toFixed(9));
const start = Date.now();
for (let i = 0; i < n; i++) {
advance(0.01);
}
const end = Date.now();
console.log(energy().toFixed(9));
console.log("elapsed:",end-start);
+31
View File
@@ -0,0 +1,31 @@
(function () {
function isPrime(p) {
const upper = Math.sqrt(p);
for(let i = 2; i <= upper; i++) {
if (p % i === 0 ) {
return false;
}
}
return true;
}
// Return n-th prime
function prime(n) {
if (n < 1) {
throw Error("n too small: " + n);
}
let count = 0;
let result = 1;
while(count < n) {
result++;
if (isPrime(result)) {
count++;
}
}
return result;
}
console.log("your prime is ", prime(100000));
}());
+16
View File
@@ -0,0 +1,16 @@
(function (){
let bytes = new Uint8Array([
0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00,
0x01, 0x07, 0x01, 0x60, 0x02, 0x7f, 0x7f, 0x01,
0x7f, 0x03, 0x02, 0x01, 0x00, 0x07, 0x07, 0x01,
0x03, 0x73, 0x75, 0x6d, 0x00, 0x00, 0x0a, 0x0a,
0x01, 0x08, 0x00, 0x20, 0x00, 0x20, 0x01, 0x6a,
0x0f, 0x0b
]);
console.log(bytes);
let mod = new WebAssembly.Module(bytes);
let instance = new WebAssembly.Instance(mod, {});
console.log(instance.exports);
return instance.exports.sum(2020, 1);
}());
+9
View File
@@ -0,0 +1,9 @@
def factorial():
f, n = 1, 1
while True: # First iteration:
yield f # yield 1 to start with and then
f, n = f * n, n+1 # f will now be 1, and n will be 2, ...
for index, factorial_number in zip(range(51), factorial()):
print('{i:3}!= {f:65}'.format(i=index, f=factorial_number))
+9
View File
@@ -0,0 +1,9 @@
def fib():
a, b = 0, 1
while True: # First iteration:
yield a # yield 0 to start with and then
a, b = b, a + b # a will now be 1, and b will also be 1, (0 + 1)
for index, fibonacci_number in zip(range(100), fib()):
print('{i:3}: {f:3}'.format(i=index, f=fibonacci_number))
+12
View File
@@ -0,0 +1,12 @@
from decimal import Decimal, getcontext
getcontext().prec=60
summation = 0
for k in range(50):
summation = summation + 1/Decimal(16)**k * (
Decimal(4)/(8*k+1)
- Decimal(2)/(8*k+4)
- Decimal(1)/(8*k+5)
- Decimal(1)/(8*k+6)
)
print(summation)
+14
View File
@@ -0,0 +1,14 @@
=begin
# The famous Hello World
# Program is trivial in
# Ruby. Superfluous:
#
# * A "main" method
# * Newline
# * Semicolons
#
# Here is the Code:
=end
puts "Hello World!"
+9
View File
@@ -0,0 +1,9 @@
# Output "I love Ruby"
say = "I love Ruby"; puts say
# Output "I *LOVE* RUBY"
say['love'] = "*love*"; puts say.upcase
# Output "I *love* Ruby", 5 times
5.times { puts say }
+6
View File
@@ -0,0 +1,6 @@
puts(2 ** 1)
puts(2 ** 2)
puts(2 ** 3)
puts(2 ** 10)
puts(2 ** 100)
puts(2 ** 1000)
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

+12
View File
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Tailscale login</title>
</head>
<body>
Loading network code...
</body>
</html>
+54
View File
@@ -0,0 +1,54 @@
worker_processes 1;
events {
worker_connections 1024;
}
error_log nginx_main_error.log info;
pid nginx_user.pid;
daemon off;
http {
access_log nginx_access.log;
error_log nginx_error.log info;
types {
text/html html htm shtml;
text/css css;
application/javascript js;
application/wasm wasm;
image/png png;
image/jpeg jpg jpeg;
image/svg+xml svg;
}
default_type application/octet-stream;
sendfile on;
server {
# listen 8080 ssl;
listen 8081;
server_name localhost;
gzip on;
gzip_types application/javascript application/wasm text/plain;
charset utf-8;
# ssl_certificate nginx.crt;
# ssl_certificate_key nginx.key;
location / {
root build;
autoindex on;
index index.html index.htm;
add_header 'Cross-Origin-Opener-Policy' 'same-origin' always;
add_header 'Cross-Origin-Embedder-Policy' 'require-corp' always;
add_header 'Cross-Origin-Resource-Policy' 'cross-origin' always;
}
location /custom-disk-images/ {
root .;
}
}
}
+3337
View File
File diff suppressed because it is too large Load Diff
+33
View File
@@ -0,0 +1,33 @@
{
"name": "webvm",
"version": "2.0.0",
"private": true,
"scripts": {
"dev": "vite dev",
"build": "vite build"
},
"devDependencies": {
"@anthropic-ai/sdk": "^0.33.0",
"@fortawesome/fontawesome-free": "^6.6.0",
"@leaningtech/cheerpx": "latest",
"@oddbird/popover-polyfill": "^0.4.4",
"@sveltejs/adapter-auto": "^3.0.0",
"@sveltejs/adapter-static": "^3.0.5",
"@sveltejs/kit": "^2.0.0",
"@sveltejs/vite-plugin-svelte": "^3.0.0",
"@xterm/addon-fit": "^0.10.0",
"@xterm/addon-web-links": "^0.11.0",
"@xterm/xterm": "^5.5.0",
"autoprefixer": "^10.4.20",
"labs": "git@github.com:leaningtech/labs.git",
"node-html-parser": "^6.1.13",
"postcss": "^8.4.47",
"postcss-discard": "^2.0.0",
"svelte": "^4.2.7",
"tailwindcss": "^3.4.9",
"vite": "^5.0.3",
"vite-plugin-static-copy": "^1.0.6",
"html2canvas-pro": "^1.5.8"
},
"type": "module"
}
+39
View File
@@ -0,0 +1,39 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
'postcss-discard': {rule: function(node, value)
{
if(!value.startsWith('.fa-') || !value.endsWith(":before"))
return false;
switch(value)
{
case '.fa-info-circle:before':
case '.fa-wifi:before':
case '.fa-microchip:before':
case '.fa-compact-disc:before':
case '.fa-discord:before':
case '.fa-github:before':
case '.fa-star:before':
case '.fa-circle:before':
case '.fa-trash-can:before':
case '.fa-book-open:before':
case '.fa-user:before':
case '.fa-screwdriver-wrench:before':
case '.fa-desktop:before':
case '.fa-mouse-pointer:before':
case '.fa-hourglass-half:before':
case '.fa-hand:before':
case '.fa-brain:before':
case '.fa-download:before':
case '.fa-keyboard:before':
case '.fa-thumbtack:before':
case '.fa-brands:before':
case '.fa-solid:before':
case '.fa-regular:before':
return false;
}
return true;
}}
},
}
+21
View File
@@ -0,0 +1,21 @@
.scrollbar {
scrollbar-color: #777 #0000;
}
.scrollbar *::-webkit-scrollbar {
height: 6px;
width: 6px;
background-color: #0000;
}
/* Add a thumb */
.scrollbar *::-webkit-scrollbar-thumb {
border-radius: 3px;
height: 6px;
width: 6px;
background: #777;
}
.scrollbar *::-webkit-scrollbar-thumb:hover {
background: #555;
}
+96
View File
@@ -0,0 +1,96 @@
async function handleFetch(request) {
// Perform the original fetch request and store the result in order to modify the response.
try {
var r = await fetch(request);
}
catch (e) {
console.error(e)
}
if (r.status === 0) {
return r;
}
// We add headers to the original response its headers, in order to enable cross-origin-isolation. And make it independent of the server config.
const newHeaders = new Headers(r.headers);
// COEP & COOP for cross-origin-isolation.
newHeaders.set("Cross-Origin-Embedder-Policy", "require-corp");
newHeaders.set("Cross-Origin-Opener-Policy", "same-origin");
newHeaders.set("Cross-Origin-Resource-Policy", "cross-origin");
/**
* This workaround is necessary due to a limitation of CheerpOS, which relies on the response URL being set to the resolved URL.
* When constructing a new response object, the URL is not set by the Response() constructor and the serviceworker respondwith() method will set the url to event.request.url in case of an empty string.
* To address this, we set the location URL to the resolved response URL and set the status code to 301 in the new Response object.
* This causes the request to bounce back to the serviceworker from Cheerpos, with the event.request.url now set to the resolved URL, which allows the respondWith method to properly set the response URL in our new response.
* https://developer.mozilla.org/en-US/docs/Web/API/FetchEvent/respondWith.
*/
if (r.redirected === true)
newHeaders.set("location", r.url);
// In case of a redirection, we set the status to 301, and body to null, in order to not transfer too much data needlessly
const moddedResponse = new Response(r.redirected === true ? null : r.body, {
headers: newHeaders,
status: r.redirected === true ? 301 : r.status,
statusText: r.statusText,
});
return moddedResponse;
}
function serviceWorkerInit() {
// Init the service worker.
self.addEventListener("install", () => self.skipWaiting());
self.addEventListener("activate", e => e.waitUntil(self.clients.claim()));
// Listen for fetch requests and call handleFetch function.
self.addEventListener("fetch", function (e) {
try {
e.respondWith(handleFetch(e.request));
} catch (err) {
console.log("Serviceworker NetworkError:" + err);
}
});
}
async function doRegister() {
try {
const registration = await navigator.serviceWorker.register(window.document.currentScript.src);
console.log("Service Worker registered", registration.scope);
// EventListener to make sure that the page gets reloaded when a new serviceworker gets installed.
// f.e on first access.
registration.addEventListener("updatefound", () => {
console.log("Reloading the page to transfer control to the Service Worker.");
try {
window.location.reload();
} catch (err) {
console.log("Service Worker failed reloading the page. ERROR:" + err);
};
});
// When the registration is active, but it's not controlling the page, we reload the page to have it take control.
// This f.e occurs when you hard-reload (shift + refresh). https://www.w3.org/TR/service-workers/#navigator-service-worker-controller
if (registration.active && !navigator.serviceWorker.controller) {
console.log("Reloading the page to transfer control to the Service Worker.");
try {
window.location.reload();
} catch (err) {
console.log("Service Worker failed reloading the page. ERROR:" + err);
};
}
}
catch {
console.error("Service Worker failed to register:", e)
}
}
async function serviceWorkerRegister() {
if (window.crossOriginIsolated) return;
if (!window.isSecureContext) {
console.log("Service Worker not registered, a secure context is required.");
return;
}
// Register the service worker and reload the page to transfer control to the serviceworker.
if ("serviceWorker" in navigator)
await doRegister();
else
console.log("Service worker is not supported in this browser");
}
if (typeof window === 'undefined') // If the script is running in a Service Worker context
serviceWorkerInit()
else // If the script is running in the browser context
serviceWorkerRegister();
+38
View File
@@ -0,0 +1,38 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>WebVM - Linux virtualization in WebAssembly</title>
<meta name="description" content="Linux virtual machine, running in the browser via HTML5/WebAssembly. Networking and graphics supported.">
<meta name="keywords" content="WebVM, Virtual Machine, CheerpX, x86 virtualization, WebAssembly, Tailscale, JIT">
<meta property="og:title" content="WebVM - Linux virtualization in WebAssembly" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="WebVM"/>
<meta property="og:image" content="https://webvm.io/assets/social_2024.png" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content="@leaningtech" />
<meta name="twitter:title" content="WebVM - Linux virtualization in WebAssembly" />
<meta name="twitter:description" content="Linux virtual machine, running in the browser via HTML5/WebAssembly. Networking and graphics supported.">
<meta name="twitter:image" content="https://webvm.io/assets/social_2024.png" />
<!-- Apple iOS web clip compatibility tags -->
<meta name="application-name" content="WebVM" />
<meta name="apple-mobile-web-app-title" content="WebVM" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
<link rel="shortcut icon" href="tower.ico">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel='stylesheet' href='scrollbar.css'>
<!-- Serviceworker script that adds the COI headers to the response headers in cases where the server does not support it. -->
<script src="serviceWorker.js"></script>
<script data-domain="webvm.io" src="https://plausible.leaningtech.com/js/script.js"></script>
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>
+116
View File
@@ -0,0 +1,116 @@
<script>
import { apiState, setApiKey, addMessage, clearMessageHistory, forceStop, messageList, currentMessage, enableThinking, getMessageDetails } from '$lib/anthropic.js';
import { tick } from 'svelte';
import { get } from 'svelte/store';
import PanelButton from './PanelButton.svelte';
import SmallButton from './SmallButton.svelte';
import { aiActivity } from './activities.js';
import html2canvas from 'html2canvas-pro';
export let handleTool;
let stopRequested = false;
function handleKeyEnter(e)
{
if(e.key != "Enter")
return;
var value = e.target.value;
if(value == "")
return;
setApiKey(value);
}
function handleMessage(e)
{
if(e.key != "Enter")
return;
e.preventDefault();
var textArea = e.target;
var value = textArea.value;
if(value == "")
return;
textArea.style.height = "unset";
// Reset the textarea
currentMessage.set("");
addMessage(value, handleTool);
}
function handleResize(e)
{
var textArea = e.target;
textArea.style.height = textArea.scrollHeight + "px";
}
async function scrollToBottom(node)
{
await tick();
node.scrollTop = node.scrollHeight;
if (!get(aiActivity)) {
document.getElementById("ai-input").focus();
}
}
function scrollMessage(node, messageList)
{
// Make sure the messages are always scrolled to the bottom
scrollToBottom(node);
return {
update(messageList) {
scrollToBottom(node);
}
}
}
async function handleStop() {
stopRequested = true;
await forceStop();
stopRequested = false;
}
async function handleDownload() {
const messageListElement = document.getElementById('message-list');
// Temporarily add padding and background for the list
messageListElement.classList.add("p-1");
const canvas = await html2canvas(messageListElement);
messageListElement.classList.remove("p-1");
const link = document.createElement('a');
link.href = canvas.toDataURL('image/png');
link.download = 'WebVM_Claude.png';
link.click();
}
function toggleThinkingMode() {
enableThinking.set(!get(enableThinking));
}
</script>
<h1 class="text-lg font-bold">Claude AI Integration</h1>
<p>WebVM is integrated with Claude by Anthropic AI. You can prompt the AI to control the system.</p>
<p>You need to provide your API key. The key is only saved locally to your browser.</p>
<div class="flex grow flex-col overflow-y-hidden gap-2">
<p class="flex flex-row gap-2">
<span class="mr-auto flex items-center">Conversation history</span>
<SmallButton buttonIcon="fa-solid fa-download" clickHandler={handleDownload} buttonTooltip="Save conversation as image"></SmallButton>
<SmallButton buttonIcon="fa-solid fa-brain" clickHandler={toggleThinkingMode} buttonTooltip="{$enableThinking ? "Disable" : "Enable"} thinking mode" bgColor={$enableThinking ? "bg-neutral-500" : "bg-neutral-700"}></SmallButton>
<SmallButton buttonIcon="fa-solid fa-trash-can" clickHandler={clearMessageHistory} buttonTooltip="Clear conversation history"></SmallButton>
</p>
<div class="flex grow overflow-y-scroll scrollbar" use:scrollMessage={$messageList}>
<div class="h-full w-full">
<div class="w-full min-h-full flex flex-col gap-2 justify-end bg-neutral-600" id="message-list">
{#each $messageList as msg}
{@const details = getMessageDetails(msg)}
{#if details.isToolUse}
<p class="bg-neutral-700 p-2 rounded-md italic"><i class='fas {details.icon} w-6 mr-2 text-center'></i>{details.messageContent}</p>
{:else if !details.isToolResult}
<p class="{msg.role == 'error' ? 'bg-red-900' : 'bg-neutral-700'} p-2 rounded-md whitespace-pre-wrap"><i class='fas {details.icon} w-6 mr-2 text-center'></i>{details.messageContent}</p>
{/if}
{/each}
</div>
</div>
</div>
</div>
{#if $apiState == "KEY_REQUIRED"}
<textarea class="bg-neutral-700 p-2 rounded-md placeholder-gray-400 resize-none shrink-0" placeholder="Insert your Claude API Key" rows="1" on:keydown={handleKeyEnter} on:input={handleResize} id="ai-input"/>
{:else if $aiActivity}
{#if stopRequested }
<PanelButton buttonIcon="fa-solid fa-hand" buttonText="Stopping...">
</PanelButton>
{:else}
<PanelButton buttonIcon="fa-solid fa-hand" clickHandler={handleStop} buttonText="Stop">
</PanelButton>
{/if}
{:else}
<textarea class="bg-neutral-700 p-2 rounded-md placeholder-gray-400 resize-none shrink-0" placeholder={handleTool === null ? "Waiting for system initialization..." : "Prompt..."} rows="1" on:keydown={handleMessage} on:input={handleResize} bind:value={$currentMessage} id="ai-input" disabled={handleTool === null}/>
{/if}
+9
View File
@@ -0,0 +1,9 @@
<script>
export let title;
export let image;
export let url;
</script>
<a href={url} target="_blank"><div class="bg-neutral-700 hover:bg-neutral-500 p-2 rounded-md">
<img class="w-full aspect-[40/21]" src={image}>
<h2 class="text-sm font-bold">{title}</h2>
</div></a>
+12
View File
@@ -0,0 +1,12 @@
<script>
import PanelButton from './PanelButton.svelte';
import { cpuPercentage } from './activities.js'
</script>
<h1 class="text-lg font-bold">Engine</h1>
<PanelButton buttonImage="assets/cheerpx.svg" clickUrl="https://cheerpx.io/docs" buttonText="Explore CheerpX">
</PanelButton>
<p><span class="font-bold">Virtual CPU: </span>{$cpuPercentage}%</p>
<p>CheerpX is a x86 virtualization engine in WebAssembly</p>
<p>It can securely run unmodified x86 binaries and libraries in the browser</p>
<p>Excited about our technology? <a class="underline" href="https://cheerpx.io/docs/getting-started" target="_blank">Start building</a> your projects using <a class="underline" href="https://cheerpx.io/" target="_blank">CheerpX</a> today!</p>
+12
View File
@@ -0,0 +1,12 @@
<script>
import PanelButton from './PanelButton.svelte';
import DiscordPresenceCount from 'labs/packages/astro-theme/components/nav/DiscordPresenceCount.svelte'
</script>
<h1 class="text-lg font-bold">Discord</h1>
<PanelButton buttonImage="assets/discord-mark-blue.svg" clickUrl="https://discord.gg/yTNZgySKGa" buttonText="Join our Discord">
<i class='fas fa-circle fa-xs ml-auto text-green-500'></i>
<span class="ml-1"><DiscordPresenceCount /></span>
</PanelButton>
<p>Do you have any question about WebVM or CheerpX?</p>
<p>Join our community, we are happy to help!</p>
+62
View File
@@ -0,0 +1,62 @@
<script>
import PanelButton from './PanelButton.svelte';
import { createEventDispatcher } from 'svelte';
import { diskLatency } from './activities.js'
var dispatch = createEventDispatcher();
let state = "START";
function handleReset()
{
if(state == "START")
state = "CONFIRM";
else if (state == "CONFIRM") {
state = "RESETTING";
dispatch('reset');
}
}
function getButtonText(state)
{
if(state == "START")
return "Reset disk";
else if (state == "RESETTING")
return "Resetting...";
else
return "Reset disk. Confirm?";
}
function getBgColor(state)
{
if(state == "START")
{
// Use default
return undefined;
}
else
{
return "bg-red-900";
}
}
function getHoverColor(state)
{
if(state == "START")
{
// Use default
return undefined;
}
else
{
return "hover:bg-red-700";
}
}
</script>
<h1 class="text-lg font-bold">Disk</h1>
<PanelButton buttonIcon="fa-solid fa-trash-can" clickHandler={handleReset} buttonText={getButtonText(state)} bgColor={getBgColor(state)} hoverColor={getHoverColor(state)}>
</PanelButton>
{#if state == "CONFIRM"}
<p><span class="font-bold">Warning: </span>WebVM will reload</p>
{:else if state == "RESETTING"}
<p><span class="font-bold">Reset in progress: </span>Please wait...</p>
{:else}
<p><span class="font-bold">Backend latency: </span>{$diskLatency}ms</p>
{/if}
<p>WebVM runs on top of a complete Linux distribution</p>
<p>Filesystems up to 2GB are supported and data is downloaded completely on-demand</p>
<p>The WebVM cloud backend uses WebSockets and it's distributed via a global CDN to minimize download latency</p>
+13
View File
@@ -0,0 +1,13 @@
<script>
import PanelButton from './PanelButton.svelte';
import GitHubStarCount from 'labs/packages/astro-theme/components/nav/GitHubStarCount.svelte'
</script>
<h1 class="text-lg font-bold">GitHub</h1>
<PanelButton buttonImage="assets/github-mark-white.svg" clickUrl="https://github.com/leaningtech/webvm" buttonText="GitHub repo">
<i class='fas fa-star fa-xs ml-auto'></i>
<span class="ml-1"><GitHubStarCount repo="leaningtech/webvm"/></span>
</PanelButton>
<p>Like WebVM? <a class="underline" href="https://github.com/leaningtech/webvm" target="_blank">Give us a star!</a></p>
<p>WebVM is FOSS, you can fork it to build your own version and begin working on your CheerpX-based project</p>
<p>Found a bug? Please open a <a class="underline" href="https://github.com/leaningtech/webvm/issues" target="_blank">GitHub issue</a></p>
+23
View File
@@ -0,0 +1,23 @@
<script>
export let icon;
export let info;
export let activity;
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
function handleMouseover() {
dispatch('mouseover', info);
}
function handleClick() {
dispatch('click', { icon, info });
}
</script>
<div
class="p-3 cursor-pointer text-center hover:bg-neutral-600 {$activity ? "text-amber-500 animate-pulse" : "hover:text-gray-100"}"
style="animation-duration: 0.5s"
on:mouseenter={handleMouseover}
on:click={handleClick}
>
<i class='{icon} fa-xl'></i>
</div>
+11
View File
@@ -0,0 +1,11 @@
<h1 class="text-lg font-bold">Information</h1>
<img src="assets/webvm_hero.png" alt="WebVM Logo" class="w-56 h-56 object-contain self-center">
<p>WebVM is a virtual Linux environment running in the browser via WebAssembly</p>
<p>It is based on:</p>
<ul class="list-disc list-inside">
<li><a class="underline" target="_blank" href="https://cheerpx.io/">CheerpX</a>: x86 JIT in Wasm</li>
<li><a class="underline" target="_blank" href="https://xtermjs.org/">Xterm.js</a>: interactive terminal</li>
<li>Local/private <a class="underline" target="_blank" href="https://cheerpx.io/docs/guides/File-System-support">file storage</a></li>
<li><a class="underline" target="_blank" href="https://cheerpx.io/docs/guides/Networking">Networking</a> via <a class="underline" target="_blank" href="https://tailscale.com/">Tailscale</a></li>
</ul>
<slot></slot>
+24
View File
@@ -0,0 +1,24 @@
<script>
import { networkData, startLogin, updateButtonData } from '$lib/network.js'
import { createEventDispatcher } from 'svelte';
import PanelButton from './PanelButton.svelte';
var dispatch = createEventDispatcher();
var connectionState = networkData.connectionState;
var exitNode = networkData.exitNode;
function handleConnect() {
connectionState.set("DOWNLOADING");
dispatch('connect');
}
let buttonData = null;
$: buttonData = updateButtonData($connectionState, handleConnect);
</script>
<h1 class="text-lg font-bold">Networking</h1>
<PanelButton buttonImage="assets/tailscale.svg" clickUrl={buttonData.clickUrl} clickHandler={buttonData.clickHandler} rightClickHandler={buttonData.rightClickHandler} buttonTooltip={buttonData.buttonTooltip} buttonText={buttonData.buttonText}>
{#if $connectionState == "CONNECTED"}
<i class='fas fa-circle fa-xs ml-auto {$exitNode ? 'text-green-500' : 'text-amber-500'}' title={$exitNode ? 'Ready' : 'No exit node'}></i>
{/if}
</PanelButton>
<p>WebVM can connect to the Internet via Tailscale</p>
<p>Using Tailscale is required since browser do not support TCP/UDP sockets (yet!)</p>
+19
View File
@@ -0,0 +1,19 @@
<script>
export let clickUrl = null;
export let clickHandler = null;
export let rightClickHandler = null;
export let buttonTooltip = null;
export let bgColor = "bg-neutral-700";
export let hoverColor = "hover:bg-neutral-500"
export let buttonImage = null;
export let buttonIcon = null;
export let buttonText;
</script>
<a href={clickUrl} target="_blank" on:click={clickHandler} on:contextmenu={rightClickHandler}><p class="flex flex-row items-center {bgColor} p-2 rounded-md shadow-md shadow-neutral-900 {(clickUrl != null || clickHandler != null) ? `${hoverColor} cursor-pointer` : ""}" title={buttonTooltip}>
{#if buttonImage}
<img src={buttonImage} class="inline w-8 h-8"/>
{:else if buttonIcon}
<i class="w-8 {buttonIcon} text-center" style="font-size: 2em;"></i>
{/if}
<span class="ml-1">{buttonText}</span><slot></slot></p></a>
+14
View File
@@ -0,0 +1,14 @@
<script>
import BlogPost from './BlogPost.svelte';
import { page } from '$app/stores';
</script>
<h1 class="text-lg font-bold">Blog posts</h1>
<div class="overflow-y-scroll scrollbar flex flex-col gap-2">
{#each $page.data.posts as post}
<BlogPost
title={post.title}
image={post.image}
url={post.url}
/>
{/each}
</div>
+135
View File
@@ -0,0 +1,135 @@
<script>
import { createEventDispatcher } from 'svelte';
import Icon from './Icon.svelte';
import InformationTab from './InformationTab.svelte';
import NetworkingTab from './NetworkingTab.svelte';
import CpuTab from './CpuTab.svelte';
import DiskTab from './DiskTab.svelte';
import AnthropicTab from './AnthropicTab.svelte';
import PostsTab from './PostsTab.svelte';
import DiscordTab from './DiscordTab.svelte';
import GitHubTab from './GitHubTab.svelte';
import SmallButton from './SmallButton.svelte';
import { cpuActivity, diskActivity, aiActivity } from './activities.js';
const icons = [
{ icon: 'fas fa-info-circle', info: 'Information', activity: null },
{ icon: 'fas fa-wifi', info: 'Networking', activity: null },
{ icon: 'fas fa-microchip', info: 'CPU', activity: cpuActivity },
{ icon: 'fas fa-compact-disc', info: 'Disk', activity: diskActivity },
{ icon: 'fas fa-robot', info: 'ClaudeAI', activity: aiActivity },
null,
{ icon: 'fas fa-book-open', info: 'Posts', activity: null },
{ icon: 'fab fa-discord', info: 'Discord', activity: null },
{ icon: 'fab fa-github', info: 'GitHub', activity: null },
];
let dispatch = createEventDispatcher();
let activeInfo = null; // Tracks currently visible info.
let hideTimeout = 0; // Timeout for hiding info panel.
export let sideBarPinned;
function showInfo(info) {
clearTimeout(hideTimeout);
hideTimeout = 0;
activeInfo = info;
}
function hideInfo() {
// Never remove the sidebar if pinning is enabled
if(sideBarPinned)
return;
// Prevents multiple timers and hides the info panel after 400ms unless interrupted.
clearTimeout(hideTimeout);
hideTimeout = setTimeout(() => {
activeInfo = null;
hideTimeout = 0;
}, 400);
}
function handleMouseEnterPanel() {
clearTimeout(hideTimeout);
hideTimeout = 0;
}
// Toggles the info panel for the clicked icon.
function handleClick(icon) {
if(sideBarPinned)
return;
// Hides the panel if the icon is active. Otherwise, shows the panel with info.
if (activeInfo === icon.info) {
activeInfo = null;
} else {
activeInfo = icon.info;
}
}
function toggleSidebarPin() {
sideBarPinned = !sideBarPinned;
dispatch('sidebarPinChange', sideBarPinned);
}
export let handleTool;
</script>
<div class="flex flex-row w-14 h-full bg-neutral-700" >
<div class="flex flex-col shrink-0 w-14 text-gray-300">
{#each icons as i}
{#if i}
<Icon
icon={i.icon}
info={i.info}
activity={i.activity}
on:mouseover={(e) => showInfo(e.detail)}
on:click={() => handleClick(i)}
/>
{:else}
<div class="grow" on:mouseenter={handleMouseEnterPanel}></div>
{/if}
{/each}
</div>
<div
class="relative flex flex-col gap-5 shrink-0 w-80 h-full z-10 p-2 bg-neutral-600 text-gray-100 opacity-95"
class:hidden={!activeInfo}
on:mouseenter={handleMouseEnterPanel}
on:mouseleave={hideInfo}
>
<div class="absolute right-2 top-2">
<SmallButton
buttonIcon="fa-solid fa-thumbtack"
clickHandler={toggleSidebarPin}
buttonTooltip={sideBarPinned ? "Unpin Sidebar" : "Pin Sidebar"}
bgColor={sideBarPinned ? "bg-neutral-500" : "bg-neutral-700"}
/>
</div>
{#if activeInfo === 'Information'}
<InformationTab>
<slot></slot>
</InformationTab>
{:else if activeInfo === 'Networking'}
<NetworkingTab on:connect/>
{:else if activeInfo === 'CPU'}
<CpuTab/>
{:else if activeInfo === 'Disk'}
<DiskTab on:reset/>
{:else if activeInfo === 'ClaudeAI'}
<AnthropicTab handleTool={handleTool} />
{:else if activeInfo === 'Posts'}
<PostsTab/>
{:else if activeInfo === 'Discord'}
<DiscordTab/>
{:else if activeInfo === 'GitHub'}
<GitHubTab/>
{:else}
<p>TODO: {activeInfo}</p>
{/if}
<div class="mt-auto text-sm text-gray-300">
<div class="pt-1 pb-1">
<a href="https://cheerpx.io/" target="_blank">
<span>Powered by CheerpX</span>
<img src="assets/cheerpx.svg" alt="CheerpX Logo" class="w-6 h-6 inline-block">
</a>
</div>
<hr class="border-t border-solid border-gray-300">
<div class="pt-1 pb-1">
<a href="https://leaningtech.com/" target="”_blank”">© 2022-2025 Leaning Technologies</a>
</div>
</div>
</div>
</div>
+11
View File
@@ -0,0 +1,11 @@
<script>
export let clickHandler = null;
export let buttonTooltip = null;
export let bgColor = "bg-neutral-700";
export let hoverColor = "hover:bg-neutral-500"
export let buttonIcon = null;
</script>
<span class="inline-block h-7 {bgColor} text-sm p-1 rounded-md shadow-md shadow-neutral-900 {(clickHandler != null) ? `${hoverColor} cursor-pointer` : ""}" title={buttonTooltip} on:click={clickHandler}>
<i class="w-5 {buttonIcon} text-center"></i>
</span>
+385
View File
@@ -0,0 +1,385 @@
<script>
import { onMount, tick } from 'svelte';
import { get } from 'svelte/store';
import Nav from 'labs/packages/global-navbar/src/Nav.svelte';
import SideBar from '$lib/SideBar.svelte';
import '$lib/global.css';
import '@xterm/xterm/css/xterm.css'
import '@fortawesome/fontawesome-free/css/all.min.css'
import { networkInterface, startLogin } from '$lib/network.js'
import { cpuActivity, diskActivity, cpuPercentage, diskLatency } from '$lib/activities.js'
import { introMessage, errorMessage, unexpectedErrorMessage } from '$lib/messages.js'
import { displayConfig, handleToolImpl } from '$lib/anthropic.js'
import { tryPlausible } from '$lib/plausible.js'
export let configObj = null;
export let processCallback = null;
export let cacheId = null;
export let cpuActivityEvents = [];
export let diskLatencies = [];
export let activityEventsInterval = 0;
var term = null;
var cx = null;
var fitAddon = null;
var cxReadFunc = null;
var blockCache = null;
var processCount = 0;
var curVT = 0;
var sideBarPinned = false;
function writeData(buf, vt)
{
if(vt != 1)
return;
term.write(new Uint8Array(buf));
}
function readData(str)
{
if(cxReadFunc == null)
return;
for(var i=0;i<str.length;i++)
cxReadFunc(str.charCodeAt(i));
}
function printMessage(msg)
{
for(var i=0;i<msg.length;i++)
term.write(msg[i] + "\n");
}
function expireEvents(list, curTime, limitTime)
{
while(list.length > 1)
{
if(list[1].t < limitTime)
{
list.shift();
}
else
{
break;
}
}
}
function cleanupEvents()
{
var curTime = Date.now();
var limitTime = curTime - 10000;
expireEvents(cpuActivityEvents, curTime, limitTime);
computeCpuActivity(curTime, limitTime);
if(cpuActivityEvents.length == 0)
{
clearInterval(activityEventsInterval);
activityEventsInterval = 0;
}
}
function computeCpuActivity(curTime, limitTime)
{
var totalActiveTime = 0;
var lastActiveTime = limitTime;
var lastWasActive = false;
for(var i=0;i<cpuActivityEvents.length;i++)
{
var e = cpuActivityEvents[i];
// NOTE: The first event could be before the limit,
// we need at least one event to correctly mark
// active time when there is long time under load
var eTime = e.t;
if(eTime < limitTime)
eTime = limitTime;
if(e.state == "ready")
{
// Inactive state, add the time from lastActiveTime
totalActiveTime += (eTime - lastActiveTime);
lastWasActive = false;
}
else
{
// Active state
lastActiveTime = eTime;
lastWasActive = true;
}
}
// Add the last interval if needed
if(lastWasActive)
{
totalActiveTime += (curTime - lastActiveTime);
}
cpuPercentage.set(Math.ceil((totalActiveTime / 10000) * 100));
}
function hddCallback(state)
{
diskActivity.set(state != "ready");
}
function latencyCallback(latency)
{
diskLatencies.push(latency);
if(diskLatencies.length > 30)
diskLatencies.shift();
// Average the latency over at most 30 blocks
var total = 0;
for(var i=0;i<diskLatencies.length;i++)
total += diskLatencies[i];
var avg = total / diskLatencies.length;
diskLatency.set(Math.ceil(avg));
}
function cpuCallback(state)
{
cpuActivity.set(state != "ready");
var curTime = Date.now();
var limitTime = curTime - 10000;
expireEvents(cpuActivityEvents, curTime, limitTime);
cpuActivityEvents.push({t: curTime, state: state});
computeCpuActivity(curTime, limitTime);
// Start an interval timer to cleanup old samples when no further activity is received
if(activityEventsInterval != 0)
clearInterval(activityEventsInterval);
activityEventsInterval = setInterval(cleanupEvents, 2000);
}
function computeXTermFontSize()
{
return parseInt(getComputedStyle(document.body).fontSize);
}
function setScreenSize(display)
{
var internalMult = 1.0;
var displayWidth = display.offsetWidth;
var displayHeight = display.offsetHeight;
var minWidth = 1024;
var minHeight = 768;
if(displayWidth < minWidth)
internalMult = minWidth / displayWidth;
if(displayHeight < minHeight)
internalMult = Math.max(internalMult, minHeight / displayHeight);
var internalWidth = Math.floor(displayWidth * internalMult);
var internalHeight = Math.floor(displayHeight * internalMult);
cx.setKmsCanvas(display, internalWidth, internalHeight);
// Compute the size to be used for AI screenshots
var screenshotMult = 1.0;
var maxWidth = 1024;
var maxHeight = 768;
if(internalWidth > maxWidth)
screenshotMult = maxWidth / internalWidth;
if(internalHeight > maxHeight)
screenshotMult = Math.min(screenshotMult, maxHeight / internalHeight);
var screenshotWidth = Math.floor(internalWidth * screenshotMult);
var screenshotHeight = Math.floor(internalHeight * screenshotMult);
// Track the state of the mouse as requested by the AI, to avoid losing the position due to user movement
displayConfig.set({width: screenshotWidth, height: screenshotHeight, mouseMult: internalMult * screenshotMult});
}
var curInnerWidth = 0;
var curInnerHeight = 0;
function handleResize()
{
// Avoid spurious resize events caused by the soft keyboard
if(curInnerWidth == window.innerWidth && curInnerHeight == window.innerHeight)
return;
curInnerWidth = window.innerWidth;
curInnerHeight = window.innerHeight;
triggerResize();
}
function triggerResize()
{
term.options.fontSize = computeXTermFontSize();
fitAddon.fit();
const display = document.getElementById("display");
if(display)
setScreenSize(display);
}
async function initTerminal()
{
const { Terminal } = await import('@xterm/xterm');
const { FitAddon } = await import('@xterm/addon-fit');
const { WebLinksAddon } = await import('@xterm/addon-web-links');
term = new Terminal({cursorBlink:true, convertEol:true, fontFamily:"monospace", fontWeight: 400, fontWeightBold: 700, fontSize: computeXTermFontSize()});
fitAddon = new FitAddon();
term.loadAddon(fitAddon);
var linkAddon = new WebLinksAddon();
term.loadAddon(linkAddon);
const consoleDiv = document.getElementById("console");
term.open(consoleDiv);
term.scrollToTop();
fitAddon.fit();
window.addEventListener("resize", handleResize);
term.focus();
term.onData(readData);
// Avoid undesired default DnD handling
function preventDefaults (e) {
e.preventDefault()
e.stopPropagation()
}
consoleDiv.addEventListener("dragover", preventDefaults, false);
consoleDiv.addEventListener("dragenter", preventDefaults, false);
consoleDiv.addEventListener("dragleave", preventDefaults, false);
consoleDiv.addEventListener("drop", preventDefaults, false);
curInnerWidth = window.innerWidth;
curInnerHeight = window.innerHeight;
if(configObj.printIntro)
printMessage(introMessage);
try
{
await initCheerpX();
}
catch(e)
{
printMessage(unexpectedErrorMessage);
printMessage([e.toString()]);
return;
}
}
function handleActivateConsole(vt)
{
if(curVT == vt)
return;
curVT = vt;
if(vt != 7)
return;
// Raise the display to the foreground
const display = document.getElementById("display");
display.parentElement.style.zIndex = 5;
tryPlausible("Display activated");
}
function handleProcessCreated()
{
processCount++;
if(processCallback)
processCallback(processCount);
}
async function initCheerpX()
{
const CheerpX = await import('@leaningtech/cheerpx');
var blockDevice = null;
switch(configObj.diskImageType)
{
case "cloud":
try
{
blockDevice = await CheerpX.CloudDevice.create(configObj.diskImageUrl);
}
catch(e)
{
// Report the failure and try again with plain HTTP
var wssProtocol = "wss:";
if(configObj.diskImageUrl.startsWith(wssProtocol))
{
// WebSocket protocol failed, try agin using plain HTTP
tryPlausible("WS Disk failure");
blockDevice = await CheerpX.CloudDevice.create("https:" + configObj.diskImageUrl.substr(wssProtocol.length));
}
else
{
// No other recovery option
throw e;
}
}
break;
case "bytes":
blockDevice = await CheerpX.HttpBytesDevice.create(configObj.diskImageUrl);
break;
case "github":
blockDevice = await CheerpX.GitHubDevice.create(configObj.diskImageUrl);
break;
default:
throw new Error("Unrecognized device type");
}
blockCache = await CheerpX.IDBDevice.create(cacheId);
var overlayDevice = await CheerpX.OverlayDevice.create(blockDevice, blockCache);
var webDevice = await CheerpX.WebDevice.create("");
var documentsDevice = await CheerpX.WebDevice.create("documents");
var dataDevice = await CheerpX.DataDevice.create();
var mountPoints = [
// The root filesystem, as an Ext2 image
{type:"ext2", dev:overlayDevice, path:"/"},
// Access to files on the Web server, relative to the current page
{type:"dir", dev:webDevice, path:"/web"},
// Access to read-only data coming from JavaScript
{type:"dir", dev:dataDevice, path:"/data"},
// Automatically created device files
{type:"devs", path:"/dev"},
// Pseudo-terminals
{type:"devpts", path:"/dev/pts"},
// The Linux 'proc' filesystem which provides information about running processes
{type:"proc", path:"/proc"},
// The Linux 'sysfs' filesystem which is used to enumerate emulated devices
{type:"sys", path:"/sys"},
// Convenient access to sample documents in the user directory
{type:"dir", dev:documentsDevice, path:"/home/user/documents"}
];
try
{
cx = await CheerpX.Linux.create({mounts: mountPoints, networkInterface: networkInterface});
}
catch(e)
{
printMessage(errorMessage);
printMessage([e.toString()]);
return;
}
cx.registerCallback("cpuActivity", cpuCallback);
cx.registerCallback("diskActivity", hddCallback);
cx.registerCallback("diskLatency", latencyCallback);
cx.registerCallback("processCreated", handleProcessCreated);
term.scrollToBottom();
cxReadFunc = cx.setCustomConsole(writeData, term.cols, term.rows);
const display = document.getElementById("display");
if(display)
{
setScreenSize(display);
cx.setActivateConsole(handleActivateConsole);
}
// Run the command in a loop, in case the user exits
while (true)
{
await cx.run(configObj.cmd, configObj.args, configObj.opts);
}
}
onMount(initTerminal);
async function handleConnect()
{
const w = window.open("login.html", "_blank");
cx.networkLogin();
try
{
w.location.href = await startLogin();
}
catch(e)
{
w.close();
console.warn(e);
}
}
async function handleReset()
{
// Be robust before initialization
if(blockCache == null)
return;
await blockCache.reset();
location.reload();
}
async function handleTool(tool)
{
return await handleToolImpl(tool, term);
}
async function handleSidebarPinChange(event)
{
sideBarPinned = event.detail;
// Make sure the pinning state of reflected in the layout
await tick();
// Adjust the layout based on the new sidebar state
triggerResize();
}
</script>
<main class="relative w-full h-full">
<Nav />
<div class="absolute top-10 bottom-0 left-0 right-0">
<SideBar on:connect={handleConnect} on:reset={handleReset} handleTool={!configObj.needsDisplay || curVT == 7 ? handleTool : null} on:sidebarPinChange={handleSidebarPinChange}>
<slot></slot>
</SideBar>
{#if configObj.needsDisplay}
<div class="absolute top-0 bottom-0 {sideBarPinned ? 'left-[23.5rem]' : 'left-14'} right-0">
<canvas class="w-full h-full cursor-none" id="display"></canvas>
</div>
{/if}
<div class="absolute top-0 bottom-0 {sideBarPinned ? 'left-[23.5rem]' : 'left-14'} right-0 p-1 scrollbar" id="console">
</div>
</div>
</main>
+7
View File
@@ -0,0 +1,7 @@
import { writable } from 'svelte/store';
export const cpuActivity = writable(false);
export const diskActivity = writable(false);
export const aiActivity = writable(false);
export const cpuPercentage = writable(0);
export const diskLatency = writable(0);
+507
View File
@@ -0,0 +1,507 @@
import { get, writable } from 'svelte/store';
import { browser } from '$app/environment'
import { aiActivity } from '$lib/activities.js'
import { tryPlausible } from '$lib/plausible.js';
import Anthropic from '@anthropic-ai/sdk';
var client = null;
var messages = [];
var stopFlag = false;
var lastScreenshot = null;
var screenshotCanvas = null;
var screenshotCtx = null;
export function setApiKey(key)
{
client = new Anthropic({apiKey: key, dangerouslyAllowBrowser: true});
// Reset messages
messages = []
messageList.set(messages);
localStorage.setItem("anthropic-api-key", key);
apiState.set("READY");
tryPlausible("ClaudeAI Key");
}
function clearApiKey()
{
localStorage.removeItem("anthropic-api-key");
apiState.set("KEY_REQUIRED");
}
function addMessageInternal(role, content)
{
messages.push({role: role, content: content});
messageList.set(messages);
}
async function sendMessages(handleTool)
{
aiActivity.set(true);
try
{
var dc = get(displayConfig);
var tool = dc ? { type: "computer_20250124", name: "computer", display_width_px: dc.width, display_height_px: dc.height, display_number: 1 } : { type: "bash_20250124", name: "bash" }
const config = {max_tokens: 2048,
messages: messages,
system: "You are running on a virtualized machine. Wait some extra time after all operations to compensate for slowdown.",
model: 'claude-3-7-sonnet-20250219',
tools: [tool],
tool_choice: {type: "auto", disable_parallel_tool_use: true},
betas: ["computer-use-2025-01-24"]
};
if(get(enableThinking))
config.thinking = { type: "enabled", budget_tokens: 1024 };
const response = await client.beta.messages.create(config);
if(stopFlag)
{
aiActivity.set(false);
return;
}
// Remove all the image payloads, we don't want to send them over and over again
for(var i=0;i<messages.length;i++)
{
var c = messages[i].content;
if(Array.isArray(c))
{
if(c[0].type == "tool_result" && c[0].content && c[0].content[0].type == "image")
delete c[0].content;
}
}
var content = response.content;
// Be robust to multiple response
for(var i=0;i<content.length;i++)
{
var c = content[i];
if(c.type == "text")
{
addMessageInternal(response.role, c.text);
}
else if(c.type == "tool_use")
{
addMessageInternal(response.role, [c]);
var commandResponse = await handleTool(c.input);
var responseObj = {type: "tool_result", tool_use_id: c.id };
if(commandResponse != null)
{
if(commandResponse instanceof Error)
{
console.warn(`Tool error: ${commandResponse.message}`);
responseObj.content = commandResponse.message;
responseObj.is_error = true;
}
else
{
responseObj.content = commandResponse;
}
}
addMessageInternal("user", [responseObj]);
if(stopFlag)
{
// Maintain state consitency by stopping after adding a valid response
aiActivity.set(false);
return;
}
sendMessages(handleTool);
}
else if(c.type == "thinking")
{
addMessageInternal(response.role, [c]);
}
else
{
console.warn(`Invalid response type: ${c.type}`);
}
}
if(response.stop_reason == "end_turn")
{
tryPlausible("ClaudeAI Success");
aiActivity.set(false);
}
}
catch(e)
{
tryPlausible("ClaudeAI Error");
if(e.status == 401)
{
addMessageInternal('error', 'Invalid API key');
clearApiKey();
}
else
{
addMessageInternal('error', e.error.error.message);
}
}
}
export function addMessage(text, handleTool)
{
addMessageInternal('user', text);
sendMessages(handleTool);
tryPlausible("ClaudeAI Use");
}
export function clearMessageHistory() {
messages.length = 0;
messageList.set(messages);
}
export function forceStop() {
stopFlag = true;
return new Promise((resolve) => {
const unsubscribe = aiActivity.subscribe((value) => {
if (!value) {
unsubscribe();
stopFlag = false;
resolve();
}
});
});
}
export function getMessageDetails(msg) {
const isToolUse = Array.isArray(msg.content) && msg.content[0].type === "tool_use";
const isToolResult = Array.isArray(msg.content) && msg.content[0].type === "tool_result";
const isThinking = Array.isArray(msg.content) && msg.content[0].type === "thinking";
let icon = "";
let messageContent = "";
if (isToolUse) {
let tool = msg.content[0].input;
if (tool.action === "screenshot") {
icon = "fa-desktop";
messageContent = "Screenshot";
} else if (tool.action === "mouse_move") {
icon = "fa-mouse-pointer";
var coords = tool.coordinate;
messageContent = `Mouse at (${coords[0]}, ${coords[1]})`;
} else if (tool.action === "left_click") {
icon = "fa-mouse-pointer";
var coords = tool.coordinate;
messageContent = `Left click at (${coords[0]}, ${coords[1]})`;
} else if (tool.action === "right_click") {
icon = "fa-mouse-pointer";
var coords = tool.coordinate;
messageContent = `Right click at (${coords[0]}, ${coords[1]})`;
} else if (tool.action === "wait") {
icon = "fa-hourglass-half";
messageContent = "Waiting";
} else if (tool.action === "key") {
icon = "fa-keyboard";
messageContent = `Key press: ${tool.text}`;
} else if (tool.action === "type") {
icon = "fa-keyboard";
messageContent = "Type text";
} else {
icon = "fa-screwdriver-wrench";
messageContent = "Use the system";
}
} else if (isThinking) {
icon = "fa-brain";
messageContent = "Thinking...";
} else {
icon = msg.role === "user" ? "fa-user" : "fa-robot";
messageContent = msg.content;
}
return {
isToolUse,
isToolResult,
icon,
messageContent,
role: msg.role
};
}
async function yieldHelper(timeout)
{
return new Promise(function(f2, r2)
{
setTimeout(f2, timeout);
});
}
async function kmsSendChar(textArea, charStr)
{
textArea.value = "_" + charStr;
var ke = new KeyboardEvent("keydown");
textArea.dispatchEvent(ke);
var ke = new KeyboardEvent("keyup");
textArea.dispatchEvent(ke);
await yieldHelper(0);
}
function getKmsInputElement()
{
// Find the CheerpX textare, it's attached to the body element
for(const node of document.body.children)
{
if(node.tagName == "TEXTAREA")
return node;
}
return null;
}
export async function handleToolImpl(tool, term)
{
if(tool.command)
{
var sentinel = "# End of AI command";
var buffer = term.buffer.active;
// Get the current cursor position
var marker = term.registerMarker();
var startLine = marker.line;
marker.dispose();
var ret = new Promise(function(f, r)
{
var callbackDisposer = term.onWriteParsed(function()
{
var curLength = buffer.length;
// Accumulate the output and see if the sentinel has been printed
var output = "";
for(var i=startLine + 1;i<curLength;i++)
{
var curLine = buffer.getLine(i).translateToString(true, 0, term.cols);;
if(curLine.indexOf(sentinel) >= 0)
{
// We are done, cleanup and return
callbackDisposer.dispose();
return f(output);
}
output += curLine + "\n";
}
});
});
term.input(tool.command);
term.input("\n");
term.input(sentinel);
term.input("\n");
return ret;
}
else if(tool.action)
{
// Desktop control
// TODO: We should have an explicit API to interact with CheerpX display
switch(tool.action)
{
case "screenshot":
{
// Insert a 3 seconds delay unconditionally, the reference implementation uses 2
await yieldHelper(3000);
var delayCount = 0;
var display = document.getElementById("display");
var dc = get(displayConfig);
if(screenshotCanvas == null)
{
screenshotCanvas = document.createElement("canvas");
screenshotCtx = screenshotCanvas.getContext("2d");
}
if(screenshotCanvas.width != dc.width || screenshotCanvas.height != dc.height)
{
screenshotCanvas.width = dc.width;
screenshotCanvas.height = dc.height;
}
while(1)
{
// Resize the canvas to a Claude compatible size
screenshotCtx.drawImage(display, 0, 0, display.width, display.height, 0, 0, dc.width, dc.height);
var dataUrl = screenshotCanvas.toDataURL("image/png");
if(dataUrl == lastScreenshot)
{
// Delay at most 3 times
if(delayCount < 3)
{
// TODO: Defensive message, validate and remove
console.warn("Identical screenshot, rate limiting");
delayCount++;
// Wait some time and retry
await yieldHelper(5000);
continue;
}
}
lastScreenshot = dataUrl;
// Remove prefix from the encoded data
dataUrl = dataUrl.substring("data:image/png;base64,".length);
var imageSrc = { type: "base64", media_type: "image/png", data: dataUrl };
var contentObj = { type: "image", source: imageSrc };
return [ contentObj ];
}
}
case "mouse_move":
{
var coords = tool.coordinate;
var dc = get(displayConfig);
var mouseX = coords[0] / dc.mouseMult;
var mouseY = coords[1] / dc.mouseMult;
var display = document.getElementById("display");
var clientRect = display.getBoundingClientRect();
var me = new MouseEvent('mousemove', { clientX: mouseX + clientRect.left, clientY: mouseY + clientRect.top });
display.dispatchEvent(me);
return null;
}
case "left_click":
{
var coords = tool.coordinate;
var dc = get(displayConfig);
var mouseX = coords[0] / dc.mouseMult;
var mouseY = coords[1] / dc.mouseMult;
var display = document.getElementById("display");
var clientRect = display.getBoundingClientRect();
var me = new MouseEvent('mousedown', { clientX: mouseX + clientRect.left, clientY: mouseY + clientRect.top, button: 0 });
display.dispatchEvent(me);
// This delay prevent X11 logic from debouncing the mouseup
await yieldHelper(60)
me = new MouseEvent('mouseup', { clientX: mouseX + clientRect.left, clientY: mouseY + clientRect.top, button: 0 });
display.dispatchEvent(me);
return null;
}
case "right_click":
{
var coords = tool.coordinate;
var dc = get(displayConfig);
var mouseX = coords[0] / dc.mouseMult;
var mouseY = coords[1] / dc.mouseMult;
var display = document.getElementById("display");
var clientRect = display.getBoundingClientRect();
var me = new MouseEvent('mousedown', { clientX: mouseX + clientRect.left, clientY: mouseY + clientRect.top, button: 2 });
display.dispatchEvent(me);
// This delay prevent X11 logic from debouncing the mouseup
await yieldHelper(60)
me = new MouseEvent('mouseup', { clientX: mouseX + clientRect.left, clientY: mouseY + clientRect.top, button: 2 });
display.dispatchEvent(me);
return null;
}
case "type":
{
var str = tool.text;
return new Promise(async function(f, r)
{
var textArea = getKmsInputElement();
for(var i=0;i<str.length;i++)
{
await kmsSendChar(textArea, str[i]);
}
f(null);
});
}
case "key":
{
var textArea = getKmsInputElement();
var key = tool.text;
// Support arbitrary order of modifiers
var isCtrl = false;
var isAlt = false;
var isShift = false;
while(1)
{
if(key.startsWith("shift+"))
{
isShift = true;
key = key.substr("shift+".length);
var ke = new KeyboardEvent("keydown", {keyCode: 0x10});
textArea.dispatchEvent(ke);
await yieldHelper(0);
continue;
}
else if(key.startsWith("ctrl+"))
{
isCtrl = true;
key = key.substr("ctrl+".length);
var ke = new KeyboardEvent("keydown", {keyCode: 0x11});
textArea.dispatchEvent(ke);
await yieldHelper(0);
continue;
}
else if(key.startsWith("alt+"))
{
isAlt = true;
key = key.substr("alt+".length);
var ke = new KeyboardEvent("keydown", {keyCode: 0x12});
textArea.dispatchEvent(ke);
await yieldHelper(0);
continue;
}
break;
}
var ret = null;
// Dispatch single chars directly and parse the rest
if(key.length == 1)
{
await kmsSendChar(textArea, key);
}
else
{
switch(tool.text)
{
case "Return":
await kmsSendChar(textArea, "\n");
break;
case "Escape":
var ke = new KeyboardEvent("keydown", {keyCode: 0x1b});
textArea.dispatchEvent(ke);
await yieldHelper(0);
ke = new KeyboardEvent("keyup", {keyCode: 0x1b});
textArea.dispatchEvent(ke);
await yieldHelper(0);
break;
default:
// TODO: Support more key combinations
ret = new Error(`Error: Invalid key '${tool.text}'`);
}
}
if(isShift)
{
var ke = new KeyboardEvent("keyup", {keyCode: 0x10});
textArea.dispatchEvent(ke);
await yieldHelper(0);
}
if(isCtrl)
{
var ke = new KeyboardEvent("keyup", {keyCode: 0x11});
textArea.dispatchEvent(ke);
await yieldHelper(0);
}
if(isAlt)
{
var ke = new KeyboardEvent("keyup", {keyCode: 0x12});
textArea.dispatchEvent(ke);
await yieldHelper(0);
}
return ret;
}
case "wait":
{
// Wait 2x what the machine expects to compensate for virtualization slowdown
await yieldHelper(tool.duration * 2 * 1000);
return null;
}
default:
{
break;
}
}
return new Error("Error: Invalid action");
}
else
{
// We can get there due to model hallucinations
return new Error("Error: Invalid tool syntax");
}
}
function initialize()
{
var savedApiKey = localStorage.getItem("anthropic-api-key");
if(savedApiKey)
setApiKey(savedApiKey);
}
export const apiState = writable("KEY_REQUIRED");
export const messageList = writable(messages);
export const currentMessage = writable("");
export const displayConfig = writable(null);
export const enableThinking = writable(false);
if(browser)
initialize();
+26
View File
@@ -0,0 +1,26 @@
@import url('https://fonts.googleapis.com/css2?family=Archivo:ital,wght@0,100..900;1,100..900&display=swap');
@tailwind base;
@tailwind utilities;
body
{
font-family: Archivo, sans-serif;
margin: 0;
height: 100%;
overflow: hidden;
background: black;
}
html
{
height: 100%;
}
@media (width <= 850px)
{
html
{
font-size: calc(100vw / 55);
}
}
+53
View File
@@ -0,0 +1,53 @@
const color = "\x1b[1;35m";
const underline = "\x1b[94;4m";
const normal = "\x1b[0m";
export const introMessage = [
"+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+",
"| |",
"| WebVM is a virtual Linux environment running in the browser via WebAssembly.|",
"| |",
"| WebVM is powered by the CheerpX virtualization engine, which enables safe, |",
"| sandboxed execution of x86 binaries, fully client-side. |",
"| |",
"| CheerpX includes an x86-to-WebAssembly JIT compiler, a virtual block-based |",
"| file system, and a Linux syscall emulator. |",
"| |",
"| Try out the new Alpine / Xorg / i3 WebVM: " + underline + "https://webvm.io/alpine.html" + normal + " |",
"| |",
"| [News] BrowserCode: run Claude Code in the browser via WebAssembly |",
"| |",
"| " + underline + "https://browsercode.io/claude" + normal + " |",
"| |",
"+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+",
"",
" Welcome to WebVM. If unsure, try these examples:",
"",
" python3 examples/python3/fibonacci.py ",
" gcc -o helloworld examples/c/helloworld.c && ./helloworld",
" objdump -d ./helloworld | less -M",
" vim examples/c/helloworld.c",
" curl --max-time 15 parrot.live # requires networking",
"",
];
export const errorMessage = [
color + "CheerpX could not start" + normal,
"",
"Check the DevTools console for more information",
"",
"CheerpX is expected to work with recent desktop versions of Chrome, Edge, Firefox and Safari",
"",
"Give it a try from a desktop version / another browser!",
"",
"CheerpX internal error message is:",
"",
];
export const unexpectedErrorMessage = [
color + "WebVM encountered an unexpected error" + normal,
"",
"Check the DevTools console for further information",
"",
"Please consider reporting a bug!",
"",
"CheerpX internal error message is:",
"",
];
+190
View File
@@ -0,0 +1,190 @@
import { writable } from 'svelte/store';
import { browser } from '$app/environment'
let authKey = undefined;
let controlUrl = undefined;
if(browser)
{
let params = new URLSearchParams("?"+window.location.hash.substr(1));
authKey = params.get("authKey") || undefined;
controlUrl = params.get("controlUrl") || undefined;
}
let dashboardUrl = controlUrl ? null : "https://login.tailscale.com/admin/machines";
let resolveLogin = null;
let rejectLogin = null;
let loginPromise = null;
let connectionState = writable("DISCONNECTED");
let exitNode = writable(false);
function resetLoginPromise()
{
loginPromise = new Promise((f,r) => {
resolveLogin = f;
rejectLogin = r;
});
}
function validateLoginUrl(url)
{
const parsedUrl = new URL(url);
if(parsedUrl.protocol != "https:" && parsedUrl.protocol != "http:")
throw new Error("Invalid Tailscale login URL scheme");
return parsedUrl.href;
}
function loginUrlCb(url)
{
try
{
url = validateLoginUrl(url);
}
catch(e)
{
connectionState.set("LOGINFAILED");
rejectLogin(e);
resetLoginPromise();
return;
}
connectionState.set("LOGINREADY");
resolveLogin(url);
}
function stateUpdateCb(state)
{
switch(state)
{
case 6 /*Running*/:
{
connectionState.set("CONNECTED");
break;
}
}
}
function netmapUpdateCb(map)
{
networkData.currentIp = map.self.addresses[0];
var exitNodeFound = false;
for(var i=0; i < map.peers.length;i++)
{
if(map.peers[i].exitNode)
{
exitNodeFound = true;
break;
}
}
if(exitNodeFound)
{
exitNode.set(true);
}
}
export async function startLogin()
{
connectionState.set("LOGINSTARTING");
const url = await loginPromise;
networkData.loginUrl = url;
return url;
}
async function handleCopyIP(event)
{
// To prevent the default contexmenu from showing up when right-clicking..
event.preventDefault();
// Copy the IP to the clipboard.
try
{
await window.navigator.clipboard.writeText(networkData.currentIp)
connectionState.set("IPCOPIED");
setTimeout(() => {
connectionState.set("CONNECTED");
}, 2000);
}
catch(msg)
{
console.log("Copy ip to clipboard: Error: " + msg);
}
}
export function updateButtonData(state, handleConnect) {
switch(state) {
case "DISCONNECTED":
return {
buttonText: "Connect to Tailscale",
isClickable: true,
clickHandler: handleConnect,
clickUrl: null,
buttonTooltip: null,
rightClickHandler: null
};
case "DOWNLOADING":
return {
buttonText: "Loading IP stack...",
isClickable: false,
clickHandler: null,
clickUrl: null,
buttonTooltip: null,
rightClickHandler: null
};
case "LOGINSTARTING":
return {
buttonText: "Starting Login...",
isClickable: false,
clickHandler: null,
clickUrl: null,
buttonTooltip: null,
rightClickHandler: null
};
case "LOGINREADY":
return {
buttonText: "Login to Tailscale",
isClickable: true,
clickHandler: null,
clickUrl: networkData.loginUrl,
buttonTooltip: null,
rightClickHandler: null
};
case "LOGINFAILED":
return {
buttonText: "Invalid login URL",
isClickable: false,
clickHandler: null,
clickUrl: null,
buttonTooltip: null,
rightClickHandler: null
};
case "CONNECTED":
return {
buttonText: `IP: ${networkData.currentIp}`,
isClickable: true,
clickHandler: null,
clickUrl: networkData.dashboardUrl,
buttonTooltip: "Right-click to copy",
rightClickHandler: handleCopyIP
};
case "IPCOPIED":
return {
buttonText: "Copied!",
isClickable: false,
clickHandler: null,
clickUrl: null,
buttonTooltip: null,
rightClickHandler: null
};
default:
return {
buttonText: `Text for state: ${state}`,
isClickable: false,
clickHandler: null,
clickUrl: null,
buttonTooltip: null,
rightClickHandler: null
};
}
}
export const networkInterface = { authKey: authKey, controlUrl: controlUrl, loginUrlCb: loginUrlCb, stateUpdateCb: stateUpdateCb, netmapUpdateCb: netmapUpdateCb };
export const networkData = { currentIp: null, connectionState: connectionState, exitNode: exitNode, loginUrl: null, dashboardUrl: dashboardUrl }
resetLoginPromise();
+6
View File
@@ -0,0 +1,6 @@
// Some ad-blockers block the plausible script from loading. Check if `plausible`
// is defined before calling it.
export function tryPlausible(msg) {
if (self.plausible)
plausible(msg)
}
+46
View File
@@ -0,0 +1,46 @@
import { parse } from 'node-html-parser';
import { read } from '$app/server';
var posts = [
"https://labs.leaningtech.com/blog/webvm-claude",
"https://labs.leaningtech.com/blog/cx-10",
"https://labs.leaningtech.com/blog/webvm-20",
"https://labs.leaningtech.com/blog/join-the-webvm-hackathon",
"https://labs.leaningtech.com/blog/mini-webvm-your-linux-box-from-dockerfile-via-wasm",
"https://labs.leaningtech.com/blog/webvm-virtual-machine-with-networking-via-tailscale",
"https://labs.leaningtech.com/blog/webvm-server-less-x86-virtual-machines-in-the-browser",
];
async function getPostData(u)
{
var ret = { title: null, image: null, url: u };
var response = await fetch(u);
var str = await response.text();
var root = parse(str);
var tags = root.getElementsByTagName("meta");
for(var i=0;i<tags.length;i++)
{
var metaName = tags[i].getAttribute("property");
var metaContent = tags[i].getAttribute("content");
switch(metaName)
{
case "og:title":
ret.title = metaContent;
break;
case "og:image":
ret.image = metaContent;
break;
}
}
return ret;
}
export async function load()
{
var ret = [];
for(var i=0;i<posts.length;i++)
{
ret.push(await getPostData(posts[i]));
}
return { posts: ret };
}
+1
View File
@@ -0,0 +1 @@
export const prerender = true;
+17
View File
@@ -0,0 +1,17 @@
<script>
import WebVM from '$lib/WebVM.svelte';
import * as configObj from '/config_terminal';
import { tryPlausible } from '$lib/plausible.js';
function handleProcessCreated(processCount)
{
// Log the first 5 processes, to get an idea of the level of interaction from the public
if(processCount <= 5)
{
tryPlausible(`Process started: ${processCount}`);
}
}
</script>
<WebVM configObj={configObj} processCallback={handleProcessCreated} cacheId="blocks_terminal">
<p>Looking for a complete desktop experience? Try the new <a class="underline" href="/alpine.html" target="_blank">Alpine Linux</a> graphical WebVM</p>
</WebVM>
+1
View File
@@ -0,0 +1 @@
export const prerender = true;
+17
View File
@@ -0,0 +1,17 @@
<script>
import WebVM from '$lib/WebVM.svelte';
import * as configObj from '/config_public_alpine';
import { tryPlausible } from '$lib/plausible.js';
function handleProcessCreated(processCount)
{
// Log only the first process, as a proxy for successful startup
if(processCount == 1)
{
tryPlausible("Alpine init");
}
}
</script>
<WebVM configObj={configObj} processCallback={handleProcessCreated} cacheId="blocks_alpine">
<p>Looking for something different? Try the classic <a class="underline" href="/" target="_blank">Debian Linux</a> terminal-based WebVM</p>
</WebVM>
+12
View File
@@ -0,0 +1,12 @@
import adapter from '@sveltejs/adapter-static';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
/** @type {import('@sveltejs/kit').Config} */
const config = {
kit: {
adapter: adapter()
},
preprocess: vitePreprocess()
};
export default config;
+9
View File
@@ -0,0 +1,9 @@
/** @type {import('tailwindcss').Config} */
export default {
content: ['./src/**/*.{html,js,svelte,ts}'],
theme: {
extend: {},
},
plugins: [],
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

+28
View File
@@ -0,0 +1,28 @@
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
import { viteStaticCopy } from 'vite-plugin-static-copy';
export default defineConfig({
resolve: {
alias: {
'/config_terminal': process.env.WEBVM_MODE == "github" ? 'config_github_terminal.js' : 'config_public_terminal.js',
"@leaningtech/cheerpx": process.env.CX_URL ? process.env.CX_URL : "@leaningtech/cheerpx"
}
},
build: {
target: "es2022"
},
plugins: [
sveltekit(),
viteStaticCopy({
targets: [
{ src: 'tower.ico', dest: '' },
{ src: 'scrollbar.css', dest: '' },
{ src: 'serviceWorker.js', dest: '' },
{ src: 'login.html', dest: '' },
{ src: 'assets/', dest: '' },
{ src: 'documents/', dest: '' }
]
})
]
});
+2
View File
@@ -0,0 +1,2 @@
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.FitAddon=t():e.FitAddon=t()}(self,(()=>(()=>{"use strict";var e={};return(()=>{var t=e;Object.defineProperty(t,"__esModule",{value:!0}),t.FitAddon=void 0,t.FitAddon=class{activate(e){this._terminal=e}dispose(){}fit(){const e=this.proposeDimensions();if(!e||!this._terminal||isNaN(e.cols)||isNaN(e.rows))return;const t=this._terminal._core;this._terminal.rows===e.rows&&this._terminal.cols===e.cols||(t._renderService.clear(),this._terminal.resize(e.cols,e.rows))}proposeDimensions(){if(!this._terminal)return;if(!this._terminal.element||!this._terminal.element.parentElement)return;const e=this._terminal._core,t=e._renderService.dimensions;if(0===t.css.cell.width||0===t.css.cell.height)return;const r=0===this._terminal.options.scrollback?0:e.viewport.scrollBarWidth,i=window.getComputedStyle(this._terminal.element.parentElement),o=parseInt(i.getPropertyValue("height")),s=Math.max(0,parseInt(i.getPropertyValue("width"))),n=window.getComputedStyle(this._terminal.element),l=o-(parseInt(n.getPropertyValue("padding-top"))+parseInt(n.getPropertyValue("padding-bottom"))),a=s-(parseInt(n.getPropertyValue("padding-right"))+parseInt(n.getPropertyValue("padding-left")))-r;return{cols:Math.max(2,Math.floor(a/t.css.cell.width)),rows:Math.max(1,Math.floor(l/t.css.cell.height))}}}})(),e})()));
//# sourceMappingURL=addon-fit.js.map
+2
View File
@@ -0,0 +1,2 @@
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.WebLinksAddon=t():e.WebLinksAddon=t()}(self,(()=>(()=>{"use strict";var e={6:(e,t)=>{function n(e){try{const t=new URL(e),n=t.password&&t.username?`${t.protocol}//${t.username}:${t.password}@${t.host}`:t.username?`${t.protocol}//${t.username}@${t.host}`:`${t.protocol}//${t.host}`;return e.toLocaleLowerCase().startsWith(n.toLocaleLowerCase())}catch(e){return!1}}Object.defineProperty(t,"__esModule",{value:!0}),t.LinkComputer=t.WebLinkProvider=void 0,t.WebLinkProvider=class{constructor(e,t,n,o={}){this._terminal=e,this._regex=t,this._handler=n,this._options=o}provideLinks(e,t){const n=o.computeLink(e,this._regex,this._terminal,this._handler);t(this._addCallbacks(n))}_addCallbacks(e){return e.map((e=>(e.leave=this._options.leave,e.hover=(t,n)=>{if(this._options.hover){const{range:o}=e;this._options.hover(t,n,o)}},e)))}};class o{static computeLink(e,t,r,i){const s=new RegExp(t.source,(t.flags||"")+"g"),[a,c]=o._getWindowedLineStrings(e-1,r),l=a.join("");let d;const p=[];for(;d=s.exec(l);){const e=d[0];if(!n(e))continue;const[t,s]=o._mapStrIdx(r,c,0,d.index),[a,l]=o._mapStrIdx(r,t,s,e.length);if(-1===t||-1===s||-1===a||-1===l)continue;const h={start:{x:s+1,y:t+1},end:{x:l,y:a+1}};p.push({range:h,text:e,activate:i})}return p}static _getWindowedLineStrings(e,t){let n,o=e,r=e,i=0,s="";const a=[];if(n=t.buffer.active.getLine(e)){const e=n.translateToString(!0);if(n.isWrapped&&" "!==e[0]){for(i=0;(n=t.buffer.active.getLine(--o))&&i<2048&&(s=n.translateToString(!0),i+=s.length,a.push(s),n.isWrapped&&-1===s.indexOf(" ")););a.reverse()}for(a.push(e),i=0;(n=t.buffer.active.getLine(++r))&&n.isWrapped&&i<2048&&(s=n.translateToString(!0),i+=s.length,a.push(s),-1===s.indexOf(" ")););}return[a,o]}static _mapStrIdx(e,t,n,o){const r=e.buffer.active,i=r.getNullCell();let s=n;for(;o;){const e=r.getLine(t);if(!e)return[-1,-1];for(let n=s;n<e.length;++n){e.getCell(n,i);const s=i.getChars();if(i.getWidth()&&(o-=s.length||1,n===e.length-1&&""===s)){const e=r.getLine(t+1);e&&e.isWrapped&&(e.getCell(0,i),2===i.getWidth()&&(o+=1))}if(o<0)return[t,n]}t++,s=0}return[t,s]}}t.LinkComputer=o}},t={};function n(o){var r=t[o];if(void 0!==r)return r.exports;var i=t[o]={exports:{}};return e[o](i,i.exports,n),i.exports}var o={};return(()=>{var e=o;Object.defineProperty(e,"__esModule",{value:!0}),e.WebLinksAddon=void 0;const t=n(6),r=/(https?|HTTPS?):[/]{2}[^\s"'!*(){}|\\\^<>`]*[^\s"':,.!?{}|\\\^~\[\]`()<>]/;function i(e,t){const n=window.open();if(n){try{n.opener=null}catch{}n.location.href=t}else console.warn("Opening link blocked as opener could not be cleared")}e.WebLinksAddon=class{constructor(e=i,t={}){this._handler=e,this._options=t}activate(e){this._terminal=e;const n=this._options,o=n.urlRegex||r;this._linkProvider=this._terminal.registerLinkProvider(new t.WebLinkProvider(this._terminal,o,this._handler,n))}dispose(){this._linkProvider?.dispose()}}})(),o})()));
//# sourceMappingURL=addon-web-links.js.map
+218
View File
@@ -0,0 +1,218 @@
/**
* Copyright (c) 2014 The xterm.js authors. All rights reserved.
* Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
* https://github.com/chjj/term.js
* @license MIT
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* Originally forked from (with the author's permission):
* Fabrice Bellard's javascript vt100 for jslinux:
* http://bellard.org/jslinux/
* Copyright (c) 2011 Fabrice Bellard
* The original design remains. The terminal itself
* has been extended to include xterm CSI codes, among
* other features.
*/
/**
* Default styles for xterm.js
*/
.xterm {
cursor: text;
position: relative;
user-select: none;
-ms-user-select: none;
-webkit-user-select: none;
}
.xterm.focus,
.xterm:focus {
outline: none;
}
.xterm .xterm-helpers {
position: absolute;
top: 0;
/**
* The z-index of the helpers must be higher than the canvases in order for
* IMEs to appear on top.
*/
z-index: 5;
}
.xterm .xterm-helper-textarea {
padding: 0;
border: 0;
margin: 0;
/* Move textarea out of the screen to the far left, so that the cursor is not visible */
position: absolute;
opacity: 0;
left: -9999em;
top: 0;
width: 0;
height: 0;
z-index: -5;
/** Prevent wrapping so the IME appears against the textarea at the correct position */
white-space: nowrap;
overflow: hidden;
resize: none;
}
.xterm .composition-view {
/* TODO: Composition position got messed up somewhere */
background: #000;
color: #FFF;
display: none;
position: absolute;
white-space: nowrap;
z-index: 1;
}
.xterm .composition-view.active {
display: block;
}
.xterm .xterm-viewport {
/* On OS X this is required in order for the scroll bar to appear fully opaque */
background-color: #000;
overflow-y: scroll;
cursor: default;
position: absolute;
right: 0;
left: 0;
top: 0;
bottom: 0;
}
.xterm .xterm-screen {
position: relative;
}
.xterm .xterm-screen canvas {
position: absolute;
left: 0;
top: 0;
}
.xterm .xterm-scroll-area {
visibility: hidden;
}
.xterm-char-measure-element {
display: inline-block;
visibility: hidden;
position: absolute;
top: 0;
left: -9999em;
line-height: normal;
}
.xterm.enable-mouse-events {
/* When mouse events are enabled (eg. tmux), revert to the standard pointer cursor */
cursor: default;
}
.xterm.xterm-cursor-pointer,
.xterm .xterm-cursor-pointer {
cursor: pointer;
}
.xterm.column-select.focus {
/* Column selection mode */
cursor: crosshair;
}
.xterm .xterm-accessibility:not(.debug),
.xterm .xterm-message {
position: absolute;
left: 0;
top: 0;
bottom: 0;
right: 0;
z-index: 10;
color: transparent;
pointer-events: none;
}
.xterm .xterm-accessibility-tree:not(.debug) *::selection {
color: transparent;
}
.xterm .xterm-accessibility-tree {
user-select: text;
white-space: pre;
}
.xterm .live-region {
position: absolute;
left: -9999px;
width: 1px;
height: 1px;
overflow: hidden;
}
.xterm-dim {
/* Dim should not apply to background, so the opacity of the foreground color is applied
* explicitly in the generated class and reset to 1 here */
opacity: 1 !important;
}
.xterm-underline-1 { text-decoration: underline; }
.xterm-underline-2 { text-decoration: double underline; }
.xterm-underline-3 { text-decoration: wavy underline; }
.xterm-underline-4 { text-decoration: dotted underline; }
.xterm-underline-5 { text-decoration: dashed underline; }
.xterm-overline {
text-decoration: overline;
}
.xterm-overline.xterm-underline-1 { text-decoration: overline underline; }
.xterm-overline.xterm-underline-2 { text-decoration: overline double underline; }
.xterm-overline.xterm-underline-3 { text-decoration: overline wavy underline; }
.xterm-overline.xterm-underline-4 { text-decoration: overline dotted underline; }
.xterm-overline.xterm-underline-5 { text-decoration: overline dashed underline; }
.xterm-strikethrough {
text-decoration: line-through;
}
.xterm-screen .xterm-decoration-container .xterm-decoration {
z-index: 6;
position: absolute;
}
.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer {
z-index: 7;
}
.xterm-decoration-overview-ruler {
z-index: 8;
position: absolute;
top: 0;
right: 0;
pointer-events: none;
}
.xterm-decoration-top {
z-index: 2;
position: relative;
}
+2
View File
File diff suppressed because one or more lines are too long