Skill: rockbenben/md-web (version 1.0.6)

--- name: md-web version: 1.0.6 description: Turn any content into a web page with a shareable URL. Use when the user wants to preview, generate, share, export, or publish content as a web page. tags: [markdown, web, docsify, s3, preview, share, publish, export] homepage: https://github.com/rockbenben/md-web metadata: clawdbot: emoji: "🌐" requires: bins: ["node"] files: - upload.js - "docsify-server/**" --- # MD Web - Markdown to Web Page Upload raw `.md` files to an S3-compatible storage bucket, where a pre-deployed Docsify server automatically renders them as web pages. This avoids sending long text in the conversation. ## When to use this skill **Uploaded content is publicly accessible.** Only use this skill when the user explicitly requests it: - User wants to **show or preview content as a web page** in a browser - User wants to **generate, export, or publish** content as a web page - User wants a **shareable link** to content - User wants output **delivered as a web page** rather than as long text in chat - User wants to **compile or organize content and present it as a web page** - User invokes this skill by name (e.g., `/md-web`) ## How to use this skill ### Step 1: Check configuration Check if `~/.md-web/config.json` exists (cross-platform: use the user's home directory). If it does NOT exist or has empty fields, follow the **Configuration** section below first. ### Step 2: Prepare the markdown file Either use an existing `.md` file, or write the content to a temporary file. Choose the temp path based on the current platform (e.g., `/tmp/` on Linux/macOS, system temp dir on Windows). Use whichever path works in the current shell environment. ### Step 3: Upload via upload.js ```bash node {SKILL_DIR}/upload.js <local-file> <remote-key> ``` - `{SKILL_DIR}`: the base directory of this skill (shown at the top when skill is loaded) - `<remote-key>`: a descriptive lowercase name with hyphens (e.g., `api-docs.md`, `project-guide.md`). A timestamp is prepended automatically to avoid filename collisions. - On first run, the script auto-detects and deploys Docsify server files. No manual setup needed. ### Step 4: Return the result - **On success**: the script prints the URL. Reply with only the filename and clickable link. Do NOT paste the markdown content into the chat. - **On failure** (non-zero exit code): report the error to the user, then fall back to sending the markdown content as text directly in the chat. Example success output: > `api-docs` - https://example.r2.dev/index.html#/20260305-091500-api-docs ## Configuration This only needs to happen once. On subsequent runs, `config.json` already exists. 1. Tell the user this skill needs an S3-compatible storage bucket with public access. Point them to `{SKILL_DIR}/README.md` for detailed setup instructions (Cloudflare R2 / AWS S3 / other S3-compatible services). 2. Ask the user to provide these 5 required fields: - **access_key**: API access key ID - **secret_key**: API secret access key - **endpoint**: S3 endpoint hostname, without `https://` (e.g., `ACCOUNT_ID.r2.cloudflarestorage.com`) - **bucket**: bucket name - **public_url**: public access URL. If the user has a custom domain bound to the bucket, use that (e.g., `https://docs.example.com`); otherwise use the default R2.dev URL (e.g., `https://pub-XXXX.r2.dev`). **Recommend custom domain** for production use — R2.dev URLs have rate limits. 3. Ask about optional settings: - **region**: S3 region. Use `auto` for Cloudflare R2, or the actual region for AWS S3 (e.g., `us-east-1`). Default is `auto`. - **expire_days**: how many days before uploaded markdown files are automatically deleted from the bucket. Default is `30`. Set to `0` to keep files forever. The script sets an S3 lifecycle rule automatically — only timestamped uploads are affected; Docsify server files are never expired. **Note**: this requires the API token to have **Admin Read & Write** permission (not just Object Read & Write). If the token lacks permission, the script will warn but still upload normally — the user can set the lifecycle rule manually in the Cloudflare Dashboard instead. 4. Write the config to `~/.md-web/config.json` (create the `~/.md-web/` directory if it doesn't exist). Use the user's home directory (`$HOME` on Unix, `%USERPROFILE%` on Windows): ```json { "access_key": "...", "secret_key": "...", "endpoint": "...", "bucket": "...", "region": "auto", "public_url": "...", "expire_days": 30 } ``` 5. Then proceed with the upload. ## Important notes - Do NOT generate HTML. Just upload the raw `.md` file — Docsify handles rendering. - Do NOT send markdown content to the chat unless upload fails. - `upload.js` uses only Node.js built-in modules (zero dependencies). - All Docsify assets (JS/CSS) are bundled locally — no external CDN dependency at runtime. ## External endpoints This skill connects only to the S3 endpoint configured by the user in `config.json`. No data is sent to the skill author or any third-party service. | Endpoint | Purpose | Data sent | |----------|---------|-----------| | User's S3 endpoint (`config.json → endpoint`) | Upload .md files and Docsify server assets | File content, S3 auth headers | ## Security & privacy - All uploaded content is **publicly accessible** via the generated URL. - Credentials (`access_key`, `secret_key`) are stored locally in `~/.md-web/config.json` (outside the skill directory, safe from upgrades) and only sent to the user's own S3 endpoint for authentication. - No telemetry, analytics, or data collection by the skill itself. - `upload.js` uses only Node.js built-in modules — no third-party dependencies. By using this skill, markdown content is uploaded to **your own** S3-compatible storage bucket and made publicly accessible. No data is sent to the skill author or any third-party service. Only install if you trust the storage provider you configure.

path: README.md
content: # MD Web

Turn any content into a web page with a shareable URL. Uploads Markdown to an S3-compatible storage bucket, rendered by Docsify.

[中文文档](README.zh.md)

## Quick Start

1. Install the skill: `clawhub install md-web`
2. On first use, the AI will guide you through bucket configuration
3. After that, tell the AI "share as a link" whenever you want to publish content as a web page

## Bucket Setup

This skill requires an **S3-compatible object storage bucket** with public access enabled. The example below uses Cloudflare R2.

### Step 1: Create a Bucket

1. Log in to the [Cloudflare Dashboard](https://dash.cloudflare.com/)
2. Select **R2 Object Storage** from the left menu
3. Click **Create bucket**, enter a name (e.g., `md-web`), use the default region
4. After creation, go to the bucket's **Settings** page
5. Find **Public Access**, click **Allow Access**, enable the **R2.dev subdomain**
6. Once enabled, a public URL will appear: `https://pub-XXXX.r2.dev`
   → This is the **public_url** in the config

### Step 2: Create an API Token

1. On the R2 page, click **Manage R2 API Tokens**
2. Click **Create API Token**
3. Set permissions to **Object Read & Write** (or **Admin Read & Write** if you want automatic file expiry — see `expire_days` below)
4. Optionally restrict scope to the specific bucket
5. After creation, you'll see:
   - **Access Key ID** → **access_key** in the config
   - **Secret Access Key** → **secret_key** in the config
   - **S3 Endpoint** in the format `https://ACCOUNT_ID.r2.cloudflarestorage.com`
     → Remove `https://`, the rest is the **endpoint**

### Step 3: Tell the AI

On first use, the AI will ask for these fields:

| Field       | Required | Description                                                               | Example                               |
| ----------- | -------- | ------------------------------------------------------------------------- | ------------------------------------- |
| access_key  | Yes      | API access key ID                                                         | `a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4`    |
| secret_key  | Yes      | API secret access key                                                     | `a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6...` |
| endpoint    | Yes      | S3 endpoint (without `https://`)                                          | `ACCOUNT_ID.r2.cloudflarestorage.com` |
| bucket      | Yes      | Bucket name                                                               | `md-web`                              |
| public_url  | Yes      | Public access URL (custom domain recommended)                             | `https://pub-XXXX.r2.dev`             |
| region      | No       | S3 region (`auto` for R2, actual region for AWS S3, e.g., `us-east-1`)   | `auto`                                |
| expire_days | No       | Days before uploaded files auto-delete (default `30`, `0` = keep forever) | `30`                                  |

> **Tip**: R2.dev URLs have rate limits. For production use, bind a custom domain to your bucket and use that as `public_url`.

The AI will automatically save these to `~/.md-web/config.json`. No repeat setup needed.

## Usage

Uploaded content is **publicly accessible** via the generated URL. The skill only runs when you explicitly ask for it:

```text
show as a web page
publish this online
/md-web path/to/file.md
share as a link
```

The AI returns a link — click to view the rendered document in your browser.

> **Note**: Markdown image references (e.g., `![](image.png)`) are not uploaded — only the `.md` file itself. Use absolute image URLs if your document includes images.

## File Structure

```yaml
md-web/                     # Skill directory (managed by ClawHub, may be replaced on upgrade)
├── SKILL.md              # AI instruction file
├── upload.js             # Upload script (pure Node.js, zero dependencies)
├── README.md             # This document
├── README.zh.md          # Chinese documentation
└── docsify-server/       # Docsify server files (auto-deployed on first upload)
    ├── index.html
    ├── README.md
    ├── .nojekyll
    └── assets/           # JS/CSS assets (bundled locally, no CDN dependency)
        ├── docsify.min.js
        ├── vue.css
        └── ...

~/.md-web/                  # User data directory (preserved across upgrades)
├── config.json           # Bucket credentials & settings (created on first use)
└── .deployed             # Deploy fingerprint (tracks server deployment state)
```

## Other S3-Compatible Services

Besides Cloudflare R2, this skill works with any S3-compatible object storage:

- **AWS S3**: endpoint is `s3.REGION.amazonaws.com`
- **Backblaze B2**: endpoint is `s3.REGION.backblazeb2.com`
- **DigitalOcean Spaces**: endpoint is `REGION.digitaloceanspaces.com`
- **Wasabi**: endpoint is `s3.REGION.wasabisys.com`
- **Tencent Cloud COS**: endpoint is `cos.REGION.myqcloud.com`

Any service that provides an S3-compatible API and a public access URL will work.


path: README.zh.md
content: # MD Web

[English](README.md)

将任意内容转为网页并生成可分享的链接。上传 Markdown 到 S3 兼容存储桶，由 Docsify 自动渲染。

## 快速开始

1. 安装 skill：`clawhub install md-web`
2. 首次使用时，AI 会引导你填写存储桶配置
3. 之后告诉 AI「用网页展示」即可将内容分享为链接

## 存储桶配置

本 skill 需要一个 **S3 兼容的对象存储桶**并开启公共访问。以下以 Cloudflare R2 为例。

### 第一步：创建存储桶

1. 登录 [Cloudflare Dashboard](https://dash.cloudflare.com/)
2. 左侧菜单选择 **R2 对象存储**
3. 点击 **创建存储桶**，填写名称（如 `md-web`），区域选默认即可
4. 创建完成后进入桶的 **设置** 页面
5. 找到 **公开访问**，点击 **允许访问**，勾选 **R2.dev 子域**
6. 开启后会显示公共 URL，格式为 `https://pub-XXXX.r2.dev`
   → 这就是配置中的 **public_url**

### 第二步：创建 API 令牌

1. 在 R2 页面点击 **管理 R2 API 令牌**
2. 点击 **创建 API 令牌**
3. 权限选择 **对象读和写**（Object Read & Write）。如需自动过期功能（见下方 `expire_days`），需选择 **管理员读和写**（Admin Read & Write）
4. 范围可选 **仅限指定桶** → 选择刚才创建的桶
5. 创建后会显示：
   - **Access Key ID** → 配置中的 **access_key**
   - **Secret Access Key** → 配置中的 **secret_key**
   - **S3 端点** 格式为 `https://ACCOUNT_ID.r2.cloudflarestorage.com`
     → 去掉 `https://`，剩余部分就是 **endpoint**

### 第三步：告诉 AI

首次使用 skill 时，AI 会询问以下信息：

| 字段        | 必填 | 说明                                                     | 示例                                  |
| ----------- | ---- | -------------------------------------------------------- | ------------------------------------- |
| access_key  | 是   | API 访问密钥 ID                                          | `a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4`    |
| secret_key  | 是   | API 秘密访问密钥                                         | `a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6...` |
| endpoint    | 是   | S3 端点（不含 https://）                                 | `ACCOUNT_ID.r2.cloudflarestorage.com` |
| bucket      | 是   | 存储桶名称                                               | `md-web`                              |
| public_url  | 是   | 公共访问 URL（推荐自定义域名）                           | `https://pub-XXXX.r2.dev`             |
| region      | 否   | S3 区域（R2 用 `auto`，AWS S3 填实际区域如 `us-east-1`） | `auto`                                |
| expire_days | 否   | 上传文件自动删除天数（默认 `30`，`0` = 永久保留）        | `30`                                  |

> **提示**：R2.dev 子域有速率限制，生产环境建议为桶绑定自定义域名作为 `public_url`。

AI 会自动将配置写入 `~/.md-web/config.json`，之后不再需要重复配置。

## 使用方式

上传的内容通过生成的 URL **公开可访问**。仅在你明确要求时触发：

```text
做成网页
预览 README.md
/md-web path/to/file.md
分享为链接
```

AI 会返回一个链接，点击即可在浏览器中阅读渲染后的文档。

> **注意**：Markdown 中的图片引用（如 `![](image.png)`）不会被上传，仅上传 `.md` 文件本身。如文档包含图片，请使用绝对 URL。

## 文件结构

```yaml
md-web/                     # Skill 目录（由 ClawHub 管理，升级时可能被替换）
├── SKILL.md              # AI 指令文件
├── upload.js             # 上传脚本（纯 Node.js，零依赖）
├── README.md             # 英文文档
├── README.zh.md          # 本文档
└── docsify-server/       # Docsify 服务文件（首次上传时自动部署）
    ├── index.html
    ├── README.md
    ├── .nojekyll
    └── assets/           # JS/CSS 资源（本地打包，无 CDN 依赖）
        ├── docsify.min.js
        ├── vue.css
        └── ...

~/.md-web/                  # 用户数据目录（升级时保留）
├── config.json           # 存储桶凭证与配置（首次使用时创建）
└── .deployed             # 部署指纹（跟踪服务端部署状态）
```

## 其他 S3 兼容服务

除 Cloudflare R2 外，本 skill 也支持任何 S3 兼容的对象存储：

- **AWS S3**：endpoint 为 `s3.REGION.amazonaws.com`
- **Backblaze B2**：endpoint 为 `s3.REGION.backblazeb2.com`
- **DigitalOcean Spaces**：endpoint 为 `REGION.digitaloceanspaces.com`
- **Wasabi**：endpoint 为 `s3.REGION.wasabisys.com`
- **腾讯云 COS**：endpoint 为 `cos.REGION.myqcloud.com`

只要能提供 S3 兼容 API 和公共访问 URL，都可以使用。


path: docsify-server/README.md
content: # MD Web

Instant Markdown sharing — paste a link, read a beautifully rendered page.

即时 Markdown 分享 —— 打开链接，阅读排版精美的页面。

## Looking for a document? / 找文档？

If you arrived here without a specific document, the link you need should look like:

如果你看到的是这个页面而非具体文档，你需要的链接格式应该是：

```
https://your-domain/index.html#/document-name
```

Check the link you received and make sure the `#/document-name` part is included.

请检查你收到的链接，确保包含 `#/文档名` 部分。

## Features / 功能

- **Full-text search / 全文搜索** — search within the current document / 搜索当前文档内容
- **Table of contents / 目录导航** — auto-generated from headings / 根据标题自动生成
- **Image zoom / 图片缩放** — click any image to view full size / 点击图片查看原图
- **Word count / 字数统计** — reading time estimate / 自动估算阅读时间


path: docsify-server/assets/countable.min.js
content: var defaultOptions={countable:!0,position:"top",margin:"10px",float:"right",fontsize:"0.9em",color:"rgb(90,90,90)",language:"english",isExpected:!0};function plugin(t,n){if(!defaultOptions.countable)return;let o;t.beforeEach(function(t){return o=t.match(/([\u4e00-\u9fa5]+?|[a-zA-Z0-9]+)/g).length,t}),t.afterEach(function(t,n){let i=o+" words",e=Math.ceil(o/400)+" min";"chinese"===defaultOptions.language&&(i=o+" 字",e=Math.ceil(o/400)+" 分钟"),n(`\n        ${"bottom"===defaultOptions.position?t:""}\n        <div style="margin-${defaultOptions.position?"bottom":"top"}: ${defaultOptions.margin};">\n            <span style="\n                  float: ${"right"===defaultOptions.float?"right":"left"};\n                  font-size: ${defaultOptions.fontsize};\n                  color:${defaultOptions.color};">\n            ${i}\n            ${defaultOptions.isExpected?`&nbsp; | &nbsp;${e}`:""}\n            </span>\n            <div style="clear: both"></div>\n        </div>\n        ${"bottom"!==defaultOptions.position?t:""}\n        `)})}window.$docsify.count=Object.assign(defaultOptions,window.$docsify.count),window.$docsify.plugins=[].concat(plugin,window.$docsify.plugins);

path: docsify-server/assets/search.min.js
content: !function(){function u(e){return e.replace(/<!-- {docsify-ignore} -->/,"").replace(/{docsify-ignore}/,"").replace(/<!-- {docsify-ignore-all} -->/,"").replace(/{docsify-ignore-all}/,"").trim()}var f={},m={EXPIRE_KEY:"docsify.search.expires",INDEX_KEY:"docsify.search.index"};function g(e){var n={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"};return String(e).replace(/[&<>"']/g,function(e){return n[e]})}function y(e){return e.text||"table"!==e.type||(e.cells.unshift(e.header),e.text=e.cells.map(function(e){return e.join(" | ")}).join(" |\n ")),e.text}function v(e){return e.text||"list"!==e.type||(e.text=e.raw),e.text}function b(o,e,s,c){void 0===e&&(e="");var d,e=window.marked.lexer(e),l=window.Docsify.slugify,p={},h="";return e.forEach(function(e,n){var t,a,i,r;"heading"===e.type&&e.depth<=c?(t=(a=(i=e.text,r={},{str:i=(i=void 0===i?"":i)&&i.replace(/^('|")/,"").replace(/('|")$/,"").replace(/(?:^|\s):([\w-]+:?)=?([\w-%]+)?/g,function(e,n,t){return-1===n.indexOf(":")?(r[n]=t&&t.replace(/&quot;/g,"")||!0,""):e}).trim(),config:r})).str,i=a.config,a=u(e.text),d=i.id?s.toURL(o,{id:l(i.id)}):s.toURL(o,{id:l(g(a))}),t&&(h=u(t)),p[d]={slug:d,title:h,body:""}):(0===n&&(d=s.toURL(o),p[d]={slug:d,title:"/"!==o?o.slice(1):"Home Page",body:e.text||""}),d&&(p[d]?p[d].body?(e.text=y(e),e.text=v(e),p[d].body+="\n"+(e.text||"")):(e.text=y(e),e.text=v(e),p[d].body=e.text||""):p[d]={slug:d,title:"",body:""}))}),l.clear(),p}function p(e){return e&&e.normalize?e.normalize("NFD").replace(/[\u0300-\u036f]/g,""):e}function o(e){var n=[],t=[];Object.keys(f).forEach(function(n){t=t.concat(Object.keys(f[n]).map(function(e){return f[n][e]}))});var a=(e=e.trim()).split(/[\s\-，\\/]+/);1!==a.length&&(a=[].concat(e,a));for(var i=0;i<t.length;i++)!function(e){var e=t[e],r=0,o="",s="",c="",d=e.title&&e.title.trim(),l=e.body&&e.body.trim(),e=e.slug||"";d&&(a.forEach(function(e){var n,t,a=new RegExp(g(p(e)).replace(/[|\\{}()[\]^$+*?.]/g,"\\$&"),"gi"),i=-1;s=d&&g(p(d)),c=l&&g(p(l)),t=d?s.search(a):-1,i=l?c.search(a):-1,(0<=t||0<=i)&&(r+=0<=t?3:0<=i?2:0,t=(t=n=0)==(n=(i=i<0?0:i)<11?0:i-10)?70:i+e.length+60,l&&t>l.length&&(t=l.length),a=c&&"..."+c.substring(n,t).replace(a,function(e){return'<em class="search-keyword">'+e+"</em>"})+"...",o+=a)}),0<r&&(e={title:s,content:l?o:"",url:e,score:r},n.push(e)))}(i);return n.sort(function(e,n){return n.score-e.score})}function r(a,i){var t,r,n,e,o="auto"===a.paths,s=o?(t=i.router,r=[],Docsify.dom.findAll(".sidebar-nav a:not(.section-link):not([data-nosearch])").forEach(function(e){var n=e.href,e=e.getAttribute("href"),n=t.parse(n).path;n&&-1===r.indexOf(n)&&!Docsify.util.isAbsolutePath(e)&&r.push(n)}),r):a.paths,c="";s.length&&o&&a.pathNamespaces?(n=s[0],Array.isArray(a.pathNamespaces)?c=a.pathNamespaces.filter(function(e){return n.slice(0,e.length)===e})[0]||c:a.pathNamespaces instanceof RegExp&&((d=n.match(a.pathNamespaces))&&(c=d[0])),e=-1===s.indexOf(c+"/"),d=-1===s.indexOf(c+"/README"),e&&d&&s.unshift(c+"/")):-1===s.indexOf("/")&&-1===s.indexOf("/README")&&s.unshift("/");var d,l=((d=a.namespace)?m.EXPIRE_KEY+"/"+d:m.EXPIRE_KEY)+c,p=((d=a.namespace)?m.INDEX_KEY+"/"+d:m.INDEX_KEY)+c,c=localStorage.getItem(l)<Date.now();if(f=JSON.parse(localStorage.getItem(p)),c)f={};else if(!o)return;var h=s.length,u=0;s.forEach(function(t){return f[t]?u++:void Docsify.get(i.router.getFile(t),!1,i.config.requestHeaders).then(function(e){var n;f[t]=b(t,e,i.router,a.depth),h===++u&&(n=a.maxAge,e=p,localStorage.setItem(l,Date.now()+n),localStorage.setItem(e,JSON.stringify(f)))})})}var s,c="";function d(e){var n=Docsify.dom.find("div.search"),t=Docsify.dom.find(n,".results-panel"),a=Docsify.dom.find(n,".clear-button"),i=Docsify.dom.find(".sidebar-nav"),n=Docsify.dom.find(".app-name");if(!e)return t.classList.remove("show"),a.classList.remove("show"),t.innerHTML="",void(s.hideOtherSidebarContent&&(i&&i.classList.remove("hide"),n&&n.classList.remove("hide")));var e=o(e),r="";e.forEach(function(e){r+='<div class="matching-post">\n<a href="'+e.url+'">\n<h2>'+e.title+"</h2>\n<p>"+e.content+"</p>\n</a>\n</div>"}),t.classList.add("show"),a.classList.add("show"),t.innerHTML=r||'<p class="empty">'+c+"</p>",s.hideOtherSidebarContent&&(i&&i.classList.add("hide"),n&&n.classList.add("hide"))}function l(e){s=e}function h(e,n){var t,a,i=n.router.parse().query.s;l(e),Docsify.dom.style("\n.sidebar {\n  padding-top: 0;\n}\n\n.search {\n  margin-bottom: 20px;\n  padding: 6px;\n  border-bottom: 1px solid #eee;\n}\n\n.search .input-wrap {\n  display: flex;\n  align-items: center;\n}\n\n.search .results-panel {\n  display: none;\n}\n\n.search .results-panel.show {\n  display: block;\n}\n\n.search input {\n  outline: none;\n  border: none;\n  width: 100%;\n  padding: 0.6em 7px;\n  font-size: inherit;\n  border: 1px solid transparent;\n}\n\n.search input:focus {\n  box-shadow: 0 0 5px var(--theme-color, #42b983);\n  border: 1px solid var(--theme-color, #42b983);\n}\n\n.search input::-webkit-search-decoration,\n.search input::-webkit-search-cancel-button,\n.search input {\n  -webkit-appearance: none;\n  -moz-appearance: none;\n  appearance: none;\n}\n\n.search input::-ms-clear {\n  display: none;\n  height: 0;\n  width: 0;\n}\n\n.search .clear-button {\n  cursor: pointer;\n  width: 36px;\n  text-align: right;\n  display: none;\n}\n\n.search .clear-button.show {\n  display: block;\n}\n\n.search .clear-button svg {\n  transform: scale(.5);\n}\n\n.search h2 {\n  font-size: 17px;\n  margin: 10px 0;\n}\n\n.search a {\n  text-decoration: none;\n  color: inherit;\n}\n\n.search .matching-post {\n  border-bottom: 1px solid #eee;\n}\n\n.search .matching-post:last-child {\n  border-bottom: 0;\n}\n\n.search p {\n  font-size: 14px;\n  overflow: hidden;\n  text-overflow: ellipsis;\n  display: -webkit-box;\n  -webkit-line-clamp: 2;\n  -webkit-box-orient: vertical;\n}\n\n.search p.empty {\n  text-align: center;\n}\n\n.app-name.hide, .sidebar-nav.hide {\n  display: none;\n}"),function(e){void 0===e&&(e="");var n=Docsify.dom.create("div",'<div class="input-wrap">\n      <input type="search" value="'+e+'" aria-label="Search text" />\n      <div class="clear-button">\n        <svg width="26" height="24">\n          <circle cx="12" cy="12" r="11" fill="#ccc" />\n          <path stroke="white" stroke-width="2" d="M8.25,8.25,15.75,15.75" />\n          <path stroke="white" stroke-width="2"d="M8.25,15.75,15.75,8.25" />\n        </svg>\n      </div>\n    </div>\n    <div class="results-panel"></div>\n    </div>'),e=Docsify.dom.find("aside");Docsify.dom.toggleClass(n,"search"),Docsify.dom.before(e,n)}(i),n=Docsify.dom.find("div.search"),a=Docsify.dom.find(n,"input"),e=Docsify.dom.find(n,".input-wrap"),Docsify.dom.on(n,"click",function(e){return-1===["A","H2","P","EM"].indexOf(e.target.tagName)&&e.stopPropagation()}),Docsify.dom.on(a,"input",function(n){clearTimeout(t),t=setTimeout(function(e){return d(n.target.value.trim())},100)}),Docsify.dom.on(e,"click",function(e){"INPUT"!==e.target.tagName&&(a.value="",d())}),i&&setTimeout(function(e){return d(i)},500)}function x(e,n){var t,a,i,r,o;l(e),t=e.placeholder,a=n.route.path,(r=Docsify.dom.getNode('.search input[type="search"]'))&&("string"==typeof t?r.placeholder=t:(i=Object.keys(t).filter(function(e){return-1<a.indexOf(e)})[0],r.placeholder=t[i])),e=e.noData,o=n.route.path,c="string"==typeof e?e:e[Object.keys(e).filter(function(e){return-1<o.indexOf(e)})[0]]}var w={placeholder:"Type to search",noData:"No Results!",paths:"auto",depth:2,maxAge:864e5,hideOtherSidebarContent:!1,namespace:void 0,pathNamespaces:void 0};$docsify.plugins=[].concat(function(e,n){var t=Docsify.util,a=n.config.search||w;Array.isArray(a)?w.paths=a:"object"==typeof a&&(w.paths=Array.isArray(a.paths)?a.paths:"auto",w.maxAge=(t.isPrimitive(a.maxAge)?a:w).maxAge,w.placeholder=a.placeholder||w.placeholder,w.noData=a.noData||w.noData,w.depth=a.depth||w.depth,w.hideOtherSidebarContent=a.hideOtherSidebarContent||w.hideOtherSidebarContent,w.namespace=a.namespace||w.namespace,w.pathNamespaces=a.pathNamespaces||w.pathNamespaces);var i="auto"===w.paths;e.mounted(function(e){h(w,n),i||r(w,n)}),e.doneEach(function(e){x(w,n),i&&r(w,n)})},$docsify.plugins)}();


path: docsify-server/assets/toc.css
content: .content {
  display: flex;
  flex-direction: row-reverse;
  justify-content: center;
}
.markdown-section {
/*   flex: 1 1 0%; */
  margin: 0 48px;
}
.nav {
  width: var(--toc-width, 700px);
/*   align-self: flex-start;
  flex: 0 0 auto; */
}
aside.nav.nothing {
  width: 0;
}

.page_toc {
  position: fixed;
  border-left-style: solid;
  border-left-width: 1px;
  border-left-color: rgba(0, 0, 0, 0.07);
  border-image-slice: 1;
  padding-left: 5px;
}

.page_toc a > * {
  pointer-events: none;
}

.page_toc code {
  background-color: #f8f8f8;
  border-radius: 2px;
  color: #e96900;
  font-family: 'Roboto Mono', Monaco, courier, monospace;
  font-size: 0.8rem;
  margin: 0 2px;
  padding: 3px 5px;
}

.page_toc p.title {
  margin: 0px 0 0px 9px;
  padding-bottom: 5px;
  font-weight: 600;
  font-size: 1.2em;
}
.page_toc .anchor:hover:after {
  content: "";
}

.page_toc ul {
  list-style-type: none;
  margin-top: 0px;
  padding-left: 10px;
  color: var(--text-color-base, black);
  text-decoration: none;
  font-weight: 300;
  line-height: 1.6em;
}

.page_toc ul a:hover span {
  color: var(--text-color-tertiary, #42b983);
  border-bottom: none !important;
  text-decoration:none !important;
}

.page_toc ul a {
  color: var(--text-color-base, black);
  text-decoration: none;
  font-weight: 300;
  line-height: 1.6em;
}

@media screen and (max-width: 1300px) {
  .page_toc {
    position: relative;
    left: 0;
    top: -20px;
    padding: 10px 0;
    border: none;
    border-bottom: 1px solid #ddd;
    font-size: 1.0em;
  }
  .page_toc a:before {
    content: "- ";
  }
  .nav {
    margin: 0 auto;
    width: 800px;
  }
  .page_toc p.title {
    font-weight: 300;
    font-size: 1.8em;
  }
  .content {
    display: block;
  }
  .markdown-section {
    margin: 0 auto;
  }
}

.page_toc .active {
  border-left: 5px solid;
  color: var(--theme-color, #42b983);
  padding-left: 10px;
}


path: docsify-server/assets/toc.js
content: var defaultOptions = {
  headings: 'h1, h2',
  scope: '.markdown-section',

  // To make work
  title: 'Contents',
  listType: 'ul',  
}

// Element builders
var tocHeading = function(Title) {
  return document.createElement('h2').appendChild(
    document.createTextNode(Title)
  )
}

var aTag = function(src) {
  var a = document.createElement('a');
  var content = src.firstChild.innerHTML;

  // Use this to clip text w/ HTML in it.
  // https://github.com/arendjr/text-clipper
  a.innerHTML = content;
  a.href = src.firstChild.href;
  a.onclick = tocClick

  // In order to remove this gotta fix the styles.
  a.setAttribute('class', 'anchor');

  return a
};

var tocClick = function(e) {
  var divs = document.querySelectorAll('.page_toc .active');

  // Remove the previous classes
  [].forEach.call(divs, function(div) {
    div.setAttribute('class', 'anchor')
  });

  // Make sure this is attached to the parent not itself
  e.currentTarget.setAttribute('class', 'active')
};

var createList = function(wrapper, count) {
  while (count--) {
    if(wrapper){
	    wrapper = wrapper.appendChild(
	      document.createElement('ul')
	    );
    }
    if (count) {
      wrapper = wrapper.appendChild(
        document.createElement('li')
      );
    }
  }

  return wrapper;
};

//------------------------------------------------------------------------

var getHeaders = function(selector) {
  var headings2 = document.querySelectorAll(selector);
  var ret = [];

  [].forEach.call(headings2, function(heading) {
    ret = ret.concat(heading);
  });

  return ret;
};

var getLevel = function(header) {
  var decs = header.match(/\d/g);

  return decs ? Math.min.apply(null, decs) : 1;
};

var jumpBack = function(currentWrapper, offset) {
  while (offset--) {
    currentWrapper = currentWrapper.parentElement;
  }

  return currentWrapper;
};

var buildTOC = function(options) {
  var ret = document.createElement('ul');
  var wrapper = ret;
  var lastLi = null;
  var selector = options.scope + ' ' + options.headings
  var headers = getHeaders(selector).filter(h => h.id);

  headers.reduce(function(prev, curr, index) {
    var currentLevel = getLevel(curr.tagName);
    var offset = currentLevel - prev;

    wrapper = (offset > 0)
      ? createList(lastLi, offset)
      : jumpBack(wrapper, -offset * 2)

    wrapper = wrapper || ret;

    var li = document.createElement('li');

    wrapper.appendChild(li).appendChild(aTag(curr));

    lastLi = li;

    return currentLevel;
  }, getLevel(options.headings));

  return ret;
};

// Docsify plugin functions
function plugin(hook, vm) {
  var userOptions = vm.config.toc;

  hook.mounted(function () {
    var content = window.Docsify.dom.find(".content");
    if (content) {
      var nav = window.Docsify.dom.create("aside", "");
      window.Docsify.dom.toggleClass(nav, "add", "nav");
      window.Docsify.dom.before(content, nav);
    }
  });

  hook.doneEach(function () {
    var nav = document.querySelectorAll('.nav')[0]
    var t = Array.from(document.querySelectorAll('.nav'))

    if (!nav) {
      return;
    }

  	const toc = buildTOC(userOptions);

    // Just unset it for now.
    if (!toc.innerHTML) {
      nav.innerHTML = null
      return;
    }

    // Fix me in the future
		var title = document.createElement('p');
		title.innerHTML = userOptions.title;
		title.setAttribute('class', 'title');

		var container = document.createElement('div');
		container.setAttribute('class', 'page_toc');
		
		container.appendChild(title);
		container.appendChild(toc);

    // Existing TOC
    var tocChild = document.querySelectorAll('.nav .page_toc');

    if (tocChild.length > 0) {
      tocChild[0].parentNode.removeChild(tocChild[0]);
    }

    nav.appendChild(container);
  });
}

// Docsify plugin options
window.$docsify['toc'] = Object.assign(defaultOptions, window.$docsify['toc']);
window.$docsify.plugins = [].concat(plugin, window.$docsify.plugins);


path: docsify-server/assets/vue.css
content: *{-webkit-font-smoothing:antialiased;-webkit-overflow-scrolling:touch;-webkit-tap-highlight-color:rgba(0,0,0,0);-webkit-text-size-adjust:none;-webkit-touch-callout:none;box-sizing:border-box}body:not(.ready){overflow:hidden}body:not(.ready) .app-nav,body:not(.ready)>nav,body:not(.ready) [data-cloak]{display:none}div#app{font-size:30px;font-weight:lighter;margin:40vh auto;text-align:center}div#app:empty:before{content:"Loading..."}img.emoji{height:1.2em}img.emoji,span.emoji{vertical-align:middle}span.emoji{font-family:Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-size:1.2em}.progress{background-color:#42b983;background-color:var(--theme-color,#42b983);height:2px;left:0;position:fixed;right:0;top:0;transition:width .2s,opacity .4s;width:0;z-index:999999}.search .search-keyword,.search a:hover{color:#42b983;color:var(--theme-color,#42b983)}.search .search-keyword{font-style:normal;font-weight:700}body,html{height:100%}body{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;color:#34495e;font-family:Source Sans Pro,Helvetica Neue,Arial,sans-serif;font-size:15px;letter-spacing:0;margin:0;overflow-x:hidden}img{max-width:100%}a[disabled]{cursor:not-allowed;opacity:.6}kbd{border:1px solid #ccc;border-radius:3px;display:inline-block;font-size:12px!important;line-height:12px;margin-bottom:3px;padding:3px 5px;vertical-align:middle}li input[type=checkbox]{margin:0 .2em .25em 0;vertical-align:middle}.app-nav{margin:25px 60px 0 0;position:absolute;right:0;text-align:right;z-index:10}.app-nav.no-badge{margin-right:25px}.app-nav p{margin:0}.app-nav>a{margin:0 1rem;padding:5px 0}.app-nav li,.app-nav ul{display:inline-block;list-style:none;margin:0}.app-nav a{color:inherit;font-size:16px;text-decoration:none;transition:color .3s}.app-nav a.active,.app-nav a:hover{color:#42b983;color:var(--theme-color,#42b983)}.app-nav a.active{border-bottom:2px solid #42b983;border-bottom:2px solid var(--theme-color,#42b983)}.app-nav li{display:inline-block;margin:0 1rem;padding:5px 0;position:relative;cursor:pointer}.app-nav li ul{background-color:#fff;border:1px solid;border-color:#ddd #ddd #ccc;border-radius:4px;box-sizing:border-box;display:none;max-height:calc(100vh - 61px);overflow-y:auto;padding:10px 0;position:absolute;right:-15px;text-align:left;top:100%;white-space:nowrap}.app-nav li ul li{display:block;font-size:14px;line-height:1rem;margin:8px 14px;white-space:nowrap}.app-nav li ul a{display:block;font-size:inherit;margin:0;padding:0}.app-nav li ul a.active{border-bottom:0}.app-nav li:hover ul{display:block}.github-corner{border-bottom:0;position:fixed;right:0;text-decoration:none;top:0;z-index:1}.github-corner:hover .octo-arm{animation:octocat-wave .56s ease-in-out}.github-corner svg{color:#fff;fill:#42b983;fill:var(--theme-color,#42b983);height:80px;width:80px}main{display:block;position:relative;width:100vw;height:100%;z-index:0}main.hidden{display:none}.anchor{display:inline-block;text-decoration:none;transition:all .3s}.anchor span{color:#34495e}.anchor:hover{text-decoration:underline}.sidebar{border-right:1px solid rgba(0,0,0,.07);overflow-y:auto;padding:40px 0 0;position:absolute;top:0;bottom:0;left:0;transition:transform .25s ease-out;width:300px;z-index:20}.sidebar>h1{margin:0 auto 1rem;font-size:1.5rem;font-weight:300;text-align:center}.sidebar>h1 a{color:inherit;text-decoration:none}.sidebar>h1 .app-nav{display:block;position:static}.sidebar .sidebar-nav{line-height:2em;padding-bottom:40px}.sidebar li.collapse .app-sub-sidebar{display:none}.sidebar ul{margin:0 0 0 15px;padding:0}.sidebar li>p{font-weight:700;margin:0}.sidebar ul,.sidebar ul li{list-style:none}.sidebar ul li a{border-bottom:none;display:block}.sidebar ul li ul{padding-left:20px}.sidebar::-webkit-scrollbar{width:4px}.sidebar::-webkit-scrollbar-thumb{background:transparent;border-radius:4px}.sidebar:hover::-webkit-scrollbar-thumb{background:hsla(0,0%,53.3%,.4)}.sidebar:hover::-webkit-scrollbar-track{background:hsla(0,0%,53.3%,.1)}.sidebar-toggle{background-color:transparent;background-color:hsla(0,0%,100%,.8);border:0;outline:none;padding:10px;position:absolute;bottom:0;left:0;text-align:center;transition:opacity .3s;width:284px;z-index:30;cursor:pointer}.sidebar-toggle:hover .sidebar-toggle-button{opacity:.4}.sidebar-toggle span{background-color:#42b983;background-color:var(--theme-color,#42b983);display:block;margin-bottom:4px;width:16px;height:2px}body.sticky .sidebar,body.sticky .sidebar-toggle{position:fixed}.content{padding-top:60px;position:absolute;top:0;right:0;bottom:0;left:300px;transition:left .25s ease}.markdown-section{margin:0 auto;max-width:80%;padding:30px 15px 40px;position:relative}.markdown-section>*{box-sizing:border-box;font-size:inherit}.markdown-section>:first-child{margin-top:0!important}.markdown-section hr{border:none;border-bottom:1px solid #eee;margin:2em 0}.markdown-section iframe{border:1px solid #eee;width:1px;min-width:100%}.markdown-section table{border-collapse:collapse;border-spacing:0;display:block;margin-bottom:1rem;overflow:auto;width:100%}.markdown-section th{font-weight:700}.markdown-section td,.markdown-section th{border:1px solid #ddd;padding:6px 13px}.markdown-section tr{border-top:1px solid #ccc}.markdown-section p.tip,.markdown-section tr:nth-child(2n){background-color:#f8f8f8}.markdown-section p.tip{border-bottom-right-radius:2px;border-left:4px solid #f66;border-top-right-radius:2px;margin:2em 0;padding:12px 24px 12px 30px;position:relative}.markdown-section p.tip:before{background-color:#f66;border-radius:100%;color:#fff;content:"!";font-family:Dosis,Source Sans Pro,Helvetica Neue,Arial,sans-serif;font-size:14px;font-weight:700;left:-12px;line-height:20px;position:absolute;height:20px;width:20px;text-align:center;top:14px}.markdown-section p.tip code{background-color:#efefef}.markdown-section p.tip em{color:#34495e}.markdown-section p.warn{background:rgba(66,185,131,.1);border-radius:2px;padding:1rem}.markdown-section ul.task-list>li{list-style-type:none}body.close .sidebar{transform:translateX(-300px)}body.close .sidebar-toggle{width:auto}body.close .content{left:0}@media print{.app-nav,.github-corner,.sidebar,.sidebar-toggle{display:none}}@media screen and (max-width:768px){.github-corner,.sidebar,.sidebar-toggle{position:fixed}.app-nav{margin-top:16px}.app-nav li ul{top:30px}main{height:auto;min-height:100vh;overflow-x:hidden}.sidebar{left:-300px;transition:transform .25s ease-out}.content{left:0;max-width:100vw;position:static;padding-top:20px;transition:transform .25s ease}.app-nav,.github-corner{transition:transform .25s ease-out}.sidebar-toggle{background-color:transparent;width:auto;padding:30px 30px 10px 10px}body.close .sidebar{transform:translateX(300px)}body.close .sidebar-toggle{background-color:hsla(0,0%,100%,.8);transition:background-color 1s;width:284px;padding:10px}body.close .content{transform:translateX(300px)}body.close .app-nav,body.close .github-corner{display:none}.github-corner:hover .octo-arm{animation:none}.github-corner .octo-arm{animation:octocat-wave .56s ease-in-out}}@keyframes octocat-wave{0%,to{transform:rotate(0)}20%,60%{transform:rotate(-25deg)}40%,80%{transform:rotate(10deg)}}section.cover{position:relative;align-items:center;background-position:50%;background-repeat:no-repeat;background-size:cover;min-height:100vh;width:100%;display:none}section.cover.show{display:flex}section.cover.has-mask .mask{background-color:#fff;opacity:.8;position:absolute;top:0;bottom:0;width:100%}section.cover .cover-main{flex:1;margin:0 16px;text-align:center;position:relative}section.cover a{color:inherit}section.cover a,section.cover a:hover{text-decoration:none}section.cover p{line-height:1.5rem;margin:1em 0}section.cover h1{color:inherit;font-size:2.5rem;font-weight:300;margin:.625rem 0 2.5rem;position:relative;text-align:center}section.cover h1 a{display:block}section.cover h1 small{bottom:-.4375rem;font-size:1rem;position:absolute}section.cover blockquote{font-size:1.5rem;text-align:center}section.cover ul{line-height:1.8;list-style-type:none;margin:1em auto;max-width:500px;padding:0}section.cover .cover-main>p:last-child a{border-radius:2rem;border:1px solid #42b983;border-color:var(--theme-color,#42b983);box-sizing:border-box;color:#42b983;color:var(--theme-color,#42b983);display:inline-block;font-size:1.05rem;letter-spacing:.1rem;margin:.5rem 1rem;padding:.75em 2rem;text-decoration:none;transition:all .15s ease}section.cover .cover-main>p:last-child a:last-child{background-color:#42b983;background-color:var(--theme-color,#42b983);color:#fff}section.cover .cover-main>p:last-child a:last-child:hover{color:inherit;opacity:.8}section.cover .cover-main>p:last-child a:hover{color:inherit}section.cover blockquote>p>a{border-bottom:2px solid #42b983;border-bottom:2px solid var(--theme-color,#42b983);transition:color .3s}section.cover blockquote>p>a:hover{color:#42b983;color:var(--theme-color,#42b983)}.sidebar,body{background-color:#fff}.sidebar{color:#364149}.sidebar li{margin:6px 0}.sidebar ul li a{color:#505d6b;font-size:14px;font-weight:400;overflow:hidden;text-decoration:none;text-overflow:ellipsis;white-space:nowrap}.sidebar ul li a:hover{text-decoration:underline}.sidebar ul li ul{padding:0}.sidebar ul li.active>a{border-right:2px solid;color:#42b983;color:var(--theme-color,#42b983);font-weight:600}.app-sub-sidebar li:before{content:"-";padding-right:4px;float:left}.markdown-section h1,.markdown-section h2,.markdown-section h3,.markdown-section h4,.markdown-section strong{color:#2c3e50;font-weight:600}.markdown-section a{color:#42b983;color:var(--theme-color,#42b983);font-weight:600}.markdown-section h1{font-size:2rem;margin:0 0 1rem}.markdown-section h2{font-size:1.75rem;margin:45px 0 .8rem}.markdown-section h3{font-size:1.5rem;margin:40px 0 .6rem}.markdown-section h4{font-size:1.25rem}.markdown-section h5{font-size:1rem}.markdown-section h6{color:#777;font-size:1rem}.markdown-section figure,.markdown-section p{margin:1.2em 0}.markdown-section ol,.markdown-section p,.markdown-section ul{line-height:1.6rem;word-spacing:.05rem}.markdown-section ol,.markdown-section ul{padding-left:1.5rem}.markdown-section blockquote{border-left:4px solid #42b983;border-left:4px solid var(--theme-color,#42b983);color:#858585;margin:2em 0;padding-left:20px}.markdown-section blockquote p{font-weight:600;margin-left:0}.markdown-section iframe{margin:1em 0}.markdown-section em{color:#7f8c8d}.markdown-section code,.markdown-section output:after,.markdown-section pre{font-family:Roboto Mono,Monaco,courier,monospace}.markdown-section code,.markdown-section pre{background-color:#f8f8f8}.markdown-section output,.markdown-section pre{margin:1.2em 0;position:relative}.markdown-section output,.markdown-section pre>code{border-radius:2px;display:block}.markdown-section output:after,.markdown-section pre>code{-moz-osx-font-smoothing:initial;-webkit-font-smoothing:initial}.markdown-section code{border-radius:2px;color:#e96900;margin:0 2px;padding:3px 5px;white-space:pre-wrap}.markdown-section>:not(h1):not(h2):not(h3):not(h4):not(h5):not(h6) code{font-size:.8rem}.markdown-section pre{padding:0 1.4rem;line-height:1.5rem;overflow:auto;word-wrap:normal}.markdown-section pre>code{color:#525252;font-size:.8rem;padding:2.2em 5px;line-height:inherit;margin:0 2px;max-width:inherit;overflow:inherit;white-space:inherit}.markdown-section output{padding:1.7rem 1.4rem;border:1px dotted #ccc}.markdown-section output>:first-child{margin-top:0}.markdown-section output>:last-child{margin-bottom:0}.markdown-section code:after,.markdown-section code:before,.markdown-section output:after,.markdown-section output:before{letter-spacing:.05rem}.markdown-section output:after,.markdown-section pre:after{color:#ccc;font-size:.6rem;font-weight:600;height:15px;line-height:15px;padding:5px 10px 0;position:absolute;right:0;text-align:right;top:0;content:attr(data-lang)}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#8e908c}.token.namespace{opacity:.7}.token.boolean,.token.number{color:#c76b29}.token.punctuation{color:#525252}.token.property{color:#c08b30}.token.tag{color:#2973b7}.token.string{color:#42b983;color:var(--theme-color,#42b983)}.token.selector{color:#6679cc}.token.attr-name{color:#2973b7}.language-css .token.string,.style .token.string,.token.entity,.token.url{color:#22a2c9}.token.attr-value,.token.control,.token.directive,.token.unit{color:#42b983;color:var(--theme-color,#42b983)}.token.function,.token.keyword{color:#e96900}.token.atrule,.token.regex,.token.statement{color:#22a2c9}.token.placeholder,.token.variable{color:#3d8fd1}.token.deleted{text-decoration:line-through}.token.inserted{border-bottom:1px dotted #202746;text-decoration:none}.token.italic{font-style:italic}.token.bold,.token.important{font-weight:700}.token.important{color:#c94922}.token.entity{cursor:help}code .token{-moz-osx-font-smoothing:initial;-webkit-font-smoothing:initial;min-height:1.5rem;position:relative;left:auto}

path: docsify-server/assets/zoom-image.min.js
content: !function(){function t(e){return"IMG"===e.tagName}function d(e){function t(){for(var e=arguments,t=arguments.length,o=Array(t),n=0;n<t;n++)o[n]=e[n];var i=o.reduce(function(e,t){return[].concat(e,z(t))},[]);return i.filter(function(e){return-1===m.indexOf(e)}).forEach(function(e){m.push(e),e.classList.add("medium-zoom-image")}),r.forEach(function(e){var t=e.type,o=e.listener,n=e.options;i.forEach(function(e){e.addEventListener(t,o,n)})}),f}function o(){var e=(0<arguments.length&&void 0!==arguments[0]?arguments[0]:{}).target;return s.original?a():i({target:e})}var n=window.Promise||function(e){function t(){}e(t,t)},i=function(){function r(){var e={width:document.documentElement.clientWidth,height:document.documentElement.clientHeight,left:0,top:0,right:0,bottom:0},t=void 0,o=void 0;u.container&&(u.container instanceof Object?(t=(e=g({},e,u.container)).width-e.left-e.right-2*u.margin,o=e.height-e.top-e.bottom-2*u.margin):(r=(i=(h(u.container)?u.container:document.querySelector(u.container)).getBoundingClientRect()).width,m=i.height,d=i.left,a=i.top,e=g({},e,{width:r,height:m,left:d,top:a})));var t=t||e.width-2*u.margin,o=o||e.height-2*u.margin,n=s.zoomedHd||s.original,i=!v(n)&&n.naturalWidth||t,r=!v(n)&&n.naturalHeight||o,d=(m=n.getBoundingClientRect()).top,a=m.left,n=m.width,m=m.height,i=Math.min(Math.max(n,i),t)/n,r=Math.min(Math.max(m,r),o)/m,r="scale("+(r=Math.min(i,r))+") translate3d("+((t-n)/2-a+u.margin+e.left)/r+"px, "+((o-m)/2-d+u.margin+e.top)/r+"px, 0)";s.zoomed.style.transform=r,s.zoomedHd&&(s.zoomedHd.style.transform=r)}var d=(0<arguments.length&&void 0!==arguments[0]?arguments[0]:{}).target;return new n(function(e){if(d&&-1===m.indexOf(d))e(f);else{function t(){c=!1,s.zoomed.removeEventListener("transitionend",t),s.original.dispatchEvent(E("medium-zoom:opened",{detail:{zoom:f}})),e(f)}var o,n;if(s.zoomed)e(f);else{if(d)s.original=d;else{if(!(0<m.length))return void e(f);var i=m;s.original=i[0]}s.original.dispatchEvent(E("medium-zoom:open",{detail:{zoom:f}})),l=window.pageYOffset||document.documentElement.scrollTop||document.body.scrollTop||0,c=!0,s.zoomed=b(s.original),document.body.appendChild(p),u.template&&(i=h(u.template)?u.template:document.querySelector(u.template),s.template=document.createElement("div"),s.template.appendChild(i.content.cloneNode(!0)),document.body.appendChild(s.template)),s.original.parentElement&&"PICTURE"===s.original.parentElement.tagName&&s.original.currentSrc&&(s.zoomed.src=s.original.currentSrc),document.body.appendChild(s.zoomed),window.requestAnimationFrame(function(){document.body.classList.add("medium-zoom--opened")}),s.original.classList.add("medium-zoom-image--hidden"),s.zoomed.classList.add("medium-zoom-image--opened"),s.zoomed.addEventListener("click",a),s.zoomed.addEventListener("transitionend",t),s.original.getAttribute("data-zoom-src")?(s.zoomedHd=s.zoomed.cloneNode(),s.zoomedHd.removeAttribute("srcset"),s.zoomedHd.removeAttribute("sizes"),s.zoomedHd.removeAttribute("loading"),s.zoomedHd.src=s.zoomed.getAttribute("data-zoom-src"),s.zoomedHd.onerror=function(){clearInterval(o),console.warn("Unable to reach the zoom image target "+s.zoomedHd.src),s.zoomedHd=null,r()},o=setInterval(function(){s.zoomedHd.complete&&(clearInterval(o),s.zoomedHd.classList.add("medium-zoom-image--opened"),s.zoomedHd.addEventListener("click",a),document.body.appendChild(s.zoomedHd),r())},10)):s.original.hasAttribute("srcset")?(s.zoomedHd=s.zoomed.cloneNode(),s.zoomedHd.removeAttribute("sizes"),s.zoomedHd.removeAttribute("loading"),n=s.zoomedHd.addEventListener("load",function(){s.zoomedHd.removeEventListener("load",n),s.zoomedHd.classList.add("medium-zoom-image--opened"),s.zoomedHd.addEventListener("click",a),document.body.appendChild(s.zoomedHd),r()})):r()}}})},a=function(){return new n(function(t){var e;!c&&s.original?(e=function e(){s.original.classList.remove("medium-zoom-image--hidden"),document.body.removeChild(s.zoomed),s.zoomedHd&&document.body.removeChild(s.zoomedHd),document.body.removeChild(p),s.zoomed.classList.remove("medium-zoom-image--opened"),s.template&&document.body.removeChild(s.template),c=!1,s.zoomed.removeEventListener("transitionend",e),s.original.dispatchEvent(E("medium-zoom:closed",{detail:{zoom:f}})),s.original=null,s.zoomed=null,s.zoomedHd=null,s.template=null,t(f)},c=!0,document.body.classList.remove("medium-zoom--opened"),s.zoomed.style.transform="",s.zoomedHd&&(s.zoomedHd.style.transform=""),s.template&&(s.template.style.transition="opacity 150ms",s.template.style.opacity=0),s.original.dispatchEvent(E("medium-zoom:close",{detail:{zoom:f}})),s.zoomed.addEventListener("transitionend",e)):t(f)})},m=[],r=[],c=!1,l=0,u=1<arguments.length&&void 0!==arguments[1]?arguments[1]:{},s={original:null,zoomed:null,zoomedHd:null,template:null};"[object Object]"===Object.prototype.toString.call(e)?u=e:!e&&"string"!=typeof e||t(e);var u=g({margin:0,background:"#fff",scrollOffset:40,container:null,template:null},u),p=y(u.background);document.addEventListener("click",function(e){e=e.target;e!==p?-1!==m.indexOf(e)&&o({target:e}):a()}),document.addEventListener("keyup",function(e){e=e.key||e.keyCode;"Escape"!==e&&"Esc"!==e&&27!==e||a()}),document.addEventListener("scroll",function(){var e;!c&&s.original&&(e=window.pageYOffset||document.documentElement.scrollTop||document.body.scrollTop||0,Math.abs(l-e)>u.scrollOffset&&setTimeout(a,150))}),window.addEventListener("resize",a);var f={open:i,close:a,toggle:o,update:function(){var e=0<arguments.length&&void 0!==arguments[0]?arguments[0]:{},t=e;return e.background&&(p.style.background=e.background),e.container&&e.container instanceof Object&&(t.container=g({},u.container,e.container)),e.template&&(e=h(e.template)?e.template:document.querySelector(e.template),t.template=e),u=g({},u,t),m.forEach(function(e){e.dispatchEvent(E("medium-zoom:update",{detail:{zoom:f}}))}),f},clone:function(){return d(g({},u,0<arguments.length&&void 0!==arguments[0]?arguments[0]:{}))},attach:t,detach:function(){for(var e=arguments,t=arguments.length,o=Array(t),n=0;n<t;n++)o[n]=e[n];s.zoomed&&a();var i=0<o.length?o.reduce(function(e,t){return[].concat(e,z(t))},[]):m;return i.forEach(function(e){e.classList.remove("medium-zoom-image"),e.dispatchEvent(E("medium-zoom:detach",{detail:{zoom:f}}))}),m=m.filter(function(e){return-1===i.indexOf(e)}),f},on:function(t,o){var n=2<arguments.length&&void 0!==arguments[2]?arguments[2]:{};return m.forEach(function(e){e.addEventListener("medium-zoom:"+t,o,n)}),r.push({type:"medium-zoom:"+t,listener:o,options:n}),f},off:function(t,o){var n=2<arguments.length&&void 0!==arguments[2]?arguments[2]:{};return m.forEach(function(e){e.removeEventListener("medium-zoom:"+t,o,n)}),r=r.filter(function(e){return!(e.type==="medium-zoom:"+t&&e.listener.toString()===o.toString())}),f},getOptions:function(){return u},getImages:function(){return m},getZoomedImage:function(){return s.original}};return f}var g=Object.assign||function(e){for(var t=arguments,o=1;o<arguments.length;o++){var n,i=t[o];for(n in i)Object.prototype.hasOwnProperty.call(i,n)&&(e[n]=i[n])}return e},h=function(e){return e&&1===e.nodeType},v=function(e){return".svg"===(e.currentSrc||e.src).substr(-4).toLowerCase()},z=function(e){try{return Array.isArray(e)?e.filter(t):NodeList.prototype.isPrototypeOf(e)?[].slice.call(e).filter(t):h(e)?[e].filter(t):"string"==typeof e?[].slice.call(document.querySelectorAll(e)).filter(t):[]}catch(e){throw new TypeError("The provided selector is invalid.\nExpects a CSS selector, a Node element, a NodeList or an array.\nSee: https://github.com/francoischalifour/medium-zoom")}},y=function(e){var t=document.createElement("div");return t.classList.add("medium-zoom-overlay"),t.style.background=e,t},b=function(e){var t=e.getBoundingClientRect(),o=t.top,n=t.left,i=t.width,r=t.height,d=e.cloneNode(),t=window.pageYOffset||document.documentElement.scrollTop||document.body.scrollTop||0,e=window.pageXOffset||document.documentElement.scrollLeft||document.body.scrollLeft||0;return d.removeAttribute("id"),d.style.position="absolute",d.style.top=o+t+"px",d.style.left=n+e+"px",d.style.width=i+"px",d.style.height=r+"px",d.style.transform="",d},E=function(e,t){var o=g({bubbles:!1,cancelable:!1,detail:void 0},t);if("function"==typeof window.CustomEvent)return new CustomEvent(e,o);t=document.createEvent("CustomEvent");return t.initCustomEvent(e,o.bubbles,o.cancelable,o.detail),t};var e,o,n,i;e=".medium-zoom-overlay{position:fixed;top:0;right:0;bottom:0;left:0;opacity:0;transition:opacity .3s;will-change:opacity}.medium-zoom--opened .medium-zoom-overlay{cursor:pointer;cursor:zoom-out;opacity:1}.medium-zoom-image{cursor:pointer;cursor:zoom-in;transition:transform .3s cubic-bezier(.2,0,.2,1)!important}.medium-zoom-image--hidden{visibility:hidden}.medium-zoom-image--opened{position:relative;cursor:pointer;cursor:zoom-out;will-change:transform}",i=(o=void 0===o?{}:o).insertAt,e&&"undefined"!=typeof document&&(n=document.head||document.getElementsByTagName("head")[0],(o=document.createElement("style")).type="text/css","top"===i&&n.firstChild?n.insertBefore(o,n.firstChild):n.appendChild(o),o.styleSheet?o.styleSheet.cssText=e:o.appendChild(document.createTextNode(e)));var r=Element.prototype.matches||Element.prototype.webkitMatchesSelector||Element.prototype.msMatchesSelector;$docsify.plugins=[].concat(function(e){var o;e.doneEach(function(e){var t=(t=Array.apply(null,document.querySelectorAll(".markdown-section img:not(.emoji):not([data-no-zoom])"))).filter(function(e){return!1===r.call(e,"a img")});o&&o.detach(),o=d(t)})},$docsify.plugins)}();


path: docsify-server/index.html
content: <!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
    <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0" />
    <title>MD Web</title>
    <meta name="description" content="share and display markdown files online" />
    <link rel="stylesheet" href="assets/vue.css" />
    <link rel="stylesheet" href="assets/toc.css" />
    <style>
      .anchor {
        font-weight: bold;
        text-shadow:
          0 1px 3px rgba(0, 0, 0, 0.4),
          0 0 30px rgba(0, 0, 0, 0.075);
      }

      .app-name {
        width: 150px;
      }

      section.cover h1 {
        margin: 1.5rem 0 1.5rem;
      }

      section.cover p {
        text-shadow:
          0 1px 3px rgba(0, 0, 0, 0.4),
          0 0 30px rgba(0, 0, 0, 0.075);
      }

      .app-sub-sidebar li:before {
        content: "○";
      }

      .page_toc {
        top: 12rem;
      }

      .github-corner {
        transform: scale(0.7);
        transform-origin: top right;
        opacity: 0.35;
        transition: opacity 0.3s;
      }

      .github-corner:hover {
        opacity: 1;
      }

      @media screen and (max-width: 1300px) {
        .nav {
          display: none;
        }
      }
    </style>
  </head>

  <body class="body--ready">
    <div id="app">Loading...</div>
    <script>
      window.$docsify = {
        name: "<p>MD Web</p>",
        nameLink: "/index.html#/",
        repo: "https://github.com/rockbenben/md-web",
        notFoundPage: "README.md",
        auto2top: true,

        loadNavbar: false,
        mergeNavbar: true,
        topMargin: 40,

        loadSidebar: false,

        maxLevel: 4,
        subMaxLevel: 2,

        search: {
          maxAge: 86400000,
          paths: "auto",
          placeholder: "Search",
          noData: "No results found",
          depth: 2,
          hideOtherSidebarContent: false,
        },

        toc: {
          scope: ".markdown-section",
          headings: "h2, h3, h4, h5, h6",
          title: "",
        },
      };
    </script>

    <script src="assets/docsify.min.js"></script>
    <script src="assets/search.min.js"></script>
    <script src="assets/zoom-image.min.js"></script>
    <script src="assets/toc.js"></script>
    <script src="assets/countable.min.js"></script>
  </body>
</html>


path: upload.js
content: #!/usr/bin/env node
/** Upload files to S3-compatible storage for md-web skill. Zero dependencies (Node.js built-in only). */

var crypto = require("node:crypto");
var https = require("node:https");
var fs = require("node:fs");
var path = require("node:path");
var os = require("node:os");

var SCRIPT_DIR = __dirname;

// ── User data dir: upgrade-safe location in home dir ──
var USER_CONFIG_DIR = path.join(os.homedir(), ".md-web");
var USER_CONFIG_PATH = path.join(USER_CONFIG_DIR, "config.json");
var LOCAL_CONFIG_PATH = path.join(SCRIPT_DIR, "config.json");
var LOCAL_DEPLOYED_FLAG = path.join(SCRIPT_DIR, ".deployed");

var CONFIG_PATH;
if (fs.existsSync(USER_CONFIG_PATH)) {
  CONFIG_PATH = USER_CONFIG_PATH;
} else if (fs.existsSync(LOCAL_CONFIG_PATH)) {
  // Migrate: copy old files to new location so future upgrades are safe
  try {
    fs.mkdirSync(USER_CONFIG_DIR, { recursive: true });
    fs.copyFileSync(LOCAL_CONFIG_PATH, USER_CONFIG_PATH);
    if (fs.existsSync(LOCAL_DEPLOYED_FLAG)) {
      fs.copyFileSync(LOCAL_DEPLOYED_FLAG, path.join(USER_CONFIG_DIR, ".deployed"));
    }
    CONFIG_PATH = USER_CONFIG_PATH;
    console.log("Config migrated to: " + USER_CONFIG_PATH);
  } catch (_) {
    CONFIG_PATH = LOCAL_CONFIG_PATH;
  }
} else {
  CONFIG_PATH = USER_CONFIG_PATH;
}

// .deployed follows the same directory as config — if migration failed and we
// fell back to skill dir, .deployed stays in skill dir too.
var DEPLOYED_FLAG = path.join(path.dirname(CONFIG_PATH), ".deployed");

// ── Load Configuration ──
var CONFIG;
try {
  CONFIG = JSON.parse(fs.readFileSync(CONFIG_PATH, "utf-8"));
} catch (e) {
  console.error("CONFIG_MISSING: " + CONFIG_PATH);
  console.error("Run this skill once to configure, or create: " + USER_CONFIG_PATH);
  process.exit(1);
}

var REQUIRED = ["access_key", "secret_key", "endpoint", "bucket", "public_url"];
var missing = REQUIRED.filter(function (k) { return !CONFIG[k]; });
if (missing.length) {
  console.error("Missing config fields: " + missing.join(", "));
  console.error("Edit: " + CONFIG_PATH);
  process.exit(1);
}
CONFIG.public_url = CONFIG.public_url.replace(/\/+$/, "");

var CONTENT_TYPES = {
  ".html": "text/html; charset=utf-8",
  ".md": "text/markdown; charset=utf-8",
  ".css": "text/css; charset=utf-8",
  ".js": "application/javascript; charset=utf-8",
  ".json": "application/json; charset=utf-8",
  ".txt": "text/plain; charset=utf-8",
};

// ── HTTPS Agent (connection reuse) ──
var agent = new https.Agent({ keepAlive: true, maxSockets: 6 });

// ── S3 Signature V4 ──
function hmacSha256(key, msg) {
  return crypto.createHmac("sha256", key).update(msg, "utf-8").digest();
}

function sha256Hex(data) {
  return crypto.createHash("sha256").update(data).digest("hex");
}

var _skCache = {};
function signingKey(dateStamp) {
  if (_skCache.ds === dateStamp) return _skCache.key;
  var region = CONFIG.region || "auto";
  var k = hmacSha256("AWS4" + CONFIG.secret_key, dateStamp);
  k = hmacSha256(k, region);
  k = hmacSha256(k, "s3");
  k = hmacSha256(k, "aws4_request");
  _skCache = { ds: dateStamp, key: k };
  return k;
}

// S3 SigV4 URI encoding: encode all except A-Z a-z 0-9 - . _ ~
function uriEncode(str) {
  return encodeURIComponent(str).replace(/[!'()*]/g, function (c) {
    return "%" + c.charCodeAt(0).toString(16).toUpperCase();
  });
}

function uriEncodePath(p) {
  return p.split("/").map(uriEncode).join("/");
}

function parseR2Error(xml) {
  var code = (xml.match(/<Code>(.*?)<\/Code>/) || [])[1];
  var msg = (xml.match(/<Message>(.*?)<\/Message>/) || [])[1];
  if (code && msg) return code + ": " + msg;
  if (code) return code;
  return xml.length > 200 ? xml.slice(0, 200) + "..." : xml;
}

// ── General S3 Request ──
// All S3 operations go through this single signing path.
// extraHeaders: signed AND sent. Content-Length + auth added automatically.
function s3Fetch(method, resourcePath, query, payload, extraHeaders) {
  var region = CONFIG.region || "auto";
  if (typeof payload === "string") payload = Buffer.from(payload, "utf-8");
  if (!payload) payload = Buffer.alloc(0);

  var encodedPath = uriEncodePath(resourcePath);

  return new Promise(function (resolve, reject) {
    var now = new Date();
    var amzDate = now.toISOString().replace(/[-:]/g, "").replace(/\.\d+Z/, "Z");
    var dateStamp = amzDate.slice(0, 8);
    var payloadHash = sha256Hex(payload);

    // Canonical headers (lowercase keys, sorted)
    var signHdrs = {
      "host": CONFIG.endpoint,
      "x-amz-content-sha256": payloadHash,
      "x-amz-date": amzDate,
    };
    if (extraHeaders) {
      Object.keys(extraHeaders).forEach(function (k) {
        signHdrs[k.toLowerCase()] = extraHeaders[k];
      });
    }

    var sortedKeys = Object.keys(signHdrs).sort();
    var canonicalHeaders = sortedKeys.map(function (k) { return k + ":" + signHdrs[k]; }).join("\n") + "\n";
    var signedHeaders = sortedKeys.join(";");
    var canonicalQS = query ? query + "=" : "";
    var canonicalRequest = method + "\n" + encodedPath + "\n" + canonicalQS + "\n" + canonicalHeaders + "\n" + signedHeaders + "\n" + payloadHash;

    var scope = dateStamp + "/" + region + "/s3/aws4_request";
    var stringToSign = "AWS4-HMAC-SHA256\n" + amzDate + "\n" + scope + "\n" + sha256Hex(canonicalRequest);
    var sig = hmacSha256(signingKey(dateStamp), stringToSign).toString("hex");
    var auth = "AWS4-HMAC-SHA256 Credential=" + CONFIG.access_key + "/" + scope + ", SignedHeaders=" + signedHeaders + ", Signature=" + sig;

    // HTTP request headers (unsigned Content-Length + auth added here)
    var reqHeaders = {
      "x-amz-content-sha256": payloadHash,
      "x-amz-date": amzDate,
      Authorization: auth,
    };
    if (payload.length > 0) reqHeaders["Content-Length"] = payload.length;
    if (extraHeaders) {
      Object.keys(extraHeaders).forEach(function (k) {
        reqHeaders[k] = extraHeaders[k];
      });
    }

    var req = https.request(
      {
        hostname: CONFIG.endpoint,
        path: encodedPath + (query ? "?" + query : ""),
        method: method,
        timeout: 30000,
        agent: agent,
        headers: reqHeaders,
      },
      function (res) {
        var body = "";
        res.on("data", function (c) { body += c; });
        res.on("end", function () {
          if (res.statusCode >= 200 && res.statusCode < 300) resolve({ status: res.statusCode, body: body });
          else reject(new Error("HTTP " + res.statusCode + " — " + parseR2Error(body)));
        });
      }
    );
    req.on("timeout", function () { req.destroy(new Error("Request timed out")); });
    req.on("error", reject);
    req.end(payload.length > 0 ? payload : undefined);
  });
}

// ── File Upload ──
function getContentType(filePath) {
  return CONTENT_TYPES[path.extname(filePath).toLowerCase()] || "application/octet-stream";
}

var CACHE_IMMUTABLE = "public, max-age=31536000, immutable";
var CACHE_SHORT = "public, max-age=300";

function cacheControlFor(remoteKey) {
  var ext = path.extname(remoteKey).toLowerCase();
  if (ext === ".js" || ext === ".css") return CACHE_IMMUTABLE;
  return CACHE_SHORT;
}

function uploadFile(localPath, remoteKey, contentType) {
  var ct = contentType || getContentType(localPath);
  var payload = fs.readFileSync(localPath);
  return s3Fetch("PUT", "/" + CONFIG.bucket + "/" + remoteKey, null, payload, {
    "Content-Type": ct,
    "Cache-Control": cacheControlFor(remoteKey),
  }).then(function () { console.log("  OK  " + remoteKey); });
}

// ── Server Deployment ──
function serverDirHash() {
  var serverDir = path.join(SCRIPT_DIR, "docsify-server");
  var hash = crypto.createHash("sha256");
  function walk(dir, prefix) {
    fs.readdirSync(dir).sort().forEach(function (name) {
      var full = path.join(dir, name);
      var rel = prefix ? prefix + "/" + name : name;
      if (fs.statSync(full).isDirectory()) { walk(full, rel); }
      else { hash.update(rel); hash.update(fs.readFileSync(full)); }
    });
  }
  walk(serverDir, "");
  return hash.digest("hex").slice(0, 12);
}

function expireDays() {
  var d = CONFIG.expire_days;
  return (d != null && d >= 0) ? d : 30;
}

function deployFingerprint() {
  return CONFIG.endpoint + "/" + CONFIG.bucket + "@" + serverDirHash() + "@" + expireDays();
}

// ── S3 Lifecycle Rule ──
function setLifecycleRule() {
  var days = expireDays();

  if (days === 0) {
    return s3Fetch("DELETE", "/" + CONFIG.bucket, "lifecycle")
      .then(function () { console.log("  Lifecycle: no expiry"); })
      .catch(function () {});
  }

  // Expire objects with prefix "20" (timestamped uploads only, not Docsify server files)
  var xml = '<LifecycleConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">' +
    '<Rule><ID>md-web-auto-expire</ID>' +
    '<Filter><Prefix>20</Prefix></Filter>' +
    '<Status>Enabled</Status>' +
    '<Expiration><Days>' + days + '</Days></Expiration>' +
    '</Rule></LifecycleConfiguration>';

  var payload = Buffer.from(xml, "utf-8");
  var md5 = crypto.createHash("md5").update(payload).digest("base64");

  return s3Fetch("PUT", "/" + CONFIG.bucket, "lifecycle", payload, {
    "Content-Type": "application/xml",
    "Content-MD5": md5,
  })
    .then(function () { console.log("  Lifecycle: " + days + " days"); })
    .catch(function (e) {
      var msg = e.message || "";
      console.error("  Warning: lifecycle — " + msg +
        (msg.indexOf("AccessDenied") !== -1
          ? " (token needs Admin Read & Write, or set in Cloudflare Dashboard)"
          : ""));
    });
}

function serverDeployed() {
  // Local flag matches current config + server hash + expire_days → skip deploy
  try {
    if (fs.readFileSync(DEPLOYED_FLAG, "utf-8") === deployFingerprint()) {
      return Promise.resolve(true);
    }
  } catch (_) {}
  return Promise.resolve(false);
}

function deployServer() {
  console.log("Deploying Docsify server...");
  var serverDir = path.join(SCRIPT_DIR, "docsify-server");
  var files = [];
  var indexFile = null;

  function walk(dir, prefix) {
    fs.readdirSync(dir).forEach(function (name) {
      var full = path.join(dir, name);
      var key = prefix ? prefix + "/" + name : name;
      if (fs.statSync(full).isDirectory()) {
        walk(full, key);
      } else if (key === "index.html") {
        indexFile = { full: full, key: key };
      } else {
        files.push({ full: full, key: key });
      }
    });
  }
  walk(serverDir, "");

  // Parallel upload (work-stealing, 4 concurrent)
  var concurrency = 4;
  var idx = 0;
  function next() {
    if (idx >= files.length) return Promise.resolve();
    var file = files[idx++];
    return uploadFile(file.full, file.key).then(next);
  }

  var workers = [];
  for (var i = 0; i < Math.min(concurrency, files.length); i++) {
    workers.push(next());
  }

  return Promise.all(workers)
    .then(function () {
      // index.html last → atomicity
      if (indexFile) return uploadFile(indexFile.full, indexFile.key);
    })
    .then(function () { return setLifecycleRule(); })
    .then(function () {
      try { fs.writeFileSync(DEPLOYED_FLAG, deployFingerprint()); } catch (_) {}
      console.log("Docsify server deployed.\n");
    });
}

// ── Timestamp ──
function timestamp() {
  var d = new Date();
  var pad = function (n) { return n < 10 ? "0" + n : "" + n; };
  return d.getFullYear() + pad(d.getMonth() + 1) + pad(d.getDate())
    + "-" + pad(d.getHours()) + pad(d.getMinutes()) + pad(d.getSeconds());
}

function addTimestamp(key) {
  var ext = path.extname(key);
  var base = key.slice(0, key.length - ext.length);
  if (ext.toLowerCase() !== ".md") { ext = ".md"; }
  return timestamp() + "-" + base + ext;
}

// ── Main ──
var args = process.argv.slice(2);
if (args.length < 2) {
  console.log("Usage: node " + path.basename(process.argv[1]) + " <local-file> <remote-key>");
  process.exit(1);
}

var localPath = args[0];
var remoteKey = addTimestamp(args[1]);

serverDeployed()
  .then(function (deployed) {
    if (!deployed) return deployServer();
  })
  .then(function () {
    return uploadFile(localPath, remoteKey);
  })
  .then(function () {
    var name = remoteKey.replace(/\.md$/i, "");
    console.log("\n" + CONFIG.public_url + "/index.html#/" + name);
    agent.destroy();
    process.exit(0);
  })
  .catch(function (e) {
    console.error("UPLOAD_FAILED: " + e.message);
    agent.destroy();
    process.exit(1);
  });


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)